@brytdesigns/web-component-dialog 0.0.0-stage → 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,60 @@
1
+ import { useDialog } from "../hooks/useDialog.js";
2
+ import "../hooks/index.js";
3
+ import { backdropKeyframes, hasKeyframes } from "../utils.js";
4
+ import { createGuardedEffect } from "@brytdesigns/web-component-core/utils";
5
+ import { getTransitionConfig } from "@brytdesigns/web-component-core/animation";
6
+ import { animate } from "motion";
7
+ import { onCleanup, onSettled } from "solid-js";
8
+ //#region src/components/dialog-backdrop.ts
9
+ var Name = "dialog-backdrop";
10
+ var Component = (_, { element }) => {
11
+ const [state, { updateAnimationQueue, close }] = useDialog(element);
12
+ const controller = new AbortController();
13
+ /**
14
+ * The starting state is applied, not animated — opening a page should not
15
+ * play a close animation at the user.
16
+ *
17
+ * Cleared in `onSettled` and nowhere else. `drawer-backdrop` also cleared it
18
+ * as the last statement of its component body, which runs before any apply
19
+ * phase, so the guard there never guarded.
20
+ */
21
+ let isFirstRender = true;
22
+ createGuardedEffect(() => state.isOpen, (isOpen) => {
23
+ if (!isOpen || isFirstRender) return;
24
+ const animation = animateBackdrop("enter");
25
+ if (!animation) return;
26
+ updateAnimationQueue(animation);
27
+ return () => {
28
+ if (animation.state !== "finished") animation.complete();
29
+ };
30
+ });
31
+ createGuardedEffect(() => state.isOpen, (isOpen) => {
32
+ if (isOpen || isFirstRender) return;
33
+ const animation = animateBackdrop("exit");
34
+ if (!animation) return;
35
+ updateAnimationQueue(animation);
36
+ return () => {
37
+ if (animation.state !== "finished") animation.complete();
38
+ };
39
+ });
40
+ onSettled(() => {
41
+ isFirstRender = false;
42
+ });
43
+ function animateBackdrop(direction) {
44
+ const style = window.getComputedStyle(element);
45
+ const keyframes = backdropKeyframes(style, direction);
46
+ if (!hasKeyframes(keyframes)) return void 0;
47
+ return animate(element, keyframes, getTransitionConfig(style));
48
+ }
49
+ element.addEventListener("click", () => {
50
+ if (state.isAnimating) return;
51
+ close();
52
+ }, { signal: controller.signal });
53
+ onCleanup(() => {
54
+ controller.abort();
55
+ });
56
+ };
57
+ //#endregion
58
+ export { Component, Name };
59
+
60
+ //# sourceMappingURL=dialog-backdrop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dialog-backdrop.js","names":[],"sources":["../../../src/components/dialog-backdrop.ts"],"sourcesContent":["import { getTransitionConfig } from \"@brytdesigns/web-component-core/animation\";\nimport type { CorrectComponentType } from \"@brytdesigns/web-component-core/utils\";\nimport { createGuardedEffect } from \"@brytdesigns/web-component-core/utils\";\nimport { animate } from \"motion\";\nimport { onCleanup, onSettled } from \"solid-js\";\n\nimport { useDialog } from \"../hooks/index.js\";\nimport { backdropKeyframes, hasKeyframes } from \"../utils.js\";\n\nexport type Props = Record<string, never>;\n\nexport const Name = \"dialog-backdrop\";\n\nexport const Component: CorrectComponentType<Props> = (_, { element }) => {\n\tconst [state, { updateAnimationQueue, close }] = useDialog(element);\n\tconst controller = new AbortController();\n\n\t/**\n\t * The starting state is applied, not animated — opening a page should not\n\t * play a close animation at the user.\n\t *\n\t * Cleared in `onSettled` and nowhere else. `drawer-backdrop` also cleared it\n\t * as the last statement of its component body, which runs before any apply\n\t * phase, so the guard there never guarded.\n\t */\n\tlet isFirstRender = true;\n\n\tcreateGuardedEffect(\n\t\t() => state.isOpen,\n\t\t(isOpen) => {\n\t\t\tif (!isOpen || isFirstRender) return;\n\t\t\tconst animation = animateBackdrop(\"enter\");\n\t\t\tif (!animation) return;\n\t\t\tupdateAnimationQueue(animation);\n\t\t\treturn () => {\n\t\t\t\tif (animation.state !== \"finished\") animation.complete();\n\t\t\t};\n\t\t},\n\t);\n\n\tcreateGuardedEffect(\n\t\t() => state.isOpen,\n\t\t(isOpen) => {\n\t\t\tif (isOpen || isFirstRender) return;\n\t\t\tconst animation = animateBackdrop(\"exit\");\n\t\t\tif (!animation) return;\n\t\t\tupdateAnimationQueue(animation);\n\t\t\treturn () => {\n\t\t\t\tif (animation.state !== \"finished\") animation.complete();\n\t\t\t};\n\t\t},\n\t);\n\n\tonSettled(() => {\n\t\tisFirstRender = false;\n\t});\n\n\tfunction animateBackdrop(direction: \"enter\" | \"exit\") {\n\t\tconst style = window.getComputedStyle(element);\n\t\tconst keyframes = backdropKeyframes(style, direction);\n\t\t// Nothing styled means nothing to animate. Handing Motion a pair of\n\t\t// empty strings produces an animation that changes nothing, which still\n\t\t// enters the queue and still flips `is-animating` for no reason.\n\t\tif (!hasKeyframes(keyframes)) return undefined;\n\t\treturn animate(element, keyframes, getTransitionConfig(style));\n\t}\n\n\telement.addEventListener(\n\t\t\"click\",\n\t\t() => {\n\t\t\tif (state.isAnimating) return;\n\t\t\tclose();\n\t\t},\n\t\t{ signal: controller.signal },\n\t);\n\n\tonCleanup(() => {\n\t\tcontroller.abort();\n\t});\n};\n"],"mappings":";;;;;;;;AAWA,IAAa,OAAO;AAEpB,IAAa,aAA0C,GAAG,EAAE,cAAc;CACzE,MAAM,CAAC,OAAO,EAAE,sBAAsB,WAAW,UAAU,OAAO;CAClE,MAAM,aAAa,IAAI,gBAAgB;;;;;;;;;CAUvC,IAAI,gBAAgB;CAEpB,0BACO,MAAM,SACX,WAAW;EACX,IAAI,CAAC,UAAU,eAAe;EAC9B,MAAM,YAAY,gBAAgB,OAAO;EACzC,IAAI,CAAC,WAAW;EAChB,qBAAqB,SAAS;EAC9B,aAAa;GACZ,IAAI,UAAU,UAAU,YAAY,UAAU,SAAS;EACxD;CACD,CACD;CAEA,0BACO,MAAM,SACX,WAAW;EACX,IAAI,UAAU,eAAe;EAC7B,MAAM,YAAY,gBAAgB,MAAM;EACxC,IAAI,CAAC,WAAW;EAChB,qBAAqB,SAAS;EAC9B,aAAa;GACZ,IAAI,UAAU,UAAU,YAAY,UAAU,SAAS;EACxD;CACD,CACD;CAEA,gBAAgB;EACf,gBAAgB;CACjB,CAAC;CAED,SAAS,gBAAgB,WAA6B;EACrD,MAAM,QAAQ,OAAO,iBAAiB,OAAO;EAC7C,MAAM,YAAY,kBAAkB,OAAO,SAAS;EAIpD,IAAI,CAAC,aAAa,SAAS,GAAG,OAAO,KAAA;EACrC,OAAO,QAAQ,SAAS,WAAW,oBAAoB,KAAK,CAAC;CAC9D;CAEA,QAAQ,iBACP,eACM;EACL,IAAI,MAAM,aAAa;EACvB,MAAM;CACP,GACA,EAAE,QAAQ,WAAW,OAAO,CAC7B;CAEA,gBAAgB;EACf,WAAW,MAAM;CAClB,CAAC;AACF"}
@@ -0,0 +1,66 @@
1
+ import { useDialog } from "../hooks/useDialog.js";
2
+ import "../hooks/index.js";
3
+ import { contentKeyframes, hasKeyframes } from "../utils.js";
4
+ import { Name as Name$1 } from "./dialog-description.js";
5
+ import { Name as Name$2 } from "./dialog-title.js";
6
+ import { createGuardedEffect, ensureId } from "@brytdesigns/web-component-core/utils";
7
+ import { getTransitionConfig } from "@brytdesigns/web-component-core/animation";
8
+ import { animate } from "motion";
9
+ import { onSettled } from "solid-js";
10
+ //#region src/components/dialog-content.ts
11
+ var Name = "dialog-content";
12
+ /**
13
+ * The element that is the dialog, as far as assistive tech is concerned.
14
+ *
15
+ * `drawer` and `modal` published no ARIA at all: no `role`, no `aria-modal`,
16
+ * no name. To a screen reader a drawer was an unlabelled `div` that happened
17
+ * to contain the focus. That is what this fixes.
18
+ */
19
+ var Component = (_, { element }) => {
20
+ const [state, { updateAnimationQueue }] = useDialog(element);
21
+ let isFirstRender = true;
22
+ element.setAttribute("role", "dialog");
23
+ if (!element.hasAttribute("tabindex")) element.setAttribute("tabindex", "-1");
24
+ onSettled(() => {
25
+ const title = element.querySelector(Name$2);
26
+ const description = element.querySelector(Name$1);
27
+ if (title && !element.hasAttribute("aria-labelledby")) element.setAttribute("aria-labelledby", ensureId(title, Name$2));
28
+ if (description && !element.hasAttribute("aria-describedby")) element.setAttribute("aria-describedby", ensureId(description, Name$1));
29
+ if (!(element.hasAttribute("aria-labelledby") || element.hasAttribute("aria-label"))) console.warn("dialog-content: no accessible name. Add a <dialog-title> inside it, or an aria-label attribute on it.", element);
30
+ });
31
+ createGuardedEffect(() => state.isOpen, (isOpen) => {
32
+ if (isOpen) element.setAttribute("aria-modal", "true");
33
+ else element.removeAttribute("aria-modal");
34
+ });
35
+ createGuardedEffect(() => state.isOpen, (isOpen) => {
36
+ if (!isOpen || isFirstRender) return;
37
+ const animation = animateContent("enter");
38
+ if (!animation) return;
39
+ updateAnimationQueue(animation);
40
+ return () => {
41
+ if (animation.state !== "finished") animation.complete();
42
+ };
43
+ });
44
+ createGuardedEffect(() => state.isOpen, (isOpen) => {
45
+ if (isOpen || isFirstRender) return;
46
+ const animation = animateContent("exit");
47
+ if (!animation) return;
48
+ updateAnimationQueue(animation);
49
+ return () => {
50
+ if (animation.state !== "finished") animation.complete();
51
+ };
52
+ });
53
+ onSettled(() => {
54
+ isFirstRender = false;
55
+ });
56
+ function animateContent(direction) {
57
+ const style = window.getComputedStyle(element);
58
+ const keyframes = contentKeyframes(style, direction);
59
+ if (!hasKeyframes(keyframes)) return void 0;
60
+ return animate(element, keyframes, getTransitionConfig(style));
61
+ }
62
+ };
63
+ //#endregion
64
+ export { Component, Name };
65
+
66
+ //# sourceMappingURL=dialog-content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dialog-content.js","names":[],"sources":["../../../src/components/dialog-content.ts"],"sourcesContent":["import { getTransitionConfig } from \"@brytdesigns/web-component-core/animation\";\nimport {\n\ttype CorrectComponentType,\n\tcreateGuardedEffect,\n\tensureId,\n} from \"@brytdesigns/web-component-core/utils\";\nimport { animate } from \"motion\";\nimport { onSettled } from \"solid-js\";\n\nimport { useDialog } from \"../hooks/index.js\";\nimport { contentKeyframes, hasKeyframes } from \"../utils.js\";\nimport { Name as DialogDescriptionName } from \"./dialog-description.js\";\nimport { Name as DialogTitleName } from \"./dialog-title.js\";\n\nexport type Props = Record<string, never>;\n\nexport const Name = \"dialog-content\";\n\n/**\n * The element that is the dialog, as far as assistive tech is concerned.\n *\n * `drawer` and `modal` published no ARIA at all: no `role`, no `aria-modal`,\n * no name. To a screen reader a drawer was an unlabelled `div` that happened\n * to contain the focus. That is what this fixes.\n */\nexport const Component: CorrectComponentType<Props> = (_, { element }) => {\n\tconst [state, { updateAnimationQueue }] = useDialog(element);\n\n\tlet isFirstRender = true;\n\n\telement.setAttribute(\"role\", \"dialog\");\n\t// Focusable so the trap has somewhere to land when the dialog holds no\n\t// focusable content of its own.\n\tif (!element.hasAttribute(\"tabindex\")) element.setAttribute(\"tabindex\", \"-1\");\n\n\t// Naming. A `<dialog-title>` inside is the usual answer; `aria-label` on\n\t// this element is the escape hatch for a dialog with no visible heading.\n\t// Authored `aria-labelledby` wins over both and is left alone.\n\tonSettled(() => {\n\t\tconst title = element.querySelector<HTMLElement>(DialogTitleName);\n\t\tconst description = element.querySelector<HTMLElement>(\n\t\t\tDialogDescriptionName,\n\t\t);\n\n\t\tif (title && !element.hasAttribute(\"aria-labelledby\")) {\n\t\t\telement.setAttribute(\"aria-labelledby\", ensureId(title, DialogTitleName));\n\t\t}\n\t\tif (description && !element.hasAttribute(\"aria-describedby\")) {\n\t\t\telement.setAttribute(\n\t\t\t\t\"aria-describedby\",\n\t\t\t\tensureId(description, DialogDescriptionName),\n\t\t\t);\n\t\t}\n\n\t\tconst named =\n\t\t\telement.hasAttribute(\"aria-labelledby\") ||\n\t\t\telement.hasAttribute(\"aria-label\");\n\t\tif (!named) {\n\t\t\tconsole.warn(\n\t\t\t\t\"dialog-content: no accessible name. Add a <dialog-title> inside it, or an aria-label attribute on it.\",\n\t\t\t\telement,\n\t\t\t);\n\t\t}\n\t});\n\n\tcreateGuardedEffect(\n\t\t() => state.isOpen,\n\t\t(isOpen) => {\n\t\t\t// `aria-modal` says focus is confined to this subtree, which is only\n\t\t\t// true while it is open — leaving it set on a closed dialog tells\n\t\t\t// assistive tech the rest of the page is unavailable when it is not.\n\t\t\tif (isOpen) element.setAttribute(\"aria-modal\", \"true\");\n\t\t\telse element.removeAttribute(\"aria-modal\");\n\t\t},\n\t);\n\n\tcreateGuardedEffect(\n\t\t() => state.isOpen,\n\t\t(isOpen) => {\n\t\t\tif (!isOpen || isFirstRender) return;\n\t\t\tconst animation = animateContent(\"enter\");\n\t\t\tif (!animation) return;\n\t\t\tupdateAnimationQueue(animation);\n\t\t\treturn () => {\n\t\t\t\tif (animation.state !== \"finished\") animation.complete();\n\t\t\t};\n\t\t},\n\t);\n\n\tcreateGuardedEffect(\n\t\t() => state.isOpen,\n\t\t(isOpen) => {\n\t\t\tif (isOpen || isFirstRender) return;\n\t\t\tconst animation = animateContent(\"exit\");\n\t\t\tif (!animation) return;\n\t\t\tupdateAnimationQueue(animation);\n\t\t\treturn () => {\n\t\t\t\tif (animation.state !== \"finished\") animation.complete();\n\t\t\t};\n\t\t},\n\t);\n\n\tonSettled(() => {\n\t\tisFirstRender = false;\n\t});\n\n\tfunction animateContent(direction: \"enter\" | \"exit\") {\n\t\tconst style = window.getComputedStyle(element);\n\t\tconst keyframes = contentKeyframes(style, direction);\n\t\tif (!hasKeyframes(keyframes)) return undefined;\n\t\treturn animate(element, keyframes, getTransitionConfig(style));\n\t}\n};\n"],"mappings":";;;;;;;;;;AAgBA,IAAa,OAAO;;;;;;;;AASpB,IAAa,aAA0C,GAAG,EAAE,cAAc;CACzE,MAAM,CAAC,OAAO,EAAE,0BAA0B,UAAU,OAAO;CAE3D,IAAI,gBAAgB;CAEpB,QAAQ,aAAa,QAAQ,QAAQ;CAGrC,IAAI,CAAC,QAAQ,aAAa,UAAU,GAAG,QAAQ,aAAa,YAAY,IAAI;CAK5E,gBAAgB;EACf,MAAM,QAAQ,QAAQ,cAA2B,MAAe;EAChE,MAAM,cAAc,QAAQ,cAC3B,MACD;EAEA,IAAI,SAAS,CAAC,QAAQ,aAAa,iBAAiB,GACnD,QAAQ,aAAa,mBAAmB,SAAS,OAAO,MAAe,CAAC;EAEzE,IAAI,eAAe,CAAC,QAAQ,aAAa,kBAAkB,GAC1D,QAAQ,aACP,oBACA,SAAS,aAAa,MAAqB,CAC5C;EAMD,IAAI,EAFH,QAAQ,aAAa,iBAAiB,KACtC,QAAQ,aAAa,YAAY,IAEjC,QAAQ,KACP,yGACA,OACD;CAEF,CAAC;CAED,0BACO,MAAM,SACX,WAAW;EAIX,IAAI,QAAQ,QAAQ,aAAa,cAAc,MAAM;OAChD,QAAQ,gBAAgB,YAAY;CAC1C,CACD;CAEA,0BACO,MAAM,SACX,WAAW;EACX,IAAI,CAAC,UAAU,eAAe;EAC9B,MAAM,YAAY,eAAe,OAAO;EACxC,IAAI,CAAC,WAAW;EAChB,qBAAqB,SAAS;EAC9B,aAAa;GACZ,IAAI,UAAU,UAAU,YAAY,UAAU,SAAS;EACxD;CACD,CACD;CAEA,0BACO,MAAM,SACX,WAAW;EACX,IAAI,UAAU,eAAe;EAC7B,MAAM,YAAY,eAAe,MAAM;EACvC,IAAI,CAAC,WAAW;EAChB,qBAAqB,SAAS;EAC9B,aAAa;GACZ,IAAI,UAAU,UAAU,YAAY,UAAU,SAAS;EACxD;CACD,CACD;CAEA,gBAAgB;EACf,gBAAgB;CACjB,CAAC;CAED,SAAS,eAAe,WAA6B;EACpD,MAAM,QAAQ,OAAO,iBAAiB,OAAO;EAC7C,MAAM,YAAY,iBAAiB,OAAO,SAAS;EACnD,IAAI,CAAC,aAAa,SAAS,GAAG,OAAO,KAAA;EACrC,OAAO,QAAQ,SAAS,WAAW,oBAAoB,KAAK,CAAC;CAC9D;AACD"}
@@ -0,0 +1,117 @@
1
+ import { provideDialogContext, useDialogContext } from "../hooks/useDialog.js";
2
+ import "../hooks/index.js";
3
+ import { hideElement, showElement } from "../utils.js";
4
+ import { closeNestedOverlays, createEscapeHandler, createFocusReturn, createGuardedEffect, disableBodyScroll, enableBodyScroll, findOverlayPanel } from "@brytdesigns/web-component-core/utils";
5
+ import { omit, onCleanup, untrack } from "solid-js";
6
+ import { createFocusTrap } from "@brytdesigns/web-component-core/focusTrap";
7
+ //#region src/components/dialog-context.ts
8
+ var Name = "dialog-context";
9
+ var Component = (props, { element }) => {
10
+ const restProps = omit(props, "isOpen", "isAnimating");
11
+ const context = provideDialogContext({
12
+ get isOpen() {
13
+ return props.isOpen;
14
+ },
15
+ get isAnimating() {
16
+ return props.isAnimating;
17
+ }
18
+ }, element);
19
+ const [state, { setElementState }] = context;
20
+ const [_, { updateAnimationQueue, ...events }] = useDialogContext(context);
21
+ element.actions = events;
22
+ const panel = () => findOverlayPanel(element, "dialog-content");
23
+ const focusReturn = createFocusReturn(element);
24
+ const focusTrap = createFocusTrap(element, () => ({
25
+ allowOutsideClick: true,
26
+ escapeDeactivates: false,
27
+ returnFocusOnDeactivate: false,
28
+ initialFocus: () => panel() ?? false,
29
+ fallbackFocus: () => panel() ?? element
30
+ }));
31
+ createGuardedEffect(() => ({
32
+ isOpen: state.isOpen,
33
+ closeOnEscape: restProps.closeOnEscape
34
+ }), ({ isOpen, closeOnEscape }) => {
35
+ if (!isOpen || !closeOnEscape) return;
36
+ return createEscapeHandler({
37
+ element,
38
+ onEscape: () => setElementState("isOpen", false)
39
+ });
40
+ });
41
+ createGuardedEffect(() => state.isOpen, (isOpen) => {
42
+ const eventName = isOpen ? "dialog:open" : "dialog:close";
43
+ element.dispatchEvent(new CustomEvent(eventName, {
44
+ bubbles: true,
45
+ detail: {
46
+ isOpen,
47
+ relatedTarget: element
48
+ }
49
+ }));
50
+ element.dispatchEvent(new CustomEvent("dialog:toggle", {
51
+ bubbles: true,
52
+ detail: {
53
+ isOpen,
54
+ relatedTarget: element
55
+ }
56
+ }));
57
+ });
58
+ createGuardedEffect(() => state.isOpen, (isOpen, previous) => {
59
+ if (previous === void 0 || isOpen) return;
60
+ closeNestedOverlays(element);
61
+ });
62
+ createGuardedEffect(() => ({
63
+ isOpen: state.isOpen,
64
+ isAnimating: state.isAnimating,
65
+ focusTrap: focusTrap(),
66
+ shouldTrapFocus: restProps.shouldTrapFocus
67
+ }), ({ isOpen, isAnimating, focusTrap, shouldTrapFocus }) => {
68
+ if (!isOpen) return;
69
+ showElement(element);
70
+ if (!shouldTrapFocus || isAnimating) return;
71
+ focusReturn.record();
72
+ focusTrap.activate();
73
+ return () => {
74
+ focusTrap.deactivate();
75
+ focusReturn.restore();
76
+ };
77
+ });
78
+ createGuardedEffect(() => ({
79
+ isOpen: state.isOpen,
80
+ isAnimating: state.isAnimating,
81
+ shouldTrapFocus: restProps.shouldTrapFocus
82
+ }), ({ isOpen, isAnimating, shouldTrapFocus }) => {
83
+ if (!isOpen || isAnimating || !shouldTrapFocus) return;
84
+ disableBodyScroll(element, { allowTouchMove: (el) => {
85
+ if (el instanceof HTMLElement) {
86
+ let node = el;
87
+ while (node && node !== document.body) {
88
+ if (node.tagName === "DIALOG-CONTENT") return true;
89
+ node = node.parentElement;
90
+ }
91
+ }
92
+ return false;
93
+ } });
94
+ return () => enableBodyScroll(element);
95
+ });
96
+ createGuardedEffect(() => state.isOpen, (isOpen) => {
97
+ if (isOpen) return;
98
+ let cancelled = false;
99
+ queueMicrotask(() => {
100
+ if (cancelled) return;
101
+ Promise.all(untrack(() => state.animationQueue)).then(() => {
102
+ if (cancelled) return;
103
+ hideElement(element);
104
+ });
105
+ });
106
+ return () => {
107
+ cancelled = true;
108
+ };
109
+ });
110
+ onCleanup(() => {
111
+ enableBodyScroll(element);
112
+ });
113
+ };
114
+ //#endregion
115
+ export { Component, Name };
116
+
117
+ //# sourceMappingURL=dialog-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dialog-context.js","names":[],"sources":["../../../src/components/dialog-context.ts"],"sourcesContent":["import { createFocusTrap } from \"@brytdesigns/web-component-core/focusTrap\";\nimport {\n\ttype CorrectComponentType,\n\tcloseNestedOverlays,\n\tcreateEscapeHandler,\n\tcreateFocusReturn,\n\tcreateGuardedEffect,\n\tdisableBodyScroll,\n\tenableBodyScroll,\n\tfindOverlayPanel,\n} from \"@brytdesigns/web-component-core/utils\";\nimport { omit, onCleanup, untrack } from \"solid-js\";\n\nimport { provideDialogContext, useDialogContext } from \"../hooks/index.js\";\nimport { hideElement, showElement } from \"../utils.js\";\n\nexport type Props = {\n\tisOpen: boolean;\n\tcloseOnEscape: boolean;\n\tshouldTrapFocus: boolean;\n\tisAnimating: boolean;\n};\n\nexport const Name = \"dialog-context\";\n\nexport const Component: CorrectComponentType<Props> = (props, { element }) => {\n\t// `splitProps` is gone. `omit` gives the rest; the picked half is a getter\n\t// object so it stays reactive, which is what splitProps returned.\n\tconst restProps = omit(props, \"isOpen\", \"isAnimating\");\n\tconst contextProps = {\n\t\tget isOpen() {\n\t\t\treturn props.isOpen;\n\t\t},\n\t\tget isAnimating() {\n\t\t\treturn props.isAnimating;\n\t\t},\n\t};\n\n\tconst context = provideDialogContext(contextProps, element);\n\n\tconst [state, { setElementState }] = context;\n\n\tconst [_, { updateAnimationQueue, ...events }] = useDialogContext(context);\n\n\telement.actions = events;\n\n\tconst panel = () => findOverlayPanel(element, \"dialog-content\");\n\tconst focusReturn = createFocusReturn(element);\n\n\tconst focusTrap = createFocusTrap(element, () => ({\n\t\tallowOutsideClick: true,\n\t\t// Escape and focus return are handled explicitly below rather than as\n\t\t// focus-trap options. As options they only applied while a trap was\n\t\t// active, so `close-on-escape=\"true\" should-trap-focus=\"false\"` silently\n\t\t// did nothing, and returning focus was a side effect of trapping it.\n\t\tescapeDeactivates: false,\n\t\treturnFocusOnDeactivate: false,\n\t\t// The panel, not its first button: a screen reader then announces the\n\t\t// dialog's name and role on open instead of \"Close\".\n\t\tinitialFocus: () => panel() ?? false,\n\t\t// Without this, an overlay holding nothing tabbable throws on activate.\n\t\tfallbackFocus: () => panel() ?? element,\n\t\t// No `onDeactivate`. It used to close the overlay, because deactivation\n\t\t// was how focus-trap's `escapeDeactivates` reported an Escape press —\n\t\t// and with Escape handled explicitly above, closing on *any* deactivation\n\t\t// is actively wrong: the trap is also deactivated whenever this effect\n\t\t// re-runs, so an overlay would close itself the moment it started\n\t\t// animating open.\n\t}));\n\n\tcreateGuardedEffect(\n\t\t() => ({\n\t\t\tisOpen: state.isOpen,\n\t\t\tcloseOnEscape: restProps.closeOnEscape,\n\t\t}),\n\t\t({ isOpen, closeOnEscape }) => {\n\t\t\tif (!isOpen || !closeOnEscape) return;\n\t\t\t// Only the innermost open overlay responds, so Escape inside a nested\n\t\t\t// overlay closes that one rather than collapsing the whole stack.\n\t\t\treturn createEscapeHandler({\n\t\t\t\telement,\n\t\t\t\tonEscape: () => setElementState(\"isOpen\", false),\n\t\t\t});\n\t\t},\n\t);\n\n\tcreateGuardedEffect(\n\t\t() => state.isOpen,\n\t\t(isOpen) => {\n\t\t\tconst eventName = isOpen ? \"dialog:open\" : \"dialog:close\";\n\t\t\telement.dispatchEvent(\n\t\t\t\tnew CustomEvent(eventName, {\n\t\t\t\t\tbubbles: true,\n\t\t\t\t\tdetail: { isOpen, relatedTarget: element },\n\t\t\t\t}),\n\t\t\t);\n\t\t\telement.dispatchEvent(\n\t\t\t\tnew CustomEvent(\"dialog:toggle\", {\n\t\t\t\t\tbubbles: true,\n\t\t\t\t\tdetail: { isOpen, relatedTarget: element },\n\t\t\t\t}),\n\t\t\t);\n\t\t},\n\t);\n\n\tcreateGuardedEffect(\n\t\t() => state.isOpen,\n\t\t(isOpen, previous) => {\n\t\t\t// Nothing to close on the mount run: a descendant that starts open was\n\t\t\t// authored that way, and closing it here would fight the markup.\n\t\t\tif (previous === undefined || isOpen) return;\n\t\t\t// Closing this one hides everything inside it, but a nested dialog\n\t\t\t// keeps its own `isOpen` until something changes it — so it would stay\n\t\t\t// \"open\" while invisible, never fire its close event, and reappear\n\t\t\t// still open when this one is opened again.\n\t\t\tcloseNestedOverlays(element);\n\t\t},\n\t);\n\n\tcreateGuardedEffect(\n\t\t() => ({\n\t\t\tisOpen: state.isOpen,\n\t\t\tisAnimating: state.isAnimating,\n\t\t\tfocusTrap: focusTrap(),\n\t\t\tshouldTrapFocus: restProps.shouldTrapFocus,\n\t\t}),\n\t\t({ isOpen, isAnimating, focusTrap, shouldTrapFocus }) => {\n\t\t\tif (!isOpen) return;\n\t\t\tshowElement(element);\n\t\t\t// Waiting out the enter animation keeps the trap off content that is\n\t\t\t// still transitioning, and means `record()` reads the trigger rather\n\t\t\t// than whatever the animation left focused.\n\t\t\tif (!shouldTrapFocus || isAnimating) return;\n\t\t\tfocusReturn.record();\n\t\t\tfocusTrap.activate();\n\t\t\t// The apply phase is not a computation, so `onCleanup` here would\n\t\t\t// never register. Its return value IS the cleanup.\n\t\t\treturn () => {\n\t\t\t\tfocusTrap.deactivate();\n\t\t\t\tfocusReturn.restore();\n\t\t\t};\n\t\t},\n\t);\n\n\tcreateGuardedEffect(\n\t\t() => ({\n\t\t\tisOpen: state.isOpen,\n\t\t\tisAnimating: state.isAnimating,\n\t\t\tshouldTrapFocus: restProps.shouldTrapFocus,\n\t\t}),\n\t\t({ isOpen, isAnimating, shouldTrapFocus }) => {\n\t\t\tif (!isOpen || isAnimating || !shouldTrapFocus) return;\n\n\t\t\tdisableBodyScroll(element, {\n\t\t\t\tallowTouchMove: (el: EventTarget) => {\n\t\t\t\t\tif (el instanceof HTMLElement) {\n\t\t\t\t\t\tlet node = el as (EventTarget & HTMLElement) | null;\n\t\t\t\t\t\twhile (node && node !== document.body) {\n\t\t\t\t\t\t\tif (node.tagName === \"DIALOG-CONTENT\") return true;\n\t\t\t\t\t\t\tnode = node.parentElement;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn false;\n\t\t\t\t},\n\t\t\t});\n\n\t\t\t// The apply phase's return value IS the cleanup, and\n\t\t\t// `disableBodyScroll` returns void — so returning its call would\n\t\t\t// register no cleanup at all, and the lock would outlive the close.\n\t\t\treturn () => enableBodyScroll(element);\n\t\t},\n\t);\n\n\tcreateGuardedEffect(\n\t\t() => state.isOpen,\n\t\t(isOpen) => {\n\t\t\tif (isOpen) return;\n\n\t\t\t// The content queues its exit animation during this same flush, and\n\t\t\t// apply phases run in creation order — this effect belongs to the\n\t\t\t// parent, so it runs first. Reading the queue here finds it empty and\n\t\t\t// hides the dialog before anything has animated, which is why closing\n\t\t\t// simply blinked out.\n\t\t\t//\n\t\t\t// A microtask lands after the rest of the flush, by which point the\n\t\t\t// exit animation is queued. Deferring is also why this cannot return\n\t\t\t// the promise chain as a cleanup: the cleanup is the flag.\n\t\t\tlet cancelled = false;\n\t\t\tqueueMicrotask(() => {\n\t\t\t\tif (cancelled) return;\n\t\t\t\tvoid Promise.all(untrack(() => state.animationQueue)).then(() => {\n\t\t\t\t\tif (cancelled) return;\n\t\t\t\t\thideElement(element);\n\t\t\t\t});\n\t\t\t});\n\n\t\t\treturn () => {\n\t\t\t\tcancelled = true;\n\t\t\t};\n\t\t},\n\t);\n\n\tonCleanup(() => {\n\t\tenableBodyScroll(element);\n\t});\n};\n"],"mappings":";;;;;;;AAuBA,IAAa,OAAO;AAEpB,IAAa,aAA0C,OAAO,EAAE,cAAc;CAG7E,MAAM,YAAY,KAAK,OAAO,UAAU,aAAa;CAUrD,MAAM,UAAU,qBAAqB;EARpC,IAAI,SAAS;GACZ,OAAO,MAAM;EACd;EACA,IAAI,cAAc;GACjB,OAAO,MAAM;EACd;CAGoC,GAAc,OAAO;CAE1D,MAAM,CAAC,OAAO,EAAE,qBAAqB;CAErC,MAAM,CAAC,GAAG,EAAE,sBAAsB,GAAG,YAAY,iBAAiB,OAAO;CAEzE,QAAQ,UAAU;CAElB,MAAM,cAAc,iBAAiB,SAAS,gBAAgB;CAC9D,MAAM,cAAc,kBAAkB,OAAO;CAE7C,MAAM,YAAY,gBAAgB,gBAAgB;EACjD,mBAAmB;EAKnB,mBAAmB;EACnB,yBAAyB;EAGzB,oBAAoB,MAAM,KAAK;EAE/B,qBAAqB,MAAM,KAAK;CAOjC,EAAE;CAEF,2BACQ;EACN,QAAQ,MAAM;EACd,eAAe,UAAU;CAC1B,KACC,EAAE,QAAQ,oBAAoB;EAC9B,IAAI,CAAC,UAAU,CAAC,eAAe;EAG/B,OAAO,oBAAoB;GAC1B;GACA,gBAAgB,gBAAgB,UAAU,KAAK;EAChD,CAAC;CACF,CACD;CAEA,0BACO,MAAM,SACX,WAAW;EACX,MAAM,YAAY,SAAS,gBAAgB;EAC3C,QAAQ,cACP,IAAI,YAAY,WAAW;GAC1B,SAAS;GACT,QAAQ;IAAE;IAAQ,eAAe;GAAQ;EAC1C,CAAC,CACF;EACA,QAAQ,cACP,IAAI,YAAY,iBAAiB;GAChC,SAAS;GACT,QAAQ;IAAE;IAAQ,eAAe;GAAQ;EAC1C,CAAC,CACF;CACD,CACD;CAEA,0BACO,MAAM,SACX,QAAQ,aAAa;EAGrB,IAAI,aAAa,KAAA,KAAa,QAAQ;EAKtC,oBAAoB,OAAO;CAC5B,CACD;CAEA,2BACQ;EACN,QAAQ,MAAM;EACd,aAAa,MAAM;EACnB,WAAW,UAAU;EACrB,iBAAiB,UAAU;CAC5B,KACC,EAAE,QAAQ,aAAa,WAAW,sBAAsB;EACxD,IAAI,CAAC,QAAQ;EACb,YAAY,OAAO;EAInB,IAAI,CAAC,mBAAmB,aAAa;EACrC,YAAY,OAAO;EACnB,UAAU,SAAS;EAGnB,aAAa;GACZ,UAAU,WAAW;GACrB,YAAY,QAAQ;EACrB;CACD,CACD;CAEA,2BACQ;EACN,QAAQ,MAAM;EACd,aAAa,MAAM;EACnB,iBAAiB,UAAU;CAC5B,KACC,EAAE,QAAQ,aAAa,sBAAsB;EAC7C,IAAI,CAAC,UAAU,eAAe,CAAC,iBAAiB;EAEhD,kBAAkB,SAAS,EAC1B,iBAAiB,OAAoB;GACpC,IAAI,cAAc,aAAa;IAC9B,IAAI,OAAO;IACX,OAAO,QAAQ,SAAS,SAAS,MAAM;KACtC,IAAI,KAAK,YAAY,kBAAkB,OAAO;KAC9C,OAAO,KAAK;IACb;GACD;GACA,OAAO;EACR,EACD,CAAC;EAKD,aAAa,iBAAiB,OAAO;CACtC,CACD;CAEA,0BACO,MAAM,SACX,WAAW;EACX,IAAI,QAAQ;EAWZ,IAAI,YAAY;EAChB,qBAAqB;GACpB,IAAI,WAAW;GACf,QAAa,IAAI,cAAc,MAAM,cAAc,CAAC,CAAC,CAAC,WAAW;IAChE,IAAI,WAAW;IACf,YAAY,OAAO;GACpB,CAAC;EACF,CAAC;EAED,aAAa;GACZ,YAAY;EACb;CACD,CACD;CAEA,gBAAgB;EACf,iBAAiB,OAAO;CACzB,CAAC;AACF"}
@@ -0,0 +1,15 @@
1
+ import { ensureId } from "@brytdesigns/web-component-core/utils";
2
+ //#region src/components/dialog-description.ts
3
+ var Name = "dialog-description";
4
+ /**
5
+ * Optional supporting text, wired to `aria-describedby` by `dialog-content`.
6
+ *
7
+ * Like `dialog-title`, this only needs an id.
8
+ */
9
+ var Component = (_, { element }) => {
10
+ ensureId(element, Name);
11
+ };
12
+ //#endregion
13
+ export { Component, Name };
14
+
15
+ //# sourceMappingURL=dialog-description.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dialog-description.js","names":[],"sources":["../../../src/components/dialog-description.ts"],"sourcesContent":["import {\n\ttype CorrectComponentType,\n\tensureId,\n} from \"@brytdesigns/web-component-core/utils\";\n\nexport type Props = Record<string, never>;\n\nexport const Name = \"dialog-description\";\n\n/**\n * Optional supporting text, wired to `aria-describedby` by `dialog-content`.\n *\n * Like `dialog-title`, this only needs an id.\n */\nexport const Component: CorrectComponentType<Props> = (_, { element }) => {\n\tensureId(element, Name);\n};\n"],"mappings":";;AAOA,IAAa,OAAO;;;;;;AAOpB,IAAa,aAA0C,GAAG,EAAE,cAAc;CACzE,SAAS,SAAS,IAAI;AACvB"}
@@ -0,0 +1,17 @@
1
+ import { ensureId } from "@brytdesigns/web-component-core/utils";
2
+ //#region src/components/dialog-title.ts
3
+ var Name = "dialog-title";
4
+ /**
5
+ * The dialog's accessible name.
6
+ *
7
+ * It only needs an id — `aria-labelledby` takes its text content from
8
+ * whatever it points at, so no role is added here. Put a real heading inside
9
+ * if the title should also appear in the document outline.
10
+ */
11
+ var Component = (_, { element }) => {
12
+ ensureId(element, Name);
13
+ };
14
+ //#endregion
15
+ export { Component, Name };
16
+
17
+ //# sourceMappingURL=dialog-title.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dialog-title.js","names":[],"sources":["../../../src/components/dialog-title.ts"],"sourcesContent":["import {\n\ttype CorrectComponentType,\n\tensureId,\n} from \"@brytdesigns/web-component-core/utils\";\n\nexport type Props = Record<string, never>;\n\nexport const Name = \"dialog-title\";\n\n/**\n * The dialog's accessible name.\n *\n * It only needs an id — `aria-labelledby` takes its text content from\n * whatever it points at, so no role is added here. Put a real heading inside\n * if the title should also appear in the document outline.\n */\nexport const Component: CorrectComponentType<Props> = (_, { element }) => {\n\tensureId(element, Name);\n};\n"],"mappings":";;AAOA,IAAa,OAAO;;;;;;;;AASpB,IAAa,aAA0C,GAAG,EAAE,cAAc;CACzE,SAAS,SAAS,IAAI;AACvB"}
@@ -0,0 +1,89 @@
1
+ import { useDialogAt, useDialogContext } from "../hooks/useDialog.js";
2
+ import "../hooks/index.js";
3
+ import { createGuardedEffect, ensureId, findDisclosureControl } from "@brytdesigns/web-component-core/utils";
4
+ import { observeElementInViewport } from "observe-element-in-viewport";
5
+ //#region src/components/dialog-trigger.ts
6
+ var Name = "dialog-trigger";
7
+ var Component = (props, { element }) => {
8
+ const dialog = useDialogAt(() => props.target, element);
9
+ createGuardedEffect(() => ({
10
+ context: dialog(),
11
+ action: props.action,
12
+ on: props.on,
13
+ preventDefault: props.preventDefault
14
+ }), ({ context, action, on, preventDefault }) => {
15
+ const controller = new AbortController();
16
+ const [state, { open, close, toggle }] = useDialogContext(context);
17
+ switch (on) {
18
+ case "enter":
19
+ if (action === "toggle") {
20
+ const unsubscribe = observeElementInViewport(element, open, close);
21
+ return () => {
22
+ unsubscribe();
23
+ controller.abort();
24
+ };
25
+ }
26
+ break;
27
+ case "exit":
28
+ if (action === "toggle") {
29
+ const unsubscribe = observeElementInViewport(element, close, open);
30
+ return () => {
31
+ unsubscribe();
32
+ controller.abort();
33
+ };
34
+ }
35
+ break;
36
+ default: element.addEventListener(on, (event) => {
37
+ if (preventDefault) event.preventDefault();
38
+ if (state.isAnimating) return;
39
+ switch (action) {
40
+ case "close":
41
+ close();
42
+ break;
43
+ case "open":
44
+ open();
45
+ break;
46
+ case "toggle": toggle();
47
+ }
48
+ }, { signal: controller.signal });
49
+ }
50
+ return () => {
51
+ controller.abort();
52
+ };
53
+ });
54
+ /**
55
+ * ARIA for the control that opens the dialog.
56
+ *
57
+ * `aria-haspopup="dialog"` rather than `aria-expanded`: this opens a dialog
58
+ * and moves focus into it, which is not the disclosure pattern
59
+ * `aria-expanded` describes. Neither `drawer-trigger` nor `modal-trigger`
60
+ * published any of this.
61
+ */
62
+ createGuardedEffect(() => ({
63
+ context: dialog(),
64
+ selector: props.target
65
+ }), ({ selector }) => {
66
+ const control = findDisclosureControl(element);
67
+ if (!control) {
68
+ console.warn("dialog-trigger: no <button> inside it, so no ARIA was applied. Wrap the trigger content in a <button type=\"button\">.", element);
69
+ return;
70
+ }
71
+ control.setAttribute("aria-haspopup", "dialog");
72
+ const root = selector ? document.querySelector(selector) : null;
73
+ if (root && !control.hasAttribute("aria-controls")) control.setAttribute("aria-controls", ensureId(root, "dialog"));
74
+ });
75
+ createGuardedEffect(() => {
76
+ const [state] = useDialogContext(dialog());
77
+ return {
78
+ isOpen: state.isOpen,
79
+ isAnimating: state.isAnimating
80
+ };
81
+ }, ({ isOpen, isAnimating }) => {
82
+ element.setAttribute("is-open", `${isOpen}`);
83
+ element.setAttribute("is-animating", `${isAnimating}`);
84
+ });
85
+ };
86
+ //#endregion
87
+ export { Component, Name };
88
+
89
+ //# sourceMappingURL=dialog-trigger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dialog-trigger.js","names":[],"sources":["../../../src/components/dialog-trigger.ts"],"sourcesContent":["import {\n\ttype CorrectComponentType,\n\tcreateGuardedEffect,\n\tensureId,\n\tfindDisclosureControl,\n} from \"@brytdesigns/web-component-core/utils\";\nimport { observeElementInViewport } from \"observe-element-in-viewport\";\n\nimport type { Action } from \"../consts.js\";\nimport { useDialogAt, useDialogContext } from \"../hooks/index.js\";\n\nexport type Props = {\n\ttarget?: string;\n\taction?: Action | \"\";\n\ton?: keyof HTMLElementEventMap | \"enter\" | \"exit\";\n\tpreventDefault: boolean;\n};\n\nexport const Name = \"dialog-trigger\";\n\nexport const Component: CorrectComponentType<Props> = (props, { element }) => {\n\t// The dialog is not an ancestor, so this is a lateral lookup. It resolves\n\t// asynchronously — waiting for a provider that may mount later, and\n\t// re-resolving if one replaces it — and reading it in an effect's compute\n\t// phase simply defers the apply until it is ready. No boundary needed.\n\tconst dialog = useDialogAt(() => props.target, element);\n\n\tcreateGuardedEffect(\n\t\t() => ({\n\t\t\tcontext: dialog(),\n\t\t\taction: props.action,\n\t\t\ton: props.on,\n\t\t\tpreventDefault: props.preventDefault,\n\t\t}),\n\t\t({ context, action, on, preventDefault }) => {\n\t\t\tconst controller = new AbortController();\n\t\t\tconst [state, { open, close, toggle }] = useDialogContext(context);\n\n\t\t\tswitch (on) {\n\t\t\t\tcase \"enter\": {\n\t\t\t\t\tif (action === \"toggle\") {\n\t\t\t\t\t\tconst unsubscribe = observeElementInViewport(element, open, close);\n\t\t\t\t\t\treturn () => {\n\t\t\t\t\t\t\tunsubscribe();\n\t\t\t\t\t\t\tcontroller.abort();\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase \"exit\": {\n\t\t\t\t\tif (action === \"toggle\") {\n\t\t\t\t\t\tconst unsubscribe = observeElementInViewport(element, close, open);\n\t\t\t\t\t\treturn () => {\n\t\t\t\t\t\t\tunsubscribe();\n\t\t\t\t\t\t\tcontroller.abort();\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tdefault: {\n\t\t\t\t\telement.addEventListener(\n\t\t\t\t\t\t//@ts-expect-error `on` is a free-form attribute\n\t\t\t\t\t\ton,\n\t\t\t\t\t\t(event: Event) => {\n\t\t\t\t\t\t\tif (preventDefault) event.preventDefault();\n\t\t\t\t\t\t\tif (state.isAnimating) return;\n\t\t\t\t\t\t\tswitch (action) {\n\t\t\t\t\t\t\t\tcase \"close\":\n\t\t\t\t\t\t\t\t\tclose();\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\tcase \"open\":\n\t\t\t\t\t\t\t\t\topen();\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\tcase \"toggle\":\n\t\t\t\t\t\t\t\t\ttoggle();\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{ signal: controller.signal },\n\t\t\t\t\t);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn () => {\n\t\t\t\tcontroller.abort();\n\t\t\t};\n\t\t},\n\t);\n\n\t/**\n\t * ARIA for the control that opens the dialog.\n\t *\n\t * `aria-haspopup=\"dialog\"` rather than `aria-expanded`: this opens a dialog\n\t * and moves focus into it, which is not the disclosure pattern\n\t * `aria-expanded` describes. Neither `drawer-trigger` nor `modal-trigger`\n\t * published any of this.\n\t */\n\tcreateGuardedEffect(\n\t\t// `target` belongs in the dependency list. It was read from the apply\n\t\t// phase, which does not track, so a trigger whose `target` changed kept\n\t\t// publishing `aria-controls` for the element it used to point at — the\n\t\t// same shape of defect as `product-variant-input` writing a stale\n\t\t// variant id, and just as quiet.\n\t\t() => ({ context: dialog(), selector: props.target }),\n\t\t({ selector }) => {\n\t\t\tconst control = findDisclosureControl(element);\n\t\t\tif (!control) {\n\t\t\t\tconsole.warn(\n\t\t\t\t\t'dialog-trigger: no <button> inside it, so no ARIA was applied. Wrap the trigger content in a <button type=\"button\">.',\n\t\t\t\t\telement,\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcontrol.setAttribute(\"aria-haspopup\", \"dialog\");\n\t\t\tconst root = selector\n\t\t\t\t? document.querySelector<HTMLElement>(selector)\n\t\t\t\t: null;\n\t\t\tif (root && !control.hasAttribute(\"aria-controls\")) {\n\t\t\t\tcontrol.setAttribute(\"aria-controls\", ensureId(root, \"dialog\"));\n\t\t\t}\n\t\t},\n\t);\n\n\t// Mirroring dialog state onto the trigger is independent of which listener\n\t// is wired, so it is its own effect rather than nested inside the one above.\n\tcreateGuardedEffect(\n\t\t() => {\n\t\t\tconst [state] = useDialogContext(dialog());\n\t\t\treturn { isOpen: state.isOpen, isAnimating: state.isAnimating };\n\t\t},\n\t\t({ isOpen, isAnimating }) => {\n\t\t\telement.setAttribute(\"is-open\", `${isOpen}`);\n\t\t\telement.setAttribute(\"is-animating\", `${isAnimating}`);\n\t\t},\n\t);\n};\n"],"mappings":";;;;;AAkBA,IAAa,OAAO;AAEpB,IAAa,aAA0C,OAAO,EAAE,cAAc;CAK7E,MAAM,SAAS,kBAAkB,MAAM,QAAQ,OAAO;CAEtD,2BACQ;EACN,SAAS,OAAO;EAChB,QAAQ,MAAM;EACd,IAAI,MAAM;EACV,gBAAgB,MAAM;CACvB,KACC,EAAE,SAAS,QAAQ,IAAI,qBAAqB;EAC5C,MAAM,aAAa,IAAI,gBAAgB;EACvC,MAAM,CAAC,OAAO,EAAE,MAAM,OAAO,YAAY,iBAAiB,OAAO;EAEjE,QAAQ,IAAR;GACC,KAAK;IACJ,IAAI,WAAW,UAAU;KACxB,MAAM,cAAc,yBAAyB,SAAS,MAAM,KAAK;KACjE,aAAa;MACZ,YAAY;MACZ,WAAW,MAAM;KAClB;IACD;IACA;GAED,KAAK;IACJ,IAAI,WAAW,UAAU;KACxB,MAAM,cAAc,yBAAyB,SAAS,OAAO,IAAI;KACjE,aAAa;MACZ,YAAY;MACZ,WAAW,MAAM;KAClB;IACD;IACA;GAGD,SACC,QAAQ,iBAEP,KACC,UAAiB;IACjB,IAAI,gBAAgB,MAAM,eAAe;IACzC,IAAI,MAAM,aAAa;IACvB,QAAQ,QAAR;KACC,KAAK;MACJ,MAAM;MACN;KACD,KAAK;MACJ,KAAK;MACL;KACD,KAAK,UACJ,OAAO;IAET;GACD,GACA,EAAE,QAAQ,WAAW,OAAO,CAC7B;EAGF;EAEA,aAAa;GACZ,WAAW,MAAM;EAClB;CACD,CACD;;;;;;;;;CAUA,2BAMQ;EAAE,SAAS,OAAO;EAAG,UAAU,MAAM;CAAO,KAClD,EAAE,eAAe;EACjB,MAAM,UAAU,sBAAsB,OAAO;EAC7C,IAAI,CAAC,SAAS;GACb,QAAQ,KACP,0HACA,OACD;GACA;EACD;EACA,QAAQ,aAAa,iBAAiB,QAAQ;EAC9C,MAAM,OAAO,WACV,SAAS,cAA2B,QAAQ,IAC5C;EACH,IAAI,QAAQ,CAAC,QAAQ,aAAa,eAAe,GAChD,QAAQ,aAAa,iBAAiB,SAAS,MAAM,QAAQ,CAAC;CAEhE,CACD;CAIA,0BACO;EACL,MAAM,CAAC,SAAS,iBAAiB,OAAO,CAAC;EACzC,OAAO;GAAE,QAAQ,MAAM;GAAQ,aAAa,MAAM;EAAY;CAC/D,IACC,EAAE,QAAQ,kBAAkB;EAC5B,QAAQ,aAAa,WAAW,GAAG,QAAQ;EAC3C,QAAQ,aAAa,gBAAgB,GAAG,aAAa;CACtD,CACD;AACD"}
@@ -0,0 +1,6 @@
1
+ import "./dialog-backdrop.js";
2
+ import "./dialog-description.js";
3
+ import "./dialog-title.js";
4
+ import "./dialog-content.js";
5
+ import "./dialog-context.js";
6
+ import "./dialog-trigger.js";
@@ -0,0 +1,20 @@
1
+ //#region src/consts.ts
2
+ /**
3
+ * Where the dialog is anchored, read from `--dialog-content--position`.
4
+ *
5
+ * Unset is the centred case — the shape `modal` had — and is deliberately not
6
+ * a member here: absence is what selects it, so there is nothing to name.
7
+ */
8
+ var POSITION = {
9
+ TOP: "top",
10
+ BOTTOM: "bottom",
11
+ LEFT: "left",
12
+ RIGHT: "right"
13
+ };
14
+ function isPosition(value) {
15
+ return Object.values(POSITION).includes(value);
16
+ }
17
+ //#endregion
18
+ export { POSITION, isPosition };
19
+
20
+ //# sourceMappingURL=consts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consts.js","names":[],"sources":["../../src/consts.ts"],"sourcesContent":["export type Position = (typeof POSITION)[keyof typeof POSITION];\nexport type Action = (typeof ACTION)[keyof typeof ACTION];\n\n/**\n * Where the dialog is anchored, read from `--dialog-content--position`.\n *\n * Unset is the centred case — the shape `modal` had — and is deliberately not\n * a member here: absence is what selects it, so there is nothing to name.\n */\nexport const POSITION = {\n\tTOP: \"top\",\n\tBOTTOM: \"bottom\",\n\tLEFT: \"left\",\n\tRIGHT: \"right\",\n} as const;\n\nexport const ACTION = {\n\tOPEN: \"open\",\n\tCLOSE: \"close\",\n\tTOGGLE: \"toggle\",\n} as const;\n\nexport function isPosition(value: string): value is Position {\n\treturn (Object.values(POSITION) as string[]).includes(value);\n}\n"],"mappings":";;;;;;;AASA,IAAa,WAAW;CACvB,KAAK;CACL,QAAQ;CACR,MAAM;CACN,OAAO;AACR;AAQA,SAAgB,WAAW,OAAkC;CAC5D,OAAQ,OAAO,OAAO,QAAQ,CAAC,CAAc,SAAS,KAAK;AAC5D"}
@@ -0,0 +1,2 @@
1
+ import { getDialogContext, provideDialogContext, useDialog, useDialogAt, useDialogContext } from "./useDialog.js";
2
+ export { getDialogContext, provideDialogContext, useDialog, useDialogAt, useDialogContext };
@@ -0,0 +1,93 @@
1
+ import { createGuardedEffect, createRemoteContext, getContextFromProvider, toHyphenated } from "@brytdesigns/web-component-core/utils";
2
+ import { awaitAllAnimations } from "@brytdesigns/web-component-core/animation";
3
+ import { createSignal, merge, omit, untrack } from "solid-js";
4
+ import { abortablePromise } from "@brytdesigns/web-component-core/promise";
5
+ import { consume, createContext, provide } from "component-register";
6
+ //#region src/hooks/useDialog.ts
7
+ function initializeDialogContext(props) {
8
+ const stateProps = omit(props, "root");
9
+ /**
10
+ * The animation queue is a SIGNAL, not a store.
11
+ *
12
+ * A store deep-proxies what you put in it, and what goes in here is a list of
13
+ * live Motion playback controls. Proxied, every `.finished`, `.state` and
14
+ * `.then` on them became a reactive read from a non-tracking scope — hundreds
15
+ * of STRICT_READ_UNTRACKED reports under any interaction, and a proxy hop on
16
+ * every property access of an object that is not state at all.
17
+ *
18
+ * Nothing here needs per-property reactivity: the queue is replaced wholesale
19
+ * and read wholesale. A signal is the right primitive, and it hands the
20
+ * controls back exactly as they went in.
21
+ */
22
+ const [animationQueue, setAnimationQueue] = createSignal([]);
23
+ createGuardedEffect(animationQueue, (queue) => {
24
+ if (!queue.length) return;
25
+ const controller = new AbortController();
26
+ const animations = awaitAllAnimations(queue);
27
+ const abortableAnimations = abortablePromise(animations, controller.signal);
28
+ setElementState("isAnimating", true);
29
+ abortableAnimations.finally(() => {
30
+ setElementState("isAnimating", false);
31
+ setAnimationQueue([]);
32
+ });
33
+ return () => {
34
+ controller.abort("Aborting dialog animations");
35
+ };
36
+ });
37
+ function setElementState(key, value) {
38
+ const currentValue = untrack(() => props[key]);
39
+ if (typeof value === "function") {
40
+ const result = value(currentValue || false);
41
+ if (`${currentValue}` === `${result}`) return;
42
+ props.root.setAttribute(toHyphenated(key), `${result}`);
43
+ return;
44
+ }
45
+ if (`${currentValue}` === `${value}`) return;
46
+ return props.root.setAttribute(toHyphenated(key), `${value}`);
47
+ }
48
+ return [merge(stateProps, { get animationQueue() {
49
+ return animationQueue();
50
+ } }), {
51
+ setElementState,
52
+ setAnimationQueue
53
+ }];
54
+ }
55
+ var DialogContextState = createContext(initializeDialogContext);
56
+ var provideDialogContext = (initialState, element) => {
57
+ const props = merge(initialState, { root: element });
58
+ return provide(DialogContextState, props, element);
59
+ };
60
+ var useDialogContext = (context) => {
61
+ const [state, { setElementState, setAnimationQueue }] = context;
62
+ function updateAnimationQueue(animation) {
63
+ setAnimationQueue((queue) => [...queue, animation]);
64
+ }
65
+ function close() {
66
+ setElementState("isOpen", false);
67
+ }
68
+ function open() {
69
+ setElementState("isOpen", true);
70
+ }
71
+ function toggle() {
72
+ setElementState("isOpen", (isOpen) => !isOpen);
73
+ }
74
+ return [state, {
75
+ updateAnimationQueue,
76
+ close,
77
+ open,
78
+ toggle
79
+ }];
80
+ };
81
+ var useDialog = (element) => {
82
+ const context = consume(DialogContextState, element);
83
+ if (!context) throw new Error("DialogContext not found! Please ensure to wrap your custom element with a dialog-context element.");
84
+ return useDialogContext(context);
85
+ };
86
+ var useDialogAt = createRemoteContext(DialogContextState);
87
+ var getDialogContext = (element) => {
88
+ return useDialogContext(getContextFromProvider(DialogContextState, element));
89
+ };
90
+ //#endregion
91
+ export { getDialogContext, provideDialogContext, useDialog, useDialogAt, useDialogContext };
92
+
93
+ //# sourceMappingURL=useDialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useDialog.js","names":[],"sources":["../../../src/hooks/useDialog.ts"],"sourcesContent":["import { awaitAllAnimations } from \"@brytdesigns/web-component-core/animation\";\nimport { abortablePromise } from \"@brytdesigns/web-component-core/promise\";\nimport {\n\tcreateGuardedEffect,\n\tcreateRemoteContext,\n\tgetContextFromProvider,\n\ttoHyphenated,\n} from \"@brytdesigns/web-component-core/utils\";\nimport {\n\tconsume,\n\tcreateContext,\n\ttype ICustomElement,\n\tprovide,\n} from \"component-register\";\nimport type { AnimationPlaybackControlsWithThen } from \"motion\";\nimport { createSignal, merge, omit, untrack } from \"solid-js\";\n\ntype StoreContext = {\n\tanimationQueue: AnimationPlaybackControlsWithThen[];\n};\n\ntype CreateContextOptions = {\n\troot: HTMLElement & ICustomElement;\n\tisOpen: boolean;\n\tisAnimating: boolean;\n};\n\ntype WalkableNode = Parameters<typeof provide>[2];\n\ntype DialogContext = ReturnType<typeof initializeDialogContext>;\n\nfunction initializeDialogContext(props: CreateContextOptions) {\n\t// `omit` returns only the rest; `root` is read straight off `props`.\n\tconst stateProps = omit(props, \"root\");\n\t/**\n\t * The animation queue is a SIGNAL, not a store.\n\t *\n\t * A store deep-proxies what you put in it, and what goes in here is a list of\n\t * live Motion playback controls. Proxied, every `.finished`, `.state` and\n\t * `.then` on them became a reactive read from a non-tracking scope — hundreds\n\t * of STRICT_READ_UNTRACKED reports under any interaction, and a proxy hop on\n\t * every property access of an object that is not state at all.\n\t *\n\t * Nothing here needs per-property reactivity: the queue is replaced wholesale\n\t * and read wholesale. A signal is the right primitive, and it hands the\n\t * controls back exactly as they went in.\n\t */\n\tconst [animationQueue, setAnimationQueue] = createSignal<\n\t\tAnimationPlaybackControlsWithThen[]\n\t>([]);\n\n\tcreateGuardedEffect(animationQueue, (queue) => {\n\t\tif (!queue.length) return;\n\t\tconst controller = new AbortController();\n\t\tconst animations = awaitAllAnimations(queue);\n\t\tconst abortableAnimations = abortablePromise(animations, controller.signal);\n\t\tsetElementState(\"isAnimating\", true);\n\t\tabortableAnimations.finally(() => {\n\t\t\t// No `batch` in v2 — writes stage and flush together by default.\n\t\t\tsetElementState(\"isAnimating\", false);\n\t\t\tsetAnimationQueue([]);\n\t\t});\n\n\t\t// v2 treats the apply phase's return value as the cleanup.\n\t\treturn () => {\n\t\t\tcontroller.abort(\"Aborting dialog animations\");\n\t\t};\n\t});\n\n\tfunction setElementState(\n\t\tkey: keyof Omit<CreateContextOptions, \"root\">,\n\t\tvalue: boolean | ((v: boolean) => boolean),\n\t) {\n\t\tconst currentValue = untrack(() => props[key]);\n\t\tif (typeof value === \"function\") {\n\t\t\tconst result = value(currentValue || false);\n\t\t\tif (`${currentValue}` === `${result}`) return;\n\t\t\tprops.root.setAttribute(toHyphenated(key), `${result}`);\n\t\t\treturn;\n\t\t}\n\t\tif (`${currentValue}` === `${value}`) return;\n\t\treturn props.root.setAttribute(toHyphenated(key), `${value}`);\n\t}\n\n\treturn [\n\t\tmerge(stateProps, {\n\t\t\tget animationQueue() {\n\t\t\t\treturn animationQueue();\n\t\t\t},\n\t\t}),\n\t\t{ setElementState, setAnimationQueue },\n\t] as const;\n}\n\nconst DialogContextState = createContext(initializeDialogContext);\n\nexport const provideDialogContext = (\n\tinitialState: Omit<CreateContextOptions, \"root\">,\n\telement: WalkableNode,\n): DialogContext => {\n\tconst props = merge(initialState, { root: element });\n\treturn provide(DialogContextState, props, element);\n};\n\nexport const useDialogContext = (context: DialogContext) => {\n\tconst [state, { setElementState, setAnimationQueue }] = context;\n\n\tfunction updateAnimationQueue(animation: AnimationPlaybackControlsWithThen) {\n\t\t// v2 store setters are draft-first, but this one REPLACES the array\n\t\t// rather than pushing into it. The effect above computes\n\t\t// `store.animationQueue` — the property, not its contents — so an\n\t\t// in-place `push` leaves that property pointing at the same array and\n\t\t// the effect never re-runs. Both `drawer` and `modal` shipped that way,\n\t\t// and never published `is-animating` at all as a result.\n\t\tsetAnimationQueue((queue) => [...queue, animation]);\n\t}\n\n\tfunction close() {\n\t\tsetElementState(\"isOpen\", false);\n\t}\n\n\tfunction open() {\n\t\tsetElementState(\"isOpen\", true);\n\t}\n\n\tfunction toggle() {\n\t\tsetElementState(\"isOpen\", (isOpen) => !isOpen);\n\t}\n\n\treturn [state, { updateAnimationQueue, close, open, toggle }] as const;\n};\n\nexport const useDialog = (element: HTMLElement & ICustomElement) => {\n\tconst context: DialogContext = consume(DialogContextState, element);\n\n\tif (!context) {\n\t\tthrow new Error(\n\t\t\t\"DialogContext not found! Please ensure to wrap your custom element with a dialog-context element.\",\n\t\t);\n\t}\n\n\treturn useDialogContext(context);\n};\n\nexport const useDialogAt =\n\tcreateRemoteContext<DialogContext>(DialogContextState);\n\nexport const getDialogContext = (element: Element) => {\n\tconst context = getContextFromProvider<DialogContext>(\n\t\tDialogContextState,\n\t\telement,\n\t);\n\treturn useDialogContext(context);\n};\n"],"mappings":";;;;;;AA+BA,SAAS,wBAAwB,OAA6B;CAE7D,MAAM,aAAa,KAAK,OAAO,MAAM;;;;;;;;;;;;;;CAcrC,MAAM,CAAC,gBAAgB,qBAAqB,aAE1C,CAAC,CAAC;CAEJ,oBAAoB,iBAAiB,UAAU;EAC9C,IAAI,CAAC,MAAM,QAAQ;EACnB,MAAM,aAAa,IAAI,gBAAgB;EACvC,MAAM,aAAa,mBAAmB,KAAK;EAC3C,MAAM,sBAAsB,iBAAiB,YAAY,WAAW,MAAM;EAC1E,gBAAgB,eAAe,IAAI;EACnC,oBAAoB,cAAc;GAEjC,gBAAgB,eAAe,KAAK;GACpC,kBAAkB,CAAC,CAAC;EACrB,CAAC;EAGD,aAAa;GACZ,WAAW,MAAM,4BAA4B;EAC9C;CACD,CAAC;CAED,SAAS,gBACR,KACA,OACC;EACD,MAAM,eAAe,cAAc,MAAM,IAAI;EAC7C,IAAI,OAAO,UAAU,YAAY;GAChC,MAAM,SAAS,MAAM,gBAAgB,KAAK;GAC1C,IAAI,GAAG,mBAAmB,GAAG,UAAU;GACvC,MAAM,KAAK,aAAa,aAAa,GAAG,GAAG,GAAG,QAAQ;GACtD;EACD;EACA,IAAI,GAAG,mBAAmB,GAAG,SAAS;EACtC,OAAO,MAAM,KAAK,aAAa,aAAa,GAAG,GAAG,GAAG,OAAO;CAC7D;CAEA,OAAO,CACN,MAAM,YAAY,EACjB,IAAI,iBAAiB;EACpB,OAAO,eAAe;CACvB,EACD,CAAC,GACD;EAAE;EAAiB;CAAkB,CACtC;AACD;AAEA,IAAM,qBAAqB,cAAc,uBAAuB;AAEhE,IAAa,wBACZ,cACA,YACmB;CACnB,MAAM,QAAQ,MAAM,cAAc,EAAE,MAAM,QAAQ,CAAC;CACnD,OAAO,QAAQ,oBAAoB,OAAO,OAAO;AAClD;AAEA,IAAa,oBAAoB,YAA2B;CAC3D,MAAM,CAAC,OAAO,EAAE,iBAAiB,uBAAuB;CAExD,SAAS,qBAAqB,WAA8C;EAO3E,mBAAmB,UAAU,CAAC,GAAG,OAAO,SAAS,CAAC;CACnD;CAEA,SAAS,QAAQ;EAChB,gBAAgB,UAAU,KAAK;CAChC;CAEA,SAAS,OAAO;EACf,gBAAgB,UAAU,IAAI;CAC/B;CAEA,SAAS,SAAS;EACjB,gBAAgB,WAAW,WAAW,CAAC,MAAM;CAC9C;CAEA,OAAO,CAAC,OAAO;EAAE;EAAsB;EAAO;EAAM;CAAO,CAAC;AAC7D;AAEA,IAAa,aAAa,YAA0C;CACnE,MAAM,UAAyB,QAAQ,oBAAoB,OAAO;CAElE,IAAI,CAAC,SACJ,MAAM,IAAI,MACT,mGACD;CAGD,OAAO,iBAAiB,OAAO;AAChC;AAEA,IAAa,cACZ,oBAAmC,kBAAkB;AAEtD,IAAa,oBAAoB,YAAqB;CAKrD,OAAO,iBAJS,uBACf,oBACA,OAEuB,CAAO;AAChC"}
@@ -0,0 +1,102 @@
1
+ // Generated by dts-bundle-generator v9.5.1
2
+
3
+ import { ICustomElement } from 'component-register';
4
+ import { AnimationPlaybackControlsWithThen } from 'motion';
5
+
6
+ /**
7
+ * `component-register` types `options.element` as a bare `ICustomElement`, but
8
+ * at runtime it is always also an `HTMLElement`. Every component in this repo
9
+ * needs the DOM half.
10
+ */
11
+ export type CorrectComponentOptions = {
12
+ element: HTMLElement & ICustomElement;
13
+ };
14
+ export type CorrectComponentType<T> = (props: T, options: CorrectComponentOptions) => unknown;
15
+ export type Props = Record<string, never>;
16
+ declare const Name = "dialog-backdrop";
17
+ declare const Component: CorrectComponentType<Props>;
18
+ type Props$1 = Record<string, never>;
19
+ declare const Name$1 = "dialog-content";
20
+ declare const Component$1: CorrectComponentType<Props$1>;
21
+ type Props$2 = {
22
+ isOpen: boolean;
23
+ closeOnEscape: boolean;
24
+ shouldTrapFocus: boolean;
25
+ isAnimating: boolean;
26
+ };
27
+ declare const Name$2 = "dialog-context";
28
+ declare const Component$2: CorrectComponentType<Props$2>;
29
+ type Props$3 = Record<string, never>;
30
+ declare const Name$3 = "dialog-description";
31
+ declare const Component$3: CorrectComponentType<Props$3>;
32
+ type Props$4 = Record<string, never>;
33
+ declare const Name$4 = "dialog-title";
34
+ declare const Component$4: CorrectComponentType<Props$4>;
35
+ export type Action = (typeof ACTION)[keyof typeof ACTION];
36
+ declare const ACTION: {
37
+ readonly OPEN: "open";
38
+ readonly CLOSE: "close";
39
+ readonly TOGGLE: "toggle";
40
+ };
41
+ type Props$5 = {
42
+ target?: string;
43
+ action?: Action | "";
44
+ on?: keyof HTMLElementEventMap | "enter" | "exit";
45
+ preventDefault: boolean;
46
+ };
47
+ declare const Name$5 = "dialog-trigger";
48
+ declare const Component$5: CorrectComponentType<Props$5>;
49
+ export type CreateContextOptions = {
50
+ root: HTMLElement & ICustomElement;
51
+ isOpen: boolean;
52
+ isAnimating: boolean;
53
+ };
54
+ export declare const useDialog: (element: HTMLElement & ICustomElement) => readonly [
55
+ {
56
+ isOpen: boolean;
57
+ isAnimating: boolean;
58
+ readonly animationQueue: AnimationPlaybackControlsWithThen[];
59
+ },
60
+ {
61
+ readonly updateAnimationQueue: (animation: AnimationPlaybackControlsWithThen) => void;
62
+ readonly close: () => void;
63
+ readonly open: () => void;
64
+ readonly toggle: () => void;
65
+ }
66
+ ];
67
+ export declare const useDialogAt: (target: () => string | undefined, host: HTMLElement) => import("solid-js").SourceAccessor<readonly [
68
+ {
69
+ isOpen: boolean;
70
+ isAnimating: boolean;
71
+ readonly animationQueue: AnimationPlaybackControlsWithThen[];
72
+ },
73
+ {
74
+ readonly setElementState: (key: keyof Omit<CreateContextOptions, "root">, value: boolean | ((v: boolean) => boolean)) => void;
75
+ readonly setAnimationQueue: import("solid-js").Setter<AnimationPlaybackControlsWithThen[]>;
76
+ }
77
+ ]>;
78
+ export declare const getDialogContext: (element: Element) => readonly [
79
+ {
80
+ isOpen: boolean;
81
+ isAnimating: boolean;
82
+ readonly animationQueue: AnimationPlaybackControlsWithThen[];
83
+ },
84
+ {
85
+ readonly updateAnimationQueue: (animation: AnimationPlaybackControlsWithThen) => void;
86
+ readonly close: () => void;
87
+ readonly open: () => void;
88
+ readonly toggle: () => void;
89
+ }
90
+ ];
91
+ export type DialogBackdropProps = Props;
92
+ export type DialogContentProps = Props$1;
93
+ export type DialogContextProps = Props$2;
94
+ export type DialogDescriptionProps = Props$3;
95
+ export type DialogTitleProps = Props$4;
96
+ export type DialogTriggerProps = DialogTrigger.Props;
97
+
98
+ declare namespace DialogTrigger {
99
+ export { Component$5 as Component, Name$5 as Name, Props$5 as Props };
100
+ }
101
+
102
+ export {};
@@ -0,0 +1,33 @@
1
+ import { getDialogContext, useDialog, useDialogAt } from "./hooks/useDialog.js";
2
+ import "./hooks/index.js";
3
+ import { Component, Name } from "./components/dialog-backdrop.js";
4
+ import { Component as Component$1, Name as Name$1 } from "./components/dialog-description.js";
5
+ import { Component as Component$2, Name as Name$2 } from "./components/dialog-title.js";
6
+ import { Component as Component$3, Name as Name$3 } from "./components/dialog-content.js";
7
+ import { Component as Component$4, Name as Name$4 } from "./components/dialog-context.js";
8
+ import { Component as Component$5, Name as Name$5 } from "./components/dialog-trigger.js";
9
+ import "./components/index.js";
10
+ import { registerDialogProperties } from "./register-properties.js";
11
+ import { customShadowlessElement } from "@brytdesigns/web-component-core/utils";
12
+ //#region src/index.ts
13
+ registerDialogProperties();
14
+ customShadowlessElement(Name$4, {
15
+ isOpen: false,
16
+ closeOnEscape: true,
17
+ shouldTrapFocus: true,
18
+ isAnimating: false
19
+ }, Component$4);
20
+ customShadowlessElement(Name$5, {
21
+ target: "",
22
+ action: "",
23
+ on: "click",
24
+ preventDefault: true
25
+ }, Component$5);
26
+ customShadowlessElement(Name, {}, Component);
27
+ customShadowlessElement(Name$3, {}, Component$3);
28
+ customShadowlessElement(Name$2, {}, Component$2);
29
+ customShadowlessElement(Name$1, {}, Component$1);
30
+ //#endregion
31
+ export { getDialogContext, useDialog, useDialogAt };
32
+
33
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/index.ts"],"sourcesContent":["import { customShadowlessElement } from \"@brytdesigns/web-component-core/utils\";\n\nimport {\n\tDialogBackdrop,\n\tDialogContent,\n\tDialogContext,\n\tDialogDescription,\n\tDialogTitle,\n\tDialogTrigger,\n} from \"./components/index.js\";\nimport { registerDialogProperties } from \"./register-properties.js\";\n\nexport type DialogBackdropProps = DialogBackdrop.Props;\nexport type DialogContentProps = DialogContent.Props;\nexport type DialogContextProps = DialogContext.Props;\nexport type DialogDescriptionProps = DialogDescription.Props;\nexport type DialogTitleProps = DialogTitle.Props;\nexport type DialogTriggerProps = DialogTrigger.Props;\n\nregisterDialogProperties();\n\ncustomShadowlessElement(\n\tDialogContext.Name,\n\t{\n\t\tisOpen: false,\n\t\t// On by default: an overlay that traps nothing and closes on no key is\n\t\t// not a dialog. Set close-on-escape=\"false\" / should-trap-focus=\"false\"\n\t\t// in markup to opt out.\n\t\tcloseOnEscape: true,\n\t\tshouldTrapFocus: true,\n\t\tisAnimating: false,\n\t},\n\tDialogContext.Component,\n);\n\ncustomShadowlessElement(\n\tDialogTrigger.Name,\n\t{ target: \"\", action: \"\", on: \"click\", preventDefault: true },\n\tDialogTrigger.Component,\n);\n\ncustomShadowlessElement(DialogBackdrop.Name, {}, DialogBackdrop.Component);\ncustomShadowlessElement(DialogContent.Name, {}, DialogContent.Component);\ncustomShadowlessElement(DialogTitle.Name, {}, DialogTitle.Component);\ncustomShadowlessElement(\n\tDialogDescription.Name,\n\t{},\n\tDialogDescription.Component,\n);\n\nexport {\n\tgetDialogContext,\n\tuseDialog,\n\tuseDialogAt,\n} from \"./hooks/index.js\";\n"],"mappings":";;;;;;;;;;;;AAmBA,yBAAyB;AAEzB,wBACC,QACA;CACC,QAAQ;CAIR,eAAe;CACf,iBAAiB;CACjB,aAAa;AACd,GACA,WACD;AAEA,wBACC,QACA;CAAE,QAAQ;CAAI,QAAQ;CAAI,IAAI;CAAS,gBAAgB;AAAK,GAC5D,WACD;AAEA,wBAAwB,MAAqB,CAAC,GAAG,SAAwB;AACzE,wBAAwB,QAAoB,CAAC,GAAG,WAAuB;AACvE,wBAAwB,QAAkB,CAAC,GAAG,WAAqB;AACnE,wBACC,QACA,CAAC,GACD,WACD"}
@@ -0,0 +1,50 @@
1
+ //#region src/register-properties.ts
2
+ /**
3
+ * Declare the dialog's animation custom properties as **non-inheriting**.
4
+ *
5
+ * Custom properties inherit by default, so a dialog nested inside another
6
+ * picks up its ancestor's settings. A centred dialog inside an edge-anchored
7
+ * one inherited `--dialog-content--position: right` and the outer's slide
8
+ * distances, took the single-axis path, and slid in from the right while its
9
+ * own scale and opacity were ignored entirely.
10
+ *
11
+ * These properties describe how *this* element animates. Inheriting them into
12
+ * a nested dialog is never what anyone means, which is exactly what
13
+ * `inherits: false` is for.
14
+ *
15
+ * The consequence worth knowing: a theme must set them on a selector that
16
+ * matches the element itself — `dialog-content`, or `#cart > dialog-content` —
17
+ * rather than on an ancestor and letting them cascade down.
18
+ */
19
+ var ANIMATION_PROPERTIES = [
20
+ "--dialog-content--position",
21
+ "--dialog-content--slide-x-from",
22
+ "--dialog-content--slide-x-to",
23
+ "--dialog-content--slide-y-from",
24
+ "--dialog-content--slide-y-to",
25
+ "--dialog-content--scale-x-from",
26
+ "--dialog-content--scale-x-to",
27
+ "--dialog-content--scale-y-from",
28
+ "--dialog-content--scale-y-to",
29
+ "--dialog-content--opacity-from",
30
+ "--dialog-content--opacity-to",
31
+ "--dialog-backdrop--opacity-from",
32
+ "--dialog-backdrop--opacity-to"
33
+ ];
34
+ var registered = false;
35
+ function registerDialogProperties() {
36
+ if (registered) return;
37
+ registered = true;
38
+ if (typeof CSS === "undefined" || typeof CSS.registerProperty !== "function") return;
39
+ for (const name of ANIMATION_PROPERTIES) try {
40
+ CSS.registerProperty({
41
+ name,
42
+ syntax: "*",
43
+ inherits: false
44
+ });
45
+ } catch {}
46
+ }
47
+ //#endregion
48
+ export { registerDialogProperties };
49
+
50
+ //# sourceMappingURL=register-properties.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register-properties.js","names":[],"sources":["../../src/register-properties.ts"],"sourcesContent":["/**\n * Declare the dialog's animation custom properties as **non-inheriting**.\n *\n * Custom properties inherit by default, so a dialog nested inside another\n * picks up its ancestor's settings. A centred dialog inside an edge-anchored\n * one inherited `--dialog-content--position: right` and the outer's slide\n * distances, took the single-axis path, and slid in from the right while its\n * own scale and opacity were ignored entirely.\n *\n * These properties describe how *this* element animates. Inheriting them into\n * a nested dialog is never what anyone means, which is exactly what\n * `inherits: false` is for.\n *\n * The consequence worth knowing: a theme must set them on a selector that\n * matches the element itself — `dialog-content`, or `#cart > dialog-content` —\n * rather than on an ancestor and letting them cascade down.\n */\nconst ANIMATION_PROPERTIES = [\n\t\"--dialog-content--position\",\n\t\"--dialog-content--slide-x-from\",\n\t\"--dialog-content--slide-x-to\",\n\t\"--dialog-content--slide-y-from\",\n\t\"--dialog-content--slide-y-to\",\n\t\"--dialog-content--scale-x-from\",\n\t\"--dialog-content--scale-x-to\",\n\t\"--dialog-content--scale-y-from\",\n\t\"--dialog-content--scale-y-to\",\n\t\"--dialog-content--opacity-from\",\n\t\"--dialog-content--opacity-to\",\n\t\"--dialog-backdrop--opacity-from\",\n\t\"--dialog-backdrop--opacity-to\",\n] as const;\n\nlet registered = false;\n\nexport function registerDialogProperties() {\n\tif (registered) return;\n\tregistered = true;\n\n\t// Older browsers, and any non-DOM environment, simply go without. The\n\t// components read these with `getPropertyValue` either way; registration\n\t// only changes whether a value reaches an element it was not set on.\n\tif (typeof CSS === \"undefined\" || typeof CSS.registerProperty !== \"function\")\n\t\treturn;\n\n\tfor (const name of ANIMATION_PROPERTIES) {\n\t\ttry {\n\t\t\tCSS.registerProperty({ name, syntax: \"*\", inherits: false });\n\t\t} catch {\n\t\t\t// Already registered — by a second copy of this package, or by a\n\t\t\t// theme's own `@property` rule. Either way the declaration stands and\n\t\t\t// re-registering would throw.\n\t\t}\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAiBA,IAAM,uBAAuB;CAC5B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACD;AAEA,IAAI,aAAa;AAEjB,SAAgB,2BAA2B;CAC1C,IAAI,YAAY;CAChB,aAAa;CAKb,IAAI,OAAO,QAAQ,eAAe,OAAO,IAAI,qBAAqB,YACjE;CAED,KAAK,MAAM,QAAQ,sBAClB,IAAI;EACH,IAAI,iBAAiB;GAAE;GAAM,QAAQ;GAAK,UAAU;EAAM,CAAC;CAC5D,QAAQ,CAIR;AAEF"}
@@ -0,0 +1,71 @@
1
+ import { POSITION, isPosition } from "./consts.js";
2
+ //#region src/utils.ts
3
+ function hideElement(element) {
4
+ element.style.display = "none";
5
+ }
6
+ function showElement(element) {
7
+ element.style.display = "block";
8
+ }
9
+ /**
10
+ * Read a `from`/`to` pair of custom properties, or nothing.
11
+ *
12
+ * Motion is handed `var(--x)` strings, and an *unset* custom property resolves
13
+ * to the empty string rather than failing — so a property nobody styled
14
+ * produced an animation between two empty values, which silently does nothing.
15
+ * That is why `drawer-backdrop` could queue a mount animation and still show
16
+ * no `is-animating` anywhere: the animation existed and animated nothing.
17
+ *
18
+ * Returning undefined for an unstyled pair keeps it out of the keyframes
19
+ * entirely, so a theme that styles only the slide gets a slide, and one that
20
+ * styles nothing gets no animation rather than an invisible one.
21
+ */
22
+ function pair(style, property) {
23
+ const from = style.getPropertyValue(`--${property}-from`).trim();
24
+ const to = style.getPropertyValue(`--${property}-to`).trim();
25
+ if (!from || !to) return void 0;
26
+ return [`var(--${property}-from)`, `var(--${property}-to)`];
27
+ }
28
+ function readPosition(style) {
29
+ const value = style.getPropertyValue("--dialog-content--position").trim();
30
+ return isPosition(value) ? value : void 0;
31
+ }
32
+ /**
33
+ * The keyframes for opening the content.
34
+ *
35
+ * An edge position slides on one axis — the drawer shape. No position is the
36
+ * centred shape, which scales and fades as well as sliding. Either way, only
37
+ * the properties a theme actually styled are included.
38
+ */
39
+ function contentKeyframes(style, direction) {
40
+ const position = readPosition(style);
41
+ const keyframes = {};
42
+ if (position === POSITION.LEFT || position === POSITION.RIGHT) keyframes.x = pair(style, "dialog-content--slide-x");
43
+ else if (position === POSITION.TOP || position === POSITION.BOTTOM) keyframes.y = pair(style, "dialog-content--slide-y");
44
+ else {
45
+ keyframes.x = pair(style, "dialog-content--slide-x");
46
+ keyframes.y = pair(style, "dialog-content--slide-y");
47
+ keyframes.scaleX = pair(style, "dialog-content--scale-x");
48
+ keyframes.scaleY = pair(style, "dialog-content--scale-y");
49
+ keyframes.opacity = pair(style, "dialog-content--opacity");
50
+ }
51
+ return orient(keyframes, direction);
52
+ }
53
+ function backdropKeyframes(style, direction) {
54
+ return orient({ opacity: pair(style, "dialog-backdrop--opacity") }, direction);
55
+ }
56
+ function orient(keyframes, direction) {
57
+ const result = {};
58
+ for (const [key, value] of Object.entries(keyframes)) {
59
+ if (!value) continue;
60
+ result[key] = direction === "enter" ? [value[0], value[1]] : [value[1], value[0]];
61
+ }
62
+ return result;
63
+ }
64
+ /** Whether anything is left to animate once unstyled properties are dropped. */
65
+ function hasKeyframes(keyframes) {
66
+ return Object.keys(keyframes).length > 0;
67
+ }
68
+ //#endregion
69
+ export { backdropKeyframes, contentKeyframes, hasKeyframes, hideElement, readPosition, showElement };
70
+
71
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","names":[],"sources":["../../src/utils.ts"],"sourcesContent":["import { isPosition, POSITION, type Position } from \"./consts.js\";\n\nexport function hideElement<T extends HTMLElement>(element: T) {\n\telement.style.display = \"none\";\n}\n\nexport function showElement<T extends HTMLElement>(element: T) {\n\telement.style.display = \"block\";\n}\n\n/**\n * Read a `from`/`to` pair of custom properties, or nothing.\n *\n * Motion is handed `var(--x)` strings, and an *unset* custom property resolves\n * to the empty string rather than failing — so a property nobody styled\n * produced an animation between two empty values, which silently does nothing.\n * That is why `drawer-backdrop` could queue a mount animation and still show\n * no `is-animating` anywhere: the animation existed and animated nothing.\n *\n * Returning undefined for an unstyled pair keeps it out of the keyframes\n * entirely, so a theme that styles only the slide gets a slide, and one that\n * styles nothing gets no animation rather than an invisible one.\n */\nfunction pair(\n\tstyle: CSSStyleDeclaration,\n\tproperty: string,\n): [string, string] | undefined {\n\tconst from = style.getPropertyValue(`--${property}-from`).trim();\n\tconst to = style.getPropertyValue(`--${property}-to`).trim();\n\tif (!from || !to) return undefined;\n\treturn [`var(--${property}-from)`, `var(--${property}-to)`];\n}\n\nexport function readPosition(style: CSSStyleDeclaration): Position | undefined {\n\tconst value = style.getPropertyValue(\"--dialog-content--position\").trim();\n\treturn isPosition(value) ? value : undefined;\n}\n\n/**\n * The keyframes for opening the content.\n *\n * An edge position slides on one axis — the drawer shape. No position is the\n * centred shape, which scales and fades as well as sliding. Either way, only\n * the properties a theme actually styled are included.\n */\nexport function contentKeyframes(\n\tstyle: CSSStyleDeclaration,\n\tdirection: \"enter\" | \"exit\",\n): Record<string, string[]> {\n\tconst position = readPosition(style);\n\tconst keyframes: Record<string, [string, string] | undefined> = {};\n\n\tif (position === POSITION.LEFT || position === POSITION.RIGHT) {\n\t\tkeyframes.x = pair(style, \"dialog-content--slide-x\");\n\t} else if (position === POSITION.TOP || position === POSITION.BOTTOM) {\n\t\tkeyframes.y = pair(style, \"dialog-content--slide-y\");\n\t} else {\n\t\tkeyframes.x = pair(style, \"dialog-content--slide-x\");\n\t\tkeyframes.y = pair(style, \"dialog-content--slide-y\");\n\t\tkeyframes.scaleX = pair(style, \"dialog-content--scale-x\");\n\t\tkeyframes.scaleY = pair(style, \"dialog-content--scale-y\");\n\t\tkeyframes.opacity = pair(style, \"dialog-content--opacity\");\n\t}\n\n\treturn orient(keyframes, direction);\n}\n\nexport function backdropKeyframes(\n\tstyle: CSSStyleDeclaration,\n\tdirection: \"enter\" | \"exit\",\n): Record<string, string[]> {\n\treturn orient(\n\t\t{ opacity: pair(style, \"dialog-backdrop--opacity\") },\n\t\tdirection,\n\t);\n}\n\nfunction orient(\n\tkeyframes: Record<string, [string, string] | undefined>,\n\tdirection: \"enter\" | \"exit\",\n): Record<string, string[]> {\n\tconst result: Record<string, string[]> = {};\n\tfor (const [key, value] of Object.entries(keyframes)) {\n\t\tif (!value) continue;\n\t\tresult[key] =\n\t\t\tdirection === \"enter\" ? [value[0], value[1]] : [value[1], value[0]];\n\t}\n\treturn result;\n}\n\n/** Whether anything is left to animate once unstyled properties are dropped. */\nexport function hasKeyframes(keyframes: Record<string, string[]>) {\n\treturn Object.keys(keyframes).length > 0;\n}\n"],"mappings":";;AAEA,SAAgB,YAAmC,SAAY;CAC9D,QAAQ,MAAM,UAAU;AACzB;AAEA,SAAgB,YAAmC,SAAY;CAC9D,QAAQ,MAAM,UAAU;AACzB;;;;;;;;;;;;;;AAeA,SAAS,KACR,OACA,UAC+B;CAC/B,MAAM,OAAO,MAAM,iBAAiB,KAAK,SAAS,MAAM,CAAC,CAAC,KAAK;CAC/D,MAAM,KAAK,MAAM,iBAAiB,KAAK,SAAS,IAAI,CAAC,CAAC,KAAK;CAC3D,IAAI,CAAC,QAAQ,CAAC,IAAI,OAAO,KAAA;CACzB,OAAO,CAAC,SAAS,SAAS,SAAS,SAAS,SAAS,KAAK;AAC3D;AAEA,SAAgB,aAAa,OAAkD;CAC9E,MAAM,QAAQ,MAAM,iBAAiB,4BAA4B,CAAC,CAAC,KAAK;CACxE,OAAO,WAAW,KAAK,IAAI,QAAQ,KAAA;AACpC;;;;;;;;AASA,SAAgB,iBACf,OACA,WAC2B;CAC3B,MAAM,WAAW,aAAa,KAAK;CACnC,MAAM,YAA0D,CAAC;CAEjE,IAAI,aAAa,SAAS,QAAQ,aAAa,SAAS,OACvD,UAAU,IAAI,KAAK,OAAO,yBAAyB;MAC7C,IAAI,aAAa,SAAS,OAAO,aAAa,SAAS,QAC7D,UAAU,IAAI,KAAK,OAAO,yBAAyB;MAC7C;EACN,UAAU,IAAI,KAAK,OAAO,yBAAyB;EACnD,UAAU,IAAI,KAAK,OAAO,yBAAyB;EACnD,UAAU,SAAS,KAAK,OAAO,yBAAyB;EACxD,UAAU,SAAS,KAAK,OAAO,yBAAyB;EACxD,UAAU,UAAU,KAAK,OAAO,yBAAyB;CAC1D;CAEA,OAAO,OAAO,WAAW,SAAS;AACnC;AAEA,SAAgB,kBACf,OACA,WAC2B;CAC3B,OAAO,OACN,EAAE,SAAS,KAAK,OAAO,0BAA0B,EAAE,GACnD,SACD;AACD;AAEA,SAAS,OACR,WACA,WAC2B;CAC3B,MAAM,SAAmC,CAAC;CAC1C,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,SAAS,GAAG;EACrD,IAAI,CAAC,OAAO;EACZ,OAAO,OACN,cAAc,UAAU,CAAC,MAAM,IAAI,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,MAAM,EAAE;CACpE;CACA,OAAO;AACR;;AAGA,SAAgB,aAAa,WAAqC;CACjE,OAAO,OAAO,KAAK,SAAS,CAAC,CAAC,SAAS;AACxC"}
package/package.json CHANGED
@@ -1,6 +1,38 @@
1
1
  {
2
- "name": "@brytdesigns/web-component-dialog",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@brytdesigns/web-component-dialog",
3
+ "version": "0.0.0",
4
+ "description": "Headless dialog web component — drawers and modals in one element",
5
+ "main": "./dist/main/index.js",
6
+ "module": "./dist/main/index.js",
7
+ "types": "./dist/main/index.d.ts",
8
+ "type": "module",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/main/index.d.ts",
12
+ "default": "./dist/main/index.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "vite build",
17
+ "typecheck": "tsc --noEmit"
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "keywords": [],
23
+ "author": "Bryt Designs",
24
+ "license": "ISC",
25
+ "peer-dependencies": {},
26
+ "dependencies": {
27
+ "@brytdesigns/web-component-core": "workspace:*",
28
+ "body-scroll-lock-upgrade": "^1.1.0",
29
+ "component-register": "0.8.7",
30
+ "motion": "^12.43.0",
31
+ "observe-element-in-viewport": "^0.0.15",
32
+ "solid-js": "2.0.0-rc.14"
33
+ },
34
+ "devDependencies": {
35
+ "@repo/vite-config": "workspace:*",
36
+ "vite": "^8.2.2"
37
+ }
38
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.