Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e7390ef
refactor(data-table): add accessibility and column primitives
oleksandrzavarzin-callstack Aug 26, 2026
40fd57b
feat(button): expose aria-expanded
oleksandrzavarzin-callstack Aug 26, 2026
5e94593
feat(data-table): expose table structure to assistive technology
oleksandrzavarzin-callstack Aug 28, 2026
5909225
fix(data-table): give pagination controls real accessible names
oleksandrzavarzin-callstack Aug 28, 2026
71f79a7
doc(data-table): update migration docs
oleksandrzavarzin-callstack Aug 28, 2026
a91122e
fix(data-table): adress the pr comments
oleksandrzavarzin-callstack Sep 1, 2026
33d651f
fix(data-table): count only real rows and survive fragment children
oleksandrzavarzin-callstack Sep 3, 2026
59302c0
fix(data-table): tell a row component from a wrapper by its type
oleksandrzavarzin-callstack Sep 4, 2026
6d3780c
Merge remote-tracking branch 'upstream/main' into feat/structural-acc…
oleksandrzavarzin-callstack Sep 14, 2026
20bc409
fix(data-table): keep pressable-only props out of the emitted types
oleksandrzavarzin-callstack Sep 14, 2026
607a423
fix(data-table): leave wrapper as written instead of cloning
oleksandrzavarzin-callstack Sep 16, 2026
e53c7ba
Merge remote-tracking branch 'upstream/main' into feat/structural-acc…
oleksandrzavarzin-callstack Sep 16, 2026
c7eebe8
fix(data-table): omit a divider between rows from counting as a row
oleksandrzavarzin-callstack Oct 15, 2026
3c0d158
Merge remote-tracking branch 'upstream/main' into feat/structural-acc…
oleksandrzavarzin-callstack Oct 16, 2026
ecc0928
fix: apply review suggestions from @satya164
oleksandrzavarzin-callstack Oct 23, 2026
4bb0ebb
fix: mirror text alignment in RTL on the web only
oleksandrzavarzin-callstack Oct 23, 2026
34a72c4
refactor(data-table): let cells register with their row
oleksandrzavarzin-callstack Oct 28, 2026
2597424
refactor(data-table): resolve columns by key and let titles name them
oleksandrzavarzin-callstack Oct 28, 2026
41f7bed
refactor(data-table): take row positions from props, not children
oleksandrzavarzin-callstack Oct 28, 2026
a12d8e6
fix(data-table): rotate the sort arrow with a CSS transition
oleksandrzavarzin-callstack Oct 28, 2026
530a65a
fix(data-table): put a button inside a sortable column header on web
oleksandrzavarzin-callstack Oct 28, 2026
0ee832d
fix(data-table): keep pressable rows out of the tab order on web
oleksandrzavarzin-callstack Oct 28, 2026
1d0bb1c
fix: remove duplicate and prop-only tests
oleksandrzavarzin-callstack Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions docs/6.x/docs/guides/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,146 @@ const theme = {
/>
```

### DataTable

The Paper 6.x `DataTable` adds table semantics. The structure it produces and the accessible names it exposes have both changed. Existing tables should still be working.

#### Touch handling

Rows, cells and titles with no touch handler render a plain `View` instead of a disabled touchable

```tsx
// Before (v5): announced as a disabled control
<DataTable.Row>
<DataTable.Cell>{item.name}</DataTable.Cell>
</DataTable.Row>

// After (v6): pass a handler if the row is meant to be pressable
<DataTable.Row onPress={() => select(item)}>
<DataTable.Cell>{item.name}</DataTable.Cell>
</DataTable.Row>
```

On the web, a pressable row is clickable but is no longer a Tab stop, because a table row cannot also be a button. Put a control in a cell so keyboard and screen reader users can reach the action:

```tsx
<DataTable.Row onPress={() => toggle(item)}>
<DataTable.Cell column="selected">
<Checkbox
status={selected ? 'checked' : 'unchecked'}
onPress={() => toggle(item)}
aria-label={`Select ${item.name}`}
/>
</DataTable.Cell>
<DataTable.Cell column="name">{item.name}</DataTable.Cell>
</DataTable.Row>
```

Pass `tabIndex={0}` on the row to keep it a Tab stop anyway.

#### Screen reader announcements

- new `rowCount` on the table and `index` on each row give the row positions. On a page of a larger set, `index` counts from the start of the whole set
- new `hasHeader` defaults to `true`. Pass `hasHeader={false}` for a table without a `DataTable.Header`, or every row position on the web is one too high
- on iOS and Android, a cell is announced with the title of its column, matched by `column`
- `nativeFocusMode="cell"` gives one stop per cell instead of one per row
- `accessible={false}` on a row opts that row out
- `formatRowPosition` replaces the wording, or removes it with `null`
- the table does not count its children, so rows in a `View`, a fragment or a component of your own get the same position as inline rows

```tsx
// Before (v5)
<DataTable>
{items.slice(from, to).map((item) => (
<DataTable.Row key={item.key}>{/* ... */}</DataTable.Row>
))}
</DataTable>

// After (v6)
<DataTable aria-label="Nutrition" rowCount={items.length}>
{items.slice(from, to).map((item, i) => (
<DataTable.Row key={item.key} index={from + i}>
{/* ... */}
</DataTable.Row>
))}
</DataTable>
```

#### Pagination labels

- `labels` is new, and localizes every control
- `aria-label="pagination-container"` and `aria-label="Options Select"` were removed: reach the controls by role or text, e.g. `getByRole('button', { name: 'Rows per page, 2' })`

```tsx
// After (v6)
<DataTable.Pagination
labels={{
container: 'Paginacja',
previousPage: 'Poprzednia strona',
nextPage: 'Następna strona',
pageStatus: ({ page, numberOfPages }) =>
`Strona ${page} z ${numberOfPages}`,
}}
/* ... */
/>
```

#### Alignment

- `numeric` is unchanged, and now also applies tabular figures
- `align` is new, accepts `'start'`, `'center'`, `'end'`

```tsx
// Before (v5): right-aligned
<DataTable.Cell numeric>{item.calories}</DataTable.Cell>

// After (v6): right-aligned, plus lined-up digits
<DataTable.Cell numeric>{item.calories}</DataTable.Cell>

// Centred, still with lined-up digits
<DataTable.Cell numeric align="center">{item.calories}</DataTable.Cell>

// Right-aligned text that is not numeric
<DataTable.Cell align="end">{item.status}</DataTable.Cell>
```

`align` defaults to `'end'` for numeric columns and `'start'` otherwise.

#### Text wrapping

- **single line, always** → single line at the default font scale, unclamped above it
- `numberOfLines` is honoured exactly at every font scale; pass `0` to never clamp

#### Column definitions

- `columns` on `DataTable` is new and optional
- `column` on a title or cell selects a column by key, and is required when the table has `columns`

```tsx
// Before (v5)
const styles = StyleSheet.create({ first: { flex: 2 } });

<DataTable.Title style={styles.first}>Dessert</DataTable.Title>
<DataTable.Cell style={styles.first}>{item.name}</DataTable.Cell>

// After (v6)
const columns = [
{ key: 'name', flex: 2 },
{ key: 'calories', numeric: true },
];

<DataTable columns={columns}>
<DataTable.Header>
<DataTable.Title column="name">Dessert</DataTable.Title>
<DataTable.Title column="calories">Calories</DataTable.Title>
</DataTable.Header>
<DataTable.Row>
<DataTable.Cell column="name">{item.name}</DataTable.Cell>
<DataTable.Cell column="calories">{item.calories}</DataTable.Cell>
</DataTable.Row>
</DataTable>
Comment on lines +559 to +568

@satya164 satya164 Oct 22, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example puts DataTable.Title and DataTable.Cell directly in DataTable. With this PR the title is then counted as a row. A complete structure shows the change better:

Suggested change
<DataTable columns={columns}>
<DataTable.Title>Dessert</DataTable.Title>
<DataTable.Cell>{item.name}</DataTable.Cell>
</DataTable>
<DataTable columns={columns}>
<DataTable.Header>
<DataTable.Title>Dessert</DataTable.Title>
</DataTable.Header>
<DataTable.Row>
<DataTable.Cell>{item.name}</DataTable.Cell>
</DataTable.Row>
</DataTable>

(via Claude, verify if it makes sense)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!
Applied your suggestion

```

### Card

#### `Card` layout
Expand Down
84 changes: 71 additions & 13 deletions example/src/Examples/DataTableExample.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as React from 'react';
import { StyleSheet } from 'react-native';

import { DataTable, Card } from 'react-native-paper';
import { Card, Checkbox, DataTable } from 'react-native-paper';
import type { DataTableColumn } from 'react-native-paper';

import ScreenWrapper from '../ScreenWrapper';

Expand All @@ -12,9 +13,21 @@ type ItemsState = Array<{
fat: number;
}>;

// Declared once, outside the component: every title and cell reads its width
// and alignment from here, and the reference has to stay stable.
const columns: readonly DataTableColumn[] = [
{ key: 'selected', width: 48 },
{ key: 'name', flex: 2 },
{ key: 'calories', numeric: true },
{ key: 'fat', numeric: true },
];

const DataTableExample = () => {
const [sortAscending, setSortAscending] = React.useState<boolean>(true);
const [page, setPage] = React.useState<number>(0);
const [selected, setSelected] = React.useState<ReadonlySet<number>>(
() => new Set()
);
const [items] = React.useState<ItemsState>([
{
key: 1,
Expand Down Expand Up @@ -71,29 +84,77 @@ const DataTableExample = () => {
setPage(0);
}, [itemsPerPage]);

const toggle = (key: number) =>
setSelected((previous) => {
const next = new Set(previous);

if (!next.delete(key)) {
next.add(key);
}

return next;
});

const allSelected = selected.size === items.length;

const toggleAll = () =>
setSelected(
allSelected ? new Set() : new Set(items.map((item) => item.key))
);

return (
<ScreenWrapper contentContainerStyle={styles.content}>
<Card>
<DataTable>
<DataTable
aria-label="Nutrition"
columns={columns}
rowCount={sortedItems.length}
>
<DataTable.Header>
<DataTable.Title column="selected" aria-label="Selected">
<Checkbox
status={
allSelected
? 'checked'
: selected.size > 0
? 'indeterminate'
: 'unchecked'
}
onPress={toggleAll}
aria-label="Select all"
/>
</DataTable.Title>
<DataTable.Title
column="name"
sortDirection={sortAscending ? 'ascending' : 'descending'}
onPress={() => setSortAscending(!sortAscending)}
style={styles.first}
>
Dessert
</DataTable.Title>
<DataTable.Title numberOfLines={2} numeric>
<DataTable.Title column="calories" numberOfLines={2}>
Calories per piece
</DataTable.Title>
<DataTable.Title numeric>Fat (g)</DataTable.Title>
<DataTable.Title column="fat">Fat (g)</DataTable.Title>
</DataTable.Header>

{sortedItems.slice(from, to).map((item) => (
<DataTable.Row key={item.key}>
<DataTable.Cell style={styles.first}>{item.name}</DataTable.Cell>
<DataTable.Cell numeric>{item.calories}</DataTable.Cell>
<DataTable.Cell numeric>{item.fat}</DataTable.Cell>
{sortedItems.slice(from, to).map((item, i) => (
// The row is a pointer shortcut. On the web it is no Tab stop, so
// the checkbox is how keyboard and screen reader users select it.
<DataTable.Row
key={item.key}
index={from + i}
onPress={() => toggle(item.key)}
>
<DataTable.Cell column="selected">
<Checkbox
status={selected.has(item.key) ? 'checked' : 'unchecked'}
onPress={() => toggle(item.key)}
aria-label={`Select ${item.name}`}
/>
</DataTable.Cell>
<DataTable.Cell column="name">{item.name}</DataTable.Cell>
<DataTable.Cell column="calories">{item.calories}</DataTable.Cell>
<DataTable.Cell column="fat">{item.fat}</DataTable.Cell>
</DataTable.Row>
))}

Expand All @@ -120,9 +181,6 @@ const styles = StyleSheet.create({
content: {
padding: 8,
},
first: {
flex: 2,
},
});

export default DataTableExample;
7 changes: 7 additions & 0 deletions src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ export type Props = Omit<ViewProps, 'style'> & {
* Accessibility role for the button. The "button" role is set by default.
*/
role?: Role;
/**
* Whether the control the button opens is currently expanded. Set this when
* the button anchors a menu or another disclosure.
*/
'aria-expanded'?: boolean;
/**
* Function to execute on press.
*/
Expand Down Expand Up @@ -171,6 +176,7 @@ const Button = ({
textColor: customTextColor,
children,
'aria-label': ariaLabel,
'aria-expanded': ariaExpanded,
accessibilityHint,
role = 'button',
hitSlop,
Expand Down Expand Up @@ -330,6 +336,7 @@ const Button = ({
aria-label={ariaLabel}
accessibilityHint={accessibilityHint}
role={role}
aria-expanded={ariaExpanded}
aria-disabled={disabled}
accessible={accessible}
hitSlop={hitSlop}
Expand Down
Loading
Loading