Use listen() helper across more components
This commit is contained in:
parent
dec5f8f1d2
commit
929a6585f0
@ -3,43 +3,43 @@
|
||||
.petty-stagger-hidden { opacity: 0; }
|
||||
|
||||
.petty-stagger-fade-up {
|
||||
animation: pettyFadeUp 0.5s ease both;
|
||||
animation: pettyFadeUp 0.5s cubic-bezier(0.2, 0, 0, 1) both;
|
||||
}
|
||||
.petty-stagger-fade-down {
|
||||
animation: pettyFadeDown 0.5s ease both;
|
||||
animation: pettyFadeDown 0.5s cubic-bezier(0.2, 0, 0, 1) both;
|
||||
}
|
||||
.petty-stagger-fade-left {
|
||||
animation: pettyFadeLeft 0.5s ease both;
|
||||
animation: pettyFadeLeft 0.5s cubic-bezier(0.2, 0, 0, 1) both;
|
||||
}
|
||||
.petty-stagger-fade-right {
|
||||
animation: pettyFadeRight 0.5s ease both;
|
||||
animation: pettyFadeRight 0.5s cubic-bezier(0.2, 0, 0, 1) both;
|
||||
}
|
||||
.petty-stagger-scale {
|
||||
animation: pettyScale 0.5s ease both;
|
||||
animation: pettyScale 0.5s cubic-bezier(0.2, 0, 0, 1) both;
|
||||
}
|
||||
.petty-stagger-blur {
|
||||
animation: pettyBlur 0.6s ease both;
|
||||
animation: pettyBlur 0.5s cubic-bezier(0.2, 0, 0, 1) both;
|
||||
}
|
||||
|
||||
.petty-reveal-hidden { opacity: 0; }
|
||||
|
||||
.petty-reveal-fade-up {
|
||||
animation: pettyFadeUp 0.6s ease both;
|
||||
animation: pettyFadeUp 0.5s cubic-bezier(0.2, 0, 0, 1) both;
|
||||
}
|
||||
.petty-reveal-fade-down {
|
||||
animation: pettyFadeDown 0.6s ease both;
|
||||
animation: pettyFadeDown 0.5s cubic-bezier(0.2, 0, 0, 1) both;
|
||||
}
|
||||
.petty-reveal-fade-left {
|
||||
animation: pettyFadeLeft 0.6s ease both;
|
||||
animation: pettyFadeLeft 0.5s cubic-bezier(0.2, 0, 0, 1) both;
|
||||
}
|
||||
.petty-reveal-fade-right {
|
||||
animation: pettyFadeRight 0.6s ease both;
|
||||
animation: pettyFadeRight 0.5s cubic-bezier(0.2, 0, 0, 1) both;
|
||||
}
|
||||
.petty-reveal-scale {
|
||||
animation: pettyScale 0.6s ease both;
|
||||
animation: pettyScale 0.5s cubic-bezier(0.2, 0, 0, 1) both;
|
||||
}
|
||||
.petty-reveal-blur {
|
||||
animation: pettyBlur 0.7s ease both;
|
||||
animation: pettyBlur 0.5s cubic-bezier(0.2, 0, 0, 1) both;
|
||||
}
|
||||
|
||||
petty-typewriter.petty-typewriter-cursor::after {
|
||||
@ -82,7 +82,7 @@ petty-counter { font-variant-numeric: tabular-nums; }
|
||||
}
|
||||
|
||||
dialog[open] {
|
||||
animation: pettyDialogIn 0.2s ease;
|
||||
animation: pettyDialogIn 0.2s cubic-bezier(0.2, 0, 0, 1);
|
||||
}
|
||||
@keyframes pettyDialogIn {
|
||||
from { opacity: 0; transform: translateY(8px) scale(0.98); }
|
||||
@ -90,7 +90,7 @@ dialog[open] {
|
||||
}
|
||||
|
||||
[popover]:popover-open {
|
||||
animation: pettyPopoverIn 0.15s ease;
|
||||
animation: pettyPopoverIn 0.15s cubic-bezier(0.2, 0, 0, 1);
|
||||
}
|
||||
@keyframes pettyPopoverIn {
|
||||
from { opacity: 0; transform: translateY(-4px); }
|
||||
@ -98,7 +98,7 @@ dialog[open] {
|
||||
}
|
||||
|
||||
[data-part="toast"] {
|
||||
animation: pettySlideIn 0.3s ease;
|
||||
animation: pettySlideIn 0.3s cubic-bezier(0.2, 0, 0, 1);
|
||||
}
|
||||
@keyframes pettySlideIn {
|
||||
from { opacity: 0; transform: translateX(100%); }
|
||||
@ -113,25 +113,10 @@ petty-loading-indicator [data-part="indicator"] {
|
||||
border: 3px solid rgba(54, 192, 241, 0.12);
|
||||
border-top-color: #36c0f1;
|
||||
border-right-color: rgba(54, 192, 241, 0.4);
|
||||
animation: pettyLoadSpin 1.1s linear infinite, pettyLoadPulse 2.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
petty-loading-indicator [data-part="container"]::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid transparent;
|
||||
border-bottom-color: rgba(54, 192, 241, 0.2);
|
||||
border-left-color: rgba(54, 192, 241, 0.1);
|
||||
animation: pettyLoadSpin 2.4s linear infinite reverse;
|
||||
animation: pettyLoadSpin 0.9s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes pettyLoadSpin {
|
||||
to { rotate: 360deg; }
|
||||
}
|
||||
|
||||
@keyframes pettyLoadPulse {
|
||||
0%, 100% { box-shadow: 0 0 8px rgba(54, 192, 241, 0.15); }
|
||||
50% { box-shadow: 0 0 20px rgba(54, 192, 241, 0.35); }
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { listen } from "../../shared/helpers";
|
||||
|
||||
/**
|
||||
* PettyAccordionItem — wraps a single `<details>` element within a PettyAccordion.
|
||||
*
|
||||
@ -34,20 +36,18 @@ export class PettyAccordionItem extends HTMLElement {
|
||||
return this.querySelector("summary")?.textContent?.trim() ?? "";
|
||||
}
|
||||
|
||||
#cleanup = (): void => {};
|
||||
|
||||
/** @internal */
|
||||
connectedCallback(): void {
|
||||
this.#syncState();
|
||||
this.#applyDisabled();
|
||||
const details = this.detailsElement;
|
||||
if (details) {
|
||||
details.addEventListener("toggle", this.#handleToggle);
|
||||
}
|
||||
this.#cleanup = listen(this.detailsElement, [["toggle", this.#handleToggle]]);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
disconnectedCallback(): void {
|
||||
const details = this.detailsElement;
|
||||
details?.removeEventListener("toggle", this.#handleToggle);
|
||||
this.#cleanup();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { listen } from "../../shared/helpers";
|
||||
|
||||
/** PettyAvatar — image with automatic fallback on load error. */
|
||||
export class PettyAvatar extends HTMLElement {
|
||||
connectedCallback(): void {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { signal, effect } from "../../signals";
|
||||
import { emit } from "../../shared/helpers";
|
||||
import { emit, listen } from "../../shared/helpers";
|
||||
|
||||
/** PettyCalendar — month grid with day selection and month navigation. */
|
||||
export class PettyCalendar extends HTMLElement {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { uniqueId } from "../../shared/aria";
|
||||
import { emit } from "../../shared/helpers";
|
||||
import { emit, listen } from "../../shared/helpers";
|
||||
|
||||
/** PettyCombobox — searchable select with popover listbox and keyboard nav. */
|
||||
export class PettyCombobox extends HTMLElement {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { emit } from "../../shared/helpers";
|
||||
import { emit, listen } from "../../shared/helpers";
|
||||
|
||||
/** PettyCommandPalette — search-driven command menu using native dialog. */
|
||||
export class PettyCommandPalette extends HTMLElement {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { emit } from "../../shared/helpers";
|
||||
import { emit, listen } from "../../shared/helpers";
|
||||
|
||||
/** PettyContextMenu — right-click menu using Popover API with keyboard nav. */
|
||||
export class PettyContextMenu extends HTMLElement {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { emit } from "../../shared/helpers";
|
||||
import { emit, listen } from "../../shared/helpers";
|
||||
|
||||
/** PettyDatePicker — date input with calendar popover integration. */
|
||||
export class PettyDatePicker extends HTMLElement {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { emit } from "../../shared/helpers";
|
||||
import { emit, listen } from "../../shared/helpers";
|
||||
|
||||
/** PettyDropdownMenu — action menu built on the Popover API. */
|
||||
export class PettyDropdownMenu extends HTMLElement {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { emit } from "../../shared/helpers";
|
||||
import { emit, listen } from "../../shared/helpers";
|
||||
|
||||
interface SchemaLike {
|
||||
safeParse: (data: unknown) => {
|
||||
@ -37,18 +37,19 @@ export class PettyForm extends HTMLElement {
|
||||
this.#schema = schema;
|
||||
}
|
||||
|
||||
#cleanup = (): void => {};
|
||||
|
||||
/** @internal */
|
||||
connectedCallback(): void {
|
||||
const form = this.querySelector("form");
|
||||
if (!form) return;
|
||||
form.addEventListener("submit", this.#handleSubmit);
|
||||
form.setAttribute("novalidate", "");
|
||||
this.#cleanup = listen(form, [["submit", this.#handleSubmit]]);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
disconnectedCallback(): void {
|
||||
const form = this.querySelector("form");
|
||||
form?.removeEventListener("submit", this.#handleSubmit);
|
||||
this.#cleanup();
|
||||
}
|
||||
|
||||
#handleSubmit = (e: Event): void => {
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { listen } from "../../shared/helpers";
|
||||
|
||||
/** PettyImage — image element with fallback display on load failure. */
|
||||
export class PettyImage extends HTMLElement {
|
||||
connectedCallback(): void {
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { listen } from "../../shared/helpers";
|
||||
|
||||
/** PettyLink — headless anchor wrapper with disabled and external support. */
|
||||
export class PettyLink extends HTMLElement {
|
||||
static observedAttributes = ["disabled", "external"];
|
||||
@ -6,13 +8,15 @@ export class PettyLink extends HTMLElement {
|
||||
return this.querySelector("a");
|
||||
}
|
||||
|
||||
#cleanup = (): void => {};
|
||||
|
||||
connectedCallback(): void {
|
||||
this.#sync();
|
||||
this.addEventListener("click", this.#handleClick);
|
||||
this.#cleanup = listen(this, [["click", this.#handleClick]]);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this.removeEventListener("click", this.#handleClick);
|
||||
this.#cleanup();
|
||||
}
|
||||
|
||||
attributeChangedCallback(): void {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { signal, effect } from "../../signals";
|
||||
import { emit, initialValue } from "../../shared/helpers";
|
||||
import { emit, initialValue, listen } from "../../shared/helpers";
|
||||
|
||||
/** PettyListbox — inline selectable list with single or multiple selection. */
|
||||
export class PettyListbox extends HTMLElement {
|
||||
@ -27,18 +27,18 @@ export class PettyListbox extends HTMLElement {
|
||||
emit(this, "change", { value: this.#value.get() });
|
||||
}
|
||||
|
||||
#cleanup = (): void => {};
|
||||
|
||||
connectedCallback(): void {
|
||||
const init = initialValue(this);
|
||||
if (init) this.#value.set(init);
|
||||
this.#stopEffect = effect(() => this.#syncChildren());
|
||||
this.addEventListener("keydown", this.#onKeydown);
|
||||
this.addEventListener("click", this.#onClick);
|
||||
this.#cleanup = listen(this, [["keydown", this.#onKeydown], ["click", this.#onClick]]);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this.#stopEffect = null;
|
||||
this.removeEventListener("keydown", this.#onKeydown);
|
||||
this.removeEventListener("click", this.#onClick);
|
||||
this.#cleanup();
|
||||
}
|
||||
|
||||
attributeChangedCallback(name: string, _old: string | null, next: string | null): void {
|
||||
@ -59,18 +59,19 @@ export class PettyListbox extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
#onClick = (e: MouseEvent): void => {
|
||||
#onClick = (e: Event): void => {
|
||||
const opt = (e.target as HTMLElement).closest("petty-listbox-option");
|
||||
if (!opt || opt.hasAttribute("disabled")) return;
|
||||
this.selectValue(opt.getAttribute("value") ?? "");
|
||||
};
|
||||
|
||||
#onKeydown = (e: KeyboardEvent): void => {
|
||||
#onKeydown = (e: Event): void => {
|
||||
const ke = e as KeyboardEvent;
|
||||
const items = this.#options();
|
||||
const active = document.activeElement as HTMLElement;
|
||||
const idx = items.indexOf(active);
|
||||
if (e.key === "ArrowDown") { e.preventDefault(); items[(idx + 1) % items.length]?.focus(); }
|
||||
else if (e.key === "ArrowUp") { e.preventDefault(); items[(idx - 1 + items.length) % items.length]?.focus(); }
|
||||
else if (e.key === "Enter" || e.key === " ") { e.preventDefault(); if (active) this.selectValue(active.getAttribute("value") ?? ""); }
|
||||
if (ke.key === "ArrowDown") { ke.preventDefault(); items[(idx + 1) % items.length]?.focus(); }
|
||||
else if (ke.key === "ArrowUp") { ke.preventDefault(); items[(idx - 1 + items.length) % items.length]?.focus(); }
|
||||
else if (ke.key === "Enter" || ke.key === " ") { ke.preventDefault(); if (active) this.selectValue(active.getAttribute("value") ?? ""); }
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { listen } from "../../shared/helpers";
|
||||
|
||||
/** PettyNavigationMenuItem — nav item with optional popover content on hover. */
|
||||
export class PettyNavigationMenuItem extends HTMLElement {
|
||||
#showTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { signal } from "../../signals";
|
||||
import { emit } from "../../shared/helpers";
|
||||
import { emit, listen, part } from "../../shared/helpers";
|
||||
import { uniqueId } from "../../shared/aria";
|
||||
|
||||
/** PettyNumberField — numeric input with increment/decrement buttons and clamping. */
|
||||
@ -7,6 +7,7 @@ export class PettyNumberField extends HTMLElement {
|
||||
static observedAttributes = ["min", "max", "step", "value", "disabled", "name"];
|
||||
|
||||
readonly #value = signal(0);
|
||||
#cleanup = (): void => {};
|
||||
|
||||
get value(): number { return this.#value.get(); }
|
||||
set value(v: number) { this.#applyValue(v); }
|
||||
@ -18,18 +19,14 @@ export class PettyNumberField extends HTMLElement {
|
||||
this.#syncInput();
|
||||
this.#wireLabel();
|
||||
|
||||
input?.addEventListener("input", this.#onInput);
|
||||
input?.addEventListener("keydown", this.#onKeydown);
|
||||
this.querySelector("[data-part=increment]")?.addEventListener("click", this.#onIncrement);
|
||||
this.querySelector("[data-part=decrement]")?.addEventListener("click", this.#onDecrement);
|
||||
const c1 = listen(input, [["input", this.#onInput], ["keydown", this.#onKeydown as EventListener]]);
|
||||
const c2 = listen(part(this, "increment"), [["click", this.#onIncrement]]);
|
||||
const c3 = listen(part(this, "decrement"), [["click", this.#onDecrement]]);
|
||||
this.#cleanup = () => { c1(); c2(); c3(); };
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
const input = this.#input();
|
||||
input?.removeEventListener("input", this.#onInput);
|
||||
input?.removeEventListener("keydown", this.#onKeydown);
|
||||
this.querySelector("[data-part=increment]")?.removeEventListener("click", this.#onIncrement);
|
||||
this.querySelector("[data-part=decrement]")?.removeEventListener("click", this.#onDecrement);
|
||||
this.#cleanup();
|
||||
}
|
||||
|
||||
attributeChangedCallback(name: string, _old: string | null, next: string | null): void {
|
||||
|
||||
@ -1,17 +1,19 @@
|
||||
import { listen } from "../../shared/helpers";
|
||||
|
||||
/** PettyPaginationItem — single page button within a pagination component. */
|
||||
export class PettyPaginationItem extends HTMLElement {
|
||||
static observedAttributes = ["value", "type", "disabled"];
|
||||
|
||||
#cleanup = (): void => {};
|
||||
|
||||
connectedCallback(): void {
|
||||
this.setAttribute("role", "button");
|
||||
this.setAttribute("tabindex", "0");
|
||||
this.addEventListener("click", this.#handleClick);
|
||||
this.addEventListener("keydown", this.#handleKeydown);
|
||||
this.#cleanup = listen(this, [["click", this.#handleClick], ["keydown", this.#handleKeydown]]);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this.removeEventListener("click", this.#handleClick);
|
||||
this.removeEventListener("keydown", this.#handleKeydown);
|
||||
this.#cleanup();
|
||||
}
|
||||
|
||||
attributeChangedCallback(name: string): void {
|
||||
@ -35,7 +37,8 @@ export class PettyPaginationItem extends HTMLElement {
|
||||
}
|
||||
|
||||
#handleClick = (): void => { this.#activate(); };
|
||||
#handleKeydown = (e: KeyboardEvent): void => {
|
||||
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); this.#activate(); }
|
||||
#handleKeydown = (e: Event): void => {
|
||||
const ke = e as KeyboardEvent;
|
||||
if (ke.key === "Enter" || ke.key === " ") { ke.preventDefault(); this.#activate(); }
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { wrapIndex } from "../../shared/keyboard";
|
||||
import { listen } from "../../shared/helpers";
|
||||
|
||||
/** PettyRadioItem — single radio option within a petty-radio-group. */
|
||||
export class PettyRadioItem extends HTMLElement {
|
||||
@ -7,17 +8,17 @@ export class PettyRadioItem extends HTMLElement {
|
||||
get value(): string { return this.getAttribute("value") ?? ""; }
|
||||
get disabled(): boolean { return this.hasAttribute("disabled"); }
|
||||
|
||||
#cleanup = (): void => {};
|
||||
|
||||
connectedCallback(): void {
|
||||
this.setAttribute("role", "radio");
|
||||
this.setAttribute("tabindex", "-1");
|
||||
if (this.disabled) this.setAttribute("aria-disabled", "true");
|
||||
this.addEventListener("click", this.#handleClick);
|
||||
this.addEventListener("keydown", this.#handleKeydown);
|
||||
this.#cleanup = listen(this, [["click", this.#handleClick], ["keydown", this.#handleKeydown]]);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this.removeEventListener("click", this.#handleClick);
|
||||
this.removeEventListener("keydown", this.#handleKeydown);
|
||||
this.#cleanup();
|
||||
}
|
||||
|
||||
attributeChangedCallback(name: string): void {
|
||||
@ -42,17 +43,18 @@ export class PettyRadioItem extends HTMLElement {
|
||||
this.focus();
|
||||
};
|
||||
|
||||
#handleKeydown = (e: KeyboardEvent): void => {
|
||||
#handleKeydown = (e: Event): void => {
|
||||
const ke = e as KeyboardEvent;
|
||||
const isVertical = this.closest("petty-radio-group")?.getAttribute("orientation") !== "horizontal";
|
||||
const prev = isVertical ? "ArrowUp" : "ArrowLeft";
|
||||
const next = isVertical ? "ArrowDown" : "ArrowRight";
|
||||
if (e.key !== prev && e.key !== next && e.key !== " ") return;
|
||||
e.preventDefault();
|
||||
if (e.key === " ") { this.#handleClick(); return; }
|
||||
if (ke.key !== prev && ke.key !== next && ke.key !== " ") return;
|
||||
ke.preventDefault();
|
||||
if (ke.key === " ") { this.#handleClick(); return; }
|
||||
const items = this.#siblings();
|
||||
const idx = items.indexOf(this);
|
||||
if (idx === -1) return;
|
||||
const delta = e.key === next ? 1 : -1;
|
||||
const delta = ke.key === next ? 1 : -1;
|
||||
const target = items[wrapIndex(idx, delta, items.length)];
|
||||
if (target) {
|
||||
this.#group()?.selectValue(target.value);
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { emit } from "../../shared/helpers";
|
||||
import { uniqueId } from "../../shared/aria";
|
||||
import { emit, listen, part, wireLabel } from "../../shared/helpers";
|
||||
|
||||
/** PettySlider — range input wrapper with label, output, and change events. */
|
||||
export class PettySlider extends HTMLElement {
|
||||
static observedAttributes = ["min", "max", "step", "value", "disabled", "name", "orientation"];
|
||||
|
||||
#cleanup = (): void => {};
|
||||
|
||||
get value(): number {
|
||||
return Number(this.#input()?.value ?? 0);
|
||||
}
|
||||
@ -18,13 +19,13 @@ export class PettySlider extends HTMLElement {
|
||||
const input = this.#input();
|
||||
if (!input) return;
|
||||
this.#syncAttrs();
|
||||
this.#wireLabel();
|
||||
wireLabel(input, part(this, "label"), "petty-slider");
|
||||
this.#syncOutput();
|
||||
input.addEventListener("input", this.#handleInput);
|
||||
this.#cleanup = listen(input, [["input", this.#handleInput]]);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this.#input()?.removeEventListener("input", this.#handleInput);
|
||||
this.#cleanup();
|
||||
}
|
||||
|
||||
attributeChangedCallback(): void {
|
||||
@ -36,15 +37,6 @@ export class PettySlider extends HTMLElement {
|
||||
return this.querySelector("input[data-part=control]");
|
||||
}
|
||||
|
||||
#wireLabel(): void {
|
||||
const input = this.#input();
|
||||
const label = this.querySelector("[data-part=label]");
|
||||
if (input && label) {
|
||||
if (!input.id) input.id = uniqueId("petty-slider");
|
||||
(label as HTMLLabelElement).htmlFor = input.id;
|
||||
}
|
||||
}
|
||||
|
||||
#syncAttrs(): void {
|
||||
const input = this.#input();
|
||||
if (!input) return;
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { listen } from "../../shared/helpers";
|
||||
|
||||
/**
|
||||
* PettyTab — individual tab button within a petty-tabs component.
|
||||
*
|
||||
@ -19,17 +21,17 @@ export class PettyTab extends HTMLElement {
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
#cleanup = (): void => {};
|
||||
|
||||
connectedCallback(): void {
|
||||
this.setAttribute("role", "tab");
|
||||
this.setAttribute("tabindex", "-1");
|
||||
this.addEventListener("click", this.#handleClick);
|
||||
this.addEventListener("keydown", this.#handleKeydown);
|
||||
this.#cleanup = listen(this, [["click", this.#handleClick], ["keydown", this.#handleKeydown]]);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
disconnectedCallback(): void {
|
||||
this.removeEventListener("click", this.#handleClick);
|
||||
this.removeEventListener("keydown", this.#handleKeydown);
|
||||
this.#cleanup();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
@ -48,12 +50,13 @@ export class PettyTab extends HTMLElement {
|
||||
active?.focus();
|
||||
};
|
||||
|
||||
#handleKeydown = (e: KeyboardEvent): void => {
|
||||
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
|
||||
#handleKeydown = (e: Event): void => {
|
||||
const ke = e as KeyboardEvent;
|
||||
if (ke.key !== "ArrowLeft" && ke.key !== "ArrowRight") return;
|
||||
const tabs = this.#siblingTabs().filter(t => !t.disabled);
|
||||
const idx = tabs.indexOf(this);
|
||||
if (idx === -1) return;
|
||||
const nextIdx = e.key === "ArrowRight"
|
||||
const nextIdx = ke.key === "ArrowRight"
|
||||
? (idx + 1) % tabs.length
|
||||
: (idx - 1 + tabs.length) % tabs.length;
|
||||
const next = tabs[nextIdx];
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { signal, effect } from "../../signals";
|
||||
import { emit } from "../../shared/helpers";
|
||||
import { emit, listen } from "../../shared/helpers";
|
||||
|
||||
/** PettyTagsInput — multi-value text input for tags, emails, or tokens. */
|
||||
export class PettyTagsInput extends HTMLElement {
|
||||
@ -41,18 +41,21 @@ export class PettyTagsInput extends HTMLElement {
|
||||
this.#dispatch();
|
||||
}
|
||||
|
||||
#cleanupInput = (): void => {};
|
||||
#cleanupSelf = (): void => {};
|
||||
|
||||
connectedCallback(): void {
|
||||
const init = this.getAttribute("value");
|
||||
if (init) this.#tags.set(init.split(",").map(s => s.trim()).filter(Boolean));
|
||||
this.#stopEffect = effect(() => this.#render());
|
||||
this.#input()?.addEventListener("keydown", this.#onKeydown);
|
||||
this.addEventListener("click", this.#onTagRemove);
|
||||
this.#cleanupInput = listen(this.#input(), [["keydown", this.#onKeydown]]);
|
||||
this.#cleanupSelf = listen(this, [["click", this.#onTagRemove]]);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this.#stopEffect = null;
|
||||
this.#input()?.removeEventListener("keydown", this.#onKeydown);
|
||||
this.removeEventListener("click", this.#onTagRemove);
|
||||
this.#cleanupInput();
|
||||
this.#cleanupSelf();
|
||||
}
|
||||
|
||||
attributeChangedCallback(name: string, _old: string | null, next: string | null): void {
|
||||
@ -73,14 +76,15 @@ export class PettyTagsInput extends HTMLElement {
|
||||
if (hidden instanceof HTMLInputElement) hidden.value = this.#tags.get().join(",");
|
||||
}
|
||||
|
||||
#onKeydown = (e: KeyboardEvent): void => {
|
||||
#onKeydown = (e: Event): void => {
|
||||
const ke = e as KeyboardEvent;
|
||||
if (this.hasAttribute("disabled")) return;
|
||||
const input = this.#input();
|
||||
if (!input) return;
|
||||
if (e.key === "Enter" || e.key === ",") {
|
||||
e.preventDefault();
|
||||
if (ke.key === "Enter" || ke.key === ",") {
|
||||
ke.preventDefault();
|
||||
if (this.addTag(input.value)) input.value = "";
|
||||
} else if (e.key === "Backspace" && input.value === "") {
|
||||
} else if (ke.key === "Backspace" && input.value === "") {
|
||||
const tags = this.#tags.get();
|
||||
const last = tags[tags.length - 1];
|
||||
if (last) this.removeTag(last);
|
||||
|
||||
@ -1,20 +1,22 @@
|
||||
import { uniqueId } from "../../shared/aria";
|
||||
import { emit } from "../../shared/helpers";
|
||||
import { emit, listen, part } from "../../shared/helpers";
|
||||
|
||||
/** PettyTextField — labeled text input with description and error wiring. */
|
||||
export class PettyTextField extends HTMLElement {
|
||||
static observedAttributes = ["name", "disabled", "required"];
|
||||
|
||||
#cleanup = (): void => {};
|
||||
|
||||
connectedCallback(): void {
|
||||
const name = this.getAttribute("name") ?? "";
|
||||
const controlId = uniqueId(`petty-tf-${name}`);
|
||||
const errorId = `${controlId}-error`;
|
||||
const descId = `${controlId}-desc`;
|
||||
|
||||
const label = this.querySelector("[data-part=label]");
|
||||
const label = part<HTMLLabelElement>(this, "label");
|
||||
const control = this.#control();
|
||||
const desc = this.querySelector("[data-part=description]");
|
||||
const error = this.querySelector("[data-part=error]");
|
||||
const desc = part(this, "description");
|
||||
const error = part(this, "error");
|
||||
|
||||
if (control) {
|
||||
control.id = controlId;
|
||||
@ -22,16 +24,16 @@ export class PettyTextField extends HTMLElement {
|
||||
const describedBy = [desc ? descId : "", error ? errorId : ""].filter(Boolean).join(" ");
|
||||
if (describedBy) control.setAttribute("aria-describedby", describedBy);
|
||||
}
|
||||
if (label && control) (label as HTMLLabelElement).htmlFor = controlId;
|
||||
if (label && control) label.htmlFor = controlId;
|
||||
if (desc) desc.id = descId;
|
||||
if (error) error.id = errorId;
|
||||
|
||||
this.#syncAttrs();
|
||||
control?.addEventListener("input", this.#handleInput);
|
||||
this.#cleanup = listen(control, [["input", this.#handleInput]]);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this.#control()?.removeEventListener("input", this.#handleInput);
|
||||
this.#cleanup();
|
||||
}
|
||||
|
||||
attributeChangedCallback(): void {
|
||||
@ -40,7 +42,7 @@ export class PettyTextField extends HTMLElement {
|
||||
|
||||
/** Display an error message on this field. */
|
||||
setError(message: string): void {
|
||||
const error = this.querySelector("[data-part=error]");
|
||||
const error = part(this, "error");
|
||||
const control = this.#control();
|
||||
if (error) error.textContent = message;
|
||||
if (control) control.setAttribute("aria-invalid", "true");
|
||||
@ -48,7 +50,7 @@ export class PettyTextField extends HTMLElement {
|
||||
|
||||
/** Clear the error message on this field. */
|
||||
clearError(): void {
|
||||
const error = this.querySelector("[data-part=error]");
|
||||
const error = part(this, "error");
|
||||
const control = this.#control();
|
||||
if (error) error.textContent = "";
|
||||
if (control) control.removeAttribute("aria-invalid");
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { wrapIndex } from "../../shared/keyboard";
|
||||
import { listen } from "../../shared/helpers";
|
||||
|
||||
/** PettyToggleGroupItem — single item within a toggle group. */
|
||||
export class PettyToggleGroupItem extends HTMLElement {
|
||||
@ -7,17 +8,17 @@ export class PettyToggleGroupItem extends HTMLElement {
|
||||
get value(): string { return this.getAttribute("value") ?? ""; }
|
||||
get disabled(): boolean { return this.hasAttribute("disabled"); }
|
||||
|
||||
#cleanup = (): void => {};
|
||||
|
||||
connectedCallback(): void {
|
||||
this.setAttribute("role", "button");
|
||||
this.setAttribute("tabindex", "0");
|
||||
if (this.disabled) this.setAttribute("aria-disabled", "true");
|
||||
this.addEventListener("click", this.#handleClick);
|
||||
this.addEventListener("keydown", this.#handleKeydown);
|
||||
this.#cleanup = listen(this, [["click", this.#handleClick], ["keydown", this.#handleKeydown]]);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this.removeEventListener("click", this.#handleClick);
|
||||
this.removeEventListener("keydown", this.#handleKeydown);
|
||||
this.#cleanup();
|
||||
}
|
||||
|
||||
attributeChangedCallback(name: string): void {
|
||||
@ -39,21 +40,22 @@ export class PettyToggleGroupItem extends HTMLElement {
|
||||
this.#group()?.toggleValue(this.value);
|
||||
};
|
||||
|
||||
#handleKeydown = (e: KeyboardEvent): void => {
|
||||
if (e.key === " " || e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
#handleKeydown = (e: Event): void => {
|
||||
const ke = e as KeyboardEvent;
|
||||
if (ke.key === " " || ke.key === "Enter") {
|
||||
ke.preventDefault();
|
||||
this.#handleClick();
|
||||
return;
|
||||
}
|
||||
const isHorizontal = this.closest("petty-toggle-group")?.getAttribute("orientation") !== "vertical";
|
||||
const prev = isHorizontal ? "ArrowLeft" : "ArrowUp";
|
||||
const next = isHorizontal ? "ArrowRight" : "ArrowDown";
|
||||
if (e.key !== prev && e.key !== next) return;
|
||||
e.preventDefault();
|
||||
if (ke.key !== prev && ke.key !== next) return;
|
||||
ke.preventDefault();
|
||||
const items = this.#siblings();
|
||||
const idx = items.indexOf(this);
|
||||
if (idx === -1) return;
|
||||
const delta = e.key === next ? 1 : -1;
|
||||
const delta = ke.key === next ? 1 : -1;
|
||||
items[wrapIndex(idx, delta, items.length)]?.focus();
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { emit } from "../../shared/helpers";
|
||||
import { emit, listen } from "../../shared/helpers";
|
||||
|
||||
/** PettyVirtualList — windowed scroll rendering only visible items plus overscan. */
|
||||
export class PettyVirtualList extends HTMLElement {
|
||||
|
||||
@ -46,7 +46,6 @@ dialog {
|
||||
|
||||
dialog::backdrop {
|
||||
background: rgba(0 0 0 / 0.4);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
/* ── Select ─────────────────────────────────────────────────────────────── */
|
||||
@ -234,7 +233,13 @@ petty-toast-region {
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
padding: 0.125rem 0.25rem;
|
||||
padding: 0.5rem;
|
||||
min-width: 2.75rem;
|
||||
min-height: 2.75rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--petty-radius);
|
||||
}
|
||||
|
||||
[data-part="toast-close"]:hover { color: var(--petty-text); }
|
||||
@ -265,10 +270,11 @@ petty-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-part="control"]:focus {
|
||||
[data-part="control"]:focus-visible {
|
||||
border-color: var(--petty-border-focus);
|
||||
box-shadow: 0 0 0 3px rgba(37 99 235 / 0.15);
|
||||
outline: none;
|
||||
outline: 2px solid var(--petty-border-focus);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
[data-part="control"][aria-invalid="true"] {
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<title>PettyUI — Component Showcase</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||
<script type="module">
|
||||
import "pettyui/button"; import "pettyui/link"; import "pettyui/separator";
|
||||
import "pettyui/badge"; import "pettyui/skeleton"; import "pettyui/avatar";
|
||||
@ -88,7 +88,7 @@
|
||||
<h3>Text Field</h3>
|
||||
<petty-text-field name="email">
|
||||
<label data-part="label">Email address</label>
|
||||
<input data-part="control" type="email" placeholder="you@example.com" />
|
||||
<input data-part="control" type="email" placeholder="you@example.com" aria-label="Email address" />
|
||||
<span data-part="description">We will never share your email.</span>
|
||||
<span data-part="error"></span>
|
||||
</petty-text-field>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user