Skip to content

Commit 7cc3be7

Browse files
committed
Improve ARIA attributes
1 parent 7234ec3 commit 7cc3be7

34 files changed

+550
-66
lines changed

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
# Changelog
22

3+
## 2.0.3
4+
5+
Additional `ariaAttributes` prop pass to row and cell renderers. Suggested usage is as follows:
6+
7+
```tsx
8+
function RowComponent({
9+
ariaAttributes,
10+
index,
11+
style,
12+
...rest
13+
}: RowComponentProps<object>) {
14+
return (
15+
<div role="listitem" style={style} {...ariaAttributes}>
16+
...
17+
</div>
18+
);
19+
}
20+
```
21+
322
## 2.0.2
423

524
Fixed edge-case bug with `Grid` imperative API `scrollToCell` method and "smooth" scrolling behavior.

lib/components/grid/Grid.test.tsx

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ describe("Grid", () => {
1010
let mountedCells: Map<string, CellComponentProps<object>> = new Map();
1111

1212
const CellComponent = vi.fn(function Cell(props: CellComponentProps<object>) {
13-
const { columnIndex, rowIndex, style } = props;
13+
const { ariaAttributes, columnIndex, rowIndex, style } = props;
1414

1515
const key = `${rowIndex},${columnIndex}`;
1616

@@ -22,7 +22,7 @@ describe("Grid", () => {
2222
});
2323

2424
return (
25-
<div role="listitem" style={style}>
25+
<div {...ariaAttributes} style={style}>
2626
Cell {key}
2727
</div>
2828
);
@@ -49,7 +49,7 @@ describe("Grid", () => {
4949
/>
5050
);
5151

52-
const items = screen.queryAllByRole("listitem");
52+
const items = screen.queryAllByRole("gridcell");
5353
expect(items).toHaveLength(0);
5454
});
5555

@@ -67,7 +67,7 @@ describe("Grid", () => {
6767
);
6868

6969
// 4 columns (+2) by 2 rows (+2)
70-
const items = screen.queryAllByRole("listitem");
70+
const items = screen.queryAllByRole("gridcell");
7171
expect(items).toHaveLength(24);
7272
});
7373

@@ -86,7 +86,7 @@ describe("Grid", () => {
8686
);
8787

8888
// 4 columns by 2 rows
89-
expect(container.querySelectorAll('[role="listitem"]')).toHaveLength(8);
89+
expect(container.querySelectorAll('[role="gridcell"]')).toHaveLength(8);
9090
});
9191

9292
test("type: function (px)", () => {
@@ -106,7 +106,7 @@ describe("Grid", () => {
106106
);
107107

108108
// 2 columns by 2 rows
109-
expect(container.querySelectorAll('[role="listitem"]')).toHaveLength(4);
109+
expect(container.querySelectorAll('[role="gridcell"]')).toHaveLength(4);
110110
});
111111

112112
test("type: string (%)", () => {
@@ -123,7 +123,7 @@ describe("Grid", () => {
123123
);
124124

125125
// 4 columns by 4 rows
126-
expect(container.querySelectorAll('[role="listitem"]')).toHaveLength(16);
126+
expect(container.querySelectorAll('[role="gridcell"]')).toHaveLength(16);
127127
});
128128
});
129129

@@ -253,4 +253,43 @@ describe("Grid", () => {
253253
// TODO
254254
});
255255
});
256+
257+
describe("aria attributes", () => {
258+
test("should adhere to the best recommended practices", () => {
259+
render(
260+
<Grid
261+
cellComponent={CellComponent}
262+
cellProps={EMPTY_OBJECT}
263+
columnCount={2}
264+
columnWidth={25}
265+
overscanCount={0}
266+
rowCount={2}
267+
rowHeight={20}
268+
/>
269+
);
270+
271+
expect(screen.queryAllByRole("grid")).toHaveLength(1);
272+
273+
const rows = screen.queryAllByRole("row");
274+
expect(rows).toHaveLength(2);
275+
expect(rows[0].getAttribute("aria-rowindex")).toBe("1");
276+
expect(rows[1].getAttribute("aria-rowindex")).toBe("2");
277+
278+
expect(screen.queryAllByRole("gridcell")).toHaveLength(4);
279+
280+
{
281+
const cells = rows[0].querySelectorAll('[role="gridcell"]');
282+
expect(cells).toHaveLength(2);
283+
expect(cells[0].getAttribute("aria-colindex")).toBe("1");
284+
expect(cells[1].getAttribute("aria-colindex")).toBe("2");
285+
}
286+
287+
{
288+
const cells = rows[1].querySelectorAll('[role="gridcell"]');
289+
expect(cells).toHaveLength(2);
290+
expect(cells[0].getAttribute("aria-colindex")).toBe("1");
291+
expect(cells[1].getAttribute("aria-colindex")).toBe("2");
292+
}
293+
});
294+
});
256295
});

