init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
/**
* The type of the action which signals a tooltip is being displayed.
*
* {
* type: SHOW_TOOLTIP,
* content: string
* }.
*/
export const SHOW_TOOLTIP = 'SHOW_TOOLTIP';
/**
* The type of the action which signals a tooltip should be hidden.
*
* {
* type: SHOW_TOOLTIP,
* content: string
* }.
*/
export const HIDE_TOOLTIP = 'HIDE_TOOLTIP';

View File

@@ -0,0 +1,31 @@
import { ReactElement } from 'react';
import { HIDE_TOOLTIP, SHOW_TOOLTIP } from './actionTypes';
/**
* Set tooltip state to visible.
*
* @param {string} content - The content of the tooltip.
* Used as unique identifier for tooltip.
* @returns {Object}
*/
export function showTooltip(content: string | ReactElement) {
return {
type: SHOW_TOOLTIP,
content
};
}
/**
* Set tooltip state to hidden.
*
* @param {string} content - The content of the tooltip.
* Used as unique identifier for tooltip.
* @returns {Object}
*/
export function hideTooltip(content: string | ReactElement) {
return {
type: HIDE_TOOLTIP,
content
};
}

View File

@@ -0,0 +1,157 @@
import React, { ReactElement, useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { keyframes } from 'tss-react';
import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../app/types';
import { isMobileBrowser } from '../../environment/utils';
import Popover from '../../popover/components/Popover.web';
import { TOOLTIP_POSITION } from '../../ui/constants.any';
import { hideTooltip, showTooltip } from '../actions';
const TOOLTIP_DELAY = 300;
const ANIMATION_DURATION = 0.2;
interface IProps {
children: ReactElement;
containerClassName?: string;
content: string | ReactElement;
position?: TOOLTIP_POSITION;
}
const useStyles = makeStyles()(theme => {
return {
container: {
backgroundColor: theme.palette.uiBackground,
borderRadius: '3px',
padding: theme.spacing(2),
...theme.typography.labelRegular,
color: theme.palette.text01,
position: 'relative',
'&.mounting-animation': {
animation: `${keyframes`
0% {
opacity: 0;
}
100% {
opacity: 1;
}
`} ${ANIMATION_DURATION}s forwards ease-in`
},
'&.unmounting': {
animation: `${keyframes`
0% {
opacity: 1;
}
100% {
opacity: 0;
}
`} ${ANIMATION_DURATION}s forwards ease-out`
}
}
};
});
const Tooltip = ({ containerClassName, content, children, position = 'top' }: IProps) => {
const dispatch = useDispatch();
const [ visible, setVisible ] = useState(false);
const [ isUnmounting, setIsUnmounting ] = useState(false);
const overflowDrawer = useSelector((state: IReduxState) => state['features/toolbox'].overflowDrawer);
const { classes, cx } = useStyles();
const timeoutID = useRef({
open: 0,
close: 0
});
const {
content: storeContent,
previousContent,
visible: isVisible
} = useSelector((state: IReduxState) => state['features/base/tooltip']);
const contentComponent = (
<div
className = { cx(classes.container, previousContent === '' && 'mounting-animation',
isUnmounting && 'unmounting') }>
{content}
</div>
);
const openPopover = () => {
setVisible(true);
dispatch(showTooltip(content));
};
const closePopover = () => {
setVisible(false);
dispatch(hideTooltip(content));
setIsUnmounting(false);
};
const onPopoverOpen = useCallback(() => {
if (isUnmounting) {
return;
}
clearTimeout(timeoutID.current.close);
timeoutID.current.close = 0;
if (!visible) {
if (isVisible) {
openPopover();
} else {
timeoutID.current.open = window.setTimeout(() => {
openPopover();
}, TOOLTIP_DELAY);
}
}
}, [ visible, isVisible, isUnmounting ]);
const onPopoverClose = useCallback(() => {
clearTimeout(timeoutID.current.open);
if (visible) {
timeoutID.current.close = window.setTimeout(() => {
setIsUnmounting(true);
}, TOOLTIP_DELAY);
}
}, [ visible ]);
useEffect(() => {
if (isUnmounting) {
setTimeout(() => {
if (timeoutID.current.close !== 0) {
closePopover();
}
}, (ANIMATION_DURATION * 1000) + 10);
}
}, [ isUnmounting ]);
useEffect(() => {
if (storeContent !== content) {
closePopover();
clearTimeout(timeoutID.current.close);
timeoutID.current.close = 0;
}
}, [ storeContent ]);
if (isMobileBrowser() || overflowDrawer) {
return children;
}
return (
<Popover
allowClick = { true }
className = { containerClassName }
content = { contentComponent }
focusable = { false }
onPopoverClose = { onPopoverClose }
onPopoverOpen = { onPopoverOpen }
position = { position }
visible = { visible }>
{children}
</Popover>
);
};
export default Tooltip;

View File

@@ -0,0 +1,50 @@
import ReducerRegistry from '../redux/ReducerRegistry';
import { HIDE_TOOLTIP, SHOW_TOOLTIP } from './actionTypes';
export interface ITooltipState {
content: string;
previousContent: string;
visible: boolean;
}
const DEFAULT_STATE = {
content: '',
previousContent: '',
visible: false
};
/**
* Reduces redux actions which mark the tooltip as displayed or hidden.
*
* @param {IDialogState} state - The current redux state.
* @param {Action} action - The redux action to reduce.
* @param {string} action.type - The type of the redux action to reduce..
* @returns {State} The next redux state that is the result of reducing the
* specified action.
*/
ReducerRegistry.register<ITooltipState>('features/base/tooltip', (state = DEFAULT_STATE, action): ITooltipState => {
switch (action.type) {
case SHOW_TOOLTIP:
return {
content: action.content,
previousContent: state.content,
visible: true
};
case HIDE_TOOLTIP: {
// The tooltip can be marked as hidden only if the hide action
// is dispatched by the tooltip that is displayed.
if (action.content === state.content) {
return {
content: '',
previousContent: '',
visible: false
};
}
return state;
}
}
return state;
});