import { html, PropertyValues, unsafeCSS } from "lit"; import { classMap } from "lit/directives/class-map.js"; import { BaseElement } from "../../core/base-element.ts"; import { toggleStyles } from "./styles.ts"; /** * CTToggle - Toggle button that can be pressed/unpressed with multiple variants and sizes * * @element ct-toggle * * @attr {boolean} pressed - Whether the toggle is pressed * @attr {boolean} disabled - Whether the toggle is disabled * @attr {string} variant - Visual style variant: "default" | "outline" * @attr {string} size - Toggle size: "default" | "sm" | "lg" * @attr {string} value - Value attribute for use in toggle groups * * @slot - Default slot for toggle content * * @fires ct-change - Fired on toggle with detail: { pressed } * * @example * Bold */ export type ToggleVariant = "default" | "outline"; export type ToggleSize = "default" | "sm" | "lg"; export class CTToggle extends BaseElement { static override properties = { pressed: { type: Boolean, reflect: true }, disabled: { type: Boolean, reflect: true }, variant: { type: String }, size: { type: String }, ariaLabel: { type: String, attribute: "aria-label" }, }; static override styles = unsafeCSS(toggleStyles); declare pressed: boolean; declare disabled: boolean; declare variant: ToggleVariant; declare size: ToggleSize; declare ariaLabel: string; private _buttonElement: HTMLButtonElement | null = null; constructor() { super(); this.pressed = false; this.disabled = false; this.variant = "default"; this.size = "default"; this.ariaLabel = ""; } get buttonElement(): HTMLButtonElement | null { if (!this._buttonElement) { this._buttonElement = this.shadowRoot?.querySelector("button") as HTMLButtonElement || null; } return this._buttonElement; } override updated(changedProperties: PropertyValues) { if (changedProperties.has("pressed")) { const oldValue = changedProperties.get("pressed"); this.setAttribute("aria-pressed", this.pressed ? "true" : "false"); if (oldValue !== undefined) { this.emit("ct-change", { pressed: this.pressed }); } } if (changedProperties.has("disabled")) { this.setAttribute("aria-disabled", this.disabled ? "true" : "false"); this.setAttribute("tabindex", this.disabled ? "-1" : "0"); } } override connectedCallback() { super.connectedCallback(); // Set ARIA attributes this.setAttribute("role", "button"); this.setAttribute("aria-pressed", this.pressed ? "true" : "false"); if (this.disabled) { this.setAttribute("aria-disabled", "true"); } this.setAttribute("tabindex", this.disabled ? "-1" : "0"); // Add event listeners this.addEventListener("click", this.handleClick); this.addEventListener("keydown", this.handleKeydown); } override disconnectedCallback() { super.disconnectedCallback(); // Clean up event listeners this.removeEventListener("click", this.handleClick); this.removeEventListener("keydown", this.handleKeydown); } override render() { const classes = { toggle: true, [`variant-${this.variant}`]: true, [`size-${this.size}`]: true, pressed: this.pressed, }; return html` `; } private handleClick = (event: Event): void => { if (this.disabled) { event.preventDefault(); event.stopPropagation(); return; } // Toggle the pressed state this.pressed = !this.pressed; }; private handleKeydown = (event: KeyboardEvent): void => { if (this.disabled) return; if (event.key === " " || event.key === "Enter") { event.preventDefault(); this.pressed = !this.pressed; } }; /** * Toggle the pressed state programmatically */ toggle(): void { if (!this.disabled) { this.pressed = !this.pressed; } } /** * Focus the toggle programmatically */ override focus(): void { this.buttonElement?.focus(); } /** * Blur the toggle programmatically */ override blur(): void { this.buttonElement?.blur(); } } globalThis.customElements.define("ct-toggle", CTToggle);