fix(dashboard): make FieldStatusCard rows keyboard-accessible [Phase 2f]#636
Merged
barach6662001-bit merged 2 commits intomainfrom Apr 24, 2026
Merged
Conversation
Closes the last low-risk plain <div onClick> row finding from the
Phase 2e clickable-elements audit (docs/accessibility/clickable-
elements-audit.md, Finding 1). Pattern is identical to Phases 2b /
2d (FieldCard, UpcomingPanel) — surgical paydown, no redesign.
Before
------
The header "Усі поля" CTA and the empty-state "Add field" CTA in
pages/Dashboard/components/FieldStatusCard were already real
<button> elements (verified during the Phase 2e audit pass — they
are listed in the false-positives appendix). The remaining debt
was the per-row clickable wrapper:
<div className={s.row} onClick={() => navigate('/fields')}>
{field name, area bar, crop tag, area text}
</div>
with cursor: pointer in the CSS module but no role, no tabIndex,
no keyDown handler, no accessible name. Keyboard users could not
reach or activate the row; screen readers heard the field name,
crop, and area as static text with no destination context.
After
-----
Each row gains the same FieldCard / UpcomingPanel contract:
* role="button" + tabIndex={0}
* onKeyDown handles Enter and Space
* Space calls preventDefault to suppress page scroll
* unrelated keys (Tab, Escape, Shift, ArrowDown, ...) are ignored
* aria-label summarises the row exactly as it reads visually:
"{fieldName}, {cropLabel}, {area} га"
* the decorative crop tag, area pill and progress bar are marked
aria-hidden because their content is duplicated in aria-label
(the bar is purely a visual indicator with no textual content
of its own)
* a token-driven :focus-visible ring (var(--brand), 2px, offset
2px) lives in the CSS module, paired with the existing
.row:hover background so keyboard and mouse focus look
identical (preserves hover/focus parity)
Notes / scope
-------------
Field-specific aria-labels are deliberate even though every row
navigates to the same /fields destination — assistive-tech users
still need to tell rows apart. This matches the recommendation in
the Phase 2e audit document.
There is no nested <button> inside the row, so the FieldCard
"decorative button → presentation span" step from Phase 2b does
not apply here.
Visual layout, the navigation target (/fields), the field
filter / sort / .slice(0, 6) logic, the empty-state behaviour
including the optional onAddField prop, and every i18n string are
preserved verbatim. The Card / Surface API is not touched, no
dependencies are added, no routes change, and no other
clickable-row debt is swept in this PR — WarehousesList,
NotificationBell, CommandPalette, AppLayout, Sidebar and the
legacy components/dashboard/* files remain untouched per the
explicit scope and per the audit's recommended phase ordering.
Tests
-----
New file: pages/Dashboard/components/__tests__/FieldStatusCard.test.tsx
17 cases across 4 groups:
empty state
- renders getStarted placeholder when fields=[]
- addField CTA calls onAddField prop when supplied
- addField CTA navigates to /fields when onAddField omitted
primary render
- one row per field with name / crop / area text
- caps the list at the first 6 fields
- falls back to notSeeded label when currentCrop is missing
accessibility
- aria-label = "{fieldName}, {cropLabel}, {area} га"
- aria-label uses notSeeded fallback when currentCrop missing
- tabIndex=0 (keyboard reachable)
- decorative crop tag / area pill / bar not exposed as buttons
activation
- click navigates to /fields
- Enter navigates
- Space navigates
- Space calls preventDefault (page scroll suppressed)
- Tab / Escape / Shift / ArrowDown do nothing
- header "Усі поля" button (real <button>) still navigates
Validation
----------
pnpm --filter frontend test → 116 / 116 (17 new + 99 existing)
npx tsc -b --noEmit → clean
npx eslint <changed files> → clean
npx vite build → green (✓ built in 31 s)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 2f — FieldStatusCard keyboard accessibility
Closes the last low-risk plain
<div onClick>row finding from the Phase 2e clickable-elements audit (Finding 1). Identical surgical pattern to #629 (FieldCard, Phase 2b) and #633 (UpcomingPanel, Phase 2d) — no redesign.Before
The header Усі поля CTA and the empty-state Додати поле CTA were already real
<button>s — verified in Phase 2e (they are listed in the audit's false-positives appendix). The remaining debt was the per-row wrapper:With
cursor: pointerin the CSS module but norole, notabIndex, noonKeyDown, no accessible name.After
Same contract as #629 / #633:
role="button"+tabIndex={0}+onKeyDown(Enter / Space +preventDefaulton Space)aria-labelmirrors the visible row content:"{fieldName}, {cropLabel}, {area} га"cropTag/area/barcarryaria-hidden="true"— their content is already inaria-label(the bar has no text of its own; it is a pure visual indicator):focus-visiblering in the CSS module (outline: 2px solid var(--brand); outline-offset: 2px;) paired with the existing.row:hoverbackground, so keyboard and mouse focus look identicalWhy field-specific labels for a shared destination
Every row navigates to the same
/fieldspage, but assistive-tech users still need to tell rows apart. This deliberate decision matches the explicit recommendation in the audit.Why no "decorative button → span" step
There is no nested
<button>inside the row. The header Усі поля CTA lives outside the row tree. So the FieldCard / Phase 2b conversion step does not apply here.Scope (per request)
In: FieldStatusCard.tsx, FieldStatusCard.module.css, new FieldStatusCard.test.tsx.
Out: WarehousesList, NotificationBell, CommandPalette, AppLayout, Sidebar, legacy
components/dashboard/*, Card / Surface API. No new dependencies, no route changes, no API or business-logic changes, no redesign. Audit phase order preserved.Tests (17, new file)
addFieldcallsonAddFieldprop when supplied; falls back to navigate when prop omittednotSeededfallback whencurrentCropis missingaria-labelformat with crop; withnotSeededfallback;tabIndex=0; decorative chips/bar not exposed as buttonspreventDefault; ignored keys; header Усі поля button (real<button>) still worksValidation
pnpm --filter frontend test→ 116 / 116 (17 new + 99 existing)npx tsc -b --noEmit→ cleannpx eslinton changed files → cleannpx vite build→ green (✓ built in 31 s)