Some checks are pending
CI / check (push) Waiting to run
- 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
78 lines
2.0 KiB
TypeScript
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(); };
|
|
}
|