feat(reactions) Open reactions menu on hover instead of click (#11364)

Fixed issue on DialogPortal where the content would flash to the initial position then move to the correct position
This commit is contained in:
Robert Pintilii 2022-04-13 16:18:54 +03:00 committed by GitHub
parent 00bb013373
commit a6ad592d25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 201 additions and 90 deletions

View File

@ -46,18 +46,12 @@
} }
.audio-preview > div:nth-child(2), .audio-preview > div:nth-child(2),
.video-preview > div:nth-child(2), .video-preview > div:nth-child(2) {
.reactions-menu-popup > div:nth-child(2) {
margin-bottom: 4px; margin-bottom: 4px;
outline: none; outline: none;
padding: 0; padding: 0;
} }
.reactions-menu-popup > div:nth-child(2) {
margin-bottom: 6px;
box-shadow: none;
}
/** /**
* The following selectors keep the chat modal full-size anywhere between 100px * The following selectors keep the chat modal full-size anywhere between 100px
* and 580px for desktop or 680px for mobile. * and 580px for desktop or 680px for mobile.

View File

@ -104,6 +104,10 @@
} }
} }
.reactions-menu-container {
padding-bottom: 6px;
}
.reactions-animations-container { .reactions-animations-container {
position: absolute; position: absolute;
width: 20%; width: 20%;
@ -112,8 +116,7 @@
height: 0; height: 0;
} }
.reactions-menu-popup-container, .reactions-menu-popup-container {
.reactions-menu-popup {
display: inline-block; display: inline-block;
position: relative; position: relative;
} }

View File

@ -60,3 +60,15 @@
} }
} }
} }
.settings-button-small-icon-container {
position: absolute;
right: -4px;
top: -3px;
& .settings-button-small-icon {
position: relative;
top: 0;
right: 0;
}
}

View File

@ -0,0 +1,138 @@
// @flow
import React from 'react';
import { Icon } from '../../../icons';
import { Popover } from '../../../popover';
type Props = {
/**
* Whether the element popup is expanded.
*/
ariaExpanded?: boolean,
/**
* The id of the element this button icon controls.
*/
ariaControls?: string,
/**
* Whether the element has a popup.
*/
ariaHasPopup?: boolean,
/**
* Aria label for the Icon.
*/
ariaLabel?: string,
/**
* The decorated component (ToolboxButton).
*/
children: React$Node,
/**
* Icon of the button.
*/
icon: Function,
/**
* Flag used for disabling the small icon.
*/
iconDisabled: boolean,
/**
* The ID of the icon button.
*/
iconId: string,
/**
* Popover close callback.
*/
onPopoverClose: Function,
/**
* Popover open callback.
*/
onPopoverOpen: Function,
/**
* The content that will be displayed inside the popover.
*/
popoverContent: React$Node,
/**
* Additional styles.
*/
styles?: Object,
/**
* Whether or not the popover is visible.
*/
visible: boolean
};
declare var APP: Object;
/**
* Displays the `ToolboxButtonWithIcon` component.
*
* @param {Object} props - Component's props.
* @returns {ReactElement}
*/
export default function ToolboxButtonWithIconPopup(props: Props) {
const {
ariaControls,
ariaExpanded,
ariaHasPopup,
ariaLabel,
children,
icon,
iconDisabled,
iconId,
onPopoverClose,
onPopoverOpen,
popoverContent,
styles,
visible
} = props;
const iconProps = {};
if (iconDisabled) {
iconProps.className
= 'settings-button-small-icon settings-button-small-icon--disabled';
} else {
iconProps.className = 'settings-button-small-icon';
iconProps.role = 'button';
iconProps.tabIndex = 0;
iconProps.ariaControls = ariaControls;
iconProps.ariaExpanded = ariaExpanded;
iconProps.containerId = iconId;
}
return (
<div
className = 'settings-button-container'
styles = { styles }>
{children}
<div className = 'settings-button-small-icon-container'>
<Popover
content = { popoverContent }
onPopoverClose = { onPopoverClose }
onPopoverOpen = { onPopoverOpen }
position = 'top'
visible = { visible }>
<Icon
{ ...iconProps }
ariaHasPopup = { ariaHasPopup }
ariaLabel = { ariaLabel }
size = { 9 }
src = { icon } />
</Popover>
</div>
</div>
);
}

View File

