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
}