diff --git a/css/_popover.scss b/css/_popover.scss index b94607027b..b5d9a3e8e0 100644 --- a/css/_popover.scss +++ b/css/_popover.scss @@ -1,49 +1,27 @@ -/** - * Mousemove padding styles are used to add invisible elements to the popover - * to allow mouse movement from the popover trigger to the popover itself - * without triggering a mouseleave event. - */ -%vertical-popover-padding { - height: 100%; - position: absolute; - top: 0; - width: 20px; - padding: 20px 0; - top: -20px; -} - -%horizontal-popover-padding { - height: 25px; - position: absolute; - right: 0; - width: 100%; - padding: 0 35px; - left: -35px; -} - -.popover-mousemove-padding-left { - @extend %vertical-popover-padding; - left: -35px; -} - -.popover-mousemove-padding-right { - @extend %vertical-popover-padding; - right: -35px; -} - -.popover-mousemove-padding-bottom { - @extend %horizontal-popover-padding; - bottom: -40px; -} - -.popover-mousemove-padding-top { - @extend %horizontal-popover-padding; - top: -40px; -} - .popover { margin: -16px -24px; z-index: $popoverZ; + + .popover-content { + margin: 16px 24px; + position: relative; + + &.top { + bottom: 8px; + } + + &.bottom { + top: 8px; + } + + &.left { + right: 8px; + } + + &.right { + left: 8px; + } + } } .excalidraw .popover { diff --git a/css/_reactions-menu.scss b/css/_reactions-menu.scss index 46b2293bfe..25e2d247e3 100644 --- a/css/_reactions-menu.scss +++ b/css/_reactions-menu.scss @@ -4,7 +4,7 @@ width: 280px; background: $menuBG; box-shadow: 0px 3px 16px rgba(0, 0, 0, 0.6), 0px 0px 4px 1px rgba(0, 0, 0, 0.25); - border-radius: 3px; + border-radius: 6px; padding: 16px; &.with-gif { @@ -104,10 +104,6 @@ } } -.reactions-menu-container { - padding-bottom: 6px; -} - .reactions-animations-container { position: absolute; width: 20%; diff --git a/css/_settings-button.scss b/css/_settings-button.scss index 1ba1ed68e9..aa68f8b529 100644 --- a/css/_settings-button.scss +++ b/css/_settings-button.scss @@ -34,20 +34,20 @@ background: #F2F3F4; box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25), 0px 0px 0px 1px rgba(0, 0, 0, 0.1); - & > svg { + & svg { fill: #040404; } &.settings-button-small-icon--disabled { background: #36383C; - &> svg { + & svg { fill: #929292; } } } - & > svg { + & svg { fill: #fff; } @@ -55,7 +55,7 @@ background-color: #36383c; cursor: default; - &> svg { + & svg { fill: #929292; } } diff --git a/package-lock.json b/package-lock.json index cba74485b6..42a8f36ba5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,6 @@ "@atlaskit/inline-message": "11.0.8", "@atlaskit/multi-select": "15.0.5", "@atlaskit/theme": "11.0.2", - "@atlaskit/tooltip": "17.1.2", "@emotion/react": "11.10.0", "@emotion/styled": "11.10.0", "@giphy/js-fetch-api": "4.7.1", @@ -599,18 +598,6 @@ "styled-components": "^3.2.6" } }, - "node_modules/@atlaskit/motion": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/@atlaskit/motion/-/motion-0.4.8.tgz", - "integrity": "sha512-+5hABjUNUgl66zNya+EmNXLrbJQ7EgXA+fNQih7qzu7MkIuzJjT5vEtpXrzckLKBu1v1HsbMQW90Wnd8MJugdw==", - "dependencies": { - "@babel/runtime": "^7.0.0", - "@emotion/core": "^10.0.9" - }, - "peerDependencies": { - "react": "^16.8.0" - } - }, "node_modules/@atlaskit/multi-select": { "version": "15.0.5", "resolved": "https://registry.npmjs.org/@atlaskit/multi-select/-/multi-select-15.0.5.tgz", @@ -789,25 +776,6 @@ "@babel/types": "^7.15.0" } }, - "node_modules/@atlaskit/tooltip": { - "version": "17.1.2", - "resolved": "https://registry.npmjs.org/@atlaskit/tooltip/-/tooltip-17.1.2.tgz", - "integrity": "sha512-0N63l18ZuyL2pjNXwY3pwnNos3HjOsFhSmnj2TLASC+RLz/6/Nk1k9gXuydfMjU2ntsr7xrVY5U7ING0mxAzQQ==", - "dependencies": { - "@atlaskit/analytics-next": "^8.1.0", - "@atlaskit/motion": "^0.4.0", - "@atlaskit/popper": "^5.0.0", - "@atlaskit/portal": "^4.0.0", - "@atlaskit/theme": "^11.0.0", - "@babel/runtime": "^7.0.0", - "@emotion/core": "^10.0.9", - "bind-event-listener": "^1.0.2" - }, - "peerDependencies": { - "react": "^16.8.0", - "react-dom": "^16.8.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", @@ -20705,15 +20673,6 @@ "react-scrolllock": "^5.0.1" } }, - "@atlaskit/motion": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/@atlaskit/motion/-/motion-0.4.8.tgz", - "integrity": "sha512-+5hABjUNUgl66zNya+EmNXLrbJQ7EgXA+fNQih7qzu7MkIuzJjT5vEtpXrzckLKBu1v1HsbMQW90Wnd8MJugdw==", - "requires": { - "@babel/runtime": "^7.0.0", - "@emotion/core": "^10.0.9" - } - }, "@atlaskit/multi-select": { "version": "15.0.5", "resolved": "https://registry.npmjs.org/@atlaskit/multi-select/-/multi-select-15.0.5.tgz", @@ -20857,21 +20816,6 @@ "@babel/types": "^7.15.0" } }, - "@atlaskit/tooltip": { - "version": "17.1.2", - "resolved": "https://registry.npmjs.org/@atlaskit/tooltip/-/tooltip-17.1.2.tgz", - "integrity": "sha512-0N63l18ZuyL2pjNXwY3pwnNos3HjOsFhSmnj2TLASC+RLz/6/Nk1k9gXuydfMjU2ntsr7xrVY5U7ING0mxAzQQ==", - "requires": { - "@atlaskit/analytics-next": "^8.1.0", - "@atlaskit/motion": "^0.4.0", - "@atlaskit/popper": "^5.0.0", - "@atlaskit/portal": "^4.0.0", - "@atlaskit/theme": "^11.0.0", - "@babel/runtime": "^7.0.0", - "@emotion/core": "^10.0.9", - "bind-event-listener": "^1.0.2" - } - }, "@babel/code-frame": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", diff --git a/package.json b/package.json index e0c23251e7..6ba912ff88 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ "@atlaskit/inline-message": "11.0.8", "@atlaskit/multi-select": "15.0.5", "@atlaskit/theme": "11.0.2", - "@atlaskit/tooltip": "17.1.2", "@emotion/react": "11.10.0", "@emotion/styled": "11.10.0", "@giphy/js-fetch-api": "4.7.1", diff --git a/react/features/app/reducers.web.ts b/react/features/app/reducers.web.ts index 52bc6886ba..4747c3775d 100644 --- a/react/features/app/reducers.web.ts +++ b/react/features/app/reducers.web.ts @@ -1,6 +1,5 @@ -// @flow - import '../base/devices/reducer'; +import '../base/tooltip/reducer'; import '../e2ee/reducer'; import '../face-landmarks/reducer'; import '../feedback/reducer'; diff --git a/react/features/app/types.ts b/react/features/app/types.ts index 0782656136..c3b7811a80 100644 --- a/react/features/app/types.ts +++ b/react/features/app/types.ts @@ -24,6 +24,7 @@ import { IResponsiveUIState } from '../base/responsive-ui/reducer'; import { ISettingsState } from '../base/settings/reducer'; import { ISoundsState } from '../base/sounds/reducer'; import { ITestingState } from '../base/testing/reducer'; +import { ITooltipState } from '../base/tooltip/reducer'; import { INoSrcDataState, ITracksState } from '../base/tracks/reducer'; import { IUserInteractionState } from '../base/user-interaction/reducer'; import { IBreakoutRoomsState } from '../breakout-rooms/reducer'; @@ -111,6 +112,7 @@ export interface IReduxState { 'features/base/responsive-ui': IResponsiveUIState; 'features/base/settings': ISettingsState; 'features/base/sounds': ISoundsState; + 'features/base/tooltip': ITooltipState; 'features/base/tracks': ITracksState; 'features/base/user-interaction': IUserInteractionState; 'features/breakout-rooms': IBreakoutRoomsState; diff --git a/react/features/base/popover/components/Popover.web.tsx b/react/features/base/popover/components/Popover.web.tsx index 358eb06956..a9ce4b02d3 100644 --- a/react/features/base/popover/components/Popover.web.tsx +++ b/react/features/base/popover/components/Popover.web.tsx @@ -5,7 +5,6 @@ import { IReduxState } from '../../../app/types'; import DialogPortal from '../../../toolbox/components/web/DialogPortal'; import Drawer from '../../../toolbox/components/web/Drawer'; import JitsiPortal from '../../../toolbox/components/web/JitsiPortal'; -import { isMobileBrowser } from '../../environment/utils'; import { connect } from '../../redux/functions'; import { getContextMenuStyle } from '../functions.web'; @@ -14,6 +13,11 @@ import { getContextMenuStyle } from '../functions.web'; */ interface IProps { + /** + * Whether the child element can be clicked on. + */ + allowClick?: boolean; + /** * A child React Element to use as the trigger for showing the dialog. */ @@ -245,7 +249,8 @@ class Popover extends Component { + style = { this.state.contextMenuStyle } + targetSelector = '.popover-content'> { * @returns {void} */ _onClick(event: React.MouseEvent) { - const { trigger, visible } = this.props; + const { allowClick, trigger, visible } = this.props; - event.stopPropagation(); + if (!allowClick) { + event.stopPropagation(); + } if (trigger === 'click') { if (visible) { this._onHideDialog(); @@ -417,20 +424,15 @@ class Popover extends Component { * @returns {ReactElement} */ _renderContent() { - const { content } = this.props; + const { content, position } = this.props; return (
- { content } - {!isMobileBrowser() && ( - <> -
-
-
-
- )} +
+ { content } +
); } diff --git a/react/features/base/popover/functions.web.ts b/react/features/base/popover/functions.web.ts index fd653aea8a..111904a11c 100644 --- a/react/features/base/popover/functions.web.ts +++ b/react/features/base/popover/functions.web.ts @@ -1,43 +1,40 @@ -const LEFT_RIGHT_OFFSET = 25; -const TOP_BOTTOM_OFFSET = 20; - const getLeftAlignedStyle = (bounds: DOMRect) => { return { position: 'fixed', - right: `${window.innerWidth - bounds.x + LEFT_RIGHT_OFFSET}px` + right: `${window.innerWidth - bounds.x}px` }; }; const getRightAlignedStyle = (bounds: DOMRect) => { return { position: 'fixed', - left: `${bounds.x + bounds.width + LEFT_RIGHT_OFFSET}px` + left: `${bounds.x + bounds.width}px` }; }; const getTopAlignedStyle = (bounds: DOMRect) => { return { position: 'fixed', - bottom: `${window.innerHeight - bounds.y + TOP_BOTTOM_OFFSET}px` + bottom: `${window.innerHeight - bounds.y}px` }; }; const getBottomAlignedStyle = (bounds: DOMRect) => { return { position: 'fixed', - top: `${bounds.y + bounds.height + TOP_BOTTOM_OFFSET}px` + top: `${bounds.y + bounds.height}px` }; }; const getLeftRightStartAlign = (bounds: DOMRect, size: DOMRectReadOnly) => { return { - top: `${Math.min(bounds.y + 15, window.innerHeight - size.height - 20)}px` + top: `${Math.min(bounds.y, window.innerHeight - size.height - 20)}px` }; }; const getLeftRightMidAlign = (bounds: DOMRect, size: DOMRectReadOnly) => { return { - bottom: `${window.innerHeight - bounds.y - bounds.height - (size.height / 2)}px` + bottom: `${window.innerHeight - bounds.y - (bounds.height / 2) - (size.height / 2)}px` }; }; @@ -49,19 +46,19 @@ const getLeftRightEndAlign = (bounds: DOMRect, size: DOMRectReadOnly) => { const getTopBotStartAlign = (bounds: DOMRect) => { return { - right: `${window.innerWidth - bounds.x + 10}px` + right: `${window.innerWidth - bounds.x - 6}px` }; }; const getTopBotMidAlign = (bounds: DOMRect, size: DOMRectReadOnly) => { return { - right: `${window.innerWidth - bounds.x - (size.width / 2)}px` + right: `${window.innerWidth - bounds.x - (bounds.width / 2) - (size.width / 2)}px` }; }; const getTopBotEndAlign = (bounds: DOMRect) => { return { - left: `${bounds.x + bounds.width + 10}px` + left: `${bounds.x + bounds.width - 6}px` }; }; diff --git a/react/features/base/react/components/web/BaseIndicator.tsx b/react/features/base/react/components/web/BaseIndicator.tsx index 82121ee3d2..9504288e47 100644 --- a/react/features/base/react/components/web/BaseIndicator.tsx +++ b/react/features/base/react/components/web/BaseIndicator.tsx @@ -4,9 +4,7 @@ import { makeStyles } from 'tss-react/mui'; import { translate } from '../../../i18n/functions'; import Icon from '../../../icons/components/Icon'; -// eslint-disable-next-line lines-around-comment -// @ts-ignore -import { Tooltip } from '../../../tooltip'; +import Tooltip from '../../../tooltip/components/Tooltip'; /** * The type of the React {@code Component} props of {@link BaseIndicator}. @@ -58,7 +56,7 @@ interface IProps extends WithTranslation { * From which side of the indicator the tooltip should appear from, * defaulting to "top". */ - tooltipPosition: string; + tooltipPosition: 'top' | 'bottom' | 'left' | 'right'; } const useStyles = makeStyles()(() => { diff --git a/react/features/base/toolbox/components/ToolboxItem.web.js b/react/features/base/toolbox/components/ToolboxItem.web.js index 21c2e68072..e03cf9c3fc 100644 --- a/react/features/base/toolbox/components/ToolboxItem.web.js +++ b/react/features/base/toolbox/components/ToolboxItem.web.js @@ -3,7 +3,7 @@ import React, { Fragment } from 'react'; import { Icon } from '../../icons'; -import { Tooltip } from '../../tooltip'; +import Tooltip from '../../tooltip/components/Tooltip'; import ContextMenuItem from '../../ui/components/web/ContextMenuItem'; import AbstractToolboxItem from './AbstractToolboxItem'; diff --git a/react/features/base/toolbox/components/web/OverflowMenuItem.js b/react/features/base/toolbox/components/web/OverflowMenuItem.js deleted file mode 100644 index 3bb7b0cbbd..0000000000 --- a/react/features/base/toolbox/components/web/OverflowMenuItem.js +++ /dev/null @@ -1,168 +0,0 @@ -// @flow - -import React, { Component } from 'react'; - -import { Icon } from '../../../icons'; -import { Tooltip } from '../../../tooltip'; - -/** - * The type of the React {@code Component} props of {@link OverflowMenuItem}. - */ -type Props = { - - /** - * A succinct description of what the item does. Used by accessibility tools - * and torture tests. - */ - accessibilityLabel: string, - - /** - * Whether menu item is disabled or not. - */ - disabled: boolean, - - /** - * A React Element to display at the end of {@code OverflowMenuItem}. - */ - elementAfter?: React$Node, - - /** - * The icon class to use for displaying an icon before the link text. - */ - icon: Object, - - /** - * Id of the icon to be rendered. - */ - iconId?: string, - - /** - * The callback to invoke when {@code OverflowMenuItem} is clicked. - */ - onClick: Function, - - /** - * The text to display in the {@code OverflowMenuItem}. - */ - text: string, - - /** - * The text to display in the tooltip. - */ - tooltip?: string, - - /** - * From which direction the tooltip should appear, relative to the button. - */ - tooltipPosition: string -}; - -/** - * A React {@code Component} for displaying a link to interact with other - * features of the application. - * - * @augments Component - */ -class OverflowMenuItem extends Component { - /** - * Default values for {@code OverflowMenuItem} component's properties. - * - * @static - */ - static defaultProps = { - tooltipPosition: 'left', - disabled: false - }; - - /** - * Initializes a new {@code OverflowMenuItem} instance. - * - * @param {*} props - The read-only properties with which the new instance - * is to be initialized. - */ - constructor(props: Props) { - super(props); - - // Bind event handler so it is only bound once for every instance. - this._onKeyPress = this._onKeyPress.bind(this); - } - - _onKeyPress: (Object) => void; - - /** - * KeyPress handler for accessibility. - * - * @param {Object} e - The key event to handle. - * - * @returns {void} - */ - _onKeyPress(e) { - if (!this.props.disabled && this.props.onClick && (e.key === ' ' || e.key === 'Enter')) { - e.preventDefault(); - this.props.onClick(); - } - } - - /** - * Implements React's {@link Component#render()}. - * - * @inheritdoc - * @returns {ReactElement} - */ - render() { - const { accessibilityLabel, disabled, elementAfter, icon, iconId, onClick } = this.props; - - let className = 'overflow-menu-item'; - - className += this.props.disabled ? ' disabled' : ''; - - return ( -
  • - - - - { this._renderText() } - { - elementAfter || null - } -
  • - ); - } - - /** - * Renders the text label to display in the {@code OverflowMenuItem}. - * - * @private - * @returns {ReactElement} - */ - _renderText() { - const textElement = ( - - { this.props.text } - - ); - - if (this.props.tooltip) { - return ( - - { textElement } - - ); - } - - return textElement; - } -} - -export default OverflowMenuItem; diff --git a/react/features/base/toolbox/components/web/ToolboxButtonWithIcon.js b/react/features/base/toolbox/components/web/ToolboxButtonWithIcon.js index e3e056f941..c7e2ea6070 100644 --- a/react/features/base/toolbox/components/web/ToolboxButtonWithIcon.js +++ b/react/features/base/toolbox/components/web/ToolboxButtonWithIcon.js @@ -4,7 +4,7 @@ import React from 'react'; import { NOTIFY_CLICK_MODE } from '../../../../toolbox/constants'; import { Icon } from '../../../icons'; -import { Tooltip } from '../../../tooltip'; +import Tooltip from '../../../tooltip/components/Tooltip'; type Props = { @@ -107,12 +107,13 @@ export default function ToolboxButtonWithIcon(props: Props) { } = props; const iconProps = {}; + let className = ''; if (iconDisabled) { - iconProps.className + className = 'settings-button-small-icon settings-button-small-icon--disabled'; } else { - iconProps.className = 'settings-button-small-icon'; + className = 'settings-button-small-icon'; iconProps.onClick = () => { if (typeof APP !== 'undefined' && notifyMode) { APP.API.notifyToolbarButtonClicked( @@ -141,6 +142,7 @@ export default function ToolboxButtonWithIcon(props: Props) {
    { - /** - * Initializes a new {@code ToolboxItem} instance. - * - * @inheritdoc - */ - constructor(props: Props) { - super(props); - - this._onKeyPress = this._onKeyPress.bind(this); - } - - _onKeyPress: (Object) => void; - - /** - * Handles 'Enter' and Space key on the button to trigger onClick for accessibility. - * - * @param {Object} event - The key event. - * @private - * @returns {void} - */ - _onKeyPress(event) { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - this.props.onClick(); - } - } - - /** - * Handles rendering of the actual item. If the label is being shown, which - * is controlled with the `showLabel` prop, the item is rendered for its - * display in an overflow menu, otherwise it will only have an icon, which - * can be displayed on any toolbar. - * - * @protected - * @returns {ReactElement} - */ - _renderItem() { - const { - disabled, - elementAfter, - onClick, - showLabel, - tooltipPosition, - toggled - } = this.props; - const className = showLabel ? 'overflow-menu-item' : 'toolbox-button'; - const props = { - 'aria-pressed': toggled, - 'aria-disabled': disabled, - 'aria-label': this.accessibilityLabel, - className: className + (disabled ? ' disabled' : ''), - onClick: disabled ? undefined : onClick, - onKeyPress: this._onKeyPress, - tabIndex: 0, - role: 'button' - }; - - const elementType = showLabel ? 'li' : 'div'; - const useTooltip = this.tooltip && this.tooltip.length > 0; - let children = ( - - { this._renderIcon() } - { showLabel && - { this.label } - } - { elementAfter } - - ); - - if (useTooltip) { - children = ( - - { children } - - ); - } - - return React.createElement(elementType, props, children); - } - - /** - * Helper function to render the item's icon. - * - * @private - * @returns {ReactElement} - */ - _renderIcon() { - const { customClass, disabled, icon, showLabel, toggled } = this.props; - const iconComponent = ; - const elementType = showLabel ? 'span' : 'div'; - const className = `${showLabel ? 'overflow-menu-item-icon' : 'toolbox-icon'} ${ - toggled ? 'toggled' : ''} ${disabled ? 'disabled' : ''} ${customClass ?? ''}`; - - return React.createElement(elementType, { className }, iconComponent); - } -} diff --git a/react/features/base/toolbox/components/web/index.js b/react/features/base/toolbox/components/web/index.js index db5b52d43a..f8cc623c65 100644 --- a/react/features/base/toolbox/components/web/index.js +++ b/react/features/base/toolbox/components/web/index.js @@ -1,4 +1,3 @@ // @flow -export { default as OverflowMenuItem } from './OverflowMenuItem'; export { default as ToolboxButtonWithIcon } from './ToolboxButtonWithIcon'; diff --git a/react/features/base/tooltip/actionTypes.tsx b/react/features/base/tooltip/actionTypes.tsx new file mode 100644 index 0000000000..44bd95a2f2 --- /dev/null +++ b/react/features/base/tooltip/actionTypes.tsx @@ -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'; diff --git a/react/features/base/tooltip/actions.tsx b/react/features/base/tooltip/actions.tsx new file mode 100644 index 0000000000..3d2aa2bd37 --- /dev/null +++ b/react/features/base/tooltip/actions.tsx @@ -0,0 +1,29 @@ +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) { + 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) { + return { + type: HIDE_TOOLTIP, + content + }; +} diff --git a/react/features/base/tooltip/components/Tooltip.tsx b/react/features/base/tooltip/components/Tooltip.tsx new file mode 100644 index 0000000000..58cdb543de --- /dev/null +++ b/react/features/base/tooltip/components/Tooltip.tsx @@ -0,0 +1,150 @@ +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 { withPixelLineHeight } from '../../styles/functions.web'; +import { hideTooltip, showTooltip } from '../actions'; + +const TOOLTIP_DELAY = 300; +const ANIMATION_DURATION = 0.2; + +interface IProps { + children: ReactElement; + containerClassName?: string; + content: string; + position?: 'top' | 'bottom' | 'left' | 'right'; +} + +const useStyles = makeStyles()(theme => { + return { + container: { + backgroundColor: theme.palette.uiBackground, + borderRadius: '3px', + padding: theme.spacing(2), + ...withPixelLineHeight(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 { classes, cx } = useStyles(); + const timeoutID = useRef({ + open: 0, + close: 0 + }); + const { + content: storeContent, + previousContent, + visible: isVisible + } = useSelector((state: IReduxState) => state['features/base/tooltip']); + + if (isMobileBrowser()) { + return children; + } + + const contentComponent = ( +
    + {content} +
    + ); + + const openPopover = () => { + setVisible(true); + dispatch(showTooltip(content)); + }; + + const closePopover = () => { + setVisible(false); + dispatch(hideTooltip(content)); + setIsUnmounting(false); + }; + + const onPopoverOpen = useCallback(() => { + clearTimeout(timeoutID.current.close); + timeoutID.current.close = 0; + if (!visible) { + if (isVisible) { + openPopover(); + } else { + timeoutID.current.open = window.setTimeout(() => { + openPopover(); + }, TOOLTIP_DELAY); + } + } + }, [ visible, isVisible ]); + + 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 ]); + + return ( + + {children} + + ); +}; + +export default Tooltip; diff --git a/react/features/base/tooltip/components/TooltipWrapper.js b/react/features/base/tooltip/components/TooltipWrapper.js deleted file mode 100644 index bc07655457..0000000000 --- a/react/features/base/tooltip/components/TooltipWrapper.js +++ /dev/null @@ -1,50 +0,0 @@ -// @flow - -import Tooltip from '@atlaskit/tooltip'; -import React from 'react'; - -import { isMobileBrowser } from '../../environment/utils'; - -type Props = { - - /** - * Children of the component. - */ - children: React$Node, - - /** - * The text to be displayed in the tooltip. - */ - content?: string | null, - - /** - * The position of the tooltip relative to the element it contains. - */ - position?: string - -} - -/** - * Wrapper of AtlasKit Tooltip that doesn't render the actual tooltip in mobile browsers. - * - * @returns {ReactElement} - */ -function TooltipWrapper({ - children, - content, - position -}: Props) { - if (isMobileBrowser()) { - return children; - } - - return ( - - {children} - - ); -} - -export default TooltipWrapper; diff --git a/react/features/base/tooltip/components/index.js b/react/features/base/tooltip/components/index.js deleted file mode 100644 index 1a246b4982..0000000000 --- a/react/features/base/tooltip/components/index.js +++ /dev/null @@ -1,3 +0,0 @@ -// @flow - -export { default as Tooltip } from './TooltipWrapper'; diff --git a/react/features/base/tooltip/index.js b/react/features/base/tooltip/index.js deleted file mode 100644 index 68ddef9ba1..0000000000 --- a/react/features/base/tooltip/index.js +++ /dev/null @@ -1,3 +0,0 @@ -// @flow - -export * from './components'; diff --git a/react/features/base/tooltip/reducer.ts b/react/features/base/tooltip/reducer.ts new file mode 100644 index 0000000000..b1433061cf --- /dev/null +++ b/react/features/base/tooltip/reducer.ts @@ -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('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; +}); diff --git a/react/features/calendar-sync/components/AddMeetingUrlButton.web.js b/react/features/calendar-sync/components/AddMeetingUrlButton.web.js index f4848c99be..1e7f30b93f 100644 --- a/react/features/calendar-sync/components/AddMeetingUrlButton.web.js +++ b/react/features/calendar-sync/components/AddMeetingUrlButton.web.js @@ -10,7 +10,7 @@ import { import { translate } from '../../base/i18n'; import { Icon, IconPlus } from '../../base/icons'; import { connect } from '../../base/redux'; -import { Tooltip } from '../../base/tooltip'; +import Tooltip from '../../base/tooltip/components/Tooltip'; import { updateCalendarEvent } from '../actions'; /** diff --git a/react/features/calendar-sync/components/JoinButton.web.js b/react/features/calendar-sync/components/JoinButton.web.js index c0a08bce04..89daf28163 100644 --- a/react/features/calendar-sync/components/JoinButton.web.js +++ b/react/features/calendar-sync/components/JoinButton.web.js @@ -4,7 +4,7 @@ import React, { Component } from 'react'; import { translate } from '../../base/i18n'; import { Icon, IconPlus } from '../../base/icons'; -import { Tooltip } from '../../base/tooltip'; +import Tooltip from '../../base/tooltip/components/Tooltip'; /** * The type of the React {@code Component} props of {@link JoinButton}. diff --git a/react/features/conference/components/web/InsecureRoomNameLabel.tsx b/react/features/conference/components/web/InsecureRoomNameLabel.tsx index 4aabc73e7f..0ed47d22b0 100644 --- a/react/features/conference/components/web/InsecureRoomNameLabel.tsx +++ b/react/features/conference/components/web/InsecureRoomNameLabel.tsx @@ -1,4 +1,3 @@ -import Tooltip from '@atlaskit/tooltip'; import React from 'react'; import { connect } from 'react-redux'; @@ -6,6 +5,7 @@ import { translate } from '../../../base/i18n/functions'; import { IconExclamationTriangle } from '../../../base/icons/svg'; import Label from '../../../base/label/components/web/Label'; import { COLORS } from '../../../base/label/constants'; +import Tooltip from '../../../base/tooltip/components/Tooltip'; import AbstractInsecureRoomNameLabel, { _mapStateToProps } from '../AbstractInsecureRoomNameLabel'; /** diff --git a/react/features/conference/components/web/RaisedHandsCountLabel.tsx b/react/features/conference/components/web/RaisedHandsCountLabel.tsx index 13c202fb18..889e85e277 100644 --- a/react/features/conference/components/web/RaisedHandsCountLabel.tsx +++ b/react/features/conference/components/web/RaisedHandsCountLabel.tsx @@ -6,9 +6,7 @@ import { makeStyles } from 'tss-react/mui'; import { IReduxState } from '../../../app/types'; import { IconRaiseHand } from '../../../base/icons/svg'; import Label from '../../../base/label/components/web/Label'; -// eslint-disable-next-line lines-around-comment -// @ts-ignore -import { Tooltip } from '../../../base/tooltip'; +import Tooltip from '../../../base/tooltip/components/Tooltip'; import { open as openParticipantsPane } from '../../../participants-pane/actions.web'; const useStyles = makeStyles()(theme => { diff --git a/react/features/conference/components/web/SubjectText.tsx b/react/features/conference/components/web/SubjectText.tsx index 223cc2d4db..c21868876f 100644 --- a/react/features/conference/components/web/SubjectText.tsx +++ b/react/features/conference/components/web/SubjectText.tsx @@ -5,9 +5,7 @@ import { makeStyles } from 'tss-react/mui'; import { getConferenceName } from '../../../base/conference/functions'; import { withPixelLineHeight } from '../../../base/styles/functions.web'; -// eslint-disable-next-line lines-around-comment -// @ts-ignore -import { Tooltip } from '../../../base/tooltip'; +import Tooltip from '../../../base/tooltip/components/Tooltip'; const useStyles = makeStyles()(theme => { return { @@ -44,13 +42,13 @@ const SubjectText = () => { const { classes } = useStyles(); return ( -
    - + +
    {subject}
    - -
    +
    +
    ); }; diff --git a/react/features/conference/components/web/ToggleTopPanelLabel.tsx b/react/features/conference/components/web/ToggleTopPanelLabel.tsx index d1b5df7bb2..e85dd5eddb 100644 --- a/react/features/conference/components/web/ToggleTopPanelLabel.tsx +++ b/react/features/conference/components/web/ToggleTopPanelLabel.tsx @@ -5,9 +5,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { IReduxState } from '../../../app/types'; import { IconArrowDown } from '../../../base/icons/svg/index'; import Label from '../../../base/label/components/web/Label'; -// eslint-disable-next-line lines-around-comment -// @ts-ignore -import { Tooltip } from '../../../base/tooltip'; +import Tooltip from '../../../base/tooltip/components/Tooltip'; import { setTopPanelVisible } from '../../../filmstrip/actions.web'; const ToggleTopPanelLabel = () => { diff --git a/react/features/connection-stats/components/ConnectionStatsTable.tsx b/react/features/connection-stats/components/ConnectionStatsTable.tsx index ac59751bb4..e54bdf8e95 100644 --- a/react/features/connection-stats/components/ConnectionStatsTable.tsx +++ b/react/features/connection-stats/components/ConnectionStatsTable.tsx @@ -230,12 +230,9 @@ const styles = (theme: Theme) => { }, contextMenu: { position: 'relative' as const, - marginTop: 0, + margin: 0, right: 'auto', - padding: `${theme.spacing(2)} ${theme.spacing(1)}`, - marginLeft: theme.spacing(1), - marginRight: theme.spacing(1), - marginBottom: theme.spacing(1) + padding: `${theme.spacing(2)} ${theme.spacing(1)}` }, download: {}, mobile: { diff --git a/react/features/display-name/components/web/DisplayName.tsx b/react/features/display-name/components/web/DisplayName.tsx index 1d2d238039..a6e182fb51 100644 --- a/react/features/display-name/components/web/DisplayName.tsx +++ b/react/features/display-name/components/web/DisplayName.tsx @@ -10,9 +10,7 @@ import { } from '../../../base/participants/functions'; import { updateSettings } from '../../../base/settings/actions'; import { withPixelLineHeight } from '../../../base/styles/functions.web'; -// eslint-disable-next-line lines-around-comment -// @ts-ignore -import { Tooltip } from '../../../base/tooltip'; +import Tooltip from '../../../base/tooltip/components/Tooltip'; import { getIndicatorsTooltipPosition } from '../../../filmstrip/functions.web'; import { appendSuffix } from '../../functions'; diff --git a/react/features/e2ee/components/E2EELabel.tsx b/react/features/e2ee/components/E2EELabel.tsx index 82701581fa..a9f5ed6770 100644 --- a/react/features/e2ee/components/E2EELabel.tsx +++ b/react/features/e2ee/components/E2EELabel.tsx @@ -5,9 +5,7 @@ import { translate } from '../../base/i18n/functions'; import { IconE2EE } from '../../base/icons/svg'; import Label from '../../base/label/components/web/Label'; import { COLORS } from '../../base/label/constants'; -// eslint-disable-next-line lines-around-comment -// @ts-ignore -import { Tooltip } from '../../base/tooltip'; +import Tooltip from '../../base/tooltip/components/Tooltip'; import { IProps, _mapStateToProps } from './AbstractE2EELabel'; diff --git a/react/features/filmstrip/components/web/PinnedIndicator.tsx b/react/features/filmstrip/components/web/PinnedIndicator.tsx index 6c667e77ae..068d36973a 100644 --- a/react/features/filmstrip/components/web/PinnedIndicator.tsx +++ b/react/features/filmstrip/components/web/PinnedIndicator.tsx @@ -27,7 +27,7 @@ interface IProps { /** * From which side of the indicator the tooltip should appear from. */ - tooltipPosition: string; + tooltipPosition: 'top' | 'bottom' | 'left' | 'right'; } const useStyles = makeStyles()(() => { diff --git a/react/features/filmstrip/components/web/RaisedHandIndicator.tsx b/react/features/filmstrip/components/web/RaisedHandIndicator.tsx index 10636c0b23..df725cda0a 100644 --- a/react/features/filmstrip/components/web/RaisedHandIndicator.tsx +++ b/react/features/filmstrip/components/web/RaisedHandIndicator.tsx @@ -27,7 +27,7 @@ interface IProps { /** * From which side of the indicator the tooltip should appear from. */ - tooltipPosition: string; + tooltipPosition: 'top' | 'bottom' | 'left' | 'right'; } const useStyles = makeStyles()(theme => { diff --git a/react/features/filmstrip/components/web/Thumbnail.tsx b/react/features/filmstrip/components/web/Thumbnail.tsx index a56bc03907..0ef9988711 100644 --- a/react/features/filmstrip/components/web/Thumbnail.tsx +++ b/react/features/filmstrip/components/web/Thumbnail.tsx @@ -30,8 +30,7 @@ import { } from '../../../base/participants/functions'; import { IParticipant } from '../../../base/participants/types'; import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants'; -// @ts-ignore -import { Tooltip } from '../../../base/tooltip'; +import Tooltip from '../../../base/tooltip/components/Tooltip'; import { trackStreamingStatusChanged } from '../../../base/tracks/actions'; import { getLocalAudioTrack, diff --git a/react/features/filmstrip/constants.ts b/react/features/filmstrip/constants.ts index e6e030ca42..77484b998b 100644 --- a/react/features/filmstrip/constants.ts +++ b/react/features/filmstrip/constants.ts @@ -222,7 +222,9 @@ export const STATS_POPOVER_POSITION = { /** * The tooltip position for the indicators on the thumbnail. */ -export const INDICATORS_TOOLTIP_POSITION = { +export const INDICATORS_TOOLTIP_POSITION: { + [x: string]: 'right' | 'left' | 'top'; +} = { [THUMBNAIL_TYPE.TILE]: 'right', [THUMBNAIL_TYPE.VERTICAL]: 'left', [THUMBNAIL_TYPE.HORIZONTAL]: 'top' diff --git a/react/features/invite/components/add-people-dialog/web/InviteByEmailSection.tsx b/react/features/invite/components/add-people-dialog/web/InviteByEmailSection.tsx index 1e87770332..6369bcb64e 100644 --- a/react/features/invite/components/add-people-dialog/web/InviteByEmailSection.tsx +++ b/react/features/invite/components/add-people-dialog/web/InviteByEmailSection.tsx @@ -11,9 +11,7 @@ import { IconOffice365, IconYahoo } from '../../../../base/icons/svg'; -// eslint-disable-next-line lines-around-comment -// @ts-ignore -import { Tooltip } from '../../../../base/tooltip'; +import Tooltip from '../../../../base/tooltip/components/Tooltip'; import { copyText } from '../../../../base/util/copyText.web'; interface IProps { diff --git a/react/features/reactions/components/web/ReactionButton.js b/react/features/reactions/components/web/ReactionButton.js index d1fe384cdb..87e0a3a270 100644 --- a/react/features/reactions/components/web/ReactionButton.js +++ b/react/features/reactions/components/web/ReactionButton.js @@ -2,7 +2,7 @@ import React from 'react'; -import { Tooltip } from '../../../base/tooltip'; +import Tooltip from '../../../base/tooltip/components/Tooltip'; import AbstractToolbarButton from '../../../toolbox/components/AbstractToolbarButton'; import type { Props as AbstractToolbarButtonProps } from '../../../toolbox/components/AbstractToolbarButton'; diff --git a/react/features/recording/components/Recording/web/HighlightButton.tsx b/react/features/recording/components/Recording/web/HighlightButton.tsx index 6cbc10c35d..77a4f1db89 100644 --- a/react/features/recording/components/Recording/web/HighlightButton.tsx +++ b/react/features/recording/components/Recording/web/HighlightButton.tsx @@ -10,9 +10,7 @@ import { IconHighlight } from '../../../../base/icons/svg'; import { MEET_FEATURES } from '../../../../base/jwt/constants'; import Label from '../../../../base/label/components/web/Label'; import { connect } from '../../../../base/redux/functions'; -// eslint-disable-next-line lines-around-comment -// @ts-ignore -import { Tooltip } from '../../../../base/tooltip'; +import Tooltip from '../../../../base/tooltip/components/Tooltip'; import BaseTheme from '../../../../base/ui/components/BaseTheme.web'; import { maybeShowPremiumFeatureDialog } from '../../../../jaas/actions'; import AbstractHighlightButton, { diff --git a/react/features/recording/components/web/RecordingLabel.tsx b/react/features/recording/components/web/RecordingLabel.tsx index c2d187284f..afcdb1d888 100644 --- a/react/features/recording/components/web/RecordingLabel.tsx +++ b/react/features/recording/components/web/RecordingLabel.tsx @@ -7,9 +7,7 @@ import { IconRecord, IconSites } from '../../../base/icons/svg'; import Label from '../../../base/label/components/web/Label'; import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet'; import { connect } from '../../../base/redux/functions'; -// eslint-disable-next-line lines-around-comment -// @ts-ignore -import { Tooltip } from '../../../base/tooltip'; +import Tooltip from '../../../base/tooltip/components/Tooltip'; import AbstractRecordingLabel, { _mapStateToProps diff --git a/react/features/speaker-stats/components/web/SpeakerStats.tsx b/react/features/speaker-stats/components/web/SpeakerStats.tsx index 3cc4fef57c..e989b5c0f0 100644 --- a/react/features/speaker-stats/components/web/SpeakerStats.tsx +++ b/react/features/speaker-stats/components/web/SpeakerStats.tsx @@ -14,9 +14,7 @@ import { IconEmotionsSad, IconEmotionsSurprised } from '../../../base/icons/svg'; -// eslint-disable-next-line lines-around-comment -// @ts-ignore -import { Tooltip } from '../../../base/tooltip'; +import Tooltip from '../../../base/tooltip/components/Tooltip'; import Dialog from '../../../base/ui/components/web/Dialog'; import { escapeRegexp } from '../../../base/util/helpers'; import { initSearch, resetSearchCriteria, toggleFaceExpressions } from '../../actions.any'; diff --git a/react/features/toolbox/components/web/DialogPortal.ts b/react/features/toolbox/components/web/DialogPortal.ts index 91f3bb21f3..f866829cd4 100644 --- a/react/features/toolbox/components/web/DialogPortal.ts +++ b/react/features/toolbox/components/web/DialogPortal.ts @@ -31,6 +31,12 @@ type Props = { * Custom style to apply to the container div. */ style?: any; + + /** + * The selector for the element we consider the content container. + * This is used to determine the correct size of the portal content. + */ + targetSelector?: string; }; /** @@ -39,7 +45,7 @@ type Props = { * * @returns {ReactElement} */ -function DialogPortal({ children, className, style, getRef, setSize }: Props) { +function DialogPortal({ children, className, style, getRef, setSize, targetSelector }: Props) { const clientWidth = useSelector((state: IReduxState) => state['features/base/responsive-ui'].clientWidth); const [ portalTarget ] = useState(() => { const portalDiv = document.createElement('div'); @@ -87,13 +93,15 @@ function DialogPortal({ children, className, style, getRef, setSize }: Props) { } }); + const target = targetSelector ? portalTarget.querySelector(targetSelector) : portalTarget; + if (document.body) { document.body.appendChild(portalTarget); - observer.observe(portalTarget); + observer.observe(target ?? portalTarget); } return () => { - observer.unobserve(portalTarget); + observer.unobserve(target ?? portalTarget); if (document.body) { document.body.removeChild(portalTarget); } diff --git a/react/features/transcribing/components/TranscribingLabel.web.tsx b/react/features/transcribing/components/TranscribingLabel.web.tsx index da6e84daab..deeeba3cd0 100644 --- a/react/features/transcribing/components/TranscribingLabel.web.tsx +++ b/react/features/transcribing/components/TranscribingLabel.web.tsx @@ -3,9 +3,7 @@ import { connect } from 'react-redux'; import { translate } from '../../base/i18n/functions'; import Label from '../../base/label/components/web/Label'; -// eslint-disable-next-line lines-around-comment -// @ts-ignore -import { Tooltip } from '../../base/tooltip'; +import Tooltip from '../../base/tooltip/components/Tooltip'; import { Props, _mapStateToProps } from './AbstractTranscribingLabel'; diff --git a/react/features/video-quality/components/VideoQualityLabel.web.js b/react/features/video-quality/components/VideoQualityLabel.web.js index 60e0e173d9..7a1f504299 100644 --- a/react/features/video-quality/components/VideoQualityLabel.web.js +++ b/react/features/video-quality/components/VideoQualityLabel.web.js @@ -8,7 +8,7 @@ import { IconPerformance } from '../../base/icons'; import { Label } from '../../base/label'; import { COLORS } from '../../base/label/constants'; import { connect } from '../../base/redux'; -import { Tooltip } from '../../base/tooltip'; +import Tooltip from '../../base/tooltip/components/Tooltip'; import { shouldDisplayTileView } from '../../video-layout'; import AbstractVideoQualityLabel, { diff --git a/react/features/virtual-background/components/VirtualBackgrounds.tsx b/react/features/virtual-background/components/VirtualBackgrounds.tsx index 72cbde9b77..a5962318b0 100644 --- a/react/features/virtual-background/components/VirtualBackgrounds.tsx +++ b/react/features/virtual-background/components/VirtualBackgrounds.tsx @@ -14,8 +14,7 @@ import { translate } from '../../base/i18n/functions'; import Icon from '../../base/icons/components/Icon'; import { IconCloseLarge } from '../../base/icons/svg'; import { withPixelLineHeight } from '../../base/styles/functions.web'; -// @ts-ignore -import { Tooltip } from '../../base/tooltip'; +import Tooltip from '../../base/tooltip/components/Tooltip'; import Spinner from '../../base/ui/components/web/Spinner'; import { BACKGROUNDS_LIMIT, IMAGES, type Image, VIRTUAL_BACKGROUND_TYPE } from '../constants'; import { toDataURL } from '../functions'; @@ -430,7 +429,7 @@ function VirtualBackgrounds({ {_images.map(image => ( { diff --git a/tsconfig.native.json b/tsconfig.native.json index 74736a621d..0908f1e64a 100644 --- a/tsconfig.native.json +++ b/tsconfig.native.json @@ -18,6 +18,7 @@ "node_modules", "react/features/analytics/handlers/GoogleAnalyticsHandler.ts", "react/features/base/components/participants-pane-list", + "react/features/base/tooltip", "react/features/connection-stats", "react/features/desktop-picker", "react/features/e2ee",