EmojiPicker: Enter adds emoji in keyboard mode, otherwise dismisses

This commit is contained in:
Scott Nonnenberg 2024-03-19 06:23:31 -07:00 committed by GitHub
parent 28eaf1689f
commit 9533796c81
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 72 additions and 15 deletions

View file

@ -193,6 +193,7 @@ export function CustomizingPreferredReactionsModal({
onClose={() => { onClose={() => {
deselectDraftEmoji(); deselectDraftEmoji();
}} }}
wasInvokedFromKeyboard={false}
/> />
</div> </div>
)} )}

View file

@ -91,7 +91,7 @@ export type PropsType = {
| 'platform' | 'platform'
| 'sortedGroupMembers' | 'sortedGroupMembers'
> & > &
EmojiPickerProps; Omit<EmojiPickerProps, 'wasInvokedFromKeyboard'>;
const INITIAL_IMAGE_STATE: ImageStateType = { const INITIAL_IMAGE_STATE: ImageStateType = {
angle: 0, angle: 0,

View file

@ -26,6 +26,7 @@ const renderEmojiPicker: ReactionPickerProps['renderEmojiPicker'] = ({
onClose={onClose} onClose={onClose}
onPickEmoji={onPickEmoji} onPickEmoji={onPickEmoji}
onSetSkinTone={onSetSkinTone} onSetSkinTone={onSetSkinTone}
wasInvokedFromKeyboard={false}
/> />
); );

View file

@ -33,6 +33,7 @@ const renderEmojiPicker: TimelineItemProps['renderEmojiPicker'] = ({
ref={ref} ref={ref}
onClose={onClose} onClose={onClose}
onPickEmoji={onPickEmoji} onPickEmoji={onPickEmoji}
wasInvokedFromKeyboard={false}
/> />
); );

View file

@ -119,6 +119,7 @@ const renderEmojiPicker: Props['renderEmojiPicker'] = ({
ref={ref} ref={ref}
onClose={onClose} onClose={onClose}
onPickEmoji={onPickEmoji} onPickEmoji={onPickEmoji}
wasInvokedFromKeyboard={false}
/> />
); );

View file

@ -57,6 +57,8 @@ export const EmojiButton = React.memo(function EmojiButtonInner({
const isRTL = i18n.getLocaleDirection() === 'rtl'; const isRTL = i18n.getLocaleDirection() === 'rtl';
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [wasInvokedFromKeyboard, setWasInvokedFromKeyboard] =
React.useState(false);
const buttonRef = React.useRef<HTMLButtonElement | null>(null); const buttonRef = React.useRef<HTMLButtonElement | null>(null);
const popperRef = React.useRef<HTMLDivElement | null>(null); const popperRef = React.useRef<HTMLDivElement | null>(null);
const refMerger = useRefMerger(); const refMerger = useRefMerger();
@ -69,25 +71,30 @@ export const EmojiButton = React.memo(function EmojiButtonInner({
}, [open, onOpen]); }, [open, onOpen]);
const handleClickButton = React.useCallback(() => { const handleClickButton = React.useCallback(() => {
setWasInvokedFromKeyboard(false);
if (open) { if (open) {
setOpen(false); setOpen(false);
} else { } else {
setOpen(true); setOpen(true);
} }
}, [open, setOpen]); }, [open, setOpen, setWasInvokedFromKeyboard]);
const handleClose = React.useCallback(() => { const handleClose = React.useCallback(() => {
setOpen(false); setOpen(false);
setWasInvokedFromKeyboard(false);
if (onClose) { if (onClose) {
onClose(); onClose();
} }
}, [setOpen, onClose]); }, [setOpen, setWasInvokedFromKeyboard, onClose]);
const api = React.useMemo( const api = React.useMemo(
() => ({ () => ({
close: () => setOpen(false), close: () => {
setOpen(false);
setWasInvokedFromKeyboard(false);
},
}), }),
[setOpen] [setOpen, setWasInvokedFromKeyboard]
); );
if (emojiButtonApi) { if (emojiButtonApi) {
@ -132,6 +139,7 @@ export const EmojiButton = React.memo(function EmojiButtonInner({
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
setWasInvokedFromKeyboard(true);
setOpen(!open); setOpen(!open);
} }
}; };
@ -180,6 +188,7 @@ export const EmojiButton = React.memo(function EmojiButtonInner({
onClose={handleClose} onClose={handleClose}
skinTone={skinTone} skinTone={skinTone}
onSetSkinTone={onSetSkinTone} onSetSkinTone={onSetSkinTone}
wasInvokedFromKeyboard={wasInvokedFromKeyboard}
recentEmojis={recentEmojis} recentEmojis={recentEmojis}
/> />
)} )}

View file

@ -57,6 +57,7 @@ export function Base(): JSX.Element {
'open_mouth', 'open_mouth',
'zipper_mouth_face', 'zipper_mouth_face',
]} ]}
wasInvokedFromKeyboard={false}
/> />
); );
} }
@ -70,6 +71,7 @@ export function NoRecents(): JSX.Element {
onClose={action('onClose')} onClose={action('onClose')}
skinTone={0} skinTone={0}
recentEmojis={[]} recentEmojis={[]}
wasInvokedFromKeyboard={false}
/> />
); );
} }
@ -84,6 +86,7 @@ export function WithSettingsButton(): JSX.Element {
onClose={action('onClose')} onClose={action('onClose')}
skinTone={0} skinTone={0}
recentEmojis={[]} recentEmojis={[]}
wasInvokedFromKeyboard={false}
/> />
); );
} }

View file

