@angular/aria 0.0.0 → 21.0.0-next.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +6 -0
- package/fesm2022/_widget-chunk.mjs +990 -0
- package/fesm2022/_widget-chunk.mjs.map +1 -0
- package/fesm2022/accordion.mjs +192 -0
- package/fesm2022/accordion.mjs.map +1 -0
- package/fesm2022/aria.mjs +7 -0
- package/fesm2022/aria.mjs.map +1 -0
- package/fesm2022/combobox.mjs +145 -0
- package/fesm2022/combobox.mjs.map +1 -0
- package/fesm2022/deferred-content.mjs +60 -0
- package/fesm2022/deferred-content.mjs.map +1 -0
- package/fesm2022/grid.mjs +213 -0
- package/fesm2022/grid.mjs.map +1 -0
- package/fesm2022/listbox.mjs +200 -0
- package/fesm2022/listbox.mjs.map +1 -0
- package/fesm2022/menu.mjs +302 -0
- package/fesm2022/menu.mjs.map +1 -0
- package/fesm2022/radio-group.mjs +197 -0
- package/fesm2022/radio-group.mjs.map +1 -0
- package/fesm2022/tabs.mjs +299 -0
- package/fesm2022/tabs.mjs.map +1 -0
- package/fesm2022/toolbar.mjs +218 -0
- package/fesm2022/toolbar.mjs.map +1 -0
- package/fesm2022/tree.mjs +288 -0
- package/fesm2022/tree.mjs.map +1 -0
- package/fesm2022/ui-patterns.mjs +2951 -0
- package/fesm2022/ui-patterns.mjs.map +1 -0
- package/package.json +91 -4
- package/types/_grid-chunk.d.ts +546 -0
- package/types/accordion.d.ts +92 -0
- package/types/aria.d.ts +6 -0
- package/types/combobox.d.ts +60 -0
- package/types/deferred-content.d.ts +38 -0
- package/types/grid.d.ts +111 -0
- package/types/listbox.d.ts +95 -0
- package/types/menu.d.ts +158 -0
- package/types/radio-group.d.ts +82 -0
- package/types/tabs.d.ts +156 -0
- package/types/toolbar.d.ts +113 -0
- package/types/tree.d.ts +135 -0
- package/types/ui-patterns.d.ts +1604 -0
|
@@ -0,0 +1,990 @@
|
|
|
1
|
+
import { computed, signal, linkedSignal } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
/** Bit flag representation of the possible modifier keys that can be present on an event. */
|
|
4
|
+
var Modifier;
|
|
5
|
+
(function (Modifier) {
|
|
6
|
+
Modifier[Modifier["None"] = 0] = "None";
|
|
7
|
+
Modifier[Modifier["Ctrl"] = 1] = "Ctrl";
|
|
8
|
+
Modifier[Modifier["Shift"] = 2] = "Shift";
|
|
9
|
+
Modifier[Modifier["Alt"] = 4] = "Alt";
|
|
10
|
+
Modifier[Modifier["Meta"] = 8] = "Meta";
|
|
11
|
+
Modifier["Any"] = "Any";
|
|
12
|
+
})(Modifier || (Modifier = {}));
|
|
13
|
+
/**
|
|
14
|
+
* Abstract base class for all event managers.
|
|
15
|
+
*
|
|
16
|
+
* Event managers are designed to normalize how event handlers are authored and create a safety net
|
|
17
|
+
* for common event handling gotchas like remembering to call preventDefault or stopPropagation.
|
|
18
|
+
*/
|
|
19
|
+
class EventManager {
|
|
20
|
+
configs = [];
|
|
21
|
+
/** Runs the handlers that match with the given event. */
|
|
22
|
+
handle(event) {
|
|
23
|
+
for (const config of this.configs) {
|
|
24
|
+
if (config.matcher(event)) {
|
|
25
|
+
config.handler(event);
|
|
26
|
+
if (config.preventDefault) {
|
|
27
|
+
event.preventDefault();
|
|
28
|
+
}
|
|
29
|
+
if (config.stopPropagation) {
|
|
30
|
+
event.stopPropagation();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Gets bit flag representation of the modifier keys present on the given event. */
|
|
37
|
+
function getModifiers(event) {
|
|
38
|
+
return ((+event.ctrlKey && Modifier.Ctrl) |
|
|
39
|
+
(+event.shiftKey && Modifier.Shift) |
|
|
40
|
+
(+event.altKey && Modifier.Alt) |
|
|
41
|
+
(+event.metaKey && Modifier.Meta));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Checks if the given event has modifiers that are an exact match for any of the given modifier
|
|
45
|
+
* flag combinations.
|
|
46
|
+
*/
|
|
47
|
+
function hasModifiers(event, modifiers) {
|
|
48
|
+
const eventModifiers = getModifiers(event);
|
|
49
|
+
const modifiersList = Array.isArray(modifiers) ? modifiers : [modifiers];
|
|
50
|
+
if (modifiersList.includes(Modifier.Any)) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return modifiersList.some(modifiers => eventModifiers === modifiers);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* An event manager that is specialized for handling keyboard events. By default this manager stops
|
|
58
|
+
* propagation and prevents default on all events it handles.
|
|
59
|
+
*/
|
|
60
|
+
class KeyboardEventManager extends EventManager {
|
|
61
|
+
options = {
|
|
62
|
+
preventDefault: true,
|
|
63
|
+
stopPropagation: true,
|
|
64
|
+
};
|
|
65
|
+
on(...args) {
|
|
66
|
+
const { modifiers, key, handler } = this._normalizeInputs(...args);
|
|
67
|
+
this.configs.push({
|
|
68
|
+
handler: handler,
|
|
69
|
+
matcher: event => this._isMatch(event, key, modifiers),
|
|
70
|
+
...this.options,
|
|
71
|
+
});
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
_normalizeInputs(...args) {
|
|
75
|
+
const key = args.length === 3 ? args[1] : args[0];
|
|
76
|
+
const handler = args.length === 3 ? args[2] : args[1];
|
|
77
|
+
const modifiers = args.length === 3 ? args[0] : Modifier.None;
|
|
78
|
+
return {
|
|
79
|
+
key: key,
|
|
80
|
+
handler: handler,
|
|
81
|
+
modifiers: modifiers,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
_isMatch(event, key, modifiers) {
|
|
85
|
+
if (!hasModifiers(event, modifiers)) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
if (key instanceof RegExp) {
|
|
89
|
+
return key.test(event.key);
|
|
90
|
+
}
|
|
91
|
+
const keyStr = typeof key === 'string' ? key : key();
|
|
92
|
+
return keyStr.toLowerCase() === event.key.toLowerCase();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The different mouse buttons that may appear on a pointer event.
|
|
98
|
+
*/
|
|
99
|
+
var MouseButton;
|
|
100
|
+
(function (MouseButton) {
|
|
101
|
+
MouseButton[MouseButton["Main"] = 0] = "Main";
|
|
102
|
+
MouseButton[MouseButton["Auxiliary"] = 1] = "Auxiliary";
|
|
103
|
+
MouseButton[MouseButton["Secondary"] = 2] = "Secondary";
|
|
104
|
+
})(MouseButton || (MouseButton = {}));
|
|
105
|
+
/** An event manager that is specialized for handling pointer events. */
|
|
106
|
+
class PointerEventManager extends EventManager {
|
|
107
|
+
options = {
|
|
108
|
+
preventDefault: false,
|
|
109
|
+
stopPropagation: false,
|
|
110
|
+
};
|
|
111
|
+
on(...args) {
|
|
112
|
+
const { button, handler, modifiers } = this._normalizeInputs(...args);
|
|
113
|
+
this.configs.push({
|
|
114
|
+
handler,
|
|
115
|
+
matcher: event => this._isMatch(event, button, modifiers),
|
|
116
|
+
...this.options,
|
|
117
|
+
});
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
_normalizeInputs(...args) {
|
|
121
|
+
if (args.length === 3) {
|
|
122
|
+
return {
|
|
123
|
+
button: args[0],
|
|
124
|
+
modifiers: args[1],
|
|
125
|
+
handler: args[2],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (args.length === 2) {
|
|
129
|
+
return {
|
|
130
|
+
button: MouseButton.Main,
|
|
131
|
+
modifiers: args[0],
|
|
132
|
+
handler: args[1],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
button: MouseButton.Main,
|
|
137
|
+
modifiers: Modifier.None,
|
|
138
|
+
handler: args[0],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
_isMatch(event, button, modifiers) {
|
|
142
|
+
return button === (event.button ?? 0) && hasModifiers(event, modifiers);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Controls internal coordinates for a grid of items. */
|
|
147
|
+
class GridData {
|
|
148
|
+
inputs;
|
|
149
|
+
/** The two-dimensional array of cells that represents the grid. */
|
|
150
|
+
cells;
|
|
151
|
+
/** The number of rows in the grid. */
|
|
152
|
+
rowCount = computed(() => this.cells().length);
|
|
153
|
+
/** The maximum number of rows in the grid, accounting for row spans. */
|
|
154
|
+
maxRowCount = computed(() => Math.max(...this._rowCountByCol().values(), 0));
|
|
155
|
+
/** The maximum number of columns in the grid, accounting for column spans. */
|
|
156
|
+
maxColCount = computed(() => Math.max(...this._colCountsByRow().values(), 0));
|
|
157
|
+
/** A map from a cell to its primary and spanned coordinates. */
|
|
158
|
+
_coordsMap = computed(() => {
|
|
159
|
+
const coordsMap = new Map();
|
|
160
|
+
const visitedCoords = new Set();
|
|
161
|
+
for (let rowIndex = 0; rowIndex < this.cells().length; rowIndex++) {
|
|
162
|
+
let colIndex = 0;
|
|
163
|
+
const row = this.cells()[rowIndex];
|
|
164
|
+
for (const cell of row) {
|
|
165
|
+
// Skip past cells that are already taken.
|
|
166
|
+
while (visitedCoords.has(`${rowIndex}:${colIndex}`)) {
|
|
167
|
+
colIndex++;
|
|
168
|
+
}
|
|
169
|
+
const rowspan = cell.rowSpan();
|
|
170
|
+
const colspan = cell.colSpan();
|
|
171
|
+
const spanCoords = [];
|
|
172
|
+
for (let rowOffset = 0; rowOffset < rowspan; rowOffset++) {
|
|
173
|
+
const row = rowIndex + rowOffset;
|
|
174
|
+
for (let colOffset = 0; colOffset < colspan; colOffset++) {
|
|
175
|
+
const col = colIndex + colOffset;
|
|
176
|
+
visitedCoords.add(`${row}:${col}`);
|
|
177
|
+
spanCoords.push({ row, col });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
coordsMap.set(cell, { coords: spanCoords[0], spanCoords });
|
|
181
|
+
colIndex += colspan;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return coordsMap;
|
|
185
|
+
});
|
|
186
|
+
/** A map from a coordinate string to the cell at that coordinate. */
|
|
187
|
+
_cellMap = computed(() => {
|
|
188
|
+
const cellMap = new Map();
|
|
189
|
+
for (const [cell, { spanCoords }] of this._coordsMap().entries()) {
|
|
190
|
+
for (const { row, col } of spanCoords) {
|
|
191
|
+
cellMap.set(`${row}:${col}`, cell);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return cellMap;
|
|
195
|
+
});
|
|
196
|
+
/** A map from a row index to the number of columns in that row. */
|
|
197
|
+
_colCountsByRow = computed(() => {
|
|
198
|
+
const colCountByRow = new Map();
|
|
199
|
+
for (const [_, { spanCoords }] of this._coordsMap().entries()) {
|
|
200
|
+
for (const { row, col } of spanCoords) {
|
|
201
|
+
const colCount = colCountByRow.get(row);
|
|
202
|
+
const newColCount = col + 1;
|
|
203
|
+
if (colCount === undefined || colCount < newColCount) {
|
|
204
|
+
colCountByRow.set(row, newColCount);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return colCountByRow;
|
|
209
|
+
});
|
|
210
|
+
/** A map from a column index to the number of rows in that column. */
|
|
211
|
+
_rowCountByCol = computed(() => {
|
|
212
|
+
const rowCountByCol = new Map();
|
|
213
|
+
for (const [_, { spanCoords }] of this._coordsMap().entries()) {
|
|
214
|
+
for (const { row, col } of spanCoords) {
|
|
215
|
+
const rowCount = rowCountByCol.get(col);
|
|
216
|
+
const newRowCount = row + 1;
|
|
217
|
+
if (rowCount === undefined || rowCount < newRowCount) {
|
|
218
|
+
rowCountByCol.set(col, newRowCount);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return rowCountByCol;
|
|
223
|
+
});
|
|
224
|
+
constructor(inputs) {
|
|
225
|
+
this.inputs = inputs;
|
|
226
|
+
this.cells = this.inputs.cells;
|
|
227
|
+
}
|
|
228
|
+
/** Gets the cell at the given coordinates. */
|
|
229
|
+
getCell(rowCol) {
|
|
230
|
+
return this._cellMap().get(`${rowCol.row}:${rowCol.col}`);
|
|
231
|
+
}
|
|
232
|
+
/** Gets the primary coordinates of the given cell. */
|
|
233
|
+
getCoords(cell) {
|
|
234
|
+
return this._coordsMap().get(cell)?.coords;
|
|
235
|
+
}
|
|
236
|
+
/** Gets all coordinates that the given cell spans. */
|
|
237
|
+
getAllCoords(cell) {
|
|
238
|
+
return this._coordsMap().get(cell)?.spanCoords;
|
|
239
|
+
}
|
|
240
|
+
/** Gets the number of rows in the given column. */
|
|
241
|
+
getRowCount(col) {
|
|
242
|
+
return this._rowCountByCol().get(col);
|
|
243
|
+
}
|
|
244
|
+
/** Gets the number of columns in the given row. */
|
|
245
|
+
getColCount(row) {
|
|
246
|
+
return this._colCountsByRow().get(row);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Controls focus for a 2D grid of cells. */
|
|
251
|
+
class GridFocus {
|
|
252
|
+
inputs;
|
|
253
|
+
/** The current active cell. */
|
|
254
|
+
activeCell = signal(undefined);
|
|
255
|
+
/** The current active cell coordinates. */
|
|
256
|
+
activeCoords = signal({ row: -1, col: -1 });
|
|
257
|
+
/** Whether the grid active state is empty (no active cell or coordinates). */
|
|
258
|
+
stateEmpty = computed(() => this.activeCell() === undefined ||
|
|
259
|
+
(this.activeCoords().row === -1 && this.activeCoords().col === -1));
|
|
260
|
+
/**
|
|
261
|
+
* Whether the grid focus state is stale.
|
|
262
|
+
*
|
|
263
|
+
* A stale state means the active cell or coordinates are no longer valid based on the
|
|
264
|
+
* current grid data, for example if the underlying cells have changed.
|
|
265
|
+
* A stale state should be re-initialized.
|
|
266
|
+
*/
|
|
267
|
+
stateStale = computed(() => {
|
|
268
|
+
if (this.stateEmpty()) {
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
const activeCell = this.activeCell();
|
|
272
|
+
const activeCellCoords = this.inputs.grid.getCoords(activeCell);
|
|
273
|
+
const activeCoords = this.activeCoords();
|
|
274
|
+
const activeCoordsCell = this.inputs.grid.getCell(activeCoords);
|
|
275
|
+
const activeCellNotValid = activeCellCoords === undefined;
|
|
276
|
+
const activeCellMismatch = activeCell !== activeCoordsCell;
|
|
277
|
+
return activeCellNotValid || activeCellMismatch;
|
|
278
|
+
});
|
|
279
|
+
/** The id of the current active cell, for ARIA activedescendant. */
|
|
280
|
+
activeDescendant = computed(() => {
|
|
281
|
+
if (this.gridDisabled() || this.inputs.focusMode() === 'roving') {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
const currentActiveCell = this.activeCell();
|
|
285
|
+
return currentActiveCell ? currentActiveCell.id() : undefined;
|
|
286
|
+
});
|
|
287
|
+
/** Whether the grid is in a disabled state. */
|
|
288
|
+
gridDisabled = computed(() => {
|
|
289
|
+
if (this.inputs.disabled()) {
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
const gridCells = this.inputs.grid.cells();
|
|
293
|
+
return gridCells.length === 0 || gridCells.every(row => row.every(cell => cell.disabled()));
|
|
294
|
+
});
|
|
295
|
+
/** The tabindex for the grid container. */
|
|
296
|
+
gridTabIndex = computed(() => {
|
|
297
|
+
if (this.gridDisabled()) {
|
|
298
|
+
return 0;
|
|
299
|
+
}
|
|
300
|
+
return this.inputs.focusMode() === 'activedescendant' ? 0 : -1;
|
|
301
|
+
});
|
|
302
|
+
constructor(inputs) {
|
|
303
|
+
this.inputs = inputs;
|
|
304
|
+
}
|
|
305
|
+
/** Returns the tabindex for the given grid cell cell. */
|
|
306
|
+
getCellTabindex(cell) {
|
|
307
|
+
if (this.gridDisabled()) {
|
|
308
|
+
return -1;
|
|
309
|
+
}
|
|
310
|
+
if (this.inputs.focusMode() === 'activedescendant') {
|
|
311
|
+
return -1;
|
|
312
|
+
}
|
|
313
|
+
return this.activeCell() === cell ? 0 : -1;
|
|
314
|
+
}
|
|
315
|
+
/** Returns true if the given cell can be navigated to. */
|
|
316
|
+
isFocusable(cell) {
|
|
317
|
+
return !cell.disabled() || !this.inputs.skipDisabled();
|
|
318
|
+
}
|
|
319
|
+
/** Focuses the given cell. */
|
|
320
|
+
focusCell(cell) {
|
|
321
|
+
if (this.gridDisabled()) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
if (!this.isFocusable(cell)) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
if (this.inputs.grid.getCoords(cell) === undefined) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
this.activeCoords.set(this.inputs.grid.getCoords(cell));
|
|
331
|
+
this.activeCell.set(cell);
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
/** Moves focus to the cell at the given coordinates if it's part of a focusable cell. */
|
|
335
|
+
focusCoordinates(coords) {
|
|
336
|
+
if (this.gridDisabled()) {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
const cell = this.inputs.grid.getCell(coords);
|
|
340
|
+
if (!cell || !this.isFocusable(cell)) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
if (this.inputs.grid.getCell(coords) === undefined) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
this.activeCoords.set(coords);
|
|
347
|
+
this.activeCell.set(this.inputs.grid.getCell(coords));
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Constants for the four cardinal directions. */
|
|
353
|
+
const direction = {
|
|
354
|
+
Up: { row: -1 },
|
|
355
|
+
Down: { row: 1 },
|
|
356
|
+
Left: { col: -1 },
|
|
357
|
+
Right: { col: 1 },
|
|
358
|
+
};
|
|
359
|
+
/** Controls navigation for a grid of items. */
|
|
360
|
+
class GridNavigation {
|
|
361
|
+
inputs;
|
|
362
|
+
/** The maximum number of steps to take when searching for the next cell. */
|
|
363
|
+
_maxSteps = computed(() => this.inputs.grid.maxRowCount() * this.inputs.grid.maxColCount());
|
|
364
|
+
constructor(inputs) {
|
|
365
|
+
this.inputs = inputs;
|
|
366
|
+
}
|
|
367
|
+
/** Navigates to the given item. */
|
|
368
|
+
gotoCell(cell) {
|
|
369
|
+
return this.inputs.gridFocus.focusCell(cell);
|
|
370
|
+
}
|
|
371
|
+
/** Navigates to the given coordinates. */
|
|
372
|
+
gotoCoords(coords) {
|
|
373
|
+
return this.inputs.gridFocus.focusCoordinates(coords);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Gets the coordinates of the next focusable cell in a given direction, without changing focus.
|
|
377
|
+
*/
|
|
378
|
+
peek(direction, fromCoords, wrap) {
|
|
379
|
+
wrap = wrap ?? (direction.row !== undefined ? this.inputs.rowWrap() : this.inputs.colWrap());
|
|
380
|
+
return this._peekDirectional(direction, fromCoords, wrap);
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Navigates to the next focusable cell in a given direction.
|
|
384
|
+
*/
|
|
385
|
+
advance(direction) {
|
|
386
|
+
const nextCoords = this.peek(direction, this.inputs.gridFocus.activeCoords());
|
|
387
|
+
return !!nextCoords && this.gotoCoords(nextCoords);
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Gets the coordinates of the first focusable cell.
|
|
391
|
+
* If a row is not provided, searches the entire grid.
|
|
392
|
+
*/
|
|
393
|
+
peekFirst(row) {
|
|
394
|
+
const fromCoords = {
|
|
395
|
+
row: row ?? 0,
|
|
396
|
+
col: -1,
|
|
397
|
+
};
|
|
398
|
+
return row === undefined
|
|
399
|
+
? this._peekDirectional(direction.Right, fromCoords, 'continuous')
|
|
400
|
+
: this._peekDirectional(direction.Right, fromCoords, 'nowrap');
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Navigates to the first focusable cell.
|
|
404
|
+
* If a row is not provided, searches the entire grid.
|
|
405
|
+
*/
|
|
406
|
+
first(row) {
|
|
407
|
+
const nextCoords = this.peekFirst(row);
|
|
408
|
+
return !!nextCoords && this.gotoCoords(nextCoords);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Gets the coordinates of the last focusable cell.
|
|
412
|
+
* If a row is not provided, searches the entire grid.
|
|
413
|
+
*/
|
|
414
|
+
peekLast(row) {
|
|
415
|
+
const fromCoords = {
|
|
416
|
+
row: row ?? this.inputs.grid.maxRowCount() - 1,
|
|
417
|
+
col: this.inputs.grid.maxColCount(),
|
|
418
|
+
};
|
|
419
|
+
return row === undefined
|
|
420
|
+
? this._peekDirectional(direction.Left, fromCoords, 'continuous')
|
|
421
|
+
: this._peekDirectional(direction.Left, fromCoords, 'nowrap');
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Navigates to the last focusable cell.
|
|
425
|
+
* If a row is not provided, searches the entire grid.
|
|
426
|
+
*/
|
|
427
|
+
last(row) {
|
|
428
|
+
const nextCoords = this.peekLast(row);
|
|
429
|
+
return !!nextCoords && this.gotoCoords(nextCoords);
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Finds the next focusable cell in a given direction based on the wrapping behavior.
|
|
433
|
+
*/
|
|
434
|
+
_peekDirectional(delta, fromCoords, wrap) {
|
|
435
|
+
const fromCell = this.inputs.grid.getCell(fromCoords);
|
|
436
|
+
const maxRowCount = this.inputs.grid.maxRowCount();
|
|
437
|
+
const maxColCount = this.inputs.grid.maxColCount();
|
|
438
|
+
const rowDelta = delta.row ?? 0;
|
|
439
|
+
const colDelta = delta.col ?? 0;
|
|
440
|
+
let nextCoords = { ...fromCoords };
|
|
441
|
+
for (let step = 0; step < this._maxSteps(); step++) {
|
|
442
|
+
const isWrapping = nextCoords.col + colDelta < 0 ||
|
|
443
|
+
nextCoords.col + colDelta >= maxColCount ||
|
|
444
|
+
nextCoords.row + rowDelta < 0 ||
|
|
445
|
+
nextCoords.row + rowDelta >= maxRowCount;
|
|
446
|
+
if (wrap === 'nowrap' && isWrapping)
|
|
447
|
+
return;
|
|
448
|
+
if (wrap === 'continuous') {
|
|
449
|
+
const generalDelta = delta.row ?? delta.col;
|
|
450
|
+
const rowStep = isWrapping ? generalDelta : rowDelta;
|
|
451
|
+
const colStep = isWrapping ? generalDelta : colDelta;
|
|
452
|
+
nextCoords = {
|
|
453
|
+
row: (nextCoords.row + rowStep + maxRowCount) % maxRowCount,
|
|
454
|
+
col: (nextCoords.col + colStep + maxColCount) % maxColCount,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
if (wrap === 'loop') {
|
|
458
|
+
nextCoords = {
|
|
459
|
+
row: (nextCoords.row + rowDelta + maxRowCount) % maxRowCount,
|
|
460
|
+
col: (nextCoords.col + colDelta + maxColCount) % maxColCount,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
// Back to original coordinates.
|
|
464
|
+
if (nextCoords.row === fromCoords.row && nextCoords.col === fromCoords.col) {
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
const nextCell = this.inputs.grid.getCell(nextCoords);
|
|
468
|
+
if (nextCell !== undefined &&
|
|
469
|
+
nextCell !== fromCell &&
|
|
470
|
+
this.inputs.gridFocus.isFocusable(nextCell)) {
|
|
471
|
+
return nextCoords;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return undefined;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Controls selection for a grid of items. */
|
|
479
|
+
class GridSelection {
|
|
480
|
+
inputs;
|
|
481
|
+
constructor(inputs) {
|
|
482
|
+
this.inputs = inputs;
|
|
483
|
+
}
|
|
484
|
+
/** Selects one or more cells in a given range. */
|
|
485
|
+
select(fromCoords, toCoords) {
|
|
486
|
+
for (const cell of this._validCells(fromCoords, toCoords ?? fromCoords)) {
|
|
487
|
+
cell.selected.set(true);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
/** Deselects one or more cells in a given range. */
|
|
491
|
+
deselect(fromCoords, toCoords) {
|
|
492
|
+
for (const cell of this._validCells(fromCoords, toCoords ?? fromCoords)) {
|
|
493
|
+
cell.selected.set(false);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/** Toggles the selection state of one or more cells in a given range. */
|
|
497
|
+
toggle(fromCoords, toCoords) {
|
|
498
|
+
for (const cell of this._validCells(fromCoords, toCoords ?? fromCoords)) {
|
|
499
|
+
cell.selected.update(state => !state);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/** Selects all valid cells in the grid. */
|
|
503
|
+
selectAll() {
|
|
504
|
+
for (const cell of this._validCells({ row: 0, col: 0 }, { row: this.inputs.grid.maxRowCount(), col: this.inputs.grid.maxColCount() })) {
|
|
505
|
+
cell.selected.set(true);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
/** Deselects all valid cells in the grid. */
|
|
509
|
+
deselectAll() {
|
|
510
|
+
for (const cell of this._validCells({ row: 0, col: 0 }, { row: this.inputs.grid.maxRowCount(), col: this.inputs.grid.maxColCount() })) {
|
|
511
|
+
cell.selected.set(false);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
/** A generator that yields all valid (selectable and not disabled) cells within a given range. */
|
|
515
|
+
*_validCells(fromCoords, toCoords) {
|
|
516
|
+
const startRow = Math.min(fromCoords.row, toCoords.row);
|
|
517
|
+
const startCol = Math.min(fromCoords.col, toCoords.col);
|
|
518
|
+
const endRow = Math.max(fromCoords.row, toCoords.row);
|
|
519
|
+
const endCol = Math.max(fromCoords.col, toCoords.col);
|
|
520
|
+
const visited = new Set();
|
|
521
|
+
for (let row = startRow; row < endRow + 1; row++) {
|
|
522
|
+
for (let col = startCol; col < endCol + 1; col++) {
|
|
523
|
+
const cell = this.inputs.grid.getCell({ row, col });
|
|
524
|
+
if (cell === undefined)
|
|
525
|
+
continue;
|
|
526
|
+
if (!cell.selectable())
|
|
527
|
+
continue;
|
|
528
|
+
if (cell.disabled())
|
|
529
|
+
continue;
|
|
530
|
+
if (visited.has(cell))
|
|
531
|
+
continue;
|
|
532
|
+
visited.add(cell);
|
|
533
|
+
yield cell;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** The main class that orchestrates the grid behaviors. */
|
|
540
|
+
class Grid {
|
|
541
|
+
inputs;
|
|
542
|
+
/** The underlying data structure for the grid. */
|
|
543
|
+
data;
|
|
544
|
+
/** Controls focus for the grid. */
|
|
545
|
+
focusBehavior;
|
|
546
|
+
/** Controls navigation for the grid. */
|
|
547
|
+
navigationBehavior;
|
|
548
|
+
/** Controls selection for the grid. */
|
|
549
|
+
selectionBehavior;
|
|
550
|
+
/** The anchor point for range selection, linked to the active coordinates. */
|
|
551
|
+
selectionAnchor = linkedSignal(() => this.focusBehavior.activeCoords());
|
|
552
|
+
/** The `tabindex` for the grid container. */
|
|
553
|
+
gridTabIndex = computed(() => this.focusBehavior.gridTabIndex());
|
|
554
|
+
/** Whether the grid is in a disabled state. */
|
|
555
|
+
gridDisabled = computed(() => this.focusBehavior.gridDisabled());
|
|
556
|
+
/** The ID of the active descendant for ARIA `activedescendant` focus management. */
|
|
557
|
+
activeDescendant = computed(() => this.focusBehavior.activeDescendant());
|
|
558
|
+
constructor(inputs) {
|
|
559
|
+
this.inputs = inputs;
|
|
560
|
+
this.data = new GridData(inputs);
|
|
561
|
+
this.focusBehavior = new GridFocus({ ...inputs, grid: this.data });
|
|
562
|
+
this.navigationBehavior = new GridNavigation({
|
|
563
|
+
...inputs,
|
|
564
|
+
grid: this.data,
|
|
565
|
+
gridFocus: this.focusBehavior,
|
|
566
|
+
});
|
|
567
|
+
this.selectionBehavior = new GridSelection({
|
|
568
|
+
...inputs,
|
|
569
|
+
grid: this.data,
|
|
570
|
+
gridFocus: this.focusBehavior,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
/** Gets the 1-based row index of a cell. */
|
|
574
|
+
rowIndex(cell) {
|
|
575
|
+
const index = this.data.getCoords(cell)?.row;
|
|
576
|
+
return index !== undefined ? index + 1 : undefined;
|
|
577
|
+
}
|
|
578
|
+
/** Gets the 1-based column index of a cell. */
|
|
579
|
+
colIndex(cell) {
|
|
580
|
+
const index = this.data.getCoords(cell)?.col;
|
|
581
|
+
return index !== undefined ? index + 1 : undefined;
|
|
582
|
+
}
|
|
583
|
+
/** Gets the `tabindex` for a given cell. */
|
|
584
|
+
cellTabIndex(cell) {
|
|
585
|
+
return this.focusBehavior.getCellTabindex(cell);
|
|
586
|
+
}
|
|
587
|
+
/** Navigates to the cell above the currently active cell. */
|
|
588
|
+
up() {
|
|
589
|
+
return this.navigationBehavior.advance(direction.Up);
|
|
590
|
+
}
|
|
591
|
+
/** Extends the selection to the cell above the selection anchor. */
|
|
592
|
+
rangeSelectUp() {
|
|
593
|
+
const coords = this.navigationBehavior.peek(direction.Up, this.selectionAnchor());
|
|
594
|
+
if (coords === undefined)
|
|
595
|
+
return;
|
|
596
|
+
this._rangeSelectCoords(coords);
|
|
597
|
+
}
|
|
598
|
+
/** Navigates to the cell below the currently active cell. */
|
|
599
|
+
down() {
|
|
600
|
+
return this.navigationBehavior.advance(direction.Down);
|
|
601
|
+
}
|
|
602
|
+
/** Extends the selection to the cell below the selection anchor. */
|
|
603
|
+
rangeSelectDown() {
|
|
604
|
+
const coords = this.navigationBehavior.peek(direction.Down, this.selectionAnchor());
|
|
605
|
+
if (coords === undefined)
|
|
606
|
+
return;
|
|
607
|
+
this._rangeSelectCoords(coords);
|
|
608
|
+
}
|
|
609
|
+
/** Navigates to the cell to the left of the currently active cell. */
|
|
610
|
+
left() {
|
|
611
|
+
return this.navigationBehavior.advance(direction.Left);
|
|
612
|
+
}
|
|
613
|
+
/** Extends the selection to the cell to the left of the selection anchor. */
|
|
614
|
+
rangeSelectLeft() {
|
|
615
|
+
const coords = this.navigationBehavior.peek(direction.Left, this.selectionAnchor());
|
|
616
|
+
if (coords === undefined)
|
|
617
|
+
return;
|
|
618
|
+
this._rangeSelectCoords(coords);
|
|
619
|
+
}
|
|
620
|
+
/** Navigates to the cell to the right of the currently active cell. */
|
|
621
|
+
right() {
|
|
622
|
+
return this.navigationBehavior.advance(direction.Right);
|
|
623
|
+
}
|
|
624
|
+
/** Extends the selection to the cell to the right of the selection anchor. */
|
|
625
|
+
rangeSelectRight() {
|
|
626
|
+
const coords = this.navigationBehavior.peek(direction.Right, this.selectionAnchor());
|
|
627
|
+
if (coords === undefined)
|
|
628
|
+
return;
|
|
629
|
+
this._rangeSelectCoords(coords);
|
|
630
|
+
}
|
|
631
|
+
/** Navigates to the first focusable cell in the grid. */
|
|
632
|
+
first() {
|
|
633
|
+
return this.navigationBehavior.first();
|
|
634
|
+
}
|
|
635
|
+
/** Navigates to the first focusable cell in the current row. */
|
|
636
|
+
firstInRow() {
|
|
637
|
+
return this.navigationBehavior.first(this.focusBehavior.activeCoords().row);
|
|
638
|
+
}
|
|
639
|
+
/** Navigates to the last focusable cell in the grid. */
|
|
640
|
+
last() {
|
|
641
|
+
return this.navigationBehavior.last();
|
|
642
|
+
}
|
|
643
|
+
/** Navigates to the last focusable cell in the current row. */
|
|
644
|
+
lastInRow() {
|
|
645
|
+
return this.navigationBehavior.last(this.focusBehavior.activeCoords().row);
|
|
646
|
+
}
|
|
647
|
+
/** Selects all cells in the current row. */
|
|
648
|
+
selectRow() {
|
|
649
|
+
const row = this.focusBehavior.activeCoords().row;
|
|
650
|
+
this.selectionBehavior.deselectAll();
|
|
651
|
+
this.selectionBehavior.select({ row, col: 0 }, { row, col: this.data.maxColCount() });
|
|
652
|
+
}
|
|
653
|
+
/** Selects all cells in the current column. */
|
|
654
|
+
selectCol() {
|
|
655
|
+
const col = this.focusBehavior.activeCoords().col;
|
|
656
|
+
this.selectionBehavior.deselectAll();
|
|
657
|
+
this.selectionBehavior.select({ row: 0, col }, { row: this.data.maxRowCount(), col });
|
|
658
|
+
}
|
|
659
|
+
/** Selects all selectable cells in the grid. */
|
|
660
|
+
selectAll() {
|
|
661
|
+
this.selectionBehavior.selectAll();
|
|
662
|
+
}
|
|
663
|
+
/** Navigates to and focuses the given cell. */
|
|
664
|
+
gotoCell(cell) {
|
|
665
|
+
return this.navigationBehavior.gotoCell(cell);
|
|
666
|
+
}
|
|
667
|
+
/** Toggles the selection state of the given cell. */
|
|
668
|
+
toggleSelect(cell) {
|
|
669
|
+
const coords = this.data.getCoords(cell);
|
|
670
|
+
if (coords === undefined)
|
|
671
|
+
return;
|
|
672
|
+
this.selectionBehavior.toggle(coords);
|
|
673
|
+
}
|
|
674
|
+
/** Extends the selection from the anchor to the given cell. */
|
|
675
|
+
rangeSelect(cell) {
|
|
676
|
+
const coords = this.data.getCoords(cell);
|
|
677
|
+
if (coords === undefined)
|
|
678
|
+
return;
|
|
679
|
+
this._rangeSelectCoords(coords);
|
|
680
|
+
}
|
|
681
|
+
/** Extends the selection to the given coordinates. */
|
|
682
|
+
_rangeSelectCoords(coords) {
|
|
683
|
+
const activeCell = this.focusBehavior.activeCell();
|
|
684
|
+
const anchorCell = this.data.getCell(coords);
|
|
685
|
+
if (activeCell === undefined || anchorCell === undefined) {
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const allCoords = [
|
|
689
|
+
...this.data.getAllCoords(activeCell),
|
|
690
|
+
...this.data.getAllCoords(anchorCell),
|
|
691
|
+
];
|
|
692
|
+
const allRows = allCoords.map(c => c.row);
|
|
693
|
+
const allCols = allCoords.map(c => c.col);
|
|
694
|
+
const fromCoords = {
|
|
695
|
+
row: Math.min(...allRows),
|
|
696
|
+
col: Math.min(...allCols),
|
|
697
|
+
};
|
|
698
|
+
const toCoords = {
|
|
699
|
+
row: Math.max(...allRows),
|
|
700
|
+
col: Math.max(...allCols),
|
|
701
|
+
};
|
|
702
|
+
this.selectionBehavior.deselectAll();
|
|
703
|
+
this.selectionBehavior.select(fromCoords, toCoords);
|
|
704
|
+
this.selectionAnchor.set(coords);
|
|
705
|
+
}
|
|
706
|
+
/** Resets the active state of the grid if it is empty or stale. */
|
|
707
|
+
resetState() {
|
|
708
|
+
if (this.focusBehavior.stateEmpty()) {
|
|
709
|
+
const firstFocusableCoords = this.navigationBehavior.peekFirst();
|
|
710
|
+
if (firstFocusableCoords === undefined) {
|
|
711
|
+
return false;
|
|
712
|
+
}
|
|
713
|
+
return this.focusBehavior.focusCoordinates(firstFocusableCoords);
|
|
714
|
+
}
|
|
715
|
+
if (this.focusBehavior.stateStale()) {
|
|
716
|
+
// Try focus on the same active cell after if a reordering happened.
|
|
717
|
+
if (this.focusBehavior.focusCell(this.focusBehavior.activeCell())) {
|
|
718
|
+
return true;
|
|
719
|
+
}
|
|
720
|
+
// If the active cell is no longer exist, focus on the coordinates instead.
|
|
721
|
+
if (this.focusBehavior.focusCoordinates(this.focusBehavior.activeCoords())) {
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
// If the cooridnates no longer valid, go back to the first available cell.
|
|
725
|
+
if (this.focusBehavior.focusCoordinates(this.navigationBehavior.peekFirst())) {
|
|
726
|
+
return true;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/** The UI pattern for a grid, handling keyboard navigation, focus, and selection. */
|
|
734
|
+
class GridPattern {
|
|
735
|
+
inputs;
|
|
736
|
+
/** The underlying grid behavior that this pattern is built on. */
|
|
737
|
+
gridBehavior;
|
|
738
|
+
/** The cells in the grid. */
|
|
739
|
+
cells = computed(() => this.gridBehavior.data.cells());
|
|
740
|
+
/** The tab index for the grid. */
|
|
741
|
+
tabIndex = computed(() => this.gridBehavior.gridTabIndex());
|
|
742
|
+
/** Whether the grid is disabled. */
|
|
743
|
+
disabled = computed(() => this.gridBehavior.gridDisabled());
|
|
744
|
+
/** The ID of the currently active descendant cell. */
|
|
745
|
+
activeDescendant = computed(() => this.gridBehavior.activeDescendant());
|
|
746
|
+
/** The currently active cell. */
|
|
747
|
+
activeCell = computed(() => this.gridBehavior.focusBehavior.activeCell());
|
|
748
|
+
/** Whether to pause grid navigation. */
|
|
749
|
+
pauseNavigation = computed(() => this.gridBehavior.data
|
|
750
|
+
.cells()
|
|
751
|
+
.flat()
|
|
752
|
+
.reduce((res, c) => res || c.widgetActivated(), false));
|
|
753
|
+
/** Whether the focus is in the grid. */
|
|
754
|
+
isFocused = signal(false);
|
|
755
|
+
/** Whether the user is currently dragging to select a range of cells. */
|
|
756
|
+
dragging = signal(false);
|
|
757
|
+
/** The keydown event manager for the grid. */
|
|
758
|
+
keydown = computed(() => {
|
|
759
|
+
const manager = new KeyboardEventManager();
|
|
760
|
+
if (this.pauseNavigation()) {
|
|
761
|
+
return manager;
|
|
762
|
+
}
|
|
763
|
+
manager
|
|
764
|
+
.on('ArrowUp', () => this.gridBehavior.up())
|
|
765
|
+
.on('ArrowDown', () => this.gridBehavior.down())
|
|
766
|
+
.on('ArrowLeft', () => this.gridBehavior.left())
|
|
767
|
+
.on('ArrowRight', () => this.gridBehavior.right())
|
|
768
|
+
.on('Home', () => this.gridBehavior.firstInRow())
|
|
769
|
+
.on('End', () => this.gridBehavior.lastInRow())
|
|
770
|
+
.on([Modifier.Ctrl], 'Home', () => this.gridBehavior.first())
|
|
771
|
+
.on([Modifier.Ctrl], 'End', () => this.gridBehavior.last());
|
|
772
|
+
if (this.inputs.enableSelection()) {
|
|
773
|
+
manager
|
|
774
|
+
.on(Modifier.Shift, 'ArrowUp', () => this.gridBehavior.rangeSelectUp())
|
|
775
|
+
.on(Modifier.Shift, 'ArrowDown', () => this.gridBehavior.rangeSelectDown())
|
|
776
|
+
.on(Modifier.Shift, 'ArrowLeft', () => this.gridBehavior.rangeSelectLeft())
|
|
777
|
+
.on(Modifier.Shift, 'ArrowRight', () => this.gridBehavior.rangeSelectRight())
|
|
778
|
+
.on([Modifier.Ctrl, Modifier.Meta], 'A', () => this.gridBehavior.selectAll())
|
|
779
|
+
.on([Modifier.Shift], ' ', () => this.gridBehavior.selectRow())
|
|
780
|
+
.on([Modifier.Ctrl, Modifier.Meta], ' ', () => this.gridBehavior.selectCol());
|
|
781
|
+
}
|
|
782
|
+
return manager;
|
|
783
|
+
});
|
|
784
|
+
/** The pointerdown event manager for the grid. */
|
|
785
|
+
pointerdown = computed(() => {
|
|
786
|
+
const manager = new PointerEventManager();
|
|
787
|
+
manager.on(e => {
|
|
788
|
+
const cell = this.inputs.getCell(e.target);
|
|
789
|
+
if (!cell)
|
|
790
|
+
return;
|
|
791
|
+
this.gridBehavior.gotoCell(cell);
|
|
792
|
+
if (this.inputs.enableSelection()) {
|
|
793
|
+
this.dragging.set(true);
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
if (this.inputs.enableSelection()) {
|
|
797
|
+
manager
|
|
798
|
+
.on([Modifier.Ctrl, Modifier.Meta], e => {
|
|
799
|
+
const cell = this.inputs.getCell(e.target);
|
|
800
|
+
if (!cell)
|
|
801
|
+
return;
|
|
802
|
+
this.gridBehavior.toggleSelect(cell);
|
|
803
|
+
})
|
|
804
|
+
.on(Modifier.Shift, e => {
|
|
805
|
+
const cell = this.inputs.getCell(e.target);
|
|
806
|
+
if (!cell)
|
|
807
|
+
return;
|
|
808
|
+
this.gridBehavior.rangeSelect(cell);
|
|
809
|
+
this.dragging.set(true);
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
return manager;
|
|
813
|
+
});
|
|
814
|
+
/** The pointerup event manager for the grid. */
|
|
815
|
+
pointerup = computed(() => {
|
|
816
|
+
const manager = new PointerEventManager();
|
|
817
|
+
if (this.inputs.enableSelection()) {
|
|
818
|
+
manager.on([Modifier.Shift, Modifier.None], () => {
|
|
819
|
+
this.dragging.set(false);
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
return manager;
|
|
823
|
+
});
|
|
824
|
+
constructor(inputs) {
|
|
825
|
+
this.inputs = inputs;
|
|
826
|
+
this.gridBehavior = new Grid({
|
|
827
|
+
...inputs,
|
|
828
|
+
cells: computed(() => this.inputs.rows().map(row => row.inputs.cells())),
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
/** Handles keydown events on the grid. */
|
|
832
|
+
onKeydown(event) {
|
|
833
|
+
if (!this.disabled()) {
|
|
834
|
+
this.keydown().handle(event);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
/** Handles pointerdown events on the grid. */
|
|
838
|
+
onPointerdown(event) {
|
|
839
|
+
if (!this.disabled()) {
|
|
840
|
+
this.pointerdown().handle(event);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
/** Handles pointermove events on the grid. */
|
|
844
|
+
onPointermove(event) {
|
|
845
|
+
if (this.disabled())
|
|
846
|
+
return;
|
|
847
|
+
if (!this.inputs.enableSelection())
|
|
848
|
+
return;
|
|
849
|
+
if (!this.dragging())
|
|
850
|
+
return;
|
|
851
|
+
const cell = this.inputs.getCell(event.target);
|
|
852
|
+
if (!cell)
|
|
853
|
+
return;
|
|
854
|
+
this.gridBehavior.rangeSelect(cell);
|
|
855
|
+
}
|
|
856
|
+
/** Handles pointerup events on the grid. */
|
|
857
|
+
onPointerup(event) {
|
|
858
|
+
if (!this.disabled()) {
|
|
859
|
+
this.pointerup().handle(event);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
/** Handles focusin events on the grid. */
|
|
863
|
+
onFocusIn(event) {
|
|
864
|
+
this.isFocused.set(true);
|
|
865
|
+
const cell = this.inputs.getCell(event.target);
|
|
866
|
+
if (!cell)
|
|
867
|
+
return;
|
|
868
|
+
this.gridBehavior.gotoCell(cell);
|
|
869
|
+
}
|
|
870
|
+
/** Indicates maybe the losing focus is caused by row/cell deletion. */
|
|
871
|
+
_maybeDeletion = signal(false);
|
|
872
|
+
/** Handles focusout events on the grid. */
|
|
873
|
+
onFocusOut(event) {
|
|
874
|
+
const parentEl = this.inputs.element();
|
|
875
|
+
const targetEl = event.relatedTarget;
|
|
876
|
+
// If a `relatedTarget` is null, then it can be caused by either
|
|
877
|
+
// - Clicking on a non-focusable element, or
|
|
878
|
+
// - The focused element is removed from the page.
|
|
879
|
+
if (targetEl === null) {
|
|
880
|
+
this._maybeDeletion.set(true);
|
|
881
|
+
}
|
|
882
|
+
if (parentEl.contains(targetEl))
|
|
883
|
+
return;
|
|
884
|
+
this.isFocused.set(false);
|
|
885
|
+
}
|
|
886
|
+
/** Indicates the losing focus is certainly caused by row/cell deletion. */
|
|
887
|
+
_deletion = signal(false);
|
|
888
|
+
/** Resets the active state of the grid if it is empty or stale. */
|
|
889
|
+
resetStateEffect() {
|
|
890
|
+
const hasReset = this.gridBehavior.resetState();
|
|
891
|
+
// If the active state has been reset right after a focusout event, then
|
|
892
|
+
// we know it's caused by a row/cell deletion.
|
|
893
|
+
if (hasReset && this._maybeDeletion()) {
|
|
894
|
+
this._deletion.set(true);
|
|
895
|
+
}
|
|
896
|
+
if (this._maybeDeletion()) {
|
|
897
|
+
this._maybeDeletion.set(false);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
/** Focuses on the active cell element. */
|
|
901
|
+
focusEffect() {
|
|
902
|
+
const activeCell = this.activeCell();
|
|
903
|
+
const hasFocus = this.isFocused();
|
|
904
|
+
const deletion = this._deletion();
|
|
905
|
+
const isRoving = this.inputs.focusMode() === 'roving';
|
|
906
|
+
if (activeCell !== undefined && isRoving && (hasFocus || deletion)) {
|
|
907
|
+
activeCell.element().focus();
|
|
908
|
+
if (deletion) {
|
|
909
|
+
this._deletion.set(false);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/** The UI pattern for a grid row. */
|
|
916
|
+
class GridRowPattern {
|
|
917
|
+
inputs;
|
|
918
|
+
/** The index of this row within the grid. */
|
|
919
|
+
rowIndex;
|
|
920
|
+
constructor(inputs) {
|
|
921
|
+
this.inputs = inputs;
|
|
922
|
+
this.rowIndex = inputs.rowIndex;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/** The UI pattern for a grid cell. */
|
|
927
|
+
class GridCellPattern {
|
|
928
|
+
inputs;
|
|
929
|
+
/** A unique identifier for the cell. */
|
|
930
|
+
id;
|
|
931
|
+
/** Whether a cell is disabled. */
|
|
932
|
+
disabled;
|
|
933
|
+
/** Whether the cell is selected. */
|
|
934
|
+
selected;
|
|
935
|
+
/** Whether the cell is selectable. */
|
|
936
|
+
selectable;
|
|
937
|
+
/** The number of rows the cell should span. */
|
|
938
|
+
rowSpan;
|
|
939
|
+
/** The number of columns the cell should span. */
|
|
940
|
+
colSpan;
|
|
941
|
+
/** The `aria-selected` attribute for the cell. */
|
|
942
|
+
ariaSelected = computed(() => this.inputs.grid().inputs.enableSelection() && this.selectable() ? this.selected() : undefined);
|
|
943
|
+
/** The `aria-rowindex` attribute for the cell. */
|
|
944
|
+
ariaRowIndex = computed(() => this.inputs.row().rowIndex() ??
|
|
945
|
+
this.inputs.rowIndex() ??
|
|
946
|
+
this.inputs.grid().gridBehavior.rowIndex(this));
|
|
947
|
+
/** The `aria-colindex` attribute for the cell. */
|
|
948
|
+
ariaColIndex = computed(() => this.inputs.colIndex() ?? this.inputs.grid().gridBehavior.colIndex(this));
|
|
949
|
+
/** The html element that should receive focus. */
|
|
950
|
+
element = computed(() => this.inputs.widget()?.element() ?? this.inputs.element());
|
|
951
|
+
/** Whether the cell is active. */
|
|
952
|
+
active = computed(() => this.inputs.grid().activeCell() === this);
|
|
953
|
+
/** The internal tab index calculation for the cell. */
|
|
954
|
+
_tabIndex = computed(() => this.inputs.grid().gridBehavior.cellTabIndex(this));
|
|
955
|
+
/** The `tabindex` for the cell. If the cell contains a widget, the cell's tabindex is -1. */
|
|
956
|
+
tabIndex = computed(() => this.inputs.widget() !== undefined ? -1 : this._tabIndex());
|
|
957
|
+
/** Whether the widget within the cell is activated. */
|
|
958
|
+
widgetActivated = computed(() => this.inputs.widget()?.inputs.activate() ?? false);
|
|
959
|
+
constructor(inputs) {
|
|
960
|
+
this.inputs = inputs;
|
|
961
|
+
this.id = inputs.id;
|
|
962
|
+
this.disabled = inputs.disabled;
|
|
963
|
+
this.rowSpan = inputs.rowSpan;
|
|
964
|
+
this.colSpan = inputs.colSpan;
|
|
965
|
+
this.selected = inputs.selected;
|
|
966
|
+
this.selectable = inputs.selectable;
|
|
967
|
+
}
|
|
968
|
+
/** Gets the `tabindex` for the widget within the cell. */
|
|
969
|
+
widgetTabIndex() {
|
|
970
|
+
return this._tabIndex();
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/** The UI pattern for a widget inside a grid cell. */
|
|
975
|
+
class GridCellWidgetPattern {
|
|
976
|
+
inputs;
|
|
977
|
+
/** The html element that should receive focus. */
|
|
978
|
+
element;
|
|
979
|
+
/** The `tabindex` for the widget. */
|
|
980
|
+
tabIndex = computed(() => this.inputs.cell().widgetTabIndex());
|
|
981
|
+
/** Whether the widget is in an active state (i.e. its containing cell is active). */
|
|
982
|
+
active = computed(() => this.inputs.cell().active());
|
|
983
|
+
constructor(inputs) {
|
|
984
|
+
this.inputs = inputs;
|
|
985
|
+
this.element = inputs.element;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
export { GridCellPattern, GridCellWidgetPattern, GridPattern, GridRowPattern, KeyboardEventManager, Modifier, PointerEventManager };
|
|
990
|
+
//# sourceMappingURL=_widget-chunk.mjs.map
|