Skip to content

Commit d479e1d

Browse files
committed
fix(aria/grid): rework selection and add calendar example
1 parent 1c6a285 commit d479e1d

File tree

20 files changed

+1158
-288
lines changed

20 files changed

+1158
-288
lines changed

src/aria/grid/grid.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ export class Grid {
7777
/** The wrapping behavior for keyboard navigation along the column axis. */
7878
readonly colWrap = input<'continuous' | 'loop' | 'nowrap'>('loop');
7979

80+
/** Whether multiple cells in the grid can be selected at once. */
81+
readonly multi = input(false, {transform: booleanAttribute});
82+
83+
/** The selection strategy used by the grid. */
84+
readonly selectionMode = input<'follow' | 'explicit'>('follow');
85+
8086
/** The UI pattern for the grid. */
8187
readonly _pattern = new GridPattern({
8288
...this,
@@ -163,6 +169,7 @@ export class GridRow {
163169
'[attr.rowspan]': '_pattern.rowSpan()',
164170
'[attr.colspan]': '_pattern.colSpan()',
165171
'[attr.data-active]': '_pattern.active()',
172+
'[attr.data-anchor]': '_pattern.anchor()',
166173
'[attr.aria-disabled]': '_pattern.disabled()',
167174
'[attr.aria-rowspan]': '_pattern.rowSpan()',
168175
'[attr.aria-colspan]': '_pattern.colSpan()',

src/aria/private/behaviors/grid/grid-navigation.spec.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,16 @@ describe('GridNavigation', () => {
164164

165165
expect(nextCoords).toBeUndefined();
166166
});
167+
168+
it('should get disabled cells when allowDisabled is true', () => {
169+
gridNav.gotoCoords({row: 1, col: 0});
170+
cells[0][0].disabled.set(true);
171+
172+
const nextCoords = gridNav.peek(direction.Up, gridFocus.activeCoords(), 'nowrap', true);
173+
174+
expect(nextCoords).toEqual({row: 0, col: 0});
175+
expect(gridNav.peek(direction.Up, gridFocus.activeCoords(), 'nowrap')).toBeUndefined();
176+
});
167177
});
168178

169179
describe('down', () => {
@@ -192,6 +202,16 @@ describe('GridNavigation', () => {
192202

193203
expect(nextCoords).toBeUndefined();
194204
});
205+
206+
it('should get disabled cells when allowDisabled is true', () => {
207+
gridNav.gotoCoords({row: 1, col: 0});
208+
cells[2][0].disabled.set(true);
209+
210+
const nextCoords = gridNav.peek(direction.Down, gridFocus.activeCoords(), 'nowrap', true);
211+
212+
expect(nextCoords).toEqual({row: 2, col: 0});
213+
expect(gridNav.peek(direction.Down, gridFocus.activeCoords(), 'nowrap')).toBeUndefined();
214+
});
195215
});
196216

197217
describe('left', () => {
@@ -222,6 +242,16 @@ describe('GridNavigation', () => {
222242

223243
expect(nextCoords).toBeUndefined();
224244
});
245+
246+
it('should get disabled cells when allowDisabled is true', () => {
247+
gridNav.gotoCoords({row: 0, col: 1});
248+
cells[0][0].disabled.set(true);
249+
250+
const nextCoords = gridNav.peek(direction.Left, gridFocus.activeCoords(), 'nowrap', true);
251+
252+
expect(nextCoords).toEqual({row: 0, col: 0});
253+
expect(gridNav.peek(direction.Left, gridFocus.activeCoords(), 'nowrap')).toBeUndefined();
254+
});
225255
});
226256

227257
describe('right', () => {
@@ -252,6 +282,16 @@ describe('GridNavigation', () => {
252282

253283
expect(nextCoords).toBeUndefined();
254284
});
285+
286+
it('should get disabled cells when allowDisabled is true', () => {
287+
gridNav.gotoCoords({row: 0, col: 1});
288+
cells[0][2].disabled.set(true);
289+
290+
const nextCoords = gridNav.peek(direction.Right, gridFocus.activeCoords(), 'nowrap', true);
291+
292+
expect(nextCoords).toEqual({row: 0, col: 2});
293+
expect(gridNav.peek(direction.Right, gridFocus.activeCoords(), 'nowrap')).toBeUndefined();
294+
});
255295
});
256296
});
257297

