This commit is contained in:
19
react/features/base/tooltip/actionTypes.ts
Normal file
19
react/features/base/tooltip/actionTypes.ts
Normal 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';
|
||||
31
react/features/base/tooltip/actions.tsx
Normal file
31
react/features/base/tooltip/actions.tsx
Normal 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
|
||||
};
|
||||
}
|
||||
157
react/features/base/tooltip/components/Tooltip.tsx
Normal file
157
react/features/base/tooltip/components/Tooltip.tsx
Normal 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;
|
||||
50
react/features/base/tooltip/reducer.ts
Normal file
50
react/features/base/tooltip/reducer.ts
Normal 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;
|
||||
});
|
||||
Reference in New Issue
Block a user