Add emoji search by name

A search box in EmojiPicker filters the flat emoji list by name/keyword
instead of only browsing by category, shared by both the composer's
emoji trigger and the message reaction picker.

Names/keywords didn't exist anywhere in this codebase before (emoji.ts is
just raw unicode characters). Rather than hand-typing entries for all 928
unique emoji -- a lot of manual work, and unlike the codepoints themselves
a wrong name only means a bad search result, not a broken emoji, so the
accuracy argument for hand-typing didn't apply here -- generated
emojiNames.ts once from emojibase-data (MIT licensed), matched against
emoji.ts by codepoint (normalizing variation-selector differences between
the two sources). emojibase-data itself was never added to package.json;
it was only ever a one-time generation tool, so this ships with zero new
runtime dependency.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ksmithandClaude Sonnet 5 committed 2026-08-16 09:04:59 -06:00
1 parent caeff5d6f0
commit 9c275b1b86
3 files changed
+1027 -7

No files matched your search

+60 -7
View File
@@ -1,5 +1,7 @@
import { useMemo, useState } from 'react'
import { useEscapeKey } from '../hooks/useEscapeKey'
import { EMOJI_CATEGORIES } from '../lib/emoji'
import { ALL_EMOJI, EMOJI_CATEGORIES } from '../lib/emoji'
import { EMOJI_NAMES } from '../lib/emojiNames'
import './EmojiPicker.css'
interface EmojiPickerProps {
@@ -9,8 +11,29 @@ interface EmojiPickerProps {
align?: 'left' | 'right'
}
function searchEmoji(query: string): string[] {
const q = query.trim().toLowerCase()
if (!q) return []
const seen = new Set<string>()
const results: string[] = []
for (const emoji of ALL_EMOJI) {
if (seen.has(emoji)) continue
const entry = EMOJI_NAMES[emoji]
if (!entry) continue
const matches = entry.name.toLowerCase().includes(q) || entry.keywords.some((k) => k.toLowerCase().includes(q))
if (matches) {
seen.add(emoji)
results.push(emoji)
}
}
return results
}
export function EmojiPicker({ onPick, onClose, placement = 'below', align = 'left' }: EmojiPickerProps) {
useEscapeKey(onClose)
const [query, setQuery] = useState('')
const searchResults = useMemo(() => searchEmoji(query), [query])
const searching = query.trim().length > 0
return (
<>
@@ -19,24 +42,54 @@ export function EmojiPicker({ onPick, onClose, placement = 'below', align = 'lef
className={`emoji-picker emoji-picker-${placement} emoji-picker-${align}`}
role="menu"
>
{EMOJI_CATEGORIES.map((category) => (
<div key={category.label} className="emoji-picker-category">
<div className="emoji-picker-category-label">{category.label}</div>
<input
type="text"
className="emoji-picker-search"
placeholder="Search emoji"
value={query}
onChange={(e) => setQuery(e.target.value)}
autoFocus
/>
{searching ? (
searchResults.length > 0 ? (
<div className="emoji-picker-grid">
{category.emoji.map((emoji) => (
{searchResults.map((emoji) => (
<button
key={emoji}
type="button"
role="menuitem"
className="emoji-picker-item"
title={EMOJI_NAMES[emoji]?.name}
onClick={() => onPick(emoji)}
>
{emoji}
</button>
))}
</div>
</div>
))}
) : (
<div className="emoji-picker-no-results">No emoji found</div>
)
) : (
EMOJI_CATEGORIES.map((category) => (
<div key={category.label} className="emoji-picker-category">
<div className="emoji-picker-category-label">{category.label}</div>
<div className="emoji-picker-grid">
{category.emoji.map((emoji) => (
<button
key={emoji}
type="button"
role="menuitem"
className="emoji-picker-item"
title={EMOJI_NAMES[emoji]?.name}
onClick={() => onPick(emoji)}
>
{emoji}
</button>
))}
</div>
</div>
))
)}
</div>
</>
)