Improved the accessibility of mobile quick options
authorAlexander Ebert <ebert@woltlab.com>
Wed, 12 Jan 2022 21:28:28 +0000 (22:28 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Wed, 12 Jan 2022 21:28:28 +0000 (22:28 +0100)
ts/WoltLabSuite/Core/Ui/Mobile.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Mobile.js
wcfsetup/install/files/style/bootstrap/mixin/dropdownMenu.scss

index 3a7bb040fbf995be02315c1436f6b68203e09d36..40c05c5c13c453dce54fa95786348a093ca252de 100644 (file)
@@ -7,6 +7,7 @@
  * @module  WoltLabSuite/Core/Ui/Mobile
  */
 
+import { createFocusTrap, FocusTrap } from "focus-trap";
 import * as Core from "../Core";
 import DomChangeListener from "../Dom/Change/Listener";
 import DomUtil from "../Dom/Util";
@@ -18,12 +19,14 @@ import { PageMenuMain } from "./Page/Menu/Main";
 import { PageMenuMainProvider } from "./Page/Menu/Main/Provider";
 import { hasValidUserMenu, PageMenuUser } from "./Page/Menu/User";
 import * as UiScreen from "./Screen";
+import * as Language from "../Language";
 
 let _dropdownMenu: HTMLUListElement | null = null;
 let _dropdownMenuMessage: HTMLElement | null = null;
 let _enabled = false;
 let _enabledLGTouchNavigation = false;
 let _enableMobileMenu = false;
+let _focusTrap: FocusTrap | undefined = undefined;
 const _knownMessages = new WeakSet<HTMLElement>();
 let _mobileSidebarEnabled = false;
 let _pageMenuMain: PageMenuMain;
@@ -37,6 +40,7 @@ function init(): void {
 
   initButtonGroupNavigation();
   initMessages();
+  initMessagesA11y();
   initMobileMenu();
 
   UiCloseOverlay.add("WoltLabSuite/Core/Ui/Mobile", closeAllMenus);
@@ -84,6 +88,8 @@ function initButtonGroupNavigation(): void {
 }
 
 function initMessages(): void {
+  const isScreenSmDown = UiScreen.is("screen-sm-down");
+
   document.querySelectorAll(".message").forEach((message: HTMLElement) => {
     if (_knownMessages.has(message)) {
       return;
@@ -113,12 +119,65 @@ function initMessages(): void {
             toggleMobileNavigation(message, quickOptions, navigation);
           }
         });
+        quickOptions.addEventListener("keydown", (event) => {
+          if (event.key === "Enter") {
+            event.preventDefault();
+
+            quickOptions.click();
+          }
+        });
+
+        if (isScreenSmDown) {
+          enableMessageA11y(quickOptions);
+        }
       }
     }
     _knownMessages.add(message);
   });
 }
 
