diff --git a/src/components/filters/FilterDropdown.tsx b/src/components/filters/FilterDropdown.tsx index 0ff5fa6c..8ac9069e 100644 --- a/src/components/filters/FilterDropdown.tsx +++ b/src/components/filters/FilterDropdown.tsx @@ -95,7 +95,7 @@ export function FilterDropdown(props: Readonly) { setActiveCategoryIndex(0); onOpen?.(); if (categories.length > 0) { - onCategorySelect?.(categories[0].label); + onCategorySelect?.(categories[0].id); } } else { onClose?.(); @@ -108,7 +108,7 @@ export function FilterDropdown(props: Readonly) { (index: number) => { if (index !== activeCategoryIndex) { setActiveCategoryIndex(index); - onCategorySelect?.(categories[index].label); + onCategorySelect?.(categories[index].id); } }, [activeCategoryIndex, categories, onCategorySelect], @@ -237,7 +237,7 @@ export function FilterDropdown(props: Readonly) { {categories.map((category, index) => ( handleCategoryClick(index)} ref={categoryRegister(index)} diff --git a/src/components/filters/FilterDropdownTypes.ts b/src/components/filters/FilterDropdownTypes.ts index 3ca14721..c63e2187 100644 --- a/src/components/filters/FilterDropdownTypes.ts +++ b/src/components/filters/FilterDropdownTypes.ts @@ -59,6 +59,11 @@ export interface FilterDropdownCategoryWithItems { * Use {@link FilterDropdownCategoryWithContent} to render custom content. */ content?: never; + /** + * Unique identifier for this category. + * Used as the React key for the category list and passed to `onCategorySelect`. + */ + id: string; /** * Whether items in this category support multi-selection (checkboxes). * When false, only one item can be selected at a time (radio buttons). @@ -107,6 +112,11 @@ export interface FilterDropdownCategoryWithContent { * and `onSearch` must not be provided. */ content: ReactNode; + /** + * Unique identifier for this category. + * Used as the React key for the category list and passed to `onCategorySelect`. + */ + id: string; /** * Not applicable when using custom content. * @see FilterDropdownCategoryWithItems @@ -218,7 +228,7 @@ export interface FilterDropdownProps { * Called every time the active category changes, including on initial open. * Use this to load or refresh items for the newly active category. */ - onCategorySelect?: (categoryLabel: string) => void; + onCategorySelect?: (categoryId: string) => void; /** * Called when the user clicks Clear. */ diff --git a/src/components/filters/__tests__/FilterDropdown-test.tsx b/src/components/filters/__tests__/FilterDropdown-test.tsx index 6416c296..a59d6056 100644 --- a/src/components/filters/__tests__/FilterDropdown-test.tsx +++ b/src/components/filters/__tests__/FilterDropdown-test.tsx @@ -24,6 +24,7 @@ import { FilterDropdown, FilterDropdownCategory, FilterDropdownProps } from '..' const CATEGORIES: FilterDropdownCategory[] = [ { + id: 'severity', isMultiSelect: true, label: 'Severity', items: [ @@ -33,6 +34,7 @@ const CATEGORIES: FilterDropdownCategory[] = [ ], }, { + id: 'type', isMultiSelect: true, label: 'Type', items: [ @@ -73,10 +75,10 @@ describe('FilterDropdown', () => { const { user } = renderFilterDropdown({ onCategorySelect }); await user.click(screen.getByRole('button', { name: 'Filters' })); - expect(onCategorySelect).toHaveBeenCalledWith('Severity'); + expect(onCategorySelect).toHaveBeenCalledWith('severity'); await user.click(screen.getByRole('option', { name: /type/i })); - expect(onCategorySelect).toHaveBeenCalledWith('Type'); + expect(onCategorySelect).toHaveBeenCalledWith('type'); await user.click(screen.getByRole('option', { name: /type/i })); expect(onCategorySelect).toHaveBeenCalledTimes(2); @@ -143,7 +145,7 @@ describe('FilterDropdown', () => { it('shows a loading spinner when category items have never been loaded', async () => { const { user } = renderFilterDropdown({ - categories: [{ label: 'Severity', items: undefined }], + categories: [{ id: 'severity', label: 'Severity', items: undefined }], }); await user.click(screen.getByRole('button', { name: 'Filters' })); @@ -164,6 +166,7 @@ describe('FilterDropdown', () => { const { user } = renderFilterDropdown({ categories: [ { + id: 'severity', isMultiSelect: false, label: 'Severity', items: [ @@ -199,12 +202,12 @@ describe('FilterDropdown', () => { onCategorySelect.mockClear(); await user.keyboard('{ArrowDown}'); - expect(onCategorySelect).toHaveBeenCalledWith('Type'); + expect(onCategorySelect).toHaveBeenCalledWith('type'); expect(screen.getByRole('option', { name: /type/i })).toHaveFocus(); onCategorySelect.mockClear(); await user.keyboard('{ArrowUp}'); - expect(onCategorySelect).toHaveBeenCalledWith('Severity'); + expect(onCategorySelect).toHaveBeenCalledWith('severity'); expect(screen.getByRole('option', { name: /severity/i })).toHaveFocus(); }); @@ -244,6 +247,7 @@ describe('FilterDropdown', () => { const { user } = renderFilterDropdown({ categories: [ { + id: 'severity', isMultiSelect, label: 'Severity', items: [ @@ -266,8 +270,9 @@ describe('FilterDropdown', () => { it('renders custom content in the right panel for a content category', async () => { const { user } = renderFilterDropdown({ categories: [ - { label: 'Severity', items: [{ label: 'High', value: 'high' }] }, + { id: 'severity', label: 'Severity', items: [{ label: 'High', value: 'high' }] }, { + id: 'date-range', label: 'Date Range', content: , onFocusContent: jest.fn(), @@ -286,8 +291,9 @@ describe('FilterDropdown', () => { const buttonRef = { current: null as HTMLButtonElement | null }; const { user } = renderFilterDropdown({ categories: [ - { label: 'Severity', items: [{ label: 'High', value: 'high' }] }, + { id: 'severity', label: 'Severity', items: [{ label: 'High', value: 'high' }] }, { + id: 'date-range', label: 'Date Range', content: (