Fix keyboard handling in ReactionPicker/Viewer and their child views

This commit is contained in:
Scott Nonnenberg 2022-09-07 11:29:08 -07:00 committed by Fedor Indutnyy
parent 0bf77fadd3
commit 03b750d072
7 changed files with 162 additions and 72 deletions

View file

@ -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';

View file

@ -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>

View file

@ -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"

View file

@ -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,7 +1979,6 @@ export class Message extends React.PureComponent<Props, State> {
</div> </div>
{reactionPickerRoot && {reactionPickerRoot &&
createPortal( createPortal(
<StopPropagation>
<Popper <Popper
placement="top" placement="top"
modifiers={[ modifiers={[
@ -2003,8 +2002,7 @@ export class Message extends React.PureComponent<Props, State> {
renderEmojiPicker, renderEmojiPicker,
}) })
} }
</Popper> </Popper>,
</StopPropagation>,
reactionPickerRoot reactionPickerRoot
)} )}
</Manager> </Manager>
@ -2613,7 +2611,6 @@ 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"
@ -2633,8 +2630,7 @@ export class Message extends React.PureComponent<Props, State> {
theme={theme} theme={theme}
/> />
)} )}
</Popper> </Popper>,
</StopPropagation>,
reactionViewerRoot reactionViewerRoot
)} )}
</Manager> </Manager>

View file

@ -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({

View file

@ -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">

View file

@ -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,20 +145,25 @@ 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') {
if (doSend) {
doSend();
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
doSend();
} }
} else {
const { shortName } = e.currentTarget.dataset;
if (shortName) { if (shortName) {
onPickEmoji({ skinTone: selectedTone, shortName });
e.stopPropagation();
e.preventDefault();
}
}
} else if (shortName) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
onPickEmoji({ skinTone: selectedTone, shortName }); 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.preventDefault();
event.stopPropagation(); event.stopPropagation();
onClose();
}
} 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',