From 9533796c81c8cce11897dec0e87d3a64a9307578 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Tue, 19 Mar 2024 06:23:31 -0700 Subject: [PATCH] EmojiPicker: Enter adds emoji in keyboard mode, otherwise dismisses --- .../CustomizingPreferredReactionsModal.tsx | 1 + ts/components/MediaEditor.tsx | 2 +- .../conversation/ReactionPicker.stories.tsx | 1 + .../conversation/TimelineItem.stories.tsx | 1 + .../conversation/TimelineMessage.stories.tsx | 1 + ts/components/emoji/EmojiButton.tsx | 17 ++++-- ts/components/emoji/EmojiPicker.stories.tsx | 3 ++ ts/components/emoji/EmojiPicker.tsx | 54 ++++++++++++++++--- ts/state/smart/EmojiPicker.tsx | 7 +-- 9 files changed, 72 insertions(+), 15 deletions(-) diff --git a/ts/components/CustomizingPreferredReactionsModal.tsx b/ts/components/CustomizingPreferredReactionsModal.tsx index 073cc06ac61a..01858437584b 100644 --- a/ts/components/CustomizingPreferredReactionsModal.tsx +++ b/ts/components/CustomizingPreferredReactionsModal.tsx @@ -193,6 +193,7 @@ export function CustomizingPreferredReactionsModal({ onClose={() => { deselectDraftEmoji(); }} + wasInvokedFromKeyboard={false} /> )} diff --git a/ts/components/MediaEditor.tsx b/ts/components/MediaEditor.tsx index 483a56c2bb49..56116acaf70b 100644 --- a/ts/components/MediaEditor.tsx +++ b/ts/components/MediaEditor.tsx @@ -91,7 +91,7 @@ export type PropsType = { | 'platform' | 'sortedGroupMembers' > & - EmojiPickerProps; + Omit; const INITIAL_IMAGE_STATE: ImageStateType = { angle: 0, diff --git a/ts/components/conversation/ReactionPicker.stories.tsx b/ts/components/conversation/ReactionPicker.stories.tsx index 70a5d020d860..739a092209de 100644 --- a/ts/components/conversation/ReactionPicker.stories.tsx +++ b/ts/components/conversation/ReactionPicker.stories.tsx @@ -26,6 +26,7 @@ const renderEmojiPicker: ReactionPickerProps['renderEmojiPicker'] = ({ onClose={onClose} onPickEmoji={onPickEmoji} onSetSkinTone={onSetSkinTone} + wasInvokedFromKeyboard={false} /> ); diff --git a/ts/components/conversation/TimelineItem.stories.tsx b/ts/components/conversation/TimelineItem.stories.tsx index fc682299b209..b4ae3daf19b1 100644 --- a/ts/components/conversation/TimelineItem.stories.tsx +++ b/ts/components/conversation/TimelineItem.stories.tsx @@ -33,6 +33,7 @@ const renderEmojiPicker: TimelineItemProps['renderEmojiPicker'] = ({ ref={ref} onClose={onClose} onPickEmoji={onPickEmoji} + wasInvokedFromKeyboard={false} /> ); diff --git a/ts/components/conversation/TimelineMessage.stories.tsx b/ts/components/conversation/TimelineMessage.stories.tsx index 69d2ed130a72..323800500a03 100644 --- a/ts/components/conversation/TimelineMessage.stories.tsx +++ b/ts/components/conversation/TimelineMessage.stories.tsx @@ -119,6 +119,7 @@ const renderEmojiPicker: Props['renderEmojiPicker'] = ({ ref={ref} onClose={onClose} onPickEmoji={onPickEmoji} + wasInvokedFromKeyboard={false} /> ); diff --git a/ts/components/emoji/EmojiButton.tsx b/ts/components/emoji/EmojiButton.tsx index 2f2480950df6..b92c91777dfa 100644 --- a/ts/components/emoji/EmojiButton.tsx +++ b/ts/components/emoji/EmojiButton.tsx @@ -57,6 +57,8 @@ export const EmojiButton = React.memo(function EmojiButtonInner({ const isRTL = i18n.getLocaleDirection() === 'rtl'; const [open, setOpen] = React.useState(false); + const [wasInvokedFromKeyboard, setWasInvokedFromKeyboard] = + React.useState(false); const buttonRef = React.useRef(null); const popperRef = React.useRef(null); const refMerger = useRefMerger(); @@ -69,25 +71,30 @@ export const EmojiButton = React.memo(function EmojiButtonInner({ }, [open, onOpen]); const handleClickButton = React.useCallback(() => { + setWasInvokedFromKeyboard(false); if (open) { setOpen(false); } else { setOpen(true); } - }, [open, setOpen]); + }, [open, setOpen, setWasInvokedFromKeyboard]); const handleClose = React.useCallback(() => { setOpen(false); + setWasInvokedFromKeyboard(false); if (onClose) { onClose(); } - }, [setOpen, onClose]); + }, [setOpen, setWasInvokedFromKeyboard, onClose]); const api = React.useMemo( () => ({ - close: () => setOpen(false), + close: () => { + setOpen(false); + setWasInvokedFromKeyboard(false); + }, }), - [setOpen] + [setOpen, setWasInvokedFromKeyboard] ); if (emojiButtonApi) { @@ -132,6 +139,7 @@ export const EmojiButton = React.memo(function EmojiButtonInner({ event.stopPropagation(); event.preventDefault(); + setWasInvokedFromKeyboard(true); setOpen(!open); } }; @@ -180,6 +188,7 @@ export const EmojiButton = React.memo(function EmojiButtonInner({ onClose={handleClose} skinTone={skinTone} onSetSkinTone={onSetSkinTone} + wasInvokedFromKeyboard={wasInvokedFromKeyboard} recentEmojis={recentEmojis} /> )} diff --git a/ts/components/emoji/EmojiPicker.stories.tsx b/ts/components/emoji/EmojiPicker.stories.tsx index 3616aed7e8e7..85830ddbd956 100644 --- a/ts/components/emoji/EmojiPicker.stories.tsx +++ b/ts/components/emoji/EmojiPicker.stories.tsx @@ -57,6 +57,7 @@ export function Base(): JSX.Element { 'open_mouth', 'zipper_mouth_face', ]} + wasInvokedFromKeyboard={false} /> ); } @@ -70,6 +71,7 @@ export function NoRecents(): JSX.Element { onClose={action('onClose')} skinTone={0} recentEmojis={[]} + wasInvokedFromKeyboard={false} /> ); } @@ -84,6 +86,7 @@ export function WithSettingsButton(): JSX.Element { onClose={action('onClose')} skinTone={0} recentEmojis={[]} + wasInvokedFromKeyboard={false} /> ); } diff --git a/ts/components/emoji/EmojiPicker.tsx b/ts/components/emoji/EmojiPicker.tsx index d198d7a00ef2..eff800ee6ed3 100644 --- a/ts/components/emoji/EmojiPicker.tsx +++ b/ts/components/emoji/EmojiPicker.tsx @@ -33,16 +33,28 @@ export type EmojiPickDataType = { export type OwnProps = { readonly i18n: LocalizerType; - readonly onPickEmoji: (o: EmojiPickDataType) => unknown; - readonly skinTone?: number; - readonly onSetSkinTone?: (tone: number) => unknown; readonly recentEmojis?: ReadonlyArray; + readonly skinTone?: number; readonly onClickSettings?: () => unknown; readonly onClose?: () => unknown; + readonly onPickEmoji: (o: EmojiPickDataType) => unknown; + readonly onSetSkinTone?: (tone: number) => unknown; + readonly wasInvokedFromKeyboard: boolean; }; export type Props = OwnProps & Pick, 'style'>; +function isEventFromMouse( + event: + | React.MouseEvent + | React.KeyboardEvent +): boolean { + return ( + ('clientX' in event && event.clientX !== 0) || + ('clientY' in event && event.clientY !== 0) + ); +} + function focusOnRender(el: HTMLElement | null) { if (el) { el.focus(); @@ -77,11 +89,16 @@ export const EmojiPicker = React.memo( style, onClickSettings, onClose, + wasInvokedFromKeyboard, }: Props, ref ) => { const isRTL = i18n.getLocaleDirection() === 'rtl'; + const [isUsingKeyboard, setIsUsingKeyboard] = React.useState( + wasInvokedFromKeyboard + ); + const [firstRecent] = React.useState(recentEmojis); const [selectedCategory, setSelectedCategory] = React.useState( categories[0] @@ -97,6 +114,9 @@ export const EmojiPicker = React.memo( | React.MouseEvent | React.KeyboardEvent ) => { + if (isEventFromMouse(e)) { + setIsUsingKeyboard(false); + } e.stopPropagation(); e.preventDefault(); @@ -129,6 +149,9 @@ export const EmojiPicker = React.memo( | React.MouseEvent | React.KeyboardEvent ) => { + if (isEventFromMouse(e)) { + setIsUsingKeyboard(false); + } e.preventDefault(); e.stopPropagation(); @@ -151,24 +174,42 @@ export const EmojiPicker = React.memo( const { shortName } = e.currentTarget.dataset; if ('key' in e) { if (e.key === 'Enter') { - if (shortName) { + if (shortName && isUsingKeyboard) { onPickEmoji({ skinTone: selectedTone, shortName }); e.stopPropagation(); e.preventDefault(); + } else if (onClose) { + onClose(); + e.stopPropagation(); + e.preventDefault(); } } } else if (shortName) { + if (isEventFromMouse(e)) { + setIsUsingKeyboard(false); + } e.stopPropagation(); e.preventDefault(); onPickEmoji({ skinTone: selectedTone, shortName }); } }, - [onPickEmoji, selectedTone] + [ + onClose, + onPickEmoji, + isUsingKeyboard, + selectedTone, + setIsUsingKeyboard, + ] ); // Handle key presses, particularly Escape React.useEffect(() => { 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 (searchMode) { event.preventDefault(); @@ -190,7 +231,6 @@ export const EmojiPicker = React.memo( 'ArrowRight', 'Enter', 'Shift', - 'Tab', ' ', // Space ].includes(event.key) ) { @@ -215,7 +255,7 @@ export const EmojiPicker = React.memo( return () => { document.removeEventListener('keydown', handler); }; - }, [onClose, searchMode, setSearchMode]); + }, [onClose, setIsUsingKeyboard, searchMode, setSearchMode]); const [, ...renderableCategories] = categories; diff --git a/ts/state/smart/EmojiPicker.tsx b/ts/state/smart/EmojiPicker.tsx index c5280d0e74c7..745543bc0a53 100644 --- a/ts/state/smart/EmojiPicker.tsx +++ b/ts/state/smart/EmojiPicker.tsx @@ -38,15 +38,16 @@ export const SmartEmojiPicker = memo( return ( ); })