Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3ea96bf
Navigation block: use <dialog>
stokesman Mar 26, 2025
f5baf29
Restore `Escape` handling to close sub menus
stokesman Mar 26, 2025
9498f66
Ensure closing logic is applied by listening to dialog `close`
stokesman Mar 26, 2025
c4f7c43
Temporarily omit focus trap testing in E2E
stokesman Mar 26, 2025
b5dc382
Try fixing color inheritance due to UA styles on <dialog>
stokesman Mar 26, 2025
6046f5c
Avoid closing submenu on hover out if opened by click
stokesman Mar 27, 2025
b9a6a53
Use async directive for submenu keydown
stokesman Mar 28, 2025
aad3ae3
Use Invoker Commands for dialog
stokesman Mar 28, 2025
a693293
Consolidate open/close effectual code
stokesman Mar 28, 2025
8837ba3
Remove tabbing assertions; remove webkit specific test
stokesman Mar 29, 2025
ecce206
Revise CSS selector and add no-JS equivalent
stokesman Mar 31, 2025
01643fa
Move aria-label to dialog element
stokesman Mar 31, 2025
ef6c7f2
Revise submenu hover conditionals
stokesman Mar 31, 2025
f550145
Restore important rules for inheriting colors when overlay is not open
stokesman Mar 31, 2025
370b204
Use `:open` instead of `[open]`
westonruter Mar 31, 2025
74b77a8
Add `:open` selector where it was missing
stokesman Mar 31, 2025
42d6402
Set focus on first link when the nav menu opens
westonruter Apr 1, 2025
966098f
Use [open] selector to support more browsers
stokesman Apr 2, 2025
c3c9a37
Fix e2e `overlayMenu` attribute values
stokesman Apr 2, 2025
4fb7c98
Update e2e test to work in webkit
stokesman Apr 2, 2025
2811cc2
Add open/close handlers only if Invoker commands aren’t supported
stokesman Apr 3, 2025
6a086f6
Fix focus return for Escape key presses
stokesman Apr 8, 2025
8188533
Merge branch 'trunk' into try/navigation-block-dialog
stokesman Apr 8, 2025
bedfafb
Fixup of merge commit
stokesman Apr 8, 2025
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
63 changes: 51 additions & 12 deletions packages/block-library/src/navigation/edit/responsive-wrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import clsx from 'clsx';
*/
import { close, Icon } from '@wordpress/icons';
import { Button } from '@wordpress/components';
import { useRefEffect } from '@wordpress/compose';
import { __ } from '@wordpress/i18n';
import { getColorClassName } from '@wordpress/block-editor';

/**
* Internal dependencies
*/
import OverlayMenuIcon from './overlay-menu-icon';
import { getScrollContainer } from '@wordpress/dom';

