@funstack/ui 0.1.0-alpha.1 → 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-clear.d.mts +34 -0
- package/dist/combobox/combobox-clear.d.mts.map +1 -0
- package/dist/combobox/combobox-clear.mjs +58 -0
- package/dist/combobox/combobox-clear.mjs.map +1 -0
- package/dist/combobox/combobox-context.mjs +23 -0
- package/dist/combobox/combobox-context.mjs.map +1 -0
- package/dist/combobox/combobox-group.d.mts +72 -0
- package/dist/combobox/combobox-group.d.mts.map +1 -0
- package/dist/combobox/combobox-group.mjs +104 -0
- package/dist/combobox/combobox-group.mjs.map +1 -0
- package/dist/combobox/combobox-input.d.mts +42 -0
- package/dist/combobox/combobox-input.d.mts.map +1 -0
- package/dist/combobox/combobox-input.mjs +102 -0
- package/dist/combobox/combobox-input.mjs.map +1 -0
- package/dist/combobox/combobox-label.d.mts +22 -0
- package/dist/combobox/combobox-label.d.mts.map +1 -0
- package/dist/combobox/combobox-label.mjs +32 -0
- package/dist/combobox/combobox-label.mjs.map +1 -0
- package/dist/combobox/combobox-list.d.mts +37 -0
- package/dist/combobox/combobox-list.d.mts.map +1 -0
- package/dist/combobox/combobox-list.mjs +70 -0
- package/dist/combobox/combobox-list.mjs.map +1 -0
- package/dist/combobox/combobox-option.d.mts +45 -0
- package/dist/combobox/combobox-option.d.mts.map +1 -0
- package/dist/combobox/combobox-option.mjs +88 -0
- package/dist/combobox/combobox-option.mjs.map +1 -0
- package/dist/combobox/combobox-popup.d.mts +49 -0
- package/dist/combobox/combobox-popup.d.mts.map +1 -0
- package/dist/combobox/combobox-popup.mjs +86 -0
- package/dist/combobox/combobox-popup.mjs.map +1 -0
- package/dist/combobox/combobox-root.d.mts +135 -0
- package/dist/combobox/combobox-root.d.mts.map +1 -0
- package/dist/combobox/combobox-root.mjs +523 -0
- package/dist/combobox/combobox-root.mjs.map +1 -0
- package/dist/combobox/combobox-trigger.d.mts +36 -0
- package/dist/combobox/combobox-trigger.d.mts.map +1 -0
- package/dist/combobox/combobox-trigger.mjs +92 -0
- package/dist/combobox/combobox-trigger.mjs.map +1 -0
- package/dist/combobox/index.d.mts +36 -0
- package/dist/combobox/index.d.mts.map +1 -0
- package/dist/combobox/index.mjs +36 -0
- package/dist/combobox/index.mjs.map +1 -0
- 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 +46 -2
- package/dist/index.mjs +8 -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/platform-attributes.d.mts +12 -1
- package/dist/internal/platform-attributes.d.mts.map +1 -1
- package/dist/internal/types.d.mts +11 -1
- package/dist/internal/types.d.mts.map +1 -1
- package/dist/internal/use-controllable-action.mjs +35 -13
- 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 +12 -31
- package/dist/internal/use-form-reset.mjs.map +1 -1
- package/dist/internal/use-hydrated-mount.mjs +21 -0
- package/dist/internal/use-hydrated-mount.mjs.map +1 -0
- 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 +184 -0
- package/dist/internal/use-native-select-value.mjs.map +1 -0
- 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/index.d.mts +30 -0
- package/dist/select/index.d.mts.map +1 -0
- package/dist/select/index.mjs +30 -0
- package/dist/select/index.mjs.map +1 -0
- package/dist/select/select-context.mjs +13 -0
- package/dist/select/select-context.mjs.map +1 -0
- package/dist/select/select-dom.mjs +44 -0
- package/dist/select/select-dom.mjs.map +1 -0
- package/dist/select/select-group-label.d.mts +22 -0
- package/dist/select/select-group-label.d.mts.map +1 -0
- package/dist/select/select-group-label.mjs +39 -0
- package/dist/select/select-group-label.mjs.map +1 -0
- package/dist/select/select-group.d.mts +28 -0
- package/dist/select/select-group.d.mts.map +1 -0
- package/dist/select/select-group.mjs +47 -0
- package/dist/select/select-group.mjs.map +1 -0
- package/dist/select/select-option.d.mts +42 -0
- package/dist/select/select-option.d.mts.map +1 -0
- package/dist/select/select-option.mjs +65 -0
- package/dist/select/select-option.mjs.map +1 -0
- package/dist/select/select-root.d.mts +68 -0
- package/dist/select/select-root.d.mts.map +1 -0
- package/dist/select/select-root.mjs +114 -0
- package/dist/select/select-root.mjs.map +1 -0
- package/dist/select/select-separator.d.mts +20 -0
- package/dist/select/select-separator.d.mts.map +1 -0
- package/dist/select/select-separator.mjs +30 -0
- package/dist/select/select-separator.mjs.map +1 -0
- package/dist/select/select-trigger.d.mts +29 -0
- package/dist/select/select-trigger.d.mts.map +1 -0
- package/dist/select/select-trigger.mjs +48 -0
- package/dist/select/select-trigger.mjs.map +1 -0
- package/dist/select/select-value.d.mts +31 -0
- package/dist/select/select-value.d.mts.map +1 -0
- package/dist/select/select-value.mjs +101 -0
- package/dist/select/select-value.mjs.map +1 -0
- 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 +7 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { dataAttr } from "../internal/data-attr.mjs";
|
|
3
|
+
import { rendersButton, useRender } from "../internal/use-render.mjs";
|
|
4
|
+
import { useComboboxContext } from "./combobox-context.mjs";
|
|
5
|
+
import { useCallback, useRef } from "react";
|
|
6
|
+
//#region src/combobox/combobox-trigger.tsx
|
|
7
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
8
|
+
/**
|
|
9
|
+
* Development check (design 9): an icon-only Trigger needs a name from the consumer. Checked a
|
|
10
|
+
* microtask after the element attaches, once per instance.
|
|
11
|
+
*/
|
|
12
|
+
function useNameWarning(part) {
|
|
13
|
+
const warned = useRef(false);
|
|
14
|
+
return useCallback((element) => {
|
|
15
|
+
if (!isDev || warned.current) return () => {};
|
|
16
|
+
queueMicrotask(() => {
|
|
17
|
+
if (warned.current || !element.isConnected) return;
|
|
18
|
+
if (element.hasAttribute("aria-label") || element.hasAttribute("aria-labelledby")) return;
|
|
19
|
+
if ((element.textContent ?? "").trim() !== "" || element.hasAttribute("title")) return;
|
|
20
|
+
warned.current = true;
|
|
21
|
+
console.error(`${part}: the button has no accessible name. Give it an \`aria-label\`.`);
|
|
22
|
+
});
|
|
23
|
+
return () => {};
|
|
24
|
+
}, [part]);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The optional button that toggles the listbox: `<button type="button" tabindex="-1"
|
|
28
|
+
* aria-expanded aria-controls>` (the APG keeps it out of the Tab sequence; the input is the one
|
|
29
|
+
* stop). A press does not take focus from the input, and a click focuses the input. No
|
|
30
|
+
* `commandfor`: the listbox is anchored to the input, never to this button (design 1).
|
|
31
|
+
*
|
|
32
|
+
* Owned keys: `type` (on a `<button>`), `tabIndex`, `disabled`, `aria-expanded`,
|
|
33
|
+
* `aria-controls` (token-merged), `onClick`, `onPointerDown`, `onPointerCancel`, `onMouseDown`,
|
|
34
|
+
* `data-open`, `data-pending`, `data-disabled`.
|
|
35
|
+
*/
|
|
36
|
+
function ComboboxTrigger({ disabled: ownDisabled = false, className, style, children, render, ...rest }) {
|
|
37
|
+
const ctx = useComboboxContext("Combobox.Trigger");
|
|
38
|
+
const { popover } = ctx;
|
|
39
|
+
const disabled = ownDisabled || ctx.disabled;
|
|
40
|
+
const openAtPress = useRef(null);
|
|
41
|
+
const ref = useNameWarning("Combobox.Trigger");
|
|
42
|
+
const state = {
|
|
43
|
+
open: popover.optimistic,
|
|
44
|
+
pending: popover.pending,
|
|
45
|
+
disabled
|
|
46
|
+
};
|
|
47
|
+
const libraryProps = {
|
|
48
|
+
...rendersButton(render) && { type: "button" },
|
|
49
|
+
tabIndex: -1,
|
|
50
|
+
disabled,
|
|
51
|
+
"aria-expanded": popover.optimistic,
|
|
52
|
+
"aria-controls": ctx.ids.list,
|
|
53
|
+
onPointerDown: (event) => {
|
|
54
|
+
openAtPress.current = popover.intended();
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
},
|
|
57
|
+
onPointerCancel: () => {
|
|
58
|
+
openAtPress.current = null;
|
|
59
|
+
},
|
|
60
|
+
onMouseDown: (event) => {
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
},
|
|
63
|
+
onClick: (event) => {
|
|
64
|
+
const wasOpen = (event.detail === 0 ? null : openAtPress.current) ?? popover.intended();
|
|
65
|
+
openAtPress.current = null;
|
|
66
|
+
if (disabled) return;
|
|
67
|
+
const native = event.nativeEvent;
|
|
68
|
+
if (wasOpen) popover.requestClose(native, "activate");
|
|
69
|
+
else popover.requestOpen(native, "activate");
|
|
70
|
+
ctx.inputRef.current?.focus();
|
|
71
|
+
},
|
|
72
|
+
"data-open": dataAttr(popover.optimistic),
|
|
73
|
+
"data-pending": dataAttr(popover.pending),
|
|
74
|
+
"data-disabled": dataAttr(disabled)
|
|
75
|
+
};
|
|
76
|
+
return useRender({
|
|
77
|
+
defaultElement: "button",
|
|
78
|
+
render,
|
|
79
|
+
state,
|
|
80
|
+
className,
|
|
81
|
+
style,
|
|
82
|
+
children,
|
|
83
|
+
props: rest,
|
|
84
|
+
libraryProps,
|
|
85
|
+
ref
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
ComboboxTrigger.displayName = "ComboboxTrigger";
|
|
89
|
+
//#endregion
|
|
90
|
+
export { ComboboxTrigger, useNameWarning };
|
|
91
|
+
|
|
92
|
+
//# sourceMappingURL=combobox-trigger.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"combobox-trigger.mjs","names":[],"sources":["../../src/combobox/combobox-trigger.tsx"],"sourcesContent":["\"use client\";\nimport { useCallback, useRef } from \"react\";\nimport type {\n ComponentPropsWithRef,\n CSSProperties,\n MouseEvent,\n PointerEvent,\n ReactNode,\n} from \"react\";\nimport { dataAttr } from \"../internal/data-attr.js\";\nimport type { RenderProp, StateValue } from \"../internal/types.js\";\nimport { rendersButton, useRender } from \"../internal/use-render.js\";\nimport type { DataAttributes } from \"../internal/use-render.js\";\nimport { useComboboxContext } from \"./combobox-context.js\";\n\nexport interface ComboboxTriggerState {\n /** Optimistic open state. */\n readonly open: boolean;\n /** The open (or close) transition is in flight. */\n readonly pending: boolean;\n readonly disabled: boolean;\n}\n\nexport interface ComboboxTriggerProps extends Omit<\n ComponentPropsWithRef<\"button\">,\n | \"onClick\"\n | \"onPointerDown\"\n | \"onMouseDown\"\n | \"onPointerCancel\"\n | \"tabIndex\"\n | \"aria-expanded\"\n | \"aria-controls\"\n | \"disabled\"\n | \"className\"\n | \"style\"\n | \"children\"\n> {\n /** Disables this button; the Root's `disabled` does too. */\n disabled?: boolean;\n className?: StateValue<string | undefined, ComboboxTriggerState>;\n style?: StateValue<CSSProperties | undefined, ComboboxTriggerState>;\n /** Rendered from optimistic state; must not suspend. */\n children?: StateValue<ReactNode, ComboboxTriggerState>;\n /** Should render a `<button>`. */\n render?: RenderProp<ComponentPropsWithRef<\"button\">, ComboboxTriggerState>;\n}\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\n/**\n * Development check (design 9): an icon-only Trigger needs a name from the consumer. Checked a\n * microtask after the element attaches, once per instance.\n */\nexport function useNameWarning(part: string): (element: HTMLElement) => () => void {\n const warned = useRef(false);\n return useCallback(\n (element: HTMLElement) => {\n if (!isDev || warned.current) return () => {};\n queueMicrotask(() => {\n if (warned.current || !element.isConnected) return;\n if (element.hasAttribute(\"aria-label\") || element.hasAttribute(\"aria-labelledby\")) return;\n if ((element.textContent ?? \"\").trim() !== \"\" || element.hasAttribute(\"title\")) return;\n warned.current = true;\n console.error(`${part}: the button has no accessible name. Give it an \\`aria-label\\`.`);\n });\n return () => {};\n },\n [part],\n );\n}\n\n/**\n * The optional button that toggles the listbox: `<button type=\"button\" tabindex=\"-1\"\n * aria-expanded aria-controls>` (the APG keeps it out of the Tab sequence; the input is the one\n * stop). A press does not take focus from the input, and a click focuses the input. No\n * `commandfor`: the listbox is anchored to the input, never to this button (design 1).\n *\n * Owned keys: `type` (on a `<button>`), `tabIndex`, `disabled`, `aria-expanded`,\n * `aria-controls` (token-merged), `onClick`, `onPointerDown`, `onPointerCancel`, `onMouseDown`,\n * `data-open`, `data-pending`, `data-disabled`.\n */\nexport function ComboboxTrigger({\n disabled: ownDisabled = false,\n className,\n style,\n children,\n render,\n ...rest\n}: ComboboxTriggerProps) {\n const ctx = useComboboxContext(\"Combobox.Trigger\");\n const { popover } = ctx;\n const disabled = ownDisabled || ctx.disabled;\n // What the user saw at the press: a press outside the popup light-dismisses it before the\n // click arrives, so the toggle is decided against the state at `pointerdown`.\n const openAtPress = useRef<boolean | null>(null);\n const ref = useNameWarning(\"Combobox.Trigger\");\n\n const state: ComboboxTriggerState = {\n open: popover.optimistic,\n pending: popover.pending,\n disabled,\n };\n\n const libraryProps: ComponentPropsWithRef<\"button\"> & DataAttributes = {\n ...(rendersButton(render) && { type: \"button\" as const }),\n tabIndex: -1,\n disabled,\n \"aria-expanded\": popover.optimistic,\n \"aria-controls\": ctx.ids.list,\n onPointerDown: (event: PointerEvent<HTMLButtonElement>) => {\n openAtPress.current = popover.intended();\n event.preventDefault();\n },\n onPointerCancel: () => {\n // The press never becomes a click.\n openAtPress.current = null;\n },\n onMouseDown: (event: MouseEvent<HTMLButtonElement>) => {\n event.preventDefault();\n },\n onClick: (event: MouseEvent<HTMLButtonElement>) => {\n // `detail === 0`: a click without a press (assistive technology, `element.click()`); a\n // snapshot left by a press that never clicked (dragged off) does not belong to it.\n const pressed = event.detail === 0 ? null : openAtPress.current;\n const wasOpen = pressed ?? popover.intended();\n openAtPress.current = null;\n if (disabled) return;\n const native = event.nativeEvent;\n if (wasOpen) popover.requestClose(native, \"activate\");\n else popover.requestOpen(native, \"activate\");\n ctx.inputRef.current?.focus();\n },\n \"data-open\": dataAttr(popover.optimistic),\n \"data-pending\": dataAttr(popover.pending),\n \"data-disabled\": dataAttr(disabled),\n };\n\n return useRender<ComponentPropsWithRef<\"button\">, ComboboxTriggerState>({\n defaultElement: \"button\",\n render,\n state,\n className,\n style,\n children,\n props: rest,\n libraryProps,\n ref,\n });\n}\n\nComboboxTrigger.displayName = \"ComboboxTrigger\";\n"],"mappings":";;;;;;AA+CA,MAAM,QAAQ,QAAQ,IAAI,aAAa;;;;;AAMvC,SAAgB,eAAe,MAAoD;CACjF,MAAM,SAAS,OAAO,KAAK;CAC3B,OAAO,aACJ,YAAyB;EACxB,IAAI,CAAC,SAAS,OAAO,SAAS,aAAa,CAAC;EAC5C,qBAAqB;GACnB,IAAI,OAAO,WAAW,CAAC,QAAQ,aAAa;GAC5C,IAAI,QAAQ,aAAa,YAAY,KAAK,QAAQ,aAAa,iBAAiB,GAAG;GACnF,KAAK,QAAQ,eAAe,GAAA,CAAI,KAAK,MAAM,MAAM,QAAQ,aAAa,OAAO,GAAG;GAChF,OAAO,UAAU;GACjB,QAAQ,MAAM,GAAG,KAAK,gEAAgE;EACxF,CAAC;EACD,aAAa,CAAC;CAChB,GACA,CAAC,IAAI,CACP;AACF;;;;;;;;;;;AAYA,SAAgB,gBAAgB,EAC9B,UAAU,cAAc,OACxB,WACA,OACA,UACA,QACA,GAAG,QACoB;CACvB,MAAM,MAAM,mBAAmB,kBAAkB;CACjD,MAAM,EAAE,YAAY;CACpB,MAAM,WAAW,eAAe,IAAI;CAGpC,MAAM,cAAc,OAAuB,IAAI;CAC/C,MAAM,MAAM,eAAe,kBAAkB;CAE7C,MAAM,QAA8B;EAClC,MAAM,QAAQ;EACd,SAAS,QAAQ;EACjB;CACF;CAEA,MAAM,eAAiE;EACrE,GAAI,cAAc,MAAM,KAAK,EAAE,MAAM,SAAkB;EACvD,UAAU;EACV;EACA,iBAAiB,QAAQ;EACzB,iBAAiB,IAAI,IAAI;EACzB,gBAAgB,UAA2C;GACzD,YAAY,UAAU,QAAQ,SAAS;GACvC,MAAM,eAAe;EACvB;EACA,uBAAuB;GAErB,YAAY,UAAU;EACxB;EACA,cAAc,UAAyC;GACrD,MAAM,eAAe;EACvB;EACA,UAAU,UAAyC;GAIjD,MAAM,WADU,MAAM,WAAW,IAAI,OAAO,YAAY,YAC7B,QAAQ,SAAS;GAC5C,YAAY,UAAU;GACtB,IAAI,UAAU;GACd,MAAM,SAAS,MAAM;GACrB,IAAI,SAAS,QAAQ,aAAa,QAAQ,UAAU;QAC/C,QAAQ,YAAY,QAAQ,UAAU;GAC3C,IAAI,SAAS,SAAS,MAAM;EAC9B;EACA,aAAa,SAAS,QAAQ,UAAU;EACxC,gBAAgB,SAAS,QAAQ,OAAO;EACxC,iBAAiB,SAAS,QAAQ;CACpC;CAEA,OAAO,UAAiE;EACtE,gBAAgB;EAChB;EACA;EACA;EACA;EACA;EACA,OAAO;EACP;EACA;CACF,CAAC;AACH;AAEA,gBAAgB,cAAc"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import "../internal/platform-attributes.mjs";
|
|
2
|
+
import { ComboboxClear, ComboboxClearProps, ComboboxClearState } from "./combobox-clear.mjs";
|
|
3
|
+
import { ComboboxList, ComboboxListProps, ComboboxListState } from "./combobox-list.mjs";
|
|
4
|
+
import { ComboboxEmpty, ComboboxEmptyProps, ComboboxEmptyState, ComboboxGroup, ComboboxGroupLabel, ComboboxGroupLabelProps, ComboboxGroupLabelState, ComboboxGroupProps, ComboboxGroupState, ComboboxStatus, ComboboxStatusProps, ComboboxStatusState } from "./combobox-group.mjs";
|
|
5
|
+
import { ComboboxInput, ComboboxInputProps, ComboboxInputState } from "./combobox-input.mjs";
|
|
6
|
+
import { ComboboxLabel, ComboboxLabelProps, ComboboxLabelState } from "./combobox-label.mjs";
|
|
7
|
+
import { ComboboxOption, ComboboxOptionProps, ComboboxOptionState } from "./combobox-option.mjs";
|
|
8
|
+
import { ComboboxPopup, ComboboxPopupProps, ComboboxPopupState } from "./combobox-popup.mjs";
|
|
9
|
+
import { ComboboxInputValueChangeDetails, ComboboxOpenChangeDetails, ComboboxRoot, ComboboxRootProps, ComboboxRootState, ComboboxValueChangeDetails } from "./combobox-root.mjs";
|
|
10
|
+
import { ComboboxTrigger, ComboboxTriggerProps, ComboboxTriggerState } from "./combobox-trigger.mjs";
|
|
11
|
+
//#region src/combobox/index.d.ts
|
|
12
|
+
/**
|
|
13
|
+
* Editable combobox with list autocomplete (WAI-ARIA APG) on `popover="auto"`: `Root` (no DOM,
|
|
14
|
+
* or one hidden input with `name`) > `Label`, `Input` (`input[role=combobox]`), `Trigger`,
|
|
15
|
+
* `Clear`, `Popup` (`div[popover=auto]`) > `List` (`div[role=listbox]`) > `Option`, `Group` >
|
|
16
|
+
* `GroupLabel`; `Empty` and `Status` in the Popup. Typing is urgent (`inputValueChangeAction`,
|
|
17
|
+
* the library's urgent lane); the results render from the deferred `query`; accepting an option
|
|
18
|
+
* is a transition with `data-pending` on it, and the listbox closes when it settles.
|
|
19
|
+
*/
|
|
20
|
+
export declare const Combobox: {
|
|
21
|
+
readonly Root: typeof ComboboxRoot;
|
|
22
|
+
readonly Label: typeof ComboboxLabel;
|
|
23
|
+
readonly Input: typeof ComboboxInput;
|
|
24
|
+
readonly Trigger: typeof ComboboxTrigger;
|
|
25
|
+
readonly Clear: typeof ComboboxClear;
|
|
26
|
+
readonly Popup: typeof ComboboxPopup;
|
|
27
|
+
readonly List: typeof ComboboxList;
|
|
28
|
+
readonly Option: typeof ComboboxOption;
|
|
29
|
+
readonly Group: typeof ComboboxGroup;
|
|
30
|
+
readonly GroupLabel: typeof ComboboxGroupLabel;
|
|
31
|
+
readonly Empty: typeof ComboboxEmpty;
|
|
32
|
+
readonly Status: typeof ComboboxStatus;
|
|
33
|
+
};
|
|
34
|
+
//#endregion
|
|
35
|
+
export type { ComboboxClearProps, ComboboxClearState, ComboboxEmptyProps, ComboboxEmptyState, ComboboxGroupLabelProps, ComboboxGroupLabelState, ComboboxGroupProps, ComboboxGroupState, ComboboxInputProps, ComboboxInputState, ComboboxInputValueChangeDetails, ComboboxLabelProps, ComboboxLabelState, ComboboxListProps, ComboboxListState, ComboboxOpenChangeDetails, ComboboxOptionProps, ComboboxOptionState, ComboboxPopupProps, ComboboxPopupState, ComboboxRootProps, ComboboxRootState, ComboboxStatusProps, ComboboxStatusState, ComboboxTriggerProps, ComboboxTriggerState, ComboboxValueChangeDetails };
|
|
36
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/combobox/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;qBA0Ba;WACX,aAAI;WACJ,cAAK;WACL,cAAK;WACL,gBAAO;WACP,cAAK;WACL,cAAK;WACL,aAAI;WACJ,eAAM;WACN,cAAK;WACL,mBAAU;WACV,cAAK;WACL,eAAM"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ComboboxTrigger } from "./combobox-trigger.mjs";
|
|
2
|
+
import { ComboboxClear } from "./combobox-clear.mjs";
|
|
3
|
+
import { ComboboxEmpty, ComboboxGroup, ComboboxGroupLabel, ComboboxStatus } from "./combobox-group.mjs";
|
|
4
|
+
import { ComboboxInput } from "./combobox-input.mjs";
|
|
5
|
+
import { ComboboxLabel } from "./combobox-label.mjs";
|
|
6
|
+
import { ComboboxList } from "./combobox-list.mjs";
|
|
7
|
+
import { ComboboxOption } from "./combobox-option.mjs";
|
|
8
|
+
import { ComboboxPopup } from "./combobox-popup.mjs";
|
|
9
|
+
import { ComboboxRoot } from "./combobox-root.mjs";
|
|
10
|
+
//#region src/combobox/index.ts
|
|
11
|
+
/**
|
|
12
|
+
* Editable combobox with list autocomplete (WAI-ARIA APG) on `popover="auto"`: `Root` (no DOM,
|
|
13
|
+
* or one hidden input with `name`) > `Label`, `Input` (`input[role=combobox]`), `Trigger`,
|
|
14
|
+
* `Clear`, `Popup` (`div[popover=auto]`) > `List` (`div[role=listbox]`) > `Option`, `Group` >
|
|
15
|
+
* `GroupLabel`; `Empty` and `Status` in the Popup. Typing is urgent (`inputValueChangeAction`,
|
|
16
|
+
* the library's urgent lane); the results render from the deferred `query`; accepting an option
|
|
17
|
+
* is a transition with `data-pending` on it, and the listbox closes when it settles.
|
|
18
|
+
*/
|
|
19
|
+
const Combobox = {
|
|
20
|
+
Root: ComboboxRoot,
|
|
21
|
+
Label: ComboboxLabel,
|
|
22
|
+
Input: ComboboxInput,
|
|
23
|
+
Trigger: ComboboxTrigger,
|
|
24
|
+
Clear: ComboboxClear,
|
|
25
|
+
Popup: ComboboxPopup,
|
|
26
|
+
List: ComboboxList,
|
|
27
|
+
Option: ComboboxOption,
|
|
28
|
+
Group: ComboboxGroup,
|
|
29
|
+
GroupLabel: ComboboxGroupLabel,
|
|
30
|
+
Empty: ComboboxEmpty,
|
|
31
|
+
Status: ComboboxStatus
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { Combobox };
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/combobox/index.ts"],"sourcesContent":["// Ships the platform attribute typings with this subpath (conventions 7.4). Type-only: the built\n// JS has no import of it.\nimport \"../internal/platform-attributes.js\";\nimport { ComboboxClear } from \"./combobox-clear.js\";\nimport {\n ComboboxEmpty,\n ComboboxGroup,\n ComboboxGroupLabel,\n ComboboxStatus,\n} from \"./combobox-group.js\";\nimport { ComboboxInput } from \"./combobox-input.js\";\nimport { ComboboxLabel } from \"./combobox-label.js\";\nimport { ComboboxList } from \"./combobox-list.js\";\nimport { ComboboxOption } from \"./combobox-option.js\";\nimport { ComboboxPopup } from \"./combobox-popup.js\";\nimport { ComboboxRoot } from \"./combobox-root.js\";\nimport { ComboboxTrigger } from \"./combobox-trigger.js\";\n\n/**\n * Editable combobox with list autocomplete (WAI-ARIA APG) on `popover=\"auto\"`: `Root` (no DOM,\n * or one hidden input with `name`) > `Label`, `Input` (`input[role=combobox]`), `Trigger`,\n * `Clear`, `Popup` (`div[popover=auto]`) > `List` (`div[role=listbox]`) > `Option`, `Group` >\n * `GroupLabel`; `Empty` and `Status` in the Popup. Typing is urgent (`inputValueChangeAction`,\n * the library's urgent lane); the results render from the deferred `query`; accepting an option\n * is a transition with `data-pending` on it, and the listbox closes when it settles.\n */\nexport const Combobox = {\n Root: ComboboxRoot,\n Label: ComboboxLabel,\n Input: ComboboxInput,\n Trigger: ComboboxTrigger,\n Clear: ComboboxClear,\n Popup: ComboboxPopup,\n List: ComboboxList,\n Option: ComboboxOption,\n Group: ComboboxGroup,\n GroupLabel: ComboboxGroupLabel,\n Empty: ComboboxEmpty,\n Status: ComboboxStatus,\n} as const;\n\nexport type {\n ComboboxRootProps,\n ComboboxRootState,\n ComboboxValueChangeDetails,\n ComboboxInputValueChangeDetails,\n ComboboxOpenChangeDetails,\n} from \"./combobox-root.js\";\nexport type { ComboboxLabelProps, ComboboxLabelState } from \"./combobox-label.js\";\nexport type { ComboboxInputProps, ComboboxInputState } from \"./combobox-input.js\";\nexport type { ComboboxTriggerProps, ComboboxTriggerState } from \"./combobox-trigger.js\";\nexport type { ComboboxClearProps, ComboboxClearState } from \"./combobox-clear.js\";\nexport type { ComboboxPopupProps, ComboboxPopupState } from \"./combobox-popup.js\";\nexport type { ComboboxListProps, ComboboxListState } from \"./combobox-list.js\";\nexport type { ComboboxOptionProps, ComboboxOptionState } from \"./combobox-option.js\";\nexport type {\n ComboboxGroupProps,\n ComboboxGroupState,\n ComboboxGroupLabelProps,\n ComboboxGroupLabelState,\n ComboboxEmptyProps,\n ComboboxEmptyState,\n ComboboxStatusProps,\n ComboboxStatusState,\n} from \"./combobox-group.js\";\n"],"mappings":";;;;;;;;;;;;;;;;;;AA0BA,MAAa,WAAW;CACtB,MAAM;CACN,OAAO;CACP,OAAO;CACP,SAAS;CACT,OAAO;CACP,OAAO;CACP,MAAM;CACN,QAAQ;CACR,OAAO;CACP,YAAY;CACZ,OAAO;CACP,QAAQ;AACV"}
|
|
@@ -13,13 +13,18 @@ export interface DialogCloseDetails extends ActionDetails {
|
|
|
13
13
|
}
|
|
14
14
|
export interface DialogCloseProps extends Omit<ComponentPropsWithRef<"button">, "onClick" | "value" | "className" | "style" | "children"> {
|
|
15
15
|
/**
|
|
16
|
-
* Runs inside this part's own transition
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* Runs inside this part's own transition (`data-pending` on the button), then closes the
|
|
17
|
+
* dialog with `value` as `returnValue`: in library mode through the Root
|
|
18
|
+
* (`openChangeAction(false)` awaited, view transition if set), in JS-free mode with
|
|
19
|
+
* `close(value)`. A rejection reaches the nearest Error Boundary above the Close part and
|
|
20
|
+
* leaves the dialog open. If the dialog was dismissed and opened again while the action ran,
|
|
21
|
+
* the close is dropped: it belonged to the earlier open. Without `action` in JS-free mode the
|
|
22
|
+
* button is a native `command="request-close"` invoker.
|
|
19
23
|
*/
|
|
20
24
|
action?: Action<[details: DialogCloseDetails]>;
|
|
21
|
-
/** Becomes `dialog.returnValue` (and `details.returnValue` of
|
|
25
|
+
/** Becomes `dialog.returnValue` (and `details.returnValue` of `openChangeAction(false)`). */
|
|
22
26
|
value?: string;
|
|
27
|
+
/** Native `disabled`; owned, so a `render` element cannot re-enable the button. */
|
|
23
28
|
disabled?: boolean;
|
|
24
29
|
className?: StateValue<string | undefined, DialogCloseState>;
|
|
25
30
|
style?: StateValue<CSSProperties | undefined, DialogCloseState>;
|
|
@@ -27,14 +32,16 @@ export interface DialogCloseProps extends Omit<ComponentPropsWithRef<"button">,
|
|
|
27
32
|
render?: RenderProp<ComponentPropsWithRef<"button">, DialogCloseState>;
|
|
28
33
|
}
|
|
29
34
|
/**
|
|
30
|
-
* A close button: `<button type="button" commandfor command="request-close">`.
|
|
31
|
-
* the browser closes the dialog (a close request: `cancel`, then `close`), with
|
|
32
|
-
* `returnValue`.
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
+
* A close button: `<button type="button" commandfor command="request-close">`. In JS-free mode
|
|
36
|
+
* without `action` the browser closes the dialog (a close request: `cancel`, then `close`), with
|
|
37
|
+
* `value` as the `returnValue`. In library mode the click is always cancelled and the close goes
|
|
38
|
+
* through the Root (`openChangeAction(false)` awaited, then `close(value)` in the commit, inside
|
|
39
|
+
* the view transition if the Popup's `transition` is set). With `action` the action runs first in
|
|
40
|
+
* this part's own transition (`data-pending` on the button); a rejection leaves the dialog open.
|
|
35
41
|
*
|
|
36
42
|
* Owned keys (conventions 1.2): `type` (on a `<button>` only), `disabled`, `commandfor`,
|
|
37
|
-
* `command`, `value`, `onClick` (with `action`), `data-pending`,
|
|
43
|
+
* `command`, `value`, `onClick` (library mode, or with `action`), `data-pending`,
|
|
44
|
+
* `data-disabled`.
|
|
38
45
|
*/
|
|
39
46
|
export declare function DialogClose({ action, value, disabled, className, style, children, render, ...rest }: DialogCloseProps): ReactNode;
|
|
40
47
|
export declare namespace DialogClose {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dialog-close.d.mts","names":[],"sources":["../../src/dialog/dialog-close.tsx"],"mappings":";;;iBAWiB;;WAEN;WACA;;;iBAIM,2BAA2B;;WAEjC;;iBAGM,yBAAyB,KACxC
|
|
1
|
+
{"version":3,"file":"dialog-close.d.mts","names":[],"sources":["../../src/dialog/dialog-close.tsx"],"mappings":";;;iBAWiB;;WAEN;WACA;;;iBAIM,2BAA2B;;WAEjC;;iBAGM,yBAAyB,KACxC;;;;;;;;;;EAYA,SAAS,QAAQ,SAAS;;EAE1B;;EAEA;EACA,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;EAC9C,WAAW,WAAW,WAAW;EACjC,SAAS,WAAW,iCAAiC;;;;;;;;;;;;;;wBAiBvC,cACd,QACA,OACA,UACA,WACA,OACA,UACA,WACG,QACF,mBAAgB;yBATH"}
|
|
@@ -8,44 +8,55 @@ import { useCallback } from "react";
|
|
|
8
8
|
//#region src/dialog/dialog-close.tsx
|
|
9
9
|
const isDev = process.env.NODE_ENV !== "production";
|
|
10
10
|
/**
|
|
11
|
-
* A close button: `<button type="button" commandfor command="request-close">`.
|
|
12
|
-
* the browser closes the dialog (a close request: `cancel`, then `close`), with
|
|
13
|
-
* `returnValue`.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* A close button: `<button type="button" commandfor command="request-close">`. In JS-free mode
|
|
12
|
+
* without `action` the browser closes the dialog (a close request: `cancel`, then `close`), with
|
|
13
|
+
* `value` as the `returnValue`. In library mode the click is always cancelled and the close goes
|
|
14
|
+
* through the Root (`openChangeAction(false)` awaited, then `close(value)` in the commit, inside
|
|
15
|
+
* the view transition if the Popup's `transition` is set). With `action` the action runs first in
|
|
16
|
+
* this part's own transition (`data-pending` on the button); a rejection leaves the dialog open.
|
|
16
17
|
*
|
|
17
18
|
* Owned keys (conventions 1.2): `type` (on a `<button>` only), `disabled`, `commandfor`,
|
|
18
|
-
* `command`, `value`, `onClick` (with `action`), `data-pending`,
|
|
19
|
+
* `command`, `value`, `onClick` (library mode, or with `action`), `data-pending`,
|
|
20
|
+
* `data-disabled`.
|
|
19
21
|
*/
|
|
20
22
|
function DialogClose({ action, value, disabled = false, className, style, children, render, ...rest }) {
|
|
21
23
|
const ctx = useDialogContext("Dialog.Close");
|
|
22
24
|
const { pending, run } = useActionTrigger(ctx.openGeneration);
|
|
23
25
|
const { ids, dialogRef, recordCloseIntent } = ctx;
|
|
24
|
-
const
|
|
26
|
+
const isLibrary = ctx.mode === "library";
|
|
27
|
+
const installClick = isLibrary || action !== void 0;
|
|
25
28
|
const state = {
|
|
26
29
|
pending,
|
|
27
30
|
disabled
|
|
28
31
|
};
|
|
29
32
|
const devRef = useCallback((element) => {
|
|
30
|
-
if (isDev && !
|
|
33
|
+
if (isDev && !installClick && element.tagName !== "BUTTON") console.error(`Dialog.Close: rendered a <${element.tagName.toLowerCase()}> in JS-free mode without \`action\`. Only a <button> runs \`command="request-close"\`; this element will not close the dialog.`);
|
|
31
34
|
return () => {};
|
|
32
|
-
}, [
|
|
35
|
+
}, [installClick]);
|
|
36
|
+
const close = (event, returnValue) => {
|
|
37
|
+
if (isLibrary) {
|
|
38
|
+
ctx.requestClose(event, "activate", returnValue);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const dialog = dialogRef.current;
|
|
42
|
+
if (dialog === null || !dialog.open) return;
|
|
43
|
+
recordCloseIntent({
|
|
44
|
+
reason: "activate",
|
|
45
|
+
event
|
|
46
|
+
});
|
|
47
|
+
dialog.close(returnValue);
|
|
48
|
+
};
|
|
33
49
|
const onClick = (event) => {
|
|
34
|
-
if (action === void 0) return;
|
|
35
50
|
event.preventDefault();
|
|
36
51
|
if (disabled) return;
|
|
37
52
|
const native = event.nativeEvent;
|
|
38
53
|
const returnValue = value ?? "";
|
|
54
|
+
if (action === void 0) {
|
|
55
|
+
close(native, returnValue);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
39
58
|
const details = createActionDetails(native, { returnValue });
|
|
40
|
-
run(() => action(details), () =>
|
|
41
|
-
const dialog = dialogRef.current;
|
|
42
|
-
if (dialog === null || !dialog.open) return;
|
|
43
|
-
recordCloseIntent({
|
|
44
|
-
reason: "activate",
|
|
45
|
-
event: native
|
|
46
|
-
});
|
|
47
|
-
dialog.close(returnValue);
|
|
48
|
-
});
|
|
59
|
+
run(() => action(details), () => close(native, returnValue));
|
|
49
60
|
};
|
|
50
61
|
const libraryProps = {
|
|
51
62
|
...rendersButton(render) && { type: "button" },
|
|
@@ -53,7 +64,7 @@ function DialogClose({ action, value, disabled = false, className, style, childr
|
|
|
53
64
|
commandfor: ids.dialog,
|
|
54
65
|
command: "request-close",
|
|
55
66
|
value,
|
|
56
|
-
...
|
|
67
|
+
...installClick && { onClick },
|
|
57
68
|
"data-pending": dataAttr(pending),
|
|
58
69
|
"data-disabled": dataAttr(disabled)
|
|
59
70
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dialog-close.mjs","names":[],"sources":["../../src/dialog/dialog-close.tsx"],"sourcesContent":["\"use client\";\nimport { useCallback } from \"react\";\nimport type { ComponentPropsWithRef, CSSProperties, MouseEvent, ReactNode } from \"react\";\nimport { createActionDetails } from \"../internal/action-details.js\";\nimport { dataAttr } from \"../internal/data-attr.js\";\nimport type { Action, ActionDetails, RenderProp, StateValue } from \"../internal/types.js\";\nimport { useActionTrigger } from \"../internal/use-action-trigger.js\";\nimport { rendersButton, useRender } from \"../internal/use-render.js\";\nimport type { DataAttributes } from \"../internal/use-render.js\";\nimport { useDialogContext } from \"./dialog-context.js\";\n\nexport interface DialogCloseState {\n /** The `action` is in flight. */\n readonly pending: boolean;\n readonly disabled: boolean;\n}\n\n/** Details passed to `Dialog.Close`'s `action`. */\nexport interface DialogCloseDetails extends ActionDetails {\n /** The `value` the dialog closes with (`dialog.returnValue`); `\"\"` without `value`. */\n readonly returnValue: string;\n}\n\nexport interface DialogCloseProps extends Omit<\n ComponentPropsWithRef<\"button\">,\n \"onClick\" | \"value\" | \"className\" | \"style\" | \"children\"\n> {\n /**\n * Runs inside this part's own transition, then closes the dialog with `
|
|
1
|
+
{"version":3,"file":"dialog-close.mjs","names":[],"sources":["../../src/dialog/dialog-close.tsx"],"sourcesContent":["\"use client\";\nimport { useCallback } from \"react\";\nimport type { ComponentPropsWithRef, CSSProperties, MouseEvent, ReactNode } from \"react\";\nimport { createActionDetails } from \"../internal/action-details.js\";\nimport { dataAttr } from \"../internal/data-attr.js\";\nimport type { Action, ActionDetails, RenderProp, StateValue } from \"../internal/types.js\";\nimport { useActionTrigger } from \"../internal/use-action-trigger.js\";\nimport { rendersButton, useRender } from \"../internal/use-render.js\";\nimport type { DataAttributes } from \"../internal/use-render.js\";\nimport { useDialogContext } from \"./dialog-context.js\";\n\nexport interface DialogCloseState {\n /** The `action` is in flight. */\n readonly pending: boolean;\n readonly disabled: boolean;\n}\n\n/** Details passed to `Dialog.Close`'s `action`. */\nexport interface DialogCloseDetails extends ActionDetails {\n /** The `value` the dialog closes with (`dialog.returnValue`); `\"\"` without `value`. */\n readonly returnValue: string;\n}\n\nexport interface DialogCloseProps extends Omit<\n ComponentPropsWithRef<\"button\">,\n \"onClick\" | \"value\" | \"className\" | \"style\" | \"children\"\n> {\n /**\n * Runs inside this part's own transition (`data-pending` on the button), then closes the\n * dialog with `value` as `returnValue`: in library mode through the Root\n * (`openChangeAction(false)` awaited, view transition if set), in JS-free mode with\n * `close(value)`. A rejection reaches the nearest Error Boundary above the Close part and\n * leaves the dialog open. If the dialog was dismissed and opened again while the action ran,\n * the close is dropped: it belonged to the earlier open. Without `action` in JS-free mode the\n * button is a native `command=\"request-close\"` invoker.\n */\n action?: Action<[details: DialogCloseDetails]>;\n /** Becomes `dialog.returnValue` (and `details.returnValue` of `openChangeAction(false)`). */\n value?: string;\n /** Native `disabled`; owned, so a `render` element cannot re-enable the button. */\n disabled?: boolean;\n className?: StateValue<string | undefined, DialogCloseState>;\n style?: StateValue<CSSProperties | undefined, DialogCloseState>;\n children?: StateValue<ReactNode, DialogCloseState>;\n render?: RenderProp<ComponentPropsWithRef<\"button\">, DialogCloseState>;\n}\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\n/**\n * A close button: `<button type=\"button\" commandfor command=\"request-close\">`. In JS-free mode\n * without `action` the browser closes the dialog (a close request: `cancel`, then `close`), with\n * `value` as the `returnValue`. In library mode the click is always cancelled and the close goes\n * through the Root (`openChangeAction(false)` awaited, then `close(value)` in the commit, inside\n * the view transition if the Popup's `transition` is set). With `action` the action runs first in\n * this part's own transition (`data-pending` on the button); a rejection leaves the dialog open.\n *\n * Owned keys (conventions 1.2): `type` (on a `<button>` only), `disabled`, `commandfor`,\n * `command`, `value`, `onClick` (library mode, or with `action`), `data-pending`,\n * `data-disabled`.\n */\nexport function DialogClose({\n action,\n value,\n disabled = false,\n className,\n style,\n children,\n render,\n ...rest\n}: DialogCloseProps) {\n const ctx = useDialogContext(\"Dialog.Close\");\n // The open-generation guard: a close from a previous opening must not close a new one.\n const { pending, run } = useActionTrigger(ctx.openGeneration);\n const { ids, dialogRef, recordCloseIntent } = ctx;\n const isLibrary = ctx.mode === \"library\";\n const hasAction = action !== undefined;\n const installClick = isLibrary || hasAction;\n\n const state: DialogCloseState = { pending, disabled };\n\n const devRef = useCallback(\n (element: HTMLElement) => {\n if (isDev && !installClick && element.tagName !== \"BUTTON\") {\n console.error(\n `Dialog.Close: rendered a <${element.tagName.toLowerCase()}> in JS-free mode without ` +\n '`action`. Only a <button> runs `command=\"request-close\"`; this element will not ' +\n \"close the dialog.\",\n );\n }\n // A cleanup, so the merged ref never calls back with `null`.\n return () => {};\n },\n [installClick],\n );\n\n const close = (event: Event, returnValue: string): void => {\n if (isLibrary) {\n // A library-owned close (design 2.3): awaited, then `close(returnValue)` in the commit. A\n // no-op when `false` is already intended (a repeated click, or a dismissal meanwhile).\n ctx.requestClose(event, \"activate\", returnValue);\n return;\n }\n // JS-free mode with `action`: a browser-owned close; there is no Root state to go through.\n const dialog = dialogRef.current;\n if (dialog === null || !dialog.open) return;\n recordCloseIntent({ reason: \"activate\", event });\n dialog.close(returnValue);\n };\n\n const onClick = (event: MouseEvent<HTMLElement>) => {\n // Cancelling the click cancels the command.\n event.preventDefault();\n if (disabled) return;\n const native = event.nativeEvent;\n const returnValue = value ?? \"\";\n if (action === undefined) {\n close(native, returnValue);\n return;\n }\n const details = createActionDetails(native, { returnValue });\n // Repeated clicks re-run the action; the first close wins (design 3.4). A close whose open\n // was superseded by a newer one (dismissed and re-opened meanwhile) is dropped by the hook.\n run(\n () => action(details),\n () => close(native, returnValue),\n );\n };\n\n const libraryProps: ComponentPropsWithRef<\"button\"> & DataAttributes = {\n ...(rendersButton(render) && { type: \"button\" as const }),\n // Owned: a render element must not re-enable a disabled Close.\n disabled,\n commandfor: ids.dialog,\n command: \"request-close\",\n value,\n ...(installClick && { onClick }),\n \"data-pending\": dataAttr(pending),\n \"data-disabled\": dataAttr(disabled),\n };\n\n return useRender<ComponentPropsWithRef<\"button\">, DialogCloseState>({\n defaultElement: \"button\",\n render,\n state,\n className,\n style,\n children,\n props: rest,\n libraryProps,\n ref: devRef,\n });\n}\n\nDialogClose.displayName = \"DialogClose\";\n"],"mappings":";;;;;;;;AA+CA,MAAM,QAAQ,QAAQ,IAAI,aAAa;;;;;;;;;;;;;AAcvC,SAAgB,YAAY,EAC1B,QACA,OACA,WAAW,OACX,WACA,OACA,UACA,QACA,GAAG,QACgB;CACnB,MAAM,MAAM,iBAAiB,cAAc;CAE3C,MAAM,EAAE,SAAS,QAAQ,iBAAiB,IAAI,cAAc;CAC5D,MAAM,EAAE,KAAK,WAAW,sBAAsB;CAC9C,MAAM,YAAY,IAAI,SAAS;CAE/B,MAAM,eAAe,aADH,WAAW,KAAA;CAG7B,MAAM,QAA0B;EAAE;EAAS;CAAS;CAEpD,MAAM,SAAS,aACZ,YAAyB;EACxB,IAAI,SAAS,CAAC,gBAAgB,QAAQ,YAAY,UAChD,QAAQ,MACN,6BAA6B,QAAQ,QAAQ,YAAY,EAAE,gIAG7D;EAGF,aAAa,CAAC;CAChB,GACA,CAAC,YAAY,CACf;CAEA,MAAM,SAAS,OAAc,gBAA8B;EACzD,IAAI,WAAW;GAGb,IAAI,aAAa,OAAO,YAAY,WAAW;GAC/C;EACF;EAEA,MAAM,SAAS,UAAU;EACzB,IAAI,WAAW,QAAQ,CAAC,OAAO,MAAM;EACrC,kBAAkB;GAAE,QAAQ;GAAY;EAAM,CAAC;EAC/C,OAAO,MAAM,WAAW;CAC1B;CAEA,MAAM,WAAW,UAAmC;EAElD,MAAM,eAAe;EACrB,IAAI,UAAU;EACd,MAAM,SAAS,MAAM;EACrB,MAAM,cAAc,SAAS;EAC7B,IAAI,WAAW,KAAA,GAAW;GACxB,MAAM,QAAQ,WAAW;GACzB;EACF;EACA,MAAM,UAAU,oBAAoB,QAAQ,EAAE,YAAY,CAAC;EAG3D,UACQ,OAAO,OAAO,SACd,MAAM,QAAQ,WAAW,CACjC;CACF;CAEA,MAAM,eAAiE;EACrE,GAAI,cAAc,MAAM,KAAK,EAAE,MAAM,SAAkB;EAEvD;EACA,YAAY,IAAI;EAChB,SAAS;EACT;EACA,GAAI,gBAAgB,EAAE,QAAQ;EAC9B,gBAAgB,SAAS,OAAO;EAChC,iBAAiB,SAAS,QAAQ;CACpC;CAEA,OAAO,UAA6D;EAClE,gBAAgB;EAChB;EACA;EACA;EACA;EACA;EACA,OAAO;EACP;EACA,KAAK;CACP,CAAC;AACH;AAEA,YAAY,cAAc"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dialog-context.mjs","names":[],"sources":["../../src/dialog/dialog-context.ts"],"sourcesContent":["\"use client\";\nimport { createContext, use } from \"react\";\nimport type { RefObject } from \"react\";\nimport type { ChangeReason } from \"../internal/types.js\";\n\n/**\n * \"static\": JS-free mode, the browser owns open and close through Invoker Commands. \"library\":\n *
|
|
1
|
+
{"version":3,"file":"dialog-context.mjs","names":[],"sources":["../../src/dialog/dialog-context.ts"],"sourcesContent":["\"use client\";\nimport { createContext, use } from \"react\";\nimport type { RefObject } from \"react\";\nimport type { ChangeReason } from \"../internal/types.js\";\n\n/**\n * \"static\": JS-free mode, the browser owns open and close through Invoker Commands. \"library\":\n * opening and the closes a part asks for are library-owned (transitions keyed on committed\n * state); dismissals are browser-owned and notified (design section 2).\n */\nexport type DialogMode = \"static\" | \"library\";\n\n/**\n * What caused the next `close` event, recorded just before the close happens (design 2.3). The\n * `close` event carries no source; without an intent the close is a `\"dismiss\"`.\n */\nexport interface DialogCloseIntent {\n readonly reason: ChangeReason;\n readonly event: Event | undefined;\n}\n\nexport interface DialogContextValue {\n readonly mode: DialogMode;\n /** `useId`-based ids: the `<dialog>`, its Title and its Description. */\n readonly ids: { readonly dialog: string; readonly title: string; readonly description: string };\n readonly modal: boolean;\n /** Committed open state (library mode); `false` in static mode. */\n readonly committed: boolean;\n /** Optimistic open state (library mode); `false` in static mode. */\n readonly optimistic: boolean;\n /** The Root's transition is pending (library mode). */\n readonly pending: boolean;\n /**\n * Open the dialog through the Root's transition (library mode): a Trigger click, a foreign\n * `show-modal` command, or mount reconciliation. A no-op when the user already intends `true`.\n */\n readonly requestOpen: (event: Event | undefined, reason: ChangeReason) => void;\n /**\n * The dialog closed natively (a dismissal, not the library's own `close()`): dispatch the\n * `false` notification unless the user already intends `false`.\n */\n readonly notifyClosed: (event: Event, reason: ChangeReason, returnValue: string) => void;\n /**\n * What the user already intends: the latest in-flight dispatch's `next`, else the optimistic\n * value of the last commit. Safe to call from handlers and effects (layout effects included,\n * also in the commit that changed it), and after an `await`. Not during render.\n */\n readonly intended: () => boolean;\n /**\n * A library-owned close (library mode, design 2.3): a Close part, a `method=\"dialog\"` submit,\n * a Form's settle close, a foreign close command. Records `returnValue` for the apply effect's\n * `close()` and dispatches `false` (`openChangeAction(false)` awaited). A no-op when `false` is\n * already intended.\n */\n readonly requestClose: (\n event: Event | undefined,\n reason: ChangeReason,\n returnValue: string,\n ) => void;\n /**\n * Read and clear the `returnValue` of the pending part-initiated close, for the apply effect's\n * `close()`; `\"\"` when there is none (a controlled close).\n */\n readonly takeCloseReturnValue: () => string;\n /** From `useControllableAction`: the latest in-flight intent, or `null`. */\n readonly latestInFlight: () => { readonly next: boolean } | null;\n /** The registered Trigger element, for the focus-return fallback (design 6.2). */\n readonly triggerRef: RefObject<HTMLElement | null>;\n /** The `<dialog>`, set by the Popup's ref; how Close and Form reach `close(value)`. */\n readonly dialogRef: RefObject<HTMLDialogElement | null>;\n /** Record what causes the next close. Cleared after a task if the dialog did not close. */\n readonly recordCloseIntent: (intent: DialogCloseIntent | undefined) => void;\n /** Read and clear the recorded intent (the Popup's `close` listener). */\n readonly takeCloseIntent: () => DialogCloseIntent | undefined;\n /**\n * Counts the opens of this dialog. A Close action reads it when it starts and skips its close\n * if a newer open started meanwhile (the dialog was dismissed and opened again).\n */\n readonly openGeneration: () => number;\n /** Advance `openGeneration` (an open dispatch, the `<dialog>`'s native `beforetoggle` to open). */\n readonly nextOpenGeneration: () => void;\n}\n\nexport const DialogContext = createContext<DialogContextValue | null>(null);\n\nexport function useDialogContext(part: string): DialogContextValue {\n const context = use(DialogContext);\n if (context === null) {\n throw new Error(`${part} must be rendered inside a Dialog.Root.`);\n }\n return context;\n}\n"],"mappings":";;;AAmFA,MAAa,gBAAgB,cAAyC,IAAI;AAE1E,SAAgB,iBAAiB,MAAkC;CACjE,MAAM,UAAU,IAAI,aAAa;CACjC,IAAI,YAAY,MACd,MAAM,IAAI,MAAM,GAAG,KAAK,wCAAwC;CAElE,OAAO;AACT"}
|
|
@@ -9,9 +9,13 @@ export interface DialogFormProps extends Omit<ComponentPropsWithRef<"form">, "me
|
|
|
9
9
|
/**
|
|
10
10
|
* React form action (`(formData) => unknown`, may be a Server Function). Passed to the form
|
|
11
11
|
* unwrapped so React's progressive enhancement is kept; the dialog closes with the submitter's
|
|
12
|
-
* `value` as `returnValue` once the action's transition settles (see `closeOnSettle`)
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* `value` as `returnValue` once the action's transition settles (see `closeOnSettle`): in
|
|
13
|
+
* library mode through the Root (`openChangeAction(false)` awaited, view transition if set),
|
|
14
|
+
* in JS-free mode with `close(value)`. A rejection leaves the dialog open. Without `action`
|
|
15
|
+
* the form renders `method="dialog"`: submitting closes the dialog, natively in JS-free mode
|
|
16
|
+
* and through the Root in library mode (a `preventDefault()` in your own `onSubmit` blocks
|
|
17
|
+
* both; `stopPropagation()` there leaves the close to the browser, notified as a dismissal).
|
|
18
|
+
* A string `action` (a URL) is a plain navigating form.
|
|
15
19
|
*/
|
|
16
20
|
action?: string | ((formData: FormData) => unknown);
|
|
17
21
|
/**
|
|
@@ -21,7 +25,9 @@ export interface DialogFormProps extends Omit<ComponentPropsWithRef<"form">, "me
|
|
|
21
25
|
* Any result that does not throw counts as success, including a `useActionState`-style result
|
|
22
26
|
* that reports validation errors: with the default the dialog closes and those errors are never
|
|
23
27
|
* seen. Throw to keep the dialog open, or set `closeOnSettle={false}` and close it yourself:
|
|
24
|
-
* a `Dialog.Close`, a
|
|
28
|
+
* a `Dialog.Close`, a submit button in a separate `method="dialog"` form, or a controlled
|
|
29
|
+
* `open`. (A `formMethod="dialog"` submitter inside this form cannot close it: React prevents
|
|
30
|
+
* every submit of a form with a function `action` and runs the action again.)
|
|
25
31
|
*
|
|
26
32
|
* The dialog is not closed either when it was closed (Escape, a Close part) while the action
|
|
27
33
|
* ran, also if the user re-opened it meanwhile.
|
|
@@ -39,7 +45,8 @@ export interface DialogFormProps extends Omit<ComponentPropsWithRef<"form">, "me
|
|
|
39
45
|
}
|
|
40
46
|
/**
|
|
41
47
|
* A form that completes the dialog. Without `action`: `<form method="dialog">`, so a submit
|
|
42
|
-
* closes the dialog
|
|
48
|
+
* closes the dialog with the submitter's `value` as `returnValue` (natively in JS-free mode;
|
|
49
|
+
* in library mode the Popup cancels the submission and closes through the Root). With a
|
|
43
50
|
* function `action`: `<form action={action}>` (React overrides the method) and a status watcher
|
|
44
51
|
* that closes the dialog once the action has settled, with the submitter's `value`, unless
|
|
45
52
|
* `closeOnSettle` is `false` or the dialog was closed while the action ran.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dialog-form.d.mts","names":[],"sources":["../../src/dialog/dialog-form.tsx"],"mappings":";;;iBAQiB;;WAEN;;iBAGM,wBAAwB,KACvC
|
|
1
|
+
{"version":3,"file":"dialog-form.d.mts","names":[],"sources":["../../src/dialog/dialog-form.tsx"],"mappings":";;;iBAQiB;;WAEN;;iBAGM,wBAAwB,KACvC;;;;;;;;;;;;EAcA,oBAAoB,UAAU;;;;;;;;;;;;;;;EAe9B;;;;;EAKA,SAAS;EACT,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;EAC9C,WAAW,WAAW,WAAW;EACjC,SAAS,WAAW,+BAA+B;;;;;;;;;;;;;;;;wBAiBrC,aACd,QACA,eACA,QACA,WACA,OACA,UACA,WACG,QACF,kBAAe;yBATF"}
|
|
@@ -1,35 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { resolveStateValue, useRender } from "../internal/use-render.mjs";
|
|
3
3
|
import { useDialogContext } from "./dialog-context.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { FormStatusWatcher } from "../internal/form-status-watcher.mjs";
|
|
5
|
+
import { useCallback, useRef, useState } from "react";
|
|
5
6
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
-
import { useFormStatus } from "react-dom";
|
|
7
7
|
//#region src/dialog/dialog-form.tsx
|
|
8
8
|
/**
|
|
9
|
-
* Observes React's form channel (design 8.1): `useFormStatus().pending` covers the action's
|
|
10
|
-
* whole transition, including the Suspense wait of what it re-renders. On `true → false` the
|
|
11
|
-
* submission succeeded (a rejection unmounts the form through the nearest Error Boundary, so the
|
|
12
|
-
* edge is never seen).
|
|
13
|
-
*/
|
|
14
|
-
function FormStatusWatcher({ onPendingChange, onSettled }) {
|
|
15
|
-
const { pending } = useFormStatus();
|
|
16
|
-
const previous = useRef(false);
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
const was = previous.current;
|
|
19
|
-
previous.current = pending;
|
|
20
|
-
if (was === pending) return;
|
|
21
|
-
onPendingChange(pending);
|
|
22
|
-
if (was && !pending) onSettled();
|
|
23
|
-
}, [
|
|
24
|
-
pending,
|
|
25
|
-
onPendingChange,
|
|
26
|
-
onSettled
|
|
27
|
-
]);
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
9
|
* A form that completes the dialog. Without `action`: `<form method="dialog">`, so a submit
|
|
32
|
-
* closes the dialog
|
|
10
|
+
* closes the dialog with the submitter's `value` as `returnValue` (natively in JS-free mode;
|
|
11
|
+
* in library mode the Popup cancels the submission and closes through the Root). With a
|
|
33
12
|
* function `action`: `<form action={action}>` (React overrides the method) and a status watcher
|
|
34
13
|
* that closes the dialog once the action has settled, with the submitter's `value`, unless
|
|
35
14
|
* `closeOnSettle` is `false` or the dialog was closed while the action ran.
|
|
@@ -41,7 +20,9 @@ function FormStatusWatcher({ onPendingChange, onSettled }) {
|
|
|
41
20
|
* Owned keys (conventions 1.2): `method` (except with a string `action`), `action`.
|
|
42
21
|
*/
|
|
43
22
|
function DialogForm({ action, closeOnSettle = true, method, className, style, children, render, ...rest }) {
|
|
44
|
-
const
|
|
23
|
+
const ctx = useDialogContext("Dialog.Form");
|
|
24
|
+
const { dialogRef, recordCloseIntent, requestClose } = ctx;
|
|
25
|
+
const isLibrary = ctx.mode === "library";
|
|
45
26
|
const [pending, setPending] = useState(false);
|
|
46
27
|
const submission = useRef(null);
|
|
47
28
|
const closedSinceSubmit = useRef(false);
|
|
@@ -73,15 +54,22 @@ function DialogForm({ action, closeOnSettle = true, method, className, style, ch
|
|
|
73
54
|
submission.current = null;
|
|
74
55
|
if (!closeOnSettle || closedSinceSubmit.current) return;
|
|
75
56
|
if (dialog === null || !dialog.open) return;
|
|
57
|
+
const returnValue = submitted?.returnValue ?? "";
|
|
58
|
+
if (isLibrary) {
|
|
59
|
+
requestClose(submitted?.event, "activate", returnValue);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
76
62
|
recordCloseIntent({
|
|
77
63
|
reason: "activate",
|
|
78
64
|
event: submitted?.event
|
|
79
65
|
});
|
|
80
|
-
dialog.close(
|
|
66
|
+
dialog.close(returnValue);
|
|
81
67
|
}, [
|
|
82
68
|
closeOnSettle,
|
|
83
69
|
dialogRef,
|
|
84
|
-
|
|
70
|
+
isLibrary,
|
|
71
|
+
recordCloseIntent,
|
|
72
|
+
requestClose
|
|
85
73
|
]);
|
|
86
74
|
return useRender({
|
|
87
75
|
defaultElement: "form",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dialog-form.mjs","names":[],"sources":["../../src/dialog/dialog-form.tsx"],"sourcesContent":["\"use client\";\nimport { useCallback,
|
|
1
|
+
{"version":3,"file":"dialog-form.mjs","names":[],"sources":["../../src/dialog/dialog-form.tsx"],"sourcesContent":["\"use client\";\nimport { useCallback, useRef, useState } from \"react\";\nimport type { ComponentPropsWithRef, CSSProperties, ReactNode } from \"react\";\nimport { FormStatusWatcher } from \"../internal/form-status-watcher.js\";\nimport type { RenderProp, StateValue } from \"../internal/types.js\";\nimport { resolveStateValue, useRender } from \"../internal/use-render.js\";\nimport { useDialogContext } from \"./dialog-context.js\";\n\nexport interface DialogFormState {\n /** `useFormStatus().pending` of this form, exposed for `className` / `children` functions. */\n readonly pending: boolean;\n}\n\nexport interface DialogFormProps extends Omit<\n ComponentPropsWithRef<\"form\">,\n \"method\" | \"action\" | \"className\" | \"style\" | \"children\"\n> {\n /**\n * React form action (`(formData) => unknown`, may be a Server Function). Passed to the form\n * unwrapped so React's progressive enhancement is kept; the dialog closes with the submitter's\n * `value` as `returnValue` once the action's transition settles (see `closeOnSettle`): in\n * library mode through the Root (`openChangeAction(false)` awaited, view transition if set),\n * in JS-free mode with `close(value)`. A rejection leaves the dialog open. Without `action`\n * the form renders `method=\"dialog\"`: submitting closes the dialog, natively in JS-free mode\n * and through the Root in library mode (a `preventDefault()` in your own `onSubmit` blocks\n * both; `stopPropagation()` there leaves the close to the browser, notified as a dismissal).\n * A string `action` (a URL) is a plain navigating form.\n */\n action?: string | ((formData: FormData) => unknown);\n /**\n * With a function `action`: close the dialog once the action's transition settles without\n * throwing. Default `true`.\n *\n * Any result that does not throw counts as success, including a `useActionState`-style result\n * that reports validation errors: with the default the dialog closes and those errors are never\n * seen. Throw to keep the dialog open, or set `closeOnSettle={false}` and close it yourself:\n * a `Dialog.Close`, a submit button in a separate `method=\"dialog\"` form, or a controlled\n * `open`. (A `formMethod=\"dialog\"` submitter inside this form cannot close it: React prevents\n * every submit of a form with a function `action` and runs the action again.)\n *\n * The dialog is not closed either when it was closed (Escape, a Close part) while the action\n * ran, also if the user re-opened it meanwhile.\n */\n closeOnSettle?: boolean;\n /**\n * Only with a string `action` (a navigating form, for example `\"post\"`). Owned otherwise:\n * `\"dialog\"` without `action`, React's own with a function `action`.\n */\n method?: ComponentPropsWithRef<\"form\">[\"method\"];\n className?: StateValue<string | undefined, DialogFormState>;\n style?: StateValue<CSSProperties | undefined, DialogFormState>;\n children?: StateValue<ReactNode, DialogFormState>;\n render?: RenderProp<ComponentPropsWithRef<\"form\">, DialogFormState>;\n}\n\n/**\n * A form that completes the dialog. Without `action`: `<form method=\"dialog\">`, so a submit\n * closes the dialog with the submitter's `value` as `returnValue` (natively in JS-free mode;\n * in library mode the Popup cancels the submission and closes through the Root). With a\n * function `action`: `<form action={action}>` (React overrides the method) and a status watcher\n * that closes the dialog once the action has settled, with the submitter's `value`, unless\n * `closeOnSettle` is `false` or the dialog was closed while the action ran.\n *\n * `DialogFormState.pending` is this form's own status; it is not emitted as `data-pending` and\n * is not folded into any control's `pending` (conventions 2.7). Submit buttons read\n * `useFormStatus()` themselves.\n *\n * Owned keys (conventions 1.2): `method` (except with a string `action`), `action`.\n */\nexport function DialogForm({\n action,\n closeOnSettle = true,\n method,\n className,\n style,\n children,\n render,\n ...rest\n}: DialogFormProps) {\n const ctx = useDialogContext(\"Dialog.Form\");\n const { dialogRef, recordCloseIntent, requestClose } = ctx;\n const isLibrary = ctx.mode === \"library\";\n const [pending, setPending] = useState(false);\n const submission = useRef<{ event: Event; returnValue: string } | null>(null);\n // The dialog closed after the last submit (Escape, a Close part): the settle must not close it\n // again, also when the user re-opened it meanwhile (the review's walkthrough 11.8 variant).\n const closedSinceSubmit = useRef(false);\n const watches = typeof action === \"function\";\n\n // The submitter's `value` comes from the native `submit` event (capture, before React's\n // handler runs the action).\n const libraryRef = useCallback((form: HTMLFormElement) => {\n const onSubmit = (event: Event) => {\n if (event.target !== form) return;\n const submitter = (event as SubmitEvent).submitter as\n | HTMLButtonElement\n | HTMLInputElement\n | null;\n submission.current = { event, returnValue: submitter?.value ?? \"\" };\n closedSinceSubmit.current = false;\n };\n const dialog = form.closest(\"dialog\");\n const onClose = (event: Event) => {\n if (event.target === dialog) closedSinceSubmit.current = true;\n };\n form.addEventListener(\"submit\", onSubmit, true);\n dialog?.addEventListener(\"close\", onClose);\n return () => {\n form.removeEventListener(\"submit\", onSubmit, true);\n dialog?.removeEventListener(\"close\", onClose);\n };\n }, []);\n\n const onSettled = useCallback(() => {\n const dialog = dialogRef.current;\n const submitted = submission.current;\n submission.current = null;\n if (!closeOnSettle || closedSinceSubmit.current) return;\n if (dialog === null || !dialog.open) return;\n const returnValue = submitted?.returnValue ?? \"\";\n if (isLibrary) {\n // A library-owned close (design 2.3): `openChangeAction(false)` awaited, then\n // `close(returnValue)` in the commit, inside the view transition if set.\n requestClose(submitted?.event, \"activate\", returnValue);\n return;\n }\n recordCloseIntent({ reason: \"activate\", event: submitted?.event });\n dialog.close(returnValue);\n }, [closeOnSettle, dialogRef, isLibrary, recordCloseIntent, requestClose]);\n\n const state: DialogFormState = { pending };\n\n return useRender<ComponentPropsWithRef<\"form\">, DialogFormState>({\n defaultElement: \"form\",\n render,\n state,\n className,\n style,\n children: (s) => (\n <>\n {resolveStateValue(children, s)}\n {watches && <FormStatusWatcher onPendingChange={setPending} onSettled={onSettled} />}\n </>\n ),\n props: rest,\n libraryProps: {\n method: action === undefined ? \"dialog\" : typeof action === \"string\" ? method : undefined,\n // React's own type wants `void | Promise<void>`; the library's actions return `unknown`\n // (conventions 2.2) and React ignores the result.\n action: action as ComponentPropsWithRef<\"form\">[\"action\"],\n },\n ref: libraryRef,\n });\n}\n\nDialogForm.displayName = \"DialogForm\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAqEA,SAAgB,WAAW,EACzB,QACA,gBAAgB,MAChB,QACA,WACA,OACA,UACA,QACA,GAAG,QACe;CAClB,MAAM,MAAM,iBAAiB,aAAa;CAC1C,MAAM,EAAE,WAAW,mBAAmB,iBAAiB;CACvD,MAAM,YAAY,IAAI,SAAS;CAC/B,MAAM,CAAC,SAAS,cAAc,SAAS,KAAK;CAC5C,MAAM,aAAa,OAAqD,IAAI;CAG5E,MAAM,oBAAoB,OAAO,KAAK;CACtC,MAAM,UAAU,OAAO,WAAW;CAIlC,MAAM,aAAa,aAAa,SAA0B;EACxD,MAAM,YAAY,UAAiB;GACjC,IAAI,MAAM,WAAW,MAAM;GAC3B,MAAM,YAAa,MAAsB;GAIzC,WAAW,UAAU;IAAE;IAAO,aAAa,WAAW,SAAS;GAAG;GAClE,kBAAkB,UAAU;EAC9B;EACA,MAAM,SAAS,KAAK,QAAQ,QAAQ;EACpC,MAAM,WAAW,UAAiB;GAChC,IAAI,MAAM,WAAW,QAAQ,kBAAkB,UAAU;EAC3D;EACA,KAAK,iBAAiB,UAAU,UAAU,IAAI;EAC9C,QAAQ,iBAAiB,SAAS,OAAO;EACzC,aAAa;GACX,KAAK,oBAAoB,UAAU,UAAU,IAAI;GACjD,QAAQ,oBAAoB,SAAS,OAAO;EAC9C;CACF,GAAG,CAAC,CAAC;CAEL,MAAM,YAAY,kBAAkB;EAClC,MAAM,SAAS,UAAU;EACzB,MAAM,YAAY,WAAW;EAC7B,WAAW,UAAU;EACrB,IAAI,CAAC,iBAAiB,kBAAkB,SAAS;EACjD,IAAI,WAAW,QAAQ,CAAC,OAAO,MAAM;EACrC,MAAM,cAAc,WAAW,eAAe;EAC9C,IAAI,WAAW;GAGb,aAAa,WAAW,OAAO,YAAY,WAAW;GACtD;EACF;EACA,kBAAkB;GAAE,QAAQ;GAAY,OAAO,WAAW;EAAM,CAAC;EACjE,OAAO,MAAM,WAAW;CAC1B,GAAG;EAAC;EAAe;EAAW;EAAW;EAAmB;CAAY,CAAC;CAIzE,OAAO,UAA0D;EAC/D,gBAAgB;EAChB;EACA,OAAA,EAL+B,QAK3B;EACJ;EACA;EACA,WAAW,MACT,qBAAA,UAAA,EAAA,UAAA,CACG,kBAAkB,UAAU,CAAC,GAC7B,WAAW,oBAAC,mBAAD;GAAmB,iBAAiB;GAAuB;EAAY,CAAA,CACnF,EAAA,CAAA;EAEJ,OAAO;EACP,cAAc;GACZ,QAAQ,WAAW,KAAA,IAAY,WAAW,OAAO,WAAW,WAAW,SAAS,KAAA;GAGxE;EACV;EACA,KAAK;CACP,CAAC;AACH;AAEA,WAAW,cAAc"}
|
|
@@ -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"}
|