feat: structural accessibility for data table - #5082
feat: structural accessibility for data table#5082oleksandrzavarzin-callstack wants to merge 6 commits into
Conversation
| const publish = useLatestCallback(() => setHeaderLabels?.(labels)); | ||
| const signature = labels.join(' '); | ||
|
|
||
| React.useEffect(publish, [publish, signature]); |
There was a problem hiding this comment.
Publishing the column names through an effect makes the table's ARIA indices a second-render value. Server-rendered output (the docs site is one, rspress.config.ts:122 aliases to react-native-web) gets aria-rowcount="6" instead of 7, and the header row and the first data row both get aria-rowindex="1" - duplicate row indices. DataTable already reads column position from ColumnIndexContext without an effect; can the header names go the same way?
| const label = | ||
| ariaLabel ?? (isWeb ? undefined : composeCellLabel({ columnLabel, value })); |
There was a problem hiding this comment.
Try const label = ariaLabel ?? (cellIsFocusUnit ? composeCellLabel({ columnLabel, value }) : undefined) - a cell wrapping a Checkbox currently gets aria-label="Pick", which buries the checkbox's own role and state behind the column name.
| 'aria-rowindex': | ||
| index == null ? undefined : index + 1 + (table?.hasHeader ? 1 : 0), | ||
| }), | ||
| accessible: accessible ?? (rowIsFocusUnit || undefined), |
There was a problem hiding this comment.
Try accessible: accessible ?? rowIsFocusUnit - the || undefined leaves the prop unset, which is exactly when Pressable applies its own default of true, so a pressable row swallows the Checkbox inside it on iOS.
| return child.props.index === undefined | ||
| ? React.cloneElement(child, { index: firstRowIndex + offset }) | ||
| : child; | ||
| }); |
There was a problem hiding this comment.
Try the provider pattern you already wrote for columns (DataTableColumnsContext.tsx:36-44) - cloneElement means a consumer's <NameRow /> wrapper gets no index and silently loses its aria-rowindex. It is also the React.Children problem #4954 names.
|
@JKobrynski Thank you for the review! |
Motivation
DataTable's structure was purely visual: bareViews andTouchableRipples with no roles, indices or header relationships, so a screen reader conveyed no table context at all. A user heard"159"with no way to know it was the Calories column of row 3.Changes
role="table"witharia-rowcount/aria-colcount,role="row"witharia-rowindex,role="columnheader"witharia-sort,role="cell"witharia-colindex."Dessert, Frozen yogurt, Calories, 159, row 3 of 6". It falls back to per-cell focus when a row holds interactive or non-text content, so nothing becomes unreachable.nativeFocusModeoverrides the choice.View.aria-sorton web, folded into the accessible name on native, with an announcement on change since focus stays on the header. NewsortAccessibilityLabelslocalizes the wording.Animated, matchingSwitchandCheckbox. Reduced motion is suppressed viaReduceMotion.Alwaysinstead of a manual snap, and150msbecomestheme.motion.duration.short3(same value).labelsprop. Data-dependent values are functions, so pluralization stays with the app's own i18n.columnsdescribes width and alignment once instead of repeatingstyleon a title and every cell. Newaligncomposes withnumeric, which is unchanged and not deprecated.numberOfLinesis honoured exactly at every font scale; the default is one line at the default scale and unclamped above it.surfaceVarianttooutlineVariant, matchingDivider.Buttongainsaria-expanded, needed by the rows-per-page anchor which never reported that it opens a menu.Breaking changes
Rows, cells and titles without a touch handler render a plain
View. They lose the ripple, the web hover background and keyboard focus, and no longer report a disabled state.Other important changes:
rowCountandfirstRowIndexfor correct positions when paginating.numericnow applies tabular figures, the one visual change to existing tables.Related issue
Child of the "Modernize non-standard components" effort; no issue exists yet.
Test plan
yarn typescript,yarn lintcleanyarn test806 passed, 167 snapshots. The 2828-line snapshot is replaced with 8 small per-component snapshots plus named contract assertions, since the old blob is where thearia-disabledregression sat unnoticed.aria-sortcycling, and noaria-disabledon any row, cell or title.adb shell uiautomator dump. Tapping a row focuses that row, each header is its own stop, and a row containing an interactive element (e.g. Checkbox) falls back to per-cell focus.Screenshots: