Skip to content

fix(a11y): add missing ARIA row/rowgroup structure to grid headers - #2680

Closed
hjaewon wants to merge 1 commit into
ghiscoding:masterfrom
hjaewon:fix/aria-grid-header-skeleton
Closed

fix(a11y): add missing ARIA row/rowgroup structure to grid headers#2680
hjaewon wants to merge 1 commit into
ghiscoding:masterfrom
hjaewon:fix/aria-grid-header-skeleton

Conversation

@hjaewon

@hjaewon hjaewon commented Jul 30, 2026

Copy link
Copy Markdown

Describe your change

The grid container gets role="grid" and header cells get role="columnheader", but the three container
levels in between (.slick-header-container > .slick-header > .slick-header-columns) carry no role. Since
elements without a role and without focusability are transparent in the accessibility tree, this produces two
axe violations on every grid:

  • aria-required-parent — one violation per column: role="columnheader" has no role="row" parent
    (Required ARIA parent role not present: row).
  • aria-required-childrenrole="grid" is computed to directly own [role=columnheader] and
    div[tabindex] (the two 0×0 focus sinks), neither of which is an allowed child of grid.

Nothing is visually broken — the impact is that screen readers do not get the table structure.

This adds the missing skeleton at element-creation time:

Element Role added
.slick-header (header scroller, L/R) rowgroup
.slick-header-columns (L/R) row
.slick-headerrow (filter row scroller, L/R) rowgroup
.slick-headerrow-columns (L/R) row
.slick-headerrow-column (filter row cell) gridcell
focus sinks (0×0, tabindex="-1") aria-hidden="true"

Two notes on scope:

  • The filter row is fixed as a unit. Giving .slick-headerrow-columns a row role without giving its
    cells a gridcell role would introduce a new aria-required-children violation (a row must own cells)
    on grids that show the filter row, so both are in this patch.
  • The focus sinks are tabindex="-1", so they are not tabbable and aria-hidden does not trigger
    aria-hidden-focus.

aria-colcount / aria-rowcount are already handled correctly (updateRowCount()), so they are untouched.

How to reproduce

Run axe (e.g. a Lighthouse accessibility audit) on any example page containing a grid. Originally reported
downstream by a consumer of a fork: 15 aria-required-parent violations (= column count) plus 1
aria-required-children violation, on a 15-column grid.

Verification

  • pnpm exec tsc --build ./tsconfig.packages.json — no errors in the changed file.
  • packages/common unit tests: 4297 passed (181 files), which includes the 2 tests added here.
    No existing assertion changes: the suite asserts className on those containers, not the full attribute set.
  • oxlint clean, prettier --check clean (long lines use the // prettier-ignore convention already used in
    this file).
  • Two unit tests added in slickGrid.spec.ts: one asserting the rowgroup > row > columnheader and
    rowgroup > row > gridcell chains, one asserting the focus sinks are aria-hidden.

The equivalent fix has also been running as a runtime patch in a downstream facade, verified against both axe
rules (ownership computed the same way axe does) across three feature combinations — plain grid, inline filter
row, and grouping + footer totals + toolbar — plus a negative control confirming the check actually catches the
violation when the roles are removed.

The grid container gets `role="grid"` and header cells get `role="columnheader"`, but the
container levels in between (`.slick-header-container > .slick-header > .slick-header-columns`)
carry no role. Elements without a role and without focusability are transparent in the
accessibility tree, so axe reports two violations on every grid:

- `aria-required-parent` (one per column): `role="columnheader"` has no `role="row"` parent
- `aria-required-children`: `role="grid"` is computed to directly own `[role=columnheader]`
  and `div[tabindex]` (the two 0x0 focus sinks), neither allowed as a child of `grid`

Nothing is visually broken; the impact is that screen readers do not get the table structure.

Roles added at element-creation time:
- `.slick-header` (header scroller, L/R) -> rowgroup
- `.slick-header-columns` (L/R) -> row
- `.slick-headerrow` (filter row scroller, L/R) -> rowgroup
- `.slick-headerrow-columns` (L/R) -> row
- `.slick-headerrow-column` (filter row cell) -> gridcell
- focus sinks (0x0, tabindex=-1) -> aria-hidden="true"

The filter row is fixed as a unit on purpose: giving `.slick-headerrow-columns` a `row` role
without giving its cells a `gridcell` role would introduce a *new* aria-required-children
violation on grids that show the filter row.

`aria-colcount` / `aria-rowcount` are already handled by updateRowCount(), so untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.0%. Comparing base (4adb63f) to head (89c4552).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #2680   +/-   ##
=======================================
  Coverage   100.0%   100.0%           
