From 7d126f73ff741a46950761ede0f53f774921dcb5 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Tue, 25 Nov 2025 15:53:08 -0500 Subject: [PATCH 1/2] fix(aria/menu): update unit tests to use ngMenuContent --- src/aria/menu/menu.spec.ts | 117 +++++++++++++++++++++---------------- 1 file changed, 66 insertions(+), 51 deletions(-) diff --git a/src/aria/menu/menu.spec.ts b/src/aria/menu/menu.spec.ts index 6e03a5da1e81..8980ea52cc87 100644 --- a/src/aria/menu/menu.spec.ts +++ b/src/aria/menu/menu.spec.ts @@ -1,7 +1,7 @@ import {Component, DebugElement} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; -import {Menu, MenuBar, MenuItem, MenuTrigger} from './menu'; +import {Menu, MenuBar, MenuContent, MenuItem, MenuTrigger} from './menu'; import {provideFakeDirectionality} from '@angular/cdk/testing/private'; describe('Standalone Menu Pattern', () => { @@ -227,25 +227,24 @@ describe('Standalone Menu Pattern', () => { const apple = getItem('Apple'); const banana = getItem('Banana'); const berries = getItem('Berries'); - const blueberry = getItem('Blueberry'); keydown(apple!, 'ArrowDown'); keydown(banana!, 'ArrowDown'); keydown(berries!, 'ArrowRight'); expect(isSubmenuExpanded()).toBe(true); - expect(document.activeElement).toBe(blueberry); + expect(document.activeElement).toBe(getItem('Blueberry')); }); it('should close submenu on arrow left', () => { const apple = getItem('Apple'); const banana = getItem('Banana'); const berries = getItem('Berries'); - const blueberry = getItem('Blueberry'); keydown(apple!, 'ArrowDown'); keydown(banana!, 'ArrowDown'); keydown(berries!, 'ArrowRight'); + const blueberry = getItem('Blueberry'); keydown(blueberry!, 'ArrowLeft'); expect(isSubmenuExpanded()).toBe(false); @@ -254,12 +253,11 @@ describe('Standalone Menu Pattern', () => { it('should close submenu on click outside', () => { const berries = getItem('Berries'); - const blueberry = getItem('Blueberry'); click(berries!); expect(isSubmenuExpanded()).toBe(true); - focusout(blueberry!, document.body); + focusout(getItem('Blueberry')!, document.body); expect(isSubmenuExpanded()).toBe(false); }); @@ -267,41 +265,39 @@ describe('Standalone Menu Pattern', () => { const apple = getItem('Apple'); const banana = getItem('Banana'); const berries = getItem('Berries'); - const blueberry = getItem('Blueberry'); keydown(apple!, 'ArrowDown'); keydown(banana!, 'ArrowDown'); keydown(berries!, 'Enter'); expect(isSubmenuExpanded()).toBe(true); - expect(document.activeElement).toBe(blueberry); + expect(document.activeElement).toBe(getItem('Blueberry')); }); it('should expand submenu on space', () => { const apple = getItem('Apple'); const banana = getItem('Banana'); const berries = getItem('Berries'); - const blueberry = getItem('Blueberry'); keydown(apple!, 'ArrowDown'); keydown(banana!, 'ArrowDown'); keydown(berries!, ' '); expect(isSubmenuExpanded()).toBe(true); - expect(document.activeElement).toBe(blueberry); + expect(document.activeElement).toBe(getItem('Blueberry')); }); it('should close submenu on escape', () => { const apple = getItem('Apple'); const banana = getItem('Banana'); const berries = getItem('Berries'); - const blueberry = getItem('Blueberry'); keydown(apple!, 'ArrowDown'); keydown(banana!, 'ArrowDown'); keydown(berries!, 'ArrowRight'); expect(isSubmenuExpanded()).toBe(true); + const blueberry = getItem('Blueberry'); expect(document.activeElement).toBe(blueberry); keydown(blueberry!, 'Escape'); @@ -332,11 +328,11 @@ describe('Standalone Menu Pattern', () => { const apple = getItem('Apple'); const banana = getItem('Banana'); const berries = getItem('Berries'); - const blueberry = getItem('Blueberry'); keydown(apple!, 'ArrowDown'); keydown(banana!, 'ArrowDown'); keydown(berries!, 'Enter'); // open submenu + const blueberry = getItem('Blueberry'); expect(document.activeElement).toBe(blueberry); @@ -351,11 +347,11 @@ describe('Standalone Menu Pattern', () => { const apple = getItem('Apple'); const banana = getItem('Banana'); const berries = getItem('Berries'); - const blueberry = getItem('Blueberry'); keydown(apple!, 'ArrowDown'); keydown(banana!, 'ArrowDown'); keydown(berries!, ' '); // open submenu + const blueberry = getItem('Blueberry'); expect(document.activeElement).toBe(blueberry); @@ -369,12 +365,12 @@ describe('Standalone Menu Pattern', () => { const apple = getItem('Apple'); const banana = getItem('Banana'); const berries = getItem('Berries'); - const blueberry = getItem('Blueberry'); keydown(apple!, 'ArrowDown'); keydown(banana!, 'ArrowDown'); keydown(berries!, 'ArrowRight'); expect(isSubmenuExpanded()).toBe(true); + const blueberry = getItem('Blueberry'); expect(document.activeElement).toBe(blueberry); const externalElement = document.createElement('button'); @@ -424,25 +420,24 @@ describe('Standalone Menu Pattern', () => { const apple = getItem('Apple'); const banana = getItem('Banana'); const berries = getItem('Berries'); - const blueberry = getItem('Blueberry'); keydown(apple!, 'ArrowDown'); keydown(banana!, 'ArrowDown'); keydown(berries!, 'ArrowLeft'); expect(isSubmenuExpanded()).toBe(true); - expect(document.activeElement).toBe(blueberry); + expect(document.activeElement).toBe(getItem('Blueberry')); }); it('should close submenu on arrow right', () => { const apple = getItem('Apple'); const banana = getItem('Banana'); const berries = getItem('Berries'); - const blueberry = getItem('Blueberry'); keydown(apple!, 'ArrowDown'); keydown(banana!, 'ArrowDown'); keydown(berries!, 'ArrowLeft'); + const blueberry = getItem('Blueberry'); keydown(blueberry!, 'ArrowRight'); expect(isSubmenuExpanded()).toBe(false); @@ -454,7 +449,13 @@ describe('Standalone Menu Pattern', () => { describe('Menu Trigger Pattern', () => { let fixture: ComponentFixture; + const focusin = (element: Element) => { + element.dispatchEvent(new FocusEvent('focusin', {bubbles: true})); + fixture.detectChanges(); + }; + const keydown = (element: Element, key: string, modifierKeys: {} = {}) => { + focusin(element); element.dispatchEvent( new KeyboardEvent('keydown', { key, @@ -466,6 +467,7 @@ describe('Menu Trigger Pattern', () => { }; const click = (element: Element, eventInit?: PointerEventInit) => { + focusin(element); element.dispatchEvent(new PointerEvent('click', {bubbles: true, ...eventInit})); fixture.detectChanges(); }; @@ -479,7 +481,6 @@ describe('Menu Trigger Pattern', () => { TestBed.configureTestingModule({}); fixture = TestBed.createComponent(MenuTriggerExample); fixture.detectChanges(); - getItem('Apple')?.focus(); } function getTrigger(): HTMLElement { @@ -598,7 +599,7 @@ describe('Menu Trigger Pattern', () => { expect(isExpanded()).toBe(false); }); - it('should close on selecting an item on space', () => { + it('should close on selecting an item on space', async () => { click(getTrigger()); keydown(getItem('Apple')!, ' '); expect(isExpanded()).toBe(false); @@ -946,20 +947,24 @@ describe('Menu Bar Pattern', () => { @Component({ template: `
-
Apple
-
Banana
-
Berries
- -
-
Blueberry
-
Blackberry
-
Strawberry
-
- -
Cherry
+ +
Apple
+
Banana
+
Berries
+ +
+ +
Blueberry
+
Blackberry
+
Strawberry
+
+
+ +
Cherry
+
`, - imports: [Menu, MenuItem], + imports: [Menu, MenuItem, MenuContent], }) class StandaloneMenuExample { onSelect(value: string) {} @@ -970,20 +975,24 @@ class StandaloneMenuExample {
-
Apple
-
Banana
-
Berries
- -
-
Blueberry
-
Blackberry
-
Strawberry
-
- -
Cherry
+ +
Apple
+
Banana
+
Berries
+ +
+ +
Blueberry
+
Blackberry
+
Strawberry
+
+
+ +
Cherry
+
`, - imports: [Menu, MenuItem, MenuTrigger], + imports: [Menu, MenuItem, MenuTrigger, MenuContent], }) class MenuTriggerExample {} @@ -994,26 +1003,32 @@ class MenuTriggerExample {}
Edit
-
Undo
-
Redo
+ +
Undo
+
Redo
+
View
-
Zoom In
-
Zoom Out
-
Full Screen
+ +
Zoom In
+
Zoom Out
+
Full Screen
+
Help
-
Documentation
-
About
+ +
Documentation
+
About
+
`, - imports: [Menu, MenuBar, MenuItem], + imports: [Menu, MenuBar, MenuItem, MenuContent], }) class MenuBarExample {} From 8949a16acb42b3e2b45ad92492c7c8214d56a2c8 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Tue, 25 Nov 2025 16:53:22 -0500 Subject: [PATCH 2/2] fix(aria/menu): focus flicker bug * Fixes an issue in standalone menus and context menus where focus would flicker when opening a menu. --- src/aria/menu/menu.spec.ts | 10 ++++++++++ src/aria/menu/menu.ts | 6 +++++- src/aria/private/menu/menu.ts | 4 ++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/aria/menu/menu.spec.ts b/src/aria/menu/menu.spec.ts index 8980ea52cc87..5d98f4f08c9e 100644 --- a/src/aria/menu/menu.spec.ts +++ b/src/aria/menu/menu.spec.ts @@ -444,6 +444,16 @@ describe('Standalone Menu Pattern', () => { expect(document.activeElement).toBe(berries); }); }); + + it('should not reset default state on hover triggers expansion', async () => { + TestBed.configureTestingModule({}); + fixture = TestBed.createComponent(StandaloneMenuExample); + fixture.detectChanges(); + + const berries = getItem('Berries'); + await mouseover(berries!); + expect(berries?.getAttribute('data-active')).toBe('true'); + }); }); describe('Menu Trigger Pattern', () => { diff --git a/src/aria/menu/menu.ts b/src/aria/menu/menu.ts index 4f1dfcb491a9..be29eb37b438 100644 --- a/src/aria/menu/menu.ts +++ b/src/aria/menu/menu.ts @@ -261,7 +261,11 @@ export class Menu { }); afterRenderEffect(() => { - if (!this._pattern.hasBeenFocused() && this._items().length) { + if ( + !this._pattern.hasBeenFocused() && + !this._pattern.hasBeenHovered() && + this._items().length + ) { untracked(() => this._pattern.setDefaultState()); } }); diff --git a/src/aria/private/menu/menu.ts b/src/aria/private/menu/menu.ts index 9be9bec6fdb8..1ef208633e1d 100644 --- a/src/aria/private/menu/menu.ts +++ b/src/aria/private/menu/menu.ts @@ -91,6 +91,9 @@ export class MenuPattern { /** Whether the menu has received focus. */ hasBeenFocused = signal(false); + /** Whether the menu trigger has been hovered. */ + hasBeenHovered = signal(false); + /** Timeout used to open sub-menus on hover. */ _openTimeout: any; @@ -195,6 +198,7 @@ export class MenuPattern { return; } + this.hasBeenHovered.set(true); const item = this.inputs.items().find(i => i.element()?.contains(event.target as Node)); if (!item) {