Files
fn_registry/frontend/functions/ui/sticker_picker.tsx
T
egutierrez 03568c88e3 chore: auto-commit (57 archivos)
- frontend/functions/core/format_datetime_short.md
- frontend/functions/core/format_datetime_short.test.ts
- frontend/functions/core/format_datetime_short.ts
- frontend/functions/core/format_duration.md
- frontend/functions/core/format_duration.test.ts
- frontend/functions/core/format_duration.ts
- frontend/functions/core/month_grid.md
- frontend/functions/core/month_grid.test.ts
- frontend/functions/core/month_grid.ts
- frontend/functions/core/string_hash_palette.md
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 03:41:58 +02:00

109 lines
3.3 KiB
TypeScript

import { type FC, useEffect, useRef } from "react";
import { Popover, type PopoverProps } from "@mantine/core";
import data from "@emoji-mart/data";
import { Picker } from "emoji-mart";
export interface StickerPickerProps {
/** Whether the picker popover is open. */
opened: boolean;
/** Called when the picker should close (click outside, Escape, or after selection). */
onClose: () => void;
/** Called with the selected emoji string (native unicode preferred, fallback to shortcode). */
onSelect: (emoji: string) => void;
/** Anchor element that opens the popover. */
target: React.ReactNode;
/** Color theme for the emoji-mart Picker. Defaults to "dark". */
theme?: "dark" | "light" | "auto";
/** Popover placement relative to the anchor. Defaults to "bottom-start". */
position?: PopoverProps["position"];
}
/**
* StickerPicker — emoji picker wrapped in a Mantine Popover.
*
* Renders an emoji-mart Picker inside a transparent Popover dropdown.
* The Picker instance is created once on mount and cleaned up on unmount
* so re-renders caused by parent state changes do not recreate it.
* onSelect and theme changes are reflected via refs without remounting.
*/
export const StickerPicker: FC<StickerPickerProps> = ({
opened,
onClose,
onSelect,
target,
theme = "dark",
position = "bottom-start",
}) => {
return (
<Popover
opened={opened}
onChange={(o) => {
if (!o) onClose();
}}
onDismiss={onClose}
position={position}
withArrow
shadow="md"
withinPortal
closeOnClickOutside
closeOnEscape
trapFocus={false}
>
<Popover.Target>{target}</Popover.Target>
<Popover.Dropdown p={0} style={{ background: "transparent", border: "none" }}>
<PickerInner
theme={theme}
onSelect={(emoji) => {
onSelect(emoji);
onClose();
}}
/>
</Popover.Dropdown>
</Popover>
);
};
// ---------------------------------------------------------------------------
// Internal: mounts emoji-mart Picker once, updates callbacks via refs.
// ---------------------------------------------------------------------------
interface PickerInnerProps {
onSelect: (emoji: string) => void;
theme: "dark" | "light" | "auto";
}
function PickerInner({ onSelect, theme }: PickerInnerProps) {
const ref = useRef<HTMLDivElement | null>(null);
const instanceRef = useRef<unknown>(null);
// Keep latest callback in ref so the Picker closure never goes stale.
const onSelectRef = useRef(onSelect);
onSelectRef.current = onSelect;
const themeRef = useRef(theme);
themeRef.current = theme;
useEffect(() => {
if (!ref.current) return;
instanceRef.current = new Picker({
data,
onEmojiSelect: (e: { native?: string; shortcodes?: string }) => {
const cb = onSelectRef.current;
if (e.native) cb(e.native);
else if (e.shortcodes) cb(e.shortcodes);
},
theme: themeRef.current,
previewPosition: "none",
skinTonePosition: "search",
autoFocus: true,
maxFrequentRows: 2,
ref,
});
return () => {
if (ref.current) ref.current.innerHTML = "";
instanceRef.current = null;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div ref={ref} />;
}