-
Notifications
You must be signed in to change notification settings - Fork 4.6k
Navigation block: use <dialog>
#69720
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
base: trunk
Are you sure you want to change the base?
Conversation
|
Size Change: +122 B (+0.01%) Total Size: 1.84 MB
ℹ️ View Unchanged
|
Also, simplify `closeMenu` and its usage
Also: - Add `modal` to nav element’s context from PHP - Rename `initMenu` to `effectOpenClose`
48eac1f to
a693293
Compare
| <div { ...dialogProps }> | ||
| <div | ||
| className="wp-block-navigation__responsive-dialog" | ||
| aria-label={ isOpen && __( 'Menu' ) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I learned recently that aria-label doesn't do anything on elements that don't have a role: https://gomakethings.com/aria-label-requires-a-valid-role/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for highlighting this. The attribute was already here but I did move the code for it—it was here. Now this element is wrapped in the dialog so I’d removed the role and aria-modal attributes. I suppose this label should move to the dialog element.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved the aria-label to the dialog element in 01643fa. This could still use some scrutiny. The label is applied only when the overlay is open. That’s how it was yet now the overlay can open without JS and the label won’t be applied. I think the idea behind conditionally applying it is to avoid its presence when the navigation has no overlay—i.e. in larger viewports. With this PR, the dialog element is there regardless of the viewport size and perhaps it’s not wrong for it to have the label even when it’s not acting as a modal.
|
Question: In #69665 (comment) you said you started this branch from my PR? Just curious because I don't see my commit in this PR's history. |
| // Toggles `has-modal-open` class on the <html> root. | ||
| document.documentElement.classList.toggle( | ||
| 'has-modal-open', | ||
| state.isMenuOpen | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be ideal if this class toggling wasn't required. Namely, the CSS rule could check if body:has(dialog.wp-block-navigation__responsive-container:open) instead. Then this would not require any JS to apply the styling.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Namely:
gutenberg/packages/block-library/src/navigation/style.scss
Lines 702 to 711 in a693293
| // The menu adds wrapping containers. | |
| .wp-block-navigation__responsive-close { | |
| width: 100%; | |
| .has-modal-open & { | |
| // 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; | |
| margin-right: auto; | |
| } |
and
gutenberg/packages/block-library/src/navigation/style.scss
Lines 731 to 735 in a693293
| // Prevent scrolling of the parent content when the modal is open. | |
| html.has-modal-open, | |
| html:has(.wp-block-navigation__responsive-container[open]) { | |
| overflow: hidden; | |
| } |
html.has-modal-open can be replaced with html:has(dialog.wp-block-navigation__responsive-container:open).
And I suppose .has-modal-open & can be replaced with html:has(dialog.wp-block-navigation__responsive-container:open) &
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be ideal if this class toggling wasn't required.
I’d agree yet I kept the classes in accord this part of the BC guidelines:
Class names and DOM nodes used inside the tree of React components are not considered part of the public API and can be modified.
Changes to these should be done with caution as it can affect the styling and behavior of third-party code (Even if they should not rely on these in the first place). Keep the old ones if possible. If not, document the changes and write a dev note.
Thanks to your suggestions though I realized I missed adding a no-JS equivalent selector for this one:
| .has-modal-open & { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, got it. Ok, I think we should then keep the class name for now, but we can add a comment to say it is deprecated, and we can write a dev note to comment this. I'll do a search for themes in the directory to see if anyone is targeting this class name so we'll have a list for outreach.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are 132 themes that are using this class name, at least at the time that WPdirectory last indexed the directory: https://wpdirectory.net/search/01JQQ5VY70TAAEMSXTAQVE69GW
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even were these class names to be kept indefinitely it would be nice to do outreach that recommends updating any selectors using them because otherwise the theme’s styles won’t apply if JS is unavailable.
| await pageUtils.pressKeys( 'Tab', { times: 2, delay: 50 } ); | ||
| await expect( closeMenuButton ).toBeFocused(); | ||
| await pageUtils.pressKeys( 'Shift+Tab', { times: 2, delay: 50 } ); | ||
| await expect( overlayMenuFirstElement ).toBeFocused(); | ||
| /** | ||
| * @todo Determine if focus trapping is required. If not, these assertions | ||
| * can be removed and seemingly the next test too — see its comment. | ||
| */ | ||
| // await pageUtils.pressKeys( 'Tab', { times: 2, delay: 50 } ); | ||
| // await expect( closeMenuButton ).toBeFocused(); | ||
| // await pageUtils.pressKeys( 'Shift+Tab', { times: 2, delay: 50 } ); | ||
| // await expect( overlayMenuFirstElement ).toBeFocused(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This tests focus wrapping, i.e. the focus moving either to the first tabbable to the last or vice versa. Since this PR removes the logic for this, focus doesn’t wrap (and tends to move into the browser’s UI). Yet thanks to the modal dialog nothing in the document outside the modal can be focused. I think that suffices and seems more accessible because focus isn’t trapped within the document by wrapping it. I just wanted to highlight this in case someone thinks differently.
I’ll go ahead and push a commit that removes these commented lines.
I did but ended up squashing the commit. I’d done it before hearing from you and mostly because my first commit revised almost everything so it seemed a diff of little interest. |
| openMenuOnHover() { | ||
| const { type, overlayOpenedBy } = getContext(); | ||
| if ( | ||
| type === 'submenu' && | ||
| // Only open on hover if the overlay is closed. | ||
| Object.values( overlayOpenedBy || {} ).filter( Boolean ) | ||
| .length === 0 | ||
| ) { | ||
| const { overlayOpenedBy } = getContext(); | ||
| // Opens only if the overlay is closed. | ||
| if ( Object.values( overlayOpenedBy || {} ).some( Boolean ) ) { | ||
| actions.openMenu( 'hover' ); | ||
| } | ||
| }, | ||
| closeMenuOnHover() { | ||
| const { type, overlayOpenedBy, submenuOpenedBy } = getContext(); | ||
| const { overlayOpenedBy, submenuOpenedBy } = getContext(); | ||
| // Closes only if not opened by click and the overlay is closed. | ||
| if ( | ||
| type === 'submenu' && | ||
| // Close only if not opened by click. | ||
| submenuOpenedBy.click === false && | ||
| // Only close on hover if the overlay is closed. | ||
| Object.values( overlayOpenedBy || {} ).filter( Boolean ) | ||
| .length === 0 | ||
| Object.values( overlayOpenedBy || {} ).some( Boolean ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn’t necessary but should introduce no behavioral differences. The type === 'submenu' conditions were useless because these handlers are only invoked by submenus (not the overlay). The .filter( Boolean ).length === 0 is basically a more verbose version of .some( Boolean ) which replaces it. Other than that this just revises the comments a bit.
|
See also @felixarntz's #63917 (comment):
|
Also add e2e coverage since it was lacking
| // 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| // 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) { | ||
| color: inherit !important; | ||
| background-color: inherit !important; | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had removed these because these properties were set to inherit otherwise and I didn’t realize the !important bit was key. I double-checked and found that out. Removing them hadn’t caused the e2e tests for colors to fail so along with restoring this I added an e2e test that fails without these.
| // 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]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be :open as opposed to [open]? I consulted Gemini who suggests they are functionally equivalent, but then says:
:openis often preferred: It's semantically clearer as it targets the state of the element (being open) rather than the implementation detail (having theopenattribute). This aligns conceptually with other state pseudo-classes like:checked,:disabled, or:focus.
| &:is(.is-menu-open, [open]) { | |
| &:is(.is-menu-open, :open) { |
It's also 1 character shorter 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! I agree :open should be used. Done in 370b204. Our style lint doesn’t like it but ignores work for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It turns out Safari doesn’t yet support :open and that was causing most of the recent e2e test failures. Even besides Safari the versions of Chrome and Firefox that don’t support it are still at usage levels above %1 which means we shouldn’t use it yet. So 966098f switches back to [open].
7c09400 to
6a086f6
Compare
westonruter
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just marking this as changes-requested since we still need to figure out the autofocus behavior when the nav menu is opened with an invoker command: #69720 (comment)
If we change the JS to focus on the close button instead, then we don't need to add an autofocus attribute on the first focusable nav menu item since the browser will handle this by default. However, if for a11y reasons it is preferable to focus on the first nav menu item, then it seems we'll need to clone the nav menu specifically for the DIALOG for use on mobile, with the non-mobile nav menu not needing to be wrapped in a DIALOG. And if we do that, then we'll need to finish the HTML Tag Processor implementation to find additional focusable elements: https://github.com/WordPress/gutenberg/pull/69720/files#r2023568298
| await colorControl.testFrontendColors( expectedNavigationColors ); | ||
| } ); | ||
|
|
||
| test( 'The submenu/overlay colors do not apply to all navigation links unless the overlay is open', async ( { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these changes seem unrelated? (thru line 330)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes these are not directly related. This adds coverage for something I broke while trying to clean up some styles. It’s possible that one of the existing tests could be modified so it would fail instead of adding this new one but I figured this is more explicit.
| * remove this test and only rely on the Overlay Interactions test above and add a (@firefox, @webkit) | ||
| * directive to the describe() statement. https://github.com/WordPress/gutenberg/pull/55198 | ||
| */ | ||
| test( 'Overlay menu interactions in Safari (@webkit)', async ( { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this test no longer work or apply?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test duplicates the test above it. As the comment preceding it indicates, the intention was to avoid flakiness of the test in Safari purportedly caused by Tab key presses. I removed those parts of the test so I followed up with the rest of what the comment recommends.
The reason I removed the assertions for Tab key and focus is because the “focus trapping“ implementation is removed and testing for it is not needed. The “focus trapping” can be removed because the dialog element, as a modal, makes all content outside it inert. Edit: also commented here
Aside: this test was a complete duplicate of the one above it and included Tab key presses so I don’t see how it was actually making any difference for Safari in the first place.
|
Something that just came to mind: Chrome and Edge both support invoker commands, and they are also implemented behind flags in Safari and Firefox (caniuse). Once invoker commands ship in stable versions of Safari and Firefox, should we even keep the Interactivity API implementation here? We could instead ship the invoker commands polyfill, if the current a11y limitation is resolved. There is precedence for this in core as a polyfill for import maps was added in Core-60348. It was removed in Core-60970 after it was determined that all browsers support import maps. We could similarly have a Removing the Interactivity API implementation would reduce maintenance and it would reduce script from being served to the browser, thus improving performance. It's still too early here to do this, as there are other aspects of the Interactivity API which aren't yet able to be replaced with invokers, namely the dropdown menus. These rely on browsers implementing interest invokers. These are just now implemented in an origin trial in Chrome 135, and the PR to add this to the HTML spec is still just a draft. In any case, I just wanted to look ahead for what this implementation could eventually look like. cc @swissspidy |

What?
Alternative to #69665
Closes #69635
Utilizes the
<dialog>element to wrap the navigation content and invokes it as a modal for the overlay.Why?
How?
<div>with<dialog>commandandcommandforattributes on the buttons to open and close the dialogTesting Instructions
#69635 is an issue that is reproducible in Firefox or Safari so test with one of those browsers.
Here is markup for testing, though you’ll have to update the navigation for your site.
Screenshots or screencast
Editor
editor-nav-overlay-before.mp4
editor-nav-overlay-69720.mov
Front end
front-end-nav-overlay-before.mp4
front-end-nav-overlay-69720.mov