export default function ResponsiveWrapper( {
children,
Expand All @@ -28,6 +30,50 @@ export default function ResponsiveWrapper( {
hasIcon,
icon,
} ) {
// Depending on the isOpen state, adds/removes a class on document root to
// match front end. Also if the document root is not the scrolling context,
// as is the case if the editor is not iframed, disables/enables scrolling.
// Since “Device previews” force the iframe this should only be applicable
// when editing in a narrow viewport or if the Navigation is set to always
// be overlaid.
const effectRootAndScrollContainer = useRefEffect(
( node ) => {
const shouldShow = isResponsive && isOpen;
const root = node.ownerDocument.documentElement;
root.classList.toggle( 'has-modal-open', shouldShow );
if ( shouldShow ) {
const isNonIframed = node.ownerDocument.defaultView === window;
// The dialog is not modal unless the canvas is iframed because
// as a modal it can’t be contained by the canvas.
node[ isNonIframed ? 'show' : 'showModal' ]();
const scrollContainer = getScrollContainer( node );
if ( root === scrollContainer ) {
return;
}
// There is some potential that the scroll container is not the
// root even when the editor is iframed but this should be okay.
// Note the front end doesn’t have equivalent logic, so there
// could be a discrepancy for such cases.
const overflowBackup = [
scrollContainer.style.getPropertyValue( 'overflow' ),
scrollContainer.style.getPropertyPriority( 'overflow' ),
];
scrollContainer.style.setProperty(
'overflow',
'hidden',
'important'
);
return () => {
scrollContainer.style.setProperty(
'overflow',
...overflowBackup
);
};
}
node.close();
},
[ isOpen, isResponsive ]
);
if ( ! isResponsive ) {
return children;
}
Expand Down Expand Up @@ -66,15 +112,6 @@ export default function ResponsiveWrapper( {

const modalId = `${ id }-modal`;

const dialogProps = {
className: 'wp-block-navigation__responsive-dialog',
...( isOpen && {
role: 'dialog',
'aria-modal': true,
'aria-label': __( 'Menu' ),
} ),
};

return (
<>
{ ! isOpen && (
Expand All @@ -90,16 +127,18 @@ export default function ResponsiveWrapper( {
</Button>
) }

<div
<dialog
className={ responsiveContainerClasses }
style={ styles }
id={ modalId }
ref={ effectRootAndScrollContainer }
aria-label={ isOpen && __( 'Menu' ) }
>
<div
className="wp-block-navigation__responsive-close"
tabIndex="-1"
>
<div { ...dialogProps }>
<div className="wp-block-navigation__responsive-dialog">
<Button
__next40pxDefaultSize
className="wp-block-navigation__responsive-container-close"
Expand All @@ -117,7 +156,7 @@ export default function ResponsiveWrapper( {
</div>
</div>
</div>
</div>
</dialog>
</>
);
}
4 changes: 4 additions & 0 deletions packages/block-library/src/navigation/editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,10 @@ $color-control-label-height: 20px;
// When not fullscreen.
.wp-block-navigation__responsive-container.is-menu-open {
position: fixed;
// This z-index was applied in style.scss but is ineffectual for a modal dialog.
// In the editor, if the canvas is not iframed then this element is not modal and
// this is applicable.
z-index: 10000;
top: $admin-bar-height-big + $header-height + $block-toolbar-height + $border-width;

@include break-medium() {
Expand Down
62 changes: 25 additions & 37 deletions packages/block-library/src/navigation/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -492,30 +492,19 @@ private static function get_responsive_container_markup( $attributes, $inner_blo
$toggle_aria_label_close = $should_display_icon_label ? 'aria-label="' . __( 'Close menu' ) . '"' : ''; // Close button label.

// Add Interactivity API directives to the markup if needed.
$open_button_directives = '';
$responsive_container_directives = '';
$responsive_dialog_directives = '';
$close_button_directives = '';
$open_button_directives = '';
$responsive_container_directives = '';
$close_button_directives = '';
$responsive_container_content_directives = '';
if ( $is_interactive ) {
$open_button_directives = '
data-wp-on-async--click="actions.openMenuOnClick"
data-wp-on--keydown="actions.handleMenuKeydown"
data-wp-init="callbacks.mountDialogInvoker"
';
$responsive_container_directives = '
data-wp-class--has-modal-open="state.isMenuOpen"
data-wp-class--is-menu-open="state.isMenuOpen"
data-wp-watch="callbacks.initMenu"
data-wp-on--keydown="actions.handleMenuKeydown"
data-wp-on-async--focusout="actions.handleMenuFocusout"
tabindex="-1"
';
$responsive_dialog_directives = '
data-wp-bind--aria-modal="state.ariaModal"
data-wp-bind--aria-label="state.ariaLabel"
data-wp-bind--role="state.roleAttribute"
data-wp-init="callbacks.mountDialog"
';
$close_button_directives = '
data-wp-on-async--click="actions.closeMenuOnClick"
data-wp-init="callbacks.mountDialogInvoker"
';
$responsive_container_content_directives = '
data-wp-watch="callbacks.focusFirstElement"
Expand All @@ -524,18 +513,28 @@ private static function get_responsive_container_markup( $attributes, $inner_blo

$overlay_inline_styles = esc_attr( safecss_filter_attr( $colors['overlay_inline_styles'] ) );

// Set the autofocus attribute on the first link.
$tag_processor = new WP_HTML_Tag_Processor( $inner_blocks_html );
while ( $tag_processor->next_tag( array( 'tag_name' => 'A' ) ) ) {
Copy link
Member

@westonruter westonruter Apr 1, 2025

Choose a reason for hiding this comment

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

This should also look for other focusable elements to correspond with:

focusFirstElement() {
const { ref } = getElement();
if ( state.isMenuOpen ) {
const focusableElements =
ref.querySelectorAll( focusableSelectors );
focusableElements?.[ 0 ]?.focus();
}
},

I only implemented the first of these focusableSelectors so far:

const focusableSelectors = [
'a[href]',
'input:not([disabled]):not([type="hidden"]):not([aria-hidden])',
'select:not([disabled]):not([aria-hidden])',
'textarea:not([disabled]):not([aria-hidden])',
'button:not([disabled]):not([aria-hidden])',
'[contenteditable]',
'[tabindex]:not([tabindex^="-"])',
];

if ( is_string( $tag_processor->get_attribute( 'href' ) ) ) {
$tag_processor->set_attribute( 'autofocus', true );
$inner_blocks_html = $tag_processor->get_updated_html();
break;
}
}

return sprintf(
'<button aria-haspopup="dialog" %3$s class="%6$s" %10$s>%8$s</button>
<div class="%5$s" %7$s id="%1$s" %11$s>
'<button aria-haspopup="dialog" %3$s class="%6$s" %10$s commandfor="%1$s" command="show-modal">%8$s</button>
<dialog class="%5$s" %7$s id="%1$s" %11$s>
<div class="wp-block-navigation__responsive-close" tabindex="-1">
<div class="wp-block-navigation__responsive-dialog" %12$s>
<button %4$s class="wp-block-navigation__responsive-container-close" %13$s>%9$s</button>
<div class="wp-block-navigation__responsive-container-content" %14$s id="%1$s-content">
<div class="wp-block-navigation__responsive-dialog">
<button %4$s class="wp-block-navigation__responsive-container-close" %12$s commandfor="%1$s" command="close">%9$s</button>
<div class="wp-block-navigation__responsive-container-content" %13$s id="%1$s-content">
%2$s
</div>
</div>
</div>
</div>',
</dialog>',
esc_attr( $modal_unique_id ),
$inner_blocks_html,
$toggle_aria_label_open,
Expand All @@ -547,7 +546,6 @@ private static function get_responsive_container_markup( $attributes, $inner_blo
$toggle_close_button_content,
$open_button_directives,
$responsive_container_directives,
$responsive_dialog_directives,
$close_button_directives,
$responsive_container_content_directives
);
Expand Down Expand Up @@ -599,16 +597,7 @@ private static function get_nav_element_directives( $is_interactive ) {
}
// When adding to this array be mindful of security concerns.
$nav_element_context = wp_interactivity_data_wp_context(
array(
'overlayOpenedBy' => array(
'click' => false,
'hover' => false,
'focus' => false,
),
'type' => 'overlay',
'roleAttribute' => '',
'ariaLabel' => __( 'Menu' ),
)
array( 'ariaLabel' => __( 'Menu' ) )
);
$nav_element_directives = '
data-wp-interactive="core/navigation" '
Expand Down Expand Up @@ -819,8 +808,7 @@ function block_core_navigation_add_directives_to_submenu( $tags, $block_attribut
) ) {
// Add directives to the parent `<li>`.
$tags->set_attribute( 'data-wp-interactive', 'core/navigation' );
$tags->set_attribute( 'data-wp-context', '{ "submenuOpenedBy": { "click": false, "hover": false, "focus": false }, "type": "submenu", "modal": null, "previousFocus": null }' );
$tags->set_attribute( 'data-wp-watch', 'callbacks.initMenu' );
$tags->set_attribute( 'data-wp-context', '{ "submenuOpenedBy": { "click": false, "hover": false, "focus": false }, "menu": null, "previousFocus": null }' );
$tags->set_attribute( 'data-wp-on--focusout', 'actions.handleMenuFocusout' );
$tags->set_attribute( 'data-wp-on--keydown', 'actions.handleMenuKeydown' );

Expand Down
57 changes: 22 additions & 35 deletions packages/block-library/src/navigation/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -466,12 +466,14 @@ button.wp-block-navigation-item__content {
}
}
.wp-block-navigation__responsive-container {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
color: inherit;
background-color: inherit;
width: auto;
height: auto;
max-width: 100%;
max-height: 100%;
box-sizing: border-box;
border: 0;

// Low specificity so that themes can override.
& :where(.wp-block-navigation-item a) {
Expand All @@ -486,9 +488,9 @@ button.wp-block-navigation-item__content {
align-items: var(--navigation-layout-align, initial);
}

// If the responsive wrapper is present but overlay is not open,
// overlay styles shouldn't apply.
&:not(.is-menu-open.is-menu-open) {
// If the responsive wrapper is present but overlay is not open, overlay colors
// shouldn't apply and !important is to override color classes or inline styles.
&:not(.is-menu-open, [open]) { /* todo: replace [open] with :open when browser support is widely available. */
color: inherit !important;
background-color: inherit !important;
}
Expand All @@ -498,10 +500,9 @@ button.wp-block-navigation-item__content {
// Inherit as much as we can regarding colors, fonts, sizes,
// but otherwise provide a baseline.
// In a future version, we can explore more customizability.
&.is-menu-open {
display: flex; // Needs to be set to override "none".
&:is(.is-menu-open, [open]) { /* todo: replace [open] with :open when browser support is widely available. */
display: flex;
flex-direction: column;
background-color: inherit;

// Animation.
@media not (prefers-reduced-motion) {
Expand All @@ -518,9 +519,6 @@ button.wp-block-navigation-item__content {
// Allow modal to scroll.
overflow: auto;

// Give it a z-index just higher than the adminbar.
z-index: 100000;

.wp-block-navigation__responsive-container-content {
// Add padding above to accommodate close button.
padding-top: calc(2rem + #{ $navigation-icon-size });
Expand Down Expand Up @@ -612,7 +610,7 @@ button.wp-block-navigation-item__content {

@include break-small() {
&:not(.hidden-by-default) {
&:not(.is-menu-open) {
&:not(.is-menu-open, [open]) { /* todo: replace [open] with :open when browser support is widely available. */
display: block;
width: 100%;
position: relative;
Expand All @@ -625,7 +623,7 @@ button.wp-block-navigation-item__content {
}
}

&.is-menu-open {
&:is(.is-menu-open, [open]) { /* todo: replace [open] with :open when browser support is widely available. */
// Override breakpoint-inherited submenu rules.
.wp-block-navigation__submenu-container.wp-block-navigation__submenu-container.wp-block-navigation__submenu-container.wp-block-navigation__submenu-container {
left: 0;
Expand All @@ -636,12 +634,12 @@ button.wp-block-navigation-item__content {

// Default menu background and font color.
.wp-block-navigation:not(.has-background)
.wp-block-navigation__responsive-container.is-menu-open {
.wp-block-navigation__responsive-container:is(.is-menu-open, [open]) { /* todo: replace [open] with :open when browser support is widely available. */
background-color: #fff;
}

.wp-block-navigation:not(.has-text-color)
.wp-block-navigation__responsive-container.is-menu-open {
.wp-block-navigation__responsive-container:is(.is-menu-open, [open]) { /* todo: replace [open] with :open when browser support is widely available. */
color: #000;
}

Expand Down Expand Up @@ -712,7 +710,7 @@ button.wp-block-navigation-item__content {
.wp-block-navigation__responsive-close {
width: 100%;

.has-modal-open & {
:is(.is-menu-open, [open]) > & { /* todo: replace [open] with :open when browser support is widely available. */
// Try to inherit wide-width when defined, so the X can align to a top-right aligned menu.
max-width: var(--wp--style--global--wide-size, 100%);
margin-left: auto;
Expand All @@ -728,28 +726,17 @@ button.wp-block-navigation-item__content {
}
}

.is-menu-open .wp-block-navigation__responsive-close,
.is-menu-open .wp-block-navigation__responsive-dialog,
.is-menu-open .wp-block-navigation__responsive-container-content {
.wp-block-navigation__responsive-container:is(.is-menu-open, [open]) /* todo: replace [open] with :open when browser support is widely available. */
:is(.wp-block-navigation__responsive-close, .wp-block-navigation__responsive-dialog, .wp-block-navigation__responsive-container-content) {
box-sizing: border-box;
}

.wp-block-navigation__responsive-dialog {
position: relative;
}

// Adjust open dialog top margin when admin-bar is visible.
// Needs to be scoped to .is-menu-open, or it will shift the position of any other navigations that may be present.
.has-modal-open .admin-bar .is-menu-open .wp-block-navigation__responsive-dialog {
margin-top: $admin-bar-height-big;

// Handle smaller admin-bar.
@include break-medium() {
margin-top: $admin-bar-height;
}
}
Comment on lines -741 to -750
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The overlay (dialog) is now in the top-layer and the admin bar is unable to appear in front of it so these styles are irrelevant.


// Prevent scrolling of the parent content when the modal is open.
html.has-modal-open {
html.has-modal-open,
html:has(.wp-block-navigation__responsive-container[open]) { /* todo: replace [open] with :open when browser support is widely available. */
overflow: hidden;
}
Loading
Loading