/backlog rows are role="row" inside a role="list" — two CRITICAL axe violations on every populated backlog, and neither role can do its job
Surfaced by MOTIR-2482 — the first sweep ever to load /backlog with axe, and the first to load it POPULATED. Filed rather than absorbed, per that card's own acceptance criterion: "any defect it surfaces that is NOT a --el-text-* contrast failure logged as its own bug rather than absorbed here."
Repo: motir-core. One PR.
The measurement
tests/e2e/shell-a11y-wide.spec.ts sweeps /backlog against a project with four ranked rows. axe (WCAG 2.1 A+AA, zero rule exclusions) reports two CRITICAL violations, both on every row:
aria-required-children— on the scroll viewport and on eachbacklog-row-*aria-required-parent— on eachbacklog-row-*
The cause — two roles that cannot compose
app/(authed)/backlog/_components/BacklogList.tsx:311 renders the scroll viewport as role="list" with an aria-label. app/(authed)/backlog/_components/BacklogRow.tsx:65 defaults rowRole = 'row', applied at line 116.
A list requires listitem children; a row requires a rowgroup / grid / table / treegrid parent. Neither holds, so BOTH fail and a screen reader gets neither a list it can count nor a grid it can navigate — the rows are announced with no container semantics at all.
The row component already KNOWS about the other arm: rowRole?: 'row' | 'listitem', and ProposedSprintPanel.tsx:254 passes rowRole="listitem" — the sprint-plan panel composes it correctly while the backlog itself does not.
The decision this card owes
Which semantic is right is a real choice, not a typo, and it should be made once and stated:
list+listitem— matchesProposedSprintPanel, matches the drag-to-rank interaction (an ordered list of items), and is the smaller diff (defaultrowRoleat theBacklogListcall site).grid/treegrid+rowgroup+row+gridcell— matches the/itemslist view, which already ships a realtreegridwitharia-level/aria-expanded(shell-a11y-detail.spec.tssweeps it), and would let the backlog's inline-edit cells be reachable by the grid keyboard model.
Read the backlog design asset under design/backlog/ before choosing; the row IS the drag handle (design panel 3), which the chosen model must not break.
Acceptance criteria
/backlogreports ZEROaria-required-childrenand ZEROaria-required-parentviolations under the sameWCAG_TAGSand zero rule exclusions the other sweeps use, on a POPULATED backlog.- The chosen semantic is applied to the container AND its rows together — the container's role, the rows' role, and any intermediate wrapper agree, verified by the sweep rather than by reading.
- The rank drag-and-drop still commits (the existing backlog dnd E2E stays green) — the row is the drag handle, and the role change must not alter the dnd-kit attribute spread order that
BacklogRow.tsx:110comments on. ProposedSprintPanel'srowRole="listitem"override is reconciled with the choice: if the backlog becomes a list, the override is redundant and goes; if it becomes a grid, the panel keeps its own list semantics deliberately and the prop's doc comment says so.- The two
disableRulesentries this defect forced intotests/e2e/shell-a11y-wide.spec.ts's/backlogsweep — each commented with this card's key — are REMOVED in the same PR, so the route returns to a zero-exclusion sweep.
Context refs
app/(authed)/backlog/_components/BacklogList.tsx— therole="list"viewport (line ~311).app/(authed)/backlog/_components/BacklogRow.tsx—rowRole(lines 65 / 92 / 116) and the dnd-kit spread-order comment (line ~110).app/(authed)/backlog/_components/ProposedSprintPanel.tsx— therowRole="listitem"call site.tests/e2e/shell-a11y-wide.spec.ts— the sweep that measured it, and the carve-out to remove.app/(authed)/items/_components/— the shippedtreegrid, if the grid arm is chosen.