From 49da6bfae7653f875148c83495e3fd79ec28295a Mon Sep 17 00:00:00 2001 From: Himanshu Pathak Date: Thu, 23 Jul 2026 14:00:26 +0530 Subject: [PATCH] Navigation: Fixes `aria-expanded` not updating on hover submenu inside overlay (#77563) * Navigation: Fix aria-expanded not syncing with hover-revealed submenu in overlay context * Navigation: Sync aria-expanded with overlay's always-expanded submenu state * Add unit tests for Navigation block view script Co-authored-by: himanshupathak95 Co-authored-by: getdave Co-authored-by: jeryj Co-authored-by: meravi Co-authored-by: s56bouya --- .../block-library/src/navigation/index.php | 2 +- .../block-library/src/navigation/test/view.js | 55 +++++++++++++++++++ packages/block-library/src/navigation/view.js | 29 +++++----- 3 files changed, 71 insertions(+), 15 deletions(-) create mode 100644 packages/block-library/src/navigation/test/view.js diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 802909a39e648b..fcb8f5f97af8e1 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -1250,7 +1250,7 @@ function block_core_navigation_add_directives_to_submenu( $tags, $block_attribut ) ) ) { $tags->set_attribute( 'data-wp-on--click', 'actions.toggleMenuOnClick' ); - $tags->set_attribute( 'data-wp-bind--aria-expanded', 'state.isMenuOpen' ); + $tags->set_attribute( 'data-wp-bind--aria-expanded', 'state.isSubmenuOpen' ); // The `aria-expanded` attribute for SSR is already added in the submenu block. } // Add directives to the submenu. diff --git a/packages/block-library/src/navigation/test/view.js b/packages/block-library/src/navigation/test/view.js new file mode 100644 index 00000000000000..a7fc4dba25b43b --- /dev/null +++ b/packages/block-library/src/navigation/test/view.js @@ -0,0 +1,55 @@ +/** + * @jest-environment jsdom + */ + +let mockRegisteredStore; +let mockCurrentContext; + +jest.mock( '@wordpress/interactivity', () => ( { + store: ( name, config ) => { + mockRegisteredStore = config; + return config; + }, + getContext: () => mockCurrentContext, + getElement: () => ( { ref: {} } ), + withSyncEvent: ( fn ) => fn, +} ) ); + +describe( 'Navigation view script', () => { + beforeEach( async () => { + jest.resetModules(); + mockRegisteredStore = null; + mockCurrentContext = null; + // Import the view.js script to register the store + await import( '../view.js' ); + } ); + + it( 'updates submenuOpenedBy.hover when hovering a submenu inside an open overlay', () => { + // Mock the context for a submenu inside an open overlay + mockCurrentContext = { + type: 'submenu', + submenuOpenedBy: { click: false, hover: false, focus: false }, + overlayOpenedBy: { click: true, hover: false, focus: false }, + }; + + const { state, actions } = mockRegisteredStore; + + // Verify initial state + expect( state.isSubmenuOpen ).toBe( true ); // because overlay is open + expect( mockCurrentContext.submenuOpenedBy.hover ).toBe( false ); + + // Simulate hover enter + actions.openMenuOnHover( { pointerType: 'mouse' } ); + + // Verify that hover state is tracked even though submenu is already "open" visually + expect( mockCurrentContext.submenuOpenedBy.hover ).toBe( true ); + expect( state.isSubmenuOpen ).toBe( true ); // remains true + + // Simulate hover leave + actions.closeMenuOnHover( { pointerType: 'mouse' } ); + + // Verify hover state is cleared + expect( mockCurrentContext.submenuOpenedBy.hover ).toBe( false ); + expect( state.isSubmenuOpen ).toBe( true ); // remains true because overlay is still open + } ); +} ); diff --git a/packages/block-library/src/navigation/view.js b/packages/block-library/src/navigation/view.js index 024786fa1fc256..bc0b2074653413 100644 --- a/packages/block-library/src/navigation/view.js +++ b/packages/block-library/src/navigation/view.js @@ -80,6 +80,17 @@ const { state, actions } = store( ? ctx.overlayOpenedBy : ctx.submenuOpenedBy; }, + get isSubmenuOpen() { + const ctx = getContext(); + // Once the overlay itself is open, its styles always expand + // every submenu regardless of hover/click/focus state, so the + // toggle's `aria-expanded` should reflect that immediately + // instead of waiting for a hover/click/focus interaction. + const isOverlayOpen = + Object.values( ctx.overlayOpenedBy || {} ).filter( Boolean ) + .length > 0; + return isOverlayOpen || state.isMenuOpen; + }, }, actions: { openMenuOnHover( event ) { @@ -88,13 +99,8 @@ const { state, actions } = store( if ( event?.pointerType === 'touch' ) { return; } - const { type, overlayOpenedBy } = getContext(); - if ( - type === 'submenu' && - // Only open on hover if the overlay is closed. - Object.values( overlayOpenedBy || {} ).filter( Boolean ) - .length === 0 - ) { + const { type } = getContext(); + if ( type === 'submenu' ) { actions.openMenu( 'hover' ); } }, @@ -102,13 +108,8 @@ const { state, actions } = store( if ( event?.pointerType === 'touch' ) { return; } - const { type, overlayOpenedBy } = getContext(); - if ( - type === 'submenu' && - // Only close on hover if the overlay is closed. - Object.values( overlayOpenedBy || {} ).filter( Boolean ) - .length === 0 - ) { + const { type } = getContext(); + if ( type === 'submenu' ) { actions.closeMenu( 'hover' ); } },