Skip to content

Commit 65a56ad

Browse files
committed
refactor(aria/accordion): Replace ContentChildren with manual regsitration of triggers
1 parent c65a8dd commit 65a56ad

File tree

5 files changed

+65
-9
lines changed

5 files changed

+65
-9
lines changed

goldens/aria/accordion/index.api.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ export class AccordionGroup {
2525
expandAll(): void;
2626
readonly multiExpandable: _angular_core.InputSignalWithTransform<boolean, unknown>;
2727
readonly _pattern: AccordionGroupPattern;
28+
_registerTrigger(trigger: AccordionTrigger): void;
2829
readonly softDisabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
2930
readonly textDirection: _angular_core.WritableSignal<_angular_cdk_bidi.Direction>;
31+
_unregisterTrigger(trigger: AccordionTrigger): void;
3032
readonly wrap: _angular_core.InputSignalWithTransform<boolean, unknown>;
3133
// (undocumented)
32-
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<AccordionGroup, "[ngAccordionGroup]", ["ngAccordionGroup"], { "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "multiExpandable": { "alias": "multiExpandable"; "required": false; "isSignal": true; }; "softDisabled": { "alias": "softDisabled"; "required": false; "isSignal": true; }; "wrap": { "alias": "wrap"; "required": false; "isSignal": true; }; }, {}, ["_triggers"], never, true, never>;
34+
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<AccordionGroup, "[ngAccordionGroup]", ["ngAccordionGroup"], { "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "multiExpandable": { "alias": "multiExpandable"; "required": false; "isSignal": true; }; "softDisabled": { "alias": "softDisabled"; "required": false; "isSignal": true; }; "wrap": { "alias": "wrap"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
3335
// (undocumented)
3436
static ɵfac: _angular_core.ɵɵFactoryDeclaration<AccordionGroup, never>;
3537
}
@@ -50,22 +52,25 @@ export class AccordionPanel {
5052
}
5153

5254
// @public
53-
export class AccordionTrigger implements OnInit {
55+
export class AccordionTrigger implements OnInit, OnDestroy {
5456
readonly active: _angular_core.Signal<boolean>;
5557
collapse(): void;
5658
readonly disabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
5759
readonly element: HTMLElement;
5860
expand(): void;
5961
readonly expanded: _angular_core.ModelSignal<boolean>;
6062
readonly id: _angular_core.InputSignal<string>;
63+
readonly index: _angular_core.InputSignal<number | undefined>;
64+
// (undocumented)
65+
ngOnDestroy(): void;
6166
// (undocumented)
6267
ngOnInit(): void;
6368
readonly panel: _angular_core.InputSignal<AccordionPanel>;
6469
readonly panelId: _angular_core.Signal<string>;
6570
_pattern: AccordionTriggerPattern;
6671
toggle(): void;
6772
// (undocumented)
68-
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<AccordionTrigger, "[ngAccordionTrigger]", ["ngAccordionTrigger"], { "panel": { "alias": "panel"; "required": true; "isSignal": true; }; "id": { "alias": "id"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "expanded": { "alias": "expanded"; "required": false; "isSignal": true; }; }, { "expanded": "expandedChange"; }, never, never, true, never>;
73+
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<AccordionTrigger, "[ngAccordionTrigger]", ["ngAccordionTrigger"], { "panel": { "alias": "panel"; "required": true; "isSignal": true; }; "id": { "alias": "id"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "index": { "alias": "index"; "required": false; "isSignal": true; }; "expanded": { "alias": "expanded"; "required": false; "isSignal": true; }; }, { "expanded": "expandedChange"; }, never, never, true, never>;
6974
// (undocumented)
7075
static ɵfac: _angular_core.ɵɵFactoryDeclaration<AccordionTrigger, never>;
7176
}

goldens/aria/private/index.api.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,12 @@ export class GridRowPattern {
437437
rowIndex: SignalLike<number | undefined>;
438438
}
439439

440+
// @public (undocumented)
441+
export interface HasElement {
442+
// (undocumented)
443+
element: HTMLElement;
444+
}
445+
440446
// @public (undocumented)
441447
export function linkedSignal<T>(sourceFn: () => T): WritableSignalLike<T>;
442448

@@ -663,6 +669,9 @@ export function signal<T>(initialValue: T): WritableSignalLike<T>;
663669
// @public (undocumented)
664670
export type SignalLike<T> = () => T;
665671

672+
// @public
673+
export function sortDirectives(a: HasElement, b: HasElement): 1 | -1;
674+
666675
// @public
667676
export interface TabInputs extends Omit<ListNavigationItem, 'index'>, Omit<ExpansionItem, 'expandable'> {
668677
tablist: SignalLike<TabListPattern>;

src/aria/accordion/accordion-group.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
ElementRef,
1212
booleanAttribute,
1313
computed,
14-
contentChildren,
1514
inject,
1615
input,
1716
signal,
@@ -75,10 +74,17 @@ export class AccordionGroup {
7574
readonly element = this._elementRef.nativeElement as HTMLElement;
7675

7776
/** The AccordionTriggers nested inside this group. */
78-
private readonly _triggers = contentChildren(AccordionTrigger, {descendants: true});
77+
private readonly _triggers = signal(new Set<AccordionTrigger>());
78+
79+
/** The AccordionTriggers nested inside this group. */
80+
private readonly _sortedTriggers = computed(() => {
81+
return [...this._triggers()].sort((a, b) => a.index()! - b.index()!);
82+
});
7983

8084
/** The corresponding patterns for the accordion triggers. */
81-
private readonly _triggerPatterns = computed(() => this._triggers().map(t => t._pattern));
85+
private readonly _triggerPatterns = computed(() => {
86+
return this._sortedTriggers().map(t => t._pattern);
87+
});
8288

8389
/** The text direction (ltr or rtl). */
8490
readonly textDirection = inject(Directionality).valueSignal;
@@ -117,4 +123,16 @@ export class AccordionGroup {
117123
collapseAll() {
118124
this._pattern.collapseAll();
119125
}
126+
127+
/** Internal method to register each trigger as we can not use contentChildren. */
128+
_registerTrigger(trigger: AccordionTrigger) {
129+
this._triggers().add(trigger);
130+
this._triggers.set(new Set(this._triggers()));
131+
}
132+
133+
/** Internal method to unregister each trigger as we can not use contentChildren. */
134+
_unregisterTrigger(trigger: AccordionTrigger) {
135+
this._triggers().delete(trigger);
136+
this._triggers.set(new Set(this._triggers()));
137+
}
120138
}

src/aria/accordion/accordion-trigger.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {
1010
Directive,
1111
ElementRef,
12+
OnDestroy,
1213
OnInit,
1314
booleanAttribute,
1415
computed,
@@ -53,7 +54,7 @@ import {AccordionPanel} from './accordion-panel';
5354
'[attr.tabindex]': '_pattern.tabIndex()',
5455
},
5556
})
56-
export class AccordionTrigger implements OnInit {
57+
export class AccordionTrigger implements OnInit, OnDestroy {
5758
/** A reference to the trigger element. */
5859
private readonly _elementRef = inject(ElementRef);
5960

@@ -69,12 +70,15 @@ export class AccordionTrigger implements OnInit {
6970
/** The unique identifier for the trigger. */
7071
readonly id = input(inject(_IdGenerator).getId('ng-accordion-trigger-', true));
7172

72-
/** The unique identifier for the correspondingtrigger panel. */
73+
/** The unique identifier for the corresponding trigger panel. */
7374
readonly panelId = computed(() => this.panel().id());
7475

7576
/** Whether the trigger is disabled. */
7677
readonly disabled = input(false, {transform: booleanAttribute});
7778

79+
/** The index of the trigger within the accordion group. */
80+
readonly index = input<number>();
81+
7882
/** Whether the corresponding panel is expanded. */
7983
readonly expanded = model<boolean>(false);
8084

@@ -93,6 +97,14 @@ export class AccordionTrigger implements OnInit {
9397
});
9498

9599
this.panel()._pattern = this._pattern;
100+
101+
this._accordionGroup._registerTrigger(this);
102+
}
103+
104+
ngOnDestroy() {
105+
this.panel()._pattern = undefined;
106+
107+
this._accordionGroup._unregisterTrigger(this);
96108
}
97109

98110
/** Expands this item. */

src/aria/accordion/accordion.spec.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {AccordionGroup} from './accordion-group';
1010

1111
describe('AccordionGroup', () => {
1212
let fixture: ComponentFixture<AccordionGroupExample>;
13+
let testComponent: AccordionGroupExample;
1314
let triggerDebugElements: DebugElement[];
1415
let panelDebugElements: DebugElement[];
1516
let triggerElements: HTMLElement[];
@@ -42,7 +43,7 @@ describe('AccordionGroup', () => {
4243
}
4344

4445
function configureAccordionComponent(opts: SetupOptions = {}) {
45-
const testComponent = fixture.componentInstance as AccordionGroupExample;
46+
testComponent = fixture.componentInstance as AccordionGroupExample;
4647

4748
if (opts.multiExpandable !== undefined) {
4849
testComponent.multiExpandable.set(opts.multiExpandable);
@@ -306,6 +307,16 @@ describe('AccordionGroup', () => {
306307
expect(isTriggerExpanded(triggerElements[0])).toBeFalse();
307308
});
308309

310+
it('should focus on new last trigger with End', () => {
311+
const items = testComponent.items().reverse();
312+
testComponent.items.set([...items]);
313+
fixture.detectChanges();
314+
315+
// Now reverse, End should move to the former first trigger.
316+
endKey(triggerElements[1]);
317+
expect(isTriggerActive(triggerElements[0])).toBeTrue();
318+
});
319+
309320
describe('wrap behavior', () => {
310321
it('should wrap to first on ArrowDown from last if wrap=true', () => {
311322
configureAccordionComponent({wrap: true});
@@ -389,6 +400,7 @@ describe('AccordionGroup', () => {
389400
ngAccordionTrigger
390401
[panel]="panel"
391402
[disabled]="item.disabled"
403+
[index]="$index"
392404
[(expanded)]="item.expanded"
393405
>{{ item.header }}</button>
394406
<div

0 commit comments

Comments
 (0)