Skip to content

fix(Pagination): announce page size selected state and active pager (ENG-209890, ENG-209864) - #1167

Open
ksharma-c-eightfold wants to merge 3 commits into
EightfoldAI:mainfrom
ksharma-c-eightfold:eng-209890-pagination-aria-selected
Open

ksharma-c-eightfold wants to merge 3 commits into
EightfoldAI:mainfrom
ksharma-c-eightfold:eng-209890-pagination-aria-selected

Conversation

@ksharma-c-eightfold

@ksharma-c-eightfold ksharma-c-eightfold commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The Pagination page-size dropdown rendered a Menu (role=menu > role=menuitem) inside the Dropdown's role=listbox overlay. Screen readers announced only the option name and position — "20 / page, 1 of 3" — with no selected state. menuitem is not a valid child of listbox and carries no selected state in ARIA.

WCAG 4.1.2 Name, Role, Value.

Separately, the pager buttons passed a boolean to aria-current. React serializes aria-* booleans to strings, so the active page rendered aria-current="true" and every inactive one rendered aria-current="false". NVDA maps the generic true token to the bare word "current", so activating a page announced only "current" with no indication of what is current.

WCAG 1.3.1 Info and Relationships.

Fix

Page-size dropdown (ENG-209890):

  • Each page-size item gets role="option" and aria-selected={item === _pageSize}
  • Menu gets role="presentation" so the ul no longer claims role="menu" inside the listbox
  • Dropdown gets ariaHaspopupValue="listbox" so the trigger declares its popup type

Resulting structure: listbox > (presentation ul) > (presentation li) > option[aria-selected].

Follows the existing Select.tsx convention for option/listbox pickers in this repo.

Pager buttons (ENG-209864):

  • All three aria-current call sites in Pager.tsx (first page, mapped middle pages, last page) now emit aria-current="page" when active and omit the attribute otherwise
  • page is the token ARIA and the APG pagination pattern define for this case; NVDA announces it as "current page"
  • Using undefined rather than false also drops the pointless aria-current="false" from every inactive pager button

Matches Breadcrumb.tsx, which already does 'aria-current': ariaCurrent ? 'page' : null. Pager.tsx was the last component in the library emitting a boolean aria-current.

No change to the pager buttons' accessible names, the active class, styling, or any pagination behaviour.

JIRA ID

Verification

DOM-verified in Chrome against Storybook pagination--change-page-size:

  • trigger aria-haspopup=listbox, aria-expanded toggles false -> true
  • overlay role=listbox; ul and all li role=presentation
  • 4 x role=option; aria-selected reads true on the current size, false on the rest
  • selection state follows selection after picking a new size
  • zero leftover role=menuitem / role=menu

tsc --noEmit clean, Pagination + Table + PersistentBar + Carousel suites pass (37 suites / 422 tests). One snapshot line updated for the new aria-haspopup.

For the pager fix, a unit test pins the attribute value at all three call sites (first page, a middle page, the last page active) and asserts exactly one button carries it — a boolean would silently regress this otherwise. Snapshots across Pagination, Table, PersistentBar and Carousel drop from 24 aria-current occurrences to 4, one per rendered pager.

Manual NVDA confirmation that the announcement is "current page" rather than the bare "current" still needs a Windows box and has not been done here.

No CSS, no focus-order, and no keyboard-behaviour changes — List/Dropdown key handlers navigate via refs and activeElement, never by role.

Screen-reader wording note: option + aria-selected is what produces "selected" / "not selected". VoiceOver speaks only the selected state and stays silent on unselected rows; NVDA announces both.

Not addressed

  • The listbox has no accessible name, so NVDA announces a bare "list"
  • Menu.types.ts declares menuItemRole/menuButtonRole/menuButtonHasRole that Menu.tsx never destructures

🤖 Generated with Claude Code

…9890)

The page size dropdown rendered a Menu (role=menu > role=menuitem) inside the
Dropdown's role=listbox overlay, so screen readers announced only the option
name and position with no selected state (WCAG 4.1.2 Name, Role, Value).

Mark the Menu role=presentation, give each item role=option plus aria-selected,
and declare aria-haspopup=listbox on the trigger. Arrow-key focus movement,
keyboard behaviour and styling are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codesandbox-ci

codesandbox-ci Bot commented Sep 13, 2026 •

Copy link
Copy Markdown

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

@codecov

codecov Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.41%. Comparing base (ad413bf) to head (0cdff78).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1167      +/-   ##
==========================================
- Coverage   85.41%   85.41%   -0.01%     
==========================================
  Files        1231     1231              
  Lines       21719    21719              
  Branches     8263     8263              
==========================================
- Hits        18552    18551       -1     
- Misses       3079     3080       +1     
  Partials       88       88              

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

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…209864)

Pager buttons passed a boolean to aria-current, which React serializes to
aria-current="true" on the active page and aria-current="false" on every
other one. NVDA announces the generic "true" token as the bare word
"current", giving no indication of what is current.

Emit the "page" token when active and omit the attribute otherwise, at all
three call sites (first page, mapped middle pages, last page). This matches
the ARIA spec, the APG pagination pattern, and Breadcrumb.tsx, which already
uses 'aria-current': ariaCurrent ? 'page' : null.

Accessible names, the active class, and pagination behaviour are unchanged.
Adds a test pinning the attribute value so a boolean cannot regress it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ksharma-c-eightfold ksharma-c-eightfold changed the title fix(Pagination): announce selected state of page size options (ENG-209890) fix(Pagination): announce page size selected state and active pager (ENG-209890, ENG-209864) Sep 22, 2026
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.

1 participant