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}