From 4e4c47f6834a3900798592c4d3a39dc5131356fb Mon Sep 17 00:00:00 2001 From: d-oit <6849456+d-oit@users.noreply.github.com> Date: Fri, 25 Sep 2026 03:43:19 +0000 Subject: [PATCH 1/3] feat(ux): add topbar quick-filter clear button Improve topbar quick-filter search input UX and accessibility: - Change search input type to semantic type="search" - Add accessible clear search button (X) when query is non-empty - Support clearing active search query on Escape keypress - Add unit tests for clear button, type="search", and Escape key handler --- src/components/studio/topbar.test.tsx | 25 ++++++++++++++++++++++++- src/components/studio/topbar.tsx | 20 +++++++++++++++++--- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/components/studio/topbar.test.tsx b/src/components/studio/topbar.test.tsx index ab3b96f9..d1dedf86 100644 --- a/src/components/studio/topbar.test.tsx +++ b/src/components/studio/topbar.test.tsx @@ -5,7 +5,7 @@ vi.mock('lucide-react', () => { const Icon = ({ className }: { className?: string }) => ( ) - return { Menu: Icon, Plus: Icon, Search: Icon } + return { Menu: Icon, Plus: Icon, Search: Icon, X: Icon } }) vi.mock('@/lib/utils', () => ({ @@ -176,4 +176,27 @@ describe('Topbar', () => { render() expect(screen.getByLabelText('Search')).toBeDefined() }) + + it('renders input with type="search"', () => { + render() + const input = screen.getByLabelText('Search') as HTMLInputElement + expect(input.type).toBe('search') + }) + + it('renders clear search button when searchQuery is non-empty and handles click', () => { + searchQuery = 'test query' + render() + const clearButton = screen.getByLabelText('Clear search') + expect(clearButton).toBeDefined() + expect(clearButton.getAttribute('title')).toBe('Clear search') + fireEvent.click(clearButton) + expect(mockSetSearchQuery).toHaveBeenCalledWith('') + }) + + it('clears searchQuery on Escape key in search input when searchQuery is non-empty', () => { + searchQuery = 'test query' + render() + fireEvent.keyDown(screen.getByLabelText('Search'), { key: 'Escape' }) + expect(mockSetSearchQuery).toHaveBeenCalledWith('') + }) }) diff --git a/src/components/studio/topbar.tsx b/src/components/studio/topbar.tsx index 871e1c3b..7d96ecad 100644 --- a/src/components/studio/topbar.tsx +++ b/src/components/studio/topbar.tsx @@ -3,7 +3,7 @@ import { useStudioStore } from '@/lib/studio/store' import type { ViewId } from '@/lib/studio/types' import { translate as timelineT } from '@/lib/i18n/messages/timeline' -import { Menu, Plus, Search } from 'lucide-react' +import { Menu, Plus, Search, X } from 'lucide-react' import type { KeyboardEvent } from 'react' /** View title metadata keyed by ViewId (bounded Map retrieval — no dynamic indexing). */ @@ -46,6 +46,9 @@ export const Topbar = () => { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault() setCommandOpen(true) + } else if (e.key === 'Escape' && searchQuery) { + e.preventDefault() + setSearchQuery('') } } @@ -96,14 +99,25 @@ export const Topbar = () => { aria-hidden /> setSearchQuery(e.target.value)} onKeyDown={handleSearchKeyDown} placeholder={placeholder} aria-label={inputAriaLabel} - className="min-h-[44px] w-full rounded-md border border-border bg-background pl-9 pr-12 text-[13px] text-ink placeholder:text-ink-faint transition-colors focus:border-saffron focus:outline-none focus:ring-2 focus:ring-saffron/40" + className="min-h-[44px] w-full rounded-md border border-border bg-background pl-9 pr-20 text-[13px] text-ink placeholder:text-ink-faint transition-colors focus:border-saffron focus:outline-none focus:ring-2 focus:ring-saffron/40" /> + {searchQuery ? ( + + ) : null}