Private
Public Access
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:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user