import { html, PropertyValues, unsafeCSS } from "lit";
import { BaseElement } from "../../core/base-element.ts";
import { toggleGroupStyles } from "./styles.ts";
/**
* CTToggleGroup - Container for managing multiple toggle buttons with single or multiple selection
*
* @element ct-toggle-group
*
* @attr {string} type - Selection type: "single" | "multiple"
* @attr {string} value - Currently selected value(s) - string for single, comma-separated for multiple
* @attr {boolean} disabled - Whether all toggles in the group are disabled
*
* @slot - Default slot for ct-toggle elements
*
* @fires ct-change - Fired on selection change with detail: { value }
*
* @example
*
* Bold
* Italic
* Underline
*
*/
export type ToggleGroupType = "single" | "multiple";
export class CTToggleGroup extends BaseElement {
static override styles = unsafeCSS(toggleGroupStyles);
static override properties = {
type: { type: String },
value: { type: String },
disabled: { type: Boolean, reflect: true },
};
declare type: ToggleGroupType;
declare value: string | string[];
declare disabled: boolean;
constructor() {
super();
this.type = "single";
this.value = "";
this.disabled = false;
}
override willUpdate(changedProperties: PropertyValues) {
if (changedProperties.has("type") && !changedProperties.has("value")) {
// Reset value when type changes
this.value = this.type === "multiple" ? [] : "";
}
// Parse value from attribute for multiple type
if (
changedProperties.has("value") && typeof this.value === "string" &&
this.type === "multiple"
) {
const valueAttr = this.getAttribute("value");
if (valueAttr) {
this.value = valueAttr.split(",").filter((v) => v);
}
}
}
override updated(changedProperties: PropertyValues) {
if (changedProperties.has("type") || changedProperties.has("value")) {
this.updateToggleSelection();
// Update attribute for persistence
if (this.type === "single" && typeof this.value === "string") {
this.setAttribute("value", this.value);
} else if (this.type === "multiple" && Array.isArray(this.value)) {
this.setAttribute("value", this.value.join(","));
}
// Emit change if value changed
if (changedProperties.has("value")) {
const oldValue = changedProperties.get("value");
const changed = this.type === "single"
? oldValue !== this.value
: JSON.stringify(oldValue) !== JSON.stringify(this.value);
if (changed && oldValue !== undefined) {
this.emit("ct-change", { value: this.value });
}
}
}
if (changedProperties.has("disabled")) {
this.updateToggleDisabled();
}
}
override connectedCallback() {
super.connectedCallback();
// Parse value attribute for multiple type
const valueAttr = this.getAttribute("value");
if (valueAttr && this.type === "multiple") {
this.value = valueAttr.split(",").filter((v) => v);
}
// Set ARIA attributes
this.setAttribute("role", "group");
// Add event listeners
this.addEventListener("ct-change", this.handleToggle);
}
override disconnectedCallback() {
super.disconnectedCallback();
// Clean up event listeners
this.removeEventListener("ct-change", this.handleToggle);
}
override firstUpdated() {
this.updateToggleSelection();
this.updateToggleDisabled();
}
override render() {
return html`
`;
}
private handleSlotChange = () => {
this.updateToggleSelection();
this.updateToggleDisabled();
};
private getToggles(): NodeListOf {
return this.querySelectorAll("ct-toggle");
}
private updateToggleSelection(): void {
const toggles = this.getToggles();
toggles.forEach((toggle) => {
const toggleValue = toggle.getAttribute("value") ||
toggle.textContent?.trim() || "";
if (this.type === "single") {
const isPressed = toggleValue === this.value;
toggle.setAttribute("pressed", isPressed ? "" : "false");
(toggle as any).pressed = isPressed;
} else if (this.type === "multiple" && Array.isArray(this.value)) {
const isPressed = this.value.includes(toggleValue);
toggle.setAttribute("pressed", isPressed ? "" : "false");
(toggle as any).pressed = isPressed;
}
});
}
private updateToggleDisabled(): void {
const toggles = this.getToggles();
toggles.forEach((toggle) => {
if (this.disabled) {
toggle.setAttribute("disabled", "");
(toggle as any).disabled = true;
} else if (!toggle.hasAttribute("disabled")) {
// Only enable if the toggle itself doesn't have disabled attribute
(toggle as any).disabled = false;
}
});
}
private handleToggle = (event: Event): void => {
event.stopPropagation();
const customEvent = event as CustomEvent;
const toggle = event.target as Element;
if (!toggle || !toggle.matches("ct-toggle")) return;
const toggleValue = toggle.getAttribute("value") ||
toggle.textContent?.trim() || "";
const isPressed = customEvent.detail.pressed;
if (this.type === "single") {
if (isPressed) {
// Select this toggle, deselect others
this.value = toggleValue;
} else {
// Allow deselecting in single mode
this.value = "";
}
} else if (this.type === "multiple") {
const currentValues = Array.isArray(this.value) ? [...this.value] : [];
if (isPressed) {
// Add to selection if not already present
if (!currentValues.includes(toggleValue)) {
currentValues.push(toggleValue);
}
} else {
// Remove from selection
const index = currentValues.indexOf(toggleValue);
if (index > -1) {
currentValues.splice(index, 1);
}
}
this.value = currentValues;
}
};
/**
* Get the currently selected value(s)
*/
getValue(): string | string[] {
return this.value;
}
/**
* Set the selected value(s)
*/
setValue(value: string | string[]): void {
this.value = value;
}
/**
* Clear all selections
*/
clear(): void {
this.value = this.type === "multiple" ? [] : "";
}
/**
* Select all toggles (multiple mode only)
*/
selectAll(): void {
if (this.type !== "multiple") return;
const toggles = this.getToggles();
const values: string[] = [];
toggles.forEach((toggle) => {
const value = toggle.getAttribute("value") ||
toggle.textContent?.trim() || "";
if (value) {
values.push(value);
}
});
this.value = values;
}
}
globalThis.customElements.define("ct-toggle-group", CTToggleGroup);