FunPicker: Keep emoji picker open on select for composition inputs

This commit is contained in:
Jamie Kyle
2025-04-14 13:49:34 -07:00
committed by GitHub
parent 30708f427d
commit fa9522b6c1
15 changed files with 76 additions and 33 deletions
-1
View File
@@ -735,7 +735,6 @@ export const CompositionArea = memo(function CompositionArea({
recentEmojis={recentEmojis} recentEmojis={recentEmojis}
emojiSkinToneDefault={emojiSkinToneDefault} emojiSkinToneDefault={emojiSkinToneDefault}
onEmojiSkinToneDefaultChange={onEmojiSkinToneDefaultChange} onEmojiSkinToneDefaultChange={onEmojiSkinToneDefaultChange}
closeOnPick
/> />
</div> </div>
)} )}
+1
View File
@@ -210,6 +210,7 @@ export function CompositionTextArea({
open={emojiPickerOpen} open={emojiPickerOpen}
onOpenChange={handleEmojiPickerOpenChange} onOpenChange={handleEmojiPickerOpenChange}
onSelectEmoji={handleSelectEmoji} onSelectEmoji={handleSelectEmoji}
closeOnSelect={false}
> >
<FunEmojiPickerButton i18n={i18n} /> <FunEmojiPickerButton i18n={i18n} />
</FunEmojiPicker> </FunEmojiPicker>
@@ -262,6 +262,7 @@ function CustomizingPreferredReactionsModalItem(props: {
onOpenChange={handleEmojiPickerOpenChange} onOpenChange={handleEmojiPickerOpenChange}
placement="bottom" placement="bottom"
onSelectEmoji={props.onSelectEmoji} onSelectEmoji={props.onSelectEmoji}
closeOnSelect
> >
{button} {button}
</FunEmojiPicker> </FunEmojiPicker>
+1
View File
@@ -1409,6 +1409,7 @@ export function MediaEditor({
onSelectEmoji={handleSelectEmoji} onSelectEmoji={handleSelectEmoji}
placement="top" placement="top"
theme={ThemeType.dark} theme={ThemeType.dark}
closeOnSelect={false}
> >
<FunEmojiPickerButton i18n={i18n} /> <FunEmojiPickerButton i18n={i18n} />
</FunEmojiPicker> </FunEmojiPicker>
+1
View File
@@ -492,6 +492,7 @@ export function ProfileEditor({
onOpenChange={handleEmojiPickerOpenChange} onOpenChange={handleEmojiPickerOpenChange}
placement="bottom" placement="bottom"
onSelectEmoji={handleSelectEmoji} onSelectEmoji={handleSelectEmoji}
closeOnSelect
> >
<FunEmojiPickerButton <FunEmojiPickerButton
i18n={i18n} i18n={i18n}
@@ -323,6 +323,7 @@ export function StoryViewsNRepliesModal({
onSelectEmoji={handleSelectEmoji} onSelectEmoji={handleSelectEmoji}
placement="top" placement="top"
theme={ThemeType.dark} theme={ThemeType.dark}
closeOnSelect={false}
> >
<FunEmojiPickerButton i18n={i18n} /> <FunEmojiPickerButton i18n={i18n} />
</FunEmojiPicker> </FunEmojiPicker>
+1
View File
@@ -493,6 +493,7 @@ export function TextStoryCreator({
placement="top" placement="top"
onSelectEmoji={handleSelectEmoji} onSelectEmoji={handleSelectEmoji}
theme={ThemeType.dark} theme={ThemeType.dark}
closeOnSelect
> >
<FunEmojiPickerButton i18n={i18n} /> <FunEmojiPickerButton i18n={i18n} />
</FunEmojiPicker> </FunEmojiPicker>
@@ -155,6 +155,7 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
onSelectEmoji={onSelectEmoji} onSelectEmoji={onSelectEmoji}
theme={theme} theme={theme}
showCustomizePreferredReactionsButton showCustomizePreferredReactionsButton
closeOnSelect
> >
<Button <Button
aria-label={i18n('icu:Reactions--more')} aria-label={i18n('icu:Reactions--more')}
@@ -65,6 +65,7 @@ export default {
theme: undefined, theme: undefined,
onSelectEmoji: action('onSelectEmoji'), onSelectEmoji: action('onSelectEmoji'),
showCustomizePreferredReactionsButton: false, showCustomizePreferredReactionsButton: false,
closeOnSelect: true,
}, },
} satisfies ComponentMeta<TemplateProps>; } satisfies ComponentMeta<TemplateProps>;
+2
View File
@@ -18,6 +18,7 @@ export type FunEmojiPickerProps = Readonly<{
onSelectEmoji: (emojiSelection: FunEmojiSelection) => void; onSelectEmoji: (emojiSelection: FunEmojiSelection) => void;
theme?: ThemeType; theme?: ThemeType;
showCustomizePreferredReactionsButton?: boolean; showCustomizePreferredReactionsButton?: boolean;
closeOnSelect: boolean;
children: ReactNode; children: ReactNode;
}>; }>;
@@ -51,6 +52,7 @@ export const FunEmojiPicker = memo(function FunEmojiPicker(
showCustomizePreferredReactionsButton={ showCustomizePreferredReactionsButton={
props.showCustomizePreferredReactionsButton ?? false props.showCustomizePreferredReactionsButton ?? false
} }
closeOnSelect={props.closeOnSelect}
/> />
</FunErrorBoundary> </FunErrorBoundary>
</FunPopover> </FunPopover>
+1
View File
@@ -74,6 +74,7 @@ export const FunPicker = memo(function FunPicker(
onSelectEmoji={props.onSelectEmoji} onSelectEmoji={props.onSelectEmoji}
onClose={handleClose} onClose={handleClose}
showCustomizePreferredReactionsButton={false} showCustomizePreferredReactionsButton={false}
closeOnSelect={false}
/> />
</FunErrorBoundary> </FunErrorBoundary>
</FunTabPanel> </FunTabPanel>
+14 -1
View File
@@ -1,7 +1,7 @@
// Copyright 2025 Signal Messenger, LLC // Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import React from 'react'; import React, { useCallback } from 'react';
import type { Placement } from 'react-aria'; import type { Placement } from 'react-aria';
import { Dialog, Popover } from 'react-aria-components'; import { Dialog, Popover } from 'react-aria-components';
import classNames from 'classnames'; import classNames from 'classnames';
@@ -14,6 +14,18 @@ export type FunPopoverProps = Readonly<{
}>; }>;
export function FunPopover(props: FunPopoverProps): JSX.Element { export function FunPopover(props: FunPopoverProps): JSX.Element {
const shouldCloseOnInteractOutside = useCallback(
(element: Element): boolean => {
// Don't close when quill steals focus
const match = element.closest('.module-composition-input__input');
if (match != null) {
return false;
}
return true;
},
[]
);
return ( return (
<Popover <Popover
data-fun-overlay data-fun-overlay
@@ -22,6 +34,7 @@ export function FunPopover(props: FunPopoverProps): JSX.Element {
'dark-theme': props.theme === ThemeType.dark, 'dark-theme': props.theme === ThemeType.dark,
})} })}
placement={props.placement} placement={props.placement}
shouldCloseOnInteractOutside={shouldCloseOnInteractOutside}
> >
<Dialog className="FunPopover__Dialog">{props.children}</Dialog> <Dialog className="FunPopover__Dialog">{props.children}</Dialog>
</Popover> </Popover>
+49 -29
View File
@@ -9,6 +9,7 @@ import {
OverlayArrow, OverlayArrow,
Popover, Popover,
} from 'react-aria-components'; } from 'react-aria-components';
import type { PressEvent } from 'react-aria';
import { VisuallyHidden } from 'react-aria'; import { VisuallyHidden } from 'react-aria';
import type { LocalizerType } from '../../../types/I18N'; import type { LocalizerType } from '../../../types/I18N';
import { strictAssert } from '../../../util/assert'; import { strictAssert } from '../../../util/assert';
@@ -145,12 +146,14 @@ export type FunPanelEmojisProps = Readonly<{
onSelectEmoji: (emojiSelection: FunEmojiSelection) => void; onSelectEmoji: (emojiSelection: FunEmojiSelection) => void;
onClose: () => void; onClose: () => void;
showCustomizePreferredReactionsButton: boolean; showCustomizePreferredReactionsButton: boolean;
closeOnSelect: boolean;
}>; }>;
export function FunPanelEmojis({ export function FunPanelEmojis({
onSelectEmoji, onSelectEmoji,
onClose, onClose,
showCustomizePreferredReactionsButton, showCustomizePreferredReactionsButton,
closeOnSelect,
}: FunPanelEmojisProps): JSX.Element { }: FunPanelEmojisProps): JSX.Element {
const fun = useFunContext(); const fun = useFunContext();
const { const {
@@ -160,14 +163,15 @@ export function FunPanelEmojis({
selectedEmojisSection, selectedEmojisSection,
onChangeSelectedEmojisSection, onChangeSelectedEmojisSection,
onOpenCustomizePreferredReactionsModal, onOpenCustomizePreferredReactionsModal,
recentEmojis, recentEmojis: unstableRecentEmojis,
onSelectEmoji: onFunSelectEmoji, onSelectEmoji: onFunSelectEmoji,
} = fun; } = fun;
const scrollerRef = useRef<HTMLDivElement>(null); const scrollerRef = useRef<HTMLDivElement>(null);
// Don't update recent emojis while the emoji panel is open
const [recentEmojis] = useState(unstableRecentEmojis);
const [focusedCellKey, setFocusedCellKey] = useState<CellKey | null>(null); const [focusedCellKey, setFocusedCellKey] = useState<CellKey | null>(null);
const [skinTonePopoverOpen, setSkinTonePopoverOpen] = useState(false); const [skinTonePopoverOpen, setSkinTonePopoverOpen] = useState(false);
const handleSkinTonePopoverOpenChange = useCallback((open: boolean) => { const handleSkinTonePopoverOpenChange = useCallback((open: boolean) => {
@@ -255,13 +259,15 @@ export function FunPanelEmojis({
); );
const handleSelectEmoji = useCallback( const handleSelectEmoji = useCallback(
(emojiSelection: FunEmojiSelection) => { (emojiSelection: FunEmojiSelection, shouldClose: boolean) => {
onFunSelectEmoji(emojiSelection); onFunSelectEmoji(emojiSelection);
onSelectEmoji(emojiSelection); onSelectEmoji(emojiSelection);
onClose(); if (closeOnSelect || shouldClose) {
setFocusedCellKey(null); setFocusedCellKey(null);
onClose();
}
}, },
[onFunSelectEmoji, onSelectEmoji, onClose] [onFunSelectEmoji, onSelectEmoji, onClose, closeOnSelect]
); );
const handleOpenCustomizePreferredReactionsModal = useCallback(() => { const handleOpenCustomizePreferredReactionsModal = useCallback(() => {
@@ -478,7 +484,10 @@ type RowProps = Readonly<{
cells: ReadonlyArray<CellLayoutNode>; cells: ReadonlyArray<CellLayoutNode>;
focusedCellKey: CellKey | null; focusedCellKey: CellKey | null;
emojiSkinToneDefault: EmojiSkinTone | null; emojiSkinToneDefault: EmojiSkinTone | null;
onSelectEmoji: (emojiSelection: FunEmojiSelection) => void; onSelectEmoji: (
emojiSelection: FunEmojiSelection,
shouldClose: boolean
) => void;
onEmojiSkinToneDefaultChange: (emojiSkinTone: EmojiSkinTone) => void; onEmojiSkinToneDefaultChange: (emojiSkinTone: EmojiSkinTone) => void;
}>; }>;
@@ -517,7 +526,10 @@ type CellProps = Readonly<{
rowIndex: number; rowIndex: number;
isTabbable: boolean; isTabbable: boolean;
emojiSkinToneDefault: EmojiSkinTone | null; emojiSkinToneDefault: EmojiSkinTone | null;
onSelectEmoji: (emojiSelection: FunEmojiSelection) => void; onSelectEmoji: (
emojiSelection: FunEmojiSelection,
shouldClose: boolean
) => void;
onEmojiSkinToneDefaultChange: (emojiSkinTone: EmojiSkinTone) => void; onEmojiSkinToneDefaultChange: (emojiSkinTone: EmojiSkinTone) => void;
}>; }>;
@@ -556,26 +568,32 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element {
return getEmojiVariantByParentKeyAndSkinTone(emojiParent.key, skinTone); return getEmojiVariantByParentKeyAndSkinTone(emojiParent.key, skinTone);
}, [emojiParent, skinTone]); }, [emojiParent, skinTone]);
const handlePress = useCallback(() => { const handlePress = useCallback(
if (emojiHasSkinToneVariants && emojiSkinToneDefault == null) { (event: PressEvent) => {
setPopoverOpen(true); if (emojiHasSkinToneVariants && emojiSkinToneDefault == null) {
return; setPopoverOpen(true);
} return;
}
onSelectEmoji({ const emojiSelection: FunEmojiSelection = {
variantKey: emojiVariant.key, variantKey: emojiVariant.key,
parentKey: emojiParent.key, parentKey: emojiParent.key,
englishShortName: emojiParent.englishShortNameDefault, englishShortName: emojiParent.englishShortNameDefault,
skinTone,
};
const shouldClose =
(event.pointerType === 'keyboard' || event.pointerType === 'virtual') &&
!(event.ctrlKey || event.metaKey);
onSelectEmoji(emojiSelection, shouldClose);
},
[
emojiHasSkinToneVariants,
emojiSkinToneDefault,
emojiVariant,
emojiParent,
onSelectEmoji,
skinTone, skinTone,
}); ]
}, [ );
emojiHasSkinToneVariants,
emojiSkinToneDefault,
emojiVariant,
emojiParent,
onSelectEmoji,
skinTone,
]);
const handleLongPress = useCallback(() => { const handleLongPress = useCallback(() => {
if (emojiHasSkinToneVariants) { if (emojiHasSkinToneVariants) {
@@ -601,12 +619,14 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element {
skinToneSelection skinToneSelection
); );
onEmojiSkinToneDefaultChange(skinToneSelection); onEmojiSkinToneDefaultChange(skinToneSelection);
onSelectEmoji({ const emojiSelection: FunEmojiSelection = {
variantKey: variant.key, variantKey: variant.key,
parentKey: emojiParent.key, parentKey: emojiParent.key,
englishShortName: emojiParent.englishShortNameDefault, englishShortName: emojiParent.englishShortNameDefault,
skinTone: skinToneSelection, skinTone: skinToneSelection,
}); };
const shouldClose = true;
onSelectEmoji(emojiSelection, shouldClose);
}, },
[ [
onEmojiSkinToneDefaultChange, onEmojiSkinToneDefaultChange,
+1 -1
View File
@@ -359,9 +359,9 @@ export function FunPanelGifs({
(_event: PressEvent, gifSelection: FunGifSelection) => { (_event: PressEvent, gifSelection: FunGifSelection) => {
onFunSelectGif(gifSelection); onFunSelectGif(gifSelection);
onSelectGif(gifSelection); onSelectGif(gifSelection);
setSelectedItemKey(null);
// Should always close, cannot select multiple // Should always close, cannot select multiple
onClose(); onClose();
setSelectedItemKey(null);
}, },
[onFunSelectGif, onSelectGif, onClose] [onFunSelectGif, onSelectGif, onClose]
); );
@@ -344,8 +344,8 @@ export function FunPanelStickers({
onFunSelectSticker(stickerSelection); onFunSelectSticker(stickerSelection);
onSelectSticker(stickerSelection); onSelectSticker(stickerSelection);
if (!(event.ctrlKey || event.metaKey)) { if (!(event.ctrlKey || event.metaKey)) {
onClose();
setFocusedCellKey(null); setFocusedCellKey(null);
onClose();
} }
}, },
[onFunSelectSticker, onSelectSticker, onClose] [onFunSelectSticker, onSelectSticker, onClose]