@@ -1882,6 +1922,17 @@ describe('GridNavigation', () => {
18821922
expect(gridFocus.activeCell()).toBe(cells[1][1]);
18831923
expect(gridFocus.activeCoords()).toEqual({row: 1, col: 1});
18841924
});
1925+
1926+
it('should get disabled cells when allowDisabled is true', () => {
1927+
const cells = createTestGrid(createGridA);
1928+
const {gridNav} = setupGridNavigation(signal(cells));
1929+
cells[0][0].disabled.set(true);
1930+
1931+
const firstCoords = gridNav.peekFirst(undefined, true);
1932+
1933+
expect(firstCoords).toEqual({row: 0, col: 0});
1934+
expect(gridNav.peekFirst()).toEqual({row: 0, col: 1});
1935+
});
18851936
});
18861937

18871938
describe('last/peekLast', () => {
@@ -1920,5 +1971,16 @@ describe('GridNavigation', () => {
19201971
expect(gridFocus.activeCell()!.id()).toBe('cell-1-1');
19211972
expect(gridFocus.activeCoords()).toEqual({row: 1, col: 2});
19221973
});
1974+
1975+
it('should get disabled cells when allowDisabled is true', () => {
1976+
const cells = createTestGrid(createGridA);
1977+
const {gridNav} = setupGridNavigation(signal(cells));
1978+
cells[2][2].disabled.set(true);
1979+
1980+
const lastCoords = gridNav.peekLast(undefined, true);
1981+
1982+
expect(lastCoords).toEqual({row: 2, col: 2});
1983+
expect(gridNav.peekLast()).toEqual({row: 2, col: 1});
1984+
});
19231985
});
19241986
});

