From 47541be326a75456bd3c603d22e77084467ac405 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 00:49:11 +0000 Subject: [PATCH] Fix tag input swallowing typed names that prefix-match an existing tag Enter and comma committed the auto-highlighted first suggestion even when the user had not moved into the menu, so a typed name could never be added if it matched an existing tag. Only an explicit arrow/hover selection now picks a suggestion, and comma always commits what was typed. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_015E2NeQgbnTVbvdpNyQ18no --- frontend/src/components/TagInput.test.tsx | 11 +++++++++++ frontend/src/components/TagInput.tsx | 17 +++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/TagInput.test.tsx b/frontend/src/components/TagInput.test.tsx index c04792d..a355398 100644 --- a/frontend/src/components/TagInput.test.tsx +++ b/frontend/src/components/TagInput.test.tsx @@ -100,4 +100,15 @@ describe('TagInput', () => { await user.keyboard('{ArrowDown}{Enter}') expect(onAdd).toHaveBeenCalledWith(['beta']) }) + + it('commits what was typed, not the auto-highlighted suggestion, on Enter or comma', async () => { + const { onAdd, user } = setup({ + suggestions: [tag('t1', 'Fourth Industrial Revolution')], + }) + const input = screen.getByRole('combobox') + await user.type(input, 'Industrial{Enter}') + expect(onAdd).toHaveBeenLastCalledWith(['Industrial']) + await user.type(input, 'Industrial,') + expect(onAdd).toHaveBeenLastCalledWith(['Industrial']) + }) }) diff --git a/frontend/src/components/TagInput.tsx b/frontend/src/components/TagInput.tsx index eb97abd..8beb4e6 100644 --- a/frontend/src/components/TagInput.tsx +++ b/frontend/src/components/TagInput.tsx @@ -43,6 +43,10 @@ export default function TagInput({ const [draft, setDraft] = useState('') const [open, setOpen] = useState(false) const [active, setActive] = useState(0) + // Whether the keyboard has moved into the menu. Without it, the first suggestion is + // always "highlighted", so typing "Industrial" and pressing Enter would silently + // swap in "Fourth Industrial Revolution" and the typed name could never be added. + const [navigated, setNavigated] = useState(false) const inputRef = useRef(null) const attached = useMemo(() => new Set(tags.map((t) => t.name.toLowerCase())), [tags]) @@ -69,12 +73,15 @@ export default function TagInput({ onAdd([name]) setDraft('') setActive(0) + setNavigated(false) } const onKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'Enter' || event.key === ',') { event.preventDefault() - const chosen = open && matches[active] ? matches[active].name : draft + // A comma is a delimiter for what was typed; only Enter can pick a suggestion. + const picked = event.key === 'Enter' && navigated && open && matches[active] + const chosen = picked ? matches[active].name : draft commit(chosen) return } @@ -87,11 +94,13 @@ export default function TagInput({ if (event.key === 'ArrowDown') { event.preventDefault() setOpen(true) + setNavigated(true) setActive((i) => Math.min(i + 1, matches.length - 1)) return } if (event.key === 'ArrowUp') { event.preventDefault() + setNavigated(true) setActive((i) => Math.max(i - 1, 0)) return } @@ -147,6 +156,7 @@ export default function TagInput({ setDraft(e.target.value) setOpen(true) setActive(0) + setNavigated(false) }} onFocus={() => setOpen(true)} // A timeout, not a plain blur handler: clicking a suggestion blurs the @@ -168,7 +178,10 @@ export default function TagInput({ type="button" role="option" aria-selected={index === active} - onMouseEnter={() => setActive(index)} + onMouseEnter={() => { + setActive(index) + setNavigated(true) + }} onClick={() => commit(tag.name)} className={`flex w-full items-center justify-between gap-2 px-3 py-1.5 text-left text-xs ${ index === active