import { useId, useMemo, useState } from 'react' import { Input } from '@/components/ui/input' import { cn } from '@/lib/utils' import { isEmoji } from '../utils/emoji' import { ICON_OPTIONS } from '../utils/iconRegistry' const MAX_ICON_RESULTS = 24 function isHttpUrl(value: string): boolean { try { const url = new URL(value) return url.protocol === 'http:' || url.protocol === 'https:' } catch { return false } } function normalizeIconQuery(query: string): string { return query.trim().toLowerCase() } function matchesIconQuery(name: string, query: string): boolean { if (!query) return true const normalized = normalizeIconQuery(query) const spacedName = name.replace(/-/g, ' ') const dashedQuery = normalized.replace(/\s+/g, '-') return name.includes(dashedQuery) || spacedName.includes(normalized) } function filterIconOptions(query: string) { return ICON_OPTIONS .filter((option) => matchesIconQuery(option.name, query)) .slice(0, MAX_ICON_RESULTS) } function shouldSelectIconSuggestion(value: string, suggestionCount: number): boolean { const trimmed = value.trim() if (!trimmed) return false if (isEmoji(trimmed) || isHttpUrl(trimmed)) return false return suggestionCount > 0 } interface IconEditableValueProps { value: string onSave: (newValue: string) => void onCancel: () => void isEditing: boolean onStartEdit: () => void } function IconEditableInput({ value, onSave, onCancel, }: Pick) { const [editValue, setEditValue] = useState(value) const [highlightedIndex, setHighlightedIndex] = useState(0) const listboxId = useId() const filteredIcons = useMemo(() => filterIconOptions(editValue), [editValue]) const commitTypedValue = () => onSave(editValue) const selectIcon = (iconName: string) => { setEditValue(iconName) onSave(iconName) } const handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'ArrowDown') { event.preventDefault() if (filteredIcons.length === 0) return setHighlightedIndex((current) => (current + 1) % filteredIcons.length) return } if (event.key === 'ArrowUp') { event.preventDefault() if (filteredIcons.length === 0) return setHighlightedIndex((current) => (current - 1 + filteredIcons.length) % filteredIcons.length) return } if (event.key === 'Enter') { event.preventDefault() if (shouldSelectIconSuggestion(editValue, filteredIcons.length)) { selectIcon(filteredIcons.at(highlightedIndex)?.name ?? editValue) return } commitTypedValue() return } if (event.key === 'Escape') { event.preventDefault() onCancel() } } return (
{ setEditValue(event.target.value) setHighlightedIndex(0) }} onKeyDown={handleKeyDown} onBlur={commitTypedValue} autoFocus role="combobox" aria-autocomplete="list" aria-controls={listboxId} aria-expanded aria-activedescendant={filteredIcons.at(highlightedIndex) ? `${listboxId}-option-${highlightedIndex}` : undefined} placeholder="Emoji, icon name, or URL" className="h-8 text-[12px]" data-testid="icon-editable-input" />
{filteredIcons.length === 0 ? (
No icons found
) : ( filteredIcons.map(({ name, Icon }, index) => ( )) )}
) } export function IconEditableValue({ value, onSave, onCancel, isEditing, onStartEdit, }: IconEditableValueProps) { if (isEditing) { return } return ( {value || '\u2014'} ) }