lib/components/grid/Grid.tsx

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -193,18 +193,25 @@ export function Grid<CellProps extends object>({
193193
if (columnCount > 0 && rowCount > 0) {
194194
for (let rowIndex = rowStartIndex; rowIndex <= rowStopIndex; rowIndex++) {
195195
const rowBounds = getRowBounds(rowIndex);
196+
197+
const columns: ReactNode[] = [];
198+
196199
for (
197200
let columnIndex = columnStartIndex;
198201
columnIndex <= columnStopIndex;
199202
columnIndex++
200203
) {
201204
const columnBounds = getColumnBounds(columnIndex);
202205

203-
children.push(
206+
columns.push(
204207
<CellComponent
205208
{...(cellProps as CellProps)}
209+
ariaAttributes={{
210+
"aria-colindex": columnIndex + 1,
211+
role: "gridcell"
212+
}}
206213
columnIndex={columnIndex}
207-
key={`${rowIndex}-${columnIndex}`}
214+
key={columnIndex}
208215
rowIndex={rowIndex}
209216
style={{
210217
position: "absolute",
@@ -217,6 +224,12 @@ export function Grid<CellProps extends object>({
217224
/>
218225
);
219226
}
227+
228+
children.push(
229+
<div key={rowIndex} role="row" aria-rowindex={rowIndex + 1}>
230+
{columns}
231+
</div>
232+
);
220233
}
221234
}
222235
return children;
@@ -236,30 +249,34 @@ export function Grid<CellProps extends object>({
236249

237250
return (
238251
<div
252+
aria-colcount={columnCount}
253+
aria-rowcount={rowCount}
254+
role="grid"
239255
{...rest}
240256
className={className}
241257
dir={dir}
242258
ref={setElement}
243259
style={{
260+
position: "relative",
244261
width: "100%",
245262
height: "100%",
246-
...style,
247263
maxHeight: "100%",
248264
maxWidth: "100%",
249265
flexGrow: 1,
250-
overflow: "auto"
266+
overflow: "auto",
267+
...style
251268
}}
252269
>
270+
{cells}
271+
253272
<div
254-
className={className}
273+
aria-hidden
255274
style={{
256-
position: "relative",
257275
height: getEstimatedHeight(),
258-
width: getEstimatedWidth()
276+
width: getEstimatedWidth(),
277+
zIndex: -1
259278
}}
260-
>
261-
{cells}
262-
</div>
279+
></div>
263280
</div>
264281
);
265282
}

lib/components/grid/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ export type GridProps<CellProps extends object> = Omit<
3030
*/
3131
cellComponent: (
3232
props: {
33+
ariaAttributes: {
34+
"aria-colindex": number;
35+
role: "gridcell";
36+
};
3337
columnIndex: number;
3438
rowIndex: number;
3539
style: CSSProperties;

lib/components/list/List.test.tsx

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ describe("List", () => {
1414
let mountedRows: Map<number, RowComponentProps<object>> = new Map();
1515

1616
const RowComponent = vi.fn(function Row(props: RowComponentProps<object>) {
17-
const { index, style } = props;
17+
const { ariaAttributes, index, style } = props;
1818

1919
useLayoutEffect(() => {
2020
mountedRows.set(index, props);
@@ -24,7 +24,7 @@ describe("List", () => {
2424
});
2525

2626
return (
27-
<div role="listitem" style={style}>
27+
<div {...ariaAttributes} style={style}>
2828
Row {index}
2929
</div>
3030
);
@@ -602,4 +602,68 @@ describe("List", () => {
602602
render(<Test />);
603603
});
604604
});
605+
606+
describe("aria attributes", () => {
607+
test("should be set by default", () => {
608+
render(
609+
<List
610+
rowCount={3}
611+
rowComponent={RowComponent}
612+
rowHeight={25}
613+
rowProps={EMPTY_OBJECT}
614+
/>
615+
);
616+
617+
expect(screen.queryAllByRole("list")).toHaveLength(1);
618+
619+
const rows = screen.queryAllByRole("listitem");
620+
expect(rows).toHaveLength(3);
621+
expect(rows[0].getAttribute("aria-posinset")).toBe("1");
622+
expect(rows[0].getAttribute("aria-setsize")).toBe("3");
623+
expect(rows[1].getAttribute("aria-posinset")).toBe("2");
624+
expect(rows[1].getAttribute("aria-setsize")).toBe("3");
625+
expect(rows[2].getAttribute("aria-posinset")).toBe("3");
626+
expect(rows[2].getAttribute("aria-setsize")).toBe("3");
627+
});
628+
629+
test("should support overrides for use cases like tabular data", () => {
630+
const TableRowComponent = (props: RowComponentProps<object>) => {
631+
const { index, style } = props;
632+
633+
return (
634+
<div aria-rowindex={index + 1} role="row" style={style}>
635+
<div role="cell" aria-colindex={1} />
636+
<div role="cell" aria-colindex={2} />
637+
<div role="cell" aria-colindex={3} />
638+
</div>
639+
);
640+
};
641+
642+
render(
643+
<List
644+
role="table"
645+
aria-colcount={3}
646+
aria-rowcount={2}
647+
rowCount={2}
648+
rowComponent={TableRowComponent}
649+
rowHeight={25}
650+
rowProps={EMPTY_OBJECT}
651+
/>
652+
);
653+
654+
const tables = screen.queryAllByRole("table");
655+
expect(tables).toHaveLength(1);
656+
expect(tables[0].getAttribute("aria-colcount")).toBe("3");
657+
expect(tables[0].getAttribute("aria-rowcount")).toBe("2");
658+
659+
const rows = screen.queryAllByRole("row");
660+
expect(rows).toHaveLength(2);
661+
662+
const columns = rows[0].querySelectorAll('[role="cell"]');
663+
expect(columns).toHaveLength(3);
664+
expect(columns[0].getAttribute("aria-colindex")).toBe("1");
665+
expect(columns[1].getAttribute("aria-colindex")).toBe("2");
666+
expect(columns[2].getAttribute("aria-colindex")).toBe("3");
667+
});
668+
});
605669
});

lib/components/list/List.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ export function List<RowProps extends object>({
102102
children.push(
103103
<RowComponent
104104
{...(rowProps as RowProps)}
105+
ariaAttributes={{
106+
"aria-posinset": index + 1,
107+
"aria-setsize": rowCount,
108+
role: "listitem"
109+
}}
105110
key={index}
106111
index={index}
107112
style={{
@@ -120,26 +125,28 @@ export function List<RowProps extends object>({
120125

121126
return (
122127
<div
128+
role="list"
123129
{...rest}
124130
className={className}
125131
ref={setElement}
126132
style={{
127-
...style,
133+
position: "relative",
128134
maxHeight: "100%",
129135
flexGrow: 1,
130-
overflowY: "auto"
136+
overflowY: "auto",
137+
...style
131138
}}
132139
>
140+
{rows}
141+
133142
<div
134-
className={className}
143+
aria-hidden
135144
style={{
136145
height: getEstimatedSize(),
137-
position: "relative",
138-
width: "100%"
146+
width: "100%",
147+
zIndex: -1
139148
}}
140-
>
141-
{rows}
142-
</div>
149+
></div>
143150
</div>
144151
);
145152
}

lib/components/list/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type {
66
Ref
77
} from "react";
88

9-
type ForbiddenKeys = "index" | "style";
9+
type ForbiddenKeys = "ariaAttributes" | "index" | "style";
1010
type ExcludeForbiddenKeys<Type> = {
1111
[Key in keyof Type]: Key extends ForbiddenKeys ? never : Type[Key];
1212
};
@@ -65,6 +65,11 @@ export type ListProps<RowProps extends object> = Omit<
6565
*/
6666
rowComponent: (
6767
props: {
68+
ariaAttributes: {
69+
"aria-posinset": number;
70+
"aria-setsize": number;
71+
role: "listitem";
72+
};
6873
index: number;
6974
style: CSSProperties;
7075
} & RowProps

0 commit comments

Comments
 (0)