import {createContext, useContext, useEffect, useMemo, useRef} from 'react' import EmojiPicker from '@emoji-mart/react' import {DropdownMenu} from 'radix-ui' import {useA11y} from '#/state/a11y' import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter' import {atoms as a, flatten} from '#/alf' import * as Menu from '../Menu' import {useWebPreloadEmoji} from './preload' import { type Emoji, type PickerProps, type RootProps, type TriggerProps, } from './types' export * from './types' const EmojiPickerContext = createContext<{ onEmojiSelect: (emoji: Emoji) => void nextFocusRef: RootProps['nextFocusRef'] } | null>(null) /** * Provides emoji picker context and wraps children in a {@link Menu.Root}. * * On emoji select, fires a `textInputWebEmitter` event (for web text inputs * that listen for emoji insertions) and forwards to the optional * `onEmojiSelect` callback. * * @platform web */ export function Root({ children, control, onEmojiSelect, preloadOnMount = true, nextFocusRef, }: RootProps) { useWebPreloadEmoji({immediate: preloadOnMount}) const value = useMemo( () => ({ onEmojiSelect: (emoji: Emoji) => { textInputWebEmitter.emit('emoji-inserted', emoji) if (onEmojiSelect) onEmojiSelect(emoji) }, nextFocusRef, }), [onEmojiSelect, nextFocusRef], ) return ( {children} ) } /** * Passthrough to {@link Menu.Trigger}. Accepts the same render-prop children * pattern. * * @platform web */ export function Trigger(props: TriggerProps) { return } /** * Renders the emoji picker inside a Radix `DropdownMenu.Portal`. * * Holding Shift while selecting an emoji keeps the picker open for * multi-select. Otherwise the menu closes after each selection. * * Must be rendered inside a {@link Root}. * * @platform web */ export function Picker({keepOpenWhenShiftHeld = true}: PickerProps) { const {onEmojiSelect, nextFocusRef} = useEmojiPickerContext() const {control} = Menu.useMenuContext() const {reduceMotionEnabled} = useA11y() const isShiftDown = useRef(false) useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { if (e.key === 'Shift') { isShiftDown.current = true } } const onKeyUp = (e: KeyboardEvent) => { if (e.key === 'Shift') { isShiftDown.current = false } } window.addEventListener('keydown', onKeyDown, true) window.addEventListener('keyup', onKeyUp, true) return () => { window.removeEventListener('keydown', onKeyDown, true) window.removeEventListener('keyup', onKeyUp, true) } }, []) return ( { if (!nextFocusRef) return let element = nextFocusRef instanceof Function ? nextFocusRef() : nextFocusRef.current if (element) { evt.preventDefault() element.focus() } }}>
evt.stopPropagation()} style={flatten([!reduceMotionEnabled && a.zoom_fade_in])}> { onEmojiSelect(emoji) if (!keepOpenWhenShiftHeld || !isShiftDown.current) { control.close() } }} />
) } function useEmojiPickerContext() { const ctx = useContext(EmojiPickerContext) if (!ctx) throw new Error( 'EmojiPicker.Picker must be used within an EmojiPicker.Root component', ) return ctx }