UNPKG

bits-ui

Version:

The headless components for Svelte.

265 lines (264 loc) 8.51 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 toolbarAttrs = createBitsAttrs({ component: "toolbar", parts: ["root", "item", "group", "group-item", "link", "button"], }); const ToolbarRootContext = new Context("Toolbar.Root"); const ToolbarGroupContext = new Context("Toolbar.Group"); export class ToolbarRootState { static create(opts) { return ToolbarRootContext.set(new ToolbarRootState(opts)); } opts; rovingFocusGroup; attachment; constructor(opts) { this.opts = opts; this.attachment = attachRef(this.opts.ref); this.rovingFocusGroup = new RovingFocusGroup({ orientation: this.opts.orientation, loop: this.opts.loop, rootNode: this.opts.ref, candidateAttr: toolbarAttrs.item, }); } props = $derived.by(() => ({ id: this.opts.id.current, role: "toolbar", "data-orientation": this.opts.orientation.current, [toolbarAttrs.root]: "", ...this.attachment, })); } class ToolbarGroupBaseState { opts; root; attachment; constructor(opts, root) { this.opts = opts; this.root = root; this.attachment = attachRef(this.opts.ref); } props = $derived.by(() => ({ id: this.opts.id.current, [toolbarAttrs.group]: "", role: "group", "data-orientation": getDataOrientation(this.root.opts.orientation.current), "data-disabled": getDataDisabled(this.opts.disabled.current), ...this.attachment, })); } class ToolbarGroupSingleState extends ToolbarGroupBaseState { opts; root; isMulti = false; anyPressed = $derived.by(() => this.opts.value.current !== ""); constructor(opts, root) { super(opts, root); this.opts = opts; this.root = root; } includesItem(item) { return this.opts.value.current === item; } toggleItem(item) { if (this.includesItem(item)) { this.opts.value.current = ""; } else { this.opts.value.current = item; } } } class ToolbarGroupMultipleState extends ToolbarGroupBaseState { opts; root; isMulti = true; anyPressed = $derived.by(() => this.opts.value.current.length > 0); constructor(opts, root) { super(opts, root); this.opts = opts; this.root = root; } includesItem(item) { return this.opts.value.current.includes(item); } toggleItem(item) { 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]; } } } export class ToolbarGroupState { static create(opts) { const { type, ...rest } = opts; const rootState = ToolbarRootContext.get(); const groupState = type === "single" ? new ToolbarGroupSingleState(rest, rootState) : new ToolbarGroupMultipleState(rest, rootState); return ToolbarGroupContext.set(groupState); } } export class ToolbarGroupItemState { static create(opts) { const group = ToolbarGroupContext.get(); return new ToolbarGroupItemState(opts, group, group.root); } opts; group; root; attachment; #isDisabled = $derived.by(() => this.opts.disabled.current || this.group.opts.disabled.current); constructor(opts, group, root) { this.opts = opts; this.group = group; this.root = root; this.attachment = attachRef(this.opts.ref); $effect(() => { 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.group.toggleItem(this.opts.value.current); } onclick(_) { if (this.#isDisabled) return; this.#toggleItem(); } onkeydown(e) { if (this.#isDisabled) return; if (e.key === kbd.ENTER || e.key === kbd.SPACE) { e.preventDefault(); this.#toggleItem(); return; } this.root.rovingFocusGroup.handleKeydown(this.opts.ref.current, e); } isPressed = $derived.by(() => this.group.includesItem(this.opts.value.current)); #ariaChecked = $derived.by(() => { return this.group.isMulti ? undefined : getAriaChecked(this.isPressed, false); }); #ariaPressed = $derived.by(() => { return this.group.isMulti ? getAriaPressed(this.isPressed) : undefined; }); #tabIndex = $state(0); props = $derived.by(() => ({ id: this.opts.id.current, role: this.group.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, [toolbarAttrs.item]: "", [toolbarAttrs["group-item"]]: "", disabled: getDisabled(this.#isDisabled), // onclick: this.onclick, onkeydown: this.onkeydown, ...this.attachment, })); } export class ToolbarLinkState { static create(opts) { return new ToolbarLinkState(opts, ToolbarRootContext.get()); } opts; root; attachment; constructor(opts, root) { this.opts = opts; this.root = root; this.attachment = attachRef(this.opts.ref); $effect(() => { this.#tabIndex = this.root.rovingFocusGroup.getTabIndex(this.opts.ref.current); }); this.onkeydown = this.onkeydown.bind(this); } onkeydown(e) { this.root.rovingFocusGroup.handleKeydown(this.opts.ref.current, e); } #role = $derived.by(() => { if (!this.opts.ref.current) return undefined; const tagName = this.opts.ref.current.tagName; if (tagName !== "A") return "link"; return undefined; }); #tabIndex = $state(0); props = $derived.by(() => ({ id: this.opts.id.current, [toolbarAttrs.link]: "", [toolbarAttrs.item]: "", role: this.#role, tabindex: this.#tabIndex, "data-orientation": getDataOrientation(this.root.opts.orientation.current), // onkeydown: this.onkeydown, ...this.attachment, })); } export class ToolbarButtonState { static create(opts) { return new ToolbarButtonState(opts, ToolbarRootContext.get()); } opts; root; attachment; constructor(opts, root) { this.opts = opts; this.root = root; this.attachment = attachRef(this.opts.ref); $effect(() => { this.#tabIndex = this.root.rovingFocusGroup.getTabIndex(this.opts.ref.current); }); this.onkeydown = this.onkeydown.bind(this); } onkeydown(e) { this.root.rovingFocusGroup.handleKeydown(this.opts.ref.current, e); } #tabIndex = $state(0); #role = $derived.by(() => { if (!this.opts.ref.current) return undefined; const tagName = this.opts.ref.current.tagName; if (tagName !== "BUTTON") return "button"; return undefined; }); props = $derived.by(() => ({ id: this.opts.id.current, [toolbarAttrs.item]: "", [toolbarAttrs.button]: "", role: this.#role, tabindex: this.#tabIndex, "data-disabled": getDataDisabled(this.opts.disabled.current), "data-orientation": getDataOrientation(this.root.opts.orientation.current), disabled: getDisabled(this.opts.disabled.current), // onkeydown: this.onkeydown, ...this.attachment, })); } // // HELPERS // function getToggleItemDataState(condition) { return condition ? "on" : "off"; }