Integrate the new user menu providers into the mobile menu
authorAlexander Ebert <ebert@woltlab.com>
Tue, 14 Dec 2021 14:44:32 +0000 (15:44 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Tue, 14 Dec 2021 14:44:32 +0000 (15:44 +0100)
ts/WoltLabSuite/Core/Ui/Mobile.ts
ts/WoltLabSuite/Core/Ui/Page/Menu/Container.ts
ts/WoltLabSuite/Core/Ui/Page/Menu/User.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Mobile.js
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Menu/Container.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 [new file with mode: 0644]
wcfsetup/install/files/style/ui/userMenu.scss

index d749270a9001f097a5e585293338469a5234f737..d366b0bb507aada48b1fea8b097780ef907fc384 100644 (file)
@@ -378,6 +378,8 @@ export function setup(enableMobileMenu: boolean): void {
  * Enables the mobile UI.
  */
 export function enable(): void {
+  UiCloseOverlay.execute();
+
   _enabled = true;
   if (_enableMobileMenu) {
     _pageMenuMain.enable();
@@ -398,6 +400,8 @@ export function enableShadow(): void {
  * Disables the mobile UI.
  */
 export function disable(): void {
+  UiCloseOverlay.execute();
+
   _enabled = false;
   if (_enableMobileMenu) {
     _pageMenuMain.disable();
index 064fcb7378c54c915e1585aaedd1c6d71a2aafff..4a1157426832d6f604a6a754ecbf800c92b6a146 100644 (file)
@@ -26,8 +26,10 @@ export class PageMenuContainer {
 
     this.buildElements();
 
-    this.content.innerHTML = "";
-    this.content.append(this.provider.getContent());
+    if (this.content.childElementCount === 0) {
+      this.content.append(this.provider.getContent());
+    }
+
     this.provider.getMenuButton().setAttribute("aria-expanded", "true");
 
     pageOverlayOpen();
index cd9b91e6b981608982cb5e3755b3ebefec5b01e3..61080f9c5ab1741b1895d5450bbe191b4814577a 100644 (file)
@@ -20,13 +20,10 @@ 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 userMenuProviders = new Map<HTMLAnchorElement, UserMenuProvider>();
   private readonly tabPanels = new Map<HTMLAnchorElement, HTMLDivElement>();
   private readonly tabs: HTMLAnchorElement[] = [];
   private readonly userMenu: HTMLElement;
@@ -62,7 +59,7 @@ export class PageMenuUser implements PageMenuProvider {
 
   getContent(): DocumentFragment {
     const fragment = document.createDocumentFragment();
-    fragment.append(...this.buildTabMenu());
+    fragment.append(this.buildTabMenu());
 
     return fragment;
   }
@@ -72,7 +69,15 @@ export class PageMenuUser implements PageMenuProvider {
   }
 
   refresh(): void {
-    this.openNotifications();
+    const activeTab = this.tabs.find((element) => element.getAttribute("aria-selected") === "true");
+    if (activeTab === undefined) {
+      this.openNotifications();
+    } else {
+      // The UI elements in the tab panel are shared and can appear in a different
+      // context. The element might have been moved elsewhere while the menu was
+      // closed.
+      this.attachViewToPanel(activeTab);
+    }
   }
 
   private openNotifications(): void {
@@ -81,7 +86,7 @@ export class PageMenuUser implements PageMenuProvider {
       throw new Error("Unable to find the notifications tab.");
     }
 
-    notifications.click();
+    this.openTab(notifications);
   }
 
   private openTab(tab: HTMLAnchorElement): void {
@@ -107,6 +112,22 @@ export class PageMenuUser implements PageMenuProvider {
     if (document.activeElement !== tab) {
       tab.focus();
     }
+
+    this.attachViewToPanel(tab);
+  }
+
+  private attachViewToPanel(tab: HTMLAnchorElement): void {
+    const tabPanel = this.tabPanels.get(tab)!;
+    if (tabPanel.childElementCount === 0) {
+      const provider = this.userMenuProviders.get(tab);
+      if (provider) {
+        const view = provider.getView();
+        tabPanel.append(view.getElement());
+        void view.open();
+      } else {
+        throw new Error("TODO: Legacy user panel menus");
+      }
+    }
   }
 
   private keydown(event: KeyboardEvent): void {
@@ -152,13 +173,15 @@ export class PageMenuUser implements PageMenuProvider {
     this.tabs[index].focus();
   }
 
-  private buildTabMenu(): TabMenu {
+  private buildTabMenu(): HTMLDivElement {
+    const tabContainer = document.createElement("div");
+    tabContainer.classList.add("pageMenuUserTabContainer");
+
     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");
+    tabContainer.append(tabList);
 
     // TODO: Inject the control panel first.
 
@@ -166,15 +189,16 @@ export class PageMenuUser implements PageMenuProvider {
       const [tab, tabPanel] = this.buildTab(provider);
 
       tabList.append(tab);
-      tabPanelContainer.append(tabPanel);
+      tabContainer.append(tabPanel);
 
       this.tabs.push(tab);
       this.tabPanels.set(tab, tabPanel);
+      this.userMenuProviders.set(tab, provider);
     });
 
     // TODO: Inject legacy user panel items.
 
-    return [tabList, tabPanelContainer];
+    return tabContainer;
   }
 
   private buildTab(provider: UserMenuProvider): TabComponents {
@@ -208,7 +232,6 @@ export class PageMenuUser implements PageMenuProvider {
     panel.setAttribute("aria-labelledby", tabId);
     panel.setAttribute("role", "tabpanel");
     panel.tabIndex = 0;
-    panel.textContent = "panel for #" + provider.getPanelButton().id;
 
     return [tab, panel];
   }
index 8d7a596abf12c665ecf30c18c090cc6d50d6c96d..7b6e0f7b841aad3e92b1dea3b90bdce37269e600 100644 (file)
@@ -315,6 +315,7 @@ define(["require", "exports", "tslib", "../Core", "../Dom/Change/Listener", "../
      * Enables the mobile UI.
      */
     function enable() {
+        CloseOverlay_1.default.execute();
         _enabled = true;
         if (_enableMobileMenu) {
             _pageMenuMain.enable();
@@ -335,6 +336,7 @@ define(["require", "exports", "tslib", "../Core", "../Dom/Change/Listener", "../
      * Disables the mobile UI.
      */
     function disable() {
+        CloseOverlay_1.default.execute();
         _enabled = false;
         if (_enableMobileMenu) {
             _pageMenuMain.disable();
index 616c73fb6c6b0d343f5cbdbff90924dec65ff4c9..d864aa5b6e5fd6ae5f430a6f22f90bbd0e595817 100644 (file)
@@ -20,8 +20,9 @@ define(["require", "exports", "tslib", "focus-trap", "../../Screen", "../../Clos
         open() {
             CloseOverlay_1.default.execute();
             this.buildElements();
-            this.content.innerHTML = "";
-            this.content.append(this.provider.getContent());
+            if (this.content.childElementCount === 0) {
+                this.content.append(this.provider.getContent());
+            }
             this.provider.getMenuButton().setAttribute("aria-expanded", "true");
             (0, Screen_1.pageOverlayOpen)();
             (0, Screen_1.scrollDisable)();
index 21aefa9ae0830c6c1f6ad6c51a8327f7535db92e..25f85f6cd7b3bcd1bf2a431bec2ed57a418c7bc2 100644 (file)
@@ -15,6 +15,7 @@ define(["require", "exports", "tslib", "./Container", "../../../Language", "../.
     Util_1 = (0, tslib_1.__importDefault)(Util_1);
     class PageMenuUser {
         constructor() {
+            this.userMenuProviders = new Map();
             this.tabPanels = new Map();
             this.tabs = [];
             this.userMenu = document.querySelector(".userPanel");
@@ -40,21 +41,30 @@ define(["require", "exports", "tslib", "./Container", "../../../Language", "../.
         }
         getContent() {
             const fragment = document.createDocumentFragment();
-            fragment.append(...this.buildTabMenu());
+            fragment.append(this.buildTabMenu());
             return fragment;
         }
         getMenuButton() {
             return this.userMenu;
         }
         refresh() {
-            this.openNotifications();
+            const activeTab = this.tabs.find((element) => element.getAttribute("aria-selected") === "true");
+            if (activeTab === undefined) {
+                this.openNotifications();
+            }
+            else {
+                // The UI elements in the tab panel are shared and can appear in a different
+                // context. The element might have been moved elsewhere while the menu was
+                // closed.
+                this.attachViewToPanel(activeTab);
+            }
         }
         openNotifications() {
             const notifications = this.tabs.find((element) => element.dataset.origin === "userNotifications");
             if (!notifications) {
                 throw new Error("Unable to find the notifications tab.");
             }
-            notifications.click();
+            this.openTab(notifications);
         }
         openTab(tab) {
             if (tab.getAttribute("aria-selected") === "true") {
@@ -74,6 +84,21 @@ define(["require", "exports", "tslib", "./Container", "../../../Language", "../.
             if (document.activeElement !== tab) {
                 tab.focus();
             }
+            this.attachViewToPanel(tab);
+        }
+        attachViewToPanel(tab) {
+            const tabPanel = this.tabPanels.get(tab);
+            if (tabPanel.childElementCount === 0) {
+                const provider = this.userMenuProviders.get(tab);
+                if (provider) {
+                    const view = provider.getView();
+                    tabPanel.append(view.getElement());
+                    void view.open();
+                }
+                else {
+                    throw new Error("TODO: Legacy user panel menus");
+                }
+            }
         }
         keydown(event) {
             const tab = event.currentTarget;
@@ -115,21 +140,24 @@ define(["require", "exports", "tslib", "./Container", "../../../Language", "../.
             this.tabs[index].focus();
         }
         buildTabMenu() {
+            const tabContainer = document.createElement("div");
+            tabContainer.classList.add("pageMenuUserTabContainer");
             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");
+            tabContainer.append(tabList);
             // TODO: Inject the control panel first.
             (0, Manager_1.getUserMenuProviders)().forEach((provider) => {
                 const [tab, tabPanel] = this.buildTab(provider);
                 tabList.append(tab);
-                tabPanelContainer.append(tabPanel);
+                tabContainer.append(tabPanel);
                 this.tabs.push(tab);
                 this.tabPanels.set(tab, tabPanel);
+                this.userMenuProviders.set(tab, provider);
             });
             // TODO: Inject legacy user panel items.
-            return [tabList, tabPanelContainer];
+            return tabContainer;
         }
         buildTab(provider) {
             const tabId = Util_1.default.getUniqueId();
@@ -157,7 +185,6 @@ define(["require", "exports", "tslib", "./Container", "../../../Language", "../.
             panel.setAttribute("aria-labelledby", tabId);
             panel.setAttribute("role", "tabpanel");
             panel.tabIndex = 0;
-            panel.textContent = "panel for #" + provider.getPanelButton().id;
             return [tab, panel];
         }
     }
index 5d15417886d8c7df1e49172eb2b87a7d01873bb7..cd1dea80d88ca4c0dffb71c7ba9075dc10e67271 100644 (file)
 
                .userPanelAvatar {
                        display: block;
-                       padding: 0 5px;
                }
        }
 
diff --git a/wcfsetup/install/files/style/ui/pageMenu.scss b/wcfsetup/install/files/style/ui/pageMenu.scss
new file mode 100644 (file)
index 0000000..05b55e2
--- /dev/null
@@ -0,0 +1,159 @@
+.pageMenuContainer {
+    background-color: rgba(0, 0, 0, 0.34);
+    bottom: 0;
+    display: grid;
+    grid-template-areas: "content";
+    grid-template-columns: auto;
+    left: 0;
+    overflow: hidden;
+    position: fixed;
+    right: 0;
+    top: 50px;
+    z-index: 300;
+}
+
+.pageMenuContent {
+    --background-color: #{$wcfUserMenuBackground};
+    --background-color-active: #{$wcfUserMenuBackgroundActive};
+    --border-color: #{$wcfUserMenuBorder};
+    --color: #{$wcfUserMenuText};
+    --color-dimmed: #{$wcfUserMenuTextDimmed};
+    --color-indicator: #{$wcfUserMenuIndicator};
+
+    background-color: var(--background-color);
+    grid-area: content;
+}
+
+.pageMenuMainContainer {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+}
+
+.pageMenuMainNavigationFooter {
+    margin-top: auto;
+}
+
+.pageMenuMainItem {
+    border-bottom: 1px solid var(--border-color);
+    position: relative;
+}
+
+.pageMenuMainItemExpandable {
+    display: grid;
+    grid-template-areas:
+        "item button"
+        "list list";
+    grid-template-columns: auto 44px;
+}
+
+.pageMenuMainItemLink {
+    align-items: center;
+    color: inherit;
+    display: flex;
+    font-weight: 600;
+    grid-area: item;
+    min-height: 44px;
+    padding: 0 10px;
+}
+
+.pageMenuMainItemToggle {
+    align-items: center;
+    display: flex;
+    justify-content: center;
+    position: relative;
+
+    &::before {
+        border-left: 1px solid var(--border-color);
+        bottom: 10px;
+        content: "";
+        left: 0;
+        position: absolute;
+        top: 10px;
+    }
+
+    .icon {
+        transform: rotate(0);
+    }
+
+    &[aria-expanded="true"] .icon {
+        transform: rotate(180deg);
+    }
+}
+
+.pageMenuMainItemList {
+    grid-area: list;
+}
+
+.pageMenuMainItem .pageMenuMainItemList {
+    padding: 10px 0 20px 0;
+
+    .pageMenuMainItem {
+        border-bottom-width: 0;
+    }
+
+    .pageMenuMainItemLink {
+        font-weight: 400;
+        min-height: 34px;
+        padding-left: 20px;
+    }
+}
+
+.pageMenuMainNavigationFooter .pageMenuMainItem:last-child {
+    border-bottom-width: 0;
+}
+
+.pageMenuUserTabContainer {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+}
+
+.pageMenuUserTabList {
+    border-bottom: 1px solid var(--border-color);
+    display: grid;
+    grid-auto-columns: minmax(70px, 1fr);
+    grid-auto-flow: column;
+    overflow: auto;
+}
+
+.pageMenuUserTab {
+    align-items: center;
+    display: flex;
+    justify-content: center;
+    height: 50px;
+    position: relative;
+
+    &:not(:last-child) {
+        border-right: 1px solid var(--border-color);
+    }
+
+    &[aria-selected="true"] {
+        background-color: var(--background-color-active);
+    }
+}
+
+.pageMenuUserTabPanel {
+    flex: 1 auto;
+}
+
+@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;
+        }
+    }
+}
index 3b88ad2dacc16a9524b014fbc747daa2dfaee240..f2ecf1198aca0c83e301dca007dabd1b807b01bd 100644 (file)
@@ -7,13 +7,8 @@
        --color-indicator: #{$wcfUserMenuIndicator};
 
        background-color: var(--background-color);
-       border-radius: 5px;
-       box-shadow: 0 19px 38px rgb(0 0 0 / 30%), 0 15px 12px rgb(0 0 0 / 22%);
        color: var(--color);
        pointer-events: all;
-       position: fixed;
-       width: 400px;
-       z-index: 450;
 
        &.userMenuControlPanel {
                .userMenuItemImage {
        .icon {
                color: var(--color);
        }
+}
 
-       /* TODO: This is for the old mobile menu only. */
-       @include screen-xs {
-               border-radius: 0;
-               bottom: 0 !important;
-               display: flex;
-               left: 0 !important;
-               flex-direction: column;
-               right: 0 !important;
-               top: 50px !important;
-               width: 100%;
-
-               .userMenuContent {
-                       flex: 1 auto;
-               }
+.dropdownMenuContainer .userMenu {
+       border-radius: 5px;
+       box-shadow: 0 19px 38px rgb(0 0 0 / 30%), 0 15px 12px rgb(0 0 0 / 22%);
+       position: fixed;
+       width: 400px;
+       z-index: 450;
+}
+
+.pageMenuUserTabPanel .userMenu {
+       display: flex;
+       flex-direction: column;
+       height: 100%;
+
+       .userMenuContent {
+               flex: 1 auto;
        }
 }