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:

  1. Replaces the mouse-only hover library with the Accessible Menu Treeview widget (v4.4.0), which implements the full ARIA Treeview keyboard interaction model
  2. Works in both vertical (accordion) and horizontal (dropdown flyout) toolbar orientations using the same widget — hoverType: 'off' for vertical, 'on' for horizontal
  3. 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 listeners
  • css/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) and toolbar.tree.accessible_menu library definitions
  • admin_toolbar.module — Updates hook_toolbar_alter() with a switch on hover_behavior; each mode loads its own library and passes its own timeout to drupalSettings
  • admin_toolbar.settings.yml — Adds hover_behavior: 'hoverintent' (backward-compatible default), hoverintent_behavior.timeout: 500, accessible_menu_behavior.timeout: 250
  • admin_toolbar.schema.yml — Adds hover_behavior enum (hover|hoverintent|accessible_menu) and accessible_menu_behavior mapping with timeout integer
  • admin_toolbar.install — Adds admin_toolbar_update_8006() to migrate old boolean hoverintent_behavior.enabled → new string hover_behavior
  • src/Form/AdminToolbarSettingsForm.php — Replaces old hoverintent checkbox with a three-option radio group; adds two conditional timeout selects (one per mode) with #states visibility; fixes form nesting (#tree => TRUE) and submitForm() to use array-based getValue() paths
  • tests/src/Functional/AdminToolbarSettingsFormTest.php — Updates existing test field paths for new form structure; adds a new test section covering the accessible_menu mode

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: 0 in 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 (accessibleMenuExpandcloseSiblings())
  • Recursive close: nested submenus close when parent closes (accessibleMenuCollapsecloseChildren())

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.install

Manual 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's hoverType is not updated if the user switches toolbar orientation after page load. The initial scan is deferred via requestAnimationFrame to avoid reading a transient orientation state on page load.
  • The focusin handler in admin_toolbar.js still runs (intentionally), adding .hover-intent during keyboard navigation. A scoped CSS override suppresses the resulting visual conflict.
  • Focus outline color contrast has not been formally audited against WCAG AA.

  • #3526087 — Use the Accessible Menu library to manage the menu structure (this MR)
  • #3286466 — Tabbing order does not satisfy 508 accessibility requirements

Merge request reports

Loading