@ -33,16 +33,28 @@ export type EmojiPickDataType = {
export type OwnProps = { export type OwnProps = {
readonly i18n: LocalizerType; readonly i18n: LocalizerType;
readonly onPickEmoji: (o: EmojiPickDataType) => unknown;
readonly skinTone?: number;
readonly onSetSkinTone?: (tone: number) => unknown;
readonly recentEmojis?: ReadonlyArray<string>; readonly recentEmojis?: ReadonlyArray<string>;
readonly skinTone?: number;
readonly onClickSettings?: () => unknown; readonly onClickSettings?: () => unknown;
readonly onClose?: () => unknown; readonly onClose?: () => unknown;
readonly onPickEmoji: (o: EmojiPickDataType) => unknown;
readonly onSetSkinTone?: (tone: number) => unknown;
readonly wasInvokedFromKeyboard: boolean;
}; };
export type Props = OwnProps & Pick<React.HTMLProps<HTMLDivElement>, 'style'>; export type Props = OwnProps & Pick<React.HTMLProps<HTMLDivElement>, 'style'>;
function isEventFromMouse(
event:
| React.MouseEvent<HTMLButtonElement>
| React.KeyboardEvent<HTMLButtonElement>
): boolean {
return (
('clientX' in event && event.clientX !== 0) ||
('clientY' in event && event.clientY !== 0)
);
}
function focusOnRender(el: HTMLElement | null) { function focusOnRender(el: HTMLElement | null) {
if (el) { if (el) {
el.focus(); el.focus();
@ -77,11 +89,16 @@ export const EmojiPicker = React.memo(
style, style,
onClickSettings, onClickSettings,
onClose, onClose,
wasInvokedFromKeyboard,
}: Props, }: Props,
ref ref
) => { ) => {
const isRTL = i18n.getLocaleDirection() === 'rtl'; const isRTL = i18n.getLocaleDirection() === 'rtl';
const [isUsingKeyboard, setIsUsingKeyboard] = React.useState(
wasInvokedFromKeyboard
);
const [firstRecent] = React.useState(recentEmojis); const [firstRecent] = React.useState(recentEmojis);
const [selectedCategory, setSelectedCategory] = React.useState<Category>( const [selectedCategory, setSelectedCategory] = React.useState<Category>(
categories[0] categories[0]
@ -97,6 +114,9 @@ export const EmojiPicker = React.memo(
| React.MouseEvent<HTMLButtonElement> | React.MouseEvent<HTMLButtonElement>
| React.KeyboardEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>
) => { ) => {
if (isEventFromMouse(e)) {
setIsUsingKeyboard(false);
}
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -129,6 +149,9 @@ export const EmojiPicker = React.memo(
| React.MouseEvent<HTMLButtonElement> | React.MouseEvent<HTMLButtonElement>
| React.KeyboardEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>
) => { ) => {
if (isEventFromMouse(e)) {
setIsUsingKeyboard(false);
}
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -151,24 +174,42 @@ export const EmojiPicker = React.memo(
const { shortName } = e.currentTarget.dataset; const { shortName } = e.currentTarget.dataset;
if ('key' in e) { if ('key' in e) {
if (e.key === 'Enter') { if (e.key === 'Enter') {
if (shortName) { if (shortName && isUsingKeyboard) {
onPickEmoji({ skinTone: selectedTone, shortName }); onPickEmoji({ skinTone: selectedTone, shortName });
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
} else if (onClose) {
onClose();
e.stopPropagation();
e.preventDefault();
} }
} }
} else if (shortName) { } else if (shortName) {
if (isEventFromMouse(e)) {
setIsUsingKeyboard(false);
}
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
onPickEmoji({ skinTone: selectedTone, shortName }); onPickEmoji({ skinTone: selectedTone, shortName });
} }
}, },
[onPickEmoji, selectedTone] [
onClose,
onPickEmoji,
isUsingKeyboard,
selectedTone,
setIsUsingKeyboard,
]
); );
// Handle key presses, particularly Escape // Handle key presses, particularly Escape
React.useEffect(() => { React.useEffect(() => {
const handler = (event: KeyboardEvent) => { const handler = (event: KeyboardEvent) => {
if (event.key === 'Tab') {
// We do NOT prevent default here to allow Tab to be used normally
setIsUsingKeyboard(true);
return;
}
if (event.key === 'Escape') { if (event.key === 'Escape') {
if (searchMode) { if (searchMode) {
event.preventDefault(); event.preventDefault();
@ -190,7 +231,6 @@ export const EmojiPicker = React.memo(
'ArrowRight', 'ArrowRight',
'Enter', 'Enter',
'Shift', 'Shift',
'Tab',
' ', // Space ' ', // Space
].includes(event.key) ].includes(event.key)
) { ) {
@ -215,7 +255,7 @@ export const EmojiPicker = React.memo(
return () => { return () => {
document.removeEventListener('keydown', handler); document.removeEventListener('keydown', handler);
}; };
}, [onClose, searchMode, setSearchMode]); }, [onClose, setIsUsingKeyboard, searchMode, setSearchMode]);
const [, ...renderableCategories] = categories; const [, ...renderableCategories] = categories;

View file

@ -38,15 +38,16 @@ export const SmartEmojiPicker = memo(
return ( return (
<EmojiPicker <EmojiPicker
ref={ref}
i18n={i18n} i18n={i18n}
skinTone={skinTone}
onClickSettings={onClickSettings} onClickSettings={onClickSettings}
onClose={onClose}
onSetSkinTone={onSetSkinTone} onSetSkinTone={onSetSkinTone}
onPickEmoji={handlePickEmoji} onPickEmoji={handlePickEmoji}
recentEmojis={recentEmojis} recentEmojis={recentEmojis}
onClose={onClose} ref={ref}
skinTone={skinTone}
style={style} style={style}
wasInvokedFromKeyboard={false}
/> />
); );
}) })