view design_system/src/components/disclosure.js @ 267:a78f5986011f

discover bundled production runtimes Locate Bazel-packaged Copilot and Python binaries by stable suffix instead of hardcoding an external repository prefix. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Fri, 07 Aug 2026 12:58:55 -0700
parents 2b6e732087ff
children
line wrap: on
line source

let nextDisclosureId = 1;

const HTMLElementBase = globalThis.HTMLElement || class {};

function rememberAttribute(state, element, name) {
  let attributes = state.get(element);
  if (!attributes) {
    attributes = new Map();
    state.set(element, attributes);
  }
  if (!attributes.has(name)) {
    attributes.set(name, element.getAttribute(name));
  }
}

function setManagedAttribute(state, element, name, value) {
  rememberAttribute(state, element, name);
  if (value === null) {
    if (element.hasAttribute(name)) element.removeAttribute(name);
  } else if (element.getAttribute(name) !== value) {
    element.setAttribute(name, value);
  }
}

function restoreAttribute(state, element, name) {
  const attributes = state.get(element);
  if (!attributes?.has(name)) return;
  const value = attributes.get(name);
  if (value === null) element.removeAttribute(name);
  else element.setAttribute(name, value);
  attributes.delete(name);
  if (!attributes.size) state.delete(element);
}

function restoreElement(state, element) {
  const attributes = state.get(element);
  if (!attributes) return;
  for (const [name, value] of attributes) {
    if (value === null) element.removeAttribute(name);
    else element.setAttribute(name, value);
  }
  state.delete(element);
}

function restoreAttributes(state) {
  for (const element of [...state.keys()]) restoreElement(state, element);
}

function pruneAttributes(state, retained) {
  for (const element of [...state.keys()]) {
    if (!retained.has(element)) restoreElement(state, element);
  }
}

function uniqueId(element, prefix) {
  const document = element.ownerDocument;
  let id;
  do id = `${prefix}-${nextDisclosureId++}`;
  while ([...document.querySelectorAll("[id]")].some(item => item.id === id));
  return id;
}

function ensureId(state, element, prefix) {
  const document = element.ownerDocument;
  const duplicate = element.id &&
    [...document.querySelectorAll("[id]")].some(
      item => item !== element && item.id === element.id,
    );
  if (!element.id || duplicate) {
    setManagedAttribute(state, element, "id", uniqueId(element, prefix));
  }
  return element.id;
}

function eventElement(event) {
  const target = event.target;
  if (target?.nodeType === 1) return target;
  return target?.parentElement || null;
}

function isDisabled(control) {
  return Boolean(control.disabled) ||
    control.getAttribute("aria-disabled") === "true";
}

function emitChange(element, detail) {
  element.dispatchEvent(new CustomEvent("zen-change", {
    bubbles: true,
    detail,
  }));
}

function ownedElements(host, selector) {
  return [...host.querySelectorAll(selector)].filter(
    element => element.closest(host.localName) === host,
  );
}

function syncDisclosureIcon(details) {
  const summary = details.querySelector(":scope > summary");
  if (!summary) return;
  let icon = summary.querySelector(
    ":scope > zen-icon[data-zen-disclosure-icon]",
  );
  if (!icon) {
    icon = document.createElement("zen-icon");
    icon.dataset.zenDisclosureIcon = "";
    icon.dataset.zenGenerated = "true";
    summary.append(icon);
  }
  if (!icon.hasAttribute("name")) icon.setAttribute("name", "chevron-down");
}

function removeGeneratedDisclosureIcons(host) {
  for (const icon of host.querySelectorAll(
    'zen-icon[data-zen-disclosure-icon][data-zen-generated="true"]',
  )) {
    icon.remove();
  }
}

/**
 * Coordinates native details elements and summary keyboard navigation.
 *
 * @extends {HTMLElement}
 */
export class ZenAccordion extends HTMLElementBase {
  static get observedAttributes() {
    return ["multiple"];
  }

  connectedCallback() {
    this._details ||= new Set();
    this._onToggle ||= event => {
      const opened = event.currentTarget;
      if (!opened.open || this.hasAttribute("multiple")) return;
      for (const details of this._details) {
        if (details !== opened) details.open = false;
      }
    };
    this._onKeydown ||= event => {
      if (!["ArrowUp", "ArrowDown", "Home", "End"].includes(event.key)) {
        return;
      }
      const summary = eventElement(event)?.closest("summary");
      const summaries = this.summaries();
      const index = summaries.indexOf(summary);
      if (index < 0) return;
      event.preventDefault();
      let next = event.key === "Home" ? 0 : summaries.length - 1;
      if (event.key === "ArrowUp") {
        next = (index - 1 + summaries.length) % summaries.length;
      } else if (event.key === "ArrowDown") {
        next = (index + 1) % summaries.length;
      }
      summaries[next]?.focus();
    };
    this.addEventListener("keydown", this._onKeydown);
    this._observer ||= new MutationObserver(() => this.sync());
    this._observer.observe(this, { childList: true, subtree: true });
    this.sync();
  }

