UNPKG

bits-ui

Version:

The headless components for Svelte.

302 lines (301 loc) 9.24 kB
import { attachRef, box, } from "svelte-toolbelt"; import { Context, watch } from "runed"; import { createBitsAttrs, getAriaExpanded, getDataOpenClosed } from "../../internal/attrs.js"; import { kbd } from "../../internal/kbd.js"; import { OpenChangeComplete } from "../../internal/open-change-complete.js"; const dialogAttrs = createBitsAttrs({ component: "dialog", parts: ["content", "trigger", "overlay", "title", "description", "close", "cancel", "action"], }); const DialogRootContext = new Context("Dialog.Root | AlertDialog.Root"); export class DialogRootState { static create(opts) { return DialogRootContext.set(new DialogRootState(opts)); } opts; triggerNode = $state(null); contentNode = $state(null); descriptionNode = $state(null); contentId = $state(undefined); titleId = $state(undefined); triggerId = $state(undefined); descriptionId = $state(undefined); cancelNode = $state(null); constructor(opts) { this.opts = opts; this.handleOpen = this.handleOpen.bind(this); this.handleClose = this.handleClose.bind(this); new OpenChangeComplete({ ref: box.with(() => this.contentNode), open: this.opts.open, enabled: true, onComplete: () => { this.opts.onOpenChangeComplete.current(this.opts.open.current); }, }); } handleOpen() { if (this.opts.open.current) return; this.opts.open.current = true; } handleClose() { if (!this.opts.open.current) return; this.opts.open.current = false; } getBitsAttr = (part) => { return dialogAttrs.getAttr(part, this.opts.variant.current); }; sharedProps = $derived.by(() => ({ "data-state": getDataOpenClosed(this.opts.open.current), })); } export class DialogTriggerState { static create(opts) { return new DialogTriggerState(opts, DialogRootContext.get()); } opts; root; attachment; constructor(opts, root) { this.opts = opts; this.root = root; this.attachment = attachRef(this.opts.ref, (v) => { this.root.triggerNode = v; this.root.triggerId = v?.id; }); this.onclick = this.onclick.bind(this); this.onkeydown = this.onkeydown.bind(this); } onclick(e) { if (this.opts.disabled.current) return; if (e.button > 0) return; this.root.handleOpen(); } onkeydown(e) { if (this.opts.disabled.current) return; if (e.key === kbd.SPACE || e.key === kbd.ENTER) { e.preventDefault(); this.root.handleOpen(); } } props = $derived.by(() => ({ id: this.opts.id.current, "aria-haspopup": "dialog", "aria-expanded": getAriaExpanded(this.root.opts.open.current), "aria-controls": this.root.contentId, [this.root.getBitsAttr("trigger")]: "", onkeydown: this.onkeydown, onclick: this.onclick, disabled: this.opts.disabled.current ? true : undefined, ...this.root.sharedProps, ...this.attachment, })); } export class DialogCloseState { static create(opts) { return new DialogCloseState(opts, DialogRootContext.get()); } opts; root; attachment; constructor(opts, root) { this.opts = opts; this.root = root; this.attachment = attachRef(this.opts.ref); this.onclick = this.onclick.bind(this); this.onkeydown = this.onkeydown.bind(this); } onclick(e) { if (this.opts.disabled.current) return; if (e.button > 0) return; this.root.handleClose(); } onkeydown(e) { if (this.opts.disabled.current) return; if (e.key === kbd.SPACE || e.key === kbd.ENTER) { e.preventDefault(); this.root.handleClose(); } } props = $derived.by(() => ({ id: this.opts.id.current, [this.root.getBitsAttr(this.opts.variant.current)]: "", onclick: this.onclick, onkeydown: this.onkeydown, disabled: this.opts.disabled.current ? true : undefined, tabindex: 0, ...this.root.sharedProps, ...this.attachment, })); } export class DialogActionState { static create(opts) { return new DialogActionState(opts, DialogRootContext.get()); } 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, [this.root.getBitsAttr("action")]: "", ...this.root.sharedProps, ...this.attachment, })); } export class DialogTitleState { static create(opts) { return new DialogTitleState(opts, DialogRootContext.get()); } opts; root; attachment; constructor(opts, root) { this.opts = opts; this.root = root; this.root.titleId = this.opts.id.current; this.attachment = attachRef(this.opts.ref); watch.pre(() => this.opts.id.current, (id) => { this.root.titleId = id; }); } props = $derived.by(() => ({ id: this.opts.id.current, role: "heading", "aria-level": this.opts.level.current, [this.root.getBitsAttr("title")]: "", ...this.root.sharedProps, ...this.attachment, })); } export class DialogDescriptionState { static create(opts) { return new DialogDescriptionState(opts, DialogRootContext.get()); } opts; root; attachment; constructor(opts, root) { this.opts = opts; this.root = root; this.root.descriptionId = this.opts.id.current; this.attachment = attachRef(this.opts.ref, (v) => { this.root.descriptionNode = v; }); watch.pre(() => this.opts.id.current, (id) => { this.root.descriptionId = id; }); } props = $derived.by(() => ({ id: this.opts.id.current, [this.root.getBitsAttr("description")]: "", ...this.root.sharedProps, ...this.attachment, })); } export class DialogContentState { static create(opts) { return new DialogContentState(opts, DialogRootContext.get()); } opts; root; attachment; constructor(opts, root) { this.opts = opts; this.root = root; this.attachment = attachRef(this.opts.ref, (v) => { this.root.contentNode = v; this.root.contentId = v?.id; }); } snippetProps = $derived.by(() => ({ open: this.root.opts.open.current })); props = $derived.by(() => ({ id: this.opts.id.current, role: this.root.opts.variant.current === "alert-dialog" ? "alertdialog" : "dialog", "aria-modal": "true", "aria-describedby": this.root.descriptionId, "aria-labelledby": this.root.titleId, [this.root.getBitsAttr("content")]: "", style: { pointerEvents: "auto", outline: this.root.opts.variant.current === "alert-dialog" ? "none" : undefined, }, tabindex: this.root.opts.variant.current === "alert-dialog" ? -1 : undefined, ...this.root.sharedProps, ...this.attachment, })); } export class DialogOverlayState { static create(opts) { return new DialogOverlayState(opts, DialogRootContext.get()); } opts; root; attachment; constructor(opts, root) { this.opts = opts; this.root = root; this.attachment = attachRef(this.opts.ref); } snippetProps = $derived.by(() => ({ open: this.root.opts.open.current })); props = $derived.by(() => ({ id: this.opts.id.current, [this.root.getBitsAttr("overlay")]: "", style: { pointerEvents: "auto", }, ...this.root.sharedProps, ...this.attachment, })); } export class AlertDialogCancelState { static create(opts) { return new AlertDialogCancelState(opts, DialogRootContext.get()); } opts; root; attachment; constructor(opts, root) { this.opts = opts; this.root = root; this.attachment = attachRef(this.opts.ref, (v) => (this.root.cancelNode = v)); this.onclick = this.onclick.bind(this); this.onkeydown = this.onkeydown.bind(this); } onclick(e) { if (this.opts.disabled.current) return; if (e.button > 0) return; this.root.handleClose(); } onkeydown(e) { if (this.opts.disabled.current) return; if (e.key === kbd.SPACE || e.key === kbd.ENTER) { e.preventDefault(); this.root.handleClose(); } } props = $derived.by(() => ({ id: this.opts.id.current, [this.root.getBitsAttr("cancel")]: "", onclick: this.onclick, onkeydown: this.onkeydown, tabindex: 0, ...this.root.sharedProps, ...this.attachment, })); }