Five dashboards implement their primary controls as bare <div onClick={...}>. A mouse
can operate them. Nothing else can: they are not in the tab order, Enter and Space do
nothing, and a screen reader announces them as plain text with no hint that they do
anything at all.
These are not decorative elements — in each case the clickable div is the feature:
| File |
Line |
What the click does |
src/components/AIPollutionForecast.jsx |
1043 |
Selects a weather condition, which re-runs the forecast |
src/components/HealthImpactDashboard.jsx |
189 |
Expands a pollutant card to reveal the health guidance |
src/components/HealthRiskCards.jsx |
246 |
Expands a risk card to reveal affected-population detail |
src/components/OceanAcidificationMonitor.jsx |
362 |
Selects an ocean region — drives the entire panel below |
src/components/OceanAcidificationMonitor.jsx |
618 |
Selects a CO₂ scenario — drives the projection chart |
src/components/ReportCards.jsx |
138 |
Expands a compliance card to reveal the breakdown |
The HealthImpactDashboard, HealthRiskCards and ReportCards cards each render a
ChevronDown that rotates on expand — the affordance is drawn, and then only wired to a
pointer.
This is a WCAG 2.1 2.1.1 Keyboard (Level A) failure and, because there is no role, a
4.1.2 Name, Role, Value (Level A) failure too. For an app whose stated purpose is
public health information, the collapsed detail includes the actual health guidance.
The project's own ESLint config already flags every one of these as an error, not a
warning:
src/components/AIPollutionForecast.jsx
1043:13 error Visible, non-interactive elements with click handlers must have at
least one keyboard listener jsx-a11y/click-events-have-key-events
1043:13 error Avoid non-native interactive elements...
jsx-a11y/no-static-element-interactions
npm run lint exits 1 on main today, and these six sites are 12 of the 21 errors. The
Lint job in CI fails on every open pull request as a result, which means the gate is
providing no signal to anyone — a genuinely new lint error would look exactly like the
status quo.
Reproduce
- Open the Health Impact dashboard.
- Press Tab repeatedly.
- Focus never lands on any pollutant card; the health guidance behind them is unreachable.
npm run lint → ✖ 21 errors.
Expected
Each of these becomes a real control: reachable by Tab, operable with Enter/Space,
announced with a role and an accessible name, and — for the expandable cards —
aria-expanded reflecting state, so a screen reader user knows the detail is there and
whether it is open.
The three selector-style controls (weather condition, ocean region, CO₂ scenario) are
single-choice lists, so aria-pressed on a button is the honest mapping rather than
inventing a listbox.
Note on scope
This deliberately does not touch src/components/RouteForm.jsx, whose autocomplete has the
same class of error — that one is #1135 and needs listbox/aria-activedescendant
semantics rather than a button, so it wants its own change.
Five dashboards implement their primary controls as bare
<div onClick={...}>. A mousecan operate them. Nothing else can: they are not in the tab order, Enter and Space do
nothing, and a screen reader announces them as plain text with no hint that they do
anything at all.
These are not decorative elements — in each case the clickable div is the feature:
src/components/AIPollutionForecast.jsxsrc/components/HealthImpactDashboard.jsxsrc/components/HealthRiskCards.jsxsrc/components/OceanAcidificationMonitor.jsxsrc/components/OceanAcidificationMonitor.jsxsrc/components/ReportCards.jsxThe
HealthImpactDashboard,HealthRiskCardsandReportCardscards each render aChevronDownthat rotates on expand — the affordance is drawn, and then only wired to apointer.
This is a WCAG 2.1 2.1.1 Keyboard (Level A) failure and, because there is no role, a
4.1.2 Name, Role, Value (Level A) failure too. For an app whose stated purpose is
public health information, the collapsed detail includes the actual health guidance.
The project's own ESLint config already flags every one of these as an error, not a
warning:
npm run lintexits 1 onmaintoday, and these six sites are 12 of the 21 errors. TheLint job in CI fails on every open pull request as a result, which means the gate is
providing no signal to anyone — a genuinely new lint error would look exactly like the
status quo.
Reproduce
npm run lint→✖ 21 errors.Expected
Each of these becomes a real control: reachable by Tab, operable with Enter/Space,
announced with a role and an accessible name, and — for the expandable cards —
aria-expandedreflecting state, so a screen reader user knows the detail is there andwhether it is open.
The three selector-style controls (weather condition, ocean region, CO₂ scenario) are
single-choice lists, so
aria-pressedon a button is the honest mapping rather thaninventing a listbox.
Note on scope
This deliberately does not touch
src/components/RouteForm.jsx, whose autocomplete has thesame class of error — that one is #1135 and needs listbox/
aria-activedescendantsemantics rather than a button, so it wants its own change.