Propagate item selections to the menu group
authorAlexander Ebert <ebert@woltlab.com>
Tue, 3 Oct 2023 14:19:47 +0000 (16:19 +0200)
committerTim Düsterhus <duesterhus@woltlab.com>
Thu, 5 Oct 2023 15:22:07 +0000 (17:22 +0200)
ts/WoltLabSuite/Core/Element/woltlab-core-menu-group.ts
ts/WoltLabSuite/Core/Element/woltlab-core-menu-item.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Element/woltlab-core-menu-group.js
wcfsetup/install/files/js/WoltLabSuite/Core/Element/woltlab-core-menu-item.js

index 51006a3696942ba26ea108376b1c7ae7f1e6764b..455cb89fd4c07f7a49ff987a943e1a6e0f640145 100644 (file)
@@ -1,5 +1,33 @@
+import WoltlabCoreMenuItemElement from "./woltlab-core-menu-item";
+
 export class WoltlabCoreMenuGroupElement extends HTMLElement {
+  readonly #items = new Set<WoltlabCoreMenuItemElement>();
+  #value = "";
+
   connectedCallback() {
+    const shadow = this.attachShadow({ mode: "open" });
+    const slot = document.createElement("slot");
+    slot.addEventListener("slotchange", () => {
+      this.#items.clear();
+
+      for (const element of slot.assignedElements()) {
+        if (!(element instanceof WoltlabCoreMenuItemElement)) {
+          element.remove();
+          continue;
+        }
+
+        this.#items.add(element);
+
+        element.setRole("menuitemcheckbox");
+
+        element.addEventListener("change", () => {
+          this.#updateValue();
+        });
+      }
+    });
+
+    shadow.append(slot);
+
     this.setAttribute("role", "group");
 
     this.label = this.getAttribute("label")!;
@@ -13,6 +41,29 @@ export class WoltlabCoreMenuGroupElement extends HTMLElement {
     this.setAttribute("label", label);
     this.setAttribute("aria-label", label);
   }
+
+  get value(): string {
+    return this.#value;
+  }
+
+  set value(value: string) {
+    const values = value.split(",");
+
+    this.#items.forEach((item) => {
+      item.selected = values.includes(item.value);
+    });
+
+    this.#updateValue();
+  }
+
+  #updateValue(): void {
+    this.#value = Array.from(this.#items)
+      .filter((item) => item.selected)
+      .map((item) => item.value)
+      .join(",");
+
+    this.setAttribute("value", this.#value);
+  }
 }
 
 export default WoltlabCoreMenuGroupElement;
index ce00aab4532f73716cf85ebd040b7fe416aec4a6..824bc3e6e7a940918ac094602477bc938f452012 100644 (file)
@@ -1,14 +1,36 @@
-import WoltlabCoreMenuGroupElement from "./woltlab-core-menu-group";
+type Role = "menuitem" | "menuitemcheckbox";
 
