Skip to content

Commit d71743f

Browse files
authored
fix(aria/grid): fix navigation bugs and add grid behavior unit tests (#32140)
1 parent eedc5a6 commit d71743f

File tree

10 files changed

+3183
-11
lines changed

10 files changed

+3183
-11
lines changed

src/aria/grid/grid.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import {GridPattern, GridRowPattern, GridCellPattern, GridCellWidgetPattern} fro
3636
'(pointerdown)': 'pattern.onPointerdown($event)',
3737
'(pointermove)': 'pattern.onPointermove($event)',
3838
'(pointerup)': 'pattern.onPointerup($event)',
39-
'(focusin)': 'pattern.onFocusIn($event)',
39+
'(focusin)': 'pattern.onFocusIn()',
4040
'(focusout)': 'pattern.onFocusOut($event)',
4141
},
4242
})

src/aria/ui-patterns/behaviors/grid/BUILD.bazel

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
load("//tools:defaults.bzl", "ts_project")
1+
load("//tools:defaults.bzl", "ng_web_test_suite", "ts_project")
22

33
package(default_visibility = ["//visibility:public"])
44

@@ -13,3 +13,18 @@ ts_project(
1313
"//src/aria/ui-patterns/behaviors/signal-like",
1414
],
1515
)
16+
17+
ts_project(
18+
name = "unit_test_sources",
19+
testonly = True,
20+
srcs = glob(["**/*.spec.ts"]),
21+
deps = [
22+
":grid",
23+
"//:node_modules/@angular/core",
24+
],
25+
)
26+
27+
ng_web_test_suite(
28+
name = "unit_tests",
29+
deps = [":unit_test_sources"],
30+
)
Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {signal, Signal, WritableSignal} from '@angular/core';
10+
import {BaseGridCell, GridData} from './grid-data';
11+
12+
export interface TestBaseGridCell extends BaseGridCell {
13+
rowSpan: WritableSignal<number>;
14+
colSpan: WritableSignal<number>;
15+
id: Signal<string>;
16+
}
17+
18+
/**
19+
* GRID A:
20+
* ┌─────┬─────┬─────┐
21+
* │ 0,0 │ 0,1 │ 0,2 │
22+
* ├─────┼─────┼─────┤
23+
* │ 1,0 │ 1,1 │ 1,2 │
24+
* ├─────┼─────┼─────┤
25+
* │ 2,0 │ 2,1 │ 2,2 │
26+
* └─────┴─────┴─────┘
27+
*/
28+
export function createGridA(): TestBaseGridCell[][] {
29+
return [
30+
[
31+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-0-0')},
32+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-0-1')},
33+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-0-2')},
34+
],
35+
[
36+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-1-0')},
37+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-1-1')},
38+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-1-2')},
39+
],
40+
[
41+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-2-0')},
42+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-2-1')},
43+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-2-2')},
44+
],
45+
];
46+
}
47+
48+
/**
49+
* GRID B:
50+
* ┌─────┬─────┬─────┐
51+
* │ 0,0 │ 0,1 │ 0,2 │
52+
* ├─────┼─────┤ │
53+
* │ 1,0 │ 1,1 │ │
54+
* ├─────┤ ├─────┤
55+
* │ 2,0 │ │ 2,2 │
56+
* │ ├─────┼─────┤
57+
* │ │ 3,1 │ 3,2 │
58+
* └─────┴─────┴─────┘
59+
*/
60+
export function createGridB(): TestBaseGridCell[][] {
61+
return [
62+
[
63+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-0-0')},
64+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-0-1')},
65+
{rowSpan: signal(2), colSpan: signal(1), id: signal('cell-0-2')},
66+
],
67+
[
68+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-1-0')},
69+
{rowSpan: signal(2), colSpan: signal(1), id: signal('cell-1-1')},
70+
],
71+
[
72+
{rowSpan: signal(2), colSpan: signal(1), id: signal('cell-2-0')},
73+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-2-2')},
74+
],
75+
[
76+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-3-1')},
77+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-3-2')},
78+
],
79+
];
80+
}
81+
82+
/**
83+
* GRID C:
84+
* ┌───────────┬─────┬─────┐
85+
* │ 0,0 │ 0,2 │ 0,3 │
86+
* ├─────┬─────┴─────┼─────┤
87+
* │ 1,0 │ 1,1 │ 1,3 │
88+
* ├─────┼─────┬─────┴─────┤
89+
* │ 2,0 │ 2,1 │ 2,2 │
90+
* └─────┴─────┴───────────┘
91+
*/
92+
export function createGridC(): TestBaseGridCell[][] {
93+
return [
94+
[
95+
{rowSpan: signal(1), colSpan: signal(2), id: signal('cell-0-0')},
96+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-0-2')},
97+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-0-3')},
98+
],
99+
[
100+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-1-0')},
101+
{rowSpan: signal(1), colSpan: signal(2), id: signal('cell-1-1')},
102+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-1-3')},
103+
],
104+
[
105+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-2-0')},
106+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-2-1')},
107+
{rowSpan: signal(1), colSpan: signal(2), id: signal('cell-2-2')},
108+
],
109+
];
110+
}
111+
112+
/**
113+
* GRID D:
114+
* ┌─────┬───────────┬─────┐
115+
* │ 0,0 │ 0,1 │ 0,3 │
116+
* │ ├───────────┼─────┤
117+
* │ │ 1,1 │ 1,3 │
118+
* ├─────┤ │ │
119+
* │ 2,0 │ │ │
120+
* ├─────┼─────┬─────┴─────┤
121+
* │ 3,0 │ 3,1 │ 3,2 │
122+
* └─────┴─────┴───────────┘
123+
*/
124+
export function createGridD(): TestBaseGridCell[][] {
125+
return [
126+
[
127+
{rowSpan: signal(2), colSpan: signal(1), id: signal('cell-0-0')},
128+
{rowSpan: signal(1), colSpan: signal(2), id: signal('cell-0-1')},
129+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-0-3')},
130+
],
131+
[
132+
{rowSpan: signal(2), colSpan: signal(2), id: signal('cell-1-1')},
133+
{rowSpan: signal(2), colSpan: signal(1), id: signal('cell-1-3')},
134+
],
135+
[{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-2-0')}],
136+
[
137+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-3-0')},
138+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-3-1')},
139+
{rowSpan: signal(1), colSpan: signal(2), id: signal('cell-3-2')},
140+
],
141+
];
142+
}
143+
144+
/**
145+
* GRID E: Uneven rows (jagged)
146+
* ┌─────┬─────┬─────┐
147+
* │ 0,0 │ 0,1 │ 0,2 │
148+
* ├─────┤ ├─────┘
149+
* │ 1,0 │ │
150+
* ├─────┼─────┤
151+
* │ 2,0 │ 2,1 │
152+
* └─────┴─────┴
153+
*/
154+
export function createGridE(): TestBaseGridCell[][] {
155+
return [
156+
[
157+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-0-0')},
158+
{rowSpan: signal(2), colSpan: signal(1), id: signal('cell-0-1')},
159+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-0-2')},
160+
],
161+
[{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-1-0')}],
162+
[
163+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-2-0')},
164+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-2-1')},
165+
],
166+
];
167+
}
168+
169+
/**
170+
* GRID F: Grid with empty rows
171+
* ┌─────┬─────┬─────┐
172+
* │ 0,0 │ 0,1 │ 0,2 │
173+
* ├─────┼─────┼─────┤
174+
* │ │ │ │
175+
* ├─────┼─────┼─────┤
176+
* │ 2,0 │ 2,1 │ 2,2 │
177+
* └─────┴─────┴─────┘
178+
*/
179+
export function createGridF(): TestBaseGridCell[][] {
180+
return [
181+
[
182+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-0-0')},
183+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-0-1')},
184+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-0-2')},
185+
],
186+
[],
187+
[
188+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-2-0')},
189+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-2-1')},
190+
{rowSpan: signal(1), colSpan: signal(1), id: signal('cell-2-2')},
191+
],
192+
];
193+
}
194+
195+
function createGridData(cells: TestBaseGridCell[][]): GridData<TestBaseGridCell> {
196+
return new GridData({cells: signal(cells)});
197+
}
198+
199+
describe('GridData', () => {
200+
describe('rowCount', () => {
201+
it('should return the number of rows in the grid', () => {});
202+
});
203+
204+
describe('maxRowCount', () => {
205+
it('should return the maximum number of rows, accounting for row spans', () => {
206+
const gridA = createGridData(createGridA());
207+
expect(gridA.maxRowCount()).toBe(3);
208+
209+
const gridB = createGridData(createGridB());
210+
expect(gridB.maxRowCount()).toBe(4);
211+
212+
const gridC = createGridData(createGridC());
213+
expect(gridC.maxRowCount()).toBe(3);
214+
215+
const gridD = createGridData(createGridD());
216+
expect(gridD.maxRowCount()).toBe(4);
217+
218+
const gridF = createGridData(createGridE());
219+
expect(gridF.maxRowCount()).toBe(3);
220+
221+
const gridG = createGridData(createGridF());
222+
expect(gridG.maxRowCount()).toBe(3);
223+
});
224+
});
225+
226+
describe('maxColCount', () => {
227+
it('should return the maximum number of columns, accounting for column spans', () => {
228+
const gridA = createGridData(createGridA());
229+
expect(gridA.maxColCount()).toBe(3);
230+
231+
const gridB = createGridData(createGridB());
232+
expect(gridB.maxColCount()).toBe(3);
233+
234+
const gridC = createGridData(createGridC());
235+
expect(gridC.maxColCount()).toBe(4);
236+
237+
const gridD = createGridData(createGridD());
238+
expect(gridD.maxColCount()).toBe(4);
239+
240+
const gridE = createGridData(createGridE());
241+
expect(gridE.maxColCount()).toBe(3);
242+
243+
const gridF = createGridData(createGridF());
244+
expect(gridF.maxColCount()).toBe(3);
245+
});
246+
});
247+
248+
describe('getCell', () => {
249+
it('should get the cell at the given coordinates', () => {
250+
const cells = createGridD();
251+
const grid = createGridData(cells);
252+
253+
expect(grid.getCell({row: 0, col: 0})).toBe(cells[0][0]);
254+
expect(grid.getCell({row: 1, col: 0})).toBe(cells[0][0]);
255+
expect(grid.getCell({row: 0, col: 1})).toBe(cells[0][1]);
256+
expect(grid.getCell({row: 0, col: 2})).toBe(cells[0][1]);
257+
expect(grid.getCell({row: 1, col: 1})).toBe(cells[1][0]);
258+
expect(grid.getCell({row: 2, col: 2})).toBe(cells[1][0]);
259+
});
260+
261+
it('should return undefined for out-of-bounds coordinates', () => {
262+
const grid = createGridData(createGridA());
263+
expect(grid.getCell({row: 5, col: 5})).toBeUndefined();
264+
expect(grid.getCell({row: -1, col: 0})).toBeUndefined();
265+
});
266+
});
267+
268+
describe('getCoords', () => {
269+
it('should get the primary coordinates of the given cell', () => {
270+
const cells = createGridD();
271+
const grid = createGridData(cells);
272+
273+
expect(grid.getCoords(cells[0][0])).toEqual({row: 0, col: 0});
274+
expect(grid.getCoords(cells[1][0])).toEqual({row: 1, col: 1});
275+
expect(grid.getCoords(cells[3][2])).toEqual({row: 3, col: 2});
276+
});
277+
});
278+
279+
describe('getAllCoords', () => {
280+
it('should get all coordinates that the given cell spans', () => {
281+
const cells = createGridD();
282+
const grid = createGridData(cells);
283+
284+
expect(grid.getAllCoords(cells[0][0])).toEqual([
285+
{row: 0, col: 0},
286+
{row: 1, col: 0},
287+
]);
288+
expect(grid.getAllCoords(cells[1][0])).toEqual([
289+
{row: 1, col: 1},
290+
{row: 1, col: 2},
291+
{row: 2, col: 1},
292+
{row: 2, col: 2},
293+
]);
294+
expect(grid.getAllCoords(cells[3][2])).toEqual([
295+
{row: 3, col: 2},
296+
{row: 3, col: 3},
297+
]);
298+
});
299+
});
300+
301+
describe('getRowCount', () => {
302+
it('should get the number of rows in the given column', () => {
303+
const grid = createGridData(createGridD());
304+
expect(grid.getRowCount(0)).toBe(4);
305+
expect(grid.getRowCount(1)).toBe(4);
306+
expect(grid.getRowCount(2)).toBe(4);
307+
expect(grid.getRowCount(3)).toBe(4);
308+
});
309+
310+
it('should return undefined for an out-of-bounds column', () => {
311+
const grid = createGridData(createGridA());
312+
expect(grid.getRowCount(5)).toBeUndefined();
313+
expect(grid.getRowCount(-1)).toBeUndefined();
314+
});
315+
});
316+
317+
describe('getColCount', () => {
318+
it('should get the number of columns in the given row', () => {
319+
const gridD = createGridData(createGridD());
320+
expect(gridD.getColCount(0)).toBe(4);
321+
expect(gridD.getColCount(1)).toBe(4);
322+
expect(gridD.getColCount(2)).toBe(4);
323+
expect(gridD.getColCount(3)).toBe(4);
324+
325+
const gridE = createGridData(createGridE());
326+
expect(gridE.getColCount(0)).toBe(3);
327+
expect(gridE.getColCount(1)).toBe(2);
328+
expect(gridE.getColCount(2)).toBe(2);
329+
});
330+
331+
it('should return undefined for an out-of-bounds row', () => {
332+
const grid = createGridData(createGridA());
333+
expect(grid.getColCount(5)).toBeUndefined();
334+
expect(grid.getColCount(-1)).toBeUndefined();
335+
});
336+
});
337+
});

src/aria/ui-patterns/behaviors/grid/grid-data.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,6 @@ export class GridData<T extends BaseGridCell> {
4141
/** The two-dimensional array of cells that represents the grid. */
4242
readonly cells: SignalLike<T[][]>;
4343

44-
/** The number of rows in the grid. */
45-
readonly rowCount = computed<number>(() => this.cells().length);
46-
4744
/** The maximum number of rows in the grid, accounting for row spans. */
4845
readonly maxRowCount = computed<number>(() => Math.max(...this._rowCountByCol().values(), 0));
4946

0 commit comments

Comments
 (0)