Skip to content

Commit d8fbab6

Browse files
committed
refactor(aria/toolbar): split directives and resolve circular dependencies
Splits up the directives across files and resolve circular dependencies in them.
1 parent 8e0c53c commit d8fbab6

File tree

7 files changed

+248
-161
lines changed

7 files changed

+248
-161
lines changed

goldens/aria/toolbar/index.api.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import * as _angular_cdk_bidi from '@angular/cdk/bidi';
88
import * as _angular_core from '@angular/core';
9+
import * as i1 from '@angular/aria/private';
910
import { OnDestroy } from '@angular/core';
1011
import { OnInit } from '@angular/core';
1112
import { ToolbarPattern } from '@angular/aria/private';
@@ -17,7 +18,7 @@ export class Toolbar<V> {
1718
constructor();
1819
readonly disabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
1920
readonly element: HTMLElement;
20-
readonly _itemPatterns: _angular_core.Signal<ToolbarWidgetPattern<V>[]>;
21+
readonly _itemPatterns: _angular_core.Signal<i1.ToolbarWidgetPattern<V>[]>;
2122
// (undocumented)
2223
_onFocus(): void;
2324
readonly orientation: _angular_core.InputSignal<"vertical" | "horizontal">;
@@ -41,7 +42,7 @@ export class ToolbarWidget<V> implements OnInit, OnDestroy {
4142
readonly active: _angular_core.Signal<boolean>;
4243
readonly disabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
4344
readonly element: HTMLElement;
44-
readonly _group: ToolbarWidgetGroup<any> | null;
45+
readonly _group: ToolbarWidgetGroup<V> | null;
4546
readonly hardDisabled: _angular_core.Signal<boolean>;
4647
readonly id: _angular_core.InputSignal<string>;
4748
// (undocumented)
@@ -50,7 +51,7 @@ export class ToolbarWidget<V> implements OnInit, OnDestroy {
5051
ngOnInit(): void;
5152
readonly _pattern: ToolbarWidgetPattern<V>;
5253
readonly selected: () => boolean;
53-
readonly _toolbarPattern: _angular_core.Signal<ToolbarPattern<any>>;
54+
readonly _toolbarPattern: _angular_core.Signal<i1.ToolbarPattern<V>>;
5455
readonly value: _angular_core.InputSignal<V>;
5556
// (undocumented)
5657
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<ToolbarWidget<any>, "[ngToolbarWidget]", ["ngToolbarWidget"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;

src/aria/toolbar/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
export {Toolbar, ToolbarWidget, ToolbarWidgetGroup} from './toolbar';
9+
export {Toolbar} from './toolbar';
10+
export {ToolbarWidget} from './toolbar-widget';
11+
export {ToolbarWidgetGroup} from './toolbar-widget-group';
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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+
Directive,
11+
ElementRef,
12+
inject,
13+
computed,
14+
input,
15+
booleanAttribute,
16+
contentChildren,
17+
} from '@angular/core';
18+
import {ToolbarWidgetPattern, ToolbarWidgetGroupPattern} from '@angular/aria/private';
19+
import {Toolbar} from './toolbar';
20+
import {ToolbarWidget} from './toolbar-widget';
21+
import {TOOLBAR_WIDGET_GROUP} from './utils';
22+
23+
/**
24+
* A directive that groups toolbar widgets, used for more complex widgets like radio groups
25+
* that have their own internal navigation.
26+
*
27+
* @developerPreview 21.0
28+
*/
29+
@Directive({
30+
selector: '[ngToolbarWidgetGroup]',
31+
exportAs: 'ngToolbarWidgetGroup',
32+
providers: [{provide: TOOLBAR_WIDGET_GROUP, useExisting: ToolbarWidgetGroup}],
33+
})
34+
export class ToolbarWidgetGroup<V> {
35+
/** A reference to the host element. */
36+
private readonly _elementRef = inject(ElementRef);
37+
38+
/** A reference to the host element. */
39+
readonly element = this._elementRef.nativeElement as HTMLElement;
40+
41+
/** The parent Toolbar. */
42+
private readonly _toolbar = inject<Toolbar<V>>(Toolbar, {optional: true});
43+
44+
/** The list of child widgets within the group. */
45+
private readonly _widgets = contentChildren(ToolbarWidget, {descendants: true});
46+
47+
/** The parent Toolbar UIPattern. */
48+
private readonly _toolbarPattern = computed(() => this._toolbar?._pattern);
49+
50+
/** Whether the widget group is disabled. */
51+
readonly disabled = input(false, {transform: booleanAttribute});
52+
53+
/** The list of toolbar items within the group. */
54+
private readonly _itemPatterns = () => this._widgets().map(w => w._pattern);
55+
56+
/** Whether the group allows multiple widgets to be selected. */
57+
readonly multi = input(false, {transform: booleanAttribute});
58+
59+
/** The ToolbarWidgetGroup UIPattern. */
60+
readonly _pattern = new ToolbarWidgetGroupPattern<ToolbarWidgetPattern<V>, V>({
61+
...this,
62+
items: this._itemPatterns,
63+
toolbar: this._toolbarPattern,
64+
});
65+
}

src/aria/toolbar/toolbar-widget.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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+
Directive,
11+
ElementRef,
12+
inject,
13+
computed,
14+
input,
15+
booleanAttribute,
16+
OnInit,
17+
OnDestroy,
18+
} from '@angular/core';
19+
import {ToolbarWidgetPattern, ToolbarWidgetGroupPattern, SignalLike} from '@angular/aria/private';
20+
import {_IdGenerator} from '@angular/cdk/a11y';
21+
import {Toolbar} from './toolbar';
22+
import {TOOLBAR_WIDGET_GROUP} from './utils';
23+
import type {ToolbarWidgetGroup} from './toolbar-widget-group';
24+
25+
/**
26+
* A widget within a toolbar.
27+
*
28+
* The `ngToolbarWidget` directive should be applied to any native HTML element that acts
29+
* as an interactive widget within an `ngToolbar` or `ngToolbarWidgetGroup`. It enables
30+
* keyboard navigation and selection within the toolbar.
31+
*
32+
* ```html
33+
* <button ngToolbarWidget value="action-id" [disabled]="isDisabled">
34+
* Perform Action
35+
* </button>
36+
* ```
37+
*
38+
* @developerPreview 21.0
39+
*/
40+
@Directive({
41+
selector: '[ngToolbarWidget]',
42+
exportAs: 'ngToolbarWidget',
43+
host: {
44+
'[attr.data-active]': 'active()',
45+
'[attr.tabindex]': '_pattern.tabIndex()',
46+
'[attr.inert]': 'hardDisabled() ? true : null',
47+
'[attr.disabled]': 'hardDisabled() ? true : null',
48+
'[attr.aria-disabled]': '_pattern.disabled()',
49+
'[id]': '_pattern.id()',
50+
},
51+
})
52+
export class ToolbarWidget<V> implements OnInit, OnDestroy {
53+
/** A reference to the host element. */
54+
private readonly _elementRef = inject(ElementRef);
55+
56+
/** A reference to the host element. */
57+
readonly element = this._elementRef.nativeElement as HTMLElement;
58+
59+
/** The parent Toolbar. */
60+
private readonly _toolbar = inject<Toolbar<V>>(Toolbar);
61+
62+
/** A unique identifier for the widget. */
63+
readonly id = input(inject(_IdGenerator).getId('ng-toolbar-widget-', true));
64+
65+
/** The parent Toolbar UIPattern. */
66+
readonly _toolbarPattern = computed(() => this._toolbar._pattern);
67+
68+
/** Whether the widget is disabled. */
69+
readonly disabled = input(false, {transform: booleanAttribute});
70+
71+
/** Whether the widget is 'hard' disabled, which is different from `aria-disabled`. A hard disabled widget cannot receive focus. */
72+
readonly hardDisabled = computed(() => this._pattern.disabled() && !this._toolbar.softDisabled());
73+
74+
/** The optional ToolbarWidgetGroup this widget belongs to. */
75+
readonly _group = inject<ToolbarWidgetGroup<V>>(TOOLBAR_WIDGET_GROUP, {optional: true});
76+
77+
/** The value associated with the widget. */
78+
readonly value = input.required<V>();
79+
80+
/** Whether the widget is currently active (focused). */
81+
readonly active = computed(() => this._pattern.active());
82+
83+
/** Whether the widget is selected (only relevant in a selection group). */
84+
readonly selected = () => this._pattern.selected();
85+
86+
private readonly _groupPattern: SignalLike<
87+
ToolbarWidgetGroupPattern<ToolbarWidgetPattern<V>, V> | undefined
88+
> = () => this._group?._pattern;
89+
90+
/** The ToolbarWidget UIPattern. */
91+
readonly _pattern = new ToolbarWidgetPattern<V>({
92+
...this,
93+
group: this._groupPattern,
94+
toolbar: this._toolbarPattern,
95+
id: this.id,
96+
value: this.value,
97+
element: () => this.element,
98+
});
99+
100+
ngOnInit() {
101+
this._toolbar._register(this);
102+
}
103+
104+
ngOnDestroy() {
105+
this._toolbar._unregister(this);
106+
}
107+
}

src/aria/toolbar/toolbar.spec.ts

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import {Component, DebugElement, Directive, inject, signal} from '@angular/core';
22
import {ComponentFixture, TestBed} from '@angular/core/testing';
33
import {By} from '@angular/platform-browser';
4-
import {Toolbar, ToolbarWidget, ToolbarWidgetGroup} from './toolbar';
54
import {provideFakeDirectionality, runAccessibilityChecks} from '@angular/cdk/testing/private';
5+
import {Toolbar} from './toolbar';
6+
import {ToolbarWidgetGroup} from './toolbar-widget-group';
7+
import {ToolbarWidget} from './toolbar-widget';
68

79
describe('Toolbar', () => {
810
let fixture: ComponentFixture<ToolbarExample>;
@@ -625,14 +627,46 @@ describe('Toolbar', () => {
625627
[disabled]="disabled()"
626628
[wrap]="wrap()"
627629
>
628-
<button ngToolbarWidget #item0="ngToolbarWidget" [aria-pressed]="item0.selected()" [disabled]="widgets[0].disabled()" value="item 0">item 0</button>
629-
<button ngToolbarWidget #item1="ngToolbarWidget" [aria-pressed]="item1.selected()" [disabled]="widgets[1].disabled()" value="item 1">item 1</button>
630+
<button
631+
ngToolbarWidget
632+
#item0="ngToolbarWidget"
633+
[aria-pressed]="item0.selected()"
634+
[disabled]="widgets[0].disabled()"
635+
value="item 0">item 0</button>
636+
637+
<button
638+
ngToolbarWidget
639+
#item1="ngToolbarWidget"
640+
[aria-pressed]="item1.selected()"
641+
[disabled]="widgets[1].disabled()"
642+
value="item 1">item 1</button>
643+
630644
<div ngToolbarWidgetGroup [disabled]="groups[0].disabled()">
631-
<button ngToolbarWidget #item2="ngToolbarWidget" [aria-pressed]="item2.selected()" [disabled]="widgets[2].disabled()" value="item 2">item 2</button>
632-
<button ngToolbarWidget #item3="ngToolbarWidget" [aria-pressed]="item3.selected()" [disabled]="widgets[3].disabled()" value="item 3">item 3</button>
633-
<button ngToolbarWidget #item4="ngToolbarWidget" [aria-pressed]="item4.selected()" [disabled]="widgets[4].disabled()" value="item 4">item 4</button>
645+
<button
646+
ngToolbarWidget
647+
#item2="ngToolbarWidget"
648+
[aria-pressed]="item2.selected()"
649+
[disabled]="widgets[2].disabled()"
650+
value="item 2">item 2</button>
651+
<button
652+
ngToolbarWidget
653+
#item3="ngToolbarWidget"
654+
[aria-pressed]="item3.selected()"
655+
[disabled]="widgets[3].disabled()"
656+
value="item 3">item 3</button>
657+
<button
658+
ngToolbarWidget
659+
#item4="ngToolbarWidget"
660+
[aria-pressed]="item4.selected()"
661+
[disabled]="widgets[4].disabled()"
662+
value="item 4">item 4</button>
634663
</div>
635-
<button ngToolbarWidget #item5="ngToolbarWidget" [aria-pressed]="item5.selected()" [disabled]="widgets[5].disabled()" value="item 5">item 5</button>
664+
<button
665+
ngToolbarWidget
666+
#item5="ngToolbarWidget"
667+
[aria-pressed]="item5.selected()"
668+
[disabled]="widgets[5].disabled()"
669+
value="item 5">item 5</button>
636670
</div>
637671
`,
638672
imports: [Toolbar, ToolbarWidget, ToolbarWidgetGroup],

0 commit comments

Comments
 (0)