  disconnectedCallback() {
    this._observer?.disconnect();
    this.removeEventListener("keydown", this._onKeydown);
    for (const details of this._details || []) {
      details.removeEventListener("toggle", this._onToggle);
    }
    removeGeneratedDisclosureIcons(this);
    this._details?.clear();
  }

  attributeChangedCallback() {
    if (this.isConnected && this._details) this.sync();
  }

  summaries() {
    return [...this._details]
      .map(details => details.querySelector(":scope > summary"))
      .filter(Boolean);
  }

  sync() {
    const current = new Set(
      ownedElements(this, "details").filter(details => {
        const parent = details.parentElement?.closest("details");
        return !parent || parent.closest("zen-accordion") !== this;
      }),
    );
    for (const details of this._details) {
      if (!current.has(details)) {
        details.removeEventListener("toggle", this._onToggle);
      }
    }
    for (const details of current) {
      if (!this._details.has(details)) {
        details.addEventListener("toggle", this._onToggle);
      }
      syncDisclosureIcon(details);
    }
    this._details = current;

    if (!this.hasAttribute("multiple")) {
      const opened = [...current].filter(details => details.open);
      for (const details of opened.slice(1)) details.open = false;
    }
  }
}

/**
 * Styles a single native details disclosure without replacing its behavior.
 *
 * @extends {HTMLElement}
 */
export class ZenCollapsible extends HTMLElementBase {
  connectedCallback() {
    this._observer ||= new MutationObserver(() => this.sync());
    this._observer.observe(this, { childList: true, subtree: true });
    this.sync();
  }

  disconnectedCallback() {
    this._observer?.disconnect();
    removeGeneratedDisclosureIcons(this);
  }

  sync() {
    for (const details of ownedElements(this, "details")) {
      syncDisclosureIcon(details);
    }
  }
}

/**
 * Connects an ARIA tablist with its native content panels.
 *
 * @extends {HTMLElement}
 */
export class ZenTabs extends HTMLElementBase {
  connectedCallback() {
    this._managed ||= new Map();
    this._onClick ||= event => {
      const tab = eventElement(event)?.closest('[role="tab"]');
      if (!this.isTab(tab) || isDisabled(tab)) return;
      this.activate(tab, true);
    };
    this._onKeydown ||= event => {
      const tab = eventElement(event)?.closest('[role="tab"]');
      if (!this.isTab(tab)) return;
      const tabs = this._tabs.filter(candidate => !isDisabled(candidate));
      const index = tabs.indexOf(tab);
      if (index < 0) return;

      let next;
      if (event.key === "Home") next = tabs[0];
      else if (event.key === "End") next = tabs[tabs.length - 1];
      else if (event.key === "ArrowLeft") {
        next = tabs[(index - 1 + tabs.length) % tabs.length];
      } else if (event.key === "ArrowRight") {
        next = tabs[(index + 1) % tabs.length];
      } else {
        return;
      }

      event.preventDefault();
      this._focusTab = next;
      this.render();
      next.focus();
      if (this.getAttribute("activation") !== "manual") {
        this.activate(next, true);
      }
    };
    this.addEventListener("click", this._onClick);
    this.addEventListener("keydown", this._onKeydown);
    this._observer ||= new MutationObserver(() => this.sync());
    this._observer.observe(this, {
      attributeFilter: ["aria-controls", "aria-disabled", "disabled", "id"],
      attributes: true,
      childList: true,
      subtree: true,
    });
    this.sync();
  }

  disconnectedCallback() {
    this._observer?.disconnect();
    this.removeEventListener("click", this._onClick);
    this.removeEventListener("keydown", this._onKeydown);
    restoreAttributes(this._managed);
    this._tablist = null;
    this._tabs = [];
    this._panels = [];
    this._pairs = new Map();
    this._active = null;
    this._focusTab = null;
  }

  isTab(tab) {
    return Boolean(
      tab && this._tabs?.includes(tab) && this._tablist?.contains(tab),
    );
  }

