@funstack/ui 0.1.0-alpha.2 → 0.1.0-alpha.3
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.
- package/dist/accordion/accordion-context.mjs.map +1 -1
- package/dist/accordion/accordion-item.mjs +18 -4
- package/dist/accordion/accordion-item.mjs.map +1 -1
- package/dist/accordion/accordion-panel.mjs +2 -3
- package/dist/accordion/accordion-panel.mjs.map +1 -1
- package/dist/accordion/accordion-root.d.mts.map +1 -1
- package/dist/accordion/accordion-root.mjs +32 -14
- package/dist/accordion/accordion-root.mjs.map +1 -1
- package/dist/accordion/accordion-trigger.mjs +4 -5
- package/dist/accordion/accordion-trigger.mjs.map +1 -1
- package/dist/checkbox/checkbox.d.mts +5 -3
- package/dist/checkbox/checkbox.d.mts.map +1 -1
- package/dist/checkbox/checkbox.mjs +25 -9
- package/dist/checkbox/checkbox.mjs.map +1 -1
- package/dist/checkbox-group/checkbox-group-context.mjs.map +1 -1
- package/dist/checkbox-group/checkbox-group-item.d.mts +2 -1
- package/dist/checkbox-group/checkbox-group-item.d.mts.map +1 -1
- package/dist/checkbox-group/checkbox-group-item.mjs +3 -1
- package/dist/checkbox-group/checkbox-group-item.mjs.map +1 -1
- package/dist/checkbox-group/checkbox-group-root.d.mts +5 -2
- package/dist/checkbox-group/checkbox-group-root.d.mts.map +1 -1
- package/dist/checkbox-group/checkbox-group-root.mjs +24 -5
- package/dist/checkbox-group/checkbox-group-root.mjs.map +1 -1
- package/dist/combobox/combobox-context.mjs.map +1 -1
- package/dist/combobox/combobox-input.d.mts +3 -1
- package/dist/combobox/combobox-input.d.mts.map +1 -1
- package/dist/combobox/combobox-input.mjs +16 -2
- package/dist/combobox/combobox-input.mjs.map +1 -1
- package/dist/combobox/combobox-list.mjs +1 -1
- package/dist/combobox/combobox-list.mjs.map +1 -1
- package/dist/combobox/combobox-root.d.mts.map +1 -1
- package/dist/combobox/combobox-root.mjs +19 -4
- package/dist/combobox/combobox-root.mjs.map +1 -1
- package/dist/dialog/dialog-close.d.mts +17 -10
- package/dist/dialog/dialog-close.d.mts.map +1 -1
- package/dist/dialog/dialog-close.mjs +31 -20
- package/dist/dialog/dialog-close.mjs.map +1 -1
- package/dist/dialog/dialog-context.mjs.map +1 -1
- package/dist/dialog/dialog-form.d.mts +12 -5
- package/dist/dialog/dialog-form.d.mts.map +1 -1
- package/dist/dialog/dialog-form.mjs +16 -28
- package/dist/dialog/dialog-form.mjs.map +1 -1
- package/dist/dialog/dialog-popup.d.mts +10 -4
- package/dist/dialog/dialog-popup.d.mts.map +1 -1
- package/dist/dialog/dialog-popup.mjs +106 -41
- package/dist/dialog/dialog-popup.mjs.map +1 -1
- package/dist/dialog/dialog-root.d.mts +14 -8
- package/dist/dialog/dialog-root.d.mts.map +1 -1
- package/dist/dialog/dialog-root.mjs +66 -11
- package/dist/dialog/dialog-root.mjs.map +1 -1
- package/dist/dialog/dialog-trigger.d.mts +3 -2
- package/dist/dialog/dialog-trigger.d.mts.map +1 -1
- package/dist/dialog/dialog-trigger.mjs.map +1 -1
- package/dist/dialog/index.d.mts +4 -2
- package/dist/dialog/index.d.mts.map +1 -1
- package/dist/dialog/index.mjs +4 -2
- package/dist/dialog/index.mjs.map +1 -1
- package/dist/field/field-control.d.mts +33 -0
- package/dist/field/field-control.d.mts.map +1 -0
- package/dist/field/field-control.mjs +68 -0
- package/dist/field/field-control.mjs.map +1 -0
- package/dist/field/field-description.d.mts +24 -0
- package/dist/field/field-description.d.mts.map +1 -0
- package/dist/field/field-description.mjs +34 -0
- package/dist/field/field-description.mjs.map +1 -0
- package/dist/field/field-error.d.mts +30 -0
- package/dist/field/field-error.d.mts.map +1 -0
- package/dist/field/field-error.mjs +42 -0
- package/dist/field/field-error.mjs.map +1 -0
- package/dist/field/field-label.d.mts +25 -0
- package/dist/field/field-label.d.mts.map +1 -0
- package/dist/field/field-label.mjs +46 -0
- package/dist/field/field-label.mjs.map +1 -0
- package/dist/field/field-root.d.mts +53 -0
- package/dist/field/field-root.d.mts.map +1 -0
- package/dist/field/field-root.mjs +317 -0
- package/dist/field/field-root.mjs.map +1 -0
- package/dist/field/index.d.mts +18 -0
- package/dist/field/index.d.mts.map +1 -0
- package/dist/field/index.mjs +17 -0
- package/dist/field/index.mjs.map +1 -0
- package/dist/form/form-root.d.mts +58 -0
- package/dist/form/form-root.d.mts.map +1 -0
- package/dist/form/form-root.mjs +146 -0
- package/dist/form/form-root.mjs.map +1 -0
- package/dist/form/form-submit.d.mts +31 -0
- package/dist/form/form-submit.d.mts.map +1 -0
- package/dist/form/form-submit.mjs +52 -0
- package/dist/form/form-submit.mjs.map +1 -0
- package/dist/form/index.d.mts +11 -0
- package/dist/form/index.d.mts.map +1 -0
- package/dist/form/index.mjs +11 -0
- package/dist/form/index.mjs.map +1 -0
- package/dist/index.d.mts +27 -1
- package/dist/index.mjs +6 -1
- package/dist/internal/content-focus-probe.mjs +42 -0
- package/dist/internal/content-focus-probe.mjs.map +1 -0
- package/dist/internal/field-context.d.mts +19 -0
- package/dist/internal/field-context.d.mts.map +1 -0
- package/dist/internal/field-context.mjs +14 -0
- package/dist/internal/field-context.mjs.map +1 -0
- package/dist/internal/focus-first-invalid.mjs +67 -0
- package/dist/internal/focus-first-invalid.mjs.map +1 -0
- package/dist/internal/form-status-watcher.mjs +31 -0
- package/dist/internal/form-status-watcher.mjs.map +1 -0
- package/dist/internal/modal-registry.mjs +39 -0
- package/dist/internal/modal-registry.mjs.map +1 -0
- package/dist/internal/types.d.mts +3 -1
- package/dist/internal/types.d.mts.map +1 -1
- package/dist/internal/use-controllable-action.mjs +33 -15
- package/dist/internal/use-controllable-action.mjs.map +1 -1
- package/dist/internal/use-field-control.mjs +107 -0
- package/dist/internal/use-field-control.mjs.map +1 -0
- package/dist/internal/use-form-owner-event.mjs +44 -0
- package/dist/internal/use-form-owner-event.mjs.map +1 -0
- package/dist/internal/use-form-reset.mjs +10 -33
- package/dist/internal/use-form-reset.mjs.map +1 -1
- package/dist/internal/use-latest.mjs +32 -0
- package/dist/internal/use-latest.mjs.map +1 -0
- package/dist/internal/use-modal-registration.mjs +47 -0
- package/dist/internal/use-modal-registration.mjs.map +1 -0
- package/dist/internal/use-native-group-reconciliation.mjs +6 -5
- package/dist/internal/use-native-group-reconciliation.mjs.map +1 -1
- package/dist/internal/use-native-select-value.mjs +7 -5
- package/dist/internal/use-native-select-value.mjs.map +1 -1
- package/dist/internal/use-popover-popup.mjs +8 -22
- package/dist/internal/use-popover-popup.mjs.map +1 -1
- package/dist/internal/use-popover-root.mjs +58 -23
- package/dist/internal/use-popover-root.mjs.map +1 -1
- package/dist/internal/validity.d.mts +24 -0
- package/dist/internal/validity.d.mts.map +1 -0
- package/dist/internal/validity.mjs +108 -0
- package/dist/internal/validity.mjs.map +1 -0
- package/dist/menu/menu-group.d.mts.map +1 -1
- package/dist/menu/menu-group.mjs +3 -2
- package/dist/menu/menu-group.mjs.map +1 -1
- package/dist/menu/menu-radio-group.mjs +9 -4
- package/dist/menu/menu-radio-group.mjs.map +1 -1
- package/dist/menu/menu-root.mjs.map +1 -1
- package/dist/radio-group/radio-group-root.d.mts +4 -2
- package/dist/radio-group/radio-group-root.d.mts.map +1 -1
- package/dist/radio-group/radio-group-root.mjs +34 -8
- package/dist/radio-group/radio-group-root.mjs.map +1 -1
- package/dist/select/select-group.d.mts.map +1 -1
- package/dist/select/select-group.mjs +6 -4
- package/dist/select/select-group.mjs.map +1 -1
- package/dist/select/select-root.d.mts +4 -2
- package/dist/select/select-root.d.mts.map +1 -1
- package/dist/select/select-root.mjs +26 -6
- package/dist/select/select-root.mjs.map +1 -1
- package/dist/slider/index.d.mts +32 -0
- package/dist/slider/index.d.mts.map +1 -0
- package/dist/slider/index.mjs +27 -0
- package/dist/slider/index.mjs.map +1 -0
- package/dist/slider/slider-arithmetic.mjs +120 -0
- package/dist/slider/slider-arithmetic.mjs.map +1 -0
- package/dist/slider/slider-context.mjs +13 -0
- package/dist/slider/slider-context.mjs.map +1 -0
- package/dist/slider/slider-indicator.d.mts +22 -0
- package/dist/slider/slider-indicator.d.mts.map +1 -0
- package/dist/slider/slider-indicator.mjs +36 -0
- package/dist/slider/slider-indicator.mjs.map +1 -0
- package/dist/slider/slider-label.d.mts +23 -0
- package/dist/slider/slider-label.d.mts.map +1 -0
- package/dist/slider/slider-label.mjs +44 -0
- package/dist/slider/slider-label.mjs.map +1 -0
- package/dist/slider/slider-root.d.mts +111 -0
- package/dist/slider/slider-root.d.mts.map +1 -0
- package/dist/slider/slider-root.mjs +582 -0
- package/dist/slider/slider-root.mjs.map +1 -0
- package/dist/slider/slider-thumb.d.mts +58 -0
- package/dist/slider/slider-thumb.d.mts.map +1 -0
- package/dist/slider/slider-thumb.mjs +145 -0
- package/dist/slider/slider-thumb.mjs.map +1 -0
- package/dist/slider/slider-track.d.mts +29 -0
- package/dist/slider/slider-track.d.mts.map +1 -0
- package/dist/slider/slider-track.mjs +54 -0
- package/dist/slider/slider-track.mjs.map +1 -0
- package/dist/slider/slider-value.d.mts +34 -0
- package/dist/slider/slider-value.d.mts.map +1 -0
- package/dist/slider/slider-value.mjs +38 -0
- package/dist/slider/slider-value.mjs.map +1 -0
- package/dist/switch/switch.d.mts +5 -3
- package/dist/switch/switch.d.mts.map +1 -1
- package/dist/switch/switch.mjs +24 -8
- package/dist/switch/switch.mjs.map +1 -1
- package/dist/tabs/tabs-root.mjs +13 -3
- package/dist/tabs/tabs-root.mjs.map +1 -1
- package/dist/toast/index.d.mts +25 -0
- package/dist/toast/index.d.mts.map +1 -0
- package/dist/toast/index.mjs +26 -0
- package/dist/toast/index.mjs.map +1 -0
- package/dist/toast/toast-action.d.mts +39 -0
- package/dist/toast/toast-action.d.mts.map +1 -0
- package/dist/toast/toast-action.mjs +64 -0
- package/dist/toast/toast-action.mjs.map +1 -0
- package/dist/toast/toast-close.d.mts +25 -0
- package/dist/toast/toast-close.d.mts.map +1 -0
- package/dist/toast/toast-close.mjs +46 -0
- package/dist/toast/toast-close.mjs.map +1 -0
- package/dist/toast/toast-context.mjs +19 -0
- package/dist/toast/toast-context.mjs.map +1 -0
- package/dist/toast/toast-description.d.mts +24 -0
- package/dist/toast/toast-description.d.mts.map +1 -0
- package/dist/toast/toast-description.mjs +35 -0
- package/dist/toast/toast-description.mjs.map +1 -0
- package/dist/toast/toast-root.d.mts +43 -0
- package/dist/toast/toast-root.d.mts.map +1 -0
- package/dist/toast/toast-root.mjs +204 -0
- package/dist/toast/toast-root.mjs.map +1 -0
- package/dist/toast/toast-title.d.mts +24 -0
- package/dist/toast/toast-title.d.mts.map +1 -0
- package/dist/toast/toast-title.mjs +33 -0
- package/dist/toast/toast-title.mjs.map +1 -0
- package/dist/toast/toast-viewport.d.mts +52 -0
- package/dist/toast/toast-viewport.d.mts.map +1 -0
- package/dist/toast/toast-viewport.mjs +621 -0
- package/dist/toast/toast-viewport.mjs.map +1 -0
- package/dist/toast/toast.d.mts +119 -0
- package/dist/toast/toast.d.mts.map +1 -0
- package/dist/toast/toast.mjs +153 -0
- package/dist/toast/toast.mjs.map +1 -0
- package/package.json +5 -1
|
@@ -44,8 +44,10 @@ export interface DialogPopupProps extends Omit<ComponentPropsWithRef<"dialog">,
|
|
|
44
44
|
experimental_closedBy?: boolean;
|
|
45
45
|
/**
|
|
46
46
|
* Library mode only. Forwarded to the `<ViewTransition default>` around the dialog. Animates
|
|
47
|
-
* the opening commit
|
|
48
|
-
*
|
|
47
|
+
* the opening commit, content changes while open, and every library-driven close (a Close
|
|
48
|
+
* part, a Form submit or settled action, a foreign close command, a controlled
|
|
49
|
+
* `open={false}`), in all engines; dismissals (Escape, a click outside) are the browser's and
|
|
50
|
+
* animate with CSS only. Ignored (dev warning) in JS-free mode.
|
|
49
51
|
*
|
|
50
52
|
* Static: without `transition` the part renders no `<ViewTransition>`; changing it between
|
|
51
53
|
* `undefined` and a value remounts the element. Any value, `"none"` included, makes a mount or
|
|
@@ -67,8 +69,12 @@ export interface DialogPopupProps extends Omit<ComponentPropsWithRef<"dialog">,
|
|
|
67
69
|
*
|
|
68
70
|
* In library mode the children render from committed state, so content that suspends keeps the
|
|
69
71
|
* dialog closed (and the Trigger pending) until it is ready; `showModal()` runs in the layout
|
|
70
|
-
* effect of the commit that mounted them.
|
|
71
|
-
*
|
|
72
|
+
* effect of the commit that mounted them. A close a part asks for (Close, a `method="dialog"`
|
|
73
|
+
* submit, a Form's settle close, a foreign close command) is cancelled and goes through the
|
|
74
|
+
* Root: `openChangeAction(false)` is awaited and `close(returnValue)` runs in the layout effect
|
|
75
|
+
* of the commit that ends it, inside the view transition if `transition` is set. A dismissal
|
|
76
|
+
* (Escape, a click outside) is the browser's: the native `close` event becomes
|
|
77
|
+
* `openChangeAction(false, { reason: "dismiss", returnValue })`.
|
|
72
78
|
*
|
|
73
79
|
* Owned keys (conventions 1.2): `id`, `open`, `closedby`, `aria-labelledby` and
|
|
74
80
|
* `aria-describedby` (token-merged), `data-modal`, `suppressHydrationWarning`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dialog-popup.d.mts","names":[],"sources":["../../src/dialog/dialog-popup.tsx"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"dialog-popup.d.mts","names":[],"sources":["../../src/dialog/dialog-popup.tsx"],"mappings":";;;iBA4BiB;;WAEN;WACA;;iBAGM,yBAAyB,KACxC;;;;;;;;;;;EAaA;;;;;;;;;;EAUA,eAAe,UAAU;;;;;;;EAOzB;;;;;;;EAOA;;;;;;;;;;;;;EAaA,aAAa;EACb,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;EAC9C,WAAW,WAAW,WAAW;;EAEjC,SAAS,WAAW,iCAAiC;;;;;;;;;;;;;;;;;;;;wBAiGvC,cACd,aACA,cACA,cACA,uBACA,YACA,WACA,OACA,UACA,WACG,QACF,mBAAgB;yBAXH"}
|
|
@@ -4,7 +4,10 @@ import { dataAttr } from "../internal/data-attr.mjs";
|
|
|
4
4
|
import { resolveStateValue, useRender } from "../internal/use-render.mjs";
|
|
5
5
|
import { useDevWarning } from "../internal/use-dev-warning.mjs";
|
|
6
6
|
import { useDialogContext } from "./dialog-context.mjs";
|
|
7
|
-
import {
|
|
7
|
+
import { useContentFocusProbe } from "../internal/content-focus-probe.mjs";
|
|
8
|
+
import { exitAnimations, useExitAnimation } from "../internal/use-exit-animation.mjs";
|
|
9
|
+
import { useModalRegistration } from "../internal/use-modal-registration.mjs";
|
|
10
|
+
import { Activity, useCallback, useEffect, useEffectEvent, useLayoutEffect, useRef, useState } from "react";
|
|
8
11
|
import { jsx } from "react/jsx-runtime";
|
|
9
12
|
//#region src/dialog/dialog-popup.tsx
|
|
10
13
|
const isDev = process.env.NODE_ENV !== "production";
|
|
@@ -52,10 +55,10 @@ function onBackdrop(dialog, event) {
|
|
|
52
55
|
const rect = dialog.getBoundingClientRect();
|
|
53
56
|
return !(rect.top <= clientY && clientY <= rect.bottom && rect.left <= clientX && clientX <= rect.right);
|
|
54
57
|
}
|
|
55
|
-
/**
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
return
|
|
58
|
+
/** The submitter's or invoker's `value` (`""` without one, or where the engine gives no source). */
|
|
59
|
+
function valueOf(element) {
|
|
60
|
+
if (element instanceof HTMLButtonElement || element instanceof HTMLInputElement) return element.value;
|
|
61
|
+
return "";
|
|
59
62
|
}
|
|
60
63
|
/**
|
|
61
64
|
* The dialog itself: a `<dialog>` with the id the Trigger's `commandfor` names, wired to Title
|
|
@@ -65,8 +68,12 @@ function ends(animation) {
|
|
|
65
68
|
*
|
|
66
69
|
* In library mode the children render from committed state, so content that suspends keeps the
|
|
67
70
|
* dialog closed (and the Trigger pending) until it is ready; `showModal()` runs in the layout
|
|
68
|
-
* effect of the commit that mounted them.
|
|
69
|
-
*
|
|
71
|
+
* effect of the commit that mounted them. A close a part asks for (Close, a `method="dialog"`
|
|
72
|
+
* submit, a Form's settle close, a foreign close command) is cancelled and goes through the
|
|
73
|
+
* Root: `openChangeAction(false)` is awaited and `close(returnValue)` runs in the layout effect
|
|
74
|
+
* of the commit that ends it, inside the view transition if `transition` is set. A dismissal
|
|
75
|
+
* (Escape, a click outside) is the browser's: the native `close` event becomes
|
|
76
|
+
* `openChangeAction(false, { reason: "dismiss", returnValue })`.
|
|
70
77
|
*
|
|
71
78
|
* Owned keys (conventions 1.2): `id`, `open`, `closedby`, `aria-labelledby` and
|
|
72
79
|
* `aria-describedby` (token-merged), `data-modal`, `suppressHydrationWarning`.
|
|
@@ -76,10 +83,19 @@ function DialogPopup({ keepMounted, initialFocus, lightDismiss = false, experime
|
|
|
76
83
|
const isLibrary = ctx.mode === "library";
|
|
77
84
|
const { modal, ids, dialogRef, triggerRef } = ctx;
|
|
78
85
|
const [dialog, setDialog] = useState(null);
|
|
79
|
-
const [exiting, setExiting] = useState(false);
|
|
80
|
-
const exitGeneration = useRef(0);
|
|
81
86
|
const awaitingClose = useRef(false);
|
|
87
|
+
const libraryClose = useRef(false);
|
|
88
|
+
const dismissedWhilePending = useRef(null);
|
|
82
89
|
const warnedFocus = useRef(false);
|
|
90
|
+
const exit = useExitAnimation();
|
|
91
|
+
const [prevCommitted, setPrevCommitted] = useState(ctx.committed);
|
|
92
|
+
if (prevCommitted !== ctx.committed) {
|
|
93
|
+
setPrevCommitted(ctx.committed);
|
|
94
|
+
if (isLibrary && prevCommitted && transition === void 0) exit.holdDuringRender();
|
|
95
|
+
}
|
|
96
|
+
const contentProbe = useContentFocusProbe(dialogRef);
|
|
97
|
+
const { wrapContent } = contentProbe;
|
|
98
|
+
useModalRegistration(dialog);
|
|
83
99
|
useDevWarning(!isLibrary && transition !== void 0, "Dialog.Popup: `transition` has no effect in JS-free mode; the browser opens and closes the dialog outside any React transition. Animate it with CSS (`@starting-style`).");
|
|
84
100
|
useDevWarning(!isLibrary && keepMounted === false, "Dialog.Popup: `keepMounted={false}` cannot be honoured in JS-free mode; the content is always mounted so that the server HTML is complete.");
|
|
85
101
|
const libraryRef = useCallback((element) => {
|
|
@@ -120,18 +136,45 @@ function DialogPopup({ keepMounted, initialFocus, lightDismiss = false, experime
|
|
|
120
136
|
useLayoutEffect(() => {
|
|
121
137
|
if (isLibrary) reconcileOnMount();
|
|
122
138
|
}, [isLibrary]);
|
|
139
|
+
/**
|
|
140
|
+
* Design 6.2 rule 2, after a close whose commit unmounted the focused content: when focus is
|
|
141
|
+
* now on `<body>` or still inside the closed dialog, focus the Trigger (if connected), else
|
|
142
|
+
* blur what holds focus. A modal dialog never needs it (the platform restores focus on
|
|
143
|
+
* `close()` unconditionally); a non-modal one restores only when focus is inside.
|
|
144
|
+
*/
|
|
145
|
+
const returnFocus = (element) => {
|
|
146
|
+
const doc = element.ownerDocument;
|
|
147
|
+
const active = doc.activeElement;
|
|
148
|
+
if (active !== null && active !== doc.body && !element.contains(active)) return;
|
|
149
|
+
const trigger = triggerRef.current;
|
|
150
|
+
if (trigger !== null && trigger.isConnected) trigger.focus();
|
|
151
|
+
else if (active instanceof HTMLElement && active !== doc.body) active.blur();
|
|
152
|
+
};
|
|
123
153
|
useLayoutEffect(() => {
|
|
124
154
|
if (!isLibrary) return;
|
|
125
155
|
const element = dialogRef.current;
|
|
126
156
|
if (element === null || !element.isConnected) return;
|
|
127
|
-
if (ctx.pending
|
|
128
|
-
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
157
|
+
if (!ctx.pending && ctx.latestInFlight() === null) {
|
|
158
|
+
const dismissed = dismissedWhilePending.current;
|
|
159
|
+
if (ctx.committed && !element.open) {
|
|
160
|
+
if (!awaitingClose.current) {
|
|
161
|
+
dismissedWhilePending.current = null;
|
|
162
|
+
if (dismissed !== null && dismissed.generation === ctx.openGeneration()) ctx.notifyClosed(dismissed.event, "dismiss", dismissed.returnValue);
|
|
163
|
+
else openDialog(element);
|
|
164
|
+
}
|
|
165
|
+
} else if (!ctx.committed && element.open) {
|
|
166
|
+
ctx.recordCloseIntent(void 0);
|
|
167
|
+
libraryClose.current = true;
|
|
168
|
+
element.close(ctx.takeCloseReturnValue());
|
|
169
|
+
if (contentProbe.hadFocus()) {
|
|
170
|
+
contentProbe.forget();
|
|
171
|
+
returnFocus(element);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
134
174
|
}
|
|
175
|
+
if (!ctx.committed && dismissedWhilePending.current !== null && ctx.latestInFlight() === null) dismissedWhilePending.current = null;
|
|
176
|
+
if (ctx.committed && element.open) contentProbe.forget();
|
|
177
|
+
if (exit.held && !ctx.committed && !exit.waiting() && !awaitingClose.current && !element.open) exit.release();
|
|
135
178
|
});
|
|
136
179
|
useEffect(() => {
|
|
137
180
|
if (isLibrary || initialFocus === void 0 || dialog === null) return;
|
|
@@ -151,22 +194,36 @@ function DialogPopup({ keepMounted, initialFocus, lightDismiss = false, experime
|
|
|
151
194
|
if (!isLibrary) return;
|
|
152
195
|
event.preventDefault();
|
|
153
196
|
ctx.requestOpen(event, "activate");
|
|
154
|
-
} else if (command === "request-close" || command === "close")
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
197
|
+
} else if (command === "request-close" || command === "close") {
|
|
198
|
+
if (isLibrary) {
|
|
199
|
+
event.preventDefault();
|
|
200
|
+
const source = event.source;
|
|
201
|
+
ctx.requestClose(event, "activate", valueOf(source));
|
|
202
|
+
} else ctx.recordCloseIntent({
|
|
203
|
+
reason: "activate",
|
|
204
|
+
event
|
|
205
|
+
});
|
|
206
|
+
}
|
|
158
207
|
});
|
|
159
208
|
const onSubmit = useEffectEvent((event) => {
|
|
160
209
|
const element = dialogRef.current;
|
|
161
210
|
const form = event.target;
|
|
162
211
|
if (element === null || !(form instanceof HTMLFormElement)) return;
|
|
163
212
|
if (form.closest("dialog") !== element) return;
|
|
164
|
-
const
|
|
213
|
+
const submitter = event.submitter;
|
|
214
|
+
const formMethod = submitter?.getAttribute("formmethod");
|
|
165
215
|
if ((formMethod != null ? formMethod.toLowerCase() : form.method) !== "dialog") return;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
216
|
+
if (!isLibrary) {
|
|
217
|
+
ctx.recordCloseIntent({
|
|
218
|
+
reason: "activate",
|
|
219
|
+
event
|
|
220
|
+
});
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (event.defaultPrevented) return;
|
|
224
|
+
if (event.cancelBubble) return;
|
|
225
|
+
event.preventDefault();
|
|
226
|
+
ctx.requestClose(event, "activate", valueOf(submitter));
|
|
170
227
|
});
|
|
171
228
|
const onClose = useEffectEvent((event) => {
|
|
172
229
|
const element = dialogRef.current;
|
|
@@ -180,29 +237,37 @@ function DialogPopup({ keepMounted, initialFocus, lightDismiss = false, experime
|
|
|
180
237
|
else if (active instanceof HTMLElement) active.blur();
|
|
181
238
|
}
|
|
182
239
|
const intent = ctx.takeCloseIntent();
|
|
240
|
+
const echo = libraryClose.current;
|
|
241
|
+
libraryClose.current = false;
|
|
183
242
|
if (!isLibrary) return;
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const generation = ++exitGeneration.current;
|
|
189
|
-
setExiting(true);
|
|
190
|
-
Promise.allSettled(animations.map((animation) => animation.finished)).then(() => {
|
|
191
|
-
if (exitGeneration.current === generation) startTransition(() => setExiting(false));
|
|
192
|
-
});
|
|
193
|
-
}
|
|
243
|
+
if (!echo || transition === void 0) {
|
|
244
|
+
const animations = exitAnimations(element);
|
|
245
|
+
if (animations.length > 0) exit.holdUntil(animations);
|
|
246
|
+
else exit.release();
|
|
194
247
|
}
|
|
195
|
-
|
|
248
|
+
if (echo) return;
|
|
249
|
+
ctx.takeCloseReturnValue();
|
|
250
|
+
if (ctx.intended()) ctx.notifyClosed(intent?.event ?? event, intent?.reason ?? "dismiss", element.returnValue);
|
|
251
|
+
else dismissedWhilePending.current = {
|
|
252
|
+
event: intent?.event ?? event,
|
|
253
|
+
returnValue: element.returnValue,
|
|
254
|
+
generation: ctx.openGeneration()
|
|
255
|
+
};
|
|
196
256
|
});
|
|
197
257
|
const onBeforeToggle = useEffectEvent((event) => {
|
|
198
258
|
if (event.target !== dialogRef.current) return;
|
|
199
259
|
const newState = event.newState;
|
|
200
260
|
awaitingClose.current = newState === "closed";
|
|
201
|
-
if (newState === "open")
|
|
261
|
+
if (newState === "open") {
|
|
262
|
+
dismissedWhilePending.current = null;
|
|
263
|
+
ctx.nextOpenGeneration();
|
|
264
|
+
contentProbe.forget();
|
|
265
|
+
}
|
|
202
266
|
});
|
|
203
267
|
useEffect(() => {
|
|
204
268
|
if (dialog === null) return;
|
|
205
269
|
const doc = dialog.ownerDocument;
|
|
270
|
+
const root = dialog.getRootNode();
|
|
206
271
|
const nativeClosedBy = experimental_closedBy && "closedBy" in dialog;
|
|
207
272
|
const listeners = [
|
|
208
273
|
[
|
|
@@ -212,7 +277,7 @@ function DialogPopup({ keepMounted, initialFocus, lightDismiss = false, experime
|
|
|
212
277
|
false
|
|
213
278
|
],
|
|
214
279
|
[
|
|
215
|
-
|
|
280
|
+
root,
|
|
216
281
|
"submit",
|
|
217
282
|
onSubmit,
|
|
218
283
|
false
|
|
@@ -329,14 +394,14 @@ function DialogPopup({ keepMounted, initialFocus, lightDismiss = false, experime
|
|
|
329
394
|
open: ctx.committed,
|
|
330
395
|
modal
|
|
331
396
|
};
|
|
332
|
-
const visible = ctx.committed ||
|
|
397
|
+
const visible = ctx.committed || exit.held;
|
|
333
398
|
const content = (s) => {
|
|
334
399
|
if (!isLibrary) return resolveStateValue(children, s);
|
|
335
400
|
if (keepMounted === true) return /* @__PURE__ */ jsx(Activity, {
|
|
336
401
|
mode: visible ? "visible" : "hidden",
|
|
337
|
-
children: resolveStateValue(children, s)
|
|
402
|
+
children: wrapContent(resolveStateValue(children, s))
|
|
338
403
|
});
|
|
339
|
-
return visible ? resolveStateValue(children, s) : null;
|
|
404
|
+
return visible ? wrapContent(resolveStateValue(children, s)) : null;
|
|
340
405
|
};
|
|
341
406
|
const libraryProps = {
|
|
342
407
|
id: ids.dialog,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dialog-popup.mjs","names":[],"sources":["../../src/dialog/dialog-popup.tsx"],"sourcesContent":["\"use client\";\nimport {\n Activity,\n startTransition,\n useCallback,\n useEffect,\n useEffectEvent,\n useLayoutEffect,\n useRef,\n useState,\n} from \"react\";\nimport type {\n ComponentPropsWithRef,\n CSSProperties,\n ReactNode,\n RefObject,\n ViewTransitionClass,\n} from \"react\";\nimport { useTransitionBoundary } from \"../internal/transition-boundary.js\";\nimport { dataAttr } from \"../internal/data-attr.js\";\nimport type { RenderProp, StateValue } from \"../internal/types.js\";\nimport { useDevWarning } from \"../internal/use-dev-warning.js\";\nimport { resolveStateValue, useRender } from \"../internal/use-render.js\";\nimport type { DataAttributes } from \"../internal/use-render.js\";\nimport { useDialogContext } from \"./dialog-context.js\";\n\nexport interface DialogPopupState {\n /** Committed open state (library mode); `false` in JS-free mode. Read `:open` for the DOM truth. */\n readonly open: boolean;\n readonly modal: boolean;\n}\n\nexport interface DialogPopupProps extends Omit<\n ComponentPropsWithRef<\"dialog\">,\n \"id\" | \"open\" | \"closedby\" | \"onToggle\" | \"onBeforeToggle\" | \"className\" | \"style\" | \"children\"\n> {\n /**\n * Library mode: what happens to the children while the dialog is closed.\n * - `false` (default): not rendered. Mounting them is part of the opening transition, so\n * content that suspends holds the open until it is ready. Unmount waits for the dialog's\n * exit animations.\n * - `true`: rendered inside `<Activity mode=\"hidden\">`: DOM and state preserved, effects\n * stopped, pre-rendered at low priority. Not in the server HTML.\n *\n * Ignored in JS-free mode (always mounted).\n */\n keepMounted?: boolean;\n /**\n * Element to focus when the dialog opens: a ref, or a selector resolved inside the dialog.\n * The library writes `autofocus` on it right before `showModal()` / `show()` and the platform\n * focuses it. Default: the platform's choice (first focusable descendant; the dialog itself if\n * none). Use `tabIndex={-1}` on a heading or wrapper for long or destructive dialogs (APG).\n *\n * In JS-free mode the browser opens the dialog itself, so the attribute is written after\n * hydration and kept while mounted; before hydration only `autoFocus` in the markup works.\n */\n initialFocus?: RefObject<HTMLElement | null> | string;\n /**\n * Close on a click outside the dialog (on the backdrop for modal dialogs). Default `false`.\n * Baseline: JS `pointerdown` + `click` listeners, which close only when both the press and the\n * click land outside (a drag that starts inside, such as selecting text, never closes); with\n * `experimental_closedBy`, native `closedby=\"any\"`, which behaves the same.\n */\n lightDismiss?: boolean;\n /**\n * Experimental (Chrome 134+, Firefox 141+; Safari 27.0 ignores it). Renders `closedby`\n * (`\"any\"` when `lightDismiss`, else `\"closerequest\"`) and skips the JS dismiss listeners where\n * `HTMLDialogElement.prototype.closedBy` exists. Elsewhere the JS baseline stays active.\n * Default `false`.\n */\n experimental_closedBy?: boolean;\n /**\n * Library mode only. Forwarded to the `<ViewTransition default>` around the dialog. Animates\n * the opening commit and content changes while open; the close is the browser's and animates\n * with CSS. Ignored (dev warning) in JS-free mode.\n *\n * Static: without `transition` the part renders no `<ViewTransition>`; changing it between\n * `undefined` and a value remounts the element. Any value, `\"none\"` included, makes a mount or\n * `<Activity>` reveal of this part inside a transition start a document view transition: the\n * page cross-fades for about 250 ms and the next transition commit waits for it (issue #35).\n */\n transition?: ViewTransitionClass;\n className?: StateValue<string | undefined, DialogPopupState>;\n style?: StateValue<CSSProperties | undefined, DialogPopupState>;\n children?: StateValue<ReactNode, DialogPopupState>;\n /** Must render a `<dialog>`. */\n render?: RenderProp<ComponentPropsWithRef<\"dialog\">, DialogPopupState>;\n}\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\n/** Resolves `initialFocus` against the dialog (a ref, or a selector inside the dialog). */\nfunction resolveInitialFocus(\n dialog: HTMLDialogElement,\n initialFocus: RefObject<HTMLElement | null> | string,\n): HTMLElement | null {\n if (typeof initialFocus === \"string\") return dialog.querySelector<HTMLElement>(initialFocus);\n return initialFocus.current;\n}\n\n/** The nearest `<dialog>` around a node (the dialog itself counts). */\nfunction closestDialog(target: EventTarget | null): Element | null {\n return target instanceof Element ? target.closest(\"dialog\") : null;\n}\n\n/**\n * An open popover that takes Escape before the dialog: `auto` or `hint` (they have a close\n * watcher). A `manual` popover, or `hint` where the engine maps it to `manual`, does not.\n */\nfunction isOpenLightPopover(element: Element): boolean {\n if (!element.hasAttribute(\"popover\")) return false;\n // The IDL attribute reflects the state the engine applies; jsdom has none.\n const reflected: unknown = (element as HTMLElement).popover;\n const state =\n typeof reflected === \"string\" ? reflected : element.getAttribute(\"popover\")!.toLowerCase();\n if (state === \"manual\") return false;\n try {\n return element.matches(\":popover-open\");\n } catch {\n return false;\n }\n}\n\n/**\n * Whether Escape belongs to a layer inside `dialog` that sits above it (design 9: \"closes the\n * innermost one only\"): the target is inside an open nested `<dialog>` or popover, or an open\n * `auto` / `hint` popover inside the dialog would take the close request (close watchers are\n * per document, not per focus).\n */\nfunction escapeBelongsToInnerLayer(dialog: HTMLDialogElement, target: EventTarget | null): boolean {\n for (\n let node = target instanceof Element ? target : null;\n node !== null && node !== dialog;\n node = node.parentElement\n ) {\n if (node instanceof HTMLDialogElement && node.open) return true;\n if (isOpenLightPopover(node)) return true;\n }\n for (const element of dialog.querySelectorAll(\"[popover]\")) {\n if (isOpenLightPopover(element)) return true;\n }\n return false;\n}\n\n/** Whether a pointer event on the dialog itself lies outside its border box (the `::backdrop`). */\nfunction onBackdrop(dialog: HTMLDialogElement, event: Event): boolean {\n if (event.target !== dialog) return false;\n const { clientX, clientY } = event as MouseEvent;\n const rect = dialog.getBoundingClientRect();\n return !(\n rect.top <= clientY &&\n clientY <= rect.bottom &&\n rect.left <= clientX &&\n clientX <= rect.right\n );\n}\n\n/** An animation that ends: infinite ones (a spinner) would hold the content forever. */\nfunction ends(animation: Animation): boolean {\n const end = animation.effect?.getComputedTiming().endTime;\n return typeof end === \"number\" && Number.isFinite(end);\n}\n\n/**\n * The dialog itself: a `<dialog>` with the id the Trigger's `commandfor` names, wired to Title\n * and Description through `aria-labelledby` / `aria-describedby`. Never renders `open`: modal\n * opening and closing go through `showModal()` / `close()` only. Style it with `:open` and\n * `:modal`; it emits `data-modal` but no `data-open`.\n *\n * In library mode the children render from committed state, so content that suspends keeps the\n * dialog closed (and the Trigger pending) until it is ready; `showModal()` runs in the layout\n * effect of the commit that mounted them. Closing is the browser's: the native `close` event\n * becomes `openChangeAction(false, { reason, returnValue })`.\n *\n * Owned keys (conventions 1.2): `id`, `open`, `closedby`, `aria-labelledby` and\n * `aria-describedby` (token-merged), `data-modal`, `suppressHydrationWarning`.\n */\nexport function DialogPopup({\n keepMounted,\n initialFocus,\n lightDismiss = false,\n experimental_closedBy = false,\n transition,\n className,\n style,\n children,\n render,\n ...rest\n}: DialogPopupProps) {\n const ctx = useDialogContext(\"Dialog.Popup\");\n const isLibrary = ctx.mode === \"library\";\n const { modal, ids, dialogRef, triggerRef } = ctx;\n const [dialog, setDialog] = useState<HTMLDialogElement | null>(null);\n // Children stay mounted while the dialog's exit animation runs (design 4.4).\n const [exiting, setExiting] = useState(false);\n const exitGeneration = useRef(0);\n // Set by the synchronous `beforetoggle` of a close, cleared by the `close` event a task later:\n // the dialog is closed but its notification is not dispatched yet, so the apply effect must\n // not re-open it from a commit that lands in between.\n const awaitingClose = useRef(false);\n const warnedFocus = useRef(false);\n\n useDevWarning(\n !isLibrary && transition !== undefined,\n \"Dialog.Popup: `transition` has no effect in JS-free mode; the browser opens and closes the \" +\n \"dialog outside any React transition. Animate it with CSS (`@starting-style`).\",\n );\n useDevWarning(\n !isLibrary && keepMounted === false,\n \"Dialog.Popup: `keepMounted={false}` cannot be honoured in JS-free mode; the content is \" +\n \"always mounted so that the server HTML is complete.\",\n );\n\n const libraryRef = useCallback(\n (element: HTMLDialogElement) => {\n if (isDev && element.tagName !== \"DIALOG\") {\n console.error(\n `Dialog.Popup: expected to render a <dialog> element but got <${element.tagName.toLowerCase()}>. ` +\n \"Top layer, backdrop, focus and Escape all come from a real <dialog>.\",\n );\n }\n dialogRef.current = element;\n setDialog(element);\n return () => {\n if (dialogRef.current === element) dialogRef.current = null;\n setDialog(null);\n };\n },\n [dialogRef],\n );\n\n // --- Opening -----------------------------------------------------------------------------\n\n const openDialog = (element: HTMLDialogElement): void => {\n let target: HTMLElement | null = null;\n if (initialFocus !== undefined) {\n target = resolveInitialFocus(element, initialFocus);\n if (target === null || !element.contains(target)) {\n if (isDev && !warnedFocus.current) {\n warnedFocus.current = true;\n console.error(\n target === null\n ? \"Dialog.Popup: `initialFocus` resolved to no element; the platform picks the focus target.\"\n : \"Dialog.Popup: `initialFocus` is outside the dialog; the platform picks the focus target.\",\n );\n }\n target = null;\n }\n }\n // The platform's dialog focusing steps honour `autofocus`; React drops `autoFocus` on\n // client-rendered content (react/react#23301). Never `.focus()` (design 6.1).\n const added = target !== null && !target.hasAttribute(\"autofocus\");\n if (added) target!.setAttribute(\"autofocus\", \"\");\n try {\n if (modal) element.showModal();\n else element.show();\n } finally {\n if (added) target!.removeAttribute(\"autofocus\");\n }\n };\n\n // Mount reconciliation (design 2.4): a click before hydration opened the dialog natively.\n // Runs before the apply effect; the dispatch makes the apply effect wait for it.\n const reconcileOnMount = useEffectEvent(() => {\n const element = dialogRef.current;\n if (element === null || !element.open) return;\n ctx.requestOpen(undefined, \"platform\");\n });\n useLayoutEffect(() => {\n if (isLibrary) reconcileOnMount();\n }, [isLibrary]);\n\n // The apply effect (design 2.5): after every commit, once nothing is in flight, the DOM\n // follows committed state. Opens on the user's dispatch, `defaultOpen`, a controlled parent,\n // the re-open of a refused or failed close; closes after a controlled close or a refused\n // pre-hydration open.\n useLayoutEffect(() => {\n if (!isLibrary) return;\n const element = dialogRef.current;\n if (element === null || !element.isConnected) return;\n if (ctx.pending || ctx.latestInFlight() !== null) return;\n if (ctx.committed && !element.open) {\n if (awaitingClose.current) return;\n openDialog(element);\n } else if (!ctx.committed && element.open) {\n // An echo: the `close` listener finds nothing intended and skips the dispatch. `close()`,\n // not `requestClose()`: a parent's close is not vetoable.\n ctx.recordCloseIntent(undefined);\n element.close();\n }\n });\n\n // JS-free mode: there is no \"right before showModal()\" (the browser runs the command), so the\n // `autofocus` attribute stays on the target while it is mounted.\n useEffect(() => {\n if (isLibrary || initialFocus === undefined || dialog === null) return;\n const target = resolveInitialFocus(dialog, initialFocus);\n if (target === null || !dialog.contains(target) || target.hasAttribute(\"autofocus\")) return;\n target.setAttribute(\"autofocus\", \"\");\n return () => target.removeAttribute(\"autofocus\");\n }, [isLibrary, initialFocus, dialog]);\n\n // --- Native listeners (conventions 7.4) ---------------------------------------------------\n\n const onCommand = useEffectEvent((event: Event) => {\n if (event.target !== dialogRef.current || event.defaultPrevented) return;\n const command = String((event as Event & { command?: string }).command ?? \"\").toLowerCase();\n if (command === \"show-modal\") {\n // A foreign invoker (design 2.2): opening goes through the library in library mode.\n if (!isLibrary) return;\n event.preventDefault();\n ctx.requestOpen(event, \"activate\");\n } else if (command === \"request-close\" || command === \"close\") {\n ctx.recordCloseIntent({ reason: \"activate\", event });\n }\n });\n\n const onSubmit = useEffectEvent((event: Event) => {\n const element = dialogRef.current;\n const form = event.target;\n if (element === null || !(form instanceof HTMLFormElement)) return;\n if (form.closest(\"dialog\") !== element) return;\n const submitter = (event as SubmitEvent).submitter;\n const formMethod = submitter?.getAttribute(\"formmethod\");\n const method = formMethod != null ? formMethod.toLowerCase() : form.method;\n if (method !== \"dialog\") return;\n ctx.recordCloseIntent({ reason: \"activate\", event });\n });\n\n const onClose = useEffectEvent((event: Event) => {\n const element = dialogRef.current;\n if (element === null || event.target !== element) return;\n awaitingClose.current = false;\n\n // Focus return is the platform's; it leaves focus inside the hidden dialog only when the\n // opener is gone (design 6.2).\n const doc = element.ownerDocument;\n const active = doc.activeElement;\n if (active !== null && active !== doc.body && element.contains(active)) {\n const trigger = triggerRef.current;\n if (trigger !== null && trigger.isConnected) trigger.focus();\n else if (active instanceof HTMLElement) active.blur();\n }\n\n const intent = ctx.takeCloseIntent();\n if (!isLibrary) return;\n\n // Keep the children while the exit animation runs (design 4.4). Firefox has none.\n if (element.isConnected && typeof element.getAnimations === \"function\") {\n void getComputedStyle(element).opacity; // flush style so the exit transitions exist\n const animations = element.getAnimations({ subtree: true }).filter(ends);\n if (animations.length > 0) {\n const generation = ++exitGeneration.current;\n setExiting(true);\n void Promise.allSettled(animations.map((animation) => animation.finished)).then(() => {\n if (exitGeneration.current === generation) startTransition(() => setExiting(false));\n });\n }\n }\n\n ctx.notifyClosed(intent?.event ?? event, intent?.reason ?? \"dismiss\", element.returnValue);\n });\n\n const onBeforeToggle = useEffectEvent((event: Event) => {\n if (event.target !== dialogRef.current) return;\n const newState = (event as ToggleEvent).newState;\n awaitingClose.current = newState === \"closed\";\n // Every native open counts as a new opening (design 3.4, as Popover 14.1): foreign,\n // pre-hydration and JS-free opens too, so a Close action from an earlier opening skips its\n // close. Counted here, synchronously inside `showModal()` / `show()`, not at the queued\n // `toggle`: an action settling between the open and that task would otherwise still see the\n // old generation and close the new open.\n if (newState === \"open\") ctx.nextOpenGeneration();\n });\n\n useEffect(() => {\n if (dialog === null) return;\n const doc = dialog.ownerDocument;\n // Feature detection only with the flag on, inside an effect (conventions 8).\n const nativeClosedBy = experimental_closedBy && \"closedBy\" in dialog;\n\n const listeners: [EventTarget, string, EventListener, boolean][] = [\n [dialog, \"command\", onCommand, false],\n [dialog, \"submit\", onSubmit, false],\n [dialog, \"close\", onClose, false],\n [dialog, \"beforetoggle\", onBeforeToggle, false],\n ];\n\n if (!modal && !nativeClosedBy) {\n // Non-modal dialogs get no close watcher Escape from the platform (design 2.6).\n listeners.push([\n dialog,\n \"keydown\",\n (event) => {\n const keyboardEvent = event as KeyboardEvent;\n if (keyboardEvent.key !== \"Escape\" || keyboardEvent.defaultPrevented) return;\n if (!dialog.open || closestDialog(keyboardEvent.target) !== dialog) return;\n // A popover or nested dialog inside takes this Escape (its close watcher); leave the\n // event alone so that it can.\n if (escapeBelongsToInnerLayer(dialog, keyboardEvent.target)) return;\n keyboardEvent.preventDefault();\n dialog.requestClose();\n },\n false,\n ]);\n }\n\n // Light dismiss needs both the press and the click outside, like native `closedby=\"any\"`: a\n // drag that starts inside (selecting text in an input) and ends on the backdrop or outside\n // fires `click` on the common ancestor and must not close (it would lose a half-filled form).\n let pressedOutside = false;\n\n const isOutside = (target: EventTarget | null): boolean => {\n if (!(target instanceof Node)) return true;\n if (dialog.contains(target)) return false;\n // The Trigger re-opening a non-modal dialog is not a click outside.\n return !(triggerRef.current?.contains(target) ?? false);\n };\n let outsideAttached = false;\n const onOutsidePointerDown = (event: Event) => {\n pressedOutside = dialog.open && isOutside(event.target);\n };\n const onOutsideClick = (event: Event) => {\n const pressed = pressedOutside;\n pressedOutside = false;\n if (!dialog.open || !pressed || !isOutside(event.target)) return;\n dialog.requestClose();\n };\n const syncOutside = () => {\n const want = dialog.open;\n if (want === outsideAttached) return;\n outsideAttached = want;\n pressedOutside = false;\n if (want) {\n doc.addEventListener(\"pointerdown\", onOutsidePointerDown, true);\n doc.addEventListener(\"click\", onOutsideClick, true);\n } else {\n doc.removeEventListener(\"pointerdown\", onOutsidePointerDown, true);\n doc.removeEventListener(\"click\", onOutsideClick, true);\n }\n };\n\n if (lightDismiss && !nativeClosedBy) {\n if (modal) {\n // A press or click on the ::backdrop targets the dialog itself, outside its border box.\n listeners.push(\n [\n dialog,\n \"pointerdown\",\n (event) => {\n pressedOutside = dialog.open && onBackdrop(dialog, event);\n },\n false,\n ],\n [\n dialog,\n \"click\",\n (event) => {\n const pressed = pressedOutside;\n pressedOutside = false;\n if (!dialog.open || !pressed || !onBackdrop(dialog, event)) return;\n dialog.requestClose();\n },\n false,\n ],\n );\n } else {\n // A document listener only while open, following the native `toggle` (the React\n // `onToggle` prop never fires on a hydrated <dialog>); reads `dialog.open`, not\n // `newState` (coalescing).\n listeners.push([dialog, \"toggle\", syncOutside, false]);\n syncOutside();\n }\n }\n\n for (const [target, type, listener, capture] of listeners) {\n target.addEventListener(type, listener, capture);\n }\n return () => {\n for (const [target, type, listener, capture] of listeners) {\n target.removeEventListener(type, listener, capture);\n }\n if (outsideAttached) {\n doc.removeEventListener(\"pointerdown\", onOutsidePointerDown, true);\n doc.removeEventListener(\"click\", onOutsideClick, true);\n }\n };\n }, [dialog, modal, lightDismiss, experimental_closedBy, triggerRef]);\n\n // Development check: the dialog has an accessible name when it is (or can be) shown.\n const shown = !isLibrary || ctx.committed;\n useEffect(() => {\n if (!isDev || dialog === null || !shown) return;\n if (dialog.hasAttribute(\"aria-label\")) return;\n const labelledBy = (dialog.getAttribute(\"aria-labelledby\") ?? \"\").split(/\\s+/);\n const doc = dialog.ownerDocument;\n if (labelledBy.some((id) => id !== \"\" && doc.getElementById(id) !== null)) return;\n console.error(\n \"Dialog.Popup: the dialog has no accessible name. Render a Dialog.Title, or give the \" +\n \"Popup an `aria-label`.\",\n );\n }, [dialog, shown]);\n\n const state: DialogPopupState = { open: ctx.committed, modal };\n const visible = ctx.committed || exiting;\n\n const content = (s: DialogPopupState): ReactNode => {\n if (!isLibrary) return resolveStateValue(children, s);\n if (keepMounted === true) {\n // The mode changes with committed state, inside the Root's transition (conventions 5.5);\n // it stays visible while an exit animation runs.\n return (\n <Activity mode={visible ? \"visible\" : \"hidden\"}>{resolveStateValue(children, s)}</Activity>\n );\n }\n return visible ? resolveStateValue(children, s) : null;\n };\n\n const libraryProps: ComponentPropsWithRef<\"dialog\"> & DataAttributes = {\n id: ids.dialog,\n // Never rendered: removing `open` by hand hides a modal dialog without `close` and leaves\n // `:modal` true (research). Owned so a `render` element cannot add it.\n open: undefined,\n closedby: experimental_closedBy ? (lightDismiss ? \"any\" : \"closerequest\") : undefined,\n \"aria-labelledby\": ids.title,\n \"aria-describedby\": ids.description,\n \"data-modal\": dataAttr(modal),\n // A pre-hydration open leaves an `open` attribute the server did not render (design 2.4).\n suppressHydrationWarning: true,\n };\n\n const element = useRender<ComponentPropsWithRef<\"dialog\">, DialogPopupState>({\n defaultElement: \"dialog\",\n render,\n state,\n className,\n style,\n children: content,\n props: rest,\n libraryProps,\n ref: libraryRef,\n });\n\n return useTransitionBoundary(\"Dialog.Popup\", isLibrary ? transition : undefined, element);\n}\n\nDialogPopup.displayName = \"DialogPopup\";\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"dialog-popup.mjs","names":[],"sources":["../../src/dialog/dialog-popup.tsx"],"sourcesContent":["\"use client\";\nimport {\n Activity,\n useCallback,\n useEffect,\n useEffectEvent,\n useLayoutEffect,\n useRef,\n useState,\n} from \"react\";\nimport type {\n ComponentPropsWithRef,\n CSSProperties,\n ReactNode,\n RefObject,\n ViewTransitionClass,\n} from \"react\";\nimport { useContentFocusProbe } from \"../internal/content-focus-probe.js\";\nimport { useTransitionBoundary } from \"../internal/transition-boundary.js\";\nimport { dataAttr } from \"../internal/data-attr.js\";\nimport type { RenderProp, StateValue } from \"../internal/types.js\";\nimport { useDevWarning } from \"../internal/use-dev-warning.js\";\nimport { exitAnimations, useExitAnimation } from \"../internal/use-exit-animation.js\";\nimport { useModalRegistration } from \"../internal/use-modal-registration.js\";\nimport { resolveStateValue, useRender } from \"../internal/use-render.js\";\nimport type { DataAttributes } from \"../internal/use-render.js\";\nimport { useDialogContext } from \"./dialog-context.js\";\n\nexport interface DialogPopupState {\n /** Committed open state (library mode); `false` in JS-free mode. Read `:open` for the DOM truth. */\n readonly open: boolean;\n readonly modal: boolean;\n}\n\nexport interface DialogPopupProps extends Omit<\n ComponentPropsWithRef<\"dialog\">,\n \"id\" | \"open\" | \"closedby\" | \"onToggle\" | \"onBeforeToggle\" | \"className\" | \"style\" | \"children\"\n> {\n /**\n * Library mode: what happens to the children while the dialog is closed.\n * - `false` (default): not rendered. Mounting them is part of the opening transition, so\n * content that suspends holds the open until it is ready. Unmount waits for the dialog's\n * exit animations.\n * - `true`: rendered inside `<Activity mode=\"hidden\">`: DOM and state preserved, effects\n * stopped, pre-rendered at low priority. Not in the server HTML.\n *\n * Ignored in JS-free mode (always mounted).\n */\n keepMounted?: boolean;\n /**\n * Element to focus when the dialog opens: a ref, or a selector resolved inside the dialog.\n * The library writes `autofocus` on it right before `showModal()` / `show()` and the platform\n * focuses it. Default: the platform's choice (first focusable descendant; the dialog itself if\n * none). Use `tabIndex={-1}` on a heading or wrapper for long or destructive dialogs (APG).\n *\n * In JS-free mode the browser opens the dialog itself, so the attribute is written after\n * hydration and kept while mounted; before hydration only `autoFocus` in the markup works.\n */\n initialFocus?: RefObject<HTMLElement | null> | string;\n /**\n * Close on a click outside the dialog (on the backdrop for modal dialogs). Default `false`.\n * Baseline: JS `pointerdown` + `click` listeners, which close only when both the press and the\n * click land outside (a drag that starts inside, such as selecting text, never closes); with\n * `experimental_closedBy`, native `closedby=\"any\"`, which behaves the same.\n */\n lightDismiss?: boolean;\n /**\n * Experimental (Chrome 134+, Firefox 141+; Safari 27.0 ignores it). Renders `closedby`\n * (`\"any\"` when `lightDismiss`, else `\"closerequest\"`) and skips the JS dismiss listeners where\n * `HTMLDialogElement.prototype.closedBy` exists. Elsewhere the JS baseline stays active.\n * Default `false`.\n */\n experimental_closedBy?: boolean;\n /**\n * Library mode only. Forwarded to the `<ViewTransition default>` around the dialog. Animates\n * the opening commit, content changes while open, and every library-driven close (a Close\n * part, a Form submit or settled action, a foreign close command, a controlled\n * `open={false}`), in all engines; dismissals (Escape, a click outside) are the browser's and\n * animate with CSS only. Ignored (dev warning) in JS-free mode.\n *\n * Static: without `transition` the part renders no `<ViewTransition>`; changing it between\n * `undefined` and a value remounts the element. Any value, `\"none\"` included, makes a mount or\n * `<Activity>` reveal of this part inside a transition start a document view transition: the\n * page cross-fades for about 250 ms and the next transition commit waits for it (issue #35).\n */\n transition?: ViewTransitionClass;\n className?: StateValue<string | undefined, DialogPopupState>;\n style?: StateValue<CSSProperties | undefined, DialogPopupState>;\n children?: StateValue<ReactNode, DialogPopupState>;\n /** Must render a `<dialog>`. */\n render?: RenderProp<ComponentPropsWithRef<\"dialog\">, DialogPopupState>;\n}\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\n/** Resolves `initialFocus` against the dialog (a ref, or a selector inside the dialog). */\nfunction resolveInitialFocus(\n dialog: HTMLDialogElement,\n initialFocus: RefObject<HTMLElement | null> | string,\n): HTMLElement | null {\n if (typeof initialFocus === \"string\") return dialog.querySelector<HTMLElement>(initialFocus);\n return initialFocus.current;\n}\n\n/** The nearest `<dialog>` around a node (the dialog itself counts). */\nfunction closestDialog(target: EventTarget | null): Element | null {\n return target instanceof Element ? target.closest(\"dialog\") : null;\n}\n\n/**\n * An open popover that takes Escape before the dialog: `auto` or `hint` (they have a close\n * watcher). A `manual` popover, or `hint` where the engine maps it to `manual`, does not.\n */\nfunction isOpenLightPopover(element: Element): boolean {\n if (!element.hasAttribute(\"popover\")) return false;\n // The IDL attribute reflects the state the engine applies; jsdom has none.\n const reflected: unknown = (element as HTMLElement).popover;\n const state =\n typeof reflected === \"string\" ? reflected : element.getAttribute(\"popover\")!.toLowerCase();\n if (state === \"manual\") return false;\n try {\n return element.matches(\":popover-open\");\n } catch {\n return false;\n }\n}\n\n/**\n * Whether Escape belongs to a layer inside `dialog` that sits above it (design 9: \"closes the\n * innermost one only\"): the target is inside an open nested `<dialog>` or popover, or an open\n * `auto` / `hint` popover inside the dialog would take the close request (close watchers are\n * per document, not per focus).\n */\nfunction escapeBelongsToInnerLayer(dialog: HTMLDialogElement, target: EventTarget | null): boolean {\n for (\n let node = target instanceof Element ? target : null;\n node !== null && node !== dialog;\n node = node.parentElement\n ) {\n if (node instanceof HTMLDialogElement && node.open) return true;\n if (isOpenLightPopover(node)) return true;\n }\n for (const element of dialog.querySelectorAll(\"[popover]\")) {\n if (isOpenLightPopover(element)) return true;\n }\n return false;\n}\n\n/** Whether a pointer event on the dialog itself lies outside its border box (the `::backdrop`). */\nfunction onBackdrop(dialog: HTMLDialogElement, event: Event): boolean {\n if (event.target !== dialog) return false;\n const { clientX, clientY } = event as MouseEvent;\n const rect = dialog.getBoundingClientRect();\n return !(\n rect.top <= clientY &&\n clientY <= rect.bottom &&\n rect.left <= clientX &&\n clientX <= rect.right\n );\n}\n\n/** The submitter's or invoker's `value` (`\"\"` without one, or where the engine gives no source). */\nfunction valueOf(element: unknown): string {\n if (element instanceof HTMLButtonElement || element instanceof HTMLInputElement) {\n return element.value;\n }\n return \"\";\n}\n\n/**\n * The dialog itself: a `<dialog>` with the id the Trigger's `commandfor` names, wired to Title\n * and Description through `aria-labelledby` / `aria-describedby`. Never renders `open`: modal\n * opening and closing go through `showModal()` / `close()` only. Style it with `:open` and\n * `:modal`; it emits `data-modal` but no `data-open`.\n *\n * In library mode the children render from committed state, so content that suspends keeps the\n * dialog closed (and the Trigger pending) until it is ready; `showModal()` runs in the layout\n * effect of the commit that mounted them. A close a part asks for (Close, a `method=\"dialog\"`\n * submit, a Form's settle close, a foreign close command) is cancelled and goes through the\n * Root: `openChangeAction(false)` is awaited and `close(returnValue)` runs in the layout effect\n * of the commit that ends it, inside the view transition if `transition` is set. A dismissal\n * (Escape, a click outside) is the browser's: the native `close` event becomes\n * `openChangeAction(false, { reason: \"dismiss\", returnValue })`.\n *\n * Owned keys (conventions 1.2): `id`, `open`, `closedby`, `aria-labelledby` and\n * `aria-describedby` (token-merged), `data-modal`, `suppressHydrationWarning`.\n */\nexport function DialogPopup({\n keepMounted,\n initialFocus,\n lightDismiss = false,\n experimental_closedBy = false,\n transition,\n className,\n style,\n children,\n render,\n ...rest\n}: DialogPopupProps) {\n const ctx = useDialogContext(\"Dialog.Popup\");\n const isLibrary = ctx.mode === \"library\";\n const { modal, ids, dialogRef, triggerRef } = ctx;\n const [dialog, setDialog] = useState<HTMLDialogElement | null>(null);\n // Set by the synchronous `beforetoggle` of a close, cleared by the `close` event a task later:\n // the dialog is closed but its notification is not dispatched yet, so the apply effect must\n // not re-open it from a commit that lands in between.\n const awaitingClose = useRef(false);\n // Set right before the library's own `close()` (the apply effect), consumed by the `close`\n // event it causes: the echo is recognised by its source, not by what is intended (review L4).\n const libraryClose = useRef(false);\n // A dismissal (Escape, a click outside) that closed the dialog while a part-initiated close\n // was in flight (review M1). Its notification was skipped because `false` was intended; if that\n // close then settles with committed `true` (a controlled parent vetoed it), the apply effect\n // sends the dismissal instead of re-opening. Valid only for the open generation it was\n // recorded in: a re-open (Trigger, foreign invoker) supersedes it.\n const dismissedWhilePending = useRef<{\n readonly event: Event;\n readonly returnValue: string;\n readonly generation: number;\n } | null>(null);\n const warnedFocus = useRef(false);\n\n // --- `held`: children stay mounted while the close animates (design 4.4) -----------------\n\n const exit = useExitAnimation();\n\n // A library-driven close commits `false` first and closes in the layout effect, so without\n // `transition` the children are held through the close commit (\"previous value in state\").\n // With `transition` the close commit unmounts them inside the view transition, whose old\n // snapshot carries the content out. (A dismissal's notification commit sets it too; the apply\n // effect's release rule clears that one.)\n const [prevCommitted, setPrevCommitted] = useState(ctx.committed);\n if (prevCommitted !== ctx.committed) {\n setPrevCommitted(ctx.committed);\n if (isLibrary && prevCommitted && transition === undefined) exit.holdDuringRender();\n }\n\n // Whether the content unmounted while holding focus since the dialog was last shown: a close\n // with `transition` removes the focused Close part before `close()` runs (design 6.2 rule 2).\n const contentProbe = useContentFocusProbe(dialogRef);\n const { wrapContent } = contentProbe;\n\n // Toast's Viewport moves its region into the topmost open modal (design/components/toast.md 4.3).\n useModalRegistration(dialog);\n\n useDevWarning(\n !isLibrary && transition !== undefined,\n \"Dialog.Popup: `transition` has no effect in JS-free mode; the browser opens and closes the \" +\n \"dialog outside any React transition. Animate it with CSS (`@starting-style`).\",\n );\n useDevWarning(\n !isLibrary && keepMounted === false,\n \"Dialog.Popup: `keepMounted={false}` cannot be honoured in JS-free mode; the content is \" +\n \"always mounted so that the server HTML is complete.\",\n );\n\n const libraryRef = useCallback(\n (element: HTMLDialogElement) => {\n if (isDev && element.tagName !== \"DIALOG\") {\n console.error(\n `Dialog.Popup: expected to render a <dialog> element but got <${element.tagName.toLowerCase()}>. ` +\n \"Top layer, backdrop, focus and Escape all come from a real <dialog>.\",\n );\n }\n dialogRef.current = element;\n setDialog(element);\n return () => {\n if (dialogRef.current === element) dialogRef.current = null;\n setDialog(null);\n };\n },\n [dialogRef],\n );\n\n // --- Opening -----------------------------------------------------------------------------\n\n const openDialog = (element: HTMLDialogElement): void => {\n let target: HTMLElement | null = null;\n if (initialFocus !== undefined) {\n target = resolveInitialFocus(element, initialFocus);\n if (target === null || !element.contains(target)) {\n if (isDev && !warnedFocus.current) {\n warnedFocus.current = true;\n console.error(\n target === null\n ? \"Dialog.Popup: `initialFocus` resolved to no element; the platform picks the focus target.\"\n : \"Dialog.Popup: `initialFocus` is outside the dialog; the platform picks the focus target.\",\n );\n }\n target = null;\n }\n }\n // The platform's dialog focusing steps honour `autofocus`; React drops `autoFocus` on\n // client-rendered content (react/react#23301). Never `.focus()` (design 6.1).\n const added = target !== null && !target.hasAttribute(\"autofocus\");\n if (added) target!.setAttribute(\"autofocus\", \"\");\n try {\n if (modal) element.showModal();\n else element.show();\n } finally {\n if (added) target!.removeAttribute(\"autofocus\");\n }\n };\n\n // Mount reconciliation (design 2.4): a click before hydration opened the dialog natively.\n // Runs before the apply effect; the dispatch makes the apply effect wait for it.\n const reconcileOnMount = useEffectEvent(() => {\n const element = dialogRef.current;\n if (element === null || !element.open) return;\n ctx.requestOpen(undefined, \"platform\");\n });\n useLayoutEffect(() => {\n if (isLibrary) reconcileOnMount();\n }, [isLibrary]);\n\n /**\n * Design 6.2 rule 2, after a close whose commit unmounted the focused content: when focus is\n * now on `<body>` or still inside the closed dialog, focus the Trigger (if connected), else\n * blur what holds focus. A modal dialog never needs it (the platform restores focus on\n * `close()` unconditionally); a non-modal one restores only when focus is inside.\n */\n const returnFocus = (element: HTMLDialogElement): void => {\n const doc = element.ownerDocument;\n const active = doc.activeElement;\n if (active !== null && active !== doc.body && !element.contains(active)) return;\n const trigger = triggerRef.current;\n if (trigger !== null && trigger.isConnected) trigger.focus();\n else if (active instanceof HTMLElement && active !== doc.body) active.blur();\n };\n\n // The apply effect (design 2.5): after every commit that re-renders the Popup, once nothing\n // is in flight, the DOM follows committed state. The Root's context is memoised (#48), so an\n // unrelated Root render does not re-render the Popup and does not run this; every path that\n // makes the DOM diverge from state goes through a dispatch, whose `pending` flips re-render the\n // Popup (review of #48, finding 4). Do not rely on an unrelated render to re-apply state.\n // Opens on the user's dispatch, `defaultOpen`, a controlled parent, the re-open of a refused\n // dismissal; closes after a part-initiated close settled, a controlled close, or a refused\n // pre-hydration open. Also returns focus after a close whose commit unmounted the focused\n // content, and releases a `held` no exit animation is going to release (4.4).\n useLayoutEffect(() => {\n if (!isLibrary) return;\n const element = dialogRef.current;\n if (element === null || !element.isConnected) return;\n if (!ctx.pending && ctx.latestInFlight() === null) {\n const dismissed = dismissedWhilePending.current;\n if (ctx.committed && !element.open) {\n // Closed, but its `close` event has not arrived yet: wait for it.\n if (!awaitingClose.current) {\n dismissedWhilePending.current = null;\n if (dismissed !== null && dismissed.generation === ctx.openGeneration()) {\n // The user dismissed the dialog while a part's close was in flight, and that close\n // was vetoed: the dismissal stands and is notified now (review M1), instead of the\n // dialog coming back. A parent that keeps `open` again gets it re-opened, as for any\n // refused dismissal.\n ctx.notifyClosed(dismissed.event, \"dismiss\", dismissed.returnValue);\n } else {\n openDialog(element);\n }\n }\n } else if (!ctx.committed && element.open) {\n // The library's own close: the `close` listener recognises it by `libraryClose` and\n // dispatches nothing. `close()`, not `requestClose()`: the veto point was\n // `openChangeAction`, not `cancel`. The part's `returnValue` (design 2.3), `\"\"` for a\n // controlled close.\n ctx.recordCloseIntent(undefined);\n libraryClose.current = true;\n element.close(ctx.takeCloseReturnValue());\n // The close commit may have unmounted the focused content before `close()`: a\n // non-modal dialog restores focus only from inside, so do it now.\n if (contentProbe.hadFocus()) {\n contentProbe.forget();\n returnFocus(element);\n }\n }\n }\n // The in-flight close settled as a close: the recorded dismissal is moot.\n if (!ctx.committed && dismissedWhilePending.current !== null && ctx.latestInFlight() === null) {\n dismissedWhilePending.current = null;\n }\n // Content that unmounted while the dialog stays open (not a close): forget it.\n if (ctx.committed && element.open) contentProbe.forget();\n // A hold the close commit set (or a dismissal's notification commit) that no exit animation\n // is going to release: the dialog is closed and its `close` event has been handled.\n if (exit.held && !ctx.committed && !exit.waiting() && !awaitingClose.current && !element.open) {\n exit.release();\n }\n });\n\n // JS-free mode: there is no \"right before showModal()\" (the browser runs the command), so the\n // `autofocus` attribute stays on the target while it is mounted.\n useEffect(() => {\n if (isLibrary || initialFocus === undefined || dialog === null) return;\n const target = resolveInitialFocus(dialog, initialFocus);\n if (target === null || !dialog.contains(target) || target.hasAttribute(\"autofocus\")) return;\n target.setAttribute(\"autofocus\", \"\");\n return () => target.removeAttribute(\"autofocus\");\n }, [isLibrary, initialFocus, dialog]);\n\n // --- Native listeners (conventions 7.4) ---------------------------------------------------\n\n const onCommand = useEffectEvent((event: Event) => {\n if (event.target !== dialogRef.current || event.defaultPrevented) return;\n const command = String((event as Event & { command?: string }).command ?? \"\").toLowerCase();\n if (command === \"show-modal\") {\n // A foreign invoker (design 2.2): opening goes through the library in library mode.\n if (!isLibrary) return;\n event.preventDefault();\n ctx.requestOpen(event, \"activate\");\n } else if (command === \"request-close\" || command === \"close\") {\n if (isLibrary) {\n // A foreign close invoker (design 2.3) goes through the library like a foreign\n // `show-modal`: same markup as Dialog.Close, same behaviour. `source` is the invoker\n // (`\"\"` where an engine gives none).\n event.preventDefault();\n const source = (event as Event & { source?: unknown }).source;\n ctx.requestClose(event, \"activate\", valueOf(source));\n } else {\n ctx.recordCloseIntent({ reason: \"activate\", event });\n }\n }\n });\n\n // On the dialog's root node (the document, or a shadow root: `submit` is not composed), bubble\n // phase: after React's root listener, so a consumer's `onSubmit` has run (design 8.1).\n const onSubmit = useEffectEvent((event: Event) => {\n const element = dialogRef.current;\n const form = event.target;\n if (element === null || !(form instanceof HTMLFormElement)) return;\n if (form.closest(\"dialog\") !== element) return;\n const submitter = (event as SubmitEvent).submitter;\n const formMethod = submitter?.getAttribute(\"formmethod\");\n const method = formMethod != null ? formMethod.toLowerCase() : form.method;\n if (method !== \"dialog\") return;\n if (!isLibrary) {\n ctx.recordCloseIntent({ reason: \"activate\", event });\n return;\n }\n // The consumer blocked the submission: the form did not submit, the dialog stays open.\n if (event.defaultPrevented) return;\n // The consumer opted out with `stopPropagation()`: the browser closes the dialog, notified\n // as a dismissal. Checked explicitly because React's root can be this same node (FUNSTACK\n // Static hydrates the document), where `stopPropagation()` does not stop this listener.\n if (event.cancelBubble) return;\n // Cancel the native close; the library closes once `openChangeAction(false)` settled.\n event.preventDefault();\n ctx.requestClose(event, \"activate\", valueOf(submitter));\n });\n\n const onClose = useEffectEvent((event: Event) => {\n const element = dialogRef.current;\n if (element === null || event.target !== element) return;\n awaitingClose.current = false;\n\n // Focus return is the platform's; it leaves focus inside the hidden dialog only when the\n // opener is gone (design 6.2).\n const doc = element.ownerDocument;\n const active = doc.activeElement;\n if (active !== null && active !== doc.body && element.contains(active)) {\n const trigger = triggerRef.current;\n if (trigger !== null && trigger.isConnected) trigger.focus();\n else if (active instanceof HTMLElement) active.blur();\n }\n\n const intent = ctx.takeCloseIntent();\n const echo = libraryClose.current;\n libraryClose.current = false;\n if (!isLibrary) return;\n\n // The library's own `close()` needs no notification, and with `transition` no hold either:\n // the close commit's view transition carried the content out (4.4).\n if (!echo || transition === undefined) {\n const animations = exitAnimations(element);\n if (animations.length > 0) {\n // Before the notification, so its commit keeps the children (urgent, internal).\n exit.holdUntil(animations);\n } else {\n // Nothing animates (Firefox, or no CSS exit): release a hold now; the commit is\n // invisible, the dialog is already `display: none`.\n exit.release();\n }\n }\n\n if (echo) return;\n // Closed by the browser: a returnValue a pending part close recorded will never be used.\n ctx.takeCloseReturnValue();\n if (ctx.intended()) {\n ctx.notifyClosed(intent?.event ?? event, intent?.reason ?? \"dismiss\", element.returnValue);\n } else {\n // A part's close is in flight (walkthrough 11.13): nothing more to notify unless that close\n // is vetoed (the apply effect).\n dismissedWhilePending.current = {\n event: intent?.event ?? event,\n returnValue: element.returnValue,\n generation: ctx.openGeneration(),\n };\n }\n });\n\n const onBeforeToggle = useEffectEvent((event: Event) => {\n if (event.target !== dialogRef.current) return;\n const newState = (event as ToggleEvent).newState;\n awaitingClose.current = newState === \"closed\";\n // Every native open counts as a new opening (design 3.4, as Popover 14.1): foreign,\n // pre-hydration and JS-free opens too, so a Close action from an earlier opening skips its\n // close. Counted here, synchronously inside `showModal()` / `show()`, not at the queued\n // `toggle`: an action settling between the open and that task would otherwise still see the\n // old generation and close the new open.\n if (newState === \"open\") {\n dismissedWhilePending.current = null;\n ctx.nextOpenGeneration();\n contentProbe.forget();\n }\n });\n\n useEffect(() => {\n if (dialog === null) return;\n const doc = dialog.ownerDocument;\n // The document, or the shadow root the dialog lives in (`submit` does not cross it).\n const root = dialog.getRootNode();\n // Feature detection only with the flag on, inside an effect (conventions 8).\n const nativeClosedBy = experimental_closedBy && \"closedBy\" in dialog;\n\n const listeners: [EventTarget, string, EventListener, boolean][] = [\n [dialog, \"command\", onCommand, false],\n // Bubble phase on the root node: after React's root listener (the consumer's `onSubmit`).\n [root, \"submit\", onSubmit, false],\n [dialog, \"close\", onClose, false],\n [dialog, \"beforetoggle\", onBeforeToggle, false],\n ];\n\n if (!modal && !nativeClosedBy) {\n // Non-modal dialogs get no close watcher Escape from the platform (design 2.6).\n listeners.push([\n dialog,\n \"keydown\",\n (event) => {\n const keyboardEvent = event as KeyboardEvent;\n if (keyboardEvent.key !== \"Escape\" || keyboardEvent.defaultPrevented) return;\n if (!dialog.open || closestDialog(keyboardEvent.target) !== dialog) return;\n // A popover or nested dialog inside takes this Escape (its close watcher); leave the\n // event alone so that it can.\n if (escapeBelongsToInnerLayer(dialog, keyboardEvent.target)) return;\n keyboardEvent.preventDefault();\n dialog.requestClose();\n },\n false,\n ]);\n }\n\n // Light dismiss needs both the press and the click outside, like native `closedby=\"any\"`: a\n // drag that starts inside (selecting text in an input) and ends on the backdrop or outside\n // fires `click` on the common ancestor and must not close (it would lose a half-filled form).\n let pressedOutside = false;\n\n const isOutside = (target: EventTarget | null): boolean => {\n if (!(target instanceof Node)) return true;\n if (dialog.contains(target)) return false;\n // The Trigger re-opening a non-modal dialog is not a click outside.\n return !(triggerRef.current?.contains(target) ?? false);\n };\n let outsideAttached = false;\n const onOutsidePointerDown = (event: Event) => {\n pressedOutside = dialog.open && isOutside(event.target);\n };\n const onOutsideClick = (event: Event) => {\n const pressed = pressedOutside;\n pressedOutside = false;\n if (!dialog.open || !pressed || !isOutside(event.target)) return;\n dialog.requestClose();\n };\n const syncOutside = () => {\n const want = dialog.open;\n if (want === outsideAttached) return;\n outsideAttached = want;\n pressedOutside = false;\n if (want) {\n doc.addEventListener(\"pointerdown\", onOutsidePointerDown, true);\n doc.addEventListener(\"click\", onOutsideClick, true);\n } else {\n doc.removeEventListener(\"pointerdown\", onOutsidePointerDown, true);\n doc.removeEventListener(\"click\", onOutsideClick, true);\n }\n };\n\n if (lightDismiss && !nativeClosedBy) {\n if (modal) {\n // A press or click on the ::backdrop targets the dialog itself, outside its border box.\n listeners.push(\n [\n dialog,\n \"pointerdown\",\n (event) => {\n pressedOutside = dialog.open && onBackdrop(dialog, event);\n },\n false,\n ],\n [\n dialog,\n \"click\",\n (event) => {\n const pressed = pressedOutside;\n pressedOutside = false;\n if (!dialog.open || !pressed || !onBackdrop(dialog, event)) return;\n dialog.requestClose();\n },\n false,\n ],\n );\n } else {\n // A document listener only while open, following the native `toggle` (the React\n // `onToggle` prop never fires on a hydrated <dialog>); reads `dialog.open`, not\n // `newState` (coalescing).\n listeners.push([dialog, \"toggle\", syncOutside, false]);\n syncOutside();\n }\n }\n\n for (const [target, type, listener, capture] of listeners) {\n target.addEventListener(type, listener, capture);\n }\n return () => {\n for (const [target, type, listener, capture] of listeners) {\n target.removeEventListener(type, listener, capture);\n }\n if (outsideAttached) {\n doc.removeEventListener(\"pointerdown\", onOutsidePointerDown, true);\n doc.removeEventListener(\"click\", onOutsideClick, true);\n }\n };\n }, [dialog, modal, lightDismiss, experimental_closedBy, triggerRef]);\n\n // Development check: the dialog has an accessible name when it is (or can be) shown.\n const shown = !isLibrary || ctx.committed;\n useEffect(() => {\n if (!isDev || dialog === null || !shown) return;\n if (dialog.hasAttribute(\"aria-label\")) return;\n const labelledBy = (dialog.getAttribute(\"aria-labelledby\") ?? \"\").split(/\\s+/);\n const doc = dialog.ownerDocument;\n if (labelledBy.some((id) => id !== \"\" && doc.getElementById(id) !== null)) return;\n console.error(\n \"Dialog.Popup: the dialog has no accessible name. Render a Dialog.Title, or give the \" +\n \"Popup an `aria-label`.\",\n );\n }, [dialog, shown]);\n\n const state: DialogPopupState = { open: ctx.committed, modal };\n const visible = ctx.committed || exit.held;\n\n const content = (s: DialogPopupState): ReactNode => {\n if (!isLibrary) return resolveStateValue(children, s);\n if (keepMounted === true) {\n // The mode changes with committed state, inside the Root's transition (conventions 5.5);\n // it stays visible while the close animates (a hidden Activity is `display: none`).\n return (\n <Activity mode={visible ? \"visible\" : \"hidden\"}>\n {wrapContent(resolveStateValue(children, s))}\n </Activity>\n );\n }\n return visible ? wrapContent(resolveStateValue(children, s)) : null;\n };\n\n const libraryProps: ComponentPropsWithRef<\"dialog\"> & DataAttributes = {\n id: ids.dialog,\n // Never rendered: removing `open` by hand hides a modal dialog without `close` and leaves\n // `:modal` true (research). Owned so a `render` element cannot add it.\n open: undefined,\n closedby: experimental_closedBy ? (lightDismiss ? \"any\" : \"closerequest\") : undefined,\n \"aria-labelledby\": ids.title,\n \"aria-describedby\": ids.description,\n \"data-modal\": dataAttr(modal),\n // A pre-hydration open leaves an `open` attribute the server did not render (design 2.4).\n suppressHydrationWarning: true,\n };\n\n const element = useRender<ComponentPropsWithRef<\"dialog\">, DialogPopupState>({\n defaultElement: \"dialog\",\n render,\n state,\n className,\n style,\n children: content,\n props: rest,\n libraryProps,\n ref: libraryRef,\n });\n\n return useTransitionBoundary(\"Dialog.Popup\", isLibrary ? transition : undefined, element);\n}\n\nDialogPopup.displayName = \"DialogPopup\";\n"],"mappings":"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"}
|
|
@@ -36,14 +36,18 @@ export interface DialogRootProps {
|
|
|
36
36
|
*/
|
|
37
37
|
defaultOpen?: boolean;
|
|
38
38
|
/**
|
|
39
|
-
* Awaited for `true` (the dialog opens when it resolves and the content has rendered)
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
* Awaited for `true` (the dialog opens when it resolves and the content has rendered) and for
|
|
40
|
+
* a `false` the user asked a part for (`Dialog.Close`, a `Dialog.Form` submit or settled
|
|
41
|
+
* action, a foreign close command: the dialog closes with `details.returnValue` when it
|
|
42
|
+
* resolves, with a view transition if the Popup's `transition` is set; a controlled parent
|
|
43
|
+
* that keeps `open` vetoes it). A notification for a dismissal (Escape, Android back, a click
|
|
44
|
+
* outside: the dialog is already closed, `returnValue` is `""`; the action runs inside the
|
|
45
|
+
* Root's transition, and a controlled parent that keeps `open` gets it re-opened, which is
|
|
46
|
+
* not a veto). A dismissal while a part's close is in flight is not notified on its own: if that
|
|
47
|
+
* close is vetoed, the dismissal is notified (`"dismiss"`) when the veto settles, and the dialog
|
|
48
|
+
* stays closed. May be a Server Function. Rejections reach the nearest Error Boundary above
|
|
42
49
|
* Root.
|
|
43
50
|
*
|
|
44
|
-
* A controlled parent that does not adopt `false` gets the dialog re-opened once the action
|
|
45
|
-
* settles: closing is the browser's, this is not a veto.
|
|
46
|
-
*
|
|
47
51
|
* A `setState` placed after your own `await` is not a Transition update; wrap it in
|
|
48
52
|
* `startTransition` (React limitation).
|
|
49
53
|
*/
|
|
@@ -59,8 +63,10 @@ export interface DialogRootProps {
|
|
|
59
63
|
* Without `open`, `defaultOpen` and `openChangeAction` the Dialog runs in **JS-free mode**: the
|
|
60
64
|
* Trigger is a native `command="show-modal"` invoker and the browser owns everything. With any of
|
|
61
65
|
* them it runs in **library mode**: opening is a transition (the Trigger shows `data-pending`,
|
|
62
|
-
* content renders from committed state, `showModal()` runs when it has committed)
|
|
63
|
-
*
|
|
66
|
+
* content renders from committed state, `showModal()` runs when it has committed), and so is a
|
|
67
|
+
* close a part asks for (Close, Form, a foreign close command: `openChangeAction(false)` is
|
|
68
|
+
* awaited, `close(returnValue)` runs when it has committed). Dismissals (Escape, a click
|
|
69
|
+
* outside) are the browser's and `openChangeAction(false)` is a notification.
|
|
64
70
|
*/
|
|
65
71
|
export declare function DialogRoot({ open, defaultOpen, openChangeAction, modal, children }: DialogRootProps): import("react").JSX.Element;
|
|
66
72
|
export declare namespace DialogRoot {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dialog-root.d.mts","names":[],"sources":["../../src/dialog/dialog-root.tsx"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"dialog-root.d.mts","names":[],"sources":["../../src/dialog/dialog-root.tsx"],"mappings":";;;;iBAciB,gCAAgC;;;;;;;;WAQtC,QAAQ;;;;;WAKR;;iBAGM;;WAEN;;WAEA;WACA;;iBAGM;;;;;EAKf;;;;;EAKA;;;;;;;;;;;;;;;;;EAiBA,mBAAmB,sBAAsB;;EAEzC;EACA,WAAW,WAAW,WAAW;;;;;;;;;;;;;;wBA8BnB,aACd,MACA,aACA,kBACA,OACA,YACC,kCAAe,IAAA;yBANF"}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { createActionDetails } from "../internal/action-details.mjs";
|
|
3
3
|
import { transitionTypes } from "../internal/transition-types.mjs";
|
|
4
|
+
import { useLatest } from "../internal/use-latest.mjs";
|
|
4
5
|
import { useControllableAction } from "../internal/use-controllable-action.mjs";
|
|
5
6
|
import { resolveStateValue } from "../internal/use-render.mjs";
|
|
6
7
|
import { useDevWarning } from "../internal/use-dev-warning.mjs";
|
|
7
8
|
import { DialogContext } from "./dialog-context.mjs";
|
|
8
|
-
import { useCallback, useId, useRef, useState } from "react";
|
|
9
|
+
import { useCallback, useId, useInsertionEffect, useMemo, useRef, useState } from "react";
|
|
9
10
|
import { jsx } from "react/jsx-runtime";
|
|
10
11
|
//#region src/dialog/dialog-root.tsx
|
|
11
12
|
/** How long a recorded close intent survives when the dialog did not close (design 2.3). */
|
|
@@ -21,8 +22,10 @@ function clearLater(intentRef, intent, dialogRef) {
|
|
|
21
22
|
* Without `open`, `defaultOpen` and `openChangeAction` the Dialog runs in **JS-free mode**: the
|
|
22
23
|
* Trigger is a native `command="show-modal"` invoker and the browser owns everything. With any of
|
|
23
24
|
* them it runs in **library mode**: opening is a transition (the Trigger shows `data-pending`,
|
|
24
|
-
* content renders from committed state, `showModal()` runs when it has committed)
|
|
25
|
-
*
|
|
25
|
+
* content renders from committed state, `showModal()` runs when it has committed), and so is a
|
|
26
|
+
* close a part asks for (Close, Form, a foreign close command: `openChangeAction(false)` is
|
|
27
|
+
* awaited, `close(returnValue)` runs when it has committed). Dismissals (Escape, a click
|
|
28
|
+
* outside) are the browser's and `openChangeAction(false)` is a notification.
|
|
26
29
|
*/
|
|
27
30
|
function DialogRoot({ open, defaultOpen, openChangeAction, modal = true, children }) {
|
|
28
31
|
const isLibraryNow = open !== void 0 || defaultOpen !== void 0 || openChangeAction !== void 0;
|
|
@@ -58,25 +61,57 @@ function DialogRoot({ open, defaultOpen, openChangeAction, modal = true, childre
|
|
|
58
61
|
const nextOpenGeneration = useCallback(() => {
|
|
59
62
|
generation.current++;
|
|
60
63
|
}, []);
|
|
61
|
-
const
|
|
62
|
-
const
|
|
64
|
+
const committedOptimistic = useLatest(optimistic);
|
|
65
|
+
const intended = useCallback(() => latestInFlight()?.next ?? committedOptimistic.current, [latestInFlight, committedOptimistic]);
|
|
66
|
+
const closeReturnValue = useRef(void 0);
|
|
67
|
+
useInsertionEffect(() => {
|
|
68
|
+
if (latestInFlight()?.next ?? optimistic) closeReturnValue.current = void 0;
|
|
69
|
+
});
|
|
70
|
+
const takeCloseReturnValue = useCallback(() => {
|
|
71
|
+
const value = closeReturnValue.current;
|
|
72
|
+
closeReturnValue.current = void 0;
|
|
73
|
+
return value ?? "";
|
|
74
|
+
}, []);
|
|
75
|
+
const requestOpen = useCallback((event, reason) => {
|
|
63
76
|
if (!isLibrary || intended()) return;
|
|
64
77
|
nextOpenGeneration();
|
|
65
78
|
dispatch(true, createActionDetails(event, {
|
|
66
79
|
reason,
|
|
67
80
|
returnValue: void 0
|
|
68
81
|
}));
|
|
69
|
-
}
|
|
70
|
-
|
|
82
|
+
}, [
|
|
83
|
+
isLibrary,
|
|
84
|
+
intended,
|
|
85
|
+
nextOpenGeneration,
|
|
86
|
+
dispatch
|
|
87
|
+
]);
|
|
88
|
+
const requestClose = useCallback((event, reason, returnValue) => {
|
|
89
|
+
if (!isLibrary || !intended()) return;
|
|
90
|
+
closeReturnValue.current = returnValue;
|
|
91
|
+
dispatch(false, createActionDetails(event, {
|
|
92
|
+
reason,
|
|
93
|
+
returnValue
|
|
94
|
+
}));
|
|
95
|
+
}, [
|
|
96
|
+
isLibrary,
|
|
97
|
+
intended,
|
|
98
|
+
dispatch
|
|
99
|
+
]);
|
|
100
|
+
const notifyClosed = useCallback((event, reason, returnValue) => {
|
|
71
101
|
if (!isLibrary || !intended()) return;
|
|
72
102
|
dispatch(false, createActionDetails(event, {
|
|
73
103
|
reason,
|
|
74
104
|
returnValue
|
|
75
105
|
}));
|
|
76
|
-
}
|
|
106
|
+
}, [
|
|
107
|
+
isLibrary,
|
|
108
|
+
intended,
|
|
109
|
+
dispatch
|
|
110
|
+
]);
|
|
77
111
|
const openState = isLibrary && optimistic;
|
|
78
112
|
const pendingState = isLibrary && pending;
|
|
79
|
-
const
|
|
113
|
+
const committedState = isLibrary && committed;
|
|
114
|
+
const context = useMemo(() => ({
|
|
80
115
|
mode,
|
|
81
116
|
ids: {
|
|
82
117
|
dialog: id,
|
|
@@ -84,9 +119,12 @@ function DialogRoot({ open, defaultOpen, openChangeAction, modal = true, childre
|
|
|
84
119
|
description: `${id}-description`
|
|
85
120
|
},
|
|
86
121
|
modal,
|
|
87
|
-
committed:
|
|
122
|
+
committed: committedState,
|
|
88
123
|
optimistic: openState,
|
|
89
124
|
pending: pendingState,
|
|
125
|
+
intended,
|
|
126
|
+
requestClose,
|
|
127
|
+
takeCloseReturnValue,
|
|
90
128
|
requestOpen,
|
|
91
129
|
notifyClosed,
|
|
92
130
|
latestInFlight,
|
|
@@ -96,7 +134,24 @@ function DialogRoot({ open, defaultOpen, openChangeAction, modal = true, childre
|
|
|
96
134
|
takeCloseIntent,
|
|
97
135
|
openGeneration,
|
|
98
136
|
nextOpenGeneration
|
|
99
|
-
}
|
|
137
|
+
}), [
|
|
138
|
+
mode,
|
|
139
|
+
id,
|
|
140
|
+
modal,
|
|
141
|
+
committedState,
|
|
142
|
+
openState,
|
|
143
|
+
pendingState,
|
|
144
|
+
intended,
|
|
145
|
+
requestClose,
|
|
146
|
+
takeCloseReturnValue,
|
|
147
|
+
requestOpen,
|
|
148
|
+
notifyClosed,
|
|
149
|
+
latestInFlight,
|
|
150
|
+
recordCloseIntent,
|
|
151
|
+
takeCloseIntent,
|
|
152
|
+
openGeneration,
|
|
153
|
+
nextOpenGeneration
|
|
154
|
+
]);
|
|
100
155
|
return /* @__PURE__ */ jsx(DialogContext, {
|
|
101
156
|
value: context,
|
|
102
157
|
children: resolveStateValue(children, {
|