bits-ui
Version:
The headless components for Svelte.
302 lines (301 loc) • 9.24 kB
JavaScript
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,
}));
}