diff design_system/src/components/button.js @ 251:117c4d53c9a4

[ui] Add HTML-first Web Component system Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Tue, 04 Aug 2026 09:14:57 -0700
parents
children 2b6e732087ff
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/design_system/src/components/button.js	Tue Aug 04 09:14:57 2026 -0700
@@ -0,0 +1,116 @@
+export class ZenButton extends HTMLElement {
+  static get observedAttributes() {
+    return ["disabled", "loading"];
+  }
+
+  connectedCallback() {
+    if (!this._blockInteraction) {
+      this._blockInteraction = event => {
+        if (!this.hasAttribute("disabled") &&
+            !this.hasAttribute("loading")) return;
+        event.preventDefault();
+        event.stopImmediatePropagation();
+      };
+    }
+    this.addEventListener("click", this._blockInteraction, true);
+    if (!this._observer) {
+      this._observer = new MutationObserver(records => {
+        for (const record of records) {
+          for (const node of record.removedNodes) {
+            if (node instanceof Element &&
+                node.matches("button, a")) {
+              this.restoreManagedState(node);
+            }
+          }
+        }
+        this.sync();
+      });
+    }
+    this._observer.observe(this, { childList: true });
+    this.sync();
+  }
+
+  disconnectedCallback() {
+    this._observer?.disconnect();
+    this.removeEventListener("click", this._blockInteraction, true);
+  }
+
+  attributeChangedCallback() {
+    this.sync();
+  }
+
+  sync() {
+    const control = this.querySelector(":scope > button, :scope > a");
+    if (!control) return;
+
+    const disabled =
+      this.hasAttribute("disabled") ||
+      this.hasAttribute("loading");
+
+    if (disabled) {
+      if (control.dataset.zenManagedAriaDisabled !== "true") {
+        control.dataset.zenOriginalAriaDisabled =
+          control.getAttribute("aria-disabled") ?? "__zen_missing__";
+        control.dataset.zenManagedAriaDisabled = "true";
+      }
+      control.setAttribute("aria-disabled", "true");
+    } else {
+      this.restoreDisabledState(control);
+    }
+
+    if (this.hasAttribute("loading")) {
+      if (control.dataset.zenManagedAriaBusy !== "true") {
+        control.dataset.zenOriginalAriaBusy =
+          control.getAttribute("aria-busy") ?? "__zen_missing__";
+        control.dataset.zenManagedAriaBusy = "true";
+      }
+      control.setAttribute("aria-busy", "true");
+    } else if (control.dataset.zenManagedAriaBusy === "true") {
+      if (control.dataset.zenOriginalAriaBusy === "__zen_missing__") {
+        control.removeAttribute("aria-busy");
+      } else {
+        control.setAttribute(
+          "aria-busy",
+          control.dataset.zenOriginalAriaBusy,
+        );
+      }
+      delete control.dataset.zenManagedAriaBusy;
+      delete control.dataset.zenOriginalAriaBusy;
+    }
+  }
+
+  restoreDisabledState(control) {
+    if (control.dataset.zenManagedAriaDisabled === "true") {
+      if (control.dataset.zenOriginalAriaDisabled === "__zen_missing__") {
+        control.removeAttribute("aria-disabled");
+      } else {
+        control.setAttribute(
+          "aria-disabled",
+          control.dataset.zenOriginalAriaDisabled,
+        );
+      }
+      delete control.dataset.zenManagedAriaDisabled;
+      delete control.dataset.zenOriginalAriaDisabled;
+    }
+  }
+
+  restoreManagedState(control) {
+    this.restoreDisabledState(control);
+    if (control.dataset.zenManagedAriaBusy === "true") {
+      if (control.dataset.zenOriginalAriaBusy === "__zen_missing__") {
+        control.removeAttribute("aria-busy");
+      } else {
+        control.setAttribute(
+          "aria-busy",
+          control.dataset.zenOriginalAriaBusy,
+        );
+      }
+      delete control.dataset.zenManagedAriaBusy;
+      delete control.dataset.zenOriginalAriaBusy;
+    }
+  }
+}
+
+if (!customElements.get("zen-button")) {
+  customElements.define("zen-button", ZenButton);
+}