@ -1,12 +1,13 @@
// @flow // @flow
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { isMobileBrowser } from '../../../base/environment/utils'; import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n'; import { translate } from '../../../base/i18n';
import { IconArrowUp } from '../../../base/icons'; import { IconArrowUp } from '../../../base/icons';
import { connect } from '../../../base/redux'; import { connect } from '../../../base/redux';
import { ToolboxButtonWithIcon } from '../../../base/toolbox/components'; import ToolboxButtonWithIconPopup from '../../../base/toolbox/components/web/ToolboxButtonWithIconPopup';
import { toggleReactionsMenuVisibility } from '../../actions.web'; import { toggleReactionsMenuVisibility } from '../../actions.web';
import { type ReactionEmojiProps } from '../../constants'; import { type ReactionEmojiProps } from '../../constants';
import { getReactionsQueue, isReactionsEnabled } from '../../functions.any'; import { getReactionsQueue, isReactionsEnabled } from '../../functions.any';
@ -14,7 +15,7 @@ import { getReactionsMenuVisibility } from '../../functions.web';
import RaiseHandButton from './RaiseHandButton'; import RaiseHandButton from './RaiseHandButton';
import ReactionEmoji from './ReactionEmoji'; import ReactionEmoji from './ReactionEmoji';
import ReactionsMenuPopup from './ReactionsMenuPopup'; import ReactionsMenu from './ReactionsMenu';
type Props = { type Props = {
@ -26,7 +27,7 @@ type Props = {
/** /**
* The button's key. * The button's key.
*/ */
buttonKey?: string, buttonKey?: string,
/** /**
* Redux dispatch function. * Redux dispatch function.
@ -84,38 +85,47 @@ function ReactionsMenuButton({
reactionsQueue, reactionsQueue,
t t
}: Props) { }: Props) {
const visible = useSelector(getReactionsMenuVisibility);
const toggleReactionsMenu = useCallback(() => { const toggleReactionsMenu = useCallback(() => {
dispatch(toggleReactionsMenuVisibility()); dispatch(toggleReactionsMenuVisibility());
}, [ dispatch ]); }, [ dispatch ]);
const openReactionsMenu = useCallback(() => {
!visible && toggleReactionsMenu();
}, [ visible, toggleReactionsMenu ]);
const reactionsMenu = (<div className = 'reactions-menu-container'>
<ReactionsMenu />
</div>);
return ( return (
<div className = 'reactions-menu-popup-container'> <div className = 'reactions-menu-popup-container'>
<ReactionsMenuPopup> {!_reactionsEnabled || isMobile ? (
{!_reactionsEnabled || isMobile ? ( <RaiseHandButton
<RaiseHandButton buttonKey = { buttonKey }
handleClick = { handleClick }
notifyMode = { notifyMode } />)
: (
<ToolboxButtonWithIconPopup
ariaControls = 'reactions-menu-dialog'
ariaExpanded = { isOpen }
ariaHasPopup = { true }
ariaLabel = { t('toolbar.accessibilityLabel.reactionsMenu') }
buttonKey = { buttonKey } buttonKey = { buttonKey }
handleClick = { handleClick } icon = { IconArrowUp }
notifyMode = { notifyMode } />) iconDisabled = { false }
: ( iconId = 'reactions-menu-button'
<ToolboxButtonWithIcon notifyMode = { notifyMode }
ariaControls = 'reactions-menu-dialog' onPopoverClose = { toggleReactionsMenu }
ariaExpanded = { isOpen } onPopoverOpen = { openReactionsMenu }
ariaHasPopup = { true } popoverContent = { reactionsMenu }
ariaLabel = { t('toolbar.accessibilityLabel.reactionsMenu') } visible = { visible }>
<RaiseHandButton
buttonKey = { buttonKey } buttonKey = { buttonKey }
icon = { IconArrowUp } handleClick = { handleClick }
iconDisabled = { false } notifyMode = { notifyMode } />
iconId = 'reactions-menu-button' </ToolboxButtonWithIconPopup>
iconTooltip = { t(`toolbar.${isOpen ? 'closeReactionsMenu' : 'openReactionsMenu'}`) } )}
notifyMode = { notifyMode }
onIconClick = { toggleReactionsMenu }>
<RaiseHandButton
buttonKey = { buttonKey }
handleClick = { handleClick }
notifyMode = { notifyMode } />
</ToolboxButtonWithIcon>
)}
</ReactionsMenuPopup>
{reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji {reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
index = { index } index = { index }
key = { uid } key = { uid }

View File

@ -1,52 +0,0 @@
// @flow
import InlineDialog from '@atlaskit/inline-dialog';
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { toggleReactionsMenuVisibility } from '../../actions.web';
import { getReactionsMenuVisibility } from '../../functions.web';
import ReactionsMenu from './ReactionsMenu';
type Props = {
/**
* Component's children (the reactions menu button).
*/
children: React$Node
}
/**
* Popup with reactions menu.
*
* @returns {ReactElement}
*/
function ReactionsMenuPopup({
children
}: Props) {
/**
* Flag controlling the visibility of the popup.
*/
const isOpen = useSelector(state => getReactionsMenuVisibility(state));
const dispatch = useDispatch();
const onClose = useCallback(() => {
dispatch(toggleReactionsMenuVisibility());
});
return (
<div className = 'reactions-menu-popup'>
<InlineDialog
content = { <ReactionsMenu /> }
isOpen = { isOpen }
onClose = { onClose }
placement = 'top'>
{children}
</InlineDialog>
</div>
);
}
export default ReactionsMenuPopup;

View File

@ -4,4 +4,3 @@ export { default as ReactionButton } from './ReactionButton';
export { default as ReactionEmoji } from './ReactionEmoji'; export { default as ReactionEmoji } from './ReactionEmoji';
export { default as ReactionsMenu } from './ReactionsMenu'; export { default as ReactionsMenu } from './ReactionsMenu';
export { default as ReactionsMenuButton } from './ReactionsMenuButton'; export { default as ReactionsMenuButton } from './ReactionsMenuButton';
export { default as ReactionsMenuPopup } from './ReactionsMenuPopup';

View File

@ -1,6 +1,6 @@
// @flow // @flow
import { useEffect, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
type Props = { type Props = {
@ -41,8 +41,11 @@ function DialogPortal({ children, className, style, getRef, setSize }: Props) {
const [ portalTarget ] = useState(() => { const [ portalTarget ] = useState(() => {
const portalDiv = document.createElement('div'); const portalDiv = document.createElement('div');
portalDiv.style.visibility = 'hidden';
return portalDiv; return portalDiv;
}); });
const timerRef = useRef();
useEffect(() => { useEffect(() => {
if (style) { if (style) {
@ -74,6 +77,10 @@ function DialogPortal({ children, className, style, getRef, setSize }: Props) {
if (contentRect.width !== size.width || contentRect.height !== size.height) { if (contentRect.width !== size.width || contentRect.height !== size.height) {
setSize && setSize(contentRect); setSize && setSize(contentRect);
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
portalTarget.style.visibility = 'visible';
}, 100);
} }
}); });