Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Focus submenu button when clicked #55198

Merged
merged 12 commits into from
Oct 18, 2023
7 changes: 7 additions & 0 deletions packages/block-library/src/navigation/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ function block_core_navigation_add_directives_to_submenu( $w, $block_attributes
$w->set_attribute( 'data-wp-effect', 'effects.core.navigation.initMenu' );
$w->set_attribute( 'data-wp-on--focusout', 'actions.core.navigation.handleMenuFocusout' );
$w->set_attribute( 'data-wp-on--keydown', 'actions.core.navigation.handleMenuKeydown' );

// This is a fix for Safari. Without it, Safari doesn't change the active
// element when the user clicks on a button. It can be removed once we add
// an overlay to capture the clicks, instead of relying on the focusout
// event.
$w->set_attribute( 'tabindex', '-1' );

if ( ! isset( $block_attributes['openSubmenusOnClick'] ) || false === $block_attributes['openSubmenusOnClick'] ) {
$w->set_attribute( 'data-wp-on--mouseenter', 'actions.core.navigation.openMenuOnHover' );
$w->set_attribute( 'data-wp-on--mouseleave', 'actions.core.navigation.closeMenuOnHover' );
Expand Down
24 changes: 18 additions & 6 deletions packages/block-library/src/navigation/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@ const focusableSelectors = [
'[tabindex]:not([tabindex^="-"])',
];

// This is a fix for Safari in iOS/iPadOS. Without it, Safari doesn't focus out
// when the user taps in the body. It can be removed once we add an overlay to
// capture the clicks, instead of relying on the focusout event.
document.addEventListener( 'click', () => {} );

const openMenu = ( store, menuOpenedOn ) => {
const { context, ref, selectors } = store;
const { context, selectors } = store;
selectors.core.navigation.menuOpenedBy( store )[ menuOpenedOn ] = true;
context.core.navigation.previousFocus = ref;
if ( context.core.navigation.type === 'overlay' ) {
// Add a `has-modal-open` class to the <html> root.
document.documentElement.classList.add( 'has-modal-open' );
Expand All @@ -33,7 +37,7 @@ const closeMenu = ( store, menuClosedOn ) => {
window.document.activeElement
)
) {
context.core.navigation.previousFocus.focus();
context.core.navigation.previousFocus?.focus();
}
context.core.navigation.modal = null;
context.core.navigation.previousFocus = null;
Expand Down Expand Up @@ -130,6 +134,8 @@ wpStore( {
closeMenu( store, 'hover' );
},
openMenuOnClick( store ) {
const { context, ref } = store;
context.core.navigation.previousFocus = ref;
openMenu( store, 'click' );
},
closeMenuOnClick( store ) {
Expand All @@ -140,13 +146,16 @@ wpStore( {
openMenu( store, 'focus' );
},
toggleMenuOnClick: ( store ) => {
const { selectors } = store;
const { selectors, context, ref } = store;
// Safari won't send focus to the clicked element, so we need to manually place it: https://bugs.webkit.org/show_bug.cgi?id=22261
if ( window.document.activeElement !== ref ) ref.focus();
const menuOpenedBy =
selectors.core.navigation.menuOpenedBy( store );
if ( menuOpenedBy.click || menuOpenedBy.focus ) {
closeMenu( store, 'click' );
closeMenu( store, 'focus' );
} else {
context.core.navigation.previousFocus = ref;
openMenu( store, 'click' );
}
},
Expand Down Expand Up @@ -194,11 +203,14 @@ wpStore( {
// event.relatedTarget === The element receiving focus (if any)
// When focusout is outsite the document,
// `window.document.activeElement` doesn't change.

// The event.relatedTarget is null when something outside the navigation menu is clicked. This is only necessary for Safari.
if (
! context.core.navigation.modal?.contains(
event.relatedTarget === null ||
( ! context.core.navigation.modal?.contains(
event.relatedTarget
) &&
event.target !== window.document.activeElement
event.target !== window.document.activeElement )
) {
closeMenu( store, 'click' );
closeMenu( store, 'focus' );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/
const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );

test.describe( 'Navigation block - Frontend interactivity', () => {
test.describe( 'Navigation block - Frontend interactivity (@firefox, @webkit)', () => {
test.beforeAll( async ( { requestUtils } ) => {
await requestUtils.activateTheme( 'emptytheme' );
await requestUtils.deleteAllTemplates( 'wp_template_part' );
Expand Down Expand Up @@ -133,10 +133,14 @@ test.describe( 'Navigation block - Frontend interactivity', () => {
const secondLevelElement = page.getByRole( 'link', {
name: 'Nested Submenu Link 1',
} );
const lastFirstLevelElement = page.getByRole( 'link', {
name: 'Complex Submenu Link 2',
} );

// Test: submenu opens on click
await expect( innerElement ).toBeHidden();
await simpleSubmenuButton.click();
await expect( simpleSubmenuButton ).toBeFocused();
await expect( innerElement ).toBeVisible();

// Test: submenu closes on click outside submenu
Expand All @@ -145,10 +149,12 @@ test.describe( 'Navigation block - Frontend interactivity', () => {

// Test: nested submenu opens on click
await complexSubmenuButton.click();
await expect( complexSubmenuButton ).toBeFocused();
await expect( firstLevelElement ).toBeVisible();
await expect( secondLevelElement ).toBeHidden();

await nestedSubmenuButton.click();
await expect( nestedSubmenuButton ).toBeFocused();
await expect( firstLevelElement ).toBeVisible();
await expect( secondLevelElement ).toBeVisible();

Expand All @@ -160,6 +166,7 @@ test.describe( 'Navigation block - Frontend interactivity', () => {
// Test: submenu opens on Enter keypress
await simpleSubmenuButton.focus();
await pageUtils.pressKeys( 'Enter' );
await expect( simpleSubmenuButton ).toBeFocused();
await expect( innerElement ).toBeVisible();

// Test: submenu closes on ESC key and focuses parent link
Expand All @@ -168,27 +175,29 @@ test.describe( 'Navigation block - Frontend interactivity', () => {
await expect( simpleSubmenuButton ).toBeFocused();

// Test: submenu closes on tab outside submenu
await simpleSubmenuButton.focus();
await pageUtils.pressKeys( 'Enter' );
await expect( simpleSubmenuButton ).toBeFocused();
await expect( innerElement ).toBeVisible();
// Tab to first element, then tab outside the submenu.
await pageUtils.pressKeys( 'Tab', { times: 2, delay: 50 } );
await expect( innerElement ).toBeHidden();
await expect( complexSubmenuButton ).toBeFocused();
await expect( innerElement ).toBeHidden();

// Test: only nested submenu closes on tab outside
await complexSubmenuButton.focus();
await pageUtils.pressKeys( 'Enter' );
await expect( complexSubmenuButton ).toBeFocused();
await expect( firstLevelElement ).toBeVisible();
await expect( secondLevelElement ).toBeHidden();

await nestedSubmenuButton.click();
await expect( nestedSubmenuButton ).toBeFocused();
await expect( firstLevelElement ).toBeVisible();
await expect( secondLevelElement ).toBeVisible();

// Tab to nested submenu first element, then tab outside the nested
// submenu.
await pageUtils.pressKeys( 'Tab', { times: 2, delay: 50 } );
await expect( lastFirstLevelElement ).toBeFocused();
await expect( firstLevelElement ).toBeVisible();
await expect( secondLevelElement ).toBeHidden();
// Tab outside the complex submenu.
Expand Down Expand Up @@ -222,7 +231,7 @@ test.describe( 'Navigation block - Frontend interactivity', () => {
await editor.saveSiteEditorEntities();
} );

test( 'submenu opens on click in the arrow', async ( { page } ) => {
test( 'submenu click on the arrow interactions', async ( { page } ) => {
await page.goto( '/' );
const arrowButton = page.getByRole( 'button', {
name: 'Submenu submenu',
Expand All @@ -239,12 +248,41 @@ test.describe( 'Navigation block - Frontend interactivity', () => {

await expect( firstLevelElement ).toBeHidden();
await expect( secondLevelElement ).toBeHidden();
// Open first submenu level
await arrowButton.click();
await expect( arrowButton ).toBeFocused();
await expect( firstLevelElement ).toBeVisible();
await expect( secondLevelElement ).toBeHidden();

// Close first submenu level, check that it closes and focus is on the arrow button
await arrowButton.click();
await expect( arrowButton ).toBeFocused();
// Move the mouse so the hover on the button doesn't keep the menu open
await page.mouse.move( 400, 400 );
await expect( firstLevelElement ).toBeHidden();
await expect( secondLevelElement ).toBeHidden();

// Open first submenu level one more time so we can test the nested submenu
await arrowButton.click();
await expect( arrowButton ).toBeFocused();
await expect( firstLevelElement ).toBeVisible();
await expect( secondLevelElement ).toBeHidden();

// Nested submenu open
await nestedSubmenuArrowButton.click();
await expect( nestedSubmenuArrowButton ).toBeFocused();
await expect( firstLevelElement ).toBeVisible();
await expect( secondLevelElement ).toBeVisible();

// Nested submenu close
await nestedSubmenuArrowButton.click();
await expect( nestedSubmenuArrowButton ).toBeFocused();
// Move the mouse so the hover on the button doesn't keep the menu open
await page.mouse.move( 400, 400 );
await expect( firstLevelElement ).toBeVisible();
await expect( secondLevelElement ).toBeHidden();

// Close menu via click on the body
await page.click( 'body' );
await expect( firstLevelElement ).toBeHidden();
await expect( secondLevelElement ).toBeHidden();
Expand Down