+function enableMessageA11y(quickOptions: HTMLElement): void {
+  quickOptions.tabIndex = 0;
+  quickOptions.setAttribute("role", "button");
+  quickOptions.setAttribute("aria-label", Language.get("wcf.global.button.more"));
+}
+
+function disableMessageA11y(quickOptions: HTMLElement): void {
+  quickOptions.removeAttribute("tabindex");
+  quickOptions.removeAttribute("role");
+  quickOptions.removeAttribute("aria-label");
+}
+
+function initMessagesA11y(): void {
+  UiScreen.on("screen-sm-down", {
+    match() {
+      document.querySelectorAll(".message").forEach((message: HTMLElement) => {
+        const navigation = message.querySelector(".jsMobileNavigation") as HTMLAnchorElement;
+        if (navigation) {
+          const quickOptions = message.querySelector(".messageQuickOptions") as HTMLElement;
+          if (quickOptions && navigation.childElementCount) {
+            enableMessageA11y(quickOptions);
+          }
+        }
+      });
+    },
+    unmatch() {
+      document.querySelectorAll(".message").forEach((message: HTMLElement) => {
+        if (!_knownMessages.has(message)) {
+          return;
+        }
+
+        const navigation = message.querySelector(".jsMobileNavigation") as HTMLAnchorElement;
+        if (navigation) {
+          const quickOptions = message.querySelector(".messageQuickOptions") as HTMLElement;
+          if (quickOptions && navigation.childElementCount) {
+            disableMessageA11y(quickOptions);
+          }
+        }
+      });
+    },
+  });
+}
 function initMobileMenu(): void {
   if (_enableMobileMenu) {
     _pageMenuMain = new PageMenuMain(_pageMenuMainProvider);
@@ -175,6 +234,9 @@ function toggleMobileNavigation(message: HTMLElement, quickOptions: HTMLElement,
     UiDropdownReusable.init("com.woltlab.wcf.jsMobileNavigation", _dropdownMenu);
   } else if (_dropdownMenu.classList.contains("dropdownOpen")) {
     closeDropdown();
+    _focusTrap!.deactivate();
+    _focusTrap = undefined;
+
     if (_dropdownMenuMessage === message) {
       // toggle behavior
       return;
@@ -196,6 +258,16 @@ function toggleMobileNavigation(message: HTMLElement, quickOptions: HTMLElement,
   });
   _dropdownMenu.classList.add("dropdownOpen");
   _dropdownMenuMessage = message;
+
+  _focusTrap = createFocusTrap(_dropdownMenu, {
+    allowOutsideClick: true,
+    escapeDeactivates(): boolean {
+      toggleMobileNavigation(message, quickOptions, navigation);
+
+      return false;
+    },
+  });
+  _focusTrap.activate();
 }
 
 function setupLGTouchNavigation(): void {
index 2b67e2920b41840d05bda4edc95a0fcd5ba4d695..7c03e7e3d2063aa4dbabacf14f615f2a7ed90272 100644 (file)
@@ -6,7 +6,7 @@
  * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @module  WoltLabSuite/Core/Ui/Mobile
  */
-define(["require", "exports", "tslib", "../Core", "../Dom/Change/Listener", "../Dom/Util", "../Environment", "./Alignment", "./CloseOverlay", "./Dropdown/Reusable", "./Page/Menu/Main", "./Page/Menu/User", "./Screen"], function (require, exports, tslib_1, Core, Listener_1, Util_1, Environment, UiAlignment, CloseOverlay_1, UiDropdownReusable, Main_1, User_1, UiScreen) {
+define(["require", "exports", "tslib", "focus-trap", "../Core", "../Dom/Change/Listener", "../Dom/Util", "../Environment", "./Alignment", "./CloseOverlay", "./Dropdown/Reusable", "./Page/Menu/Main", "./Page/Menu/User", "./Screen", "../Language"], function (require, exports, tslib_1, focus_trap_1, Core, Listener_1, Util_1, Environment, UiAlignment, CloseOverlay_1, UiDropdownReusable, Main_1, User_1, UiScreen, Language) {
     "use strict";
     Object.defineProperty(exports, "__esModule", { value: true });
     exports.removeShadow = exports.rebuildShadow = exports.disableShadow = exports.disable = exports.enableShadow = exports.enable = exports.setup = void 0;
@@ -18,11 +18,13 @@ define(["require", "exports", "tslib", "../Core", "../Dom/Change/Listener", "../
     CloseOverlay_1 = (0, tslib_1.__importDefault)(CloseOverlay_1);
     UiDropdownReusable = (0, tslib_1.__importStar)(UiDropdownReusable);
     UiScreen = (0, tslib_1.__importStar)(UiScreen);
+    Language = (0, tslib_1.__importStar)(Language);
     let _dropdownMenu = null;
     let _dropdownMenuMessage = null;
     let _enabled = false;
     let _enabledLGTouchNavigation = false;
     let _enableMobileMenu = false;
+    let _focusTrap = undefined;
     const _knownMessages = new WeakSet();
     let _mobileSidebarEnabled = false;
     let _pageMenuMain;
@@ -34,6 +36,7 @@ define(["require", "exports", "tslib", "../Core", "../Dom/Change/Listener", "../
         _enabled = true;
         initButtonGroupNavigation();
         initMessages();
+        initMessagesA11y();
         initMobileMenu();
         CloseOverlay_1.default.add("WoltLabSuite/Core/Ui/Mobile", closeAllMenus);
         Listener_1.default.add("WoltLabSuite/Core/Ui/Mobile", () => {
@@ -73,6 +76,7 @@ define(["require", "exports", "tslib", "../Core", "../Dom/Change/Listener", "../
         });
     }
     function initMessages() {
+        const isScreenSmDown = UiScreen.is("screen-sm-down");
         document.querySelectorAll(".message").forEach((message) => {
             if (_knownMessages.has(message)) {
                 return;
@@ -97,11 +101,59 @@ define(["require", "exports", "tslib", "../Core", "../Dom/Change/Listener", "../
                             toggleMobileNavigation(message, quickOptions, navigation);
                         }
                     });
+                    quickOptions.addEventListener("keydown", (event) => {
+                        if (event.key === "Enter") {
+                            event.preventDefault();
+                            quickOptions.click();
+                        }
+                    });
+                    if (isScreenSmDown) {
+                        enableMessageA11y(quickOptions);
+                    }
                 }
             }
             _knownMessages.add(message);
         });
     }
+    function enableMessageA11y(quickOptions) {
+        quickOptions.tabIndex = 0;
+        quickOptions.setAttribute("role", "button");
+        quickOptions.setAttribute("aria-label", Language.get("wcf.global.button.more"));
+    }
+    function disableMessageA11y(quickOptions) {
+        quickOptions.removeAttribute("tabindex");
+        quickOptions.removeAttribute("role");
+        quickOptions.removeAttribute("aria-label");
+    }
+    function initMessagesA11y() {
+        UiScreen.on("screen-sm-down", {
+            match() {
+                document.querySelectorAll(".message").forEach((message) => {
+                    const navigation = message.querySelector(".jsMobileNavigation");
+                    if (navigation) {
+                        const quickOptions = message.querySelector(".messageQuickOptions");
+                        if (quickOptions && navigation.childElementCount) {
+                            enableMessageA11y(quickOptions);
+                        }
+                    }
+                });
+            },
+            unmatch() {
+                document.querySelectorAll(".message").forEach((message) => {
+                    if (!_knownMessages.has(message)) {
+                        return;
+                    }
+                    const navigation = message.querySelector(".jsMobileNavigation");
+                    if (navigation) {
+                        const quickOptions = message.querySelector(".messageQuickOptions");
+                        if (quickOptions && navigation.childElementCount) {
+                            disableMessageA11y(quickOptions);
+                        }
+                    }
+                });
+            },
+        });
+    }
     function initMobileMenu() {
         if (_enableMobileMenu) {
             _pageMenuMain = new Main_1.PageMenuMain(_pageMenuMainProvider);
@@ -151,6 +203,8 @@ define(["require", "exports", "tslib", "../Core", "../Dom/Change/Listener", "../
         }
         else if (_dropdownMenu.classList.contains("dropdownOpen")) {
             closeDropdown();
+            _focusTrap.deactivate();
+            _focusTrap = undefined;
             if (_dropdownMenuMessage === message) {
                 // toggle behavior
                 return;
@@ -172,6 +226,14 @@ define(["require", "exports", "tslib", "../Core", "../Dom/Change/Listener", "../
         });
         _dropdownMenu.classList.add("dropdownOpen");
         _dropdownMenuMessage = message;
+        _focusTrap = (0, focus_trap_1.createFocusTrap)(_dropdownMenu, {
+            allowOutsideClick: true,
+            escapeDeactivates() {
+                toggleMobileNavigation(message, quickOptions, navigation);
+                return false;
+            },
+        });
+        _focusTrap.activate();
     }
     function setupLGTouchNavigation() {
         _enabledLGTouchNavigation = true;
index 5f4eb159cda57f7d34ceca02aeaa154b72e677d7..85ae4770edfbfc72b91f3f2bd47c5fd9f1234660 100644 (file)
@@ -26,6 +26,7 @@
                display: block;
 
                &:hover:not(.dropdownDivider):not(.dropdownList):not(.dropdownText),
+               &:focus-within,
                &.dropdownList > li:hover:not(.dropdownDivider),
                &.dropdownNavigationItem,
                &.active {