Mats Bosson bf576905a7
Some checks are pending
CI / check (push) Waiting to run
All components, schemas, tests, MCP, and showcase
- 51 headless Web Components (45 core + 6 animation)
- Shared helpers: emit(), part(), listen(), wireLabel(), initialValue()
- Zero `new CustomEvent` or `static #counter` — all use shared utils
- Zod schemas for all 44 core components
- MCP package with discover, inspect, compose, validate tools
- Showcase with Aperture Science theme, M3 Expressive motion
- 81 tests passing, TypeScript strict mode clean
- Signals (~500B), SPA router (~400B), zero dependencies
2026-03-31 20:21:41 +07:00

78 lines
2.0 KiB
TypeScript

import { emit, listen } from "../../shared/helpers";
/** PettyCollapsible — single disclosure wrapper on native details element. */
export class PettyCollapsible extends HTMLElement {
static observedAttributes = ["disabled"];
#cleanup = (): void => {};
get detailsElement(): HTMLDetailsElement | null {
return this.querySelector("details");
}
get isOpen(): boolean {
return this.detailsElement?.open ?? false;
}
/** Opens the collapsible. */
open(): void {
const d = this.detailsElement;
if (d && !this.hasAttribute("disabled")) d.open = true;
}
/** Closes the collapsible. */
close(): void {
const d = this.detailsElement;
if (d) d.open = false;
}
/** Toggles the collapsible open/closed state. */
toggle(): void {
if (this.isOpen) this.close();
else this.open();
}
connectedCallback(): void {
const d = this.detailsElement;
if (!d) return;
this.#syncState();
this.#applyDisabled();
this.#cleanup = listen(d, [["toggle", this.#handleToggle]]);
}
disconnectedCallback(): void {
this.#cleanup();
}
attributeChangedCallback(name: string): void {
if (name === "disabled") this.#applyDisabled();
}
#handleToggle = (): void => {
this.#syncState();
emit(this, "toggle", { open: this.isOpen });
};
#syncState(): void {
const d = this.detailsElement;
this.dataset.state = d?.open ? "open" : "closed";
const summary = d?.querySelector("summary");
if (summary) summary.setAttribute("aria-expanded", String(d?.open ?? false));
}
#applyDisabled(): void {
const disabled = this.hasAttribute("disabled");
const summary = this.detailsElement?.querySelector("summary");
if (!summary) return;
if (disabled) {
summary.setAttribute("aria-disabled", "true");
summary.addEventListener("click", this.#preventToggle);
} else {
summary.removeAttribute("aria-disabled");
summary.removeEventListener("click", this.#preventToggle);
}
}
#preventToggle = (e: Event): void => { e.preventDefault(); };
}