Skip to content

Commit

Permalink
Navigation block: Set correct aria-expanded on hover (WordPress#50953)
Browse files Browse the repository at this point in the history
* Set correct aria-expanded on hover

* Store both `click` and `hover` in `isMenuOpen`

* Fix nav menu

* Remove menuOpenedOn debugger

* Add comments and fix example HTML

* Fix PHP lint

* Just in case openSubmenusOnClick exists but it's false

* Switch to isMenuOpen selector in roleAttribute
  • Loading branch information
luisherranz authored and sethrubenstein committed Jul 13, 2023
1 parent 7d1fdf9 commit 00931ae
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 83 deletions.
62 changes: 35 additions & 27 deletions lib/experimental/interactivity-api/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,18 @@ function gutenberg_block_core_file_add_directives_to_content( $block_content, $b
*
* <nav
* data-wp-island
* data-wp-context='{ "core": { "navigation": { "isMenuOpen": false, "overlay": true, "roleAttribute": "" } } }'
* data-wp-context='{ "core": { "navigation": { "isMenuOpen": { "click": false, "hover": false }, "overlay": true, "roleAttribute": "" } } }'
* >
* <button
* class="wp-block-navigation__responsive-container-open"
* data-wp-on.click="actions.core.navigation.openMenu"
* data-wp-on.click="actions.core.navigation.openMenuOnClick"
* data-wp-on.keydown="actions.core.navigation.handleMenuKeydown"
* >
* <div
* class="wp-block-navigation__responsive-container"
* data-wp-class.has-modal-open="context.core.navigation.isMenuOpen"
* data-wp-class.is-menu-open="context.core.navigation.isMenuOpen"
* data-wp-bind.aria-hidden="!context.core.navigation.isMenuOpen"
* data-wp-class.has-modal-open="selectors.core.navigation.isMenuOpen"
* data-wp-class.is-menu-open="selectors.core.navigation.isMenuOpen"
* data-wp-bind.aria-hidden="!selectors.core.navigation.isMenuOpen"
* data-wp-effect="effects.core.navigation.initMenu"
* data-wp-on.keydown="actions.core.navigation.handleMenuKeydown"
* data-wp-on.focusout="actions.core.navigation.handleMenuFocusout"
Expand All @@ -55,13 +55,13 @@ function gutenberg_block_core_file_add_directives_to_content( $block_content, $b
* <div class="wp-block-navigation__responsive-close">
* <div
* class="wp-block-navigation__responsive-dialog"
* data-wp-bind.aria-modal="context.core.navigation.isMenuOpen"
* data-wp-bind.aria-modal="selectors.core.navigation.isMenuOpen"
* data-wp-bind.role="selectors.core.navigation.roleAttribute"
* data-wp-effect="effects.core.navigation.focusFirstElement"
* >
* <button
* class="wp-block-navigation__responsive-container-close"
* data-wp-on.click="actions.core.navigation.closeMenu"
* data-wp-on.click="actions.core.navigation.closeMenuOnclick"
* >
* <svg>
* <button>
Expand All @@ -72,15 +72,16 @@ function gutenberg_block_core_file_add_directives_to_content( $block_content, $b
* </nav>
*
* @param string $block_content Markup of the navigation block.
* @param array $block Block object.
*
* @return string Navigation block markup with the proper directives
*/
function gutenberg_block_core_navigation_add_directives_to_markup( $block_content ) {
function gutenberg_block_core_navigation_add_directives_to_markup( $block_content, $block ) {
$w = new WP_HTML_Tag_Processor( $block_content );
// Add directives to the `<nav>` element.
if ( $w->next_tag( 'nav' ) ) {
$w->set_attribute( 'data-wp-island', '' );
$w->set_attribute( 'data-wp-context', '{ "core": { "navigation": { "isMenuOpen": false, "overlay": true, "roleAttribute": "" } } }' );
$w->set_attribute( 'data-wp-context', '{ "core": { "navigation": { "isMenuOpen": { "click": false, "hover": false }, "overlay": true, "roleAttribute": "" } } }' );
};

// Add directives to the open menu button.
Expand All @@ -90,14 +91,14 @@ function gutenberg_block_core_navigation_add_directives_to_markup( $block_conten
'class_name' => 'wp-block-navigation__responsive-container-open',
)
) ) {
$w->set_attribute( 'data-wp-on.click', 'actions.core.navigation.openMenu' );
$w->set_attribute( 'data-wp-on.click', 'actions.core.navigation.openMenuOnClick' );
$w->set_attribute( 'data-wp-on.keydown', 'actions.core.navigation.handleMenuKeydown' );
$w->remove_attribute( 'data-micromodal-trigger' );
} else {
// If the open modal button not found, we handle submenus immediately.
$w = new WP_HTML_Tag_Processor( $w->get_updated_html() );

gutenberg_block_core_navigation_add_directives_to_submenu( $w );
gutenberg_block_core_navigation_add_directives_to_submenu( $w, $block['attrs'] );

return $w->get_updated_html();
}
Expand All @@ -109,9 +110,9 @@ function gutenberg_block_core_navigation_add_directives_to_markup( $block_conten
'class_name' => 'wp-block-navigation__responsive-container',
)
) ) {
$w->set_attribute( 'data-wp-class.has-modal-open', 'context.core.navigation.isMenuOpen' );
$w->set_attribute( 'data-wp-class.is-menu-open', 'context.core.navigation.isMenuOpen' );
$w->set_attribute( 'data-wp-bind.aria-hidden', '!context.core.navigation.isMenuOpen' );
$w->set_attribute( 'data-wp-class.has-modal-open', 'selectors.core.navigation.isMenuOpen' );
$w->set_attribute( 'data-wp-class.is-menu-open', 'selectors.core.navigation.isMenuOpen' );
$w->set_attribute( 'data-wp-bind.aria-hidden', '!selectors.core.navigation.isMenuOpen' );
$w->set_attribute( 'data-wp-effect', 'effects.core.navigation.initMenu' );
$w->set_attribute( 'data-wp-on.keydown', 'actions.core.navigation.handleMenuKeydown' );
$w->set_attribute( 'data-wp-on.focusout', 'actions.core.navigation.handleMenuFocusout' );
Expand All @@ -135,7 +136,7 @@ function gutenberg_block_core_navigation_add_directives_to_markup( $block_conten
'class_name' => 'wp-block-navigation__responsive-dialog',
)
) ) {
$w->set_attribute( 'data-wp-bind.aria-modal', 'context.core.navigation.isMenuOpen' );
$w->set_attribute( 'data-wp-bind.aria-modal', 'selectors.core.navigation.isMenuOpen' );
$w->set_attribute( 'data-wp-bind.role', 'selectors.core.navigation.roleAttribute' );
$w->set_attribute( 'data-wp-effect', 'effects.core.navigation.focusFirstElement' );
};
Expand All @@ -147,12 +148,12 @@ function gutenberg_block_core_navigation_add_directives_to_markup( $block_conten
'class_name' => 'wp-block-navigation__responsive-container-close',
)
) ) {
$w->set_attribute( 'data-wp-on.click', 'actions.core.navigation.closeMenu' );
$w->set_attribute( 'data-wp-on.click', 'actions.core.navigation.closeMenuOnClick' );
$w->remove_attribute( 'data-micromodal-close' );
};

