✨ EmojiPicker component (#10249)

This commit is contained in:
Samuel Newman
2026-04-17 02:42:10 -07:00
committed by GitHub
parent 8f56fca82c
commit a97b15b204
17 changed files with 397 additions and 479 deletions
@@ -1,4 +1,4 @@
import {useCallback, useEffect, useRef, useState} from 'react'
import {useCallback, useRef, useState} from 'react'
import {Pressable, View} from 'react-native'
import {useLingui} from '@lingui/react/macro'
import {flushSync} from 'react-dom'
@@ -11,13 +11,9 @@ import {
useMessageDraft,
useSaveMessageDraft,
} from '#/state/messages/message-drafts'
import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter'
import {
type Emoji,
type EmojiPickerPosition,
} from '#/view/com/composer/text-input/web/EmojiPicker'
import {atoms as a, flatten, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import * as EmojiPicker from '#/components/EmojiPicker'
import {useSharedInputStyles} from '#/components/forms/TextField'
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
@@ -30,13 +26,11 @@ export function MessageInput({
hasEmbed,
setEmbed,
children,
openEmojiPicker,
}: {
onSendMessage: (message: string) => void
hasEmbed: boolean
setEmbed: (embedUrl: string | undefined) => void
children?: React.ReactNode
openEmojiPicker?: (pos: EmojiPickerPosition) => void
}) {
const {isMobile} = useWebMediaQueries()
const {t: l} = useLingui()
@@ -104,12 +98,11 @@ export function MessageInput({
}, [])
const onEmojiInserted = useCallback(
(emoji: Emoji) => {
(emoji: EmojiPicker.Emoji) => {
if (!textAreaRef.current) {
return
}
const position = textAreaRef.current.selectionStart ?? 0
textAreaRef.current.focus()
flushSync(() => {
setMessage(
message =>
@@ -121,12 +114,6 @@ export function MessageInput({
},
[setMessage],
)
useEffect(() => {
textInputWebEmitter.addListener('emoji-inserted', onEmojiInserted)
return () => {
textInputWebEmitter.removeListener('emoji-inserted', onEmojiInserted)
}
}, [onEmojiInserted])
useSaveMessageDraft(message)
useExtractEmbedFromFacets(message, setEmbed)
@@ -152,49 +139,45 @@ export function MessageInput({
// @ts-expect-error web only
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}>
<Button
onPress={e => {
e.currentTarget.measure((_fx, _fy, _width, _height, px, py) => {
openEmojiPicker?.({
top: py,
left: px,
right: px,
bottom: py,
nextFocusRef:
textAreaRef as unknown as React.MutableRefObject<HTMLElement>,
})
})
}}
style={[
a.rounded_full,
a.overflow_hidden,
a.align_center,
a.justify_center,
{
marginTop: 5,
height: 30,
width: 30,
},
]}
label={l`Open emoji picker`}>
{state => (
<View
style={[
a.absolute,
a.inset_0,
a.align_center,
a.justify_center,
{
backgroundColor:
state.hovered || state.focused || state.pressed
? t.atoms.bg.backgroundColor
: undefined,
},
]}>
<EmojiSmile size="lg" />
</View>
)}
</Button>
<EmojiPicker.Root
onEmojiSelect={onEmojiInserted}
nextFocusRef={textAreaRef}>
<EmojiPicker.Trigger label={l`Open emoji picker`}>
{({props, state}) => (
<Button
style={[
a.rounded_full,
a.overflow_hidden,
a.align_center,
a.justify_center,
{
marginTop: 5,
height: 30,
width: 30,
},
]}
label={props.accessibilityLabel}
{...props}>
<View
style={[
a.absolute,
a.inset_0,
a.align_center,
a.justify_center,
{
backgroundColor:
state.hovered || state.focused || state.pressed
? t.atoms.bg.backgroundColor
: undefined,
},
]}>
<EmojiSmile size="lg" />
</View>
</Button>
)}
</EmojiPicker.Trigger>
<EmojiPicker.Picker />
</EmojiPicker.Root>
<TextareaAutosize
ref={textAreaRef}
style={flatten([