Files
SkinbaseNova/resources/js/components/common/EmojiMartPicker.jsx

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} />
}