view design_system/src/components/button.js @ 261:b401627fc49e

Add JRPG mock flows and interactive previews Add scripted mock SSE commands, custom event forwarding, animated chat turns, full-height message navigation, and a cyberpunk resume dossier. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <mrjunejune@users.noreply.github.com>
date Wed, 05 Aug 2026 20:38:32 -0700
parents 2b6e732087ff
children
line wrap: on
line source

/**
 * Enhances a native button or link with disabled and loading states.
 *
 * @extends {HTMLElement}
 */
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);
}