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 = ({ opened, onClose, onSelect, target, theme = "dark", position = "bottom-start", }) => { return ( { if (!o) onClose(); }} onDismiss={onClose} position={position} withArrow shadow="md" withinPortal closeOnClickOutside closeOnEscape trapFocus={false} > {target} { onSelect(emoji); onClose(); }} /> ); }; // --------------------------------------------------------------------------- // 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(null); const instanceRef = useRef(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
; }