From b91d08a0d82664a466e7765389b611ff007794a6 Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Sat, 24 Oct 2026 13:28:38 +0800 Subject: [PATCH 1/3] fix(aria/menu): follow external focus in menus A focusin event on a menu now updates the active item from the focused descendant without moving focus. This keeps Enter and Space from acting on a stale item when focus is managed externally. Fixes #33934 --- src/aria/menu/menu.spec.ts | 11 +++++++++++ src/aria/menu/menu.ts | 2 +- src/aria/private/menu/menu.ts | 11 ++++++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/aria/menu/menu.spec.ts b/src/aria/menu/menu.spec.ts index e6d422b29a5c..316feba0141b 100644 --- a/src/aria/menu/menu.spec.ts +++ b/src/aria/menu/menu.spec.ts @@ -212,6 +212,17 @@ describe('Standalone Menu Pattern', () => { expect(fixture.componentInstance.itemSelected).toHaveBeenCalledWith('Banana'); }); + it('should select the focused item on enter when focus was moved externally', async () => { + const cherry = getItem('Cherry')!; + spyOn(fixture.componentInstance, 'itemSelected'); + + cherry.focus(); + await fixture.whenStable(); + await keydown(cherry, 'Enter'); + + expect(fixture.componentInstance.itemSelected).toHaveBeenCalledWith('Cherry'); + }); + it('should select an item on space', async () => { const banana = getItem('Banana'); spyOn(fixture.componentInstance, 'itemSelected'); diff --git a/src/aria/menu/menu.ts b/src/aria/menu/menu.ts index cb7fafaaaf23..0cfd9682258c 100644 --- a/src/aria/menu/menu.ts +++ b/src/aria/menu/menu.ts @@ -67,7 +67,7 @@ import {MENU_COMPONENT} from './menu-tokens'; '(mouseover)': '_pattern.onMouseOver($event)', '(mouseout)': '_pattern.onMouseOut($event)', '(focusout)': '_pattern.onFocusOut($event)', - '(focusin)': '_pattern.onFocusIn()', + '(focusin)': '_pattern.onFocusIn($event)', '(click)': '_pattern.onClick($event)', }, hostDirectives: [ diff --git a/src/aria/private/menu/menu.ts b/src/aria/private/menu/menu.ts index c7ff24ec72d6..e25169dd1f39 100644 --- a/src/aria/private/menu/menu.ts +++ b/src/aria/private/menu/menu.ts @@ -333,9 +333,18 @@ export class MenuPattern { } /** Handles focusin events for the menu. */ - onFocusIn() { + onFocusIn(event?: FocusEvent) { this.isFocused.set(true); this.hasBeenInteracted.set(true); + + const target = event && _getEventTarget(event); + const focusedItem = target && this.inputs.items().find(item => + item.element()?.contains(target), + ); + + if (focusedItem) { + this.listBehavior.goto(focusedItem, {focusElement: false}); + } } /** Handles the focusout event for the menu. */ From 80461524873b95e8cea6ccc8a2a4fc9e61513402 Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Sat, 24 Oct 2026 13:28:39 +0800 Subject: [PATCH 2/3] fix(aria/tabs): follow external focus in tab lists A focusin event on a tab list now updates its active tab from the focused descendant without selecting it or moving focus. Subsequent keyboard navigation therefore starts from the focused tab. Fixes #33934 --- src/aria/private/tabs/tabs.ts | 11 ++++++++--- src/aria/tabs/tab-list.ts | 2 +- src/aria/tabs/tabs.spec.ts | 15 +++++++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/aria/private/tabs/tabs.ts b/src/aria/private/tabs/tabs.ts index 85a82d8496a1..458aafb9ffb9 100644 --- a/src/aria/private/tabs/tabs.ts +++ b/src/aria/private/tabs/tabs.ts @@ -268,8 +268,13 @@ export class TabListPattern { } /** Handles focusin events for the tablist. */ - onFocusIn() { + onFocusIn(event?: FocusEvent) { this.hasBeenInteracted.set(true); + + const item = event && this._getItem(event); + if (item) { + this.navigationBehavior.goto(item, {focusElement: false}); + } } /** Opens the given tab or the current active tab. */ @@ -295,8 +300,8 @@ export class TabListPattern { } } - /** Returns the tab item associated with the given pointer event. */ - private _getItem(e: PointerEvent) { + /** Returns the tab item associated with the given event. */ + private _getItem(e: Event) { const target = _getEventTarget(e); if (!target) { return; diff --git a/src/aria/tabs/tab-list.ts b/src/aria/tabs/tab-list.ts index 6cccfb77ab72..949148eab81b 100644 --- a/src/aria/tabs/tab-list.ts +++ b/src/aria/tabs/tab-list.ts @@ -54,7 +54,7 @@ import type {Tab} from './tab'; '[attr.aria-activedescendant]': '_pattern.activeDescendant()', '(keydown)': '_pattern.onKeydown($event)', '(click)': '_pattern.onClick($event)', - '(focusin)': '_pattern.onFocusIn()', + '(focusin)': '_pattern.onFocusIn($event)', }, providers: [{provide: TAB_LIST, useExisting: TabList}], }) diff --git a/src/aria/tabs/tabs.spec.ts b/src/aria/tabs/tabs.spec.ts index 065677e040d1..be3bfdce94a6 100644 --- a/src/aria/tabs/tabs.spec.ts +++ b/src/aria/tabs/tabs.spec.ts @@ -616,6 +616,21 @@ describe('Tabs', () => { expect(tabElements[1].getAttribute('aria-selected')).toBe('true'); }); + it('should act on the tab that receives external focus', async () => { + await updateTabs({selectedTab: 'tab1'}); + + tabElements[2].focus(); + await fixture.whenStable(); + expect(isTabFocused(2)).toBe(true); + + await enter(); + expect(testComponent.selectedTab()).toBe('tab3'); + + await left(); + expect(isTabFocused(1)).toBe(true); + expect(testComponent.selectedTab()).toBe('tab3'); + }); + it('should select focused tab on Enter', async () => { await updateTabs({selectedTab: 'tab1'}); expect(testComponent.selectedTab()).toBe('tab1'); From f1dd93bae6d21a97067db32f25cea1bad511e2ad Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Sat, 24 Oct 2026 13:34:38 +0800 Subject: [PATCH 3/3] fix(aria/menu): format external focus lookup --- src/aria/private/menu/menu.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/aria/private/menu/menu.ts b/src/aria/private/menu/menu.ts index e25169dd1f39..8c0443542458 100644 --- a/src/aria/private/menu/menu.ts +++ b/src/aria/private/menu/menu.ts @@ -338,9 +338,8 @@ export class MenuPattern { this.hasBeenInteracted.set(true); const target = event && _getEventTarget(event); - const focusedItem = target && this.inputs.items().find(item => - item.element()?.contains(target), - ); + const focusedItem = + target && this.inputs.items().find(item => item.element()?.contains(target)); if (focusedItem) { this.listBehavior.goto(focusedItem, {focusElement: false});