Use native buttons for the mobile page header
authorAlexander Ebert <ebert@woltlab.com>
Tue, 23 Aug 2022 15:43:01 +0000 (17:43 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Tue, 23 Aug 2022 15:43:01 +0000 (17:43 +0200)
com.woltlab.wcf/templates/pageHeaderMenu.tpl
com.woltlab.wcf/templates/pageHeaderUser.tpl
ts/WoltLabSuite/Core/Ui/Page/Menu/Main.ts
ts/WoltLabSuite/Core/Ui/Page/Menu/User.ts
wcfsetup/install/files/acp/templates/pageHeaderMenu.tpl
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Menu/Main.js
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Menu/User.js
wcfsetup/install/files/style/layout/pageHeader.scss
wcfsetup/install/files/style/ui/pageMenu.scss

index bb79215d9688f97f575452adc34e6fd6fcdc1ef8..c71cd3f24a30a0418455c8957272a7253beb08e9 100644 (file)
@@ -1 +1,9 @@
 {@$__wcf->getBoxHandler()->getBoxByIdentifier('com.woltlab.wcf.MainMenu')->render()}
+<button class="pageHeaderMenuMobile" aria-expanded="false" aria-label="{lang}wcf.menu.page{/lang}">
+       <span class="pageHeaderMenuMobileInactive">
+               {icon size=32 name='bars'}
+       </span>
+       <span class="pageHeaderMenuMobileActive">
+               {icon size=32 name='xmark'}
+       </span>
+</button>
index 3326fa39692f2e1acbfb87d2f08fbc5a45f6a76d..586a7a5ddf029664d18c23eb29f45609188c6212 100644 (file)
@@ -1,12 +1,4 @@
 <nav id="topMenu" class="userPanel{if $__wcf->user->userID} userPanelLoggedIn{/if}">
-       {if $__wcf->user->userID}
-               <span class="userPanelAvatar" aria-hidden="true">{@$__wcf->getUserProfileHandler()->getAvatar()->getImageTag(32, false)}</span>
-       {else}
-               <a href="{link controller='Login' url=$__wcf->getRequestURI()}{/link}" class="userPanelLoginLink jsTooltip" title="{lang}wcf.user.loginOrRegister{/lang}">
-                       {icon size=32 name='arrow-right-to-bracket'}
-               </a>
-       {/if}
-       
        <ul class="userPanelItems">
                {if $__wcf->user->userID}
                        <!-- user menu -->
                </li>
        </ul>
 </nav>
+{if $__wcf->user->userID}
+       <button class="pageHeaderUserMobile" aria-expanded="false" aria-label="{lang}wcf.menu.user{/lang}">
+               <span class="pageHeaderUserMobileInactive">
+                       {@$__wcf->getUserProfileHandler()->getAvatar()->getImageTag(32, false)}
+               </span>
+               <span class="pageHeaderUserMobileActive">
+                       {icon size=32 name='xmark'}
+               </span>
+       </button>
+{else}
+       <a href="{link controller='Login' url=$__wcf->getRequestURI()}{/link}" class="userPanelLoginLink jsTooltip" title="{lang}wcf.user.loginOrRegister{/lang}">
+               {icon size=32 name='arrow-right-to-bracket'}
+       </a>
+{/if}
index deed873a1333e496723607b29f9782c3594331fa..b96f202958d9903a5cb8a96c485a4dc15a687550 100644 (file)
@@ -13,12 +13,10 @@ import * as Language from "../../../Language";
 import DomUtil from "../../../Dom/Util";
 import { MenuItem, PageMenuMainProvider } from "./Main/Provider";
 
-type CallbackOpen = (event: MouseEvent) => void;
-
 export class PageMenuMain implements PageMenuProvider {
-  private readonly callbackOpen: CallbackOpen;
   private readonly container: PageMenuContainer;
   private readonly mainMenu: HTMLElement;
+  private readonly mainMenuButton: HTMLButtonElement;
   private readonly menuItemBadges = new Map<string, HTMLElement>();
   private readonly menuItemProvider: PageMenuMainProvider;
   private readonly observer: MutationObserver;
@@ -27,14 +25,14 @@ export class PageMenuMain implements PageMenuProvider {
     this.mainMenu = document.querySelector(".mainMenu")!;
     this.menuItemProvider = menuItemProvider;
 
-    this.container = new PageMenuContainer(this);
-
-    this.callbackOpen = (event) => {
-      event.preventDefault();
+    this.mainMenuButton = document.querySelector(".pageHeaderMenuMobile") as HTMLButtonElement;
+    this.mainMenuButton.addEventListener("click", (event) => {
       event.stopPropagation();
 
       this.container.toggle();
-    };
+    });
+
+    this.container = new PageMenuContainer(this);
 
     this.observer = new MutationObserver((mutations) => {
       let refreshUnreadIndicator = false;
@@ -54,11 +52,8 @@ export class PageMenuMain implements PageMenuProvider {
   }
 
   enable(): void {
-    this.mainMenu.setAttribute("aria-expanded", "false");
-    this.mainMenu.setAttribute("aria-label", Language.get("wcf.menu.page"));
-    this.mainMenu.setAttribute("role", "button");
-    this.mainMenu.tabIndex = 0;
-    this.mainMenu.addEventListener("click", this.callbackOpen);
+    this.mainMenuButton.setAttribute("aria-expanded", "false");
+    this.mainMenuButton.querySelector("fa-icon")!.setIcon("bars");
 
     this.refreshUnreadIndicator();
   }
@@ -66,11 +61,8 @@ export class PageMenuMain implements PageMenuProvider {
   disable(): void {
     this.container.close();
 
-    this.mainMenu.removeAttribute("aria-expanded");
-    this.mainMenu.removeAttribute("aria-label");
-    this.mainMenu.removeAttribute("role");
-    this.mainMenu.removeAttribute("tabindex");
-    this.mainMenu.removeEventListener("click", this.callbackOpen);
+    this.mainMenuButton.setAttribute("aria-expanded", "false");
+    this.mainMenuButton.querySelector("fa-icon")!.setIcon("bars");
   }
 
   getContent(): DocumentFragment {
@@ -98,7 +90,7 @@ export class PageMenuMain implements PageMenuProvider {
   }
 
   getMenuButton(): HTMLElement {
-    return this.mainMenu;
+    return this.mainMenuButton;
   }
 
   sleep(): void {
index 620289ae96c70441acc59f6237390942345abfc1..b7a2dfb177699c3ad8a9900e4bea1933e61fc93b 100644 (file)
@@ -18,9 +18,7 @@ import { getElement as getControlPanelElement } from "../../User/Menu/ControlPan
 import * as EventHandler from "../../../Event/Handler";
 import { on as onMediaQueryChange } from "../../Screen";
 
-type CallbackOpen = (event: MouseEvent) => void;
-
-type Tab = HTMLAnchorElement;
+type Tab = HTMLButtonElement;
 type TabPanel = HTMLElement;
 type TabComponents = [Tab, TabPanel];
 
@@ -42,7 +40,6 @@ type LegacyUserPanelApi = {
 
 export class PageMenuUser implements PageMenuProvider {
   private activeTab?: Tab = undefined;
-  private readonly callbackOpen: CallbackOpen;
   private readonly container: PageMenuContainer;
   private readonly legacyUserPanels = new Map<Tab, LegacyUserPanelApi>();
   private readonly observer: MutationObserver;
@@ -51,10 +48,20 @@ export class PageMenuUser implements PageMenuProvider {
   private readonly tabPanels = new Map<Tab, HTMLElement>();
   private readonly tabs: Tab[] = [];
   private readonly userMenu: HTMLElement;
+  private readonly userMenuButton: HTMLButtonElement;
 
   constructor() {
     this.userMenu = document.querySelector(".userPanel")!;
 
+    this.userMenuButton = document.querySelector(".pageHeaderUserMobile") as HTMLButtonElement;
+    this.userMenuButton.addEventListener("click", (event) => {
+      event.stopPropagation();
+
+      // Clicking too early while the page is still loading
+      // causes an incomplete tab menu.
+      void isReady.then(() => this.container.toggle());
+    });
+
     this.container = new PageMenuContainer(this);
 
     const isReady = new Promise<void>((resolve) => {
@@ -69,15 +76,6 @@ export class PageMenuUser implements PageMenuProvider {
       }
     });
 
-    this.callbackOpen = (event) => {
-      event.preventDefault();
-      event.stopPropagation();
-
-      // Clicking too early while the page is still loading
-      // causes an incomplete tab menu.
-      void isReady.then(() => this.container.toggle());
-    };
-
     onMediaQueryChange("screen-lg", {
       match: () => this.detachViewsFromPanel(),
       unmatch: () => this.detachViewsFromPanel(),
@@ -89,11 +87,7 @@ export class PageMenuUser implements PageMenuProvider {
   }
 
   enable(): void {
-    this.userMenu.setAttribute("aria-expanded", "false");
-    this.userMenu.setAttribute("aria-label", Language.get("wcf.menu.user"));
-    this.userMenu.setAttribute("role", "button");
-    this.userMenu.tabIndex = 0;
-    this.userMenu.addEventListener("click", this.callbackOpen);
+    this.userMenuButton.setAttribute("aria-expanded", "false");
 
     this.refreshUnreadIndicator();
   }
@@ -101,11 +95,7 @@ export class PageMenuUser implements PageMenuProvider {
   disable(): void {
     this.container.close();
 
-    this.userMenu.removeAttribute("aria-expanded");
-    this.userMenu.removeAttribute("aria-label");
-    this.userMenu.removeAttribute("role");
-    this.userMenu.removeAttribute("tabindex");
-    this.userMenu.removeEventListener("click", this.callbackOpen);
+    this.userMenuButton.setAttribute("aria-expanded", "false");
   }
 
   getContent(): DocumentFragment {
@@ -116,7 +106,7 @@ export class PageMenuUser implements PageMenuProvider {
   }
 
   getMenuButton(): HTMLElement {
-    return this.userMenu;
+    return this.userMenuButton;
   }
 
   sleep(): void {
@@ -346,8 +336,14 @@ export class PageMenuUser implements PageMenuProvider {
     const panelButton = provider.getPanelButton();
     const button = panelButton.querySelector("a")!;
 
+    let icon = button.querySelector("fa-icon")?.outerHTML;
+    if (icon === undefined) {
+      // Fallback for the upgrade to 6.0.
+      icon = '<fa-icon size="32" name="question"></fa-icon>';
+    }
+
     const data: TabData = {
-      icon: button.querySelector(".icon")!.outerHTML,
+      icon,
       label: button.dataset.title || button.title,
       origin: panelButton.id,
     };
@@ -356,12 +352,11 @@ export class PageMenuUser implements PageMenuProvider {
   }
 
   private buildControlPanelTab(tabList: HTMLElement, tabContainer: HTMLElement): void {
-    const panel = document.getElementById("topMenu")!;
     const userMenu = document.getElementById("userMenu")!;
     const userMenuButton = userMenu.querySelector("a")!;
 
     const data: TabData = {
-      icon: panel.querySelector(".userPanelAvatar .userAvatarImage")!.outerHTML,
+      icon: this.userMenuButton.querySelector(".userAvatarImage")!.outerHTML,
       label: userMenuButton.dataset.title || userMenuButton.title,
       origin: userMenu.id,
     };
@@ -420,7 +415,7 @@ export class PageMenuUser implements PageMenuProvider {
     const tabId = DomUtil.getUniqueId();
     const panelId = DomUtil.getUniqueId();
 
-    const tab = document.createElement("a");
+    const tab = document.createElement("button");
     tab.classList.add("pageMenuUserTab");
     tab.dataset.hasUnreadContent = "false";
     tab.dataset.origin = data.origin;
index 678f849ff06bc1fa55de6c04d163e9dbf6283f02..459813bdd5b5ead5aa589346593f8b4af5744a54 100644 (file)
@@ -1,3 +1,11 @@
 <div id="mainMenu" class="mainMenu">
        <!-- Placeholder for the mobile UI. -->
 </div>
+<button class="pageHeaderMenuMobile" aria-expanded="false" aria-label="{lang}wcf.menu.page{/lang}">
+       <span class="pageHeaderMenuMobileInactive">
+               {icon size=32 name='bars'}
+       </span>
+       <span class="pageHeaderMenuMobileActive">
+               {icon size=32 name='xmark'}
+       </span>
+</button>
index 1ed76da0a5c862565f9f80795b1c07b00ee853f1..c42b4b68182aaf2daf7c7bb5e3087ee70f4cb0ee 100644 (file)
@@ -18,12 +18,12 @@ define(["require", "exports", "tslib", "./Container", "../../../Language", "../.
             this.menuItemBadges = new Map();
             this.mainMenu = document.querySelector(".mainMenu");
             this.menuItemProvider = menuItemProvider;
-            this.container = new Container_1.default(this);
-            this.callbackOpen = (event) => {
-                event.preventDefault();
+            this.mainMenuButton = document.querySelector(".pageHeaderMenuMobile");
+            this.mainMenuButton.addEventListener("click", (event) => {
                 event.stopPropagation();
                 this.container.toggle();
-            };
+            });
+            this.container = new Container_1.default(this);
             this.observer = new MutationObserver((mutations) => {
                 let refreshUnreadIndicator = false;
                 mutations.forEach((mutation) => {
@@ -38,20 +38,14 @@ define(["require", "exports", "tslib", "./Container", "../../../Language", "../.
             this.watchForChanges();
         }
         enable() {
-            this.mainMenu.setAttribute("aria-expanded", "false");
-            this.mainMenu.setAttribute("aria-label", Language.get("wcf.menu.page"));
-            this.mainMenu.setAttribute("role", "button");
-            this.mainMenu.tabIndex = 0;
-            this.mainMenu.addEventListener("click", this.callbackOpen);
+            this.mainMenuButton.setAttribute("aria-expanded", "false");
+            this.mainMenuButton.querySelector("fa-icon").setIcon("bars");
             this.refreshUnreadIndicator();
         }
         disable() {
             this.container.close();
-            this.mainMenu.removeAttribute("aria-expanded");
-            this.mainMenu.removeAttribute("aria-label");
-            this.mainMenu.removeAttribute("role");
-            this.mainMenu.removeAttribute("tabindex");
-            this.mainMenu.removeEventListener("click", this.callbackOpen);
+            this.mainMenuButton.setAttribute("aria-expanded", "false");
+            this.mainMenuButton.querySelector("fa-icon").setIcon("bars");
         }
         getContent() {
             const container = document.createElement("div");
@@ -72,7 +66,7 @@ define(["require", "exports", "tslib", "./Container", "../../../Language", "../.
             return fragment;
         }
         getMenuButton() {
-            return this.mainMenu;
+            return this.mainMenuButton;
         }
         sleep() {
             this.watchForChanges();
index 36f7e72e3aa28393d1087d12736e9466b016ea1c..4387d4a9495c4dabb827195dd944e848f2cc4bc7 100644 (file)
@@ -24,6 +24,13 @@ define(["require", "exports", "tslib", "./Container", "../../../Language", "../.
             this.tabPanels = new Map();
             this.tabs = [];
             this.userMenu = document.querySelector(".userPanel");
+            this.userMenuButton = document.querySelector(".pageHeaderUserMobile");
+            this.userMenuButton.addEventListener("click", (event) => {
+                event.stopPropagation();
+                // Clicking too early while the page is still loading
+                // causes an incomplete tab menu.
+                void isReady.then(() => this.container.toggle());
+            });
             this.container = new Container_1.default(this);
             const isReady = new Promise((resolve) => {
                 if (document.readyState === "complete") {
@@ -37,13 +44,6 @@ define(["require", "exports", "tslib", "./Container", "../../../Language", "../.
                     });
                 }
             });
-            this.callbackOpen = (event) => {
-                event.preventDefault();
-                event.stopPropagation();
-                // Clicking too early while the page is still loading
-                // causes an incomplete tab menu.
-                void isReady.then(() => this.container.toggle());
-            };
             (0, Screen_1.on)("screen-lg", {
                 match: () => this.detachViewsFromPanel(),
                 unmatch: () => this.detachViewsFromPanel(),
@@ -53,20 +53,12 @@ define(["require", "exports", "tslib", "./Container", "../../../Language", "../.
             });
         }
         enable() {
-            this.userMenu.setAttribute("aria-expanded", "false");
-            this.userMenu.setAttribute("aria-label", Language.get("wcf.menu.user"));
-            this.userMenu.setAttribute("role", "button");
-            this.userMenu.tabIndex = 0;
-            this.userMenu.addEventListener("click", this.callbackOpen);
+            this.userMenuButton.setAttribute("aria-expanded", "false");
             this.refreshUnreadIndicator();
         }
         disable() {
             this.container.close();
-            this.userMenu.removeAttribute("aria-expanded");
-            this.userMenu.removeAttribute("aria-label");
-            this.userMenu.removeAttribute("role");
-            this.userMenu.removeAttribute("tabindex");
-            this.userMenu.removeEventListener("click", this.callbackOpen);
+            this.userMenuButton.setAttribute("aria-expanded", "false");
         }
         getContent() {
             const fragment = document.createDocumentFragment();
@@ -74,7 +66,7 @@ define(["require", "exports", "tslib", "./Container", "../../../Language", "../.
             return fragment;
         }
         getMenuButton() {
-            return this.userMenu;
+            return this.userMenuButton;
         }
         sleep() {
             if (this.activeTab) {
@@ -262,21 +254,26 @@ define(["require", "exports", "tslib", "./Container", "../../../Language", "../.
             return tabContainer;
         }
         buildTab(provider) {
+            var _a;
             const panelButton = provider.getPanelButton();
             const button = panelButton.querySelector("a");
+            let icon = (_a = button.querySelector("fa-icon")) === null || _a === void 0 ? void 0 : _a.outerHTML;
+            if (icon === undefined) {
+                // Fallback for the upgrade to 6.0.
+                icon = '<fa-icon size="32" name="question"></fa-icon>';
+            }
             const data = {
-                icon: button.querySelector(".icon").outerHTML,
+                icon,
                 label: button.dataset.title || button.title,
                 origin: panelButton.id,
             };
             return this.buildTabComponents(data);
         }
         buildControlPanelTab(tabList, tabContainer) {
-            const panel = document.getElementById("topMenu");
             const userMenu = document.getElementById("userMenu");
             const userMenuButton = userMenu.querySelector("a");
             const data = {
-                icon: panel.querySelector(".userPanelAvatar .userAvatarImage").outerHTML,
+                icon: this.userMenuButton.querySelector(".userAvatarImage").outerHTML,
                 label: userMenuButton.dataset.title || userMenuButton.title,
                 origin: userMenu.id,
             };
@@ -318,7 +315,7 @@ define(["require", "exports", "tslib", "./Container", "../../../Language", "../.
         buildTabComponents(data) {
             const tabId = Util_1.default.getUniqueId();
             const panelId = Util_1.default.getUniqueId();
-            const tab = document.createElement("a");
+            const tab = document.createElement("button");
             tab.classList.add("pageMenuUserTab");
             tab.dataset.hasUnreadContent = "false";
             tab.dataset.origin = data.origin;
index e92b5db1e7afde50fd8c30bbee79d2b304319f96..e6b127e0c1385bf8c97b1cb4ce64ea4e51449116 100644 (file)
                        }
                }
        }
-
-       .userPanelAvatar {
-               display: none;
-       }
 }
 
 /* LOGO */
 
 @include screen-lg {
        .pageHeaderSearchMobile,
-       .userPanelLoginLink {
+       .pageHeaderMenuMobile,
+       .userPanelLoginLink,
+       .pageHeaderUserMobile {
                display: none;
        }
 }
 
        .pageHeaderSearchMobile,
        .userPanel,
-       .mainMenu {
+       .pageHeaderMenuMobile {
                align-items: center;
                display: flex;
                height: 40px;
                grid-area: search;
        }
 
-       .userPanel {
+       .pageHeaderUserMobile {
+               display: flex;
                grid-area: user;
+               justify-content: center;
+               width: 40px;
 
-               .userPanelItems {
+               &[aria-expanded="false"] .pageHeaderUserMobileActive {
                        display: none;
                }
 
-               .userPanelAvatar {
-                       display: block;
+               &[aria-expanded="true"] .pageHeaderUserMobileInactive {
+                       display: none;
                }
        }
 
+       .userPanel,
        .mainMenu {
+               display: none;
+       }
+
+       .pageHeaderMenuMobile {
                grid-area: menu;
 
-               &::before {
-                       content: $fa-var-bars;
+               &[aria-expanded="false"] .pageHeaderMenuMobileActive {
+                       display: none;
                }
 
-               .boxContent {
+               &[aria-expanded="true"] .pageHeaderMenuMobileInactive {
                        display: none;
                }
        }
 
-       .mainMenu[aria-expanded="false"],
-       .userPanel[aria-expanded="false"] {
+       .pageHeaderMenuMobile[aria-expanded="false"],
+       .pageHeaderUserMobile[aria-expanded="false"] {
                position: relative;
 
                &.pageMenuMobileButtonHasContent::after {
index 5c7fd49e47a0944b7de850b8ac62a9d6cf56c9c3..e454afb6a53fc09e6b3664b3050262b3129b6072 100644 (file)
 }
 
 @include screen-md-down {
-       .mainMenu[aria-expanded="true"]::before {
-               content: $fa-var-times;
-       }
-
-       .userPanel.userPanelLoggedIn[aria-expanded="true"] {
-               &::before {
-                       content: $fa-var-times;
-                       color: $wcfHeaderLink;
-                       font-family: FontAwesome;
-                       font-size: 28px;
-                       line-height: 32px;
-                       padding: 5px 5px;
-               }
-
-               .userPanelAvatar {
-                       display: none;
-               }
-       }
-
-       .mainMenu[aria-expanded="true"]::after,
-       .userPanel.userPanelLoggedIn[aria-expanded="true"]::after {
+       .pageHeaderMenuMobile[aria-expanded="true"]::after,
+       .pageHeaderUserMobile[aria-expanded="true"]::after {
                border: 8px solid transparent;
                border-top-width: 0;
                border-bottom-color: $wcfUserMenuBackground;
-               bottom: -5px;
+               bottom: 0;
                content: "";
                position: absolute;
        }
-
-       .userPanel.userPanelLoggedIn[aria-expanded="true"]::after {
-               bottom: 0;
-       }
 }
 
 @include screen-sm-md {