Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/block-library/src/navigation/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
55 changes: 55 additions & 0 deletions packages/block-library/src/navigation/test/view.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* @jest-environment jsdom
*/
Comment on lines +1 to +3

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have stopped using this In #79672, it causes issues with isolated layout. Can we please create a follow up to remove that comment?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have created a PR to remvoe it and also added an ESLint rule to prevent it - #80676


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
} );
} );
29 changes: 15 additions & 14 deletions packages/block-library/src/navigation/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) {
Expand All @@ -88,27 +99,17 @@ 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' );
}
},
closeMenuOnHover( event ) {
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' );
}
},
Expand Down
Loading