UNPKG

bits-ui

Version:

The headless components for Svelte.

162 lines (161 loc) 5.47 kB
import { attachRef, } from "svelte-toolbelt"; import { Context } from "runed"; import { createBitsAttrs, getAriaChecked, getAriaPressed, getDataDisabled, getDataOrientation, getDisabled, } from "../../internal/attrs.js"; import { kbd } from "../../internal/kbd.js"; import { RovingFocusGroup } from "../../internal/roving-focus-group.js"; export const toggleGroupAttrs = createBitsAttrs({ component: "toggle-group", parts: ["root", "item"], }); const ToggleGroupRootContext = new Context("ToggleGroup.Root"); class ToggleGroupBaseState { opts; rovingFocusGroup; attachment; constructor(opts) { this.opts = opts; this.attachment = attachRef(this.opts.ref); this.rovingFocusGroup = new RovingFocusGroup({ candidateAttr: toggleGroupAttrs.item, rootNode: opts.ref, loop: opts.loop, orientation: opts.orientation, }); } props = $derived.by(() => ({ id: this.opts.id.current, [toggleGroupAttrs.root]: "", role: "group", "data-orientation": getDataOrientation(this.opts.orientation.current), "data-disabled": getDataDisabled(this.opts.disabled.current), ...this.attachment, })); } class ToggleGroupSingleState extends ToggleGroupBaseState { opts; isMulti = false; anyPressed = $derived.by(() => this.opts.value.current !== ""); constructor(opts) { super(opts); this.opts = opts; } includesItem(item) { return this.opts.value.current === item; } toggleItem(item, id) { if (this.includesItem(item)) { this.opts.value.current = ""; } else { this.opts.value.current = item; this.rovingFocusGroup.setCurrentTabStopId(id); } } } class ToggleGroupMultipleState extends ToggleGroupBaseState { opts; isMulti = true; anyPressed = $derived.by(() => this.opts.value.current.length > 0); constructor(opts) { super(opts); this.opts = opts; } includesItem(item) { return this.opts.value.current.includes(item); } toggleItem(item, id) { if (this.includesItem(item)) { this.opts.value.current = this.opts.value.current.filter((v) => v !== item); } else { this.opts.value.current = [...this.opts.value.current, item]; this.rovingFocusGroup.setCurrentTabStopId(id); } } } export class ToggleGroupRootState { static create(opts) { const { type, ...rest } = opts; const rootState = type === "single" ? new ToggleGroupSingleState(rest) : new ToggleGroupMultipleState(rest); return ToggleGroupRootContext.set(rootState); } } export class ToggleGroupItemState { static create(opts) { return new ToggleGroupItemState(opts, ToggleGroupRootContext.get()); } opts; root; attachment; #isDisabled = $derived.by(() => this.opts.disabled.current || this.root.opts.disabled.current); isPressed = $derived.by(() => this.root.includesItem(this.opts.value.current)); #ariaChecked = $derived.by(() => { return this.root.isMulti ? undefined : getAriaChecked(this.isPressed, false); }); #ariaPressed = $derived.by(() => { return this.root.isMulti ? getAriaPressed(this.isPressed) : undefined; }); constructor(opts, root) { this.opts = opts; this.root = root; this.attachment = attachRef(this.opts.ref); $effect(() => { if (!this.root.opts.rovingFocus.current) { this.#tabIndex = 0; } else { this.#tabIndex = this.root.rovingFocusGroup.getTabIndex(this.opts.ref.current); } }); this.onclick = this.onclick.bind(this); this.onkeydown = this.onkeydown.bind(this); } #toggleItem() { if (this.#isDisabled) return; this.root.toggleItem(this.opts.value.current, this.opts.id.current); } onclick(_) { if (this.#isDisabled) return; this.root.toggleItem(this.opts.value.current, this.opts.id.current); } onkeydown(e) { if (this.#isDisabled) return; if (e.key === kbd.ENTER || e.key === kbd.SPACE) { e.preventDefault(); this.#toggleItem(); return; } if (!this.root.opts.rovingFocus.current) return; this.root.rovingFocusGroup.handleKeydown(this.opts.ref.current, e); } #tabIndex = $state(0); snippetProps = $derived.by(() => ({ pressed: this.isPressed, })); props = $derived.by(() => ({ id: this.opts.id.current, role: this.root.isMulti ? undefined : "radio", tabindex: this.#tabIndex, "data-orientation": getDataOrientation(this.root.opts.orientation.current), "data-disabled": getDataDisabled(this.#isDisabled), "data-state": getToggleItemDataState(this.isPressed), "data-value": this.opts.value.current, "aria-pressed": this.#ariaPressed, "aria-checked": this.#ariaChecked, disabled: getDisabled(this.#isDisabled), [toggleGroupAttrs.item]: "", // onclick: this.onclick, onkeydown: this.onkeydown, ...this.attachment, })); } function getToggleItemDataState(condition) { return condition ? "on" : "off"; }