Skip to content

Commit 09a617c

Browse files
committed
refactor(aria/grid): split directives and resolve circular dependencies
Splits up the directives across files and resolve circular dependencies in them.
1 parent 5cb59cd commit 09a617c

File tree

8 files changed

+391
-306
lines changed

8 files changed

+391
-306
lines changed

goldens/aria/grid/index.api.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
55
```ts
66

7+
import * as _angular_aria_private_public_api from '@angular/aria/private/public-api';
78
import * as _angular_cdk_bidi from '@angular/cdk/bidi';
89
import * as _angular_core from '@angular/core';
910
import { ElementRef } from '@angular/core';
@@ -83,7 +84,7 @@ export class GridCellWidget {
8384
// @public
8485
export class GridRow {
8586
readonly element: HTMLElement;
86-
readonly _gridPattern: Signal<GridPattern>;
87+
readonly _gridPattern: Signal<_angular_aria_private_public_api.GridPattern>;
8788
readonly _pattern: GridRowPattern;
8889
readonly rowIndex: _angular_core.InputSignal<number | undefined>;
8990
// (undocumented)

src/aria/grid/BUILD.bazel

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ package(default_visibility = ["//visibility:public"])
44

55
ng_project(
66
name = "grid",
7-
srcs = [
8-
"grid.ts",
9-
"index.ts",
10-
],
7+
srcs = glob(
8+
["**/*.ts"],
9+
exclude = ["**/*.spec.ts"],
10+
),
1111
deps = [
1212
"//:node_modules/@angular/core",
1313
"//src/aria/private",

src/aria/grid/grid-cell-widget.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {_IdGenerator} from '@angular/cdk/a11y';
10+
import {
11+
afterRenderEffect,
12+
booleanAttribute,
13+
computed,
14+
Directive,
15+
ElementRef,
16+
inject,
17+
input,
18+
output,
19+
Signal,
20+
} from '@angular/core';
21+
import {GridCellWidgetPattern} from '../private';
22+
import {GRID_CELL} from './grid-tokens';
23+
24+
/**
25+
* Represents an interactive element inside a `GridCell`. It allows for pausing grid navigation to
26+
* interact with the widget.
27+
*
28+
* When the user interacts with the widget (e.g., by typing in an input or opening a menu), grid
29+
* navigation is temporarily suspended to allow the widget to handle keyboard
30+
* events.
31+
*
32+
* ```html
33+
* <td ngGridCell>
34+
* <button ngGridCellWidget>Click Me</button>
35+
* </td>
36+
* ```
37+
*
38+
* @developerPreview 21.0
39+
*/
40+
@Directive({
41+
selector: '[ngGridCellWidget]',
42+
exportAs: 'ngGridCellWidget',
43+
host: {
44+
'[attr.data-active]': 'active()',
45+
'[attr.data-active-control]': 'isActivated() ? "widget" : "cell"',
46+
'[tabindex]': '_tabIndex()',
47+
},
48+
})
49+
export class GridCellWidget {
50+
/** A reference to the host element. */
51+
private readonly _elementRef = inject(ElementRef);
52+
53+
/** A reference to the host element. */
54+
readonly element = this._elementRef.nativeElement as HTMLElement;
55+
56+
/** Whether the widget is currently active (focused). */
57+
readonly active = computed(() => this._pattern.active());
58+
59+
/** The parent cell. */
60+
private readonly _cell = inject(GRID_CELL);
61+
62+
/** A unique identifier for the widget. */
63+
readonly id = input(inject(_IdGenerator).getId('ng-grid-cell-widget-', true));
64+
65+
/** The type of widget, which determines how it is activated. */
66+
readonly widgetType = input<'simple' | 'complex' | 'editable'>('simple');
67+
68+
/** Whether the widget is disabled. */
69+
readonly disabled = input(false, {transform: booleanAttribute});
70+
71+
/** The target that will receive focus instead of the widget. */
72+
readonly focusTarget = input<ElementRef | HTMLElement | undefined>();
73+
74+
/** Emits when the widget is activated. */
75+
readonly onActivate = output<KeyboardEvent | FocusEvent | undefined>();
76+
77+
/** Emits when the widget is deactivated. */
78+
readonly onDeactivate = output<KeyboardEvent | FocusEvent | undefined>();
79+
80+
/** The tabindex override. */
81+
readonly tabindex = input<number | undefined>();
82+
83+
/**
84+
* The tabindex value set to the element.
85+
* If a focus target exists then return -1. Unless an override.
86+
*/
87+
protected readonly _tabIndex: Signal<number> = computed(
88+
() => this.tabindex() ?? (this.focusTarget() ? -1 : this._pattern.tabIndex()),
89+
);
90+
91+
/** The UI pattern for the grid cell widget. */
92+
readonly _pattern = new GridCellWidgetPattern({
93+
...this,
94+
element: () => this.element,
95+
cell: () => this._cell._pattern,
96+
focusTarget: computed(() => {
97+
if (this.focusTarget() instanceof ElementRef) {
98+
return (this.focusTarget() as ElementRef).nativeElement;
99+
}
100+
return this.focusTarget();
101+
}),
102+
});
103+
104+
/** Whether the widget is activated. */
105+
get isActivated(): Signal<boolean> {
106+
return this._pattern.isActivated.asReadonly();
107+
}
108+
109+
constructor() {
110+
afterRenderEffect(() => {
111+
const activateEvent = this._pattern.lastActivateEvent();
112+
if (activateEvent) {
113+
this.onActivate.emit(activateEvent);
114+
}
115+
});
116+
117+
afterRenderEffect(() => {
118+
const deactivateEvent = this._pattern.lastDeactivateEvent();
119+
if (deactivateEvent) {
120+
this.onDeactivate.emit(deactivateEvent);
121+
}
122+
});
123+
}
124+
125+
/** Activates the widget. */
126+
activate(): void {
127+
this._pattern.activate();
128+
}
129+
130+
/** Deactivates the widget. */
131+
deactivate(): void {
132+
this._pattern.deactivate();
133+
}
134+
}

src/aria/grid/grid-cell.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {_IdGenerator} from '@angular/cdk/a11y';
10+
import {
11+
booleanAttribute,
12+
computed,
13+
contentChildren,
14+
Directive,
15+
ElementRef,
16+
inject,
17+
input,
18+
model,
19+
Signal,
20+
} from '@angular/core';
21+
import {Directionality} from '@angular/cdk/bidi';
22+
import {GridCellPattern} from '../private';
23+
import {GridCellWidget} from './grid-cell-widget';
24+
import {GRID_CELL, GRID_ROW} from './grid-tokens';
25+
26+
/**
27+
* Represents a cell within a grid row. It is the primary focusable element
28+
* within the grid. It can be disabled and can have its selection state managed
29+
* through the `selected` input.
30+
*
31+
* ```html
32+
* <td ngGridCell [disabled]="isDisabled" [(selected)]="isSelected">
33+
* Cell Content
34+
* </td>
35+
* ```
36+
*
37+
* @developerPreview 21.0
38+
*/
39+
@Directive({
40+
selector: '[ngGridCell]',
41+
exportAs: 'ngGridCell',
42+
host: {
43+
'[attr.role]': 'role()',
44+
'[attr.id]': '_pattern.id()',
45+
'[attr.rowspan]': '_pattern.rowSpan()',
46+
'[attr.colspan]': '_pattern.colSpan()',
47+
'[attr.data-active]': 'active()',
48+
'[attr.data-anchor]': '_pattern.anchor()',
49+
'[attr.aria-disabled]': '_pattern.disabled()',
50+
'[attr.aria-rowspan]': '_pattern.rowSpan()',
51+
'[attr.aria-colspan]': '_pattern.colSpan()',
52+
'[attr.aria-rowindex]': '_pattern.ariaRowIndex()',
53+
'[attr.aria-colindex]': '_pattern.ariaColIndex()',
54+
'[attr.aria-selected]': '_pattern.ariaSelected()',
55+
'[tabindex]': '_tabIndex()',
56+
},
57+
providers: [{provide: GRID_CELL, useExisting: GridCell}],
58+
})
59+
export class GridCell {
60+
/** A reference to the host element. */
61+
private readonly _elementRef = inject(ElementRef);
62+
63+
/** A reference to the host element. */
64+
readonly element = this._elementRef.nativeElement as HTMLElement;
65+
66+
/** Whether the cell is currently active (focused). */
67+
readonly active = computed(() => this._pattern.active());
68+
69+
/** The widgets contained within this cell, if any. */
70+
private readonly _widgets = contentChildren(GridCellWidget, {descendants: true});
71+
72+
/** The UI pattern for the widget in this cell. */
73+
private readonly _widgetPatterns: Signal<any[]> = computed(() =>
74+
this._widgets().map(w => w._pattern),
75+
);
76+
77+
/** The parent row. */
78+
private readonly _row = inject(GRID_ROW);
79+
80+
/** Text direction. */
81+
readonly textDirection = inject(Directionality).valueSignal;
82+
83+
/** A unique identifier for the cell. */
84+
readonly id = input(inject(_IdGenerator).getId('ng-grid-cell-', true));
85+
86+
/** The ARIA role for the cell. */
87+
readonly role = input<'gridcell' | 'columnheader' | 'rowheader'>('gridcell');
88+
89+
/** The number of rows the cell should span. */
90+
readonly rowSpan = input<number>(1);
91+
92+
/** The number of columns the cell should span. */
93+
readonly colSpan = input<number>(1);
94+
95+
/** The index of this cell's row within the grid. */
96+
readonly rowIndex = input<number>();
97+
98+
/** The index of this cell's column within the grid. */
99+
readonly colIndex = input<number>();
100+
101+
/** Whether the cell is disabled. */
102+
readonly disabled = input(false, {transform: booleanAttribute});
103+
104+
/** Whether the cell is selected. */
105+
readonly selected = model<boolean>(false);
106+
107+
/** Whether the cell is selectable. */
108+
readonly selectable = input<boolean>(true);
109+
110+
/** Orientation of the widgets in the cell. */
111+
readonly orientation = input<'vertical' | 'horizontal'>('horizontal');
112+
113+
/** Whether widgets navigation wraps. */
114+
readonly wrap = input(true, {transform: booleanAttribute});
115+
116+
/** The tabindex override. */
117+
readonly tabindex = input<number | undefined>();
118+
119+
/**
120+
* The tabindex value set to the element.
121+
* If a focus target exists then return -1. Unless an override.
122+
*/
123+
protected readonly _tabIndex: Signal<number> = computed(
124+
() => this.tabindex() ?? this._pattern.tabIndex(),
125+
);
126+
127+
/** The UI pattern for the grid cell. */
128+
readonly _pattern = new GridCellPattern({
129+
...this,
130+
grid: this._row._gridPattern,
131+
row: () => this._row._pattern,
132+
widgets: this._widgetPatterns,
133+
getWidget: e => this._getWidget(e),
134+
element: () => this.element,
135+
});
136+
137+
constructor() {}
138+
139+
/** Gets the cell widget pattern for a given element. */
140+
private _getWidget(element: Element | null | undefined): any | undefined {
141+
let target = element;
142+
143+
while (target) {
144+
const pattern = this._widgetPatterns().find(w => w.element() === target);
145+
if (pattern) {
146+
return pattern;
147+
}
148+
149+
target = target.parentElement?.closest('[ngGridCellWidget]');
150+
}
151+
152+
return undefined;
153+
}
154+
}

src/aria/grid/grid-row.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {
10+
computed,
11+
contentChildren,
12+
Directive,
13+
ElementRef,
14+
inject,
15+
input,
16+
Signal,
17+
} from '@angular/core';
18+
import {GridRowPattern} from '../private';
19+
import {Grid} from './grid';
20+
import {GRID_CELL, GRID_ROW} from './grid-tokens';
21+
22+
/**
23+
* Represents a row within a grid. It is a container for `ngGridCell` directives.
24+
*
25+
* ```html
26+
* <tr ngGridRow>
27+
* <!-- ... cells ... -->
28+
* </tr>
29+
* ```
30+
*
31+
* @developerPreview 21.0
32+
*/
33+
@Directive({
34+
selector: '[ngGridRow]',
35+
exportAs: 'ngGridRow',
36+
host: {
37+
'role': 'row',
38+
'[attr.aria-rowindex]': '_pattern.rowIndex()',
39+
},
40+
providers: [{provide: GRID_ROW, useExisting: GridRow}],
41+
})
42+
export class GridRow {
43+
/** A reference to the host element. */
44+
private readonly _elementRef = inject(ElementRef);
45+
46+
/** A reference to the host element. */
47+
readonly element = this._elementRef.nativeElement as HTMLElement;
48+
49+
/** The cells that make up this row. */
50+
private readonly _cells = contentChildren(GRID_CELL, {descendants: true});
51+
52+
/** The UI patterns for the cells in this row. */
53+
private readonly _cellPatterns: Signal<any[]> = computed(() =>
54+
this._cells().map(c => c._pattern),
55+
);
56+
57+
/** The parent grid. */
58+
private readonly _grid = inject(Grid);
59+
60+
/** The parent grid UI pattern. */
61+
readonly _gridPattern = computed(() => this._grid._pattern);
62+
63+
/** The index of this row within the grid. */
64+
readonly rowIndex = input<number>();
65+
66+
/** The UI pattern for the grid row. */
67+
readonly _pattern = new GridRowPattern({
68+
...this,
69+
cells: this._cellPatterns,
70+
grid: this._gridPattern,
71+
});
72+
}

0 commit comments

Comments
 (0)