src/aria/private/behaviors/grid/grid-navigation.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,14 @@ export class GridNavigation<T extends GridNavigationCell> {
7373
/**
7474
* Gets the coordinates of the next focusable cell in a given direction, without changing focus.
7575
*/
76-
peek(direction: Delta, fromCoords: RowCol, wrap?: WrapStrategy): RowCol | undefined {
76+
peek(
77+
direction: Delta,
78+
fromCoords: RowCol,
79+
wrap?: WrapStrategy,
80+
allowDisabled?: boolean,
81+
): RowCol | undefined {
7782
wrap = wrap ?? (direction.row !== undefined ? this.inputs.rowWrap() : this.inputs.colWrap());
78-
return this._peekDirectional(direction, fromCoords, wrap);
83+
return this._peekDirectional(direction, fromCoords, wrap, allowDisabled);
7984
}
8085

8186
/**
@@ -90,14 +95,14 @@ export class GridNavigation<T extends GridNavigationCell> {
9095
* Gets the coordinates of the first focusable cell.
9196
* If a row is not provided, searches the entire grid.
9297
*/
93-
peekFirst(row?: number): RowCol | undefined {
98+
peekFirst(row?: number, allowDisabled?: boolean): RowCol | undefined {
9499
const fromCoords = {
95100
row: row ?? 0,
96101
col: -1,
97102
};
98103
return row === undefined
99-
? this._peekDirectional(direction.Right, fromCoords, 'continuous')
100-
: this._peekDirectional(direction.Right, fromCoords, 'nowrap');
104+
? this._peekDirectional(direction.Right, fromCoords, 'continuous', allowDisabled)
105+
: this._peekDirectional(direction.Right, fromCoords, 'nowrap', allowDisabled);
101106
}
102107

103108
/**
@@ -113,14 +118,14 @@ export class GridNavigation<T extends GridNavigationCell> {
113118
* Gets the coordinates of the last focusable cell.
114119
* If a row is not provided, searches the entire grid.
115120
*/
116-
peekLast(row?: number): RowCol | undefined {
121+
peekLast(row?: number, allowDisabled?: boolean): RowCol | undefined {
117122
const fromCoords = {
118123
row: row ?? this.inputs.grid.maxRowCount() - 1,
119124
col: this.inputs.grid.maxColCount(),
120125
};
121126
return row === undefined
122-
? this._peekDirectional(direction.Left, fromCoords, 'continuous')
123-
: this._peekDirectional(direction.Left, fromCoords, 'nowrap');
127+
? this._peekDirectional(direction.Left, fromCoords, 'continuous', allowDisabled)
128+
: this._peekDirectional(direction.Left, fromCoords, 'nowrap', allowDisabled);
124129
}
125130

126131
/**
@@ -139,6 +144,7 @@ export class GridNavigation<T extends GridNavigationCell> {
139144
delta: Delta,
140145
fromCoords: RowCol,
141146
wrap: 'continuous' | 'loop' | 'nowrap',
147+
allowDisabled: boolean = false,
142148
): RowCol | undefined {
143149
const fromCell = this.inputs.grid.getCell(fromCoords);
144150
const maxRowCount = this.inputs.grid.maxRowCount();
@@ -190,7 +196,7 @@ export class GridNavigation<T extends GridNavigationCell> {
190196
if (
191197
nextCell !== undefined &&
192198
nextCell !== fromCell &&
193-
this.inputs.gridFocus.isFocusable(nextCell)
199+
(allowDisabled || this.inputs.gridFocus.isFocusable(nextCell))
194200
) {
195201
return nextCoords;
196202
}

src/aria/private/behaviors/grid/grid-selection.spec.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ function setupGridSelection(
5858
const gridSelection = new GridSelection({
5959
grid: gridData,
6060
gridFocus: gridFocus,
61+
// Unused by Grid Selection.
62+
enableSelection: signal(true),
63+
multi: signal(false),
64+
selectionMode: signal('explicit'),
6165
...gridFocusInputs,
6266
...inputs,
6367
});
@@ -207,4 +211,97 @@ describe('GridSelection', () => {
207211
expect(validCellIds.length).toBe(allCellIds.length - 2);
208212
});
209213
});
214+
215+
describe('undo', () => {
216+
it('should undo a select operation', () => {
217+
const cells = createTestGrid(createGridA);
218+
const {gridSelection} = setupGridSelection(signal(cells));
219+
220+
gridSelection.select({row: 1, col: 1});
221+
expect(cells[1][1].selected()).toBe(true);
222+
223+
gridSelection.undo();
224+
expect(cells[1][1].selected()).toBe(false);
225+
});
226+
227+
it('should undo a deselect operation', () => {
228+
const cells = createTestGrid(createGridA);
229+
const {gridSelection} = setupGridSelection(signal(cells));
230+
cells[1][1].selected.set(true);
231+
232+
gridSelection.deselect({row: 1, col: 1});
233+
expect(cells[1][1].selected()).toBe(false);
234+
235+
gridSelection.undo();
236+
expect(cells[1][1].selected()).toBe(true);
237+
});
238+
239+
it('should undo a toggle operation', () => {
240+
const cells = createTestGrid(createGridA);
241+
const {gridSelection} = setupGridSelection(signal(cells));
242+
cells[0][0].selected.set(true);
243+
244+
gridSelection.toggle({row: 0, col: 0}, {row: 0, col: 1});
245+
expect(cells[0][0].selected()).toBe(false);
246+
expect(cells[0][1].selected()).toBe(true);
247+
248+
gridSelection.undo();
249+
expect(cells[0][0].selected()).toBe(true);
250+
expect(cells[0][1].selected()).toBe(false);
251+
});
252+
253+
it('should undo a selectAll operation', () => {
254+
const cells = createTestGrid(createGridA);
255+
const {gridSelection} = setupGridSelection(signal(cells));
256+
257+
gridSelection.selectAll();
258+
expect(cells.flat().every(c => c.selected())).toBe(true);
259+
260+
gridSelection.undo();
261+
expect(cells.flat().every(c => !c.selected())).toBe(true);
262+
});
263+
264+
it('should undo a deselectAll operation', () => {
265+
const cells = createTestGrid(createGridA);
266+
const {gridSelection} = setupGridSelection(signal(cells));
267+
cells.flat().forEach(c => c.selected.set(true));
268+
269+
gridSelection.deselectAll();
270+
expect(cells.flat().every(c => !c.selected())).toBe(true);
271+
272+
gridSelection.undo();
273+
expect(cells.flat().every(c => c.selected())).toBe(true);
274+
});
275+
276+
it('should do nothing if there is nothing to undo', () => {
277+
const cells = createTestGrid(createGridA);
278+
const {gridSelection} = setupGridSelection(signal(cells));
279+
cells[1][1].selected.set(true);
280+
281+
gridSelection.undo();
282+
expect(cells[1][1].selected()).toBe(true);
283+
});
284+
285+
it('should only undo the last operation', () => {
286+
const cells = createTestGrid(createGridA);
287+
const {gridSelection} = setupGridSelection(signal(cells));
288+
289+
gridSelection.select({row: 0, col: 0});
290+
gridSelection.select({row: 1, col: 1});
291+
expect(cells[1][1].selected()).toBe(true);
292+
293+
gridSelection.undo();
294+
expect(cells[0][0].selected()).toBe(true);
295+
expect(cells[1][1].selected()).toBe(false);
296+
});
297+
298+
it('should do nothing after undoing once', () => {
299+
const cells = createTestGrid(createGridA);
300+
const {gridSelection} = setupGridSelection(signal(cells));
301+
gridSelection.select({row: 1, col: 1});
302+
gridSelection.undo();
303+
gridSelection.undo();
304+
expect(cells[1][1].selected()).toBe(false);
305+
});
306+
});
210307
});

0 commit comments

Comments
 (0)