Fix keyboard handling in ReactionPicker/Viewer and their child views

This commit is contained in:
automated-signal 2022-09-07 13:20:28 -07:00 committed by GitHub
parent 5061fbac79
commit d3e27157ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 162 additions and 72 deletions

View File

@ -1,7 +1,12 @@
// Copyright 2021 Signal Messenger, LLC
// 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 classNames from 'classnames';
@ -48,6 +53,8 @@ type PropsType = {
} & (
| {
onClick: MouseEventHandler<HTMLButtonElement>;
// TODO: DESKTOP-4121
onKeyDown?: KeyboardEventHandler<HTMLButtonElement>;
}
| {
type: 'submit';

View File

@ -173,6 +173,11 @@ export function CustomizingPreferredReactionsModal({
onClick={() => {
resetDraftEmoji();
}}
onKeyDown={event => {
if (event.key === 'Enter' || event.key === 'Space') {
resetDraftEmoji();
}
}}
variant={ButtonVariant.SecondaryAffirmative}
>
{i18n('reset')}
@ -182,6 +187,11 @@ export function CustomizingPreferredReactionsModal({
onClick={() => {
savePreferredReactions();
}}
onKeyDown={event => {
if (event.key === 'Enter' || event.key === 'Space') {
savePreferredReactions();
}
}}
>
{i18n('save')}
</Button>

View File

@ -35,6 +35,13 @@ export const ReactionPickerPickerEmojiButton = React.forwardRef<
event.stopPropagation();
onClick();
}}
onKeyDown={event => {
if (event.key === 'Enter' || event.key === 'Space') {
event.stopPropagation();
event.preventDefault();
onClick();
}
}}
>
<Emoji size={48} emoji={emoji} title={title} />
</button>
@ -54,6 +61,13 @@ export const ReactionPickerPickerMoreButton = ({
event.stopPropagation();
onClick();
}}
onKeyDown={event => {
if (event.key === 'Enter' || event.key === 'Space') {
event.stopPropagation();
event.preventDefault();
onClick();
}
}}
tabIndex={0}
title={i18n('Reactions--more')}
type="button"

View File

@ -495,9 +495,9 @@ export class Message extends React.PureComponent<Props, State> {
};
public handleFocus = (): void => {
const { interactionMode } = this.props;
const { interactionMode, isSelected } = this.props;
if (interactionMode === 'keyboard') {
if (interactionMode === 'keyboard' && !isSelected) {
this.setSelected();
}
};
@ -1979,32 +1979,30 @@ export class Message extends React.PureComponent<Props, State> {
</div>
{reactionPickerRoot &&
createPortal(
<StopPropagation>
<Popper
placement="top"
modifiers={[
offsetDistanceModifier(4),
this.popperPreventOverflowModifier(),
]}
>
{({ ref, style }) =>
renderReactionPicker({
ref,
style,
selected: selectedReaction,
onClose: this.toggleReactionPicker,
onPick: emoji => {
this.toggleReactionPicker(true);
reactToMessage(id, {
emoji,
remove: emoji === selectedReaction,
});
},
renderEmojiPicker,
})
}
</Popper>
</StopPropagation>,
<Popper
placement="top"
modifiers={[
offsetDistanceModifier(4),
this.popperPreventOverflowModifier(),
]}
>
{({ ref, style }) =>
renderReactionPicker({
ref,
style,
selected: selectedReaction,
onClose: this.toggleReactionPicker,
onPick: emoji => {
this.toggleReactionPicker(true);
reactToMessage(id, {
emoji,
remove: emoji === selectedReaction,
});
},
renderEmojiPicker,
})
}
</Popper>,
reactionPickerRoot
)}
</Manager>
@ -2613,28 +2611,26 @@ export class Message extends React.PureComponent<Props, State> {
</Reference>
{reactionViewerRoot &&
createPortal(
<StopPropagation>
<Popper
placement={popperPlacement}
strategy="fixed"
modifiers={[this.popperPreventOverflowModifier()]}
>
{({ ref, style }) => (
<ReactionViewer
ref={ref}
style={{
...style,
zIndex: 2,
}}
getPreferredBadge={getPreferredBadge}
reactions={reactions}
i18n={i18n}
onClose={this.toggleReactionViewer}
theme={theme}
/>
)}
</Popper>
</StopPropagation>,
<Popper
placement={popperPlacement}
strategy="fixed"
modifiers={[this.popperPreventOverflowModifier()]}
>
{({ ref, style }) => (
<ReactionViewer
ref={ref}
style={{
...style,
zIndex: 2,
}}
getPreferredBadge={getPreferredBadge}
reactions={reactions}
i18n={i18n}
onClose={this.toggleReactionViewer}
theme={theme}
/>
)}
</Popper>,
reactionViewerRoot
)}
</Manager>

View File

@ -4,7 +4,7 @@
import * as React from 'react';
import { convertShortName } from '../emoji/lib';
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 {
ReactionPickerPicker,
@ -75,7 +75,7 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
);
// Focus first button and restore focus on unmount
const [focusRef] = useRestoreFocus();
const [focusRef] = useDelayedRestoreFocus();
if (pickingOther) {
return renderEmojiPicker({

View File

@ -193,6 +193,13 @@ export const ReactionViewer = React.forwardRef<HTMLDivElement, Props>(
event.stopPropagation();
setSelectedReactionCategory(id);
}}
onKeyDown={event => {
if (event.key === 'Enter' || event.key === 'Space') {
event.stopPropagation();
event.preventDefault();
setSelectedReactionCategory(id);
}
}}
>
{isAll ? (
<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 handleToggleSearch = React.useCallback(
(e: React.MouseEvent) => {
(
e:
| React.MouseEvent<HTMLButtonElement>
| React.KeyboardEvent<HTMLButtonElement>
) => {
e.stopPropagation();
e.preventDefault();
setSearchText('');
setSelectedCategory(categories[0]);
setSearchMode(m => !m);
@ -115,7 +121,11 @@ export const EmojiPicker = React.memo(
);
const handlePickTone = React.useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
(
e:
| React.MouseEvent<HTMLButtonElement>
| React.KeyboardEvent<HTMLButtonElement>
) => {
e.preventDefault();
e.stopPropagation();
@ -135,19 +145,24 @@ export const EmojiPicker = React.memo(
| React.MouseEvent<HTMLButtonElement>
| React.KeyboardEvent<HTMLButtonElement>
) => {
const { shortName } = e.currentTarget.dataset;
if ('key' in e) {
if (e.key === 'Enter' && doSend) {
e.stopPropagation();
e.preventDefault();
doSend();
}
} else {
const { shortName } = e.currentTarget.dataset;
if (shortName) {
e.stopPropagation();
e.preventDefault();
onPickEmoji({ skinTone: selectedTone, shortName });
if (e.key === 'Enter') {
if (doSend) {
doSend();
e.stopPropagation();
e.preventDefault();
}
if (shortName) {
onPickEmoji({ skinTone: selectedTone, shortName });
e.stopPropagation();
e.preventDefault();
}
}
} else if (shortName) {
e.stopPropagation();
e.preventDefault();
onPickEmoji({ skinTone: selectedTone, shortName });
}
},
[doSend, onPickEmoji, selectedTone]
@ -158,14 +173,16 @@ export const EmojiPicker = React.memo(
const handler = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
if (searchMode) {
event.preventDefault();
event.stopPropagation();
setScrollToRow(0);
setSearchText('');
setSearchMode(false);
} else {
onClose?.();
} else if (onClose) {
event.preventDefault();
event.stopPropagation();
onClose();
}
event.preventDefault();
event.stopPropagation();
} else if (!searchMode && !event.ctrlKey && !event.metaKey) {
if (
[
@ -251,8 +268,14 @@ export const EmojiPicker = React.memo(
);
const handleSelectCategory = React.useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
(
e:
| React.MouseEvent<HTMLButtonElement>
| React.KeyboardEvent<HTMLButtonElement>
) => {
e.stopPropagation();
e.preventDefault();
const { category } = e.currentTarget.dataset;
if (category) {
setSelectedCategory(category);
@ -326,6 +349,11 @@ export const EmojiPicker = React.memo(
<button
type="button"
onClick={handleToggleSearch}
onKeyDown={event => {
if (event.key === 'Enter' || event.key === 'Select') {
handleToggleSearch(event);
}
}}
title={i18n('EmojiPicker--search-placeholder')}
className={classNames(
'module-emoji-picker__button',
@ -354,6 +382,11 @@ export const EmojiPicker = React.memo(
data-category={cat}
title={cat}
onClick={handleSelectCategory}
onKeyDown={event => {
if (event.key === 'Enter' || event.key === 'Space') {
handleSelectCategory(event);
}
}}
className={classNames(
'module-emoji-picker__button',
'module-emoji-picker__button--icon',
@ -412,7 +445,25 @@ export const EmojiPicker = React.memo(
<button
aria-label={i18n('CustomizingPreferredReactions__title')}
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')}
type="button"
/>
@ -425,6 +476,11 @@ export const EmojiPicker = React.memo(
key={tone}
data-tone={tone}
onClick={handlePickTone}
onKeyDown={event => {
if (event.key === 'Enter' || event.key === 'Space') {
handlePickTone(event);
}
}}
title={i18n('EmojiPicker--skin-tone', [`${tone}`])}
className={classNames(
'module-emoji-picker__button',