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:
parent
00bb013373
commit
a6ad592d25
|
@ -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.
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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 }
|
||||||
|
|
|
@ -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;
|
|
|
@ -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';
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue