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
|
||||
// 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';
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Add table
Reference in a new issue