forked from angular/components
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgrid.ts
More file actions
157 lines (137 loc) · 5.1 KB
/
grid.ts
File metadata and controls
157 lines (137 loc) · 5.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {
afterRenderEffect,
booleanAttribute,
computed,
contentChildren,
Directive,
ElementRef,
inject,
input,
Signal,
} from '@angular/core';
import {Directionality} from '@angular/cdk/bidi';
import {GridPattern, GridCellPattern} from '../private';
import {GRID_ROW} from './grid-tokens';
/**
* The container for a grid. It provides keyboard navigation and focus management for the grid's
* rows and cells. It manages the overall behavior of the grid, including focus
* wrapping, selection, and disabled states.
*
* ```html
* <table ngGrid [multi]="true" [enableSelection]="true">
* @for (row of gridData; track row) {
* <tr ngGridRow>
* @for (cell of row; track cell) {
* <td ngGridCell [disabled]="cell.disabled">
* {{cell.value}}
* </td>
* }
* </tr>
* }
* </table>
* ```
*
* @developerPreview 21.0
*
* @see [Grid](guide/aria/grid)
*/
@Directive({
selector: '[ngGrid]',
exportAs: 'ngGrid',
host: {
'role': 'grid',
'[tabindex]': '_pattern.tabIndex()',
'[attr.aria-disabled]': '_pattern.disabled()',
'[attr.aria-multiselectable]': '_pattern.multiSelectable()',
'[attr.aria-activedescendant]': '_pattern.activeDescendant()',
'(keydown)': '_pattern.onKeydown($event)',
'(pointerdown)': '_pattern.onPointerdown($event)',
'(focusin)': '_pattern.onFocusIn($event)',
'(focusout)': '_pattern.onFocusOut($event)',
},
})
export class Grid {
/** A reference to the host element. */
private readonly _elementRef = inject(ElementRef);
/** A reference to the host element. */
readonly element = this._elementRef.nativeElement as HTMLElement;
/** The rows that make up the grid. */
private readonly _rows = contentChildren(GRID_ROW, {descendants: true});
/** The UI patterns for the rows in the grid. */
private readonly _rowPatterns: Signal<any[]> = computed(() => this._rows().map(r => r._pattern));
/** Text direction. */
readonly textDirection = inject(Directionality).valueSignal;
/** Whether selection is enabled for the grid. */
readonly enableSelection = input(false, {transform: booleanAttribute});
/** Whether the grid is disabled. */
readonly disabled = input(false, {transform: booleanAttribute});
/**
* Whether to allow disabled items to receive focus. When `true`, disabled items are
* focusable but not interactive. When `false`, disabled items are skipped during navigation.
*/
readonly softDisabled = input(true, {transform: booleanAttribute});
/**
* The focus strategy used by the grid.
* - `roving`: Focus is moved to the active cell using `tabindex`.
* - `activedescendant`: Focus remains on the grid container, and `aria-activedescendant` is used to indicate the active cell.
*/
readonly focusMode = input<'roving' | 'activedescendant'>('roving');
/**
* The wrapping behavior for keyboard navigation along the row axis.
* - `continuous`: Navigation wraps from the last row to the first, and vice-versa.
* - `loop`: Navigation wraps within the current row.
* - `nowrap`: Navigation stops at the first/last item in the row.
*/
readonly rowWrap = input<'continuous' | 'loop' | 'nowrap'>('loop');
/**
* The wrapping behavior for keyboard navigation along the column axis.
* - `continuous`: Navigation wraps from the last column to the first, and vice-versa.
* - `loop`: Navigation wraps within the current column.
* - `nowrap`: Navigation stops at the first/last item in the column.
*/
readonly colWrap = input<'continuous' | 'loop' | 'nowrap'>('loop');
/** Whether multiple cells in the grid can be selected. */
readonly multi = input(false, {transform: booleanAttribute});
/**
* The selection strategy used by the grid.
* - `follow`: The focused cell is automatically selected.
* - `explicit`: Cells are selected explicitly by the user (e.g., via click or spacebar).
*/
readonly selectionMode = input<'follow' | 'explicit'>('follow');
/** The UI pattern for the grid. */
readonly _pattern = new GridPattern({
...this,
rows: this._rowPatterns,
getCell: e => this._getCell(e),
element: () => this.element,
});
constructor() {
afterRenderEffect(() => this._pattern.setDefaultStateEffect());
afterRenderEffect(() => this._pattern.resetStateEffect());
afterRenderEffect(() => this._pattern.resetFocusEffect());
afterRenderEffect(() => this._pattern.restoreFocusEffect());
afterRenderEffect(() => this._pattern.focusEffect());
}
/** Gets the cell pattern for a given element. */
private _getCell(element: Element | null | undefined): GridCellPattern | undefined {
let target = element;
while (target) {
for (const row of this._rowPatterns()) {
for (const cell of row.inputs.cells()) {
if (cell.element() === target) {
return cell;
}
}
}
target = target.parentElement?.closest('[ngGridCell]');
}
return undefined;
}
}