You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
LeaderboardPage (src/app/leaderboard/page.tsx) renders a plain Chakra Table whose contents silently replace themselves every 30 seconds (REFRESH_MS in useLeaderboard.ts) and whenever the user changes sortKey/page/search — with no aria-live region announcing that the table refreshed, no aria-sort attribute on the sortable column headers (Th elements), and the sort control is a plain Select disconnected from the table headers it affects, so there's no programmatic association a screen reader can use to know which column is currently driving the sort order. HistoryPage (src/app/history/page.tsx) has the same underlying Table/Th/Td structure with sortable-looking columns (Date, Action, Amount, Credits Earned) but no sort functionality or semantics at all, and its own periodic-refresh-free but pagination-driven content swap likewise has no live-region announcement when the page changes.
This is functionally distinct from the color-contrast and theme-toggle work already completed under closed issue #44 — it's about interaction semantics and dynamic-content announcements, not color. Under WCAG 2.1 SC 4.1.3 (Status Messages) and the ARIA table sorting pattern, both tables should announce content changes and expose sort state programmatically; neither does.
Acceptance Criteria
Add aria-sort (ascending/descending/none) to the relevant Th elements in LeaderboardPage, kept in sync with sortKey.
Add an aria-live="polite" region (visually hidden if needed) that announces when the leaderboard/history table content updates due to auto-refresh, sort change, or pagination, including a summary of what changed (e.g. 'Leaderboard updated, showing rank 1-10 of 4,213').
Ensure the leaderboard's sort Select and the table headers are programmatically associated (e.g. via aria-controls or by making the headers themselves the sort control, matching the ARIA table-sort design pattern) rather than being two independent, unlinked controls.
Add automated accessibility tests (axe-core, extending the pattern in tests/contrast-audit.spec.ts) asserting the aria-sort/live-region behavior on both pages.
Relevant Files
src/app/leaderboard/page.tsx — Table/Th elements (~L100-160) with no aria-sort, and periodic refresh with no live-region announcement
src/app/history/page.tsx — Table/Th elements (~L144-201) with the same lack of sort/live-region semantics
src/hooks/useLeaderboard.ts — drives the 30s auto-refresh that currently has no accessible announcement
Additional Notes
Re-read both src/app/leaderboard/page.tsx and src/app/history/page.tsx directly to verify. Confirmed:
LeaderboardPage's sort control is a plain Chakra Select (value={sortKey}, onChange={(e) => setSortKey(...)}) fully independent from the Th header cells rendered further down (#, Address, Credits, Stake) — there is no onClick on any Th, no aria-sort, and no aria-controls/id pairing between the Select and the table, exactly as described.
LeaderboardPage additionally has a manual Button onClick={refresh} alongside the automatic 30s refresh from useLeaderboard — meaning there are actually two independent triggers for content replacement (auto-refresh timer and manual button), both needing the same live-region treatment; the manual refresh path is a detail not called out in the original body and worth an explicit acceptance-criteria bullet since a live-region fix that only hooks into the timer-driven refresh would miss the manual button's clicks.
HistoryPage's Th cells (Date, Action, Amount, Credits Earned, Transaction) confirmed to have zero sort affordance at all — not even a visual sort indicator — so "sortable-looking" in the original body is accurate only in that they resemble a data table header, not that any sort UI exists; the fix here is closer to "add sorting" than "add semantics to existing sorting," which is a slightly larger scope than the Leaderboard side and worth flagging as an open question: does this issue want sort added to History, or just live-region announcements for its existing pagination-driven changes? The acceptance criteria as written only asks for aria-sort on Leaderboard and live-region on both — worth being explicit that History doesn't get new sort functionality here, only the live-region.
Table variant="unstyled" is used on both pages — Chakra's unstyled variant strips the default ARIA-friendly styling hooks but doesn't strip semantics, so role="table"/row/columnheader are still emitted correctly by the underlying HTML table elements; this rules out a base-semantics issue and confirms the gap really is only sort-state and live-region announcements, not fundamentally broken table markup.
Edge cases to cover
Empty search results / zero rows on Leaderboard (filteredCount === 0 branch, confirmed present in the file) — the live region should still announce "No results found" rather than going silent, since a screen-reader user who just typed a search term needs confirmation the search executed.
Pagination edge case: last page with fewer than PAGE_SIZE rows — the announcement text (e.g. "showing rank 1-10 of 4,213") needs to correctly compute the upper bound on a partial final page, not just page * PAGE_SIZE.
Debounced search input: if searchQuery changes trigger a live-region update on every keystroke rather than after debounce settles, screen readers will be spammed; the announcement should be tied to when paged/filteredCount actually changes, not to keystroke-level state.
Simultaneous auto-refresh + user-initiated sort change landing in close succession — ensure the live region doesn't emit two overlapping/garbled announcements (a queued aria-live="polite" region naturally handles this, but worth an explicit test).
Implementation sketch
Add a shared, visually-hidden (Box position="absolute" w="1px" h="1px" overflow="hidden", the standard "visually-hidden" pattern) aria-live="polite" aria-atomic="true" region per page, driven by a useEffect that watches paged/filteredCount/sortKey/currentPage and sets an announcement string.
On Leaderboard's Th cells: add aria-sort={sortKey === 'credits' && column === 'credits' ? 'descending' : 'none'} (mapping each sortable column), and make the Th itself clickable (onClick={() => setSortKey(column)}) in addition to (or instead of) the Select, with aria-controls linking the Select to the table's id if both controls are kept.
For History: add the same visually-hidden live region tied to currentPage/paged, with no aria-sort changes since no sort exists to announce.
axe-core (extending tests/contrast-audit.spec.ts): assert aria-sort values on Leaderboard Th elements match sortKey state across all sort options.
RTL: simulate the 30s auto-refresh (mock timers), assert the live region's text content updates to reflect new data.
RTL: click the manual "Refresh" button on Leaderboard, assert the live region updates (covers the dual-trigger gap noted above).
RTL: type a search query that yields zero results, assert the live region announces "No results" rather than remaining stale from the prior state.
RTL: paginate History to its last (partial) page, assert the announced range's upper bound matches the actual row count, not a full PAGE_SIZE multiple.
Problem
LeaderboardPage(src/app/leaderboard/page.tsx) renders a plain ChakraTablewhose contents silently replace themselves every 30 seconds (REFRESH_MSinuseLeaderboard.ts) and whenever the user changessortKey/page/search — with noaria-liveregion announcing that the table refreshed, noaria-sortattribute on the sortable column headers (Thelements), and the sort control is a plainSelectdisconnected from the table headers it affects, so there's no programmatic association a screen reader can use to know which column is currently driving the sort order.HistoryPage(src/app/history/page.tsx) has the same underlyingTable/Th/Tdstructure with sortable-looking columns (Date, Action, Amount, Credits Earned) but no sort functionality or semantics at all, and its own periodic-refresh-free but pagination-driven content swap likewise has no live-region announcement when the page changes.This is functionally distinct from the color-contrast and theme-toggle work already completed under closed issue #44 — it's about interaction semantics and dynamic-content announcements, not color. Under WCAG 2.1 SC 4.1.3 (Status Messages) and the ARIA table sorting pattern, both tables should announce content changes and expose sort state programmatically; neither does.
Acceptance Criteria
aria-sort(ascending/descending/none) to the relevantThelements inLeaderboardPage, kept in sync withsortKey.aria-live="polite"region (visually hidden if needed) that announces when the leaderboard/history table content updates due to auto-refresh, sort change, or pagination, including a summary of what changed (e.g. 'Leaderboard updated, showing rank 1-10 of 4,213').Selectand the table headers are programmatically associated (e.g. viaaria-controlsor by making the headers themselves the sort control, matching the ARIA table-sort design pattern) rather than being two independent, unlinked controls.tests/contrast-audit.spec.ts) asserting thearia-sort/live-region behavior on both pages.Relevant Files
src/app/leaderboard/page.tsx— Table/Th elements (~L100-160) with no aria-sort, and periodic refresh with no live-region announcementsrc/app/history/page.tsx— Table/Th elements (~L144-201) with the same lack of sort/live-region semanticssrc/hooks/useLeaderboard.ts— drives the 30s auto-refresh that currently has no accessible announcementAdditional Notes
Re-read both
src/app/leaderboard/page.tsxandsrc/app/history/page.tsxdirectly to verify. Confirmed:LeaderboardPage's sort control is a plain ChakraSelect(value={sortKey},onChange={(e) => setSortKey(...)}) fully independent from theThheader cells rendered further down (#,Address,Credits,Stake) — there is noonClickon anyTh, noaria-sort, and noaria-controls/idpairing between theSelectand the table, exactly as described.LeaderboardPageadditionally has a manualButton onClick={refresh}alongside the automatic 30s refresh fromuseLeaderboard— meaning there are actually two independent triggers for content replacement (auto-refresh timer and manual button), both needing the same live-region treatment; the manual refresh path is a detail not called out in the original body and worth an explicit acceptance-criteria bullet since a live-region fix that only hooks into the timer-driven refresh would miss the manual button's clicks.HistoryPage'sThcells (Date, Action, Amount, Credits Earned, Transaction) confirmed to have zero sort affordance at all — not even a visual sort indicator — so "sortable-looking" in the original body is accurate only in that they resemble a data table header, not that any sort UI exists; the fix here is closer to "add sorting" than "add semantics to existing sorting," which is a slightly larger scope than the Leaderboard side and worth flagging as an open question: does this issue want sort added to History, or just live-region announcements for its existing pagination-driven changes? The acceptance criteria as written only asks foraria-sorton Leaderboard and live-region on both — worth being explicit that History doesn't get new sort functionality here, only the live-region.Table variant="unstyled"is used on both pages — Chakra'sunstyledvariant strips the default ARIA-friendly styling hooks but doesn't strip semantics, sorole="table"/row/columnheaderare still emitted correctly by the underlying HTML table elements; this rules out a base-semantics issue and confirms the gap really is only sort-state and live-region announcements, not fundamentally broken table markup.Edge cases to cover
filteredCount === 0branch, confirmed present in the file) — the live region should still announce "No results found" rather than going silent, since a screen-reader user who just typed a search term needs confirmation the search executed.PAGE_SIZErows — the announcement text (e.g. "showing rank 1-10 of 4,213") needs to correctly compute the upper bound on a partial final page, not justpage * PAGE_SIZE.searchQuerychanges trigger a live-region update on every keystroke rather than after debounce settles, screen readers will be spammed; the announcement should be tied to whenpaged/filteredCountactually changes, not to keystroke-level state.aria-live="polite"region naturally handles this, but worth an explicit test).Implementation sketch
Box position="absolute" w="1px" h="1px" overflow="hidden", the standard "visually-hidden" pattern)aria-live="polite" aria-atomic="true"region per page, driven by auseEffectthat watchespaged/filteredCount/sortKey/currentPageand sets an announcement string.Thcells: addaria-sort={sortKey === 'credits' && column === 'credits' ? 'descending' : 'none'}(mapping each sortable column), and make theThitself clickable (onClick={() => setSortKey(column)}) in addition to (or instead of) theSelect, witharia-controlslinking theSelectto the table'sidif both controls are kept.currentPage/paged, with noaria-sortchanges since no sort exists to announce.Test plan
tests/contrast-audit.spec.ts): assertaria-sortvalues on LeaderboardThelements matchsortKeystate across all sort options.PAGE_SIZEmultiple.Cross-references