Skip to content

Commit bac43b6

Browse files
committed
fix(aria/accordion): resolve triggers applied via host directives on nested clicks
1 parent 008280b commit bac43b6

2 files changed

Lines changed: 146 additions & 5 deletions

File tree

‎src/aria/accordion/accordion.spec.ts‎

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

1212
describe('AccordionGroup', () => {
13-
let fixture: ComponentFixture<AccordionGroupWithLoop>;
13+
let fixture: ComponentFixture<unknown>;
1414
let testComponent: AccordionGroupWithLoop;
1515
let groupElement: HTMLElement;
1616

@@ -68,6 +68,79 @@ describe('AccordionGroup', () => {
6868
});
6969
});
7070

71+
describe('triggers applied with hostDirectives', () => {
72+
beforeEach(() => {
73+
fixture = TestBed.createComponent(AccordionWithHostDirectiveTrigger);
74+
fixture.detectChanges();
75+
});
76+
77+
it('should toggle when clicking a nested child', async () => {
78+
const trigger = fixture.nativeElement.querySelector('#host-trigger');
79+
const child = trigger.querySelector('span');
80+
81+
child.dispatchEvent(new PointerEvent('click', {bubbles: true}));
82+
await fixture.whenStable();
83+
84+
expect(trigger.getAttribute('aria-expanded')).toBe('true');
85+
expect(fixture.nativeElement.querySelector('#host-content')).not.toBeNull();
86+
});
87+
88+
it('should update the active trigger when focusing a nested child', async () => {
89+
const trigger = fixture.nativeElement.querySelector('#host-trigger');
90+
const child = trigger.querySelector('span');
91+
92+
child.dispatchEvent(new FocusEvent('focusin', {bubbles: true}));
93+
await fixture.whenStable();
94+
95+
expect(trigger.getAttribute('tabindex')).toBe('0');
96+
});
97+
98+
it('should toggle when clicking the host element directly', async () => {
99+
const trigger = fixture.nativeElement.querySelector('#host-trigger');
100+
101+
trigger.dispatchEvent(new PointerEvent('click', {bubbles: true}));
102+
await fixture.whenStable();
103+
104+
expect(trigger.getAttribute('aria-expanded')).toBe('true');
105+
});
106+
107+
it('should not toggle when clicking panel content', async () => {
108+
const trigger = fixture.nativeElement.querySelector('#host-trigger');
109+
const content = fixture.nativeElement.querySelector('#host-content');
110+
111+
content.dispatchEvent(new PointerEvent('click', {bubbles: true}));
112+
await fixture.whenStable();
113+
114+
expect(trigger.getAttribute('aria-expanded')).toBe('false');
115+
});
116+
});
117+
118+
describe('nested accordion groups', () => {
119+
it('should only toggle the inner panel', async () => {
120+
fixture = TestBed.createComponent(NestedAccordionGroups);
121+
fixture.detectChanges();
122+
123+
const outerTrigger = fixture.nativeElement.querySelector('#outer-trigger');
124+
const innerTrigger = fixture.nativeElement.querySelector('#inner-trigger');
125+
innerTrigger.querySelector('span').dispatchEvent(new PointerEvent('click', {bubbles: true}));
126+
await fixture.whenStable();
127+
128+
expect(outerTrigger.getAttribute('aria-expanded')).toBe('false');
129+
expect(innerTrigger.getAttribute('aria-expanded')).toBe('true');
130+
});
131+
});
132+
133+
it('should toggle a direct trigger with a nested child', async () => {
134+
fixture = TestBed.createComponent(AccordionWithDirectNestedTrigger);
135+
fixture.detectChanges();
136+
const trigger = fixture.nativeElement.querySelector('#direct-trigger');
137+
138+
trigger.querySelector('span').dispatchEvent(new PointerEvent('click', {bubbles: true}));
139+
await fixture.whenStable();
140+
141+
expect(trigger.getAttribute('aria-expanded')).toBe('true');
142+
});
143+
71144
afterEach(async () => {
72145
await runAccessibilityChecks(fixture.nativeElement);
73146
});
@@ -773,3 +846,71 @@ class AccordionPanelWithoutTrigger {}
773846
changeDetection: ChangeDetectionStrategy.Eager,
774847
})
775848
class AccordionWithMultipleExpandedItems {}
849+
850+
@Component({
851+
selector: '[hostDirectiveTrigger]',
852+
hostDirectives: [
853+
{
854+
directive: AccordionTrigger,
855+
inputs: ['panel'],
856+
},
857+
],
858+
template: '<ng-content />',
859+
imports: [],
860+
})
861+
class HostDirectiveTrigger {}
862+
863+
@Component({
864+
template: `
865+
<div ngAccordionGroup>
866+
<button id="host-trigger" hostDirectiveTrigger [panel]="panel">
867+
Trigger <span tabindex="0">nested</span>
868+
</button>
869+
<div ngAccordionPanel #panel="ngAccordionPanel">
870+
<ng-template ngAccordionContent><span id="host-content">Content</span></ng-template>
871+
</div>
872+
</div>
873+
`,
874+
imports: [AccordionGroup, AccordionPanel, AccordionContent, HostDirectiveTrigger],
875+
changeDetection: ChangeDetectionStrategy.Eager,
876+
})
877+
class AccordionWithHostDirectiveTrigger {}
878+
879+
@Component({
880+
template: `
881+
<div ngAccordionGroup>
882+
<button id="direct-trigger" ngAccordionTrigger [panel]="panel">
883+
Trigger <span>nested</span>
884+
</button>
885+
<div ngAccordionPanel #panel="ngAccordionPanel">
886+
<ng-template ngAccordionContent>Content</ng-template>
887+
</div>
888+
</div>
889+
`,
890+
imports: [AccordionGroup, AccordionTrigger, AccordionPanel, AccordionContent],
891+
changeDetection: ChangeDetectionStrategy.Eager,
892+
})
893+
class AccordionWithDirectNestedTrigger {}
894+
895+
@Component({
896+
template: `
897+
<div ngAccordionGroup>
898+
<button id="outer-trigger" ngAccordionTrigger [panel]="outerPanel" [expanded]="true">Outer</button>
899+
<div ngAccordionPanel #outerPanel="ngAccordionPanel">
900+
<ng-template ngAccordionContent>
901+
<div ngAccordionGroup>
902+
<button id="inner-trigger" ngAccordionTrigger [panel]="innerPanel">
903+
Inner <span>nested</span>
904+
</button>
905+
<div ngAccordionPanel #innerPanel="ngAccordionPanel">
906+
<ng-template ngAccordionContent>Inner content</ng-template>
907+
</div>
908+
</div>
909+
</ng-template>
910+
</div>
911+
</div>
912+
`,
913+
imports: [AccordionGroup, AccordionTrigger, AccordionPanel, AccordionContent],
914+
changeDetection: ChangeDetectionStrategy.Eager,
915+
})
916+
class NestedAccordionGroups {}

‎src/aria/private/accordion/accordion.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,15 +147,15 @@ export class AccordionGroupPattern {
147147
private _findTriggerPattern(
148148
element: Element | null | undefined,
149149
): AccordionTriggerPattern | undefined {
150+
const items = this.inputs.items();
150151
let target = element;
151-
152-
while (target) {
153-
const pattern = this.inputs.items().find(t => t.element() === target);
152+
while (target && target !== this.inputs.element()) {
153+
const pattern = items.find(t => t.element() === target);
154154
if (pattern) {
155155
return pattern;
156156
}
157157

158-
target = target.parentElement?.closest('[ngAccordionTrigger]');
158+
target = target.parentElement;
159159
}
160160

161161
return undefined;

0 commit comments

Comments
 (0)