INIT
This commit is contained in:
+358
@@ -0,0 +1,358 @@
|
||||
import { attachRef, boxWith, onDestroyEffect, } from "svelte-toolbelt";
|
||||
import { Context, watch } from "runed";
|
||||
import { createBitsAttrs, boolToStr, getDataOpenClosed, boolToEmptyStrOrUndef, } from "../../internal/attrs.js";
|
||||
import { kbd } from "../../internal/kbd.js";
|
||||
import { PresenceManager } from "../../internal/presence-manager.svelte.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) {
|
||||
const parent = DialogRootContext.getOr(null);
|
||||
return DialogRootContext.set(new DialogRootState(opts, parent));
|
||||
}
|
||||
opts;
|
||||
triggerNode = $state(null);
|
||||
contentNode = $state(null);
|
||||
overlayNode = $state(null);
|
||||
descriptionNode = $state(null);
|
||||
contentId = $state(undefined);
|
||||
titleId = $state(undefined);
|
||||
triggerId = $state(undefined);
|
||||
descriptionId = $state(undefined);
|
||||
cancelNode = $state(null);
|
||||
nestedOpenCount = $state(0);
|
||||
depth;
|
||||
parent;
|
||||
contentPresence;
|
||||
overlayPresence;
|
||||
constructor(opts, parent) {
|
||||
this.opts = opts;
|
||||
this.parent = parent;
|
||||
this.depth = parent ? parent.depth + 1 : 0;
|
||||
this.handleOpen = this.handleOpen.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
this.contentPresence = new PresenceManager({
|
||||
ref: boxWith(() => this.contentNode),
|
||||
open: this.opts.open,
|
||||
enabled: true,
|
||||
onComplete: () => {
|
||||
this.opts.onOpenChangeComplete.current(this.opts.open.current);
|
||||
},
|
||||
});
|
||||
this.overlayPresence = new PresenceManager({
|
||||
ref: boxWith(() => this.overlayNode),
|
||||
open: this.opts.open,
|
||||
enabled: true,
|
||||
});
|
||||
watch(() => this.opts.open.current, (isOpen) => {
|
||||
if (!this.parent)
|
||||
return;
|
||||
if (isOpen) {
|
||||
this.parent.incrementNested();
|
||||
}
|
||||
else {
|
||||
this.parent.decrementNested();
|
||||
}
|
||||
}, { lazy: true });
|
||||
onDestroyEffect(() => {
|
||||
if (this.opts.open.current) {
|
||||
this.parent?.decrementNested();
|
||||
}
|
||||
});
|
||||
}
|
||||
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);
|
||||
};
|
||||
incrementNested() {
|
||||
this.nestedOpenCount++;
|
||||
this.parent?.incrementNested();
|
||||
}
|
||||
decrementNested() {
|
||||
if (this.nestedOpenCount === 0)
|
||||
return;
|
||||
this.nestedOpenCount--;
|
||||
this.parent?.decrementNested();
|
||||
}
|
||||
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": boolToStr(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,
|
||||
"--bits-dialog-depth": this.root.depth,
|
||||
"--bits-dialog-nested-count": this.root.nestedOpenCount,
|
||||
// CSS containment isolates style/layout calculations from the rest of the page,
|
||||
// improving performance when there's a large DOM behind the dialog.
|
||||
// Paint is omitted so tooltips/selects can render outside dialog bounds.
|
||||
contain: "layout style",
|
||||
},
|
||||
tabindex: this.root.opts.variant.current === "alert-dialog" ? -1 : undefined,
|
||||
"data-nested-open": boolToEmptyStrOrUndef(this.root.nestedOpenCount > 0),
|
||||
"data-nested": boolToEmptyStrOrUndef(this.root.parent !== null),
|
||||
...this.root.sharedProps,
|
||||
...this.attachment,
|
||||
}));
|
||||
get shouldRender() {
|
||||
return this.root.contentPresence.shouldRender;
|
||||
}
|
||||
}
|
||||
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, (v) => (this.root.overlayNode = v));
|
||||
}
|
||||
snippetProps = $derived.by(() => ({ open: this.root.opts.open.current }));
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
[this.root.getBitsAttr("overlay")]: "",
|
||||
style: {
|
||||
pointerEvents: "auto",
|
||||
"--bits-dialog-depth": this.root.depth,
|
||||
"--bits-dialog-nested-count": this.root.nestedOpenCount,
|
||||
},
|
||||
"data-nested-open": boolToEmptyStrOrUndef(this.root.nestedOpenCount > 0),
|
||||
"data-nested": boolToEmptyStrOrUndef(this.root.parent !== null),
|
||||
...this.root.sharedProps,
|
||||
...this.attachment,
|
||||
}));
|
||||
get shouldRender() {
|
||||
return this.root.overlayPresence.shouldRender;
|
||||
}
|
||||
}
|
||||
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,
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user