Fix keyboard handling in ReactionPicker/Viewer and their child views
This commit is contained in:
parent
0bf77fadd3
commit
03b750d072
7 changed files with 162 additions and 72 deletions
|
@ -1,7 +1,12 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { CSSProperties, MouseEventHandler, ReactNode } from 'react';
|
import type {
|
||||||
|
CSSProperties,
|
||||||
|
KeyboardEventHandler,
|
||||||
|
MouseEventHandler,
|
||||||
|
ReactNode,
|
||||||
|
} from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
@ -48,6 +53,8 @@ type PropsType = {
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
onClick: MouseEventHandler<HTMLButtonElement>;
|
onClick: MouseEventHandler<HTMLButtonElement>;
|
||||||
|
// TODO: DESKTOP-4121
|
||||||
|
onKeyDown?: KeyboardEventHandler<HTMLButtonElement>;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'submit';
|
type: 'submit';
|
||||||
|
|
|
@ -173,6 +173,11 @@ export function CustomizingPreferredReactionsModal({
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
resetDraftEmoji();
|
resetDraftEmoji();
|
||||||
}}
|
}}
|
||||||
|
onKeyDown={event => {
|
||||||
|
if (event.key === 'Enter' || event.key === 'Space') {
|
||||||
|
resetDraftEmoji();
|
||||||
|
}
|
||||||
|
}}
|
||||||
variant={ButtonVariant.SecondaryAffirmative}
|
variant={ButtonVariant.SecondaryAffirmative}
|
||||||
>
|
>
|
||||||
{i18n('reset')}
|
{i18n('reset')}
|
||||||
|
@ -182,6 +187,11 @@ export function CustomizingPreferredReactionsModal({
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
savePreferredReactions();
|
savePreferredReactions();
|
||||||
}}
|
}}
|
||||||
|
onKeyDown={event => {
|
||||||
|
if (event.key === 'Enter' || event.key === 'Space') {
|
||||||
|
savePreferredReactions();
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{i18n('save')}
|
{i18n('save')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -35,6 +35,13 @@ export const ReactionPickerPickerEmojiButton = React.forwardRef<
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
onClick();
|
onClick();
|
||||||
}}
|
}}
|
||||||
|
onKeyDown={event => {
|
||||||
|
if (event.key === 'Enter' || event.key === 'Space') {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Emoji size={48} emoji={emoji} title={title} />
|
<Emoji size={48} emoji={emoji} title={title} />
|
||||||
</button>
|
</button>
|
||||||
|
@ -54,6 +61,13 @@ export const ReactionPickerPickerMoreButton = ({
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
onClick();
|
onClick();
|
||||||
}}
|
}}
|
||||||
|
onKeyDown={event => {
|
||||||
|
if (event.key === 'Enter' || event.key === 'Space') {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
}}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
title={i18n('Reactions--more')}
|
title={i18n('Reactions--more')}
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
@ -495,9 +495,9 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
};
|
};
|
||||||
|
|
||||||
public handleFocus = (): void => {
|
public handleFocus = (): void => {
|
||||||
const { interactionMode } = this.props;
|
const { interactionMode, isSelected } = this.props;
|
||||||
|
|
||||||
if (interactionMode === 'keyboard') {
|
if (interactionMode === 'keyboard' && !isSelected) {
|
||||||
this.setSelected();
|
this.setSelected();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1979,32 +1979,30 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
</div>
|
</div>
|
||||||
{reactionPickerRoot &&
|
{reactionPickerRoot &&
|
||||||
createPortal(
|
createPortal(
|
||||||
<StopPropagation>
|
<Popper
|
||||||
<Popper
|
placement="top"
|
||||||
placement="top"
|
modifiers={[
|
||||||
modifiers={[
|
offsetDistanceModifier(4),
|
||||||
offsetDistanceModifier(4),
|
this.popperPreventOverflowModifier(),
|
||||||
this.popperPreventOverflowModifier(),
|
]}
|
||||||
]}
|
>
|
||||||
>
|
{({ ref, style }) =>
|
||||||
{({ ref, style }) =>
|
renderReactionPicker({
|
||||||
renderReactionPicker({
|
ref,
|
||||||
ref,
|
style,
|
||||||
style,
|
selected: selectedReaction,
|
||||||
selected: selectedReaction,
|
onClose: this.toggleReactionPicker,
|
||||||
onClose: this.toggleReactionPicker,
|
onPick: emoji => {
|
||||||
onPick: emoji => {
|
this.toggleReactionPicker(true);
|
||||||
this.toggleReactionPicker(true);
|
reactToMessage(id, {
|
||||||
reactToMessage(id, {
|
emoji,
|
||||||
emoji,
|
remove: emoji === selectedReaction,
|
||||||
remove: emoji === selectedReaction,
|
});
|
||||||
});
|
},
|
||||||
},
|
renderEmojiPicker,
|
||||||
renderEmojiPicker,
|
})
|
||||||
})
|
}
|
||||||
}
|
</Popper>,
|
||||||
</Popper>
|
|
||||||
</StopPropagation>,
|
|
||||||
reactionPickerRoot
|
reactionPickerRoot
|
||||||
)}
|
)}
|
||||||
</Manager>
|
</Manager>
|
||||||
|
@ -2613,28 +2611,26 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
</Reference>
|
</Reference>
|
||||||
{reactionViewerRoot &&
|
{reactionViewerRoot &&
|
||||||
createPortal(
|
createPortal(
|
||||||
<StopPropagation>
|
<Popper
|
||||||
<Popper
|
placement={popperPlacement}
|
||||||
placement={popperPlacement}
|
strategy="fixed"
|
||||||
strategy="fixed"
|
modifiers={[this.popperPreventOverflowModifier()]}
|
||||||
modifiers={[this.popperPreventOverflowModifier()]}
|
>
|
||||||
>
|
{({ ref, style }) => (
|
||||||
{({ ref, style }) => (
|
<ReactionViewer
|
||||||
<ReactionViewer
|
ref={ref}
|
||||||
ref={ref}
|
style={{
|
||||||
style={{
|
...style,
|
||||||
...style,
|
zIndex: 2,
|
||||||
zIndex: 2,
|
}}
|
||||||
}}
|
getPreferredBadge={getPreferredBadge}
|
||||||
getPreferredBadge={getPreferredBadge}
|
reactions={reactions}
|
||||||
reactions={reactions}
|
i18n={i18n}
|
||||||
i18n={i18n}
|
onClose={this.toggleReactionViewer}
|
||||||
onClose={this.toggleReactionViewer}
|
theme={theme}
|
||||||
theme={theme}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
</Popper>,
|
||||||
</Popper>
|
|
||||||
</StopPropagation>,
|
|
||||||
reactionViewerRoot
|
reactionViewerRoot
|
||||||
)}
|
)}
|
||||||
</Manager>
|
</Manager>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { convertShortName } from '../emoji/lib';
|
import { convertShortName } from '../emoji/lib';
|
||||||
import type { Props as EmojiPickerProps } from '../emoji/EmojiPicker';
|
import type { Props as EmojiPickerProps } from '../emoji/EmojiPicker';
|
||||||
import { useRestoreFocus } from '../../hooks/useRestoreFocus';
|
import { useDelayedRestoreFocus } from '../../hooks/useRestoreFocus';
|
||||||
import type { LocalizerType } from '../../types/Util';
|
import type { LocalizerType } from '../../types/Util';
|
||||||
import {
|
import {
|
||||||
ReactionPickerPicker,
|
ReactionPickerPicker,
|
||||||
|
@ -75,7 +75,7 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
|
||||||
);
|
);
|
||||||
|
|
||||||
// Focus first button and restore focus on unmount
|
// Focus first button and restore focus on unmount
|
||||||
const [focusRef] = useRestoreFocus();
|
const [focusRef] = useDelayedRestoreFocus();
|
||||||
|
|
||||||
if (pickingOther) {
|
if (pickingOther) {
|
||||||
return renderEmojiPicker({
|
return renderEmojiPicker({
|
||||||
|
|
|
@ -193,6 +193,13 @@ export const ReactionViewer = React.forwardRef<HTMLDivElement, Props>(
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setSelectedReactionCategory(id);
|
setSelectedReactionCategory(id);
|
||||||
}}
|
}}
|
||||||
|
onKeyDown={event => {
|
||||||
|
if (event.key === 'Enter' || event.key === 'Space') {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
setSelectedReactionCategory(id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{isAll ? (
|
{isAll ? (
|
||||||
<span className="module-reaction-viewer__header__button__all">
|
<span className="module-reaction-viewer__header__button__all">
|
||||||
|
|
|
@ -89,8 +89,14 @@ export const EmojiPicker = React.memo(
|
||||||
const [selectedTone, setSelectedTone] = React.useState(skinTone);
|
const [selectedTone, setSelectedTone] = React.useState(skinTone);
|
||||||
|
|
||||||
const handleToggleSearch = React.useCallback(
|
const handleToggleSearch = React.useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(
|
||||||
|
e:
|
||||||
|
| React.MouseEvent<HTMLButtonElement>
|
||||||
|
| React.KeyboardEvent<HTMLButtonElement>
|
||||||
|
) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
setSearchText('');
|
setSearchText('');
|
||||||
setSelectedCategory(categories[0]);
|
setSelectedCategory(categories[0]);
|
||||||
setSearchMode(m => !m);
|
setSearchMode(m => !m);
|
||||||
|
@ -115,7 +121,11 @@ export const EmojiPicker = React.memo(
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePickTone = React.useCallback(
|
const handlePickTone = React.useCallback(
|
||||||
(e: React.MouseEvent<HTMLButtonElement>) => {
|
(
|
||||||
|
e:
|
||||||
|
| React.MouseEvent<HTMLButtonElement>
|
||||||
|
| React.KeyboardEvent<HTMLButtonElement>
|
||||||
|
) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
@ -135,19 +145,24 @@ export const EmojiPicker = React.memo(
|
||||||
| React.MouseEvent<HTMLButtonElement>
|
| React.MouseEvent<HTMLButtonElement>
|
||||||
| React.KeyboardEvent<HTMLButtonElement>
|
| React.KeyboardEvent<HTMLButtonElement>
|
||||||
) => {
|
) => {
|
||||||
|
const { shortName } = e.currentTarget.dataset;
|
||||||
if ('key' in e) {
|
if ('key' in e) {
|
||||||
if (e.key === 'Enter' && doSend) {
|
if (e.key === 'Enter') {
|
||||||
e.stopPropagation();
|
if (doSend) {
|
||||||
e.preventDefault();
|
doSend();
|
||||||
doSend();
|
e.stopPropagation();
|
||||||
}
|
e.preventDefault();
|
||||||
} else {
|
}
|
||||||
const { shortName } = e.currentTarget.dataset;
|
if (shortName) {
|
||||||
if (shortName) {
|
onPickEmoji({ skinTone: selectedTone, shortName });
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onPickEmoji({ skinTone: selectedTone, shortName });
|
}
|
||||||
}
|
}
|
||||||
|
} else if (shortName) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
onPickEmoji({ skinTone: selectedTone, shortName });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[doSend, onPickEmoji, selectedTone]
|
[doSend, onPickEmoji, selectedTone]
|
||||||
|
@ -158,14 +173,16 @@ export const EmojiPicker = React.memo(
|
||||||
const handler = (event: KeyboardEvent) => {
|
const handler = (event: KeyboardEvent) => {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
if (searchMode) {
|
if (searchMode) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
setScrollToRow(0);
|
setScrollToRow(0);
|
||||||
setSearchText('');
|
setSearchText('');
|
||||||
setSearchMode(false);
|
setSearchMode(false);
|
||||||
} else {
|
} else if (onClose) {
|
||||||
onClose?.();
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
onClose();
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
} else if (!searchMode && !event.ctrlKey && !event.metaKey) {
|
} else if (!searchMode && !event.ctrlKey && !event.metaKey) {
|
||||||
if (
|
if (
|
||||||
[
|
[
|
||||||
|
@ -251,8 +268,14 @@ export const EmojiPicker = React.memo(
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSelectCategory = React.useCallback(
|
const handleSelectCategory = React.useCallback(
|
||||||
(e: React.MouseEvent<HTMLButtonElement>) => {
|
(
|
||||||
|
e:
|
||||||
|
| React.MouseEvent<HTMLButtonElement>
|
||||||
|
| React.KeyboardEvent<HTMLButtonElement>
|
||||||
|
) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
const { category } = e.currentTarget.dataset;
|
const { category } = e.currentTarget.dataset;
|
||||||
if (category) {
|
if (category) {
|
||||||
setSelectedCategory(category);
|
setSelectedCategory(category);
|
||||||
|
@ -326,6 +349,11 @@ export const EmojiPicker = React.memo(
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleToggleSearch}
|
onClick={handleToggleSearch}
|
||||||
|
onKeyDown={event => {
|
||||||
|
if (event.key === 'Enter' || event.key === 'Select') {
|
||||||
|
handleToggleSearch(event);
|
||||||
|
}
|
||||||
|
}}
|
||||||
title={i18n('EmojiPicker--search-placeholder')}
|
title={i18n('EmojiPicker--search-placeholder')}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-emoji-picker__button',
|
'module-emoji-picker__button',
|
||||||
|
@ -354,6 +382,11 @@ export const EmojiPicker = React.memo(
|
||||||
data-category={cat}
|
data-category={cat}
|
||||||
title={cat}
|
title={cat}
|
||||||
onClick={handleSelectCategory}
|
onClick={handleSelectCategory}
|
||||||
|
onKeyDown={event => {
|
||||||
|
if (event.key === 'Enter' || event.key === 'Space') {
|
||||||
|
handleSelectCategory(event);
|
||||||
|
}
|
||||||
|
}}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-emoji-picker__button',
|
'module-emoji-picker__button',
|
||||||
'module-emoji-picker__button--icon',
|
'module-emoji-picker__button--icon',
|
||||||
|
@ -412,7 +445,25 @@ export const EmojiPicker = React.memo(
|
||||||
<button
|
<button
|
||||||
aria-label={i18n('CustomizingPreferredReactions__title')}
|
aria-label={i18n('CustomizingPreferredReactions__title')}
|
||||||
className="module-emoji-picker__button module-emoji-picker__button--footer module-emoji-picker__button--settings"
|
className="module-emoji-picker__button module-emoji-picker__button--footer module-emoji-picker__button--settings"
|
||||||
onClick={onClickSettings}
|
onClick={event => {
|
||||||
|
if (onClickSettings) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
onClickSettings();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onKeyDown={event => {
|
||||||
|
if (
|
||||||
|
onClickSettings &&
|
||||||
|
(event.key === 'Enter' || event.key === 'Space')
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
onClickSettings();
|
||||||
|
}
|
||||||
|
}}
|
||||||
title={i18n('CustomizingPreferredReactions__title')}
|
title={i18n('CustomizingPreferredReactions__title')}
|
||||||
type="button"
|
type="button"
|
||||||
/>
|
/>
|
||||||
|
@ -425,6 +476,11 @@ export const EmojiPicker = React.memo(
|
||||||
key={tone}
|
key={tone}
|
||||||
data-tone={tone}
|
data-tone={tone}
|
||||||
onClick={handlePickTone}
|
onClick={handlePickTone}
|
||||||
|
onKeyDown={event => {
|
||||||
|
if (event.key === 'Enter' || event.key === 'Space') {
|
||||||
|
handlePickTone(event);
|
||||||
|
}
|
||||||
|
}}
|
||||||
title={i18n('EmojiPicker--skin-tone', [`${tone}`])}
|
title={i18n('EmojiPicker--skin-tone', [`${tone}`])}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-emoji-picker__button',
|
'module-emoji-picker__button',
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue