Skip to content

Commit 7296503

Browse files
committed
fixup! feat(aria/grid): create the aria grid
1 parent 7314b4f commit 7296503

File tree

2 files changed

+59
-162
lines changed

2 files changed

+59
-162
lines changed

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

Lines changed: 45 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ type ExactlyOneKey<T, K extends keyof T = keyof T> = {
1919
/** Represents a directional change in the grid, either by row or by column. */
2020
type Delta = ExactlyOneKey<{row: -1 | 1; col: -1 | 1}>;
2121

22+
/** */
23+
export const Direction: Record<'Up' | 'Down' | 'Left' | 'Right', Delta> = {
24+
Up: {row: -1},
25+
Down: {row: 1},
26+
Left: {col: -1},
27+
Right: {col: 1},
28+
} as const;
29+
2230
/** Represents an item in a collection, such as a listbox option, than can be navigated to. */
2331
export interface GridNavigationCell extends GridFocusCell {}
2432

@@ -59,47 +67,15 @@ export class GridNavigation<T extends GridNavigationCell> {
5967
return this.inputs.gridFocus.focusCoordinates(coords);
6068
}
6169

62-
/** Gets the coordinates of the cell above the given coordinates. */
63-
peekUp(fromCoords: RowCol): RowCol | undefined {
64-
return this._peekDirectional({row: -1}, fromCoords, this.inputs.rowWrap());
65-
}
66-
67-
/** Navigates to the item above the current item. */
68-
up(): boolean {
69-
const nextCoords = this.peekUp(this.inputs.gridFocus.activeCoords());
70-
return !!nextCoords && this.gotoCoords(nextCoords);
71-
}
72-
73-
/** Gets the coordinates of the cell below the given coordinates. */
74-
peekDown(fromCoords: RowCol): RowCol | undefined {
75-
return this._peekDirectional({row: 1}, fromCoords, this.inputs.rowWrap());
76-
}
77-
78-
/** Navigates to the item below the current item. */
79-
down(): boolean {
80-
const nextCoords = this.peekDown(this.inputs.gridFocus.activeCoords());
81-
return !!nextCoords && this.gotoCoords(nextCoords);
82-
}
83-
84-
/** Gets the coordinates of the cell to the left of the given coordinates. */
85-
peekLeft(fromCoords: RowCol): RowCol | undefined {
86-
return this._peekDirectional({col: -1}, fromCoords, this.inputs.colWrap());
70+
/** */
71+
peek(direction: Delta, fromCoords: RowCol): RowCol | undefined {
72+
const wrap = direction.row !== undefined ? this.inputs.rowWrap() : this.inputs.colWrap();
73+
return this._peekDirectional(direction, fromCoords, wrap);
8774
}
8875

89-
/** Navigates to the item to the left of the current item. */
90-
left(): boolean {
91-
const nextCoords = this.peekLeft(this.inputs.gridFocus.activeCoords());
92-
return !!nextCoords && this.gotoCoords(nextCoords);
93-
}
94-
95-
/** Gets the coordinates of the cell to the right of the given coordinates. */
96-
peekRight(fromCoords: RowCol): RowCol | undefined {
97-
return this._peekDirectional({col: 1}, fromCoords, this.inputs.colWrap());
98-
}
99-
100-
/** Navigates to the item to the right of the current item. */
101-
right(): boolean {
102-
const nextCoords = this.peekRight(this.inputs.gridFocus.activeCoords());
76+
/** */
77+
advance(direction: Delta): boolean {
78+
const nextCoords = this.peek(direction, this.inputs.gridFocus.activeCoords());
10379
return !!nextCoords && this.gotoCoords(nextCoords);
10480
}
10581

@@ -108,14 +84,13 @@ export class GridNavigation<T extends GridNavigationCell> {
10884
* If a row is not provided, searches the entire grid.
10985
*/
11086
peekFirst(row?: number): RowCol | undefined {
111-
const delta: Delta = {col: 1};
112-
const startCoords = {
87+
const fromCoords = {
11388
row: row ?? 0,
11489
col: -1,
11590
};
11691
return row === undefined
117-
? this._peekContinuous(delta, startCoords)
118-
: this._peek(delta, startCoords);
92+
? this._peekDirectional(Direction.Right, fromCoords, 'continuous')
93+
: this._peekDirectional(Direction.Right, fromCoords, 'nowrap');
11994
}
12095

12196
/**
@@ -132,14 +107,13 @@ export class GridNavigation<T extends GridNavigationCell> {
132107
* If a row is not provided, searches the entire grid.
133108
*/
134109
peekLast(row?: number): RowCol | undefined {
135-
const delta: Delta = {col: -1};
136-
const startCoords = {
110+
const fromCoords = {
137111
row: row ?? this.inputs.grid.maxRowCount() - 1,
138112
col: this.inputs.grid.maxColCount(),
139113
};
140114
return row === undefined
141-
? this._peekContinuous(delta, startCoords)
142-
: this._peek(delta, startCoords);
115+
? this._peekDirectional(Direction.Left, fromCoords, 'continuous')
116+
: this._peekDirectional(Direction.Left, fromCoords, 'nowrap');
143117
}
144118

145119
/**
@@ -152,120 +126,56 @@ export class GridNavigation<T extends GridNavigationCell> {
152126
}
153127

154128
/**
155-
* Finds the next focusable cell in a given direction, with continuous wrapping.
156-
* This means that when the end of a row/column is reached, it wraps to the
157-
* beginning of the next/previous row/column.
129+
* Finds the next focusable cell in a given direction based on the wrapping behavior.
158130
*/
159-
private _peekContinuous(delta: Delta, startCoords: RowCol): RowCol | undefined {
160-
const startCell = this.inputs.grid.getCell(startCoords);
131+
private _peekDirectional(
132+
delta: Delta,
133+
fromCoords: RowCol,
134+
wrap: 'continuous' | 'loop' | 'nowrap',
135+
): RowCol | undefined {
136+
const fromCell = this.inputs.grid.getCell(fromCoords);
161137
const maxRowCount = this.inputs.grid.maxRowCount();
162138
const maxColCount = this.inputs.grid.maxColCount();
163139
const rowDelta = delta.row ?? 0;
164140
const colDelta = delta.col ?? 0;
165141
const generalDelta = delta.row ?? delta.col;
166-
let nextCoords = {...startCoords};
142+
let nextCoords = {...fromCoords};
167143

168144
for (let step = 0; step < this._maxSteps(); step++) {
169145
const isWrapping =
170146
nextCoords.col + colDelta < 0 ||
171147
nextCoords.col + colDelta >= maxColCount ||
172148
nextCoords.row + rowDelta < 0 ||
173149
nextCoords.row + rowDelta >= maxRowCount;
174-
const rowStep = isWrapping ? generalDelta : rowDelta;
175-
const colStep = isWrapping ? generalDelta : colDelta;
176150

177-
nextCoords = {
178-
row: (nextCoords.row + rowStep + maxRowCount) % maxRowCount,
179-
col: (nextCoords.col + colStep + maxColCount) % maxColCount,
180-
};
151+
if (wrap === 'nowrap' && isWrapping) return;
181152

182-
// Back to original coordinates.
183-
if (nextCoords.row === startCoords.row && nextCoords.col === startCoords.col) {
184-
return undefined;
185-
}
153+
if (wrap === 'continuous') {
154+
const rowStep = isWrapping ? generalDelta : rowDelta;
155+
const colStep = isWrapping ? generalDelta : colDelta;
186156

187-
const nextCell = this.inputs.grid.getCell(nextCoords);
188-
if (
189-
nextCell !== undefined &&
190-
nextCell !== startCell &&
191-
this.inputs.gridFocus.isFocusable(nextCell)
192-
) {
193-
return nextCoords;
157+
nextCoords = {
158+
row: (nextCoords.row + rowStep + maxRowCount) % maxRowCount,
159+
col: (nextCoords.col + colStep + maxColCount) % maxColCount,
160+
};
194161
}
195-
}
196162

197-
return undefined;
198-
}
199-
200-
/**
201-
* Finds the next focusable cell in a given direction, with loop wrapping.
202-
* This means that when the end of a row/column is reached, it wraps to the
203-
* beginning of the same row/column.
204-
*/
205-
private _peekLoop(delta: Delta, startCoords: RowCol): RowCol | undefined {
206-
const startCell = this.inputs.grid.getCell(startCoords);
207-
const maxRowCount = this.inputs.grid.maxRowCount();
208-
const maxColCount = this.inputs.grid.maxColCount();
209-
const rowDelta = delta.row ?? 0;
210-
const colDelta = delta.col ?? 0;
211-
let nextCoords = {...startCoords};
212-
213-
for (let step = 0; step < this._maxSteps(); step++) {
214-
nextCoords = {
215-
row: (nextCoords.row + rowDelta + maxRowCount) % maxRowCount,
216-
col: (nextCoords.col + colDelta + maxColCount) % maxColCount,
217-
};
218-
219-
// Back to original coordinates.
220-
if (nextCoords.row === startCoords.row && nextCoords.col === startCoords.col) {
221-
return undefined;
222-
}
223-
224-
const nextCell = this.inputs.grid.getCell(nextCoords);
225-
if (
226-
nextCell !== undefined &&
227-
nextCell !== startCell &&
228-
this.inputs.gridFocus.isFocusable(nextCell)
229-
) {
230-
return nextCoords;
163+
if (wrap === 'loop') {
164+
nextCoords = {
165+
row: (nextCoords.row + rowDelta + maxRowCount) % maxRowCount,
166+
col: (nextCoords.col + colDelta + maxColCount) % maxColCount,
167+
};
231168
}
232-
}
233-
234-
return undefined;
235-
}
236-
237-
/**
238-
* Finds the next focusable cell in a given direction, without wrapping.
239-
* This means that when the end of a row/column is reached, it stops.
240-
*/
241-
private _peek(delta: Delta, startCoords: RowCol): RowCol | undefined {
242-
const startCell = this.inputs.grid.getCell(startCoords);
243-
const maxRowCount = this.inputs.grid.maxRowCount();
244-
const maxColCount = this.inputs.grid.maxColCount();
245-
const rowDelta = delta.row ?? 0;
246-
const colDelta = delta.col ?? 0;
247-
let nextCoords = {...startCoords};
248-
249-
for (let step = 0; step < this._maxSteps(); step++) {
250-
nextCoords = {
251-
row: nextCoords.row + rowDelta,
252-
col: nextCoords.col + colDelta,
253-
};
254169

255-
// Out of boundary.
256-
if (
257-
nextCoords.row < 0 ||
258-
nextCoords.row >= maxRowCount ||
259-
nextCoords.col < 0 ||
260-
nextCoords.col >= maxColCount
261-
) {
170+
// Back to original coordinates.
171+
if (nextCoords.row === fromCoords.row && nextCoords.col === fromCoords.col) {
262172
return undefined;
263173
}
264174

265175
const nextCell = this.inputs.grid.getCell(nextCoords);
266176
if (
267177
nextCell !== undefined &&
268-
nextCell !== startCell &&
178+
nextCell !== fromCell &&
269179
this.inputs.gridFocus.isFocusable(nextCell)
270180
) {
271181
return nextCoords;
@@ -274,22 +184,4 @@ export class GridNavigation<T extends GridNavigationCell> {
274184

275185
return undefined;
276186
}
277-
278-
/**
279-
* Finds the next focusable cell in a given direction based on the wrapping behavior.
280-
*/
281-
private _peekDirectional(
282-
delta: Delta,
283-
fromCoords: RowCol,
284-
wrap: 'continuous' | 'loop' | 'nowrap',
285-
): RowCol | undefined {
286-
switch (wrap) {
287-
case 'nowrap':
288-
return this._peek(delta, fromCoords);
289-
case 'loop':
290-
return this._peekLoop(delta, fromCoords);
291-
case 'continuous':
292-
return this._peekContinuous(delta, fromCoords);
293-
}
294-
}
295187
}

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

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ import {computed, linkedSignal} from '@angular/core';
1010
import {SignalLike} from '../signal-like/signal-like';
1111
import {GridData, BaseGridCell, GridDataInputs, RowCol} from './grid-data';
1212
import {GridFocus, GridFocusCell, GridFocusInputs} from './grid-focus';
13-
import {GridNavigation, GridNavigationCell, GridNavigationInputs} from './grid-navigation';
13+
import {
14+
Direction,
15+
GridNavigation,
16+
GridNavigationCell,
17+
GridNavigationInputs,
18+
} from './grid-navigation';
1419
import {GridSelectionCell, GridSelectionInputs, GridSelection} from './grid-selection';
1520

1621
/** A type that represents a cell in a grid, combining all cell-related interfaces. */
@@ -86,51 +91,51 @@ export class Grid<T extends GridCell> {
8691

8792
/** Navigates to the cell above the currently active cell. */
8893
up(): boolean {
89-
return this.navigationBehavior.up();
94+
return this.navigationBehavior.advance(Direction.Up);
9095
}
9196

9297
/** Extends the selection to the cell above the selection anchor. */
9398
rangeSelectUp(): void {
94-
const coords = this.navigationBehavior.peekUp(this.selectionAnchor());
99+
const coords = this.navigationBehavior.peek(Direction.Up, this.selectionAnchor());
95100
if (coords === undefined) return;
96101

97102
this._rangeSelectCoords(coords);
98103
}
99104

100105
/** Navigates to the cell below the currently active cell. */
101106
down(): boolean {
102-
return this.navigationBehavior.down();
107+
return this.navigationBehavior.advance(Direction.Down);
103108
}
104109

105110
/** Extends the selection to the cell below the selection anchor. */
106111
rangeSelectDown(): void {
107-
const coords = this.navigationBehavior.peekDown(this.selectionAnchor());
112+
const coords = this.navigationBehavior.peek(Direction.Down, this.selectionAnchor());
108113
if (coords === undefined) return;
109114

110115
this._rangeSelectCoords(coords);
111116
}
112117

113118
/** Navigates to the cell to the left of the currently active cell. */
114119
left(): boolean {
115-
return this.navigationBehavior.left();
120+
return this.navigationBehavior.advance(Direction.Left);
116121
}
117122

118123
/** Extends the selection to the cell to the left of the selection anchor. */
119124
rangeSelectLeft(): void {
120-
const coords = this.navigationBehavior.peekLeft(this.selectionAnchor());
125+
const coords = this.navigationBehavior.peek(Direction.Left, this.selectionAnchor());
121126
if (coords === undefined) return;
122127

123128
this._rangeSelectCoords(coords);
124129
}
125130

126131
/** Navigates to the cell to the right of the currently active cell. */
127132
right(): boolean {
128-
return this.navigationBehavior.right();
133+
return this.navigationBehavior.advance(Direction.Right);
129134
}
130135

131136
/** Extends the selection to the cell to the right of the selection anchor. */
132137
rangeSelectRight(): void {
133-
const coords = this.navigationBehavior.peekRight(this.selectionAnchor());
138+
const coords = this.navigationBehavior.peek(Direction.Right, this.selectionAnchor());
134139
if (coords === undefined) return;
135140

136141
this._rangeSelectCoords(coords);

0 commit comments

Comments
 (0)