Skip to content

[Bug] Dashboard collapsible headers and context menus are not keyboard reachable #3515

Description

@yuki-uix

Proposed Workgroup

Developer Experience & Ecosystem

Describe the bug

Fourteen interactive controls in dashboard/frontend/src are plain

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:

  • Toggle headers become
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.

    Activity

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Metadata

    Metadata

    Assignees

    No one assigned

      Labels

      acceptedAccepted by the accountable Workgroup or Maintainer; not necessarily scheduled.bugSomething isn't workingwg/developer-experience-ecosystemOwned by the Developer Experience and Ecosystem Workgroup.

      Type

      No type

      Projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions