Skip to content

Commit 6202429

Browse files
committed
refactor(aria/accordion): Change to use template references to match panel with trigger
1 parent e896823 commit 6202429

22 files changed

+177
-465
lines changed

goldens/aria/accordion/index.api.md

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import * as _angular_cdk_bidi from '@angular/cdk/bidi';
88
import * as _angular_core from '@angular/core';
99
import { OnDestroy } from '@angular/core';
10+
import { OnInit } from '@angular/core';
1011
import { WritableSignal } from '@angular/core';
1112

1213
// @public
@@ -19,18 +20,16 @@ export class AccordionContent {
1920

2021
// @public
2122
export class AccordionGroup {
22-
constructor();
2323
collapseAll(): void;
2424
readonly disabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
25-
readonly element: HTMLElement;
2625
expandAll(): void;
2726
readonly multiExpandable: _angular_core.InputSignalWithTransform<boolean, unknown>;
2827
readonly _pattern: AccordionGroupPattern;
2928
readonly softDisabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
3029
readonly textDirection: _angular_core.WritableSignal<_angular_cdk_bidi.Direction>;
3130
readonly wrap: _angular_core.InputSignalWithTransform<boolean, unknown>;
3231
// (undocumented)
33-
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", "_panels"], never, true, never>;
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>;
3433
// (undocumented)
3534
static ɵfac: _angular_core.ɵɵFactoryDeclaration<AccordionGroup, never>;
3635
}
@@ -42,31 +41,29 @@ export class AccordionPanel {
4241
collapse(): void;
4342
expand(): void;
4443
readonly id: _angular_core.InputSignal<string>;
45-
readonly panelId: _angular_core.InputSignal<string>;
46-
readonly _pattern: AccordionPanelPattern;
4744
toggle(): void;
4845
readonly visible: _angular_core.Signal<boolean>;
4946
// (undocumented)
50-
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<AccordionPanel, "[ngAccordionPanel]", ["ngAccordionPanel"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; "panelId": { "alias": "panelId"; "required": true; "isSignal": true; }; }, {}, never, never, true, [{ directive: typeof DeferredContentAware; inputs: { "preserveContent": "preserveContent"; }; outputs: {}; }]>;
47+
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<AccordionPanel, "[ngAccordionPanel]", ["ngAccordionPanel"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; }, {}, never, never, true, [{ directive: typeof DeferredContentAware; inputs: { "preserveContent": "preserveContent"; }; outputs: {}; }]>;
5148
// (undocumented)
5249
static ɵfac: _angular_core.ɵɵFactoryDeclaration<AccordionPanel, never>;
5350
}
5451

5552
// @public
56-
export class AccordionTrigger {
57-
readonly _accordionPanelPattern: WritableSignal<AccordionPanelPattern | undefined>;
53+
export class AccordionTrigger implements OnInit {
5854
readonly active: _angular_core.Signal<boolean>;
5955
collapse(): void;
6056
readonly disabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
61-
readonly element: HTMLElement;
6257
expand(): void;
6358
readonly expanded: _angular_core.ModelSignal<boolean>;
6459
readonly id: _angular_core.InputSignal<string>;
65-
readonly panelId: _angular_core.InputSignal<string>;
66-
readonly _pattern: AccordionTriggerPattern;
60+
// (undocumented)
61+
ngOnInit(): void;
62+
readonly panel: _angular_core.InputSignal<AccordionPanel>;
63+
_pattern: AccordionTriggerPattern;
6764
toggle(): void;
6865
// (undocumented)
69-
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<AccordionTrigger, "[ngAccordionTrigger]", ["ngAccordionTrigger"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; "panelId": { "alias": "panelId"; "required": true; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "expanded": { "alias": "expanded"; "required": false; "isSignal": true; }; }, { "expanded": "expandedChange"; }, never, never, true, never>;
66+
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>;
7067
// (undocumented)
7168
static ɵfac: _angular_core.ɵɵFactoryDeclaration<AccordionTrigger, never>;
7269
}

goldens/aria/private/index.api.md

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,43 +31,24 @@ export class AccordionGroupPattern {
3131
toggle(): void;
3232
}
3333

34-
// @public
35-
export interface AccordionPanelInputs {
36-
accordionTrigger: SignalLike<AccordionTriggerPattern | undefined>;
37-
id: SignalLike<string>;
38-
panelId: SignalLike<string>;
39-
}
40-
41-
// @public
42-
export class AccordionPanelPattern {
43-
constructor(inputs: AccordionPanelInputs);
44-
accordionTrigger: SignalLike<AccordionTriggerPattern | undefined>;
45-
hidden: SignalLike<boolean>;
46-
id: SignalLike<string>;
47-
// (undocumented)
48-
readonly inputs: AccordionPanelInputs;
49-
}
50-
5134
// @public
5235
export interface AccordionTriggerInputs extends Omit<ListNavigationItem & ListFocusItem, 'index'>, Omit<ExpansionItem, 'expandable'> {
5336
accordionGroup: SignalLike<AccordionGroupPattern>;
54-
accordionPanel: SignalLike<AccordionPanelPattern | undefined>;
55-
panelId: SignalLike<string>;
37+
accordionPanelId: SignalLike<string>;
5638
}
5739

5840
// @public
5941
export class AccordionTriggerPattern implements ListNavigationItem, ListFocusItem, ExpansionItem {
6042
constructor(inputs: AccordionTriggerInputs);
6143
readonly active: SignalLike<boolean>;
6244
close(): void;
63-
readonly controls: SignalLike<string | undefined>;
45+
readonly controls: SignalLike<string>;
6446
readonly disabled: SignalLike<boolean>;
6547
readonly element: SignalLike<HTMLElement>;
6648
readonly expandable: SignalLike<boolean>;
6749
readonly expanded: WritableSignalLike<boolean>;
6850
readonly hardDisabled: SignalLike<boolean>;
6951
readonly id: SignalLike<string>;
70-
readonly index: SignalLike<number>;
7152
// (undocumented)
7253
readonly inputs: AccordionTriggerInputs;
7354
open(): void;

src/aria/accordion/accordion-group.ts

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,13 @@ import {
1212
ElementRef,
1313
inject,
1414
contentChildren,
15-
afterRenderEffect,
1615
signal,
1716
booleanAttribute,
1817
computed,
1918
} from '@angular/core';
2019
import {Directionality} from '@angular/cdk/bidi';
2120
import {AccordionGroupPattern, AccordionTriggerPattern} from '../private';
2221
import {AccordionTrigger} from './accordion-trigger';
23-
import {AccordionPanel} from './accordion-panel';
2422
import {ACCORDION_GROUP} from './accordion-tokens';
2523

2624
/**
@@ -32,12 +30,12 @@ import {ACCORDION_GROUP} from './accordion-tokens';
3230
* It supports both single and multiple expansion modes.
3331
*
3432
* ```html
35-
* <div ngAccordionGroup [multiExpandable]="true" [(expandedPanels)]="expandedItems">
33+
* <div ngAccordionGroup [multiExpandable]="true">
3634
* <div class="accordion-item">
3735
* <h3>
3836
* <button ngAccordionTrigger panelId="item-1">Item 1</button>
3937
* </h3>
40-
* <div ngAccordionPanel panelId="item-1">
38+
* <div ngAccordionPanel #panel1 panelId="item-1">
4139
* <ng-template ngAccordionContent>
4240
* <p>Content for Item 1.</p>
4341
* </ng-template>
@@ -47,7 +45,7 @@ import {ACCORDION_GROUP} from './accordion-tokens';
4745
* <h3>
4846
* <button ngAccordionTrigger panelId="item-2">Item 2</button>
4947
* </h3>
50-
* <div ngAccordionPanel panelId="item-2">
48+
* <div ngAccordionPanel #panel2 panelId="item-2">
5149
* <ng-template ngAccordionContent>
5250
* <p>Content for Item 2.</p>
5351
* </ng-template>
@@ -74,16 +72,17 @@ export class AccordionGroup {
7472
private readonly _elementRef = inject(ElementRef);
7573

7674
/** A reference to the group element. */
77-
readonly element = this._elementRef.nativeElement as HTMLElement;
75+
private readonly element = this._elementRef.nativeElement as HTMLElement;
7876

7977
/** The AccordionTriggers nested inside this group. */
8078
private readonly _triggers = contentChildren(AccordionTrigger, {descendants: true});
8179

8280
/** The AccordionTrigger patterns nested inside this group. */
83-
private readonly _triggerPatterns = computed(() => this._triggers().map(t => t._pattern));
84-
85-
/** The AccordionPanels nested inside this group. */
86-
private readonly _panels = contentChildren(AccordionPanel, {descendants: true});
81+
private readonly _triggerPatterns = computed(() =>
82+
this._triggers()
83+
.map(t => t._pattern)
84+
.filter(p => !!p),
85+
);
8786

8887
/** The text direction (ltr or rtl). */
8988
readonly textDirection = inject(Directionality).valueSignal;
@@ -107,29 +106,13 @@ export class AccordionGroup {
107106
readonly _pattern: AccordionGroupPattern = new AccordionGroupPattern({
108107
...this,
109108
activeItem: signal(undefined),
110-
items: this._triggerPatterns,
109+
items: this._triggerPatterns, // TODO(adolgachev): Move to single init of all patterns.
111110
// TODO(ok7sai): Investigate whether an accordion should support horizontal mode.
112111
orientation: () => 'vertical',
113112
getItem: e => this._getItem(e),
114113
element: () => this.element,
115114
});
116115

117-
constructor() {
118-
// Effect to link triggers with their corresponding panels and update the group's items.
119-
afterRenderEffect(() => {
120-
const triggers = this._triggers();
121-
const panels = this._panels();
122-
123-
for (const trigger of triggers) {
124-
const panel = panels.find(p => p.panelId() === trigger.panelId());
125-
trigger._accordionPanelPattern.set(panel?._pattern);
126-
if (panel) {
127-
panel._accordionTriggerPattern.set(trigger._pattern);
128-
}
129-
}
130-
});
131-
}
132-
133116
/** Expands all accordion panels if multi-expandable. */
134117
expandAll() {
135118
this._pattern.expansionBehavior.openAll();

src/aria/accordion/accordion-panel.ts

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
WritableSignal,
1717
} from '@angular/core';
1818
import {_IdGenerator} from '@angular/cdk/a11y';
19-
import {DeferredContentAware, AccordionPanelPattern, AccordionTriggerPattern} from '../private';
19+
import {DeferredContentAware, AccordionTriggerPattern} from '../private';
2020

2121
/**
2222
* The content panel of an accordion item that is conditionally visible.
@@ -50,8 +50,8 @@ import {DeferredContentAware, AccordionPanelPattern, AccordionTriggerPattern} fr
5050
],
5151
host: {
5252
'role': 'region',
53-
'[attr.id]': '_pattern.id()',
54-
'[attr.aria-labelledby]': '_pattern.accordionTrigger()?.id()',
53+
'[attr.id]': 'id()',
54+
'[attr.aria-labelledby]': '_accordionTriggerPattern()?.id()',
5555
'[attr.inert]': '!visible() ? true : null',
5656
},
5757
})
@@ -62,23 +62,13 @@ export class AccordionPanel {
6262
/** A global unique identifier for the panel. */
6363
readonly id = input(inject(_IdGenerator).getId('ng-accordion-panel-', true));
6464

65-
/** A local unique identifier for the panel, used to match with its trigger's `panelId`. */
66-
readonly panelId = input.required<string>();
67-
6865
/** Whether the accordion panel is visible. True if the associated trigger is expanded. */
69-
readonly visible = computed(() => !this._pattern.hidden());
66+
readonly visible = computed(() => this._accordionTriggerPattern()?.expanded() === true);
7067

7168
/** The parent accordion trigger pattern that controls this panel. This is set by AccordionGroup. */
7269
readonly _accordionTriggerPattern: WritableSignal<AccordionTriggerPattern | undefined> =
7370
signal(undefined);
7471

75-
/** The UI pattern instance for this panel. */
76-
readonly _pattern: AccordionPanelPattern = new AccordionPanelPattern({
77-
id: this.id,
78-
panelId: this.panelId,
79-
accordionTrigger: () => this._accordionTriggerPattern(),
80-
});
81-
8272
constructor() {
8373
// Connect the panel's hidden state to the DeferredContentAware's visibility.
8474
afterRenderEffect(() => {

src/aria/accordion/accordion-trigger.ts

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@ import {
1010
Directive,
1111
input,
1212
ElementRef,
13+
OnInit,
1314
inject,
14-
signal,
1515
model,
1616
booleanAttribute,
1717
computed,
18-
WritableSignal,
1918
} from '@angular/core';
2019
import {_IdGenerator} from '@angular/cdk/a11y';
21-
import {AccordionPanelPattern, AccordionTriggerPattern} from '../private';
20+
import {AccordionTriggerPattern} from '../private';
2221
import {ACCORDION_GROUP} from './accordion-tokens';
22+
import {AccordionPanel} from './accordion-panel';
2323

2424
/**
2525
* The trigger that toggles the visibility of its associated `ngAccordionPanel`.
@@ -45,50 +45,51 @@ import {ACCORDION_GROUP} from './accordion-tokens';
4545
host: {
4646
'[attr.data-active]': 'active()',
4747
'role': 'button',
48-
'[id]': '_pattern.id()',
48+
'[id]': 'id()',
4949
'[attr.aria-expanded]': 'expanded()',
5050
'[attr.aria-controls]': '_pattern.controls()',
5151
'[attr.aria-disabled]': '_pattern.disabled()',
5252
'[attr.disabled]': '_pattern.hardDisabled() ? true : null',
5353
'[attr.tabindex]': '_pattern.tabIndex()',
5454
},
5555
})
56-
export class AccordionTrigger {
56+
export class AccordionTrigger implements OnInit {
5757
/** A reference to the trigger element. */
5858
private readonly _elementRef = inject(ElementRef);
5959

6060
/** A reference to the trigger element. */
61-
readonly element = this._elementRef.nativeElement as HTMLElement;
61+
private readonly element = this._elementRef.nativeElement as HTMLElement;
6262

6363
/** The parent AccordionGroup. */
6464
private readonly _accordionGroup = inject(ACCORDION_GROUP);
6565

66+
/** The associated AccordionPanel. */
67+
readonly panel = input.required<AccordionPanel>();
68+
6669
/** A unique identifier for the widget. */
6770
readonly id = input(inject(_IdGenerator).getId('ng-accordion-trigger-', true));
6871

69-
/** A local unique identifier for the trigger, used to match with its panel's `panelId`. */
70-
readonly panelId = input.required<string>();
71-
7272
/** Whether the trigger is disabled. */
7373
readonly disabled = input(false, {transform: booleanAttribute});
7474

7575
/** Whether the corresponding panel is expanded. */
7676
readonly expanded = model<boolean>(false);
7777

7878
/** Whether the trigger is active. */
79-
readonly active = computed(() => this._pattern.active());
80-
81-
/** The accordion panel pattern controlled by this trigger. This is set by AccordionGroup. */
82-
readonly _accordionPanelPattern: WritableSignal<AccordionPanelPattern | undefined> =
83-
signal(undefined);
79+
readonly active = computed(() => this._pattern!.active());
8480

8581
/** The UI pattern instance for this trigger. */
86-
readonly _pattern: AccordionTriggerPattern = new AccordionTriggerPattern({
87-
...this,
88-
accordionGroup: computed(() => this._accordionGroup._pattern),
89-
accordionPanel: this._accordionPanelPattern,
90-
element: () => this.element,
91-
});
82+
_pattern!: AccordionTriggerPattern;
83+
84+
ngOnInit() {
85+
this._pattern = new AccordionTriggerPattern({
86+
...this,
87+
accordionGroup: computed(() => this._accordionGroup._pattern),
88+
accordionPanelId: () => this.panel().id(),
89+
element: () => this.element,
90+
});
91+
this.panel()._accordionTriggerPattern.set(this._pattern);
92+
}
9293

9394
/** Expands this item. */
9495
expand() {

src/aria/accordion/accordion.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,13 +387,13 @@ describe('AccordionGroup', () => {
387387
<div class="item-container">
388388
<button
389389
ngAccordionTrigger
390-
[panelId]="item.panelId"
390+
[panel]="panel"
391391
[disabled]="item.disabled"
392392
[(expanded)]="item.expanded"
393393
>{{ item.header }}</button>
394394
<div
395395
ngAccordionPanel
396-
[panelId]="item.panelId"
396+
#panel="ngAccordionPanel"
397397
>
398398
<ng-template ngAccordionContent>
399399
{{ item.content }}

0 commit comments

Comments
 (0)