UNPKG

bits-ui

Version:

The headless components for Svelte.

211 lines (210 loc) 7.33 kB
import { attachRef } from "svelte-toolbelt"; import { Context, watch } from "runed"; import { createBitsAttrs, getAriaChecked, getAriaReadonly, getAriaRequired, getDataDisabled, getDataReadonly, } from "../../internal/attrs.js"; import { kbd } from "../../internal/kbd.js"; import { arraysAreEqual } from "../../internal/arrays.js"; const checkboxAttrs = createBitsAttrs({ component: "checkbox", parts: ["root", "group", "group-label", "input"], }); export const CheckboxGroupContext = new Context("Checkbox.Group"); export class CheckboxGroupState { static create(opts) { return CheckboxGroupContext.set(new CheckboxGroupState(opts)); } opts; attachment; labelId = $state(undefined); constructor(opts) { this.opts = opts; this.attachment = attachRef(this.opts.ref); } addValue(checkboxValue) { if (!checkboxValue) return; if (!this.opts.value.current.includes(checkboxValue)) { const newValue = [...$state.snapshot(this.opts.value.current), checkboxValue]; this.opts.value.current = newValue; if (arraysAreEqual(this.opts.value.current, newValue)) return; this.opts.onValueChange.current(newValue); } } removeValue(checkboxValue) { if (!checkboxValue) return; const index = this.opts.value.current.indexOf(checkboxValue); if (index === -1) return; const newValue = this.opts.value.current.filter((v) => v !== checkboxValue); this.opts.value.current = newValue; if (arraysAreEqual(this.opts.value.current, newValue)) return; this.opts.onValueChange.current(newValue); } props = $derived.by(() => ({ id: this.opts.id.current, role: "group", "aria-labelledby": this.labelId, "data-disabled": getDataDisabled(this.opts.disabled.current), [checkboxAttrs.group]: "", ...this.attachment, })); } export class CheckboxGroupLabelState { static create(opts) { return new CheckboxGroupLabelState(opts, CheckboxGroupContext.get()); } opts; group; attachment; constructor(opts, group) { this.opts = opts; this.group = group; this.group.labelId = this.opts.id.current; this.attachment = attachRef(this.opts.ref); watch.pre(() => this.opts.id.current, (id) => { this.group.labelId = id; }); } props = $derived.by(() => ({ id: this.opts.id.current, "data-disabled": getDataDisabled(this.group.opts.disabled.current), [checkboxAttrs["group-label"]]: "", ...this.attachment, })); } const CheckboxRootContext = new Context("Checkbox.Root"); export class CheckboxRootState { static create(opts, group = null) { return CheckboxRootContext.set(new CheckboxRootState(opts, group)); } opts; group; trueName = $derived.by(() => { if (this.group && this.group.opts.name.current) return this.group.opts.name.current; return this.opts.name.current; }); trueRequired = $derived.by(() => { if (this.group && this.group.opts.required.current) return true; return this.opts.required.current; }); trueDisabled = $derived.by(() => { if (this.group && this.group.opts.disabled.current) return true; return this.opts.disabled.current; }); trueReadonly = $derived.by(() => { if (this.group && this.group.opts.readonly.current) return true; return this.opts.readonly.current; }); attachment; constructor(opts, group) { this.opts = opts; this.group = group; this.attachment = attachRef(this.opts.ref); this.onkeydown = this.onkeydown.bind(this); this.onclick = this.onclick.bind(this); watch.pre([() => $state.snapshot(this.group?.opts.value.current), () => this.opts.value.current], ([groupValue, value]) => { if (!groupValue || !value) return; this.opts.checked.current = groupValue.includes(value); }); watch.pre(() => this.opts.checked.current, (checked) => { if (!this.group) return; if (checked) { this.group?.addValue(this.opts.value.current); } else { this.group?.removeValue(this.opts.value.current); } }); } onkeydown(e) { if (this.trueDisabled || this.trueReadonly) return; if (e.key === kbd.ENTER) e.preventDefault(); if (e.key === kbd.SPACE) { e.preventDefault(); this.#toggle(); } } #toggle() { if (this.opts.indeterminate.current) { this.opts.indeterminate.current = false; this.opts.checked.current = true; } else { this.opts.checked.current = !this.opts.checked.current; } } onclick(e) { if (this.trueDisabled || this.trueReadonly) return; if (this.opts.type.current === "submit") { this.#toggle(); return; } e.preventDefault(); this.#toggle(); } snippetProps = $derived.by(() => ({ checked: this.opts.checked.current, indeterminate: this.opts.indeterminate.current, })); props = $derived.by(() => ({ id: this.opts.id.current, role: "checkbox", type: this.opts.type.current, disabled: this.trueDisabled, "aria-checked": getAriaChecked(this.opts.checked.current, this.opts.indeterminate.current), "aria-required": getAriaRequired(this.trueRequired), "aria-readonly": getAriaReadonly(this.trueReadonly), "data-disabled": getDataDisabled(this.trueDisabled), "data-readonly": getDataReadonly(this.trueReadonly), "data-state": getCheckboxDataState(this.opts.checked.current, this.opts.indeterminate.current), [checkboxAttrs.root]: "", // onclick: this.onclick, onkeydown: this.onkeydown, ...this.attachment, })); } export class CheckboxInputState { static create() { return new CheckboxInputState(CheckboxRootContext.get()); } root; trueChecked = $derived.by(() => { if (!this.root.group) return this.root.opts.checked.current; if (this.root.opts.value.current !== undefined && this.root.group.opts.value.current.includes(this.root.opts.value.current)) { return true; } return false; }); shouldRender = $derived.by(() => Boolean(this.root.trueName)); constructor(root) { this.root = root; } props = $derived.by(() => ({ type: "checkbox", checked: this.root.opts.checked.current === true, disabled: this.root.trueDisabled, required: this.root.trueRequired, name: this.root.trueName, value: this.root.opts.value.current, readonly: this.root.trueReadonly, })); } function getCheckboxDataState(checked, indeterminate) { if (indeterminate) return "indeterminate"; return checked ? "checked" : "unchecked"; }