Use listen() helper across more components

This commit is contained in:
Mats Bosson 2026-04-01 01:42:10 +07:00
parent dec5f8f1d2
commit 929a6585f0
26 changed files with 419 additions and 339 deletions

View File

@ -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); }
}

View File

@ -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 */

View File

@ -1,3 +1,5 @@
import { listen } from "../../shared/helpers";
/** PettyAvatar — image with automatic fallback on load error. */
export class PettyAvatar extends HTMLElement {
connectedCallback(): void {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 => {

View File

@ -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 {

View File

@ -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 {

View File

@ -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") ?? ""); }
};
}

View File

@ -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;

View File

@ -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 {

View File

@ -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(); }
};
}

View File

@ -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);

View File

@ -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;

View File

@ -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];

View File

@ -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);

View File

@ -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");

View File

@ -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();
};
}

View File

@ -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 {

View File

@ -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"] {

View File

@ -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