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:
2026-08-16 09:04:59 -06:00
co-authored by Claude Sonnet 5
parent caeff5d6f0
commit 9c275b1b86
3 changed files with 1027 additions and 7 deletions
+25
View File
@@ -35,6 +35,31 @@
right: 0;
}
.emoji-picker-search {
position: sticky;
top: 0;
z-index: 1;
background: var(--ds-surface-2);
border: 1px solid var(--ds-border);
border-radius: var(--radius);
padding: 6px 10px;
color: var(--ds-text);
font-size: 0.82rem;
outline: none;
flex: none;
}
.emoji-picker-search:focus {
border-color: var(--ds-accent);
}
.emoji-picker-no-results {
font-size: 0.82rem;
color: var(--ds-muted);
padding: var(--sp-3) var(--sp-2);
text-align: center;
}
.emoji-picker-category-label {
font-size: 0.7rem;
font-weight: 700;
+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>
</>
)