-const enum MenuItemType {
-  Checkbox,
-  Item,
+interface WoltlabCoreMenuItemEventMap {
+  beforeSelect: CustomEvent;
+  change: CustomEvent;
 }
 
+// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
 export class WoltlabCoreMenuItemElement extends HTMLElement {
-  #type: MenuItemType = MenuItemType.Item;
   #checkmark?: FaIcon;
 
+  constructor() {
+    super();
+
+    this.addEventListener("click", () => {
+      if (this.disabled) {
+        return;
+      }
+
+      const evt = new CustomEvent("beforeSelect", {
+        cancelable: true,
+      });
+      this.dispatchEvent(evt);
+
+      if (!evt.defaultPrevented) {
+        this.selected = !this.selected;
+
+        const evt = new CustomEvent("change");
+        this.dispatchEvent(evt);
+      }
+    });
+  }
+
   connectedCallback() {
     const shadow = this.attachShadow({ mode: "open" });
 
@@ -21,44 +43,14 @@ export class WoltlabCoreMenuItemElement extends HTMLElement {
     shadow.append(defaultSlot);
 
     this.tabIndex = -1;
-    this.disabled = this.hasAttribute("disabled");
-
-    if (this.parentElement! instanceof WoltlabCoreMenuGroupElement) {
-      this.#type = MenuItemType.Checkbox;
-      this.setAttribute("role", "menuitemcheckbox");
-
-      this.selected = this.hasAttribute("selected");
-
-      if (this.#checkmark === undefined) {
-        this.#checkmark = document.createElement("fa-icon");
-        this.#checkmark.setIcon("check");
-        this.#checkmark.slot = "checkmark";
-      }
-
-      this.append(this.#checkmark);
-    } else {
-      this.#type = MenuItemType.Item;
-      this.setAttribute("role", "menuitem");
-
-      this.removeAttribute("aria-checked");
-
-      this.#checkmark?.remove();
-    }
+    this.setAttribute("role", "menuitem");
   }
 
   get selected(): boolean {
-    if (this.#type !== MenuItemType.Item) {
-      return false;
-    }
-
     return this.hasAttribute("selected");
   }
 
   set selected(checked: boolean) {
-    if (this.#type !== MenuItemType.Checkbox) {
-      return;
-    }
-
     if (checked) {
       this.setAttribute("selected", "");
     } else {
@@ -89,8 +81,43 @@ export class WoltlabCoreMenuItemElement extends HTMLElement {
   set value(value: string) {
     this.setAttribute("value", value);
   }
+
+  setRole(role: Role): void {
+    this.setAttribute("role", role);
+    this.#updateAriaSelected();
+
+    if (role === "menuitem") {
+      this.#checkmark?.remove();
+    } else if (role === "menuitemcheckbox") {
+      if (this.#checkmark === undefined) {
+        this.#checkmark = document.createElement("fa-icon");
+        this.#checkmark.setIcon("check");
+        this.#checkmark.slot = "checkmark";
+      }
+
+      this.append(this.#checkmark);
+    }
+  }
+
+  #updateAriaSelected(): void {
+    const role = this.getAttribute("role") as Role;
+    if (role === "menuitemcheckbox") {
+      this.setAttribute("aria-checked", String(this.selected === true));
+    }
+  }
 }
 
-export default WoltlabCoreMenuItemElement;
+// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
+export interface WoltlabCoreMenuItemElement extends HTMLElement {
+  addEventListener: {
+    <T extends keyof WoltlabCoreMenuItemEventMap>(
+      type: T,
+      listener: (this: WoltlabCoreMenuItemElement, ev: WoltlabCoreMenuItemEventMap[T]) => any,
+      options?: boolean | AddEventListenerOptions,
+    ): void;
+  } & HTMLElement["addEventListener"];
+}
 
 window.customElements.define("woltlab-core-menu-item", WoltlabCoreMenuItemElement);
+
+export default WoltlabCoreMenuItemElement;
index 30bfb78f72aaaf8488073371f7cef43520e0a52f..256097c83c998b948513f0624bc03bbfb2f5b168 100644 (file)
@@ -1,9 +1,29 @@
-define(["require", "exports"], function (require, exports) {
+define(["require", "exports", "tslib", "./woltlab-core-menu-item"], function (require, exports, tslib_1, woltlab_core_menu_item_1) {
     "use strict";
     Object.defineProperty(exports, "__esModule", { value: true });
     exports.WoltlabCoreMenuGroupElement = void 0;
+    woltlab_core_menu_item_1 = tslib_1.__importDefault(woltlab_core_menu_item_1);
     class WoltlabCoreMenuGroupElement extends HTMLElement {
+        #items = new Set();
+        #value = "";
         connectedCallback() {
+            const shadow = this.attachShadow({ mode: "open" });
+            const slot = document.createElement("slot");
+            slot.addEventListener("slotchange", () => {
+                this.#items.clear();
+                for (const element of slot.assignedElements()) {
+                    if (!(element instanceof woltlab_core_menu_item_1.default)) {
+                        element.remove();
+                        continue;
+                    }
+                    this.#items.add(element);
+                    element.setRole("menuitemcheckbox");
+                    element.addEventListener("change", () => {
+                        this.#updateValue();
+                    });
+                }
+            });
+            shadow.append(slot);
             this.setAttribute("role", "group");
             this.label = this.getAttribute("label");
         }
@@ -14,6 +34,23 @@ define(["require", "exports"], function (require, exports) {
             this.setAttribute("label", label);
             this.setAttribute("aria-label", label);
         }
+        get value() {
+            return this.#value;
+        }
+        set value(value) {
+            const values = value.split(",");
+            this.#items.forEach((item) => {
+                item.selected = values.includes(item.value);
+            });
+            this.#updateValue();
+        }
+        #updateValue() {
+            this.#value = Array.from(this.#items)
+                .filter((item) => item.selected)
+                .map((item) => item.value)
+                .join(",");
+            this.setAttribute("value", this.#value);
+        }
     }
     exports.WoltlabCoreMenuGroupElement = WoltlabCoreMenuGroupElement;
     exports.default = WoltlabCoreMenuGroupElement;
index 4d61ae7d643fe5ac640d38efcb4e1dbe783024b1..bb28a320bd64cd3068713073d53a4209c4ce805f 100644 (file)
@@ -1,11 +1,27 @@
-define(["require", "exports", "tslib", "./woltlab-core-menu-group"], function (require, exports, tslib_1, woltlab_core_menu_group_1) {
+define(["require", "exports"], function (require, exports) {
     "use strict";
     Object.defineProperty(exports, "__esModule", { value: true });
     exports.WoltlabCoreMenuItemElement = void 0;
-    woltlab_core_menu_group_1 = tslib_1.__importDefault(woltlab_core_menu_group_1);
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
     class WoltlabCoreMenuItemElement extends HTMLElement {
-        #type = 1 /* MenuItemType.Item */;
         #checkmark;
+        constructor() {
+            super();
+            this.addEventListener("click", () => {
+                if (this.disabled) {
+                    return;
+                }
+                const evt = new CustomEvent("beforeSelect", {
+                    cancelable: true,
+                });
+                this.dispatchEvent(evt);
+                if (!evt.defaultPrevented) {
+                    this.selected = !this.selected;
+                    const evt = new CustomEvent("change");
+                    this.dispatchEvent(evt);
+                }
+            });
+        }
         connectedCallback() {
             const shadow = this.attachShadow({ mode: "open" });
             const checkmarkSlot = document.createElement("slot");
@@ -15,35 +31,12 @@ define(["require", "exports", "tslib", "./woltlab-core-menu-group"], function (r
             defaultSlot.id = "slot";
             shadow.append(defaultSlot);
             this.tabIndex = -1;
-            this.disabled = this.hasAttribute("disabled");
-            if (this.parentElement instanceof woltlab_core_menu_group_1.default) {
-                this.#type = 0 /* MenuItemType.Checkbox */;
-                this.setAttribute("role", "menuitemcheckbox");
-                this.selected = this.hasAttribute("selected");
-                if (this.#checkmark === undefined) {
-                    this.#checkmark = document.createElement("fa-icon");
-                    this.#checkmark.setIcon("check");
-                    this.#checkmark.slot = "checkmark";
-                }
-                this.append(this.#checkmark);
-            }
-            else {
-                this.#type = 1 /* MenuItemType.Item */;
-                this.setAttribute("role", "menuitem");
-                this.removeAttribute("aria-checked");
-                this.#checkmark?.remove();
-            }
+            this.setAttribute("role", "menuitem");
         }
         get selected() {
-            if (this.#type !== 1 /* MenuItemType.Item */) {
-                return false;
-            }
             return this.hasAttribute("selected");
         }
         set selected(checked) {
-            if (this.#type !== 0 /* MenuItemType.Checkbox */) {
-                return;
-            }
             if (checked) {
                 this.setAttribute("selected", "");
             }
@@ -70,8 +63,29 @@ define(["require", "exports", "tslib", "./woltlab-core-menu-group"], function (r
         set value(value) {
             this.setAttribute("value", value);
         }
+        setRole(role) {
+            this.setAttribute("role", role);
+            this.#updateAriaSelected();
+            if (role === "menuitem") {
+                this.#checkmark?.remove();
+            }
+            else if (role === "menuitemcheckbox") {
+                if (this.#checkmark === undefined) {
+                    this.#checkmark = document.createElement("fa-icon");
+                    this.#checkmark.setIcon("check");
+                    this.#checkmark.slot = "checkmark";
+                }
+                this.append(this.#checkmark);
+            }
+        }
+        #updateAriaSelected() {
+            const role = this.getAttribute("role");
+            if (role === "menuitemcheckbox") {
+                this.setAttribute("aria-checked", String(this.selected === true));
+            }
+        }
     }
     exports.WoltlabCoreMenuItemElement = WoltlabCoreMenuItemElement;
-    exports.default = WoltlabCoreMenuItemElement;
     window.customElements.define("woltlab-core-menu-item", WoltlabCoreMenuItemElement);
+    exports.default = WoltlabCoreMenuItemElement;
 });