Add Accessible Menu (WAI-ARIA Treeview) as a third hover behavior option
Summary
This MR implements #3526087 — integrating the Accessible Menu library's Treeview widget as an opt-in, WAI-ARIA-compliant third hover behavior option for admin_toolbar, sitting alongside the existing Default Hover and HoverIntent modes.
The feature is entirely opt-in. Existing sites are not affected. The default hover behavior remains hoverintent.
Problem
The admin toolbar's horizontal flyout menus have no keyboard or screen reader support beyond what the browser provides natively. Users navigating by keyboard can reach top-level items via Tab, but opening and closing nested flyouts requires mouse interaction. This fails WCAG 2.1 Success Criterion 2.1.1 (Keyboard) and does not implement the WAI-ARIA Authoring Practices Guide Navigation Treeview pattern.
Solution
A new accessible_menu mode is added to the existing hover_behavior settings field. When selected, it:
- Replaces the mouse-only hover library with the Accessible Menu Treeview widget (v4.4.0), which implements the full ARIA Treeview keyboard interaction model
- Works in both vertical (accordion) and horizontal (dropdown flyout) toolbar orientations using the same widget —
hoverType: 'off'for vertical,'on'for horizontal - Provides instant open / configurable delayed close on hover: dropdowns open immediately on mouse-over; closing after mouse-out is delayed by a configurable timeout (250–2000ms) to prevent accidental dismissals
Changes
New files
js/admin_toolbar.accessible_menu.js— Treeview widget initialization, event listenerscss/admin_toolbar.accessible_menu.css— Dropdown positioning, float reset, hover-intent bleed-through fix, focus styling
Modified files
- composer.json — Adds
npm-asset/accessible-menu: ^4.4.0(installed via asset-packagist to accessible-menu) - admin_toolbar.libraries.yml — Adds
accessible_menu_library(external) andtoolbar.tree.accessible_menulibrary definitions admin_toolbar.module— Updateshook_toolbar_alter()with aswitchonhover_behavior; each mode loads its own library and passes its own timeout todrupalSettingsadmin_toolbar.settings.yml— Addshover_behavior: 'hoverintent'(backward-compatible default),hoverintent_behavior.timeout: 500,accessible_menu_behavior.timeout: 250admin_toolbar.schema.yml— Addshover_behaviorenum (hover|hoverintent|accessible_menu) andaccessible_menu_behaviormapping withtimeoutintegeradmin_toolbar.install— Addsadmin_toolbar_update_8006()to migrate old booleanhoverintent_behavior.enabled→ new stringhover_behaviorsrc/Form/AdminToolbarSettingsForm.php— Replaces old hoverintent checkbox with a three-option radio group; adds two conditional timeout selects (one per mode) with#statesvisibility; fixes form nesting (#tree => TRUE) andsubmitForm()to use array-basedgetValue()pathstests/src/Functional/AdminToolbarSettingsFormTest.php— Updates existing test field paths for new form structure; adds a new test section covering theaccessible_menumode
Keyboard Interaction
When Accessible Menu mode is active, the toolbar tray becomes a single WAI-ARIA Treeview composite widget (role="tree", roving tabindex):
| Key | Behavior |
|---|---|
| Tab | Enter / exit the tree |
| ↓ / ↑ | Move between items |
| → | Open submenu or move into first child |
| ← | Close submenu or move back to parent |
| Enter / Space | Open/close submenu or follow link |
| Home / End | Jump to first / last item |
| a–z | Jump to next matching item |
| * | Expand all siblings |
Mouse Interaction
- Hover in: Dropdown opens instantly (
hoverDelay: 0in the Treeview constructor) - Hover out: Dropdown closes after a configurable delay (default 250ms, set at
/admin/config/user-interface/admin-toolbar); moving the pointer back in before the timer fires cancels the close - Sibling exclusivity: hovering a new top-level item closes the previous one (
accessibleMenuExpand→closeSiblings()) - Recursive close: nested submenus close when parent closes (
accessibleMenuCollapse→closeChildren())
Technical note: hover close delay implementation
The Accessible Menu library registers its own pointerleave handler on the root menu element during new Treeview(). With hoverDelay: 0, this would close everything instantly on mouse-out, overriding any delay we set separately. The fix exploits event listener registration order: our pointerleave listener is registered before new Treeview(), so it fires first and calls event.stopImmediatePropagation() to prevent the library's handler from running. Our handler then sets a setTimeout of closeDelay ms:
// 1. Register BEFORE Treeview init
menuElement.addEventListener('pointerleave', (event) => {
event.stopImmediatePropagation();
closeTimeoutId = setTimeout(() => {
treeview.closeChildren();
treeview.blur();
}, closeDelay);
});
// 2. Create widget (its pointerleave is now blocked)
treeview = new window.AccessibleMenu.Treeview({ hoverDelay: 0, ... });Configuration
Settings are at /admin/config/user-interface/admin-toolbar. No configuration changes are needed for existing sites — the default remains hoverintent.
For new accessible_menu mode installs: the Accessible Menu library is automatically installed via Composer (npm-asset/accessible-menu: ^4.4.0). The settings form shows an installation warning if the library file is not found at libraries/accessible-menu/dist/accessible-menu.iife.js.
Testing
# Run the updated functional test
vendor/bin/phpunit web/modules/contrib/admin_toolbar/tests/src/Functional/AdminToolbarSettingsFormTest.php
# Expected: 1 test, 10 assertions, OK
# Code standards
vendor/bin/phpcs --standard=Drupal --extensions=php,module,install,yml \
web/modules/contrib/admin_toolbar/src \
web/modules/contrib/admin_toolbar/admin_toolbar.module \
web/modules/contrib/admin_toolbar/admin_toolbar.installManual testing checklist:
- Set hover behavior to "Accessible Menu" and save
- Verify hover-to-open is instant, hover-out-to-close is delayed
- Change timeout value, verify it takes effect
- Navigate with keyboard only (Tab → arrow keys → Enter)
- Verify nested submenu closes recursively when parent closes
- Verify focus ring is removed on mouse re-entry
- Switch back to HoverIntent — verify it works normally
- Switch to Default Hover — verify it works normally
Known Limitations
- The Accessible Menu library has no public
destroy()API, so the widget'shoverTypeis not updated if the user switches toolbar orientation after page load. The initial scan is deferred viarequestAnimationFrameto avoid reading a transient orientation state on page load. - The
focusinhandler inadmin_toolbar.jsstill runs (intentionally), adding.hover-intentduring keyboard navigation. A scoped CSS override suppresses the resulting visual conflict. - Focus outline color contrast has not been formally audited against WCAG AA.