  sync() {
    this._tablist = ownedElements(this, '[role="tablist"]')[0] || null;
    this._tabs = this._tablist
      ? [...this._tablist.querySelectorAll('[role="tab"]')].filter(
        tab => tab.closest("zen-tabs") === this,
      )
      : [];
    this._panels = ownedElements(this, '[role="tabpanel"]');
    pruneAttributes(
      this._managed,
      new Set([...this._tabs, ...this._panels]),
    );

    for (const tab of this._tabs) {
      ensureId(this._managed, tab, "zen-tab");
    }
    for (const panel of this._panels) {
      ensureId(this._managed, panel, "zen-tabpanel");
    }

    const available = new Set(this._panels);
    this._pairs = new Map();
    for (const [index, tab] of this._tabs.entries()) {
      const controlled = tab.getAttribute("aria-controls");
      let panel = this._panels.find(
        candidate => available.has(candidate) && candidate.id === controlled,
      );
      if (!panel && available.has(this._panels[index])) {
        panel = this._panels[index];
      }
      panel ||= [...available][0] || null;
      if (panel) {
        available.delete(panel);
        this._pairs.set(tab, panel);
        setManagedAttribute(
          this._managed,
          tab,
          "aria-controls",
          panel.id,
        );
        setManagedAttribute(
          this._managed,
          panel,
          "aria-labelledby",
          tab.id,
        );
      } else {
        restoreAttribute(this._managed, tab, "aria-controls");
      }
    }
    for (const panel of available) {
      restoreAttribute(this._managed, panel, "aria-labelledby");
    }

    if (!this._tabs.includes(this._active) || isDisabled(this._active)) {
      this._active = this._tabs.find(
        tab => !isDisabled(tab) && tab.getAttribute("aria-selected") === "true",
      ) || this._tabs.find(tab => !isDisabled(tab)) || this._tabs[0] || null;
    }
    if (!this._tabs.includes(this._focusTab) || isDisabled(this._focusTab)) {
      this._focusTab = this._active;
    }
    this.render();
  }

  render() {
    for (const tab of this._tabs) {
      const active = tab === this._active;
      setManagedAttribute(
        this._managed,
        tab,
        "aria-selected",
        String(active),
      );
      setManagedAttribute(
        this._managed,
        tab,
        "tabindex",
        tab === this._focusTab && !isDisabled(tab) ? "0" : "-1",
      );
    }
    const activePanel = this._pairs.get(this._active);
    for (const panel of this._panels) {
      setManagedAttribute(
        this._managed,
        panel,
        "hidden",
        panel === activePanel ? null : "",
      );
    }
  }

  activate(tab, notify) {
    if (!this.isTab(tab) || isDisabled(tab)) return;
    this._focusTab = tab;
    if (tab === this._active) {
      this.render();
      return;
    }
    this._active = tab;
    this.render();
    if (notify) {
      emitChange(this, {
        value: tab.dataset.value ?? tab.getAttribute("value") ?? tab.id,
      });
    }
  }
}

/**
 * Manages the pressed state of one direct native button.
 *
 * @extends {HTMLElement}
 */
export class ZenToggle extends HTMLElementBase {
  static get observedAttributes() {
    return ["pressed"];
  }

  get pressed() {
    return this.hasAttribute("pressed");
  }

  set pressed(value) {
    this.toggleAttribute("pressed", Boolean(value));
  }

  connectedCallback() {
    this._managed ||= new Map();
    this._onClick ||= event => {
      const button = eventElement(event)?.closest("button");
      if (button !== this._button || isDisabled(button)) return;
      this.pressed = !this.pressed;
      emitChange(this, { pressed: this.pressed });
    };
    this.addEventListener("click", this._onClick);
    this._observer ||= new MutationObserver(() => this.sync());
    this._observer.observe(this, { childList: true });
    this.sync();
  }

  disconnectedCallback() {
    this._observer?.disconnect();
    this.removeEventListener("click", this._onClick);
    restoreAttributes(this._managed);
    this._button = null;
  }

  attributeChangedCallback() {
    if (this.isConnected && this._managed) this.sync();
  }

  sync() {
    const button = this.querySelector(":scope > button");
    if (button !== this._button) {
      if (this._button) restoreElement(this._managed, this._button);
      this._button = button;
    }
    if (button) {
      setManagedAttribute(
        this._managed,
        button,
        "aria-pressed",
        String(this.pressed),
      );
    }
  }
}

function buttonValue(button) {
  return button.getAttribute("value") ?? button.dataset.value ?? "";
}

