bits-ui
Version:
The headless components for Svelte.
211 lines (210 loc) • 7.33 kB
JavaScript
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";
}