import { html, PropertyValues, unsafeCSS } from "lit";
import { classMap } from "lit/directives/class-map.js";
import { BaseElement } from "../../core/base-element.ts";
import { oneOf } from "../../core/property-guards.ts";
import { toggleStyles } from "./styles.ts";
/** @deprecated Use ComponentSize instead */
export type ToggleSize = "sm" | "md" | "lg";
/**
* CFToggle - Toggle button that can be pressed/unpressed with multiple variants and sizes
*
* @element cf-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: "sm" | "md" | "lg" (default: "md")
* @attr {string} value - Value attribute for use in toggle groups
*
* @slot - Default slot for toggle content
*
* @fires cf-change - Fired on toggle with detail: { pressed }
*
* @example
* Bold
*/
export type ToggleVariant = "default" | "outline";
const toggleVariants = ["default", "outline"] as const;
const toggleSizes = ["sm", "md", "lg"] as const;
export class CFToggle extends BaseElement {
// No delegatesFocus — the host owns role="button", tabindex, and all
// event handlers. Focus stays on the host; the inner button is purely
// visual and aria-hidden.
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: "sm" | "md" | "lg";
declare ariaLabel: string;
private _buttonElement: HTMLButtonElement | null = null;
constructor() {
super();
this.pressed = false;
this.disabled = false;
this.variant = "default";
this.size = "md";
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) {
// cf-change is emitted from click/keydown handlers only — NOT here.
// Emitting from updated() causes infinite loops when a parent
// (cf-toggle-group) programmatically sets pressed.
if (changedProperties.has("pressed") || changedProperties.has("disabled")) {
this._updateAriaAttributes();
}
}
protected override willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (changedProperties.has("variant") || changedProperties.has("size")) {
this.variant = oneOf(this.variant, toggleVariants, "default");
this.size = oneOf(this.size, toggleSizes, "md");
}
}
override connectedCallback() {
if (!this.hasAttribute("role")) {
this.setAttribute("role", "button");
}
if (!this.hasAttribute("exportparts")) {
this.setAttribute("exportparts", "toggle");
}
super.connectedCallback();
this._updateAriaAttributes();
// 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);
}
private _updateAriaAttributes(): void {
this.setAttribute("aria-pressed", this.pressed ? "true" : "false");
this.setAttribute("aria-disabled", this.disabled ? "true" : "false");
this.tabIndex = this.disabled ? -1 : 0;
}
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;
}
this.pressed = !this.pressed;
this.emit("cf-change", { pressed: this.pressed });
};
private handleKeydown = (event: KeyboardEvent): void => {
if (this.disabled) return;
if (event.key === " " || event.key === "Enter") {
event.preventDefault();
this.pressed = !this.pressed;
this.emit("cf-change", { pressed: this.pressed });
}
};
/**
* Toggle the pressed state programmatically
*/
toggle(): void {
if (!this.disabled) {
this.pressed = !this.pressed;
}
}
/**
* Focus the toggle programmatically
*/
override focus(): void {
super.focus();
}
/**
* Blur the toggle programmatically
*/
override blur(): void {
super.blur();
}
}