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={() => {
deselectDraftEmoji();
}}
wasInvokedFromKeyboard={false}
/>
</div>
)}

View file

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

View file

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

View file

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

View file

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

View file

@ -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<HTMLButtonElement | null>(null);
const popperRef = React.useRef<HTMLDivElement | null>(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}
/>
)}

View file

@ -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}
/>
);
}

View file

@ -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<string>;
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<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) {
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<Category>(
categories[0]
@ -97,6 +114,9 @@ export const EmojiPicker = React.memo(
| React.MouseEvent<HTMLButtonElement>
| React.KeyboardEvent<HTMLButtonElement>
) => {
if (isEventFromMouse(e)) {
setIsUsingKeyboard(false);
}
e.stopPropagation();
e.preventDefault();
@ -129,6 +149,9 @@ export const EmojiPicker = React.memo(
| React.MouseEvent<HTMLButtonElement>
| React.KeyboardEvent<HTMLButtonElement>
) => {
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;

View file

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