elements carrying no tabIndex, no key handler, and no ARIA role. They cannot be reached or activated with a keyboard, and assistive technology does not announce them as interactive.
The Dashboard otherwise takes accessibility seriously — the tree carries 740 aria-* and 269 role= attributes, and the same files use real elements a few lines away. These fourteen look like drift rather than a decision, and nothing currently prevents the next one: dashboard/frontend/eslint.config.js does not include eslint-plugin-jsx-a11y, so click-events-have-key-events and no-static-element-interactions never run.
Collapsible section headers (7). A keyboard user cannot expand or collapse these regions at all:
- src/components/ChatComponentWebToolCards.tsx:39
- src/components/ChatComponentWebToolCards.tsx:167
- src/components/ExpressionBuilderToolbox.tsx:30
- src/pages/builderPageDashboardViews.tsx:29
- src/pages/builderPageValidationPanel.tsx:32
- src/pages/builderPageGlobalSettingsSafetySection.tsx:30
- src/pages/topology/components/CustomNodes/DecisionNode.tsx:99
Expression builder context-menu items (7). Add-child, wrap AND/OR/NOT, unwrap, and change-operator are keyboard-unreachable, so nested decision rules cannot be edited without a mouse:
- src/components/ExpressionBuilderContextMenu.tsx:60,64,67,70,101,108,113
Two further hits at src/pages/topology/components/ResultCard/ResultCard.tsx:35-36 are the conventional modal-backdrop click target. They are listed for completeness and are not part of the proposed fix.
To Reproduce
Verified statically against current main. From dashboard/frontend/src:
grep -rn '<div[^>]*onClick' --include='*.tsx' . | grep -v '\.test\.'
Twenty sites are reported. For each of the fourteen listed above, the element's own tag carries no tabIndex, no onKeyDown / onKeyUp / onKeyPress, and no role, so no keyboard event can reach the handler.
The remaining six are all modal-overlay backdrops. Four declare a role (ViewModal.tsx:34, ClawRoomTeamDetailsModal.tsx:56, DslImportModal.tsx:37, UsersPageUserDialog.tsx:86); the two ResultCard.tsx lines do not, which is worth noting only as the same drift in a place where the backdrop pattern itself is fine.
Expected behavior
Controls that perform an action are reachable and operable from the keyboard and exposed with the right role:
with aria-expanded bound to the collapsed state and aria-controls naming the region they toggle.
Context-menu items become inside a role="menu" container, so the existing menu is operable with arrow keys, Enter, and Escape.
eslint-plugin-jsx-a11y is added to dashboard/frontend/eslint.config.js with at least click-events-have-key-events and no-static-element-interactions enabled, so a new static-element handler fails lint instead of relying on review.
Impact
Keyboard-only and screen-reader operators cannot expand the Builder validation panel, safety settings, sidebar sections, web-tool result cards, or decision-node rules, and cannot edit nested decision rules through the expression builder at all. Because these regions gate configuration editing, the affected users lose Dashboard functionality rather than only convenience.
There is also a testing cost. The Playwright suite addresses the UI through getByRole: all 27 specs under dashboard/frontend/e2e use it, 676 times in total. A control with no role cannot be targeted by the repository's own established selector strategy, so these fourteen sites are structurally hard to cover in E2E — the accessibility gap and the coverage gap are the same gap.
Additional context
Scope is dashboard/frontend only; no backend, router, or config contract is touched.
The lint rule is what keeps the class closed, so I would rather add the gate and fix the existing violations together than land either half alone. make dashboard-check already runs dashboard-lint, and dashboard-test.yml already runs make dashboard-check, so the rule enters CI enforcement without any new workflow or job.
If the Workgroup prefers a narrower first change, the rules can start at warn with the fourteen sites fixed, then be promoted to error once the tree is clean.
I am happy to implement this if the Workgroup accepts it.
Proposed Workgroup
Developer Experience & Ecosystem
Describe the bug
Fourteen interactive controls in dashboard/frontend/src are plain
The Dashboard otherwise takes accessibility seriously — the tree carries 740 aria-* and 269 role= attributes, and the same files use real elements a few lines away. These fourteen look like drift rather than a decision, and nothing currently prevents the next one: dashboard/frontend/eslint.config.js does not include eslint-plugin-jsx-a11y, so click-events-have-key-events and no-static-element-interactions never run.
Collapsible section headers (7). A keyboard user cannot expand or collapse these regions at all:
Expression builder context-menu items (7). Add-child, wrap AND/OR/NOT, unwrap, and change-operator are keyboard-unreachable, so nested decision rules cannot be edited without a mouse:
Two further hits at src/pages/topology/components/ResultCard/ResultCard.tsx:35-36 are the conventional modal-backdrop click target. They are listed for completeness and are not part of the proposed fix.
To Reproduce
Verified statically against current main. From dashboard/frontend/src:
grep -rn '<div[^>]*onClick' --include='*.tsx' . | grep -v '\.test\.'Twenty sites are reported. For each of the fourteen listed above, the element's own tag carries no tabIndex, no onKeyDown / onKeyUp / onKeyPress, and no role, so no keyboard event can reach the handler.
The remaining six are all modal-overlay backdrops. Four declare a role (ViewModal.tsx:34, ClawRoomTeamDetailsModal.tsx:56, DslImportModal.tsx:37, UsersPageUserDialog.tsx:86); the two ResultCard.tsx lines do not, which is worth noting only as the same drift in a place where the backdrop pattern itself is fine.
Expected behavior
Controls that perform an action are reachable and operable from the keyboard and exposed with the right role:
- Toggle headers become
with aria-expanded bound to the collapsed state and aria-controls naming the region they toggle.Impact
Keyboard-only and screen-reader operators cannot expand the Builder validation panel, safety settings, sidebar sections, web-tool result cards, or decision-node rules, and cannot edit nested decision rules through the expression builder at all. Because these regions gate configuration editing, the affected users lose Dashboard functionality rather than only convenience.
There is also a testing cost. The Playwright suite addresses the UI through getByRole: all 27 specs under dashboard/frontend/e2e use it, 676 times in total. A control with no role cannot be targeted by the repository's own established selector strategy, so these fourteen sites are structurally hard to cover in E2E — the accessibility gap and the coverage gap are the same gap.
Additional context
Scope is dashboard/frontend only; no backend, router, or config contract is touched.
The lint rule is what keeps the class closed, so I would rather add the gate and fix the existing violations together than land either half alone. make dashboard-check already runs dashboard-lint, and dashboard-test.yml already runs make dashboard-check, so the rule enters CI enforcement without any new workflow or job.
If the Workgroup prefers a narrower first change, the rules can start at warn with the fourteen sites fixed, then be promoted to error once the tree is clean.
I am happy to implement this if the Workgroup accepts it.