Skip to content

fix: give the dashboard card and tile controls a keyboard (#1140) - #1145

Open
MOHITKOURAV01 wants to merge 1 commit into
Aditya8369:mainfrom
MOHITKOURAV01:fix/1140-keyboard-card-controls
Open

fix: give the dashboard card and tile controls a keyboard (#1140)#1145
MOHITKOURAV01 wants to merge 1 commit into
Aditya8369:mainfrom
MOHITKOURAV01:fix/1140-keyboard-card-controls

Conversation

@MOHITKOURAV01

Copy link
Copy Markdown
Contributor

Description

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 What the click does
AIPollutionForecast.jsx Selects a weather condition, which re-runs the forecast
HealthImpactDashboard.jsx Expands a pollutant card to reveal the health guidance
HealthRiskCards.jsx Expands a risk card to reveal affected-population detail
OceanAcidificationMonitor.jsx Selects an ocean region — drives the whole panel below
OceanAcidificationMonitor.jsx Selects a CO₂ scenario — drives the projection chart
ReportCards.jsx Expands a compliance card to reveal the breakdown

Three of them even render a ChevronDown that rotates on expand — the affordance is drawn,
and then wired to a pointer only.

WCAG 2.1 2.1.1 Keyboard (A) and, with no role, 4.1.2 Name, Role, Value (A). For an
app whose purpose is public health information, the collapsed detail is the health
guidance.


Related Issue

Closes #1140


Type of Change

  • Bug Fix
  • New Feature
  • Documentation
  • UI/UX Improvement
  • Refactoring
  • Performance Improvement
  • Accessibility

Changes Made

src/components/ui/PressableCard.jsx (new)DisclosureButton and SelectionButton.

Both render a real <button>, and that is the point: tab order, Enter and Space
activation, the button role, the focus ring and disabled handling all come from the
element, rather than from hand-rolled onKeyDown handlers that would have to be got right
five separate times and would still miss Space's keyup-activation semantics.

  • DisclosureButton carries aria-expanded and aria-controls — the part a <div> could
    never provide, since aria-expanded is what tells a screen reader user there is detail
    here at all and whether it is currently open.
  • SelectionButton carries aria-pressed rather than role="option". These are buttons
    that stay pressed, not a <select> drawn as cards; claiming listbox semantics would
    oblige the group to implement arrow-key navigation and aria-activedescendant, which is
    a bigger promise than any of these panels keeps. There is a test asserting no option
    or listbox role is claimed, so this doesn't get "upgraded" later by accident.
  • RESET_BUTTON_STYLE strips the user-agent button styling (background, border, padding,
    font, centred text). Callers spread their own style after it, so the existing look is
    unchanged at every site.

The six call sites now use those. Two details worth flagging in review:

  • In HealthImpactDashboard the disclosure panel moves out of the button and alongside it.
    Wrapping the expanded body in the button would work, but it would fold the entire card's
    text into the button's accessible name the moment it opened. There's a test for that.
  • Decorative glyphs inside the buttons (the ⚪/🔵 selection dot, the status icon, the
    chevron) get aria-hidden, so the accessible name is the pollutant or the region rather
    than the name plus a read-aloud emoji.

One thing outside the a11y scope, fixed because it was in the way:

const circumference = 2 * Math.PI(40);   // HealthScoreRing

Math.PI is a number, so calling it threw TypeError: Math.PI is not a function and took
HealthImpactDashboard down on render. The keyboard fix there can't be verified on a
component that can't mount, so it's fixed here (2 * Math.PI * 40, matching the
<circle r="40"> the dash array is drawn onto). Happy to split it out if you'd rather.


Testing

  • Tested locally
  • No console errors
  • Existing functionality works as expected
$ npx vitest run src/components/ui/PressableCard.test.jsx src/components/keyboardCardControls.test.jsx
 ✓ src/components/ui/PressableCard.test.jsx (15 tests)
 ✓ src/components/keyboardCardControls.test.jsx (8 tests)
 Test Files  2 passed (2)
      Tests  23 passed (23)

keyboardCardControls.test.jsx drives the real dashboards rather than the shared button, so
a component that merely imports PressableCard without using it would still fail. All 8
of those fail against the code before this change.

Lintnpm run lint goes from 22 errors to 10, and the 12 removed are exactly
these sites:

before:  ✖ 230 problems (22 errors, 208 warnings)
after:   ✖ 218 problems (10 errors, 208 warnings)

The 10 that remain are #1129's three parse errors, RouteForm (#1135),
VoiceAlertManager (#1136), Dashboard (#1133), and a jest reference in a Vitest file —
each already tracked elsewhere.

Two things I did not take on

  • RouteForm.jsx has the same class of error, but that one is Commute route form's location autocomplete is unusable by keyboard: suggestions are click-only and the ARIA is invalid #1135 and wants
    listbox / aria-activedescendant semantics rather than a button, so it needs its own
    change.
  • HealthRiskCards and ReportCards are not rendered in the tests. Both import
    framer-motion, which is not in package.json and is not installed, so they cannot be
    mounted at all in this repo today (Failed to resolve import "framer-motion"). Ten
    components are in that state. They still get the fix here — it's a source change that makes
    the controls real buttons and clears their lint errors — but I can't honestly claim a
    rendered test for them. That missing dependency looks worth its own issue; say the word
    and I'll open one.

Note on CI

Lint, Build and Playwright are red on main and on every open PR (npm run build fails on
#1129's parse errors). This branch reduces the lint error count but cannot make that job
green on its own.

…#1140)

Five dashboards implemented their primary controls as bare <div onClick>. A
mouse could operate them; nothing else could. No tab stop, no Enter or Space, no
role -- a WCAG 2.1.1 (Keyboard) failure and a 4.1.2 (Name, Role, Value) one. In
each case the clickable div was the feature: selecting a weather condition, an
ocean region or a CO2 scenario, or expanding a card to reveal the health
guidance behind it. Three of them even draw a rotating chevron and then wire it
to a pointer only.

src/components/ui/PressableCard.jsx adds DisclosureButton and SelectionButton.
Both render a real <button>, which is the point: tab order, Enter and Space
activation, the button role, focus ring and disabled handling come from the
element rather than from hand-rolled onKeyDown handlers that would have to be
got right five separate times. DisclosureButton carries aria-expanded and
aria-controls -- the part a div could never provide, since it is what tells a
screen reader user there is detail here at all. SelectionButton carries
aria-pressed rather than role="option": these are buttons that stay pressed, not
a listbox, and claiming listbox semantics would oblige arrow-key navigation and
aria-activedescendant that none of these panels implement.

RESET_BUTTON_STYLE strips the user-agent button styling, and callers spread
their own style after it so the existing look is unchanged.

Applied at all six sites. In HealthImpactDashboard the disclosure panel moves
out of the button and alongside it, so opening a card no longer folds its whole
body into the button's accessible name.

Also fixed, because it is in the way: HealthScoreRing computed
`2 * Math.PI(40)`. Math.PI is a number, so calling it threw
"TypeError: Math.PI is not a function" and took HealthImpactDashboard down on
render -- the keyboard fix there could not be verified on a component that
cannot mount.

npm run lint drops from 22 errors to 10; the 12 removed are exactly these
sites. The remaining 10 are Aditya8369#1129's three parse errors, RouteForm (Aditya8369#1135),
VoiceAlertManager (Aditya8369#1136), Dashboard (Aditya8369#1133) and a jest reference in a Vitest
file.

Tests: 15 for the shared buttons, 8 driving the three dashboards that can be
mounted. All 8 fail against the code before this change.
@vercel

vercel Bot commented Aug 29, 2026

Copy link
Copy Markdown

@MOHITKOURAV01 is attempting to deploy a commit to the Aditya Mahajan's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added the ECSoC26 Contributions considered under ECSoC'26 label Aug 29, 2026
@github-actions

Copy link
Copy Markdown

Thank You for Your Contribution! 🎉

Hi @MOHITKOURAV01,

Thank you for opening this Pull Request and contributing to our project. We truly appreciate your efforts.

Please make sure that:

  • Your code follows the project's guidelines.
  • You have linked the appropriate issue (if applicable).
  • Screenshots are added for UI/UX changes.
  • Your PR is ready for review.

The maintainer @Aditya8369 will review your PR shortly!

Happy Contributing! 🚀

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

Labels

ECSoC26 Contributions considered under ECSoC'26

Projects

None yet

1 participant