Skip to content

Commit d8c2ff0

Browse files
committed
feat(a11y): bulk select-all / unselect / clear-selection actions
Add selection controls to the summary bar: a Select all / Unselect all toggle (over the currently-visible, filtered violations) and a Clear selection action (clears the whole selection, including any hidden by the filter) with a live count. Selecting drives the in-page highlight + the fix-prompt set as before. Co-authored-with an agent.
1 parent b35e851 commit d8c2ff0

3 files changed

Lines changed: 77 additions & 3 deletions

File tree

plugins/a11y/src/spa/app.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,36 @@ export function App() {
140140
return out
141141
})
142142

143+
// Keys of the currently-visible (filtered) violations, for bulk selection.
144+
const visibleKeys = createMemo(() =>
145+
groups().flatMap(g => g.violations.map(v => selKey(g.report.route, v.ruleId))))
146+
const allVisibleSelected = createMemo(() => {
147+
const keys = visibleKeys()
148+
return keys.length > 0 && keys.every(k => selected().has(k))
149+
})
150+
151+
// Select all / unselect all — toggles the whole visible set at once.
152+
function toggleSelectAll() {
153+
const keys = visibleKeys()
154+
if (keys.length === 0)
155+
return
156+
const allSel = keys.every(k => selected().has(k))
157+
setSelected((prev) => {
158+
const next = new Set(prev)
159+
for (const k of keys) {
160+
if (allSel)
161+
next.delete(k)
162+
else
163+
next.add(k)
164+
}
165+
return next
166+
})
167+
}
168+
// Clear the entire selection, including any hidden by the current filter.
169+
function clearSelection() {
170+
setSelected(new Set<string>())
171+
}
172+
143173
const selectionApi: SelectionApi = {
144174
isSelected: (route, ruleId) => selected().has(selKey(route, ruleId)),
145175
toggle: (route, ruleId) => {
@@ -330,6 +360,10 @@ export function App() {
330360
totalNodes={totalNodes()}
331361
totalRules={totalRules()}
332362
routeCount={routes().length}
363+
selectedCount={selectedItems().length}
364+
allSelected={allVisibleSelected()}
365+
onToggleSelectAll={toggleSelectAll}
366+
onClearSelection={clearSelection}
333367
autoScan={autoScan()}
334368
onToggleAutoScan={setAutoScan}
335369
showBestPractice={showBestPractice()}

plugins/a11y/src/spa/components/SummaryBar.stories.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ const base = {
2121
totalNodes: 18,
2222
totalRules: 7,
2323
routeCount: 3,
24+
selectedCount: 0,
25+
allSelected: false,
26+
onToggleSelectAll: noop,
27+
onClearSelection: noop,
2428
autoScan: true,
2529
onToggleAutoScan: noop,
2630
showBestPractice: true,
@@ -30,3 +34,5 @@ const base = {
3034

3135
export const Overview: Story = { args: base }
3236
export const Filtered: Story = { args: { ...base, filter: 'serious' } }
37+
export const WithSelection: Story = { args: { ...base, selectedCount: 4 } }
38+
export const AllSelected: Story = { args: { ...base, selectedCount: 7, allSelected: true } }

plugins/a11y/src/spa/components/SummaryBar.tsx

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Impact } from '../../shared/protocol.ts'
2+
import { Show } from 'solid-js'
23
import { Summary } from './Summary.tsx'
34
import { Switch } from './Switch.tsx'
45

@@ -10,25 +11,35 @@ interface SummaryBarProps {
1011
totalNodes: number
1112
totalRules: number
1213
routeCount: number
14+
/** Number of violations currently selected. */
15+
selectedCount: number
16+
/** Whether every currently-visible violation is selected. */
17+
allSelected: boolean
18+
/** Select all visible violations, or unselect them when all are selected. */
19+
onToggleSelectAll: () => void
20+
/** Clear the entire selection (including any hidden by the filter). */
21+
onClearSelection: () => void
1322
autoScan: boolean
1423
onToggleAutoScan: (enabled: boolean) => void
1524
showBestPractice: boolean
1625
onToggleBestPractice: (show: boolean) => void
1726
onClearAll: () => void
1827
}
1928

29+
const ACTION = 'inline-flex items-center gap-1.5 text-xs color-muted bg-secondary border border-base rounded-md px-2.5 py-1 cursor-pointer transition hover:bg-active outline-none focus-visible:ring-2 focus-visible:ring-primary-500/40 disabled:op-40 disabled:cursor-default'
30+
2031
/**
2132
* The compact, sticky summary band that heads the single-page panel: the
22-
* severity chips (doubling as the impact filter), a one-line count, and the
23-
* scan / best-practice / clear controls.
33+
* severity chips (doubling as the impact filter), a one-line count, bulk
34+
* selection actions, and the scan / best-practice / clear controls.
2435
*/
2536
export function SummaryBar(props: SummaryBarProps) {
2637
const plural = (n: number, one: string) => `${n} ${n === 1 ? one : `${one}s`}`
2738
return (
2839
<div class="flex flex-col gap-2 pt-3 pb-2.5 sticky top-0 z-[2] bg-base">
2940
<Summary counts={props.counts} active={props.filter} onToggle={props.onToggleFilter} onHover={props.onHoverImpact} />
3041

31-
<div class="flex items-center gap-3 flex-wrap">
42+
<div class="flex items-center gap-2 flex-wrap">
3243
<span class="text-[11.5px] color-muted tabular-nums">
3344
{plural(props.totalNodes, 'issue')}
3445
{' · '}
@@ -37,6 +48,29 @@ export function SummaryBar(props: SummaryBarProps) {
3748
{plural(props.routeCount, 'route')}
3849
</span>
3950

51+
<button
52+
type="button"
53+
class={ACTION}
54+
onClick={() => props.onToggleSelectAll()}
55+
title={props.allSelected ? 'Unselect all visible violations' : 'Select all visible violations'}
56+
>
57+
<span aria-hidden class={`shrink-0 ${props.allSelected ? 'i-ph-check-square-duotone' : 'i-ph-square-duotone'}`} />
58+
{props.allSelected ? 'Unselect all' : 'Select all'}
59+
</button>
60+
61+
<Show when={props.selectedCount > 0}>
62+
<button
63+
type="button"
64+
class={ACTION}
65+
onClick={() => props.onClearSelection()}
66+
title="Clear the whole selection"
67+
>
68+
<span aria-hidden class="i-ph-x shrink-0" />
69+
Clear selection
70+
<span class="inline-flex items-center justify-center min-w-4 h-4 px-1 rounded-full bg-active text-[10px] font-bold tabular-nums">{props.selectedCount}</span>
71+
</button>
72+
</Show>
73+
4074
<span class="flex-1" />
4175

4276
<Switch label="Best-practice" checked={props.showBestPractice} onChange={props.onToggleBestPractice} />

0 commit comments

Comments
 (0)