Basic prototype for ARIA compliant menus
authorAlexander Ebert <ebert@woltlab.com>
Fri, 29 Sep 2023 14:17:37 +0000 (16:17 +0200)
committerTim Düsterhus <duesterhus@woltlab.com>
Thu, 5 Oct 2023 15:22:07 +0000 (17:22 +0200)
ts/WoltLabSuite/Core/Bootstrap.ts
ts/WoltLabSuite/Core/Element/woltlab-core-menu-group.ts [new file with mode: 0644]
ts/WoltLabSuite/Core/Element/woltlab-core-menu-item.ts [new file with mode: 0644]
ts/WoltLabSuite/Core/Element/woltlab-core-menu-separator.ts [new file with mode: 0644]
ts/WoltLabSuite/Core/Element/woltlab-core-menu.ts [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Bootstrap.js
wcfsetup/install/files/js/WoltLabSuite/Core/Element/woltlab-core-menu-group.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Element/woltlab-core-menu-item.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Element/woltlab-core-menu-separator.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Element/woltlab-core-menu.js [new file with mode: 0644]

index 736d3cd3b0e5088fc617418cde0941023b41984e..351fed5d6e020be38827baa8192bca5093476cfb 100644 (file)
@@ -168,4 +168,11 @@ export function setup(options: BoostrapOptions): void {
   whenFirstSeen("[data-google-maps-geocoding]", () => {
     void import("./Component/GoogleMaps/Geocoding").then(({ setup }) => setup());
   });
+
+  whenFirstSeen("woltlab-core-menu", () => {
+    void import("./Element/woltlab-core-menu");
+    void import("./Element/woltlab-core-menu-group");
+    void import("./Element/woltlab-core-menu-item");
+    void import("./Element/woltlab-core-menu-separator");
+  });
 }
diff --git a/ts/WoltLabSuite/Core/Element/woltlab-core-menu-group.ts b/ts/WoltLabSuite/Core/Element/woltlab-core-menu-group.ts
new file mode 100644 (file)
index 0000000..51006a3
--- /dev/null
@@ -0,0 +1,20 @@
+export class WoltlabCoreMenuGroupElement extends HTMLElement {
+  connectedCallback() {
+    this.setAttribute("role", "group");
+
+    this.label = this.getAttribute("label")!;
+  }
+
+  get label(): string {
+    return this.getAttribute("label")!;
+  }
+
+  set label(label: string) {
+    this.setAttribute("label", label);
+    this.setAttribute("aria-label", label);
+  }
+}
+
+export default WoltlabCoreMenuGroupElement;
+
+window.customElements.define("woltlab-core-menu-group", WoltlabCoreMenuGroupElement);
diff --git a/ts/WoltLabSuite/Core/Element/woltlab-core-menu-item.ts b/ts/WoltLabSuite/Core/Element/woltlab-core-menu-item.ts
new file mode 100644 (file)
index 0000000..46816ec
--- /dev/null
@@ -0,0 +1,80 @@
+import WoltlabCoreMenuGroupElement from "./woltlab-core-menu-group";
+
+const enum MenuItemType {
+  Checkbox,
+  Item,
+}
+
+export class WoltlabCoreMenuItemElement extends HTMLElement {
+  #type: MenuItemType = MenuItemType.Item;
+
+  connectedCallback() {
+    const shadow = this.attachShadow({ mode: "open" });
+
+    const defaultSlot = document.createElement("slot");
+    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");
+    } else {
+      this.#type = MenuItemType.Item;
+      this.setAttribute("role", "menuitem");
+
+      this.removeAttribute("aria-checked");
+    }
+  }
+
+  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 {
+      this.removeAttribute("selected");
+    }
+
+    this.setAttribute("aria-checked", String(checked === true));
+  }
+
+  get disabled(): boolean {
+    return this.hasAttribute("disabled");
+  }
+
+  set disabled(disabled: boolean) {
+    if (disabled) {
+      this.setAttribute("disabled", "");
+    } else {
+      this.removeAttribute("disabled");
+    }
+
+    this.setAttribute("aria-disabled", String(disabled === true));
+  }
+
+  get value(): string {
+    return this.getAttribute("value")!;
+  }
+
+  set value(value: string) {
+    this.setAttribute("value", value);
+  }
+}
+
+export default WoltlabCoreMenuItemElement;
+
+window.customElements.define("woltlab-core-menu-item", WoltlabCoreMenuItemElement);
diff --git a/ts/WoltLabSuite/Core/Element/woltlab-core-menu-separator.ts b/ts/WoltLabSuite/Core/Element/woltlab-core-menu-separator.ts
new file mode 100644 (file)
index 0000000..fa453f0
--- /dev/null
@@ -0,0 +1,9 @@
+export class WoltlabCoreMenuSeparatorElement extends HTMLElement {
+  connectedCallback() {
+    this.setAttribute("role", "separator");
+  }
+}
+
+export default WoltlabCoreMenuSeparatorElement;
+
+window.customElements.define("woltlab-core-menu-separator", WoltlabCoreMenuSeparatorElement);
diff --git a/ts/WoltLabSuite/Core/Element/woltlab-core-menu.ts b/ts/WoltLabSuite/Core/Element/woltlab-core-menu.ts
new file mode 100644 (file)
index 0000000..09a8a01
--- /dev/null
@@ -0,0 +1,20 @@
+export class WoltlabCoreMenuElement extends HTMLElement {
+  connectedCallback() {
+    this.setAttribute("role", "menu");
+
+    this.label = this.getAttribute("label")!;
+  }
+
+  get label(): string {
+    return this.getAttribute("label")!;
+  }
+
+  set label(label: string) {
+    this.setAttribute("label", label);
+    this.setAttribute("aria-label", label);
+  }
+}
+
+export default WoltlabCoreMenuElement;
+
+window.customElements.define("woltlab-core-menu", WoltlabCoreMenuElement);
index 3c7f0fb55081b40e4d9a9f63d69554c32f1b9ebb..745a702239ab152ee1340871a613d13c65e559e8 100644 (file)
@@ -133,6 +133,12 @@ define(["require", "exports", "tslib", "./Core", "./Date/Picker", "./Devtools",
         (0, LazyLoader_1.whenFirstSeen)("[data-google-maps-geocoding]", () => {
             void new Promise((resolve_5, reject_5) => { require(["./Component/GoogleMaps/Geocoding"], resolve_5, reject_5); }).then(tslib_1.__importStar).then(({ setup }) => setup());
         });
+        (0, LazyLoader_1.whenFirstSeen)("woltlab-core-menu", () => {
+            void new Promise((resolve_6, reject_6) => { require(["./Element/woltlab-core-menu"], resolve_6, reject_6); }).then(tslib_1.__importStar);
+            void new Promise((resolve_7, reject_7) => { require(["./Element/woltlab-core-menu-group"], resolve_7, reject_7); }).then(tslib_1.__importStar);
+            void new Promise((resolve_8, reject_8) => { require(["./Element/woltlab-core-menu-item"], resolve_8, reject_8); }).then(tslib_1.__importStar);
+            void new Promise((resolve_9, reject_9) => { require(["./Element/woltlab-core-menu-separator"], resolve_9, reject_9); }).then(tslib_1.__importStar);
+        });
     }
     exports.setup = setup;
 });
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Element/woltlab-core-menu-group.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Element/woltlab-core-menu-group.js
new file mode 100644 (file)
index 0000000..30bfb78
--- /dev/null
@@ -0,0 +1,21 @@
+define(["require", "exports"], function (require, exports) {
+    "use strict";
+    Object.defineProperty(exports, "__esModule", { value: true });
+    exports.WoltlabCoreMenuGroupElement = void 0;
+    class WoltlabCoreMenuGroupElement extends HTMLElement {
+        connectedCallback() {
+            this.setAttribute("role", "group");
+            this.label = this.getAttribute("label");
+        }
+        get label() {
+            return this.getAttribute("label");
+        }
+        set label(label) {
+            this.setAttribute("label", label);
+            this.setAttribute("aria-label", label);
+        }
+    }
+    exports.WoltlabCoreMenuGroupElement = WoltlabCoreMenuGroupElement;
+    exports.default = WoltlabCoreMenuGroupElement;
+    window.customElements.define("woltlab-core-menu-group", WoltlabCoreMenuGroupElement);
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Element/woltlab-core-menu-item.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Element/woltlab-core-menu-item.js
new file mode 100644 (file)
index 0000000..5dd2035
--- /dev/null
@@ -0,0 +1,65 @@
+define(["require", "exports", "tslib", "./woltlab-core-menu-group"], function (require, exports, tslib_1, woltlab_core_menu_group_1) {
+    "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);
+    class WoltlabCoreMenuItemElement extends HTMLElement {
+        #type = 1 /* MenuItemType.Item */;
+        connectedCallback() {
+            const shadow = this.attachShadow({ mode: "open" });
+            const defaultSlot = document.createElement("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");
+            }
+            else {
+                this.#type = 1 /* MenuItemType.Item */;
+                this.setAttribute("role", "menuitem");
+                this.removeAttribute("aria-checked");
+            }
+        }
+        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", "");
+            }
+            else {
+                this.removeAttribute("selected");
+            }
+            this.setAttribute("aria-checked", String(checked === true));
+        }
+        get disabled() {
+            return this.hasAttribute("disabled");
+        }
+        set disabled(disabled) {
+            if (disabled) {
+                this.setAttribute("disabled", "");
+            }
+            else {
+                this.removeAttribute("disabled");
+            }
+            this.setAttribute("aria-disabled", String(disabled === true));
+        }
+        get value() {
+            return this.getAttribute("value");
+        }
+        set value(value) {
+            this.setAttribute("value", value);
+        }
+    }
+    exports.WoltlabCoreMenuItemElement = WoltlabCoreMenuItemElement;
+    exports.default = WoltlabCoreMenuItemElement;
+    window.customElements.define("woltlab-core-menu-item", WoltlabCoreMenuItemElement);
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Element/woltlab-core-menu-separator.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Element/woltlab-core-menu-separator.js
new file mode 100644 (file)
index 0000000..4f4f983
--- /dev/null
@@ -0,0 +1,13 @@
+define(["require", "exports"], function (require, exports) {
+    "use strict";
+    Object.defineProperty(exports, "__esModule", { value: true });
+    exports.WoltlabCoreMenuSeparatorElement = void 0;
+    class WoltlabCoreMenuSeparatorElement extends HTMLElement {
+        connectedCallback() {
+            this.setAttribute("role", "separator");
+        }
+    }
+    exports.WoltlabCoreMenuSeparatorElement = WoltlabCoreMenuSeparatorElement;
+    exports.default = WoltlabCoreMenuSeparatorElement;
+    window.customElements.define("woltlab-core-menu-separator", WoltlabCoreMenuSeparatorElement);
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Element/woltlab-core-menu.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Element/woltlab-core-menu.js
new file mode 100644 (file)
index 0000000..f2d0f35
--- /dev/null
@@ -0,0 +1,21 @@
+define(["require", "exports"], function (require, exports) {
+    "use strict";
+    Object.defineProperty(exports, "__esModule", { value: true });
+    exports.WoltlabCoreMenuElement = void 0;
+    class WoltlabCoreMenuElement extends HTMLElement {
+        connectedCallback() {
+            this.setAttribute("role", "menu");
+            this.label = this.getAttribute("label");
+        }
+        get label() {
+            return this.getAttribute("label");
+        }
+        set label(label) {
+            this.setAttribute("label", label);
+            this.setAttribute("aria-label", label);
+        }
+    }
+    exports.WoltlabCoreMenuElement = WoltlabCoreMenuElement;
+    exports.default = WoltlabCoreMenuElement;
+    window.customElements.define("woltlab-core-menu", WoltlabCoreMenuElement);
+});