=======================================
  Files         200      200           
  Lines       25469    25470    +1     
  Branches     9007     9007           
=======================================
+ Hits        25469    25470    +1     
Flag Coverage Δ
angular 100.0% <ø> (ø)
universal 100.0% <100.0%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pkg-pr-new

pkg-pr-new Bot commented Jul 30, 2026

Copy link
Copy Markdown
angular-slickgrid

npm i https://pkg.pr.new/angular-slickgrid@2680

aurelia-slickgrid

npm i https://pkg.pr.new/aurelia-slickgrid@2680

slickgrid-react

npm i https://pkg.pr.new/slickgrid-react@2680

slickgrid-vue

npm i https://pkg.pr.new/slickgrid-vue@2680

@slickgrid-universal/angular-row-detail-plugin

npm i https://pkg.pr.new/@slickgrid-universal/angular-row-detail-plugin@2680

@slickgrid-universal/aurelia-row-detail-plugin

npm i https://pkg.pr.new/@slickgrid-universal/aurelia-row-detail-plugin@2680

@slickgrid-universal/react-row-detail-plugin

npm i https://pkg.pr.new/@slickgrid-universal/react-row-detail-plugin@2680

@slickgrid-universal/vue-row-detail-plugin

npm i https://pkg.pr.new/@slickgrid-universal/vue-row-detail-plugin@2680

@slickgrid-universal/binding

npm i https://pkg.pr.new/@slickgrid-universal/binding@2680

@slickgrid-universal/common

npm i https://pkg.pr.new/@slickgrid-universal/common@2680

@slickgrid-universal/composite-editor-component

npm i https://pkg.pr.new/@slickgrid-universal/composite-editor-component@2680

@slickgrid-universal/custom-footer-component

npm i https://pkg.pr.new/@slickgrid-universal/custom-footer-component@2680

@slickgrid-universal/custom-tooltip-plugin

npm i https://pkg.pr.new/@slickgrid-universal/custom-tooltip-plugin@2680

@slickgrid-universal/empty-warning-component

npm i https://pkg.pr.new/@slickgrid-universal/empty-warning-component@2680

@slickgrid-universal/event-pub-sub

npm i https://pkg.pr.new/@slickgrid-universal/event-pub-sub@2680

@slickgrid-universal/excel-export

npm i https://pkg.pr.new/@slickgrid-universal/excel-export@2680

@slickgrid-universal/graphql

npm i https://pkg.pr.new/@slickgrid-universal/graphql@2680

@slickgrid-universal/odata

npm i https://pkg.pr.new/@slickgrid-universal/odata@2680

@slickgrid-universal/pagination-component

npm i https://pkg.pr.new/@slickgrid-universal/pagination-component@2680

@slickgrid-universal/pdf-export

npm i https://pkg.pr.new/@slickgrid-universal/pdf-export@2680

@slickgrid-universal/row-detail-view-plugin

npm i https://pkg.pr.new/@slickgrid-universal/row-detail-view-plugin@2680

@slickgrid-universal/rxjs-observable

npm i https://pkg.pr.new/@slickgrid-universal/rxjs-observable@2680

@slickgrid-universal/sql

npm i https://pkg.pr.new/@slickgrid-universal/sql@2680

@slickgrid-universal/text-export

npm i https://pkg.pr.new/@slickgrid-universal/text-export@2680

@slickgrid-universal/utils

npm i https://pkg.pr.new/@slickgrid-universal/utils@2680

@slickgrid-universal/vanilla-bundle

npm i https://pkg.pr.new/@slickgrid-universal/vanilla-bundle@2680

@slickgrid-universal/vanilla-force-bundle

npm i https://pkg.pr.new/@slickgrid-universal/vanilla-force-bundle@2680

@slickgrid-universal/web-mcp

npm i https://pkg.pr.new/@slickgrid-universal/web-mcp@2680

commit: 89c4552

@hjaewon

hjaewon commented Jul 30, 2026

Copy link
Copy Markdown
Author

Closing this — submitted prematurely on my side, not ready for review. Sorry for the noise.

@hjaewon hjaewon closed this Jul 30, 2026
@hjaewon
hjaewon deleted the fix/aria-grid-header-skeleton branch July 30, 2026 04:00
@ghiscoding

ghiscoding commented Jul 30, 2026

Copy link
Copy Markdown
Owner

you could have left it open but changed to "Draft" then I know it isn't ready yet... a11y fixes are more than welcome, since that is not my expertise at all, so thanks in advance... :)

@ghiscoding

Copy link
Copy Markdown
Owner

@hjaewon this PR seemed like a good start, are you planning on re-opening the PR? I'll be pushing a new release soon, thanks

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants