Mono repo -> Master (#22553)
Combines the following repositories into one: https://github.com/mattermost/mattermost-server https://github.com/mattermost/mattermost-webapp https://github.com/mattermost/focalboard https://github.com/mattermost/mattermost-plugin-playbooks
Этот коммит содержится в:
24
webapp/platform/components/README.md
Обычный файл
24
webapp/platform/components/README.md
Обычный файл
@@ -0,0 +1,24 @@
|
||||
# Mattermost Components
|
||||
|
||||
The goal of this package is to be a place where components common to all products can be shared.
|
||||
|
||||
Currently a work in progress. Next steps involve implementing webpack module federation in the webapp and locking down how the development experience will work for the webapp multi product architecture.
|
||||
|
||||
## Usage
|
||||
|
||||
Coming soon with multi product architecture.
|
||||
|
||||
## Compilation
|
||||
|
||||
Building is done using rollup. This must be done so the webapp webpack will pick up the changes. (multi product development experience coming soon)
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
or from the root of the webapp with
|
||||
|
||||
```bash
|
||||
npm run build --workspace=packages/mattermost
|
||||
```
|
||||
|
||||
42
webapp/platform/components/babel.config.js
Обычный файл
42
webapp/platform/components/babel.config.js
Обычный файл
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
const config = {
|
||||
presets: [
|
||||
['@babel/preset-env', {
|
||||
targets: {
|
||||
chrome: 66,
|
||||
firefox: 60,
|
||||
edge: 42,
|
||||
safari: 12,
|
||||
},
|
||||
modules: false,
|
||||
}],
|
||||
['@babel/preset-react', {
|
||||
useBuiltIns: true,
|
||||
}],
|
||||
['@babel/typescript', {
|
||||
allExtensions: true,
|
||||
isTSX: true,
|
||||
}],
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-transform-runtime',
|
||||
[
|
||||
'babel-plugin-styled-components',
|
||||
{
|
||||
ssr: false,
|
||||
fileName: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
'formatjs',
|
||||
{
|
||||
idInterpolationPattern: '[sha512:contenthash:base64:6]',
|
||||
ast: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
61
webapp/platform/components/package.json
Обычный файл
61
webapp/platform/components/package.json
Обычный файл
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"name": "@mattermost/components",
|
||||
"version": "7.4.0",
|
||||
"module": "dist/index.esm.js",
|
||||
"types": "dist/index.esm.d.ts",
|
||||
"styles": "dist/index.esm.css",
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"run": "rollup -c --watch",
|
||||
"clean": "rm -rf node_modules dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.17.6",
|
||||
"@babel/core": "^7.17.7",
|
||||
"@babel/plugin-transform-runtime": "^7.17.0",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@babel/preset-react": "^7.16.7",
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"@rollup/plugin-babel": "^5.3.1",
|
||||
"@rollup/plugin-commonjs": "^21.0.2",
|
||||
"@rollup/plugin-node-resolve": "^13.1.3",
|
||||
"@rollup/plugin-typescript": "^8.3.1",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/react-bootstrap": "^0.32.22",
|
||||
"@types/react-dom": "^17.0.2",
|
||||
"@types/react-redux": "^7.1.21",
|
||||
"@types/shallow-equals": "^1.0.0",
|
||||
"@types/styled-components": "^5.1.19",
|
||||
"babel-loader": "^8.2.3",
|
||||
"babel-plugin-formatjs": "10.3.14",
|
||||
"babel-plugin-styled-components": "^2.0.6",
|
||||
"css-loader": "^6.7.1",
|
||||
"rollup": "^2.75.7",
|
||||
"rollup-plugin-auto-external": "^2.0.0",
|
||||
"rollup-plugin-peer-deps-external": "^2.2.4",
|
||||
"rollup-plugin-scss": "^3.0.0",
|
||||
"rollup-plugin-ts": "^2.0.5",
|
||||
"sass": "^1.49.9",
|
||||
"sass-loader": "^12.6.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"typescript": "^4.3.4",
|
||||
"webpack": "^5.70.0",
|
||||
"webpack-cli": "^4.9.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/runtime-corejs3": "^7.17.8",
|
||||
"@mui/base": "5.0.0-alpha.116",
|
||||
"@mui/material": "5.11.7",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"classnames": "^2.3.1",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^17.0.2",
|
||||
"react-bootstrap": "github:mattermost/react-bootstrap#d821e2b1db1059bd36112d7587fd1b0912b27626",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-intl": "^5.20.0",
|
||||
"shallow-equals": "^1.0.0",
|
||||
"styled-components": "^5.3.5",
|
||||
"tippy.js": "^6.3.7"
|
||||
}
|
||||
}
|
||||
44
webapp/platform/components/rollup.config.js
Обычный файл
44
webapp/platform/components/rollup.config.js
Обычный файл
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import scss from 'rollup-plugin-scss';
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
|
||||
import packagejson from './package.json';
|
||||
|
||||
const externals = [
|
||||
...Object.keys(packagejson.dependencies || {}),
|
||||
...Object.keys(packagejson.peerDependencies || {}),
|
||||
'mattermost-redux',
|
||||
'reselect',
|
||||
];
|
||||
|
||||
export default [
|
||||
{
|
||||
input: 'src/index.tsx',
|
||||
output: [
|
||||
{
|
||||
sourcemap: true,
|
||||
file: packagejson.module,
|
||||
format: 'es',
|
||||
globals: {'styled-components': 'styled'},
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
scss(),
|
||||
resolve({
|
||||
browser: true,
|
||||
extensions: ['.ts', '.tsx'],
|
||||
}),
|
||||
commonjs(),
|
||||
typescript(),
|
||||
],
|
||||
external: (pkg) => externals.some((external) => pkg.startsWith(external)),
|
||||
watch: {
|
||||
clearScreen: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {MutableRefObject, useEffect} from 'react';
|
||||
|
||||
export function useClickOutsideRef(ref: MutableRefObject<HTMLElement | null>, handler: (event: MouseEvent) => void): void {
|
||||
useEffect(() => {
|
||||
function onMouseDown(event: MouseEvent) {
|
||||
const target = event.target as any;
|
||||
if (ref.current && target instanceof Node && !ref.current.contains(target)) {
|
||||
handler(event);
|
||||
}
|
||||
}
|
||||
|
||||
// Bind the event listener
|
||||
document.addEventListener('mousedown', onMouseDown);
|
||||
return () => {
|
||||
// Unbind the event listener on clean up
|
||||
document.removeEventListener('mousedown', onMouseDown);
|
||||
};
|
||||
}, [ref, handler]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useEffect, useRef, useState, useMemo} from 'react';
|
||||
|
||||
export function useElementAvailable(
|
||||
elementIds: string[],
|
||||
intervalMS = 250,
|
||||
): boolean {
|
||||
const checkAvailableInterval = useRef<NodeJS.Timeout | null>(null);
|
||||
const [available, setAvailable] = useState(false);
|
||||
useEffect(() => {
|
||||
if (available) {
|
||||
if (checkAvailableInterval.current) {
|
||||
clearInterval(checkAvailableInterval.current);
|
||||
checkAvailableInterval.current = null;
|
||||
}
|
||||
return;
|
||||
} else if (checkAvailableInterval.current) {
|
||||
return;
|
||||
}
|
||||
checkAvailableInterval.current = setInterval(() => {
|
||||
if (elementIds.every((x) => document.getElementById(x))) {
|
||||
setAvailable(true);
|
||||
if (checkAvailableInterval.current) {
|
||||
clearInterval(checkAvailableInterval.current);
|
||||
checkAvailableInterval.current = null;
|
||||
}
|
||||
}
|
||||
}, intervalMS);
|
||||
}, []);
|
||||
|
||||
return useMemo(() => available, [available]);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useLayoutEffect, useMemo, useState} from 'react';
|
||||
import throttle from 'lodash/throttle';
|
||||
|
||||
import {useElementAvailable} from './useElementAvailable';
|
||||
|
||||
export type Coords = {
|
||||
x?: string;
|
||||
y?: string;
|
||||
}
|
||||
|
||||
export type PunchOutCoordsHeightAndWidth = Coords & {
|
||||
width: string;
|
||||
height: string;
|
||||
}
|
||||
|
||||
type PunchOutOffset = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export const useMeasurePunchouts = (elementIds: string[], additionalDeps: any[], offset?: PunchOutOffset): PunchOutCoordsHeightAndWidth | null => {
|
||||
const elementsAvailable = useElementAvailable(elementIds);
|
||||
const [size, setSize] = useState({x: window.innerWidth, y: window.innerHeight});
|
||||
const updateSize = throttle(() => {
|
||||
setSize({x: window.innerWidth, y: window.innerHeight});
|
||||
}, 100, {trailing: true});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
window.addEventListener('resize', updateSize);
|
||||
return () =>
|
||||
window.removeEventListener('resize', updateSize);
|
||||
}, []);
|
||||
|
||||
const channelPunchout = useMemo(() => {
|
||||
let minX = Number.MAX_SAFE_INTEGER;
|
||||
let minY = Number.MAX_SAFE_INTEGER;
|
||||
let maxX = Number.MIN_SAFE_INTEGER;
|
||||
let maxY = Number.MIN_SAFE_INTEGER;
|
||||
for (let i = 0; i < elementIds.length; i++) {
|
||||
const rectangle = document.getElementById(elementIds[i])?.getBoundingClientRect();
|
||||
if (!rectangle) {
|
||||
return null;
|
||||
}
|
||||
if (rectangle.x < minX) {
|
||||
minX = rectangle.x;
|
||||
}
|
||||
if (rectangle.y < minY) {
|
||||
minY = rectangle.y;
|
||||
}
|
||||
if (rectangle.x + rectangle.width > maxX) {
|
||||
maxX = rectangle.x + rectangle.width;
|
||||
}
|
||||
if (rectangle.y + rectangle.height > maxY) {
|
||||
maxY = rectangle.y + rectangle.height;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
x: `${minX + (offset ? offset.x : 0)}px`,
|
||||
y: `${minY + (offset ? offset.y : 0)}px`,
|
||||
width: `${(maxX - minX) + (offset ? offset.width : 0)}px`,
|
||||
height: `${(maxY - minY) + (offset ? offset.height : 0)}px`,
|
||||
};
|
||||
}, [...elementIds, ...additionalDeps, size, elementsAvailable]);
|
||||
return channelPunchout;
|
||||
};
|
||||
18
webapp/platform/components/src/focus_trap/index.tsx
Обычный файл
18
webapp/platform/components/src/focus_trap/index.tsx
Обычный файл
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import MuiFocusTrap, {FocusTrapProps as MuiFocusTrapProps} from '@mui/base/FocusTrap';
|
||||
|
||||
export interface Props {
|
||||
active: MuiFocusTrapProps['open'];
|
||||
children: MuiFocusTrapProps['children'];
|
||||
}
|
||||
|
||||
export const FocusTrap = ({active, children}: Props) => {
|
||||
return (
|
||||
<MuiFocusTrap open={active}>
|
||||
{children}
|
||||
</MuiFocusTrap>
|
||||
);
|
||||
};
|
||||
226
webapp/platform/components/src/generic_modal/generic_modal.tsx
Обычный файл
226
webapp/platform/components/src/generic_modal/generic_modal.tsx
Обычный файл
@@ -0,0 +1,226 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {FocusTrap} from '../focus_trap';
|
||||
|
||||
export type Props = {
|
||||
className?: string;
|
||||
onExited: () => void;
|
||||
modalHeaderText?: React.ReactNode;
|
||||
show?: boolean;
|
||||
handleCancel?: () => void;
|
||||
handleConfirm?: () => void;
|
||||
handleEnterKeyPress?: () => void;
|
||||
handleKeydown?: (event?: React.KeyboardEvent<HTMLDivElement>) => void;
|
||||
confirmButtonText?: React.ReactNode;
|
||||
confirmButtonClassName?: string;
|
||||
cancelButtonText?: React.ReactNode;
|
||||
cancelButtonClassName?: string;
|
||||
isConfirmDisabled?: boolean;
|
||||
isDeleteModal?: boolean;
|
||||
id: string;
|
||||
autoCloseOnCancelButton?: boolean;
|
||||
autoCloseOnConfirmButton?: boolean;
|
||||
|
||||
/**
|
||||
* If false, bootrap's Modal will not enforce focus on the modal and will
|
||||
* transfer the mechanism to the FocusTrap component instead.
|
||||
*/
|
||||
enforceFocus?: boolean;
|
||||
container?: React.ReactNode | React.ReactNodeArray;
|
||||
ariaLabel?: string;
|
||||
errorText?: string;
|
||||
compassDesign?: boolean;
|
||||
backdrop?: boolean;
|
||||
backdropClassName?: string;
|
||||
headerButton?: React.ReactNode;
|
||||
tabIndex?: number;
|
||||
children: React.ReactNode;
|
||||
keyboardEscape?: boolean;
|
||||
};
|
||||
|
||||
type State = {
|
||||
show: boolean;
|
||||
isFocalTrapActive: boolean;
|
||||
}
|
||||
|
||||
export class GenericModal extends React.PureComponent<Props, State> {
|
||||
static defaultProps: Partial<Props> = {
|
||||
show: true,
|
||||
id: 'genericModal',
|
||||
autoCloseOnCancelButton: true,
|
||||
autoCloseOnConfirmButton: true,
|
||||
enforceFocus: true,
|
||||
keyboardEscape: true,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
show: props.show!,
|
||||
isFocalTrapActive: false,
|
||||
};
|
||||
}
|
||||
|
||||
onHide = () => {
|
||||
this.setState({show: false});
|
||||
}
|
||||
|
||||
handleCancel = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
event.preventDefault();
|
||||
if (this.props.autoCloseOnCancelButton) {
|
||||
this.onHide();
|
||||
}
|
||||
if (this.props.handleCancel) {
|
||||
this.props.handleCancel();
|
||||
}
|
||||
}
|
||||
|
||||
handleConfirm = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
event.preventDefault();
|
||||
if (this.props.autoCloseOnConfirmButton) {
|
||||
this.onHide();
|
||||
}
|
||||
if (this.props.handleConfirm) {
|
||||
this.props.handleConfirm();
|
||||
}
|
||||
}
|
||||
|
||||
private onEnterKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === 'Enter') {
|
||||
if (this.props.autoCloseOnConfirmButton) {
|
||||
this.onHide();
|
||||
}
|
||||
if (this.props.handleEnterKeyPress) {
|
||||
this.props.handleEnterKeyPress();
|
||||
}
|
||||
}
|
||||
this.props.handleKeydown?.(event);
|
||||
}
|
||||
|
||||
private handleShow = () => {
|
||||
if (this.props.enforceFocus === false) {
|
||||
this.setState({isFocalTrapActive: true});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let confirmButton;
|
||||
if (this.props.handleConfirm) {
|
||||
const isConfirmOrDeleteClassName = this.props.isDeleteModal ? 'delete' : 'confirm';
|
||||
let confirmButtonText: React.ReactNode = (
|
||||
<FormattedMessage
|
||||
id='generic_modal.confirm'
|
||||
defaultMessage='Confirm'
|
||||
/>
|
||||
);
|
||||
if (this.props.confirmButtonText) {
|
||||
confirmButtonText = this.props.confirmButtonText;
|
||||
}
|
||||
|
||||
confirmButton = (
|
||||
<button
|
||||
type='submit'
|
||||
className={classNames('GenericModal__button', isConfirmOrDeleteClassName, this.props.confirmButtonClassName, {
|
||||
disabled: this.props.isConfirmDisabled,
|
||||
})}
|
||||
onClick={this.handleConfirm}
|
||||
disabled={this.props.isConfirmDisabled}
|
||||
>
|
||||
{confirmButtonText}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
let cancelButton;
|
||||
if (this.props.handleCancel) {
|
||||
let cancelButtonText: React.ReactNode = (
|
||||
<FormattedMessage
|
||||
id='generic_modal.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
);
|
||||
if (this.props.cancelButtonText) {
|
||||
cancelButtonText = this.props.cancelButtonText;
|
||||
}
|
||||
|
||||
cancelButton = (
|
||||
<button
|
||||
type='button'
|
||||
className={classNames('GenericModal__button cancel', this.props.cancelButtonClassName)}
|
||||
onClick={this.handleCancel}
|
||||
>
|
||||
{cancelButtonText}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const headerText = this.props.modalHeaderText && (
|
||||
<div className='GenericModal__header'>
|
||||
<h1 id='genericModalLabel'>
|
||||
{this.props.modalHeaderText}
|
||||
</h1>
|
||||
{this.props.headerButton}
|
||||
</div>
|
||||
);
|
||||
|
||||
const isFocusTrapActive = this.props.enforceFocus === false ? this.state.isFocalTrapActive : false;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
id={this.props.id}
|
||||
role='dialog'
|
||||
aria-label={this.props.ariaLabel}
|
||||
aria-labelledby={this.props.ariaLabel ? undefined : 'genericModalLabel'}
|
||||
dialogClassName={classNames('a11y__modal GenericModal', {GenericModal__compassDesign: this.props.compassDesign}, this.props.className)}
|
||||
show={this.state.show}
|
||||
onShow={this.handleShow}
|
||||
restoreFocus={true}
|
||||
enforceFocus={this.props.enforceFocus}
|
||||
onHide={this.onHide}
|
||||
onExited={this.props.onExited}
|
||||
backdrop={this.props.backdrop}
|
||||
backdropClassName={this.props.backdropClassName}
|
||||
container={this.props.container}
|
||||
keyboard={this.props.keyboardEscape}
|
||||
>
|
||||
<FocusTrap active={isFocusTrapActive}>
|
||||
<div
|
||||
onKeyDown={this.onEnterKeyDown}
|
||||
tabIndex={this.props.tabIndex || 0}
|
||||
className='GenericModal__wrapper-enter-key-press-catcher'
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
{this.props.compassDesign && headerText}
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{this.props.compassDesign ? (
|
||||
this.props.errorText && (
|
||||
<div className='genericModalError'>
|
||||
<i className='icon icon-alert-outline'/>
|
||||
<span>{this.props.errorText}</span>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
headerText
|
||||
)}
|
||||
<div className='GenericModal__body'>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</Modal.Body>
|
||||
{(cancelButton || confirmButton) && <Modal.Footer>
|
||||
{cancelButton}
|
||||
{confirmButton}
|
||||
</Modal.Footer>}
|
||||
</div>
|
||||
</FocusTrap>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
18
webapp/platform/components/src/index.tsx
Обычный файл
18
webapp/platform/components/src/index.tsx
Обычный файл
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// type
|
||||
export type {Props as GenericModalProps} from './generic_modal/generic_modal';
|
||||
export type {CircleSkeletonLoaderProps, RectangleSkeletonLoaderProps} from './skeleton_loader';
|
||||
export type {Props as FocusTrapProps} from './focus_trap';
|
||||
|
||||
// components
|
||||
export {GenericModal} from './generic_modal/generic_modal';
|
||||
export {CircleSkeletonLoader, RectangleSkeletonLoader} from './skeleton_loader';
|
||||
export * from './tour_tip';
|
||||
export * from './pulsating_dot';
|
||||
export {FocusTrap} from './focus_trap';
|
||||
|
||||
// hooks
|
||||
export * from './common/hooks/useMeasurePunchouts';
|
||||
export {useElementAvailable} from './common/hooks/useElementAvailable';
|
||||
43
webapp/platform/components/src/pulsating_dot/index.tsx
Обычный файл
43
webapp/platform/components/src/pulsating_dot/index.tsx
Обычный файл
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {Coords} from '../common/hooks/useMeasurePunchouts';
|
||||
|
||||
import './pulsating_dot.scss';
|
||||
|
||||
type Props = {
|
||||
targetRef?: React.RefObject<HTMLImageElement>;
|
||||
className?: string;
|
||||
onClick?: (e: React.MouseEvent) => void;
|
||||
coords?: Coords;
|
||||
}
|
||||
|
||||
export class PulsatingDot extends React.PureComponent<Props> {
|
||||
public render() {
|
||||
let customStyles = {};
|
||||
if (this.props?.coords) {
|
||||
customStyles = {
|
||||
transform: `translate(${this.props.coords?.x}px, ${this.props.coords?.y}px)`,
|
||||
};
|
||||
}
|
||||
let effectiveClassName = 'pulsating_dot';
|
||||
if (this.props.onClick) {
|
||||
effectiveClassName += ' pulsating_dot-clickable';
|
||||
}
|
||||
if (this.props.className) {
|
||||
effectiveClassName = effectiveClassName + ' ' + this.props.className;
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={effectiveClassName}
|
||||
onClick={this.props.onClick}
|
||||
ref={this.props.targetRef}
|
||||
style={{...customStyles}}
|
||||
data-testid={'pulsating_dot'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
37
webapp/platform/components/src/pulsating_dot/pulsating_dot.scss
Обычный файл
37
webapp/platform/components/src/pulsating_dot/pulsating_dot.scss
Обычный файл
@@ -0,0 +1,37 @@
|
||||
.pulsating_dot {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: auto;
|
||||
|
||||
&-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&,
|
||||
&::before,
|
||||
&::after {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-color: var(--online-indicator);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: block;
|
||||
content: "";
|
||||
}
|
||||
|
||||
&::after {
|
||||
animation: pulse1 2s ease 0s infinite;
|
||||
}
|
||||
|
||||
&::before {
|
||||
animation: pulse2 2s ease 0s infinite;
|
||||
}
|
||||
}
|
||||
81
webapp/platform/components/src/skeleton_loader/index.tsx
Обычный файл
81
webapp/platform/components/src/skeleton_loader/index.tsx
Обычный файл
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import styled, {keyframes} from 'styled-components';
|
||||
|
||||
const skeletonFade = keyframes`
|
||||
0% {
|
||||
background-color: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
}
|
||||
50% {
|
||||
background-color: rgba(var(--center-channel-color-rgb), 0.16);
|
||||
}
|
||||
100% {
|
||||
background-color: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
}
|
||||
`;
|
||||
|
||||
const BaseLoader = styled.div`
|
||||
animation-duration: 1500ms;
|
||||
animation-iteration-count: infinite;
|
||||
animation-name: ${skeletonFade};
|
||||
animation-timing-function: ease-in-out;
|
||||
background-color: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
`;
|
||||
|
||||
export interface CircleSkeletonLoaderProps {
|
||||
size: string | number;
|
||||
}
|
||||
|
||||
/**
|
||||
* CircleSkeletonLoader is a component that renders a filled circle with a loading animation.
|
||||
* It is used to indicate that the content is loading.
|
||||
* @param props.size - The size of the circle. When in number, it is treated as pixels.
|
||||
* @example
|
||||
* <CircleSkeletonLoader size={20}/>
|
||||
* <CircleSkeletonLoader size="50%"/>
|
||||
*/
|
||||
export const CircleSkeletonLoader = styled(BaseLoader)<CircleSkeletonLoaderProps>`
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
height: ${(props) => getCorrectSizeDimension(props.size)};
|
||||
width: ${(props) => getCorrectSizeDimension(props.size)};
|
||||
`;
|
||||
|
||||
export interface RectangleSkeletonLoaderProps {
|
||||
height: string | number;
|
||||
width?: string | number;
|
||||
borderRadius?: number;
|
||||
margin?: string;
|
||||
flex?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* RectangleSkeletonLoader is a component that renders a filled rectangle with a loading animation.
|
||||
* It is used to indicate that the content is loading.
|
||||
* @param props.height - The height of the rectangle eg. 20, "20em", "20%". When in number, it is treated as pixels.
|
||||
* @param props.width - The width of the rectangle eg. 30, '100%'. When in number, it is treated as pixels.
|
||||
* @param props.borderRadius - The border radius of the rectangle eg. 4
|
||||
* @param props.margin - The margin of the rectangle eg. '0 10px', '10px 0 0 10px'
|
||||
* @param props.flex - The flex short hand of flex grow, shrink, basis of the rectangle, under flex parent css eg. '1 1 auto'
|
||||
* @default
|
||||
* width: 100% , borderRadius: 8px
|
||||
* @example
|
||||
* <RectangleSkeletonLoader height='100px' />
|
||||
* <RectangleSkeletonLoader height={40} width={100} borderRadius={4} margin='0 10px 0 0' flex='1' />
|
||||
*/
|
||||
export const RectangleSkeletonLoader = styled(BaseLoader)<RectangleSkeletonLoaderProps>`
|
||||
height: ${(props) => getCorrectSizeDimension(props.height)};
|
||||
width: ${(props) => getCorrectSizeDimension(props.width, '100%')};
|
||||
border-radius: ${(props) => props?.borderRadius ?? 8}px;
|
||||
margin: ${(props) => props?.margin ?? null};
|
||||
flex: ${(props) => props?.flex ?? null};
|
||||
`;
|
||||
|
||||
function getCorrectSizeDimension(size: number | string | undefined, fallback: string | null = null) {
|
||||
if (size) {
|
||||
return (typeof size === 'string') ? size : `${size}px`;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
5
webapp/platform/components/src/tour_tip/index.ts
Обычный файл
5
webapp/platform/components/src/tour_tip/index.ts
Обычный файл
@@ -0,0 +1,5 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export * from './tour_tip';
|
||||
export * from './tour_tip_backdrop';
|
||||
456
webapp/platform/components/src/tour_tip/tour_tip.scss
Обычный файл
456
webapp/platform/components/src/tour_tip/tour_tip.scss
Обычный файл
@@ -0,0 +1,456 @@
|
||||
.tour-tip {
|
||||
display: flex;
|
||||
|
||||
&__box {
|
||||
&.tippy-box {
|
||||
padding: 18px 24px 24px;
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||
background: var(--center-channel-bg);
|
||||
border-radius: 4px;
|
||||
color: var(--center-channel-color-rgb);
|
||||
filter: drop-shadow(0 12px 32px rgba(0, 0, 0, 0.12));
|
||||
|
||||
.tippy-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tippy-arrow {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-color: rgba(var(--center-channel-color-rgb), 0.16);
|
||||
color: var(--center-channel-bg);
|
||||
}
|
||||
|
||||
.tippy-arrow::before {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-color: rgba(var(--center-channel-color-rgb), 0.16);
|
||||
background: var(--center-channel-bg);
|
||||
color: var(--center-channel-bg);
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
// fix for https://mattermost.atlassian.net/browse/MM-41711. This covers the current placements we use for the channels and other tools tour
|
||||
&[data-placement^=right] > .tippy-arrow {
|
||||
transform: translate3d(0, 14px, 0) !important;
|
||||
}
|
||||
|
||||
&[data-placement^=top] > .tippy-arrow {
|
||||
transform: translate3d(14px, 0, 0) !important;
|
||||
}
|
||||
|
||||
&[data-placement^=bottom] > .tippy-arrow {
|
||||
transform: translate3d(14px, 0, 0) !important;
|
||||
}
|
||||
|
||||
&[data-placement=bottom-end] > .tippy-arrow {
|
||||
transform: translate3d(317px, 0, 0) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__pulsating-dot-ctr {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__pulsating-dot-ctr[data-pulsating-dot-placement=top] {
|
||||
top: 0;
|
||||
left: calc(50% - 6px);
|
||||
transform: translate(0, 6px);
|
||||
}
|
||||
|
||||
&__pulsating-dot-ctr[data-pulsating-dot-placement=top-start] {
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform: translate(6px, 6px);
|
||||
}
|
||||
|
||||
&__pulsating-dot-ctr[data-pulsating-dot-placement=top-end] {
|
||||
top: 0;
|
||||
right: 0;
|
||||
transform: translate(-6px, 6px);
|
||||
}
|
||||
|
||||
&__pulsating-dot-ctr[data-pulsating-dot-placement=bottom] {
|
||||
bottom: 0;
|
||||
left: calc(50% - 6px);
|
||||
transform: translate(0, -6px);
|
||||
}
|
||||
|
||||
&__pulsating-dot-ctr[data-pulsating-dot-placement=bottom-start] {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
transform: translate(6px, -6px);
|
||||
}
|
||||
|
||||
&__pulsating-dot-ctr[data-pulsating-dot-placement=bottom-end] {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
transform: translate(-6px, -6px);
|
||||
}
|
||||
|
||||
&__pulsating-dot-ctr[data-pulsating-dot-placement=left] {
|
||||
top: calc(50% - 6px);
|
||||
left: 0;
|
||||
transform: translate(6px, 0);
|
||||
}
|
||||
|
||||
&__pulsating-dot-ctr[data-pulsating-dot-placement=left-start] {
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform: translate(6px, 6px);
|
||||
}
|
||||
|
||||
&__pulsating-dot-ctr[data-pulsating-dot-placement=left-end] {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
transform: translate(6px, -6px);
|
||||
}
|
||||
|
||||
&__pulsating-dot-ctr[data-pulsating-dot-placement=right] {
|
||||
top: calc(50% - 6px);
|
||||
right: 0;
|
||||
transform: translate(-6px, 0);
|
||||
}
|
||||
|
||||
&__pulsating-dot-ctr[data-pulsating-dot-placement=right-start] {
|
||||
top: 0;
|
||||
right: 0;
|
||||
transform: translate(-6px, 6px);
|
||||
}
|
||||
|
||||
&__pulsating-dot-ctr[data-pulsating-dot-placement=right-end] {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
transform: translate(-6px, -6px);
|
||||
}
|
||||
|
||||
&__overlay {
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
&__title {
|
||||
flex: none;
|
||||
flex-grow: 1;
|
||||
order: 0;
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: 1.4rem;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
&__close {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
width: 3.2rem;
|
||||
height: 3.2rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: unset;
|
||||
margin-right: -8px;
|
||||
margin-left: 1.2rem;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||
font-weight: 600;
|
||||
|
||||
&:hover {
|
||||
background: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 1.8rem;
|
||||
line-height: 1.8rem;
|
||||
|
||||
::before {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 0.6rem;
|
||||
|
||||
p,
|
||||
div {
|
||||
margin: 0 0 0.8rem;
|
||||
font-size: 1.4rem;
|
||||
line-height: 2rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__body:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&__image {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 2.4rem;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 136px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
&__btn-ctr {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
display: flex;
|
||||
height: 3.2rem;
|
||||
align-items: center;
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
&__confirm-btn {
|
||||
background: var(--button-bg);
|
||||
color: var(--button-color);
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
background:
|
||||
linear-gradient(
|
||||
0deg,
|
||||
rgba(var(--center-channel-color-rgb), 0.16),
|
||||
rgba(var(--center-channel-color-rgb), 0.16)
|
||||
),
|
||||
var(--button-bg);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background:
|
||||
linear-gradient(
|
||||
0deg,
|
||||
rgba(var(--center-channel-color-rgb), 0.32),
|
||||
rgba(var(--center-channel-color-rgb), 0.32)
|
||||
),
|
||||
var(--button-bg);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: inset 0 0 0 2px var(--sidebar-text-active-border);
|
||||
}
|
||||
|
||||
.icon-chevron-right::before {
|
||||
margin-right: -7px;
|
||||
}
|
||||
}
|
||||
|
||||
&__cancel-btn {
|
||||
margin-right: 4px;
|
||||
background: rgba(var(--button-bg-rgb), 0.08);
|
||||
border-radius: 4px;
|
||||
color: var(--button-bg);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
background: rgba(var(--button-bg-rgb), 0.04);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(var(--button-bg-rgb), 0.08);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: inset 0 0 0 2px var(--sidebar-text-active-border);
|
||||
}
|
||||
|
||||
.icon-chevron-left::before {
|
||||
margin-left: -7px;
|
||||
}
|
||||
}
|
||||
|
||||
&__dot-ctr {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&__dot-ring {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 4px;
|
||||
background: transparent;
|
||||
border-radius: 50%;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__dot-ring-active {
|
||||
background: rgba(var(--button-bg-rgb), 0.16);
|
||||
}
|
||||
|
||||
&__dot {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: rgba(var(--button-bg-rgb), 0.32);
|
||||
border-radius: 6px;
|
||||
|
||||
&.active {
|
||||
background: rgba(var(--button-bg-rgb), 1);
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 2.4rem;
|
||||
|
||||
&-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
}
|
||||
}
|
||||
|
||||
&__opt {
|
||||
align-self: flex-end;
|
||||
margin-top: 1.2rem;
|
||||
font-size: 12px;
|
||||
|
||||
span {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
&__backdrop {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
|
||||
&--transparent {
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// adding important as temporary fix, will be removing tippy very soon (WIP)
|
||||
.tippy-box[data-placement^=right] > .tippy-arrow::before {
|
||||
top: -1px !important;
|
||||
border-width: 1px 0 0 1px !important;
|
||||
transform: rotate(-45deg) !important;
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^=left] > .tippy-arrow::before {
|
||||
top: -1px !important;
|
||||
border-width: 1px 1px 0 0 !important;
|
||||
transform: rotate(45deg) !important;
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^=bottom] > .tippy-arrow::before {
|
||||
left: 1px !important;
|
||||
border-width: 1px 0 0 1px !important;
|
||||
transform: rotate(45deg) !important;
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^=top] > .tippy-arrow::before {
|
||||
left: 1px !important;
|
||||
border-width: 0 0 1px 1px !important;
|
||||
transform: rotate(-45deg) !important;
|
||||
}
|
||||
|
||||
// this style is defined outside of the block scope because is intended to affect the tippy element
|
||||
.tippy-blue-style {
|
||||
background: var(--button-bg) !important;
|
||||
color: var(--sidebar-text) !important;
|
||||
|
||||
.tippy-arrow {
|
||||
border-color: var(--button-bg) !important;
|
||||
color: var(--button-bg) !important;
|
||||
|
||||
&::before {
|
||||
border-width: 0 !important;
|
||||
border-color: var(--button-bg) !important;
|
||||
border-left-color: initial;
|
||||
background-color: var(--button-bg) !important;
|
||||
transform-origin: unset !important;
|
||||
}
|
||||
}
|
||||
|
||||
.tour-tip__header {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.icon-close {
|
||||
color: var(--sidebar-text) !important;
|
||||
}
|
||||
|
||||
// style buttons while in the blue style
|
||||
.tour-tip {
|
||||
&__btn {
|
||||
background: var(--button-color);
|
||||
color: var(--button-bg);
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
background: var(--button-color);
|
||||
color: var(--button-bg);
|
||||
}
|
||||
}
|
||||
|
||||
&__dot-ring {
|
||||
.tour-tip__dot {
|
||||
background: var(--offline-indicator);
|
||||
}
|
||||
}
|
||||
|
||||
&__dot-ring-active {
|
||||
.active {
|
||||
background: var(--button-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
247
webapp/platform/components/src/tour_tip/tour_tip.tsx
Обычный файл
247
webapp/platform/components/src/tour_tip/tour_tip.tsx
Обычный файл
@@ -0,0 +1,247 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useRef} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import Tippy from '@tippyjs/react';
|
||||
import {Placement} from 'tippy.js';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {PunchOutCoordsHeightAndWidth} from '../common/hooks/useMeasurePunchouts';
|
||||
|
||||
import 'tippy.js/dist/tippy.css';
|
||||
import 'tippy.js/themes/light-border.css';
|
||||
import 'tippy.js/animations/scale-subtle.css';
|
||||
import 'tippy.js/animations/perspective-subtle.css';
|
||||
import {PulsatingDot} from '../pulsating_dot';
|
||||
|
||||
import {TourTipBackdrop} from './tour_tip_backdrop';
|
||||
import './tour_tip.scss';
|
||||
|
||||
export type TourTipEventSource = 'next' | 'prev' | 'dismiss' | 'jump' | 'skipped' | 'open' | 'punchOut'
|
||||
|
||||
type Props = {
|
||||
show: boolean;
|
||||
screen: JSX.Element;
|
||||
title: JSX.Element;
|
||||
step: number;
|
||||
|
||||
tourSteps?: Record<string, number>;
|
||||
nextBtn?: JSX.Element;
|
||||
prevBtn?: JSX.Element;
|
||||
imageURL?: string;
|
||||
singleTip?: boolean;
|
||||
showOptOut?: boolean;
|
||||
placement?: Placement;
|
||||
pulsatingDotPlacement?: Omit<Placement, 'auto'| 'auto-end'>;
|
||||
pulsatingDotTranslate?: {x: number; y: number};
|
||||
offset?: [number, number];
|
||||
width?: string | number;
|
||||
zIndex?: number;
|
||||
className?: string;
|
||||
hideBackdrop?: boolean;
|
||||
tippyBlueStyle?: boolean;
|
||||
|
||||
// if you don't want punchOut just assign null, keep null as hook may return null first than actual value
|
||||
overlayPunchOut: PunchOutCoordsHeightAndWidth | null;
|
||||
|
||||
// if we want to interact with element visible via punchOut
|
||||
interactivePunchOut?: boolean;
|
||||
|
||||
handleOpen?: (e: React.MouseEvent) => void;
|
||||
handleNext?: (e: React.MouseEvent) => void;
|
||||
handlePrevious?: (e: React.MouseEvent) => void;
|
||||
handleJump?: (e: React.MouseEvent, jumpToStep: number) => void;
|
||||
handleSkip?: (e: React.MouseEvent) => void;
|
||||
handleDismiss?: (e: React.MouseEvent) => void;
|
||||
handlePunchOut?: (e: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
export const TourTip = ({
|
||||
title,
|
||||
screen,
|
||||
imageURL,
|
||||
overlayPunchOut,
|
||||
singleTip,
|
||||
step,
|
||||
show,
|
||||
interactivePunchOut,
|
||||
tourSteps,
|
||||
handleOpen,
|
||||
handleDismiss,
|
||||
handleNext,
|
||||
handlePrevious,
|
||||
handleSkip,
|
||||
handleJump,
|
||||
handlePunchOut,
|
||||
pulsatingDotTranslate,
|
||||
pulsatingDotPlacement,
|
||||
nextBtn,
|
||||
prevBtn,
|
||||
className,
|
||||
offset = [-18, 4],
|
||||
placement = 'right-start',
|
||||
showOptOut = true,
|
||||
width = 352,
|
||||
zIndex = 999,
|
||||
hideBackdrop = false,
|
||||
tippyBlueStyle = false,
|
||||
}: Props) => {
|
||||
const FIRST_STEP_INDEX = 0;
|
||||
const triggerRef = useRef(null);
|
||||
const onJump = (event: React.MouseEvent, jumpToStep: number) => {
|
||||
if (handleJump) {
|
||||
handleJump(event, jumpToStep);
|
||||
}
|
||||
};
|
||||
|
||||
// This needs to be changed if root-portal node isn't available to maybe body
|
||||
const rootPortal = document.getElementById('root-portal');
|
||||
|
||||
const dots = [];
|
||||
if (!singleTip && tourSteps) {
|
||||
for (let dot = FIRST_STEP_INDEX; dot < (Object.values(tourSteps).length - 1); dot++) {
|
||||
let className = 'tour-tip__dot';
|
||||
let circularRing = 'tour-tip__dot-ring';
|
||||
|
||||
if (dot === step) {
|
||||
className += ' active';
|
||||
circularRing += ' tour-tip__dot-ring-active';
|
||||
}
|
||||
dots.push(
|
||||
<div className={circularRing}>
|
||||
<a
|
||||
href='#'
|
||||
key={'dotactive' + dot}
|
||||
className={className}
|
||||
data-screen={dot}
|
||||
onClick={(e) => onJump(e, dot)}
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<div
|
||||
className='tour-tip__header'
|
||||
data-testid={'current_tutorial_tip'}
|
||||
>
|
||||
<h4 className='tour-tip__header__title'>
|
||||
{title}
|
||||
</h4>
|
||||
<button
|
||||
className='tour-tip__header__close'
|
||||
onClick={handleDismiss}
|
||||
data-testid={'close_tutorial_tip'}
|
||||
>
|
||||
<i className='icon icon-close'/>
|
||||
</button>
|
||||
</div>
|
||||
<div className='tour-tip__body'>
|
||||
{screen}
|
||||
</div>
|
||||
{imageURL && (
|
||||
<div className='tour-tip__image'>
|
||||
<img
|
||||
src={imageURL}
|
||||
alt={'tutorial tour tip product image'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{(nextBtn || prevBtn || showOptOut) && (<div className='tour-tip__footer'>
|
||||
<div className='tour-tip__footer-buttons'>
|
||||
<div className='tour-tip__dot-ctr'>{dots}</div>
|
||||
<div className={'tour-tip__btn-ctr'}>
|
||||
{step !== 0 && prevBtn && (
|
||||
<button
|
||||
id='tipPreviousButton'
|
||||
className='tour-tip__btn tour-tip__cancel-btn'
|
||||
onClick={handlePrevious}
|
||||
>
|
||||
{prevBtn}
|
||||
</button>
|
||||
)}
|
||||
{nextBtn && (
|
||||
<button
|
||||
id='tipNextButton'
|
||||
className='tour-tip__btn tour-tip__confirm-btn'
|
||||
onClick={handleNext}
|
||||
>
|
||||
{nextBtn}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{showOptOut && (
|
||||
<div className='tour-tip__opt'>
|
||||
<FormattedMessage
|
||||
id='tutorial_tip.seen'
|
||||
defaultMessage='Seen this before? '
|
||||
/>
|
||||
<a
|
||||
href='#'
|
||||
onClick={handleSkip}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='tutorial_tip.out'
|
||||
defaultMessage='Opt out of these tips.'
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
id='tipButton'
|
||||
ref={triggerRef}
|
||||
onClick={handleOpen}
|
||||
className='tour-tip__pulsating-dot-ctr'
|
||||
data-pulsating-dot-placement={pulsatingDotPlacement || 'right'}
|
||||
style={{
|
||||
transform: `translate(${pulsatingDotTranslate?.x}px, ${pulsatingDotTranslate?.y}px)`,
|
||||
}}
|
||||
>
|
||||
<PulsatingDot/>
|
||||
</div>
|
||||
<TourTipBackdrop
|
||||
show={show}
|
||||
onDismiss={handleDismiss}
|
||||
onPunchOut={handlePunchOut}
|
||||
interactivePunchOut={interactivePunchOut}
|
||||
overlayPunchOut={overlayPunchOut}
|
||||
appendTo={rootPortal!}
|
||||
transparent={hideBackdrop}
|
||||
/>
|
||||
{show && (
|
||||
<Tippy
|
||||
showOnCreate={show}
|
||||
content={content}
|
||||
animation='scale-subtle'
|
||||
trigger='click'
|
||||
duration={[250, 150]}
|
||||
maxWidth={width}
|
||||
aria={{content: 'labelledby'}}
|
||||
allowHTML={true}
|
||||
zIndex={zIndex}
|
||||
reference={triggerRef}
|
||||
interactive={true}
|
||||
appendTo={rootPortal!}
|
||||
offset={offset}
|
||||
className={classNames(
|
||||
'tour-tip__box',
|
||||
className,
|
||||
{'tippy-blue-style': tippyBlueStyle},
|
||||
)}
|
||||
placement={placement}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
84
webapp/platform/components/src/tour_tip/tour_tip_backdrop.tsx
Обычный файл
84
webapp/platform/components/src/tour_tip/tour_tip_backdrop.tsx
Обычный файл
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import {PunchOutCoordsHeightAndWidth} from '../common/hooks/useMeasurePunchouts';
|
||||
|
||||
type Props = {
|
||||
overlayPunchOut: PunchOutCoordsHeightAndWidth | null;
|
||||
show: boolean;
|
||||
interactivePunchOut?: boolean;
|
||||
onDismiss?: (e: React.MouseEvent) => void;
|
||||
onPunchOut?: (e: React.MouseEvent) => void;
|
||||
appendTo: HTMLElement;
|
||||
transparent?: boolean;
|
||||
}
|
||||
|
||||
const TourTipRootPortal = ({children, show, element}: {children: React.ReactNode ; show: boolean; element: Element}) =>
|
||||
(show ? ReactDOM.createPortal(
|
||||
children,
|
||||
element,
|
||||
) : null);
|
||||
|
||||
export const TourTipBackdrop = ({
|
||||
show,
|
||||
overlayPunchOut,
|
||||
interactivePunchOut,
|
||||
onDismiss,
|
||||
onPunchOut,
|
||||
appendTo,
|
||||
transparent,
|
||||
}: Props) => {
|
||||
const vertices = [];
|
||||
if (overlayPunchOut) {
|
||||
const {x, y, width, height} = overlayPunchOut;
|
||||
|
||||
// draw to top left of punch out
|
||||
vertices.push('0% 0%');
|
||||
vertices.push('0% 100%');
|
||||
vertices.push('100% 100%');
|
||||
vertices.push('100% 0%');
|
||||
vertices.push(`${x} 0%`);
|
||||
vertices.push(`${x} ${y}`);
|
||||
|
||||
// draw punch out
|
||||
vertices.push(`calc(${x} + ${width}) ${y}`);
|
||||
vertices.push(`calc(${x} + ${width}) calc(${y} + ${height})`);
|
||||
vertices.push(`${x} calc(${y} + ${height})`);
|
||||
vertices.push(`${x} ${y}`);
|
||||
|
||||
// close off punch out
|
||||
vertices.push(`${x} 0%`);
|
||||
vertices.push('0% 0%');
|
||||
}
|
||||
const backdrop = (
|
||||
<div
|
||||
onClick={onDismiss}
|
||||
className={`tour-tip__backdrop ${transparent ? 'tour-tip__backdrop--transparent' : ''}`}
|
||||
style={{
|
||||
clipPath: vertices.length ? `polygon(${vertices.join(', ')})` : undefined,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
const overlay = interactivePunchOut ? backdrop : (
|
||||
<>
|
||||
<div
|
||||
className={'tour-tip__overlay'}
|
||||
onClick={onPunchOut || onDismiss}
|
||||
/>
|
||||
{backdrop}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<TourTipRootPortal
|
||||
show={show}
|
||||
element={appendTo}
|
||||
>
|
||||
{overlay}
|
||||
</TourTipRootPortal>
|
||||
);
|
||||
};
|
||||
|
||||
21
webapp/platform/components/tsconfig.json
Обычный файл
21
webapp/platform/components/tsconfig.json
Обычный файл
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"paths": {
|
||||
"mattermost-redux/*": ["./node_modules/mattermost-redux/src/*"],
|
||||
"@mattermost/types/*": ["./node_modules/@mattermost/types/src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
Ссылка в новой задаче
Block a user