Draft for the layout of the user menu
authorAlexander Ebert <ebert@woltlab.com>
Mon, 13 Dec 2021 13:35:20 +0000 (14:35 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Mon, 13 Dec 2021 13:35:20 +0000 (14:35 +0100)
ts/WoltLabSuite/Core/Ui/Mobile.ts
ts/WoltLabSuite/Core/Ui/Page/Menu/Main.ts
ts/WoltLabSuite/Core/Ui/Page/Menu/User.ts
ts/WoltLabSuite/Core/Ui/User/Menu/Manager.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Mobile.js
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/js/WoltLabSuite/Core/Ui/User/Menu/Manager.js

index d1821f9301be903d821578240e1eff1af002d381..d749270a9001f097a5e585293338469a5234f737 100644 (file)
@@ -15,7 +15,7 @@ import * as UiAlignment from "./Alignment";
 import UiCloseOverlay from "./CloseOverlay";
 import * as UiDropdownReusable from "./Dropdown/Reusable";
 import { PageMenuMain } from "./Page/Menu/Main";
-import UiPageMenuUser from "./Page/Menu/User";
+import { hasValidUserMenu, PageMenuUser } from "./Page/Menu/User";
 import * as UiScreen from "./Screen";
 
 interface MainMenuMorePayload {
@@ -31,7 +31,7 @@ let _enableMobileMenu = false;
 const _knownMessages = new WeakSet<HTMLElement>();
 let _mobileSidebarEnabled = false;
 let _pageMenuMain: PageMenuMain;
-let _pageMenuUser: UiPageMenuUser | undefined = undefined;
+let _pageMenuUser: PageMenuUser | undefined = undefined;
 let _messageGroups: HTMLCollection | null = null;
 const _sidebars: HTMLElement[] = [];
 
@@ -166,8 +166,9 @@ function initMobileMenu(): void {
     _pageMenuMain = new PageMenuMain();
     _pageMenuMain.enable();
 
-    if (UiPageMenuUser.hasValidMenu()) {
-      _pageMenuUser = new UiPageMenuUser();
+    if (hasValidUserMenu()) {
+      _pageMenuUser = new PageMenuUser();
+      _pageMenuUser.enable();
     }
   }
 }
index e9e6b9934bf0588752142f62a5068d5e58a5aeb7..0b5532dc659c4d26937e1466d4047a4e1b0a671b 100644 (file)
@@ -1,5 +1,5 @@
 /**
- * Provides the touch-friendly fullscreen main menu.
+ * Provides the touch-friendly main menu.
  *
  * @author Alexander Ebert
  * @copyright 2001-2021 WoltLab GmbH
index 1efcc28777a6f3c48cc1d686b09e56bccbc178d2..a1deaf16a466dab98f7431126d0f6e32bcd894c4 100644 (file)
 /**
- * Provides the touch-friendly fullscreen user menu.
+ * Provides the touch-friendly user menu.
  *
- * @author  Alexander Ebert
- * @copyright  2001-2019 WoltLab GmbH
- * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module  WoltLabSuite/Core/Ui/Page/Menu/User
+ * @author Alexander Ebert
+ * @copyright 2001-2021 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module WoltLabSuite/Core/Ui/Page/Menu/User
  */
 
-import * as Core from "../../../Core";
-import * as EventHandler from "../../../Event/Handler";
+import PageMenuContainer from "./Container";
+import { PageMenuProvider } from "./Provider";
 import * as Language from "../../../Language";
-import UiPageMenuAbstract from "./Abstract";
+import { getUserMenuProviders } from "../../User/Menu/Manager";
+import { UserMenuProvider } from "../../User/Menu/Data/Provider";
+import DomUtil from "../../../Dom/Util";
 
-interface EventPayload {
-  count: number;
-  identifier: string;
-}
+type CallbackOpen = (event: MouseEvent) => void;
+
+type Tab = HTMLAnchorElement;
+type TabPanel = HTMLDivElement;
+type TabComponents = [Tab, TabPanel];
+
+type TabList = HTMLDivElement;
+type TabPanelContainer = HTMLDivElement;
+type TabMenu = [TabList, TabPanelContainer];
+
+export class PageMenuUser implements PageMenuProvider {
+  private readonly callbackOpen: CallbackOpen;
+  private readonly container: PageMenuContainer;
+  private readonly userMenu: HTMLElement;
 
-class UiPageMenuUser extends UiPageMenuAbstract {
-  /**
-   * Initializes the touch-friendly fullscreen user menu.
-   */
   constructor() {
-    super("com.woltlab.wcf.UserMenuMobile", "pageUserMenuMobile", "#pageHeader .userPanel");
+    this.userMenu = document.querySelector(".userPanel")!;
+
+    this.container = new PageMenuContainer(this);
 
-    EventHandler.add("com.woltlab.wcf.userMenu", "updateBadge", (data) => this.updateBadge(data));
+    this.callbackOpen = (event) => {
+      event.preventDefault();
+      event.stopPropagation();
 
-    this.button.setAttribute("aria-label", Language.get("wcf.menu.user"));
-    this.button.setAttribute("role", "button");
+      this.container.toggle();
+    };
   }
 
-  close(event?: Event): boolean {
-    // The user menu is not initialized if there are no items to display.
-    if (this.menu === undefined) {
-      return false;
-    }
+  enable(): void {
+    this.userMenu.setAttribute("aria-expanded", "false");
+    this.userMenu.setAttribute("role", "button");
+    this.userMenu.tabIndex = 0;
+    this.userMenu.addEventListener("click", this.callbackOpen);
+  }
 
-    const dropdown = window.WCF.Dropdown.Interactive.Handler.getOpenDropdown();
-    if (dropdown) {
-      if (event) {
-        event.preventDefault();
-        event.stopPropagation();
-      }
+  disable(): void {
+    this.container.close();
 
-      dropdown.close();
+    this.userMenu.removeAttribute("aria-expanded");
+    this.userMenu.removeAttribute("role");
+    this.userMenu.removeAttribute("tabindex");
+    this.userMenu.removeEventListener("click", this.callbackOpen);
+  }
 
-      return true;
-    }
+  getContent(): DocumentFragment {
+    const fragment = document.createDocumentFragment();
+    fragment.append(...this.buildTabMenu());
 
-    return super.close(event);
+    return fragment;
   }
 
-  private updateBadge(data: EventPayload): void {
-    this.menu.querySelectorAll(".menuOverlayItemBadge").forEach((item: HTMLElement) => {
-      if (item.dataset.badgeIdentifier === data.identifier) {
-        let badge = item.querySelector(".badge");
-        if (data.count) {
-          if (badge === null) {
-            badge = document.createElement("span");
-            badge.className = "badge badgeUpdate";
-            item.appendChild(badge);
-          }
-
-          badge.textContent = data.count.toString();
-        } else if (badge !== null) {
-          badge.remove();
-        }
-
-        this.updateButtonState();
-      }
-    });
+  getMenuButton(): HTMLElement {
+    return this.userMenu;
   }
 
-  static hasValidMenu(): boolean {
-    const menu = document.querySelector("#pageUserMenuMobile > .menuOverlayItemList")!;
-    if (menu.childElementCount === 1 && menu.children[0].classList.contains("menuOverlayTitle")) {
-      const userPanel = document.querySelector("#pageHeader .userPanel")!;
-      userPanel.classList.add("hideUserPanel");
-      return false;
-    }
+  private buildTabMenu(): TabMenu {
+    const tabList = document.createElement("div");
+    tabList.classList.add("pageMenuUserTabList");
+    tabList.setAttribute("role", "tablist");
+    tabList.setAttribute("aria-label", Language.get("TODO"));
+
+    const tabPanelContainer = document.createElement("div");
 
-    return true;
+    // TODO: Inject the control panel first.
+
+    getUserMenuProviders().forEach((provider) => {
+      const [tab, tabPanel] = this.buildTab(provider);
+
+      tabList.append(tab);
+      tabPanelContainer.append(tabPanel);
+    });
+
+    // TODO: Inject legacy user panel items.
+
+    return [tabList, tabPanelContainer];
+  }
+
+  private buildTab(provider: UserMenuProvider): TabComponents {
+    const tabId = DomUtil.getUniqueId();
+    const panelId = DomUtil.getUniqueId();
+
+    const tab = document.createElement("a");
+    tab.classList.add("pageMenuUserTab");
+    tab.id = tabId;
+    tab.setAttribute("aria-controls", panelId);
+    tab.setAttribute("aria-selected", "false");
+    tab.setAttribute("role", "tab");
+    tab.tabIndex = -1;
+
+    const button = provider.getPanelButton().querySelector("a")!;
+    tab.setAttribute("aria-label", button.dataset.title || button.title);
+    tab.innerHTML = button.querySelector(".icon")!.outerHTML;
+
+    const panel = document.createElement("div");
+    panel.classList.add("pageMenuUserTabPanel");
+    panel.id = panelId;
+    panel.hidden = true;
+    panel.setAttribute("aria-labelledby", tabId);
+    panel.setAttribute("role", "tabpanel");
+    panel.tabIndex = 0;
+
+    return [tab, panel];
   }
 }
 
-Core.enableLegacyInheritance(UiPageMenuUser);
+export function hasValidUserMenu(): boolean {
+  return true;
+}
 
-export = UiPageMenuUser;
+export default PageMenuUser;
index f5fb67413e0b05229dee80608aecb26415fad2e7..5864b7154f281dbb768d5c0d7ad7d95d9a0f6d77 100644 (file)
@@ -94,6 +94,10 @@ function getView(provider: UserMenuProvider): UserMenuView {
   return views.get(provider)!;
 }
 
+export function getUserMenuProviders(): Set<UserMenuProvider> {
+  return providers;
+}
+
 export function getContainer(): HTMLElement {
   if (container === undefined) {
     container = document.createElement("div");
index 112d6b17853fb701a7abf840b817567828866abf..8d7a596abf12c665ecf30c18c090cc6d50d6c96d 100644 (file)
@@ -17,7 +17,6 @@ define(["require", "exports", "tslib", "../Core", "../Dom/Change/Listener", "../
     UiAlignment = (0, tslib_1.__importStar)(UiAlignment);
     CloseOverlay_1 = (0, tslib_1.__importDefault)(CloseOverlay_1);
     UiDropdownReusable = (0, tslib_1.__importStar)(UiDropdownReusable);
-    User_1 = (0, tslib_1.__importDefault)(User_1);
     UiScreen = (0, tslib_1.__importStar)(UiScreen);
     let _dropdownMenu = null;
     let _dropdownMenuMessage = null;
@@ -138,8 +137,9 @@ define(["require", "exports", "tslib", "../Core", "../Dom/Change/Listener", "../
         if (_enableMobileMenu) {
             _pageMenuMain = new Main_1.PageMenuMain();
             _pageMenuMain.enable();
-            if (User_1.default.hasValidMenu()) {
-                _pageMenuUser = new User_1.default();
+            if ((0, User_1.hasValidUserMenu)()) {
+                _pageMenuUser = new User_1.PageMenuUser();
+                _pageMenuUser.enable();
             }
         }
     }
index 923ca0608de7f8e5e065205e3d0760fea386f84b..b1fc6bf777f39c13727056ae5a265a3c830393c7 100644 (file)
@@ -1,5 +1,5 @@
 /**
- * Provides the touch-friendly fullscreen main menu.
+ * Provides the touch-friendly main menu.
  *
  * @author Alexander Ebert
  * @copyright 2001-2021 WoltLab GmbH
index 4027977d9199315d9af96a23b887251067696cd1..6692db57e086e21d5b51b8c78628121590c27837 100644 (file)
@@ -1,72 +1,91 @@
 /**
- * Provides the touch-friendly fullscreen user menu.
+ * Provides the touch-friendly user menu.
  *
- * @author  Alexander Ebert
- * @copyright  2001-2019 WoltLab GmbH
- * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module  WoltLabSuite/Core/Ui/Page/Menu/User
+ * @author Alexander Ebert
+ * @copyright 2001-2021 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module WoltLabSuite/Core/Ui/Page/Menu/User
  */
-define(["require", "exports", "tslib", "../../../Core", "../../../Event/Handler", "../../../Language", "./Abstract"], function (require, exports, tslib_1, Core, EventHandler, Language, Abstract_1) {
+define(["require", "exports", "tslib", "./Container", "../../../Language", "../../User/Menu/Manager", "../../../Dom/Util"], function (require, exports, tslib_1, Container_1, Language, Manager_1, Util_1) {
     "use strict";
-    Core = (0, tslib_1.__importStar)(Core);
-    EventHandler = (0, tslib_1.__importStar)(EventHandler);
+    Object.defineProperty(exports, "__esModule", { value: true });
+    exports.hasValidUserMenu = exports.PageMenuUser = void 0;
+    Container_1 = (0, tslib_1.__importDefault)(Container_1);
     Language = (0, tslib_1.__importStar)(Language);
-    Abstract_1 = (0, tslib_1.__importDefault)(Abstract_1);
-    class UiPageMenuUser extends Abstract_1.default {
-        /**
-         * Initializes the touch-friendly fullscreen user menu.
-         */
+    Util_1 = (0, tslib_1.__importDefault)(Util_1);
+    class PageMenuUser {
         constructor() {
-            super("com.woltlab.wcf.UserMenuMobile", "pageUserMenuMobile", "#pageHeader .userPanel");
-            EventHandler.add("com.woltlab.wcf.userMenu", "updateBadge", (data) => this.updateBadge(data));
-            this.button.setAttribute("aria-label", Language.get("wcf.menu.user"));
-            this.button.setAttribute("role", "button");
+            this.userMenu = document.querySelector(".userPanel");
+            this.container = new Container_1.default(this);
+            this.callbackOpen = (event) => {
+                event.preventDefault();
+                event.stopPropagation();
+                this.container.toggle();
+            };
         }
-        close(event) {
-            // The user menu is not initialized if there are no items to display.
-            if (this.menu === undefined) {
-                return false;
-            }
-            const dropdown = window.WCF.Dropdown.Interactive.Handler.getOpenDropdown();
-            if (dropdown) {
-                if (event) {
-                    event.preventDefault();
-                    event.stopPropagation();
-                }
-                dropdown.close();
-                return true;
-            }
-            return super.close(event);
+        enable() {
+            this.userMenu.setAttribute("aria-expanded", "false");
+            this.userMenu.setAttribute("role", "button");
+            this.userMenu.tabIndex = 0;
+            this.userMenu.addEventListener("click", this.callbackOpen);
         }
-        updateBadge(data) {
-            this.menu.querySelectorAll(".menuOverlayItemBadge").forEach((item) => {
-                if (item.dataset.badgeIdentifier === data.identifier) {
-                    let badge = item.querySelector(".badge");
-                    if (data.count) {
-                        if (badge === null) {
-                            badge = document.createElement("span");
-                            badge.className = "badge badgeUpdate";
-                            item.appendChild(badge);
-                        }
-                        badge.textContent = data.count.toString();
-                    }
-                    else if (badge !== null) {
-                        badge.remove();
-                    }
-                    this.updateButtonState();
-                }
+        disable() {
+            this.container.close();
+            this.userMenu.removeAttribute("aria-expanded");
+            this.userMenu.removeAttribute("role");
+            this.userMenu.removeAttribute("tabindex");
+            this.userMenu.removeEventListener("click", this.callbackOpen);
+        }
+        getContent() {
+            const fragment = document.createDocumentFragment();
+            fragment.append(...this.buildTabMenu());
+            return fragment;
+        }
+        getMenuButton() {
+            return this.userMenu;
+        }
+        buildTabMenu() {
+            const tabList = document.createElement("div");
+            tabList.classList.add("pageMenuUserTabList");
+            tabList.setAttribute("role", "tablist");
+            tabList.setAttribute("aria-label", Language.get("TODO"));
+            const tabPanelContainer = document.createElement("div");
+            // TODO: Inject the control panel first.
+            (0, Manager_1.getUserMenuProviders)().forEach((provider) => {
+                const [tab, tabPanel] = this.buildTab(provider);
+                tabList.append(tab);
+                tabPanelContainer.append(tabPanel);
             });
+            // TODO: Inject legacy user panel items.
+            return [tabList, tabPanelContainer];
         }
-        static hasValidMenu() {
-            const menu = document.querySelector("#pageUserMenuMobile > .menuOverlayItemList");
-            if (menu.childElementCount === 1 && menu.children[0].classList.contains("menuOverlayTitle")) {
-                const userPanel = document.querySelector("#pageHeader .userPanel");
-                userPanel.classList.add("hideUserPanel");
-                return false;
-            }
-            return true;
+        buildTab(provider) {
+            const tabId = Util_1.default.getUniqueId();
+            const panelId = Util_1.default.getUniqueId();
+            const tab = document.createElement("a");
+            tab.classList.add("pageMenuUserTab");
+            tab.id = tabId;
+            tab.setAttribute("aria-controls", panelId);
+            tab.setAttribute("aria-selected", "false");
+            tab.setAttribute("role", "tab");
+            tab.tabIndex = -1;
+            const button = provider.getPanelButton().querySelector("a");
+            tab.setAttribute("aria-label", button.dataset.title || button.title);
+            tab.innerHTML = button.querySelector(".icon").outerHTML;
+            const panel = document.createElement("div");
+            panel.classList.add("pageMenuUserTabPanel");
+            panel.id = panelId;
+            panel.hidden = true;
+            panel.setAttribute("aria-labelledby", tabId);
+            panel.setAttribute("role", "tabpanel");
+            panel.tabIndex = 0;
+            return [tab, panel];
         }
     }
-    Core.enableLegacyInheritance(UiPageMenuUser);
-    return UiPageMenuUser;
+    exports.PageMenuUser = PageMenuUser;
+    function hasValidUserMenu() {
+        return true;
+    }
+    exports.hasValidUserMenu = hasValidUserMenu;
+    exports.default = PageMenuUser;
 });
index f67fe06d01d3a8857d8efb5c665526508e5cbeae..2552cfc0e68657a85b035f6166ca70e6e992e027 100644 (file)
@@ -10,7 +10,7 @@
 define(["require", "exports", "tslib", "../../Alignment", "../../CloseOverlay", "../../../Event/Handler", "../../../Dom/Util"], function (require, exports, tslib_1, Alignment, CloseOverlay_1, EventHandler, Util_1) {
     "use strict";
     Object.defineProperty(exports, "__esModule", { value: true });
-    exports.registerProvider = exports.getContainer = void 0;
+    exports.registerProvider = exports.getContainer = exports.getUserMenuProviders = void 0;
     Alignment = (0, tslib_1.__importStar)(Alignment);
     CloseOverlay_1 = (0, tslib_1.__importDefault)(CloseOverlay_1);
     EventHandler = (0, tslib_1.__importStar)(EventHandler);
@@ -75,6 +75,10 @@ define(["require", "exports", "tslib", "../../Alignment", "../../CloseOverlay",
         }
         return views.get(provider);
     }
+    function getUserMenuProviders() {
+        return providers;
+    }
+    exports.getUserMenuProviders = getUserMenuProviders;
     function getContainer() {
         if (container === undefined) {
             container = document.createElement("div");