Updated as of #66
This issue is a blocker for putting Navigation into the Standard profile.
Problem/Motivation
When using the mobile version of the navigation, it is possible to tab through the admin navigation, and into the content of the page.
This violates WCAG 2.4.7 focus visible, because the content item that has focus will be obscured by the navigation menu.
Steps to reproduce
- Enable the Navigation module.
- Log in as admin.
- Reduce the viewport width until the Navigation "Expand sidebar" button appears at top-left.
- Activate the "Expand sidebar" button to open the sidebar.
- Press the
TABkey until the focus moves off the end of the sidebar.
Expected behavior
After moving off the end of the sidebar, the focus moves off the page onto the browser UI.
Actual behavior
The focus continues to move through elements in the page content, some of which are hidden by the sidebar.
Proposed resolution
- When the "mobile" Navigation sidebar is expanded, add
inertto elements outside of the sidebar. - When the sidebar is closed, remove
inertfrom those elements.
Remaining tasks
- Screenshots for issue summary.
- Maintainer review.
- #3572169: Put the mobile sidebar close button into the TAB sequence
- #3572628: Mobile sidebar should close by ESCAPE key
- Tests.
User interface changes
Elements outside of the Navigation sidebar will be inert and not in the tab sequence.
Introduced terminology
API changes
Data model changes
Release notes snippet
| Comment | File | Size | Author |
|---|---|---|---|
| #72 | Cursor_and_admin-toolbar-wrapper_js_—_drupal_and_mikeherchel_macbookpro___Sites_drupal_core_and_Welcome____Drush_Site-Install.png | 473.63 KB | mherchel |
| #55 | aggregation.mp4 | 784.84 KB | rkoller |
| #53 | arrows.mp4 | 67.04 KB | rkoller |
| #50 | menu.mp4 | 223.77 KB | rkoller |
| #45 | no_trap.mp4 | 275.46 KB | rkoller |
Issue fork drupal-3443571
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
mherchelComment #3
ckrinaComment #4
ckrinaComment #5
katannshaw commentedI agree with mherchel that when a user enters the mobile nav, keyboard focus must be trapped within it until the user:
* Selects the Close/OK/X button
* Selects the ESC key
* Clicks outside of the menu with their mouse
However I disagree on the WCAG success criteria that this violates. I believe that this violates 2.1.2 No Keyboard Trap (Level A). Under an example in this SC titled "A modal dialog box", it mentions:
The WCAG 2.4.7 Focus Visible (Level AA) SC refers to the visible focus indicator for focusable elements.
I like Mike's idea.
Comment #7
finnsky commentedProbably it is possible to merge that core/misc/focus-trap.js logic with existing arrow navigation:
https://git.drupalcode.org/project/drupal/-/blob/11.x/core/modules/navig...
Or move it on core level out of module.
Comment #8
mherchelYeah, this is my thought. In the MR above, I abstracted it to be on the core level, and then make use of it in both navigation and olivero (to show that it can be used in multiple places).
Comment #9
mherchelComment #10
finnsky commentedimo it should be possible to ignore desktop somehow.
Comment #11
mherchelYeah, that makes sense. Should be doable!
Comment #12
nod_The existing tabbingmanager feature has tests, this one should too
Comment #13
kentr commentedI believe this would be SC 2.4.11 Focus Not Obscured (Minimum) (Level AA). It directly addresses user openable, persistent disclosures and uses slide-out navigation as an example.
Comment #14
mgiffordTagging
Comment #15
catchComment #17
smustgrave commentedCan I ask to help get this through faster that maybe we move the olivero change to a follow up?
Comment #18
kentr commentedRebased.
Comment #19
cwilcox808 commentedIf the "mobile" navigation should be a modal dialog, I think adding keyboard listeners are no longer the way to implement it.
- Hijacking Tab, Shift-Tab prevents keyboard users from navigating to the browser UI while the navigation is open.
- Keyboard listeners don't prevent screen reader users from navigating beyond the navigation.
These were always problems with the keyboard listeners but the alternative was more challenging to implement.
Instead, the mobile navigation should be contained within a
<dialog>element that's opened using.showModal(). The dialog will be modal, preventing access to the rest of the page (including by screen readers), while not preventing keyboard access to the browser UI. It closes when the ESC key is pressed or when a close button is used. The close button does not have to be scripted:To close the dialog when a pointer clicks outside of it (aka "light dismiss"), you can use
<dialog closedby="any">. closedby is new and not supported in all browsers but it doesn't take many lines of JavaScript to check for support and implement a fallback.However the dialog is closed, focus will automatically return to the button that opened it.
I would go even further and propose implementing it using the
show-modalInvoker command instead, using JavaScript as a fallback in browsers that don't support them (or add the full Invoker command polyfill as a resource any module can use).Comment #20
mherchelYeah, we don't need to include Olivero here. Back when I posted that I was thinking it would be nice to have an API for that.
Comment #21
smustgrave commentedSo this is going to be our first "a11y-bugsmash" ONE!
Starting the triage
Can we update the summary to use the standard template. Sounds like agreement for the solution is to keep the scope to the navigation module and only trigger the trap on mobile? Someone correct me if I'm wrong.
Also if possible could we get small gifs of the trap before/after?
For test coverage a javascript test could be added or maybe even just expanding an existing one. Something like
put focus at the top
Doing a loop based on the number of elements (would have to know that) press the tab key
verify focus is still on the top.
Comment #23
mherchelgonna mess around with this for a bit.
Comment #24
mherchelThis was easier than expected... until I tried to create a new branch. Posted the issue in Drupal Slack #gitlab at https://drupal.slack.com/archives/CGKLP028K/p1770330716822829
In the meantime, I'm attaching a patch like it's 2017! 😎
Comment #25
mherchelSetting back to NW. Its setting inert at wide widths too.
Will get back to this tomorrow.
Comment #28
kentr commented@mherchel:
With the patch in #25, the "Skip to main content" link and the "Expand sidebar" button are still focusable when the sidebar is open. So, the focus disappears for a bit when tabbing through them.
Is this expected?
Comment #29
kentr commentedI was mistaken. The skip link isn't focusable. But the hidden Expand sidebar button is.
After that, the next item in the ring is the Home Drupalicon button at top-left of sidebar, but it doesn't have a focus indicator.
To me, the Expand sidebar button shouldn't be tab-able when the sidebar is open, because it's hidden and there is a close button.
I also think the sidebar should be closable with the
ESCAPEkey and that the close button should be tab-able. But those are separate issues.Comment #30
kentr commentedThe three vertical dots menu toggle button (top-right of screen) is also tab-able with the sidebar open. Seems like it shouldn't be.
Comment #31
mherchelGood catches. Will look at this!
Comment #33
smustgrave commentedJust going to chime in if we can still update the summary and track open questions in th remaining tasks section? May help others
Comment #34
mherchelOK. Got the MR working and pushed fix so it won't allow the expand toolbar button to be focused.
Note that there's a
tabindex="-1"on the close button at mobile. Its being added incore/modules/navigation/layouts/navigation.html.twigThis means there's no way to close (that I can find) via keyboard. We'll need a followup for this
Comment #35
mherchelNote that tests are failing because the filesize of the JS changed. I'll fix that once I get signoff from maintainers on the fix.
We'll also need to write some nightwatch tests to cover this. I haven't done that in years 😬
Comment #36
rkollerhm i've checked out MR14669 and quickly tested on safari 26.2 on macOS 26.2 as well as latest edge and in each case the focus wasnt trapped within the mobile navigation.
and yep the close button should be reachable by keyboard same as there should be an option to close the mobile navigation by pressing the ESC key. but from my perspective if moved to a follow up those followup(s) and this issue should go into the same new version of drupal.
Comment #37
kentr commentedYeah, I realized this after the fact.
So that we don't cause a regression (removing the only way to close by keyboard), would it be in scope to change the close button in this issue?
Comment #38
kentr commentedI'm working on updating the IS.
I think this can be tested with functional javascript.
Comment #39
kentr commentedUpdated the IS, but still needs screenshots.
Removed Portland2024 tag.
Comment #40
mherchelDid you clear your cache? You're able to focus on elements outside of the navigation? Can you post a video?
Comment #41
mherchelI just tested Safari, and it seems to be working for me. I'd like to know what elements you're able to focus on too.
Comment #42
cwilcox808 commentedI made a prototype to demonstrate the approach I've suggested. It uses a modal dialog when the navigation should cover the page. When the viewport is wide enough, it's non-modal and beside the page.
Comment #43
kentr commentedThere's an effort to use native dialogs throughout core: #2158943: Add a native dialog element to deprecate the jQuery UI dialog.
I wonder if whatever we do with
dialoghere needs to be coordinated with that effort.Comment #44
kentr commented@cwilcox808 Nice prototype!
Maybe what we do here will influence that global effort 🙂.
Comment #45
rkollerre #41 & #42 recorded one video (no_trap.mp4) clearing the cache again and then tabbing through the interface and recorded a second video (rotor.mp4) with the rotor. the latter illustrates that everything is available in the AOM and therefore in consequence also in the rotor.
Comment #46
smustgrave commentedFunny I tested the MR and while tabbing I ended up out of the webpage and tabbing the actual development bar in the browser
Comment #47
mherchel@rkoller what browser are you using? I don't know how rotors operate, but this patch doesn't remove anything from the AOM, it just prevents tabbing. Are you able to tab outside if using the keyboard?
I'm curious what happens if you test out Olivero's mobile menu with the rotor. In there, we listen for the tab event and redirect it to the beginning/end as necessary (
inertwasn't a thing when that was written).Comment #48
rkolleri am on tahoe 26.2, tested in safari 26.2, and the latest firefox developer edition, and the latest version of edge. in all of them the same result i am able to tab out. and no_trap.mp4 illustrated that, there i'Ve tabbed out with the keyboard.
and testing out oliveros mobile menu, you mean go to for example the front page in olivero? that is what i actually did in the no_trap.mp4 (the behavior on a claro page is identical)
Comment #49
mherchel@rkoller
Comment #50
rkolleruh now i'Ve realized what you meant with the olivero menu, i was completely on the wrong train of thought. recorded another video (menu.mp4). i've activated keycastr. that way you are able to see which keystrokes were entered. i was only tabbing through the page with a mobile viewport. first opening the navigation sidebar then tabbing on, tabbing out of the navigation sidebar and then when reaching the menu button for olivero opening it and then tabbing within the olivero menu. there in contrast to the navigation sidebar the focus is trapped. hope that helps.
update: also tested the same approach in the latest safari technology preview, firefox developer edition, and edge, and in all three the same behavior like illustrated in the menu.mp4 video for safari 26.2
Comment #51
mherchelIt looks like you're hitting the arrow-right key instead of tabbing. Am I looking at this correctly?
Comment #52
mherchelAlso, I see youre hitting CMD+R. What is that?
I'm trying to figure out how to reproduce.
Comment #53
rkollernope that is the tab key in keycastr,. arrows would look like that: arrows.mp4
and cmd-r is simply reloading the page so the tabbing starts from the top of the DOM.
Comment #54
mherchelGotcha. I'm still confused why this wouldn't be working for you.
Comment #55
rkolleri guess i found one interesting detail, checkout aggregation.mp4 (recorded in edge)
first with the aggregation turned on (that was the case for me in previous videos) when i go to the front page and tab i tab through the navigation sidebar and onto the front page and am able to to expand also the olivero nav. then i go back to the aggregation page and deactivate aggregation. when i then go to the front page the focus is trap. but as you notice i experience the same like @smustgrave in #46, when the end of the navigation sidebar is reached instead of jumping directly to the top of the sidebar again the tab order takes a detour to the toolbar of the browser.
Comment #56
kentr commentedFrom my interpretation of #19, this is preferred.
Sounds like we don't want to prevent users from getting to the browser UI via keyboard, and that we don't really want a "trap".
Comment #57
mherchelYeah thats perfectly fine. The point is to not to be able to focus elements that are currently hidden.
You likely needed to clear your cache after enabling aggregation. This makes a lot more sense.
Comment #58
kentr commentedFollowup for the Close button: #3572169: Put the mobile sidebar close button into the TAB sequence
Comment #59
kentr commentedTIL we're not supposed to remove event-related tags when the event is over, unless no one from the event actually worked on the issue.
Comment #61
smustgrave commentedSo seems like this is working as advertised right? Next step would be to get test coverage then. I'm not familiar enough with nightwatch to try that but feel javascript test should be possible too right?
Comment #62
finnsky commentedI think my cat wanted to join the Drupal community.
Comment #64
kentr commentedNot sure that the fully native
dialogimplementation meets Drupal's supported browser requirements, though.At least,
closedbyisn't supported on Safari.Comment #65
mherchelPosted question about using
inertvia redirecting tabs at https://web-a11y.slack.com/archives/C042TSFGN/p1770743434010279Comment #66
mgiffordWe have to resolve this. This is something that we consider a blocker.
Comment #67
kentr commentedAdded #3572169: Put the mobile sidebar close button into the TAB sequence and #3572628: Mobile sidebar should close by ESCAPE key to the Remaining tasks so that they don't fall through the cracks.
Don't we also need to move focus to the sidebar when it is opened?
Comment #68
mherchelJust pushed some Nightwatch tests.
Disclosure. I used the AI tool Claude Code to write the majority of the test. I then reviewed every line, made some changes, and also ran prettier.
I verified that the tests pass locally, and that if I uncomment out the focus trap JavaScript, the tests fail as expected.
I'm expecting these tests to fail, because of the changes in the JS size. I'll update that test shortly.
Comment #69
mherchelAlright. I updated the ScriptBytes in the tests to account for the additional JS. We should be good.
Comment #70
cwilcox808 commented#64
closedbyand Invoker commands are not supported by all browsers that meet Drupal's system requirements but polyfills will make them work in browsers that lack native support. (Popovers are supported in all the browsers.)Because the alternatives all require JavaScript to function, I would expect that including some JavaScript temporarily, until all supported browsers natively support the features, would be acceptable.
My prototype also required JavaScript to fully support the desired behavior but even if no JavaScript is run, the navigation can still be accessed in all supported browsers.
Comment #71
rkollerfor reference. the issues i had in safari were due to some caching issues. a suggestion by @catch on slack solved the problem. by deleting
files/cssandfiles/cssthe focus remained trapped now consistently in safari, firefox and edge. the sole other nitpick i've raised on slack before, that the focus when it gets into the safari ui aka the addressbar was not visible is also obsolete. after the removal of those two directories and the caches newly generated it is also clearly visible now. so from a functional testing perspective this looks good. and also taking into consideration that the feedback on the a11y slack was also positive and confirming in regard of the approach taken here.Comment #72
mherchel@smustgrave asked me to show the test failures when the JS isn't there. Screenshot and test output below!
Comment #73
smustgrave commentedThis is perfect! thanks @mherchel!
Manually testing I see that I do tab through the entire menu on mobile, as mentioned the sequence does go into the browser inspector, but comes back around to the mobile menu. So I can't get to any content on the page (as expected)
LGTM!
Comment #74
nod_few things
Comment #75
mherchelResponded to @nod_'s comment in Gitlab
Comment #76
smustgrave commentedBelieve feedback from @nod_ has been addressed
Comment #77
nod_not quite yet :)
Comment #78
mherchelAddressed @nod_'s latest comments. MR updated. Should be good to go :)
Comment #79
smustgrave commentedManually testing this still seems to work as advertised.
Comment #80
lauriiiCommitted! Thank you for everyone who worked on this 🙌
Comment #85
godotislateI think this has broken
Drupal\Tests\demo_umami\FunctionalJavascript\AssetAggregationAcrossPagesTeston HEAD:https://git.drupalcode.org/project/drupal/-/jobs/8558724
Comment #87
godotislateFix: https://git.drupalcode.org/project/drupal/-/merge_requests/14826
Comment #89
catchCommitted/pushed the test update, thanks!