From 8e32989e9913289e3b92ae55204398dbda468a68 Mon Sep 17 00:00:00 2001 From: Utkarsh Agarwal Date: Sun, 25 Oct 2026 22:15:32 +0530 Subject: [PATCH] fix(aria/accordion): keyboard events inside panels toggling the accordion The accordion group handled keydown events from anywhere inside it including interactive content in panels. This prevented the default behavior of keys like Enter on a link or Space on a button and toggled the panel instead. These changes only handle events coming from a trigger or the group itself. Fixes #34011. --- src/aria/accordion/accordion.spec.ts | 75 +++++++++++++++++++++++++ src/aria/private/accordion/accordion.ts | 3 + 2 files changed, 78 insertions(+) diff --git a/src/aria/accordion/accordion.spec.ts b/src/aria/accordion/accordion.spec.ts index 12d0515b8ad9..bfd02dc690e4 100644 --- a/src/aria/accordion/accordion.spec.ts +++ b/src/aria/accordion/accordion.spec.ts @@ -487,6 +487,57 @@ describe('AccordionGroup', () => { }); }); + describe('with interactive content inside a panel', () => { + let link: HTMLElement; + let button: HTMLElement; + let input: HTMLElement; + + const dispatchKeydown = async (key: string, target: HTMLElement) => { + const event = new KeyboardEvent('keydown', {bubbles: true, cancelable: true, key}); + target.dispatchEvent(event); + await fixture.whenStable(); + return event; + }; + + beforeEach(async () => { + fixture = TestBed.createComponent(AccordionWithInteractiveContent); + testComponent = fixture.componentInstance; + groupElement = fixture.nativeElement.querySelector('[ngAccordionGroup]') as HTMLElement; + testComponent.expandItem('item-1'); + await setupTriggerAndPanels(); + + link = panelElements[0].querySelector('a')!; + button = panelElements[0].querySelector('button')!; + input = panelElements[0].querySelector('input')!; + triggerElements[1].focus(); + await fixture.whenStable(); + expect(isTriggerExpanded(0)).toBeTrue(); + }); + + it('should not toggle the panel or prevent default on Enter from a link', async () => { + const event = await dispatchKeydown('Enter', link); + + expect(event.defaultPrevented).toBeFalse(); + expect(isTriggerExpanded(0)).toBeTrue(); + }); + + it('should not toggle the panel or prevent default on Space from a button', async () => { + const event = await dispatchKeydown(' ', button); + + expect(event.defaultPrevented).toBeFalse(); + expect(isTriggerExpanded(0)).toBeTrue(); + }); + + it('should not move focus or prevent default on Home and End from an input', async () => { + const homeEvent = await dispatchKeydown('Home', input); + const endEvent = await dispatchKeydown('End', input); + + expect(homeEvent.defaultPrevented).toBeFalse(); + expect(endEvent.defaultPrevented).toBeFalse(); + expect(isTriggerActive(1)).toBeTrue(); + }); + }); + describe('structural validations', () => { let consoleSpy: jasmine.Spy; @@ -696,6 +747,30 @@ class AccordionGroupWithIfs extends AccordionGroupWithLoop { includeThird = signal(true); } +@Component({ + template: ` +
+ @for (item of items(); track item.panelId) { +
+ +
+ + Link inside panel + + + +
+
+ } +
+ `, + imports: [AccordionGroup, AccordionTrigger, AccordionPanel, AccordionContent], + changeDetection: ChangeDetectionStrategy.Eager, +}) +class AccordionWithInteractiveContent extends AccordionGroupWithLoop {} + @Component({ template: `
diff --git a/src/aria/private/accordion/accordion.ts b/src/aria/private/accordion/accordion.ts index 9cd9fc8935e4..72ebed476e50 100644 --- a/src/aria/private/accordion/accordion.ts +++ b/src/aria/private/accordion/accordion.ts @@ -93,6 +93,9 @@ export class AccordionGroupPattern { /** Handles keydown events on the trigger, delegating to the group if not disabled. */ onKeydown(event: KeyboardEvent): void { + const target = _getEventTarget(event); + if (target !== this.inputs.element() && !this._findTriggerPattern(target)) return; + this.keydown().handle(event); }