From d2e8a95e3cccd58d7d08be9692a5e6ed5945b2af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20I=C3=9Fbr=C3=BCcker?= Date: Sat, 11 Jan 2025 12:44:20 +0100 Subject: [PATCH] Fix menu dropdown focus traps (#944) --- bookmarks/frontend/behaviors/dropdown.js | 29 +++++++++++++++++++++ bookmarks/styles/theme/dropdowns.css | 12 ++++++--- bookmarks/templates/bookmarks/nav_menu.html | 6 ++--- bookmarks/templates/bookmarks/search.html | 3 ++- bookmarks/tests/test_bookmark_search_tag.py | 19 +++++++------- 5 files changed, 52 insertions(+), 17 deletions(-) diff --git a/bookmarks/frontend/behaviors/dropdown.js b/bookmarks/frontend/behaviors/dropdown.js index 954ced8..bc4c580 100644 --- a/bookmarks/frontend/behaviors/dropdown.js +++ b/bookmarks/frontend/behaviors/dropdown.js @@ -6,23 +6,38 @@ class DropdownBehavior extends Behavior { this.opened = false; this.onClick = this.onClick.bind(this); this.onOutsideClick = this.onOutsideClick.bind(this); + this.onEscape = this.onEscape.bind(this); + this.onFocusOut = this.onFocusOut.bind(this); + + // Prevent opening the dropdown automatically on focus, so that it only + // opens on click then JS is enabled + this.element.style.setProperty("--dropdown-focus-display", "none"); + this.element.addEventListener("keydown", this.onEscape); + this.element.addEventListener("focusout", this.onFocusOut); this.toggle = element.querySelector(".dropdown-toggle"); + this.toggle.setAttribute("aria-expanded", "false"); this.toggle.addEventListener("click", this.onClick); } destroy() { this.close(); this.toggle.removeEventListener("click", this.onClick); + this.element.removeEventListener("keydown", this.onEscape); + this.element.removeEventListener("focusout", this.onFocusOut); } open() { + this.opened = true; this.element.classList.add("active"); + this.toggle.setAttribute("aria-expanded", "true"); document.addEventListener("click", this.onOutsideClick); } close() { + this.opened = false; this.element.classList.remove("active"); + this.toggle.setAttribute("aria-expanded", "false"); document.removeEventListener("click", this.onOutsideClick); } @@ -39,6 +54,20 @@ class DropdownBehavior extends Behavior { this.close(); } } + + onEscape(event) { + if (event.key === "Escape" && this.opened) { + event.preventDefault(); + this.close(); + this.toggle.focus(); + } + } + + onFocusOut(event) { + if (!this.element.contains(event.relatedTarget)) { + this.close(); + } + } } registerBehavior("ld-dropdown", DropdownBehavior); diff --git a/bookmarks/styles/theme/dropdowns.css b/bookmarks/styles/theme/dropdowns.css index 07a1715..1d58ac9 100644 --- a/bookmarks/styles/theme/dropdowns.css +++ b/bookmarks/styles/theme/dropdowns.css @@ -1,5 +1,7 @@ /* Dropdown */ .dropdown { + --dropdown-focus-display: block; + display: inline-block; position: relative; @@ -20,9 +22,13 @@ } } - &.active .menu, - .dropdown-toggle:focus + .menu, - .menu:hover { + &:focus-within .menu { + /* Use custom CSS property to allow disabling opening on focus when using JS */ + display: var(--dropdown-focus-display); + } + + &.active .menu { + /* Always show menu when class is added through JS */ display: block; } diff --git a/bookmarks/templates/bookmarks/nav_menu.html b/bookmarks/templates/bookmarks/nav_menu.html index e817361..0667fbc 100644 --- a/bookmarks/templates/bookmarks/nav_menu.html +++ b/bookmarks/templates/bookmarks/nav_menu.html @@ -3,11 +3,11 @@ {# Basic menu list #}
Add bookmark -