Signal-Desktop/ts/components/fun/panels/FunPanelEmojis.tsx

817 lines
26 KiB
TypeScript

// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { memo, useCallback, useMemo, useRef, useState } from 'react';
import type { MouseEvent, PointerEvent } from 'react';
import {
Dialog,
DialogTrigger,
Heading,
OverlayArrow,
Popover,
} from 'react-aria-components';
import { VisuallyHidden } from 'react-aria';
import * as Tooltip from '@radix-ui/react-tooltip';
import type { LocalizerType } from '../../../types/I18N';
import { strictAssert } from '../../../util/assert';
import { missingCaseError } from '../../../util/missingCaseError';
import type { FunEmojisSection } from '../constants';
import {
FunEmojisBase,
FunEmojisSectionOrder,
FunSectionCommon,
} from '../constants';
import {
FunGridCell,
FunGridContainer,
FunGridHeader,
FunGridHeaderButton,
FunGridHeaderIcon,
FunGridHeaderPopover,
FunGridHeaderPopoverHeader,
FunGridHeaderText,
FunGridRow,
FunGridRowGroup,
FunGridScrollerSection,
} from '../base/FunGrid';
import { FunItemButton } from '../base/FunItem';
import {
FunPanel,
FunPanelBody,
FunPanelFooter,
FunPanelHeader,
} from '../base/FunPanel';
import { FunScroller } from '../base/FunScroller';
import { FunSearch } from '../base/FunSearch';
import {
FunSubNav,
FunSubNavIcon,
FunSubNavListBox,
FunSubNavListBoxItem,
} from '../base/FunSubNav';
import type { EmojiParentKey, EmojiVariantKey } from '../data/emojis';
import {
EmojiSkinTone,
emojiParentKeyConstant,
EmojiPickerCategory,
emojiVariantConstant,
getEmojiParentByKey,
getEmojiPickerCategoryParentKeys,
getEmojiVariantByParentKeyAndSkinTone,
normalizeShortNameCompletionDisplay,
isEmojiVariantKey,
getEmojiParentKeyByVariantKey,
getEmojiVariantByKey,
getEmojiSkinToneByVariantKey,
} from '../data/emojis';
import { useFunEmojiSearch } from '../useFunEmojiSearch';
import { FunKeyboard } from '../keyboard/FunKeyboard';
import type { GridKeyboardState } from '../keyboard/GridKeyboardDelegate';
import { GridKeyboardDelegate } from '../keyboard/GridKeyboardDelegate';
import type {
CellKey,
CellLayoutNode,
GridSectionNode,
} from '../virtual/useFunVirtualGrid';
import { useFunVirtualGrid } from '../virtual/useFunVirtualGrid';
import { FunSkinTonesList } from '../FunSkinTones';
import { FunStaticEmoji } from '../FunEmoji';
import { useFunContext } from '../FunProvider';
import { FunResults, FunResultsHeader } from '../base/FunResults';
import { useFunEmojiLocalizer } from '../useFunEmojiLocalizer';
import { FunTooltip } from '../base/FunTooltip';
function getTitleForSection(
i18n: LocalizerType,
section: FunEmojisSection
): string {
if (section === FunSectionCommon.SearchResults) {
return i18n('icu:FunPanelEmojis__SectionTitle--SearchResults');
}
if (section === FunSectionCommon.Recents) {
return i18n('icu:FunPanelEmojis__SectionTitle--Recents');
}
if (section === FunEmojisBase.ThisMessage) {
return i18n('icu:FunPanelEmojis__SectionTitle--ThisMessage');
}
if (section === EmojiPickerCategory.SmileysAndPeople) {
return i18n('icu:FunPanelEmojis__SectionTitle--SmileysAndPeople');
}
if (section === EmojiPickerCategory.AnimalsAndNature) {
return i18n('icu:FunPanelEmojis__SectionTitle--AnimalsAndNature');
}
if (section === EmojiPickerCategory.FoodAndDrink) {
return i18n('icu:FunPanelEmojis__SectionTitle--FoodAndDrink');
}
if (section === EmojiPickerCategory.TravelAndPlaces) {
return i18n('icu:FunPanelEmojis__SectionTitle--TravelAndPlaces');
}
if (section === EmojiPickerCategory.Activities) {
return i18n('icu:FunPanelEmojis__SectionTitle--Activities');
}
if (section === EmojiPickerCategory.Objects) {
return i18n('icu:FunPanelEmojis__SectionTitle--Objects');
}
if (section === EmojiPickerCategory.Symbols) {
return i18n('icu:FunPanelEmojis__SectionTitle--Symbols');
}
if (section === EmojiPickerCategory.Flags) {
return i18n('icu:FunPanelEmojis__SectionTitle--Flags');
}
throw missingCaseError(section);
}
const EMOJI_GRID_COLUMNS = 8;
const EMOJI_GRID_CELL_WIDTH = 40;
const EMOJI_GRID_CELL_HEIGHT = 40;
const EMOJI_GRID_SECTION_GAP = 20;
const EMOJI_GRID_HEADER_SIZE = 28;
const EMOJI_GRID_ROW_SIZE = EMOJI_GRID_CELL_HEIGHT;
function toGridSectionNode(
section: FunEmojisSection,
emojiKeys: ReadonlyArray<EmojiVariantKey>
): GridSectionNode {
return {
id: section,
key: `section-${section}`,
header: {
key: `header-${section}`,
},
cells: emojiKeys.map(emojiKey => {
return {
key: `cell-${section}-${emojiKey}`,
value: emojiKey,
};
}),
};
}
export type FunEmojiSelection = Readonly<{
variantKey: EmojiVariantKey;
parentKey: EmojiParentKey;
englishShortName: string;
skinTone: EmojiSkinTone;
}>;
export type FunPanelEmojisProps = Readonly<{
onSelectEmoji: (emojiSelection: FunEmojiSelection) => void;
onClose: () => void;
showCustomizePreferredReactionsButton: boolean;
closeOnSelect: boolean;
messageEmojis?: ReadonlyArray<EmojiVariantKey>;
}>;
export function FunPanelEmojis({
onSelectEmoji,
onClose,
showCustomizePreferredReactionsButton,
closeOnSelect,
messageEmojis: unstableMessageEmojis = [],
}: FunPanelEmojisProps): JSX.Element {
const fun = useFunContext();
const {
i18n,
searchInput,
onSearchInputChange,
selectedEmojisSection,
onChangeSelectedEmojisSection,
onOpenCustomizePreferredReactionsModal,
recentEmojis: unstableRecentEmojis,
onSelectEmoji: onFunSelectEmoji,
} = fun;
const scrollerRef = useRef<HTMLDivElement>(null);
// Don't update recent emojis or this message emojis while the emoji panel is open
const [recentEmojis] = useState(unstableRecentEmojis);
const [messageEmojis] = useState(unstableMessageEmojis);
const [focusedCellKey, setFocusedCellKey] = useState<CellKey | null>(null);
const [skinTonePopoverOpen, setSkinTonePopoverOpen] = useState(false);
const handleSkinTonePopoverOpenChange = useCallback((open: boolean) => {
setSkinTonePopoverOpen(open);
}, []);
const searchEmojis = useFunEmojiSearch();
const searchQuery = useMemo(() => fun.searchInput.trim(), [fun.searchInput]);
const sections = useMemo(() => {
const skinTone = fun.emojiSkinToneDefault ?? EmojiSkinTone.None;
if (searchQuery !== '') {
return [
toGridSectionNode(
FunSectionCommon.SearchResults,
searchEmojis(searchQuery).map(result => {
return getEmojiVariantByParentKeyAndSkinTone(
result.parentKey,
skinTone
).key;
})
),
];
}
const result: Array<GridSectionNode> = [];
for (const section of FunEmojisSectionOrder) {
if (section === FunEmojisBase.ThisMessage) {
if (messageEmojis.length > 0) {
result.push(
toGridSectionNode(FunEmojisBase.ThisMessage, messageEmojis)
);
}
continue;
}
if (section === FunSectionCommon.Recents) {
if (recentEmojis.length > 0) {
result.push(
toGridSectionNode(
FunSectionCommon.Recents,
recentEmojis.map(parentKey => {
return getEmojiVariantByParentKeyAndSkinTone(
parentKey,
skinTone
).key;
})
)
);
}
continue;
}
const emojiKeys = getEmojiPickerCategoryParentKeys(section);
result.push(
toGridSectionNode(
section,
emojiKeys.map(parentKey => {
return getEmojiVariantByParentKeyAndSkinTone(parentKey, skinTone)
.key;
})
)
);
}
return result;
}, [
fun.emojiSkinToneDefault,
searchQuery,
searchEmojis,
messageEmojis,
recentEmojis,
]);
const [virtualizer, layout] = useFunVirtualGrid({
scrollerRef,
sections,
columns: EMOJI_GRID_COLUMNS,
overscan: 8,
sectionGap: EMOJI_GRID_SECTION_GAP,
headerSize: EMOJI_GRID_HEADER_SIZE,
rowSize: EMOJI_GRID_ROW_SIZE,
focusedCellKey,
});
const keyboard = useMemo(() => {
return new GridKeyboardDelegate(virtualizer, layout);
}, [virtualizer, layout]);
const handleSelectSection = useCallback(
(section: FunEmojisSection) => {
const layoutSection = layout.sections.find(s => s.id === section);
strictAssert(layoutSection != null, `Expected section for ${section}`);
onChangeSelectedEmojisSection(section);
virtualizer.scrollToOffset(layoutSection.header.item.start, {
align: 'start',
});
},
[virtualizer, layout, onChangeSelectedEmojisSection]
);
const handleScrollSectionChange = useCallback(
(id: string) => {
onChangeSelectedEmojisSection(id as FunEmojisSection);
},
[onChangeSelectedEmojisSection]
);
const handleKeyboardStateChange = useCallback(
(state: GridKeyboardState) => {
if (state.cell == null) {
setFocusedCellKey(null);
return;
}
const { cellKey, sectionKey } = state.cell;
const section = layout.sections.find(s => s.key === sectionKey);
strictAssert(section != null, `Expected section for ${sectionKey}`);
setFocusedCellKey(cellKey);
onChangeSelectedEmojisSection(section.id as FunEmojisSection);
},
[onChangeSelectedEmojisSection, layout]
);
const handleSelectEmoji = useCallback(
(emojiSelection: FunEmojiSelection, shouldClose: boolean) => {
onFunSelectEmoji(emojiSelection);
onSelectEmoji(emojiSelection);
if (closeOnSelect || shouldClose) {
setFocusedCellKey(null);
onClose();
}
},
[onFunSelectEmoji, onSelectEmoji, onClose, closeOnSelect]
);
const handleOpenCustomizePreferredReactionsModal = useCallback(() => {
onOpenCustomizePreferredReactionsModal();
onClose();
}, [onOpenCustomizePreferredReactionsModal, onClose]);
const hasSearchQuery = useMemo(() => {
return searchInput.length > 0;
}, [searchInput]);
return (
<FunPanel>
<FunPanelHeader>
<FunSearch
i18n={i18n}
searchInput={searchInput}
onSearchInputChange={onSearchInputChange}
placeholder={i18n('icu:FunPanelEmojis__SearchLabel')}
aria-label={i18n('icu:FunPanelEmojis__SearchPlaceholder')}
/>
{showCustomizePreferredReactionsButton && (
<button
type="button"
aria-label={i18n(
'icu:FunPanelEmojis__CustomizeReactionsButtonLabel'
)}
className="FunPanelEmojis__CustomizePreferredReactionsButton"
onClick={handleOpenCustomizePreferredReactionsModal}
>
<span className="FunPanelEmojis__CustomizePreferredReactionsButton__Icon" />
</button>
)}
</FunPanelHeader>
{!hasSearchQuery && (
<FunPanelFooter>
<FunSubNav>
<FunSubNavListBox
aria-label={i18n('icu:FunPanelEmojis__SubNavLabel')}
selected={selectedEmojisSection}
onSelect={handleSelectSection}
>
{recentEmojis.length > 0 && (
<FunSubNavListBoxItem
id={FunSectionCommon.Recents}
label={i18n(
'icu:FunPanelEmojis__SubNavCategoryLabel--Recents'
)}
>
<FunSubNavIcon iconClassName="FunSubNav__Icon--Recents" />
</FunSubNavListBoxItem>
)}
<FunSubNavListBoxItem
id={EmojiPickerCategory.SmileysAndPeople}
label={i18n(
'icu:FunPanelEmojis__SubNavCategoryLabel--SmileysAndPeople'
)}
>
<FunSubNavIcon iconClassName="FunSubNav__Icon--SmileysAndPeople" />
</FunSubNavListBoxItem>
<FunSubNavListBoxItem
id={EmojiPickerCategory.AnimalsAndNature}
label={i18n(
'icu:FunPanelEmojis__SubNavCategoryLabel--AnimalsAndNature'
)}
>
<FunSubNavIcon iconClassName="FunSubNav__Icon--AnimalsAndNature" />
</FunSubNavListBoxItem>
<FunSubNavListBoxItem
id={EmojiPickerCategory.FoodAndDrink}
label={i18n(
'icu:FunPanelEmojis__SubNavCategoryLabel--FoodAndDrink'
)}
>
<FunSubNavIcon iconClassName="FunSubNav__Icon--FoodAndDrink" />
</FunSubNavListBoxItem>
<FunSubNavListBoxItem
id={EmojiPickerCategory.Activities}
label={i18n(
'icu:FunPanelEmojis__SubNavCategoryLabel--Activities'
)}
>
<FunSubNavIcon iconClassName="FunSubNav__Icon--Activities" />
</FunSubNavListBoxItem>
<FunSubNavListBoxItem
id={EmojiPickerCategory.TravelAndPlaces}
label={i18n(
'icu:FunPanelEmojis__SubNavCategoryLabel--TravelAndPlaces'
)}
>
<FunSubNavIcon iconClassName="FunSubNav__Icon--TravelAndPlaces" />
</FunSubNavListBoxItem>
<FunSubNavListBoxItem
id={EmojiPickerCategory.Objects}
label={i18n('icu:FunPanelEmojis__SubNavCategoryLabel--Objects')}
>
<FunSubNavIcon iconClassName="FunSubNav__Icon--Objects" />
</FunSubNavListBoxItem>
<FunSubNavListBoxItem
id={EmojiPickerCategory.Symbols}
label={i18n('icu:FunPanelEmojis__SubNavCategoryLabel--Symbols')}
>
<FunSubNavIcon iconClassName="FunSubNav__Icon--Symbols" />
</FunSubNavListBoxItem>
<FunSubNavListBoxItem
id={EmojiPickerCategory.Flags}
label={i18n('icu:FunPanelEmojis__SubNavCategoryLabel--Flags')}
>
<FunSubNavIcon iconClassName="FunSubNav__Icon--Flags" />
</FunSubNavListBoxItem>
</FunSubNavListBox>
</FunSubNav>
</FunPanelFooter>
)}
<FunPanelBody>
<Tooltip.Provider skipDelayDuration={0}>
<FunScroller
ref={scrollerRef}
sectionGap={EMOJI_GRID_SECTION_GAP}
onScrollSectionChange={handleScrollSectionChange}
>
{layout.sections.length === 0 && (
<FunResults aria-busy={false}>
<FunResultsHeader>
{i18n('icu:FunPanelEmojis__SearchResults__EmptyHeading')}{' '}
<FunStaticEmoji
size={16}
role="presentation"
emoji={emojiVariantConstant('\u{1F641}')}
/>
</FunResultsHeader>
</FunResults>
)}
{layout.sections.length > 0 && (
<FunKeyboard
scrollerRef={scrollerRef}
keyboard={keyboard}
onStateChange={handleKeyboardStateChange}
>
<FunGridContainer
totalSize={layout.totalHeight}
columnCount={EMOJI_GRID_COLUMNS}
cellWidth={EMOJI_GRID_CELL_WIDTH}
cellHeight={EMOJI_GRID_CELL_HEIGHT}
>
{layout.sections.map(section => {
return (
<FunGridScrollerSection
key={section.key}
id={section.id}
sectionOffset={section.sectionOffset}
sectionSize={section.sectionSize}
>
<FunGridHeader
id={section.header.key}
headerOffset={section.header.headerOffset}
headerSize={section.header.headerSize}
>
<FunGridHeaderText>
{getTitleForSection(
i18n,
section.id as FunEmojisSection
)}
</FunGridHeaderText>
{section.id ===
EmojiPickerCategory.SmileysAndPeople && (
<SectionSkinToneHeaderPopover
i18n={i18n}
open={skinTonePopoverOpen}
onOpenChange={handleSkinTonePopoverOpenChange}
onSelectSkinTone={
fun.onEmojiSkinToneDefaultChange
}
/>
)}
</FunGridHeader>
<FunGridRowGroup
aria-labelledby={section.header.key}
colCount={section.colCount}
rowCount={section.rowCount}
rowGroupOffset={section.rowGroup.rowGroupOffset}
rowGroupSize={section.rowGroup.rowGroupSize}
>
{section.rowGroup.rows.map(row => {
return (
<Row
key={row.key}
i18n={i18n}
rowIndex={row.rowIndex}
cells={row.cells}
focusedCellKey={focusedCellKey}
emojiSkinToneDefault={fun.emojiSkinToneDefault}
onSelectEmoji={handleSelectEmoji}
onEmojiSkinToneDefaultChange={
fun.onEmojiSkinToneDefaultChange
}
/>
);
})}
</FunGridRowGroup>
</FunGridScrollerSection>
);
})}
</FunGridContainer>
</FunKeyboard>
)}
</FunScroller>
</Tooltip.Provider>
</FunPanelBody>
</FunPanel>
);
}
type RowProps = Readonly<{
i18n: LocalizerType;
rowIndex: number;
cells: ReadonlyArray<CellLayoutNode>;
focusedCellKey: CellKey | null;
emojiSkinToneDefault: EmojiSkinTone | null;
onSelectEmoji: (
emojiSelection: FunEmojiSelection,
shouldClose: boolean
) => void;
onEmojiSkinToneDefaultChange: (emojiSkinTone: EmojiSkinTone) => void;
}>;
const Row = memo(function Row(props: RowProps): JSX.Element {
return (
<FunGridRow rowIndex={props.rowIndex}>
{props.cells.map(cell => {
const isTabbable =
props.focusedCellKey != null
? cell.key === props.focusedCellKey
: cell.rowIndex === 0 && cell.colIndex === 0;
return (
<Cell
key={cell.key}
i18n={props.i18n}
value={cell.value}
cellKey={cell.key}
rowIndex={cell.rowIndex}
colIndex={cell.colIndex}
isTabbable={isTabbable}
emojiSkinToneDefault={props.emojiSkinToneDefault}
onSelectEmoji={props.onSelectEmoji}
onEmojiSkinToneDefaultChange={props.onEmojiSkinToneDefaultChange}
/>
);
})}
</FunGridRow>
);
});
type CellProps = Readonly<{
i18n: LocalizerType;
value: string;
cellKey: CellKey;
colIndex: number;
rowIndex: number;
isTabbable: boolean;
emojiSkinToneDefault: EmojiSkinTone | null;
onSelectEmoji: (
emojiSelection: FunEmojiSelection,
shouldClose: boolean
) => void;
onEmojiSkinToneDefaultChange: (emojiSkinTone: EmojiSkinTone) => void;
}>;
const Cell = memo(function Cell(props: CellProps): JSX.Element {
const {
i18n,
emojiSkinToneDefault,
onSelectEmoji,
onEmojiSkinToneDefaultChange,
} = props;
const emojiLocalizer = useFunEmojiLocalizer();
const popoverTriggerRef = useRef<HTMLButtonElement>(null);
const [popoverOpen, setPopoverOpen] = useState(false);
const handlePopoverOpenChange = useCallback((open: boolean) => {
setPopoverOpen(open);
}, []);
const emojiParent = useMemo(() => {
const isVariantKey = isEmojiVariantKey(props.value);
strictAssert(isVariantKey, 'Cell value is not a variant key');
const parentKey = getEmojiParentKeyByVariantKey(props.value);
return getEmojiParentByKey(parentKey);
}, [props.value]);
const emojiHasSkinToneVariants = useMemo(() => {
return emojiParent.defaultSkinToneVariants != null;
}, [emojiParent.defaultSkinToneVariants]);
const emojiVariant = useMemo(() => {
const isVariantKey = isEmojiVariantKey(props.value);
strictAssert(isVariantKey, 'Cell value is not a variant key');
return getEmojiVariantByKey(props.value);
}, [props.value]);
const skinTone = useMemo(() => {
return getEmojiSkinToneByVariantKey(emojiVariant.key);
}, [emojiVariant.key]);
const handleClick = useCallback(
(event: PointerEvent) => {
if (emojiHasSkinToneVariants && emojiSkinToneDefault == null) {
setPopoverOpen(true);
return;
}
const emojiSelection: FunEmojiSelection = {
variantKey: emojiVariant.key,
parentKey: emojiParent.key,
englishShortName: emojiParent.englishShortNameDefault,
skinTone,
};
const shouldClose =
event.nativeEvent.pointerType !== 'mouse' &&
!(event.ctrlKey || event.metaKey);
onSelectEmoji(emojiSelection, shouldClose);
},
[
emojiHasSkinToneVariants,
emojiSkinToneDefault,
emojiVariant.key,
emojiParent.key,
emojiParent.englishShortNameDefault,
skinTone,
onSelectEmoji,
]
);
const handleLongPress = useCallback(() => {
if (emojiHasSkinToneVariants) {
setPopoverOpen(true);
}
}, [emojiHasSkinToneVariants]);
const handleContextMenu = useCallback(
(event: MouseEvent) => {
if (emojiHasSkinToneVariants) {
event.stopPropagation();
event.preventDefault();
setPopoverOpen(true);
}
},
[emojiHasSkinToneVariants]
);
const handleSelectSkinTone = useCallback(
(skinToneSelection: EmojiSkinTone) => {
const variant = getEmojiVariantByParentKeyAndSkinTone(
emojiParent.key,
skinToneSelection
);
onEmojiSkinToneDefaultChange(skinToneSelection);
const emojiSelection: FunEmojiSelection = {
variantKey: variant.key,
parentKey: emojiParent.key,
englishShortName: emojiParent.englishShortNameDefault,
skinTone: skinToneSelection,
};
const shouldClose = true;
onSelectEmoji(emojiSelection, shouldClose);
},
[
onEmojiSkinToneDefaultChange,
emojiParent.key,
emojiParent.englishShortNameDefault,
onSelectEmoji,
]
);
const emojiName = useMemo(() => {
return emojiLocalizer.getLocaleShortName(emojiVariant.key);
}, [emojiVariant.key, emojiLocalizer]);
const emojiShortNameDisplay = useMemo(() => {
return normalizeShortNameCompletionDisplay(emojiName);
}, [emojiName]);
return (
<FunGridCell
data-key={props.cellKey}
colIndex={props.colIndex}
rowIndex={props.rowIndex}
>
<FunTooltip
side="top"
content={`:${emojiShortNameDisplay}:`}
collisionBoundarySelector=".FunScroller__Viewport"
collisionPadding={6}
// `skipDelayDuration=0` doesn't work with `disableHoverableContent`
// FIX: https://github.com/radix-ui/primitives/pull/3562
// disableHoverableContent
>
<FunItemButton
ref={popoverTriggerRef}
excludeFromTabOrder={!props.isTabbable}
aria-label={emojiName}
onClick={handleClick}
onLongPress={handleLongPress}
onContextMenu={handleContextMenu}
longPressAccessibilityDescription={i18n(
'icu:FunPanelEmojis__SkinTonePicker__LongPressAccessibilityDescription'
)}
>
<FunStaticEmoji role="presentation" size={32} emoji={emojiVariant} />
</FunItemButton>
</FunTooltip>
{emojiHasSkinToneVariants && (
<Popover
data-fun-overlay
isOpen={popoverOpen}
onOpenChange={handlePopoverOpenChange}
triggerRef={popoverTriggerRef}
className="FunPanelEmojis__CellPopover"
placement="bottom"
offset={6}
>
<OverlayArrow className="FunPanelEmojis__CellPopoverOverlayArrow">
<svg width={12} height={12} viewBox="0 0 12 12">
<path d="M0 0 L6 6 L12 0" />
</svg>
</OverlayArrow>
<Dialog className="FunPanelEmojis__CellPopoverDialog">
<VisuallyHidden>
<Heading slot="title">
{i18n(
'icu:FunPanelEmojis__SkinTonePicker__SelectSkinToneForSelectedEmoji',
{ emojiName }
)}
</Heading>
</VisuallyHidden>
<FunSkinTonesList
i18n={i18n}
emoji={emojiParent.key}
skinTone={null}
onSelectSkinTone={handleSelectSkinTone}
/>
</Dialog>
</Popover>
)}
</FunGridCell>
);
});
type SectionSkinToneHeaderPopoverProps = Readonly<{
i18n: LocalizerType;
open: boolean;
onOpenChange: (open: boolean) => void;
onSelectSkinTone: (emojiSkinTone: EmojiSkinTone) => void;
}>;
function SectionSkinToneHeaderPopover(
props: SectionSkinToneHeaderPopoverProps
): JSX.Element {
const { i18n, onOpenChange, onSelectSkinTone } = props;
const handleSelectSkinTone = useCallback(
(emojiSkinTone: EmojiSkinTone) => {
onSelectSkinTone(emojiSkinTone);
onOpenChange(false);
},
[onSelectSkinTone, onOpenChange]
);
return (
<DialogTrigger isOpen={props.open} onOpenChange={props.onOpenChange}>
<FunGridHeaderButton
label={i18n('icu:FunPanelEmojis__ChangeSkinToneButtonLabel')}
>
<FunGridHeaderIcon iconClassName="FunGrid__HeaderIcon--More" />
</FunGridHeaderButton>
<FunGridHeaderPopover>
<FunGridHeaderPopoverHeader>
{i18n('icu:FunPanelEmojis__SkinTonePicker__ChooseDefaultLabel')}
</FunGridHeaderPopoverHeader>
<FunSkinTonesList
i18n={i18n}
emoji={emojiParentKeyConstant('\u{270B}')}
skinTone={null}
onSelectSkinTone={handleSelectSkinTone}
/>
</FunGridHeaderPopover>
</DialogTrigger>
);
}