Skip to content

Commit c5b2f0a

Browse files
authored
feat(cdk-experimental/ui-patterns): create grid navigation behavior (#31290)
* feat(cdk-experimental/ui-patterns): create grid navigation behavior * fixup! feat(cdk-experimental/ui-patterns): create grid navigation behavior
1 parent 394336c commit c5b2f0a

File tree

5 files changed

+1626
-52
lines changed

5 files changed

+1626
-52
lines changed

src/cdk-experimental/ui-patterns/behaviors/grid-focus/grid-focus.spec.ts

Lines changed: 135 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {computed, Signal, signal, WritableSignal} from '@angular/core';
10-
import {GridFocus, GridFocusInputs, GridFocusCell} from './grid-focus';
10+
import {GridFocus, GridFocusInputs, GridFocusCell, RowCol} from './grid-focus';
1111

1212
// Helper type for test cells, extending GridFocusCell
1313
interface TestGridCell extends GridFocusCell {
@@ -23,13 +23,13 @@ type TestSetupInputs = Partial<GridFocusInputs<TestGridCell>> & {
2323
gridFocus?: WritableSignal<GridFocus<TestGridCell> | undefined>;
2424
};
2525

26-
function createTestCell(
26+
export function createTestCell(
2727
gridFocus: Signal<GridFocus<TestGridCell> | undefined>,
2828
opts: {id: string; rowspan?: number; colspan?: number},
2929
): TestGridCell {
3030
const el = document.createElement('div');
3131
spyOn(el, 'focus').and.callThrough();
32-
let coordinates: Signal<{row: number; column: number}> = signal({row: -1, column: -1});
32+
let coordinates: Signal<RowCol> = signal({row: -1, col: -1});
3333
const cell: TestGridCell = {
3434
id: signal(opts.id),
3535
element: signal(el as HTMLElement),
@@ -39,13 +39,13 @@ function createTestCell(
3939
rowindex: signal(-1),
4040
colindex: signal(-1),
4141
};
42-
coordinates = computed(() => gridFocus()?.getCoordinates(cell) ?? {row: -1, column: -1});
42+
coordinates = computed(() => gridFocus()?.getCoordinates(cell) ?? {row: -1, col: -1});
4343
cell.rowindex = computed(() => coordinates().row);
44-
cell.colindex = computed(() => coordinates().column);
44+
cell.colindex = computed(() => coordinates().col);
4545
return cell;
4646
}
4747

48-
function createTestCells(
48+
export function createTestCells(
4949
gridFocus: Signal<GridFocus<TestGridCell> | undefined>,
5050
numRows: number,
5151
numCols: number,
@@ -60,7 +60,7 @@ function createTestCells(
6060
}
6161

6262
// Main helper function to instantiate GridFocus and its dependencies for testing
63-
function setupGridFocus(inputs: TestSetupInputs = {}): {
63+
export function setupGridFocus(inputs: TestSetupInputs = {}): {
6464
cells: TestGridCell[][];
6565
gridFocus: GridFocus<TestGridCell>;
6666
} {
@@ -70,7 +70,7 @@ function setupGridFocus(inputs: TestSetupInputs = {}): {
7070
const gridFocus = inputs.gridFocus ?? signal<GridFocus<TestGridCell> | undefined>(undefined);
7171
const cells = inputs.cells ?? createTestCells(gridFocus, numRows, numCols);
7272

73-
const activeCoords = inputs.activeCoords ?? signal({row: 0, column: 0});
73+
const activeCoords = inputs.activeCoords ?? signal({row: 0, col: 0});
7474
const focusMode = signal<'roving' | 'activedescendant'>(
7575
inputs.focusMode ? inputs.focusMode() : 'roving',
7676
);
@@ -95,20 +95,20 @@ function setupGridFocus(inputs: TestSetupInputs = {}): {
9595

9696
describe('GridFocus', () => {
9797
describe('Initialization', () => {
98-
it('should initialize with activeCell at {row: 0, column: 0} by default', () => {
98+
it('should initialize with activeCell at {row: 0, col: 0} by default', () => {
9999
const {gridFocus} = setupGridFocus();
100-
expect(gridFocus.inputs.activeCoords()).toEqual({row: 0, column: 0});
100+
expect(gridFocus.inputs.activeCoords()).toEqual({row: 0, col: 0});
101101
});
102102

103103
it('should compute activeCell based on activeCell', () => {
104104
const {gridFocus, cells} = setupGridFocus({
105-
activeCoords: signal({row: 1, column: 1}),
105+
activeCoords: signal({row: 1, col: 1}),
106106
});
107107
expect(gridFocus.activeCell()).toBe(cells[1][1]);
108108
});
109109

110110
it('should compute activeCell correctly when rowspan and colspan are set', () => {
111-
const activeCoords = signal({row: 0, column: 0});
111+
const activeCoords = signal({row: 0, col: 0});
112112
const gridFocusSignal = signal<GridFocus<TestGridCell> | undefined>(undefined);
113113

114114
// Visualization of this irregular grid.
@@ -130,24 +130,58 @@ describe('GridFocus', () => {
130130
gridFocus: gridFocusSignal,
131131
});
132132

133-
activeCoords.set({row: 0, column: 0});
133+
activeCoords.set({row: 0, col: 0});
134134
expect(gridFocus.activeCell()).toBe(cell_0_0);
135-
activeCoords.set({row: 0, column: 1});
135+
activeCoords.set({row: 0, col: 1});
136136
expect(gridFocus.activeCell()).toBe(cell_0_0);
137-
activeCoords.set({row: 1, column: 0});
137+
activeCoords.set({row: 1, col: 0});
138138
expect(gridFocus.activeCell()).toBe(cell_0_0);
139-
activeCoords.set({row: 1, column: 1});
139+
activeCoords.set({row: 1, col: 1});
140140
expect(gridFocus.activeCell()).toBe(cell_0_0);
141141

142-
activeCoords.set({row: 0, column: 2});
142+
activeCoords.set({row: 0, col: 2});
143143
expect(gridFocus.activeCell()).toBe(cell_0_2);
144144

145-
activeCoords.set({row: 1, column: 2});
145+
activeCoords.set({row: 1, col: 2});
146146
expect(gridFocus.activeCell()).toBe(cell_1_2);
147147
});
148+
149+
it('should compute rowCount and colCount correctly', () => {
150+
const {gridFocus} = setupGridFocus({
151+
numRows: 2,
152+
numCols: 3,
153+
});
154+
expect(gridFocus.rowCount()).toBe(2);
155+
expect(gridFocus.colCount()).toBe(3);
156+
});
157+
158+
it('should compute rowCount and colCount correctly when rowspan and colspan are set', () => {
159+
const gridFocusSignal = signal<GridFocus<TestGridCell> | undefined>(undefined);
160+
161+
// Visualization of this irregular grid.
162+
//
163+
// +---+---+---+
164+
// | |0,2|
165+
// + 0,0 +---+
166+
// | |1,2|
167+
// +---+---+---+
168+
//
169+
const cell_0_0 = createTestCell(gridFocusSignal, {id: `cell-0-0`, rowspan: 2, colspan: 2});
170+
const cell_0_2 = createTestCell(gridFocusSignal, {id: `cell-0-2`});
171+
const cell_1_2 = createTestCell(gridFocusSignal, {id: `cell-1-2`});
172+
const cells = signal<TestGridCell[][]>([[cell_0_0, cell_0_2], [cell_1_2]]);
173+
174+
const {gridFocus} = setupGridFocus({
175+
cells,
176+
gridFocus: gridFocusSignal,
177+
});
178+
179+
expect(gridFocus.rowCount()).toBe(2);
180+
expect(gridFocus.colCount()).toBe(3);
181+
});
148182
});
149183

150-
describe('isGridDisabled()', () => {
184+
describe('isGridDisabled', () => {
151185
it('should return true if inputs.disabled is true', () => {
152186
const {gridFocus} = setupGridFocus({disabled: signal(true)});
153187
expect(gridFocus.isGridDisabled()).toBeTrue();
@@ -171,7 +205,7 @@ describe('GridFocus', () => {
171205
});
172206
});
173207

174-
describe('getActiveDescendant()', () => {
208+
describe('getActiveDescendant', () => {
175209
it('should return undefined if focusMode is "roving"', () => {
176210
const {gridFocus} = setupGridFocus({focusMode: signal('roving')});
177211
expect(gridFocus.getActiveDescendant()).toBeUndefined();
@@ -188,13 +222,13 @@ describe('GridFocus', () => {
188222
it('should return the activeCell id if focusMode is "activedescendant"', () => {
189223
const {gridFocus, cells} = setupGridFocus({
190224
focusMode: signal('activedescendant'),
191-
activeCoords: signal({row: 2, column: 2}),
225+
activeCoords: signal({row: 2, col: 2}),
192226
});
193227
expect(gridFocus.getActiveDescendant()).toBe(cells[2][2].id());
194228
});
195229
});
196230

197-
describe('getGridTabindex()', () => {
231+
describe('getGridTabindex', () => {
198232
it('should return 0 if grid is disabled', () => {
199233
const {gridFocus} = setupGridFocus({disabled: signal(true)});
200234
expect(gridFocus.getGridTabindex()).toBe(0);
@@ -211,7 +245,7 @@ describe('GridFocus', () => {
211245
});
212246
});
213247

214-
describe('getCellTabindex(cell)', () => {
248+
describe('getCellTabindex', () => {
215249
it('should return -1 if grid is disabled', () => {
216250
const {gridFocus, cells} = setupGridFocus({
217251
numRows: 1,
@@ -247,7 +281,7 @@ describe('GridFocus', () => {
247281
});
248282
});
249283

250-
describe('isFocusable(cell)', () => {
284+
describe('isFocusable', () => {
251285
it('should return true if cell is not disabled', () => {
252286
const {gridFocus, cells} = setupGridFocus({
253287
numRows: 1,
@@ -283,65 +317,127 @@ describe('GridFocus', () => {
283317
});
284318
});
285319

286-
describe('focus(cell)', () => {
320+
describe('focusCoordinates', () => {
321+
it('should return false and not change state if grid is disabled', () => {
322+
const activeCoords = signal({row: 0, col: 0});
323+
const {gridFocus, cells} = setupGridFocus({
324+
activeCoords,
325+
disabled: signal(true),
326+
});
327+
328+
const success = gridFocus.focusCoordinates({row: 1, col: 0});
329+
330+
expect(success).toBeFalse();
331+
expect(activeCoords()).toEqual({row: 0, col: 0});
332+
expect(cells[1][0].element().focus).not.toHaveBeenCalled();
333+
});
334+
335+
it('should return false and not change state if cell is not focusable', () => {
336+
const activeCoords = signal({row: 0, col: 0});
337+
const {gridFocus, cells} = setupGridFocus({activeCoords});
338+
cells[1][0].disabled.set(true);
339+
340+
const success = gridFocus.focusCoordinates({row: 1, col: 0});
341+
342+
expect(success).toBeFalse();
343+
expect(activeCoords()).toEqual({row: 0, col: 0});
344+
expect(cells[1][0].element().focus).not.toHaveBeenCalled();
345+
});
346+
347+
it('should focus cell, update activeCell and prevActiveCell in "roving" mode', () => {
348+
const activeCoords = signal({row: 0, col: 0});
349+
const {gridFocus, cells} = setupGridFocus({
350+
activeCoords,
351+
focusMode: signal('roving'),
352+
});
353+
354+
const success = gridFocus.focusCoordinates({row: 1, col: 0});
355+
356+
expect(success).toBeTrue();
357+
expect(activeCoords()).toEqual({row: 1, col: 0});
358+
expect(cells[1][0].element().focus).toHaveBeenCalled();
359+
360+
expect(gridFocus.activeCell()).toBe(cells[1][0]);
361+
expect(gridFocus.prevActiveCoords()).toEqual({row: 0, col: 0});
362+
});
363+
364+
it('should update activeCell and prevActiveCell but not call element.focus in "activedescendant" mode', () => {
365+
const activeCoords = signal({row: 0, col: 0});
366+
const {gridFocus, cells} = setupGridFocus({
367+
activeCoords,
368+
focusMode: signal('activedescendant'),
369+
});
370+
371+
const success = gridFocus.focusCoordinates({row: 1, col: 0});
372+
373+
expect(success).toBeTrue();
374+
expect(activeCoords()).toEqual({row: 1, col: 0});
375+
expect(cells[1][0].element().focus).not.toHaveBeenCalled();
376+
377+
expect(gridFocus.activeCell()).toBe(cells[1][0]);
378+
expect(gridFocus.prevActiveCoords()).toEqual({row: 0, col: 0});
379+
});
380+
});
381+
382+
describe('focusCell', () => {
287383
it('should return false and not change state if grid is disabled', () => {
288-
const activeCoords = signal({row: 0, column: 0});
384+
const activeCoords = signal({row: 0, col: 0});
289385
const {gridFocus, cells} = setupGridFocus({
290386
activeCoords,
291387
disabled: signal(true),
292388
});
293389

294-
const success = gridFocus.focus({row: 1, column: 0});
390+
const success = gridFocus.focusCell(cells[1][0]);
295391

296392
expect(success).toBeFalse();
297-
expect(activeCoords()).toEqual({row: 0, column: 0});
393+
expect(activeCoords()).toEqual({row: 0, col: 0});
298394
expect(cells[1][0].element().focus).not.toHaveBeenCalled();
299395
});
300396

301397
it('should return false and not change state if cell is not focusable', () => {
302-
const activeCoords = signal({row: 0, column: 0});
398+
const activeCoords = signal({row: 0, col: 0});
303399
const {gridFocus, cells} = setupGridFocus({activeCoords});
304400
cells[1][0].disabled.set(true);
305401

306-
const success = gridFocus.focus({row: 1, column: 0});
402+
const success = gridFocus.focusCell(cells[1][0]);
307403

308404
expect(success).toBeFalse();
309-
expect(activeCoords()).toEqual({row: 0, column: 0});
405+
expect(activeCoords()).toEqual({row: 0, col: 0});
310406
expect(cells[1][0].element().focus).not.toHaveBeenCalled();
311407
});
312408

313409
it('should focus cell, update activeCell and prevActiveCell in "roving" mode', () => {
314-
const activeCoords = signal({row: 0, column: 0});
410+
const activeCoords = signal({row: 0, col: 0});
315411
const {gridFocus, cells} = setupGridFocus({
316412
activeCoords,
317413
focusMode: signal('roving'),
318414
});
319415

320-
const success = gridFocus.focus({row: 1, column: 0});
416+
const success = gridFocus.focusCell(cells[1][0]);
321417

322418
expect(success).toBeTrue();
323-
expect(activeCoords()).toEqual({row: 1, column: 0});
419+
expect(activeCoords()).toEqual({row: 1, col: 0});
324420
expect(cells[1][0].element().focus).toHaveBeenCalled();
325421

326422
expect(gridFocus.activeCell()).toBe(cells[1][0]);
327-
expect(gridFocus.prevActiveCoords()).toEqual({row: 0, column: 0});
423+
expect(gridFocus.prevActiveCoords()).toEqual({row: 0, col: 0});
328424
});
329425

330426
it('should update activeCell and prevActiveCell but not call element.focus in "activedescendant" mode', () => {
331-
const activeCoords = signal({row: 0, column: 0});
427+
const activeCoords = signal({row: 0, col: 0});
332428
const {gridFocus, cells} = setupGridFocus({
333429
activeCoords,
334430
focusMode: signal('activedescendant'),
335431
});
336432

337-
const success = gridFocus.focus({row: 1, column: 0});
433+
const success = gridFocus.focusCell(cells[1][0]);
338434

339435
expect(success).toBeTrue();
340-
expect(activeCoords()).toEqual({row: 1, column: 0});
436+
expect(activeCoords()).toEqual({row: 1, col: 0});
341437
expect(cells[1][0].element().focus).not.toHaveBeenCalled();
342438

343439
expect(gridFocus.activeCell()).toBe(cells[1][0]);
344-
expect(gridFocus.prevActiveCoords()).toEqual({row: 0, column: 0});
440+
expect(gridFocus.prevActiveCoords()).toEqual({row: 0, col: 0});
345441
});
346442
});
347443
});

0 commit comments

Comments
 (0)