function parseMultipleValue(value) {
  if (!value) return [];
  if (value.trim().startsWith("[")) {
    try {
      const parsed = JSON.parse(value);
      if (Array.isArray(parsed)) return parsed.map(String);
    } catch {
      // Fall through to token parsing.
    }
  }
  return value.trim().split(/[\s,]+/).filter(Boolean);
}

/**
 * Manages single or multiple pressed buttons with roving focus.
 *
 * @extends {HTMLElement}
 */
export class ZenToggleGroup extends HTMLElementBase {
  static get observedAttributes() {
    return ["type", "value"];
  }

  get values() {
    return [...(this._values || [])];
  }

  set values(values) {
    this.setValues(Array.isArray(values) ? values.map(String) : [], false);
  }

  connectedCallback() {
    this._managed ||= new Map();
    this._buttons ||= [];
    if (!this._initialized) {
      this._values = this.initialValues();
      this._initialized = true;
    }
    this._hasConnected = true;
    this._onClick ||= event => {
      const button = eventElement(event)?.closest("button");
      if (!this._buttons.includes(button) || isDisabled(button)) return;
      const value = buttonValue(button);
      this._focusButton = button;
      let values;
      if (this.multiple) {
        values = this._values.includes(value)
          ? this._values.filter(item => item !== value)
          : [...this._values, value];
      } else {
        values = this._values.includes(value) ? [] : [value];
      }
      this.setValues(values, true);
    };
    this._onKeydown ||= event => {
      const button = eventElement(event)?.closest("button");
      const buttons = this._buttons.filter(candidate => !isDisabled(candidate));
      const index = buttons.indexOf(button);
      if (index < 0) return;

      let next;
      if (event.key === "Home") next = buttons[0];
      else if (event.key === "End") next = buttons[buttons.length - 1];
      else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
        next = buttons[(index - 1 + buttons.length) % buttons.length];
      } else if (event.key === "ArrowRight" || event.key === "ArrowDown") {
        next = buttons[(index + 1) % buttons.length];
      } else {
        return;
      }
      event.preventDefault();
      this._focusButton = next;
      this.render();
      next.focus();
    };
    this.addEventListener("click", this._onClick);
    this.addEventListener("keydown", this._onKeydown);
    this._observer ||= new MutationObserver(() => this.sync());
    this._observer.observe(this, {
      attributeFilter: ["aria-disabled", "data-value", "disabled", "value"],
      attributes: true,
      childList: true,
      subtree: true,
    });
    this.sync();
  }

  disconnectedCallback() {
    this._observer?.disconnect();
    this.removeEventListener("click", this._onClick);
    this.removeEventListener("keydown", this._onKeydown);
    restoreAttributes(this._managed);
    this._buttons = [];
    this._focusButton = null;
  }

  attributeChangedCallback(name) {
    if (name === "value" && !this._settingValue &&
        this.hasAttribute("value")) {
      this._values = this.readValue();
      this._initialized = true;
    } else if (name === "value" && !this._settingValue) {
      this._values = [];
      this._initialized = true;
    } else if (name === "type" && !this._hasConnected &&
               this.hasAttribute("value")) {
      this._values = this.readValue();
      this._initialized = true;
    }
    if (this.isConnected && this._managed) this.sync();
  }

  get multiple() {
    return this.getAttribute("type") === "multiple";
  }

  readValue() {
    const value = this.getAttribute("value");
    if (value === null) return [];
    return this.multiple ? parseMultipleValue(value) : [value];
  }

  initialValues() {
    if (this.hasAttribute("value")) return this.readValue();
    const pressed = [...this.querySelectorAll(":scope > button")]
      .filter(button => button.getAttribute("aria-pressed") === "true");
    return pressed.map(buttonValue);
  }

  sync() {
    const buttons = [...this.querySelectorAll(":scope > button")];
    pruneAttributes(this._managed, new Set([this, ...buttons]));
    this._buttons = buttons;
    if (!this.hasAttribute("role")) {
      setManagedAttribute(this._managed, this, "role", "group");
    }
    if (!this.multiple && this._values.length > 1) {
      this._values = this._values.slice(0, 1);
      this.writeValue();
    }
    if (!buttons.includes(this._focusButton) ||
        isDisabled(this._focusButton)) {
      this._focusButton = buttons.find(
        button =>
          !isDisabled(button) &&
          button.getAttribute("tabindex") === "0",
      ) || buttons.find(
        button =>
          !isDisabled(button) && this._values.includes(buttonValue(button)),
      ) || buttons.find(button => !isDisabled(button)) || null;
    }
    this.render();
  }

  render() {
    for (const button of this._buttons) {
      setManagedAttribute(
        this._managed,
        button,
        "aria-pressed",
        String(this._values.includes(buttonValue(button))),
      );
      setManagedAttribute(
        this._managed,
        button,
        "tabindex",
        button === this._focusButton ? "0" : "-1",
      );
    }
  }

  writeValue() {
    this._settingValue = true;
    const value = this.multiple
      ? (this._values.some(item => /[\s,]/.test(item))
        ? JSON.stringify(this._values)
        : this._values.join(" "))
      : this._values[0];
    if (value === undefined || value === "") this.removeAttribute("value");
    else this.setAttribute("value", value);
    this._settingValue = false;
  }

  setValues(values, notify) {
    const unique = [...new Set(values.map(String))];
    this._values = this.multiple ? unique : unique.slice(0, 1);
    this.writeValue();
    if (this.isConnected) this.render();
    if (notify) emitChange(this, { values: this.values });
  }
}

