Skip to content

Commit 4986a9b

Browse files
committed
fix: address CodeRabbit review - keyboard accessibility and type validation
- Add role, tabIndex, and onKeyDown to CookbookCard for keyboard nav - Wrap filter chips in <button> with aria-pressed for accessibility - Handle unknown type query param values explicitly (filter returns empty) - Add test for invalid type filter value
1 parent 2c0aaaf commit 4986a9b

3 files changed

Lines changed: 28 additions & 9 deletions

File tree

frontend/src/features/cookbook/components/catalogue-grid.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,16 @@ function CookbookCard({ item }: { item: CookbookItem }) {
3737

3838
return (
3939
<Card
40-
className="cursor-pointer transition-colors hover:border-primary/50"
40+
role="link"
41+
tabIndex={0}
42+
className="cursor-pointer transition-colors hover:border-primary/50 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none"
4143
onClick={() => navigate(`/cookbook/${item.name}`)}
44+
onKeyDown={(e) => {
45+
if (e.key === 'Enter' || e.key === ' ') {
46+
e.preventDefault()
47+
navigate(`/cookbook/${item.name}`)
48+
}
49+
}}
4250
>
4351
<CardHeader>
4452
<div className="flex items-start justify-between gap-2">

frontend/src/features/cookbook/components/filter-bar.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,9 @@ describe('applyFilters', () => {
117117
expect(result).toHaveLength(1)
118118
expect(result[0].name).toBe('energy-trading')
119119
})
120+
121+
it('filters out all items for unknown type value', () => {
122+
const result = applyFilters(mockItems, { ...emptyFilters, type: 'invalid' })
123+
expect(result).toHaveLength(0)
124+
})
120125
})

frontend/src/features/cookbook/components/filter-bar.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,9 @@ export function useFilterState(): [FilterState, (patch: Partial<FilterState>) =>
6363
export function applyFilters(items: CookbookItem[], filters: FilterState): CookbookItem[] {
6464
return items.filter((item) => {
6565
if (filters.type) {
66-
const typeLabel = filters.type === 'pattern' ? 'registry:pattern' : 'registry:ui'
67-
if (item.type !== typeLabel) return false
66+
const typeMap: Record<string, string> = { pattern: 'registry:pattern', ui: 'registry:ui' }
67+
const typeLabel = typeMap[filters.type]
68+
if (!typeLabel || item.type !== typeLabel) return false
6869
}
6970

7071
if (filters.category) {
@@ -163,17 +164,22 @@ interface FilterChipGroupProps {
163164

164165
function FilterChipGroup({ label, options, value, onChange }: FilterChipGroupProps) {
165166
return (
166-
<div className="flex items-center gap-1">
167+
<div className="flex items-center gap-1" role="group" aria-label={`${label} filter`}>
167168
<span className="text-xs text-muted-foreground mr-1">{label}:</span>
168169
{options.map((opt) => (
169-
<Badge
170+
<button
170171
key={opt.value}
171-
variant={value === opt.value ? 'default' : 'outline'}
172-
className="cursor-pointer text-xs"
172+
type="button"
173+
aria-pressed={value === opt.value}
173174
onClick={() => onChange(value === opt.value ? '' : opt.value)}
174175
>
175-
{opt.label}
176-
</Badge>
176+
<Badge
177+
variant={value === opt.value ? 'default' : 'outline'}
178+
className="cursor-pointer text-xs"
179+
>
180+
{opt.label}
181+
</Badge>
182+
</button>
177183
))}
178184
</div>
179185
)

0 commit comments

Comments
 (0)