UNPKG

bits-ui

Version:

The headless components for Svelte.

143 lines (142 loc) 5.41 kB
import { attachRef } from "svelte-toolbelt"; import { Context, watch } from "runed"; import { createBitsAttrs, getAriaChecked, getAriaRequired, getDataDisabled, getDataReadonly, getAriaDisabled, } from "../../internal/attrs.js"; import { kbd } from "../../internal/kbd.js"; import { RovingFocusGroup } from "../../internal/roving-focus-group.js"; const radioGroupAttrs = createBitsAttrs({ component: "radio-group", parts: ["root", "item"], }); const RadioGroupRootContext = new Context("RadioGroup.Root"); export class RadioGroupRootState { static create(opts) { return RadioGroupRootContext.set(new RadioGroupRootState(opts)); } opts; hasValue = $derived.by(() => this.opts.value.current !== ""); rovingFocusGroup; attachment; constructor(opts) { this.opts = opts; this.attachment = attachRef(this.opts.ref); this.rovingFocusGroup = new RovingFocusGroup({ rootNode: this.opts.ref, candidateAttr: radioGroupAttrs.item, loop: this.opts.loop, orientation: this.opts.orientation, }); } isChecked(value) { return this.opts.value.current === value; } setValue(value) { this.opts.value.current = value; } props = $derived.by(() => ({ id: this.opts.id.current, role: "radiogroup", "aria-required": getAriaRequired(this.opts.required.current), "aria-disabled": getAriaDisabled(this.opts.disabled.current), "aria-readonly": this.opts.readonly.current ? "true" : undefined, "data-disabled": getDataDisabled(this.opts.disabled.current), "data-readonly": getDataReadonly(this.opts.readonly.current), "data-orientation": this.opts.orientation.current, [radioGroupAttrs.root]: "", ...this.attachment, })); } export class RadioGroupItemState { static create(opts) { return new RadioGroupItemState(opts, RadioGroupRootContext.get()); } opts; root; attachment; checked = $derived.by(() => this.root.opts.value.current === this.opts.value.current); #isDisabled = $derived.by(() => this.opts.disabled.current || this.root.opts.disabled.current); #isReadonly = $derived.by(() => this.root.opts.readonly.current); #isChecked = $derived.by(() => this.root.isChecked(this.opts.value.current)); #tabIndex = $state(-1); constructor(opts, root) { this.opts = opts; this.root = root; this.attachment = attachRef(this.opts.ref); if (this.opts.value.current === this.root.opts.value.current) { this.root.rovingFocusGroup.setCurrentTabStopId(this.opts.id.current); this.#tabIndex = 0; } else if (!this.root.opts.value.current) { this.#tabIndex = 0; } $effect(() => { this.#tabIndex = this.root.rovingFocusGroup.getTabIndex(this.opts.ref.current); }); watch([() => this.opts.value.current, () => this.root.opts.value.current], () => { if (this.opts.value.current === this.root.opts.value.current) { this.root.rovingFocusGroup.setCurrentTabStopId(this.opts.id.current); this.#tabIndex = 0; } }); this.onclick = this.onclick.bind(this); this.onkeydown = this.onkeydown.bind(this); this.onfocus = this.onfocus.bind(this); } onclick(_) { if (this.opts.disabled.current || this.#isReadonly) return; this.root.setValue(this.opts.value.current); } onfocus(_) { if (!this.root.hasValue || this.#isReadonly) return; this.root.setValue(this.opts.value.current); } onkeydown(e) { if (this.#isDisabled) return; if (e.key === kbd.SPACE) { e.preventDefault(); if (!this.#isReadonly) { this.root.setValue(this.opts.value.current); } return; } this.root.rovingFocusGroup.handleKeydown(this.opts.ref.current, e, true); } snippetProps = $derived.by(() => ({ checked: this.#isChecked })); props = $derived.by(() => ({ id: this.opts.id.current, disabled: this.#isDisabled ? true : undefined, "data-value": this.opts.value.current, "data-orientation": this.root.opts.orientation.current, "data-disabled": getDataDisabled(this.#isDisabled), "data-readonly": getDataReadonly(this.#isReadonly), "data-state": this.#isChecked ? "checked" : "unchecked", "aria-checked": getAriaChecked(this.#isChecked, false), [radioGroupAttrs.item]: "", type: "button", role: "radio", tabindex: this.#tabIndex, // onkeydown: this.onkeydown, onfocus: this.onfocus, onclick: this.onclick, ...this.attachment, })); } export class RadioGroupInputState { static create() { return new RadioGroupInputState(RadioGroupRootContext.get()); } root; shouldRender = $derived.by(() => this.root.opts.name.current !== undefined); props = $derived.by(() => ({ name: this.root.opts.name.current, value: this.root.opts.value.current, required: this.root.opts.required.current, disabled: this.root.opts.disabled.current, })); constructor(root) { this.root = root; } }