/**
 * Controls a responsive native sidebar region and its trigger.
 *
 * @extends {HTMLElement}
 */
export class ZenSidebar extends HTMLElementBase {
  static get observedAttributes() {
    return ["open"];
  }

  get open() {
    return this.hasAttribute("open");
  }

  set open(value) {
    this.toggleAttribute("open", Boolean(value));
  }

  connectedCallback() {
    this._managed ||= new Map();
    this._onClick ||= event => {
      const trigger = eventElement(event)?.closest(
        "[data-zen-sidebar-trigger]",
      );
      if (trigger !== this._trigger || isDisabled(trigger)) return;
      this.setOpen(!this.open, true);
    };
    this._onKeydown ||= event => {
      if (event.key !== "Escape" || !this.open) return;
      event.preventDefault();
      this.setOpen(false, true);
      this._trigger?.focus();
    };
    this._onMediaChange ||= () => this.syncState();
    this.addEventListener("click", this._onClick);
    this.addEventListener("keydown", this._onKeydown);
    this._observer ||= new MutationObserver(() => this.sync());
    this._observer.observe(this, { childList: true });
    this._media = globalThis.matchMedia?.("(max-width: 640px)") || null;
    if (this._media?.addEventListener) {
      this._media.addEventListener("change", this._onMediaChange);
    } else {
      this._media?.addListener?.(this._onMediaChange);
    }
    this.sync();
  }

  disconnectedCallback() {
    this._observer?.disconnect();
    this.removeEventListener("click", this._onClick);
    this.removeEventListener("keydown", this._onKeydown);
    if (this._media?.removeEventListener) {
      this._media.removeEventListener("change", this._onMediaChange);
    } else {
      this._media?.removeListener?.(this._onMediaChange);
    }
    restoreAttributes(this._managed);
    this._trigger = null;
    this._panel = null;
    this._media = null;
  }

  attributeChangedCallback() {
    if (this.isConnected) this.syncState();
  }

  sync() {
    const trigger = ownedElements(
      this,
      "[data-zen-sidebar-trigger]",
    )[0] || null;
    const panel = ownedElements(
      this,
      "[data-zen-sidebar-panel]",
    )[0] || null;
    if (trigger !== this._trigger || panel !== this._panel) {
      if (this._trigger) restoreElement(this._managed, this._trigger);
      if (this._panel) restoreElement(this._managed, this._panel);
      this._trigger = trigger;
      this._panel = panel;
    }
    if (!trigger || !panel) return;
    const panelId = ensureId(this._managed, panel, "zen-sidebar-panel");
    setManagedAttribute(
      this._managed,
      trigger,
      "aria-controls",
      panelId,
    );
    this.syncState();
  }

  syncState() {
    if (!this._trigger || !this._panel) return;
    setManagedAttribute(
      this._managed,
      this._panel,
      "hidden",
      this.open ? null : "",
    );
    setManagedAttribute(
      this._managed,
      this._trigger,
      "aria-expanded",
      String(this.open),
    );
  }

  setOpen(open, notify) {
    if (this.open === open) return;
    this.open = open;
    this.syncState();
    if (notify) emitChange(this, { open });
  }
}

const definitions = {
  "zen-accordion": ZenAccordion,
  "zen-collapsible": ZenCollapsible,
  "zen-sidebar": ZenSidebar,
  "zen-tabs": ZenTabs,
  "zen-toggle": ZenToggle,
  "zen-toggle-group": ZenToggleGroup,
};

const registry = globalThis.customElements;
if (registry) {
  for (const [name, definition] of Object.entries(definitions)) {
    if (!registry.get(name)) registry.define(name, definition);
  }
}