// Submenus.
gutenberg_block_core_navigation_add_directives_to_submenu( $w );
gutenberg_block_core_navigation_add_directives_to_submenu( $w, $block['attrs'] );

return $w->get_updated_html();
};
Expand All @@ -163,15 +164,17 @@ function gutenberg_block_core_navigation_add_directives_to_markup( $block_conten
*
* <li
* class="has-child"
* data-wp-context='{ "core": { "navigation": { "isMenuOpen": false, "overlay": false } } }'
* data-wp-context='{ "core": { "navigation": { "isMenuOpen": { "click": false, "hover": false, "overlay": false } } }'
* data-wp-effect="effects.core.navigation.initMenu"
* data-wp-on.keydown="actions.core.navigation.handleMenuKeydown"
* data-wp-on.focusout="actions.core.navigation.handleMenuFocusout"
* data-wp-on.mouseenter="actions.core.navigation.openMenuOnHover"
* data-wp-on.mouseleave="actions.core.navigation.closeMenuOnHover"
* >
* <button
* class="wp-block-navigation-submenu__toggle"
* data-wp-on.click="actions.core.navigation.openMenu"
* data-wp-bind.aria-expanded="context.core.navigation.isMenuOpen"
* data-wp-on.click="actions.core.navigation.toggleMenuOnClick"
* data-wp-bind.aria-expanded="selectors.core.navigation.isMenuOpen"
* >
* </button>
* <span>Title</span>
Expand All @@ -180,22 +183,27 @@ function gutenberg_block_core_navigation_add_directives_to_markup( $block_conten
* </ul>
* </li>
*
* @param string $w Markup of the navigation block.
* @param string $w Markup of the navigation block.
* @param array $block_attributes Block attributes.
*
* @return void
*/
function gutenberg_block_core_navigation_add_directives_to_submenu( $w ) {
function gutenberg_block_core_navigation_add_directives_to_submenu( $w, $block_attributes ) {
while ( $w->next_tag(
array(
'tag_name' => 'LI',
'class_name' => 'has-child',
)
) ) {
// Add directives to the parent `<li>`.
$w->set_attribute( 'data-wp-context', '{ "core": { "navigation": { "isMenuOpen": false, "overlay": false } } }' );
$w->set_attribute( 'data-wp-context', '{ "core": { "navigation": { "isMenuOpen": { "click": false, "hover": false }, "overlay": false } } }' );
$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' );
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' );
}

// Add directives to the toggle submenu button.
if ( $w->next_tag(
Expand All @@ -204,16 +212,16 @@ function gutenberg_block_core_navigation_add_directives_to_submenu( $w ) {
'class_name' => 'wp-block-navigation-submenu__toggle',
)
) ) {
$w->set_attribute( 'data-wp-on.click', 'actions.core.navigation.toggleMenu' );
$w->set_attribute( 'data-wp-bind.aria-expanded', 'context.core.navigation.isMenuOpen' );
$w->set_attribute( 'data-wp-on.click', 'actions.core.navigation.toggleMenuOnClick' );
$w->set_attribute( 'data-wp-bind.aria-expanded', 'selectors.core.navigation.isMenuOpen' );
};

// Iterate through subitems if exist.
gutenberg_block_core_navigation_add_directives_to_submenu( $w );
gutenberg_block_core_navigation_add_directives_to_submenu( $w, $block_attributes );
}
};

add_filter( 'render_block_core/navigation', 'gutenberg_block_core_navigation_add_directives_to_markup', 10, 1 );
add_filter( 'render_block_core/navigation', 'gutenberg_block_core_navigation_add_directives_to_markup', 10, 2 );

/**
* Replaces view script for the File, Navigation, and Image blocks with version using Interactivity API.
Expand Down
130 changes: 74 additions & 56 deletions packages/block-library/src/navigation/interactivity.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,40 @@ const focusableSelectors = [
'[tabindex]:not([tabindex^="-"])',
];

const openMenu = ( { context, ref }, menuOpenedOn ) => {
context.core.navigation.isMenuOpen[ menuOpenedOn ] = true;
context.core.navigation.previousFocus = ref;
if ( context.core.navigation.overlay ) {
// Add a `has-modal-open` class to the <html> root.
document.documentElement.classList.add( 'has-modal-open' );
}
};

const closeMenu = ( { context, selectors }, menuClosedOn ) => {
context.core.navigation.isMenuOpen[ menuClosedOn ] = false;
// Check if the menu is still open or not.
if ( ! selectors.core.navigation.isMenuOpen( { context } ) ) {
if (
context.core.navigation.modal.contains(
window.document.activeElement
)
) {
context.core.navigation.previousFocus.focus();
}
context.core.navigation.modal = null;
context.core.navigation.previousFocus = null;
if ( context.core.navigation.overlay ) {
document.documentElement.classList.remove( 'has-modal-open' );
}
}
};

store( {
effects: {
core: {
navigation: {
initMenu: ( { context, ref } ) => {
if ( context.core.navigation.isMenuOpen ) {
initMenu: ( { context, selectors, ref } ) => {
if ( selectors.core.navigation.isMenuOpen( { context } ) ) {
const focusableElements =
ref.querySelectorAll( focusableSelectors );
context.core.navigation.modal = ref;
Expand All @@ -32,8 +60,8 @@ store( {
focusableElements[ focusableElements.length - 1 ];
}
},
focusFirstElement: ( { context, ref } ) => {
if ( context.core.navigation.isMenuOpen ) {
focusFirstElement: ( { context, selectors, ref } ) => {
if ( selectors.core.navigation.isMenuOpen( { context } ) ) {
ref.querySelector(
'.wp-block-navigation-item > *:first-child'
).focus();
Expand All @@ -45,62 +73,51 @@ store( {
selectors: {
core: {
navigation: {
roleAttribute: ( { context } ) => {
return context.core.navigation.overlay &&
context.core.navigation.isMenuOpen
roleAttribute: ( { context, selectors } ) =>
context.core.navigation.overlay &&
selectors.core.navigation.isMenuOpen( { context } )
? 'dialog'
: '';
},
: '',
isMenuOpen: ( { context } ) =>
// The menu is opened if either `click` or `hover` is true.
Object.values( context.core.navigation.isMenuOpen ).filter(
Boolean
).length > 0,
},
},
},
actions: {
core: {
navigation: {
openMenu: ( { context, ref } ) => {
context.core.navigation.isMenuOpen = true;
context.core.navigation.previousFocus = ref;
if ( context.core.navigation.overlay ) {
// It adds a `has-modal-open` class to the <html> root
document.documentElement.classList.add(
'has-modal-open'
);
}
openMenuOnHover( args ) {
openMenu( args, 'hover' );
},
closeMenu: ( { context } ) => {
if ( context.core.navigation.isMenuOpen ) {
context.core.navigation.isMenuOpen = false;
if (
context.core.navigation.modal.contains(
window.document.activeElement
)
) {
context.core.navigation.previousFocus.focus();
}
context.core.navigation.modal = null;
context.core.navigation.previousFocus = null;
if ( context.core.navigation.overlay ) {
document.documentElement.classList.remove(
'has-modal-open'
);
}
}
closeMenuOnHover( args ) {
closeMenu( args, 'hover' );
},
toggleMenu: ( { context, actions, ref } ) => {
if ( context.core.navigation.isMenuOpen ) {
actions.core.navigation.closeMenu( { context } );
openMenuOnClick( args ) {
openMenu( args, 'click' );
},
closeMenuOnClick( args ) {
closeMenu( args, 'click' );
},
toggleMenuOnClick: ( args ) => {
const { context } = args;
if ( context.core.navigation.isMenuOpen.click ) {
closeMenu( args, 'click' );
} else {
actions.core.navigation.openMenu( { context, ref } );
openMenu( args, 'click' );
}
},
handleMenuKeydown: ( { actions, context, event } ) => {
handleMenuKeydown: ( args ) => {
const { context, event } = args;
if ( context.core.navigation.isMenuOpen ) {
// If Escape close the menu
if (
event?.key === 'Escape' ||
event?.keyCode === 27
) {
actions.core.navigation.closeMenu( { context } );
closeMenu( args, 'click' );
return;
}

Expand Down Expand Up @@ -129,20 +146,21 @@ store( {
}
}
},
handleMenuFocusout: ( { actions, context, event } ) => {
if ( context.core.navigation.isMenuOpen ) {
// If focus is outside modal, and in the document, close menu
// event.target === The element losing focus
// event.relatedTarget === The element receiving focus (if any)
// When focusout is outsite the document, `window.document.activeElement` doesn't change
if (
! context.core.navigation.modal.contains(
event.relatedTarget
) &&
event.target !== window.document.activeElement
) {
actions.core.navigation.closeMenu( { context } );
}
handleMenuFocusout: ( args ) => {
const { context, event } = args;
// If focus is outside modal, and in the document, close menu
// event.target === The element losing focus
// event.relatedTarget === The element receiving focus (if any)
// When focusout is outsite the document,
// `window.document.activeElement` doesn't change
if (
context.core.navigation.isMenuOpen.click &&
! context.core.navigation.modal.contains(
event.relatedTarget
) &&
event.target !== window.document.activeElement
) {
closeMenu( args, 'click' );
}
},
},
Expand Down

0 comments on commit 00931ae

Please sign in to comment.