105 lines
3.0 KiB
JavaScript
105 lines
3.0 KiB
JavaScript
import React, { useEffect, useRef, useCallback } from 'react'
|
|
|
|
let emojiMartPromise = null
|
|
|
|
function ensureEmojiMart() {
|
|
if (!emojiMartPromise) {
|
|
emojiMartPromise = import('emoji-mart')
|
|
}
|
|
return emojiMartPromise
|
|
}
|
|
|
|
export default function EmojiMartPicker({
|
|
data,
|
|
onEmojiSelect,
|
|
onClickOutside,
|
|
theme = 'auto',
|
|
previewPosition = 'bottom',
|
|
skinTonePosition = 'preview',
|
|
maxFrequentRows = 4,
|
|
perLine = 9,
|
|
navPosition = 'top',
|
|
set = 'native',
|
|
locale = 'en',
|
|
autoFocus = false,
|
|
searchPosition,
|
|
dynamicWidth,
|
|
noCountryFlags,
|
|
className = '',
|
|
}) {
|
|
const hostRef = useRef(null)
|
|
const pickerRef = useRef(null)
|
|
|
|
// Keep refs pointing at the latest callback props so stable wrappers
|
|
// never capture a stale closure.
|
|
const onEmojiSelectRef = useRef(onEmojiSelect)
|
|
const onClickOutsideRef = useRef(onClickOutside)
|
|
onEmojiSelectRef.current = onEmojiSelect
|
|
onClickOutsideRef.current = onClickOutside
|
|
|
|
// Stable wrappers with fixed identity — safe to pass once to the Picker
|
|
// constructor without needing to re-initialise the element on every render.
|
|
const stableOnEmojiSelect = useCallback((emoji) => {
|
|
onEmojiSelectRef.current?.(emoji)
|
|
}, [])
|
|
|
|
const stableOnClickOutside = useCallback((e) => {
|
|
onClickOutsideRef.current?.(e)
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
|
|
// emoji-mart's Picker stores callbacks in `this.props` during construction.
|
|
// connectedCallback reads from `this.props`, NOT from plain element
|
|
// properties set after construction, so we MUST use `new Picker(props)`
|
|
// rather than `document.createElement('em-emoji-picker')` + property
|
|
// assignment, which would leave onEmojiSelect as null internally.
|
|
ensureEmojiMart().then(({ Picker }) => {
|
|
if (cancelled || !hostRef.current) return
|
|
|
|
if (!pickerRef.current) {
|
|
const pickerProps = {
|
|
data,
|
|
onEmojiSelect: stableOnEmojiSelect,
|
|
onClickOutside: stableOnClickOutside,
|
|
theme,
|
|
previewPosition,
|
|
skinTonePosition,
|
|
maxFrequentRows,
|
|
perLine,
|
|
navPosition,
|
|
set,
|
|
locale,
|
|
autoFocus,
|
|
}
|
|
if (searchPosition !== undefined) pickerProps.searchPosition = searchPosition
|
|
if (dynamicWidth !== undefined) pickerProps.dynamicWidth = dynamicWidth
|
|
if (noCountryFlags !== undefined) pickerProps.noCountryFlags = noCountryFlags
|
|
|
|
const el = new Picker(pickerProps)
|
|
pickerRef.current = el
|
|
hostRef.current.replaceChildren(el)
|
|
}
|
|
})
|
|
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
// Run once on mount. Callbacks stay fresh via refs; static display options
|
|
// (theme, perLine, etc.) don't change during a single picker session.
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (hostRef.current) {
|
|
hostRef.current.replaceChildren()
|
|
}
|
|
|
|
pickerRef.current = null
|
|
}
|
|
}, [])
|
|
|
|
return <div ref={hostRef} className={className} />
|
|
} |