@funstack/ui 0.1.0-alpha.1 → 0.1.0-alpha.2
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/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 +40 -0
- package/dist/combobox/combobox-input.d.mts.map +1 -0
- package/dist/combobox/combobox-input.mjs +88 -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 +508 -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/index.d.mts +20 -2
- package/dist/index.mjs +3 -1
- 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 +8 -0
- package/dist/internal/types.d.mts.map +1 -1
- package/dist/internal/use-controllable-action.mjs +5 -1
- package/dist/internal/use-controllable-action.mjs.map +1 -1
- package/dist/internal/use-form-reset.mjs +5 -1
- 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-native-group-reconciliation.mjs.map +1 -1
- package/dist/internal/use-native-select-value.mjs +182 -0
- package/dist/internal/use-native-select-value.mjs.map +1 -0
- 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 +45 -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 +66 -0
- package/dist/select/select-root.d.mts.map +1 -0
- package/dist/select/select-root.mjs +94 -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/package.json +3 -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"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "./internal/platform-attributes.mjs";
|
|
2
|
-
import { Action, ActionDetails, ChangeAction, ChangeReason, RenderProp, StateValue } from "./internal/types.mjs";
|
|
2
|
+
import { Action, ActionDetails, ChangeAction, ChangeReason, RenderProp, StateValue, UrgentChangeAction } from "./internal/types.mjs";
|
|
3
3
|
import { AccordionItemProps, AccordionItemState } from "./accordion/accordion-item.mjs";
|
|
4
4
|
import { AccordionPanelProps, AccordionPanelState } from "./accordion/accordion-panel.mjs";
|
|
5
5
|
import { AccordionRootExclusiveProps, AccordionRootMultipleProps, AccordionRootProps, AccordionRootState, AccordionValueChangeDetails } from "./accordion/accordion-root.mjs";
|
|
@@ -12,6 +12,16 @@ import { CheckboxGroupParentProps, CheckboxGroupParentState } from "./checkbox-g
|
|
|
12
12
|
import { CheckboxGroup } from "./checkbox-group/index.mjs";
|
|
13
13
|
import { Checkbox, CheckboxCheckedChangeDetails, CheckboxProps, CheckboxState } from "./checkbox/checkbox.mjs";
|
|
14
14
|
import "./checkbox/index.mjs";
|
|
15
|
+
import { ComboboxClearProps, ComboboxClearState } from "./combobox/combobox-clear.mjs";
|
|
16
|
+
import { ComboboxListProps, ComboboxListState } from "./combobox/combobox-list.mjs";
|
|
17
|
+
import { ComboboxEmptyProps, ComboboxEmptyState, ComboboxGroupLabelProps, ComboboxGroupLabelState, ComboboxGroupProps, ComboboxGroupState, ComboboxStatusProps, ComboboxStatusState } from "./combobox/combobox-group.mjs";
|
|
18
|
+
import { ComboboxInputProps, ComboboxInputState } from "./combobox/combobox-input.mjs";
|
|
19
|
+
import { ComboboxLabelProps, ComboboxLabelState } from "./combobox/combobox-label.mjs";
|
|
20
|
+
import { ComboboxOptionProps, ComboboxOptionState } from "./combobox/combobox-option.mjs";
|
|
21
|
+
import { ComboboxPopupProps, ComboboxPopupState } from "./combobox/combobox-popup.mjs";
|
|
22
|
+
import { ComboboxInputValueChangeDetails, ComboboxOpenChangeDetails, ComboboxRootProps, ComboboxRootState, ComboboxValueChangeDetails } from "./combobox/combobox-root.mjs";
|
|
23
|
+
import { ComboboxTriggerProps, ComboboxTriggerState } from "./combobox/combobox-trigger.mjs";
|
|
24
|
+
import { Combobox } from "./combobox/index.mjs";
|
|
15
25
|
import { DialogCloseDetails, DialogCloseProps, DialogCloseState } from "./dialog/dialog-close.mjs";
|
|
16
26
|
import { DialogPopupProps, DialogPopupState } from "./dialog/dialog-popup.mjs";
|
|
17
27
|
import { DialogDescriptionProps, DialogDescriptionState } from "./dialog/dialog-description.mjs";
|
|
@@ -57,4 +67,12 @@ import { RadioGroupItemProps, RadioGroupItemState } from "./radio-group/radio-gr
|
|
|
57
67
|
import { RadioGroupRootProps, RadioGroupRootState, RadioGroupValueChangeDetails } from "./radio-group/radio-group-root.mjs";
|
|
58
68
|
import { RadioGroupLabelProps } from "./radio-group/radio-group-label.mjs";
|
|
59
69
|
import { RadioGroup } from "./radio-group/index.mjs";
|
|
60
|
-
|
|
70
|
+
import { SelectRootProps, SelectRootState, SelectValueChangeDetails } from "./select/select-root.mjs";
|
|
71
|
+
import { SelectGroupProps } from "./select/select-group.mjs";
|
|
72
|
+
import { SelectGroupLabelProps } from "./select/select-group-label.mjs";
|
|
73
|
+
import { SelectOptionProps, SelectOptionState } from "./select/select-option.mjs";
|
|
74
|
+
import { SelectSeparatorProps } from "./select/select-separator.mjs";
|
|
75
|
+
import { SelectTriggerProps } from "./select/select-trigger.mjs";
|
|
76
|
+
import { SelectValueProps } from "./select/select-value.mjs";
|
|
77
|
+
import { Select } from "./select/index.mjs";
|
|
78
|
+
export { Accordion, type AccordionItemProps, type AccordionItemState, type AccordionPanelProps, type AccordionPanelState, type AccordionRootExclusiveProps, type AccordionRootMultipleProps, type AccordionRootProps, type AccordionRootState, type AccordionTriggerProps, type AccordionTriggerState, type AccordionValueChangeDetails, type Action, type ActionDetails, type ChangeAction, type ChangeReason, Checkbox, type CheckboxCheckedChangeDetails, CheckboxGroup, type CheckboxGroupItemProps, type CheckboxGroupItemState, type CheckboxGroupLabelProps, type CheckboxGroupParentProps, type CheckboxGroupParentState, type CheckboxGroupRootProps, type CheckboxGroupRootState, type CheckboxGroupValueChangeDetails, type CheckboxProps, type CheckboxState, Combobox, type ComboboxClearProps, type ComboboxClearState, type ComboboxEmptyProps, type ComboboxEmptyState, type ComboboxGroupLabelProps, type ComboboxGroupLabelState, type ComboboxGroupProps, type ComboboxGroupState, type ComboboxInputProps, type ComboboxInputState, type ComboboxInputValueChangeDetails, type ComboboxLabelProps, type ComboboxLabelState, type ComboboxListProps, type ComboboxListState, type ComboboxOpenChangeDetails, type ComboboxOptionProps, type ComboboxOptionState, type ComboboxPopupProps, type ComboboxPopupState, type ComboboxRootProps, type ComboboxRootState, type ComboboxStatusProps, type ComboboxStatusState, type ComboboxTriggerProps, type ComboboxTriggerState, type ComboboxValueChangeDetails, Dialog, type DialogCloseDetails, type DialogCloseProps, type DialogCloseState, type DialogDescriptionProps, type DialogDescriptionState, type DialogFormProps, type DialogFormState, type DialogOpenChangeDetails, type DialogPopupProps, type DialogPopupState, type DialogRootProps, type DialogRootState, type DialogTitleProps, type DialogTitleState, type DialogTriggerProps, type DialogTriggerState, Menu, type MenuAlign, type MenuCheckboxItemProps, type MenuCheckboxItemState, type MenuGroupLabelProps, type MenuGroupLabelState, type MenuGroupProps, type MenuGroupState, type MenuItemDetails, type MenuItemProps, type MenuItemState, type MenuOpenChangeDetails, type MenuPopupProps, type MenuPopupState, type MenuRadioGroupProps, type MenuRadioGroupState, type MenuRadioItemProps, type MenuRadioItemState, type MenuRootProps, type MenuRootState, type MenuSeparatorProps, type MenuSeparatorState, type MenuSide, type MenuSubmenuTriggerProps, type MenuSubmenuTriggerState, type MenuTriggerProps, type MenuTriggerState, Popover, type PopoverAlign, type PopoverCloseProps, type PopoverCloseState, type PopoverDescriptionProps, type PopoverDescriptionState, type PopoverOpenChangeDetails, type PopoverPopupProps, type PopoverPopupState, type PopoverRootProps, type PopoverRootState, type PopoverSide, type PopoverTitleProps, type PopoverTitleState, type PopoverTriggerProps, type PopoverTriggerState, RadioGroup, type RadioGroupItemProps, type RadioGroupItemState, type RadioGroupLabelProps, type RadioGroupRootProps, type RadioGroupRootState, type RadioGroupValueChangeDetails, type RenderProp, Select, type SelectGroupLabelProps, type SelectGroupProps, type SelectOptionProps, type SelectOptionState, type SelectRootProps, type SelectRootState, type SelectSeparatorProps, type SelectTriggerProps, type SelectValueChangeDetails, type SelectValueProps, type StateValue, Switch, type SwitchCheckedChangeDetails, type SwitchProps, type SwitchState, Tabs, type TabsActivationMode, type TabsIndicatorProps, type TabsIndicatorState, type TabsListProps, type TabsListState, type TabsOrientation, type TabsPanelProps, type TabsPanelState, type TabsRootProps, type TabsRootState, type TabsTabProps, type TabsTabState, type TabsValueChangeDetails, Toggle, type TogglePressedChangeDetails, type ToggleProps, type ToggleState, Tooltip, type TooltipAlign, type TooltipPopupProps, type TooltipPopupState, type TooltipProvides, type TooltipRootProps, type TooltipRootState, type TooltipSide, type TooltipTriggerProps, type TooltipTriggerState, type UrgentChangeAction };
|
package/dist/index.mjs
CHANGED
|
@@ -9,4 +9,6 @@ import { Popover } from "./popover/index.mjs";
|
|
|
9
9
|
import { Menu } from "./menu/index.mjs";
|
|
10
10
|
import { RadioGroup } from "./radio-group/index.mjs";
|
|
11
11
|
import { CheckboxGroup } from "./checkbox-group/index.mjs";
|
|
12
|
-
|
|
12
|
+
import { Select } from "./select/index.mjs";
|
|
13
|
+
import { Combobox } from "./combobox/index.mjs";
|
|
14
|
+
export { Accordion, Checkbox, CheckboxGroup, Combobox, Dialog, Menu, Popover, RadioGroup, Select, Switch, Tabs, Toggle, Tooltip };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as React from "react";
|
|
1
2
|
//#region src/internal/platform-attributes.d.ts
|
|
2
3
|
declare module "react" {
|
|
3
4
|
interface HTMLAttributes<T> {
|
|
@@ -41,6 +42,17 @@ declare module "react" {
|
|
|
41
42
|
/** `interestfor` on `<area href>` (Chromium 142+). Not in `@types/react` 19.3.0 (DefinitelyTyped #74664). */
|
|
42
43
|
interestfor?: string | undefined;
|
|
43
44
|
}
|
|
45
|
+
namespace JSX {
|
|
46
|
+
interface IntrinsicElements {
|
|
47
|
+
/**
|
|
48
|
+
* `<selectedcontent>` (customizable select: Chromium 135+, Safari 27): inside a select's
|
|
49
|
+
* `<button>`, the browser clones the selected option's content into it. React renders the
|
|
50
|
+
* unknown lowercase tag verbatim on client and server. `Select.Value` renders it.
|
|
51
|
+
* Not in `@types/react` 19.3.0 (no DefinitelyTyped issue filed as of 2026-10-17).
|
|
52
|
+
*/
|
|
53
|
+
selectedcontent: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
44
56
|
interface CSSProperties {
|
|
45
57
|
/** `interest-delay` shorthand (Chromium 142+). Not in `csstype` 3.2.3. */
|
|
46
58
|
interestDelay?: string | undefined;
|
|
@@ -51,5 +63,4 @@ declare module "react" {
|
|
|
51
63
|
}
|
|
52
64
|
}
|
|
53
65
|
//#endregion
|
|
54
|
-
export {};
|
|
55
66
|
//# sourceMappingURL=platform-attributes.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform-attributes.d.mts","names":[],"sources":["../../src/internal/platform-attributes.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"platform-attributes.d.mts","names":[],"sources":["../../src/internal/platform-attributes.ts"],"mappings":";;;YAkBY,eAAe;;;;;;IAMvB;;;;;;IAMA;;YAGQ,qBAAqB;;;;;;IAM7B;;;;;IAKA;;;;;;IAMA;;YAWQ,qBAAqB;;IAE7B;;YAGQ,mBAAmB;;IAE3B;;YAGQ;cACE;;;;;;;MAOR,iBAAiB,MAAM,kBAAkB,MAAM,eAAe,cAAc;;;YAItE;;IAER;;IAEA;;IAEA"}
|
|
@@ -13,6 +13,14 @@ export interface ActionDetails {
|
|
|
13
13
|
}
|
|
14
14
|
/** An action called with the next value of a controllable state. */
|
|
15
15
|
export type ChangeAction<T, Details extends ActionDetails = ActionDetails> = Action<[next: T, details: Details]>;
|
|
16
|
+
/**
|
|
17
|
+
* A change action the library calls **synchronously, outside any Transition, and does not
|
|
18
|
+
* await**: the urgent lane (conventions 2.6; first used by Combobox's `inputValueChangeAction`).
|
|
19
|
+
* The body runs during the DOM event, so `(next) => setText(next)` is an urgent update, which is
|
|
20
|
+
* what a controlled text input needs. Nothing is pending or optimistic for it; a returned promise
|
|
21
|
+
* is ignored (development warns once).
|
|
22
|
+
*/
|
|
23
|
+
export type UrgentChangeAction<T, Details extends ActionDetails = ActionDetails> = (next: T, details: Details) => void;
|
|
16
24
|
/**
|
|
17
25
|
* Why a change action was dispatched (conventions 2.2). One library-wide vocabulary: every
|
|
18
26
|
* `<Component><State>ChangeDetails` that exposes `reason` declares it with this type.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["../../src/internal/types.ts"],"mappings":";;;;;;;;YAQY,OAAO,4CAA4C,MAAM;iBAEpD;;WAEN,OAAO;;;YAIN,aAAa,GAAG,gBAAgB,gBAAgB,iBAAiB,QAC1E,MAAM,GAAG,SAAS;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../src/internal/types.ts"],"mappings":";;;;;;;;YAQY,OAAO,4CAA4C,MAAM;iBAEpD;;WAEN,OAAO;;;YAIN,aAAa,GAAG,gBAAgB,gBAAgB,iBAAiB,QAC1E,MAAM,GAAG,SAAS;;;;;;;;YAUT,mBAAmB,GAAG,gBAAgB,gBAAgB,kBAChE,MAAM,GACN,SAAS;;;;;;;;;;;;;YAeC;;YAGA,WAAW,GAAG,SAAS,MAAM,OAAO,UAAU;;;;;;YAO9C,WAAW,OAAO,SAC1B,aAAa,QAAQ,YACnB,OAAO,OAAO,OAAO,UAAU"}
|
|
@@ -76,12 +76,16 @@ function useControllableAction(options) {
|
|
|
76
76
|
const { count, next } = inFlight.current;
|
|
77
77
|
return count > 0 ? { next } : null;
|
|
78
78
|
}
|
|
79
|
+
function adopt(next) {
|
|
80
|
+
if (!isControlled) setInternal(next);
|
|
81
|
+
}
|
|
79
82
|
return {
|
|
80
83
|
committed,
|
|
81
84
|
optimistic,
|
|
82
85
|
pending,
|
|
83
86
|
dispatch,
|
|
84
|
-
latestInFlight
|
|
87
|
+
latestInFlight,
|
|
88
|
+
adopt
|
|
85
89
|
};
|
|
86
90
|
}
|
|
87
91
|
function useDevWarnings(isControlled, hasDefault, warn) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-controllable-action.mjs","names":[],"sources":["../../src/internal/use-controllable-action.ts"],"sourcesContent":["\"use client\";\nimport {\n addTransitionType,\n startTransition,\n useEffect,\n useOptimistic,\n useRef,\n useState,\n useTransition,\n} from \"react\";\nimport type { ActionDetails, ChangeAction } from \"./types.js\";\n\nexport interface UseControllableActionOptions<T, Details extends ActionDetails> {\n /** The controlled prop (`x`). `undefined` means uncontrolled. */\n controlled: T | undefined;\n /** The `defaultX` prop as the consumer passed it; `undefined` when omitted. */\n defaultValue: T | undefined;\n /** Initial uncontrolled value when neither `x` nor `defaultX` is given. */\n initialValue: T;\n /** The `xChangeAction` prop. Awaited inside this hook's transition. */\n action: ChangeAction<T, Details> | undefined;\n /**\n * View Transition types to attach to the commit that ends a dispatch. Receives the previous\n * committed value and the details object (Tabs derives next/prev direction from them).\n */\n transitionTypes?: (next: T, prev: T, details: Details) => readonly string[];\n /**\n * Optimistic reducer (conventions 3.4). Default: replace the value with `next`. Multi-item\n * states (Accordion's open set) pass a reducer keyed by item, reading the item from `details`,\n * so stacked optimistic patches re-base onto a changed committed value.\n */\n reducer?: (prev: T, next: T, details: Details) => T;\n /** Names used in development warnings. */\n warn?: { readonly component: string; readonly prop: string };\n}\n\nexport interface ControllableAction<T, Details extends ActionDetails> {\n /** Committed value (`x ?? internal`). Content regions render this. */\n readonly committed: T;\n /** Optimistic value; trigger parts render this. */\n readonly optimistic: T;\n /** An action dispatched through this hook is in flight. */\n readonly pending: boolean;\n /** Runs the action inside the hook's transition with optimistic feedback. */\n readonly dispatch: (next: T, details: Details) => void;\n /**\n * The `next` of the most recent dispatch while any dispatch's action is still unsettled, else\n * `null`. Lets a component with a browser-owned path (Accordion's find-in-page reveal) build on\n * what the user already intends instead of on the DOM. Read it from handlers and effects only.\n */\n readonly latestInFlight: () => { readonly next: T } | null;\n}\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\nfunction capitalize(s: string): string {\n return s.charAt(0).toUpperCase() + s.slice(1);\n}\n\n/** Reports an error that cannot reach a boundary (conventions 2.4: never swallow). */\nfunction report(error: unknown): void {\n if (typeof reportError === \"function\") reportError(error);\n else console.error(error);\n}\n\n/**\n * The one place the state model of conventions section 3 is implemented: committed state,\n * optimistic state (`useOptimistic`, reducer form), the component's own transition, the\n * post-`await` write of uncontrolled state, the latest-dispatch guard, and the surfacing of\n * superseded rejections (conventions 2.4).\n */\nexport function useControllableAction<T, Details extends ActionDetails = ActionDetails>(\n options: UseControllableActionOptions<T, Details>,\n): ControllableAction<T, Details> {\n const { controlled, defaultValue, initialValue, action, transitionTypes, reducer, warn } =\n options;\n\n // React keeps only the newest thenable per `useTransition`, so a rejection from a dispatch that\n // was superseded by a later one would be dropped entirely (no boundary, no `reportError`). The\n // hook catches those itself, keeps the first, and throws it from render so it reaches the same\n // nearest Error Boundary as the latest dispatch's rejection does through React.\n //\n // The throw sits before `useTransition` on purpose: React retries an errored render with every\n // pending lane, and in that retry `useTransition` unwraps the still-pending action thenable and\n // suspends, which would hold the boundary commit until the newer action settles.\n const [supersededError, setSupersededError] = useState<{ readonly error: unknown } | null>(null);\n const hasSupersededError = useRef(false);\n if (supersededError !== null) throw supersededError.error;\n\n const [internal, setInternal] = useState<T>(\n defaultValue === undefined ? initialValue : defaultValue,\n );\n const isControlled = controlled !== undefined;\n const committed = isControlled ? controlled : internal;\n const [optimistic, setOptimistic] = useOptimistic(\n committed,\n (prev: T, patch: { next: T; details: Details }) =>\n reducer ? reducer(prev, patch.next, patch.details) : patch.next,\n );\n const [pending, startOwnTransition] = useTransition();\n const latest = useRef(0);\n const inFlight = useRef<{ count: number; next: T | undefined }>({ count: 0, next: undefined });\n const mounted = useRef(false);\n\n useDevWarnings(isControlled, defaultValue !== undefined, warn);\n\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n\n function dispatch(next: T, details: Details): void {\n const prev = committed;\n const id = ++latest.current;\n inFlight.current.count += 1;\n inFlight.current.next = next;\n startOwnTransition(async () => {\n for (const type of transitionTypes?.(next, prev, details) ?? []) addTransitionType(type);\n setOptimistic({ next, details });\n try {\n await action?.(next, details);\n } catch (error) {\n settle();\n // The latest dispatch's rejection takes React's own route to the boundary.\n if (id === latest.current) throw error;\n // Nothing to throw to once unmounted (React drops its own thenables the same way).\n if (!mounted.current) return;\n if (hasSupersededError.current) {\n // One boundary reset per burst is enough; later errors are still reported.\n report(error);\n return;\n }\n hasSupersededError.current = true;\n // Outside the transition by now (after an await), so this is an urgent update: the owner\n // re-renders promptly and throws.\n setSupersededError({ error });\n return;\n }\n settle();\n if (!isControlled && id === latest.current) {\n startTransition(() => setInternal(next));\n }\n });\n }\n\n function settle(): void {\n inFlight.current.count -= 1;\n if (inFlight.current.count === 0) inFlight.current.next = undefined;\n }\n\n function latestInFlight(): { readonly next: T } | null {\n const { count, next } = inFlight.current;\n return count > 0 ? { next: next as T } : null;\n }\n\n return { committed, optimistic, pending, dispatch, latestInFlight };\n}\n\nfunction useDevWarnings(\n isControlled: boolean,\n hasDefault: boolean,\n warn: { readonly component: string; readonly prop: string } | undefined,\n): void {\n const initialMode = useRef(isControlled);\n const warnedBoth = useRef(false);\n const warnedSwitch = useRef(false);\n const component = warn?.component ?? \"A component\";\n const prop = warn?.prop ?? \"value\";\n\n useEffect(() => {\n if (!isDev) return;\n const defaultProp = `default${capitalize(prop)}`;\n\n if (isControlled && hasDefault && !warnedBoth.current) {\n warnedBoth.current = true;\n console.error(\n `${component}: both \\`${prop}\\` and \\`${defaultProp}\\` are set. ` +\n `Use \\`${prop}\\` for controlled mode or \\`${defaultProp}\\` for uncontrolled mode, not both.`,\n );\n }\n\n if (initialMode.current !== isControlled && !warnedSwitch.current) {\n warnedSwitch.current = true;\n const from = initialMode.current ? \"controlled\" : \"uncontrolled\";\n const to = isControlled ? \"controlled\" : \"uncontrolled\";\n console.error(\n `${component}: \\`${prop}\\` changed from ${from} to ${to}. Switching modes after mount ` +\n `is not supported; keep \\`${prop}\\` either always defined or always undefined.`,\n );\n }\n }, [component, prop, isControlled, hasDefault]);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-controllable-action.mjs","names":[],"sources":["../../src/internal/use-controllable-action.ts"],"sourcesContent":["\"use client\";\nimport {\n addTransitionType,\n startTransition,\n useEffect,\n useOptimistic,\n useRef,\n useState,\n useTransition,\n} from \"react\";\nimport type { ActionDetails, ChangeAction } from \"./types.js\";\n\nexport interface UseControllableActionOptions<T, Details extends ActionDetails> {\n /** The controlled prop (`x`). `undefined` means uncontrolled. */\n controlled: T | undefined;\n /** The `defaultX` prop as the consumer passed it; `undefined` when omitted. */\n defaultValue: T | undefined;\n /** Initial uncontrolled value when neither `x` nor `defaultX` is given. */\n initialValue: T;\n /** The `xChangeAction` prop. Awaited inside this hook's transition. */\n action: ChangeAction<T, Details> | undefined;\n /**\n * View Transition types to attach to the commit that ends a dispatch. Receives the previous\n * committed value and the details object (Tabs derives next/prev direction from them).\n */\n transitionTypes?: (next: T, prev: T, details: Details) => readonly string[];\n /**\n * Optimistic reducer (conventions 3.4). Default: replace the value with `next`. Multi-item\n * states (Accordion's open set) pass a reducer keyed by item, reading the item from `details`,\n * so stacked optimistic patches re-base onto a changed committed value.\n */\n reducer?: (prev: T, next: T, details: Details) => T;\n /** Names used in development warnings. */\n warn?: { readonly component: string; readonly prop: string };\n}\n\nexport interface ControllableAction<T, Details extends ActionDetails> {\n /** Committed value (`x ?? internal`). Content regions render this. */\n readonly committed: T;\n /** Optimistic value; trigger parts render this. */\n readonly optimistic: T;\n /** An action dispatched through this hook is in flight. */\n readonly pending: boolean;\n /** Runs the action inside the hook's transition with optimistic feedback. */\n readonly dispatch: (next: T, details: Details) => void;\n /**\n * The `next` of the most recent dispatch while any dispatch's action is still unsettled, else\n * `null`. Lets a component with a browser-owned path (Accordion's find-in-page reveal) build on\n * what the user already intends instead of on the DOM. Read it from handlers and effects only.\n */\n readonly latestInFlight: () => { readonly next: T } | null;\n /**\n * Uncontrolled mode only (a no-op when controlled): writes the committed value without calling\n * the action, without an optimistic step and without a transition. For a component that adopts\n * a default the browser chose (Select with neither `value` nor `defaultValue` takes the first\n * enabled option, design/components/select.md 5). Call it from a layout effect so the write is\n * flushed before paint.\n */\n readonly adopt: (next: T) => void;\n}\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\nfunction capitalize(s: string): string {\n return s.charAt(0).toUpperCase() + s.slice(1);\n}\n\n/** Reports an error that cannot reach a boundary (conventions 2.4: never swallow). */\nfunction report(error: unknown): void {\n if (typeof reportError === \"function\") reportError(error);\n else console.error(error);\n}\n\n/**\n * The one place the state model of conventions section 3 is implemented: committed state,\n * optimistic state (`useOptimistic`, reducer form), the component's own transition, the\n * post-`await` write of uncontrolled state, the latest-dispatch guard, and the surfacing of\n * superseded rejections (conventions 2.4).\n */\nexport function useControllableAction<T, Details extends ActionDetails = ActionDetails>(\n options: UseControllableActionOptions<T, Details>,\n): ControllableAction<T, Details> {\n const { controlled, defaultValue, initialValue, action, transitionTypes, reducer, warn } =\n options;\n\n // React keeps only the newest thenable per `useTransition`, so a rejection from a dispatch that\n // was superseded by a later one would be dropped entirely (no boundary, no `reportError`). The\n // hook catches those itself, keeps the first, and throws it from render so it reaches the same\n // nearest Error Boundary as the latest dispatch's rejection does through React.\n //\n // The throw sits before `useTransition` on purpose: React retries an errored render with every\n // pending lane, and in that retry `useTransition` unwraps the still-pending action thenable and\n // suspends, which would hold the boundary commit until the newer action settles.\n const [supersededError, setSupersededError] = useState<{ readonly error: unknown } | null>(null);\n const hasSupersededError = useRef(false);\n if (supersededError !== null) throw supersededError.error;\n\n const [internal, setInternal] = useState<T>(\n defaultValue === undefined ? initialValue : defaultValue,\n );\n const isControlled = controlled !== undefined;\n const committed = isControlled ? controlled : internal;\n const [optimistic, setOptimistic] = useOptimistic(\n committed,\n (prev: T, patch: { next: T; details: Details }) =>\n reducer ? reducer(prev, patch.next, patch.details) : patch.next,\n );\n const [pending, startOwnTransition] = useTransition();\n const latest = useRef(0);\n const inFlight = useRef<{ count: number; next: T | undefined }>({ count: 0, next: undefined });\n const mounted = useRef(false);\n\n useDevWarnings(isControlled, defaultValue !== undefined, warn);\n\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n\n function dispatch(next: T, details: Details): void {\n const prev = committed;\n const id = ++latest.current;\n inFlight.current.count += 1;\n inFlight.current.next = next;\n startOwnTransition(async () => {\n for (const type of transitionTypes?.(next, prev, details) ?? []) addTransitionType(type);\n setOptimistic({ next, details });\n try {\n await action?.(next, details);\n } catch (error) {\n settle();\n // The latest dispatch's rejection takes React's own route to the boundary.\n if (id === latest.current) throw error;\n // Nothing to throw to once unmounted (React drops its own thenables the same way).\n if (!mounted.current) return;\n if (hasSupersededError.current) {\n // One boundary reset per burst is enough; later errors are still reported.\n report(error);\n return;\n }\n hasSupersededError.current = true;\n // Outside the transition by now (after an await), so this is an urgent update: the owner\n // re-renders promptly and throws.\n setSupersededError({ error });\n return;\n }\n settle();\n if (!isControlled && id === latest.current) {\n startTransition(() => setInternal(next));\n }\n });\n }\n\n function settle(): void {\n inFlight.current.count -= 1;\n if (inFlight.current.count === 0) inFlight.current.next = undefined;\n }\n\n function latestInFlight(): { readonly next: T } | null {\n const { count, next } = inFlight.current;\n return count > 0 ? { next: next as T } : null;\n }\n\n function adopt(next: T): void {\n if (!isControlled) setInternal(next);\n }\n\n return { committed, optimistic, pending, dispatch, latestInFlight, adopt };\n}\n\nfunction useDevWarnings(\n isControlled: boolean,\n hasDefault: boolean,\n warn: { readonly component: string; readonly prop: string } | undefined,\n): void {\n const initialMode = useRef(isControlled);\n const warnedBoth = useRef(false);\n const warnedSwitch = useRef(false);\n const component = warn?.component ?? \"A component\";\n const prop = warn?.prop ?? \"value\";\n\n useEffect(() => {\n if (!isDev) return;\n const defaultProp = `default${capitalize(prop)}`;\n\n if (isControlled && hasDefault && !warnedBoth.current) {\n warnedBoth.current = true;\n console.error(\n `${component}: both \\`${prop}\\` and \\`${defaultProp}\\` are set. ` +\n `Use \\`${prop}\\` for controlled mode or \\`${defaultProp}\\` for uncontrolled mode, not both.`,\n );\n }\n\n if (initialMode.current !== isControlled && !warnedSwitch.current) {\n warnedSwitch.current = true;\n const from = initialMode.current ? \"controlled\" : \"uncontrolled\";\n const to = isControlled ? \"controlled\" : \"uncontrolled\";\n console.error(\n `${component}: \\`${prop}\\` changed from ${from} to ${to}. Switching modes after mount ` +\n `is not supported; keep \\`${prop}\\` either always defined or always undefined.`,\n );\n }\n }, [component, prop, isControlled, hasDefault]);\n}\n"],"mappings":"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"}
|
|
@@ -14,10 +14,13 @@ import { useEffectEvent, useLayoutEffect, useRef } from "react";
|
|
|
14
14
|
* (rule 7).
|
|
15
15
|
* - `onReset` always sees the latest committed render's values (`useEffectEvent`).
|
|
16
16
|
*/
|
|
17
|
-
function useFormReset(ref, onReset) {
|
|
17
|
+
function useFormReset(ref, onReset, onResetEvent) {
|
|
18
18
|
const handleReset = useEffectEvent((event) => {
|
|
19
19
|
onReset(event);
|
|
20
20
|
});
|
|
21
|
+
const handleResetEvent = useEffectEvent((event) => {
|
|
22
|
+
onResetEvent?.(event);
|
|
23
|
+
});
|
|
21
24
|
const subscription = useRef(null);
|
|
22
25
|
useLayoutEffect(() => {
|
|
23
26
|
const form = ref.current?.form ?? null;
|
|
@@ -27,6 +30,7 @@ function useFormReset(ref, onReset) {
|
|
|
27
30
|
subscription.current = null;
|
|
28
31
|
if (form === null) return;
|
|
29
32
|
const listener = (event) => {
|
|
33
|
+
handleResetEvent(event);
|
|
30
34
|
queueMicrotask(() => {
|
|
31
35
|
if (event.defaultPrevented) return;
|
|
32
36
|
handleReset(event);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-form-reset.mjs","names":[],"sources":["../../src/internal/use-form-reset.ts"],"sourcesContent":["\"use client\";\nimport { useEffectEvent, useLayoutEffect, useRef } from \"react\";\nimport type { RefObject } from \"react\";\n\n/** A form-associated element whose form owner the hook follows. */\nexport type FormAssociatedElement = HTMLInputElement | HTMLFieldSetElement;\n\ninterface Subscription {\n readonly form: HTMLFormElement;\n readonly listener: (event: Event) => void;\n}\n\n/**\n * Stable `reset` subscription on the form owner of `ref.current` (conventions 3.8).\n *\n * - The owner (`element.form`, which honours the `form` attribute) is re-read after every commit,\n * but the listener is moved only when the owner changed. It is never removed and re-added on an\n * ordinary re-render: React's automatic reset after a successful `<form action>` runs in the\n * commit's mutation phase, between an effect's cleanup and its next run, so a subscription that\n * followed every commit would miss it (rule 4).\n * - The `reset` event fires before the reset algorithm, so `onReset` is called in a microtask,\n * when the inputs hold their reset values (rule 5), and only if no listener cancelled the reset\n * (rule 7).\n * - `onReset` always sees the latest committed render's values (`useEffectEvent`).\n */\nexport function useFormReset(\n ref: RefObject<FormAssociatedElement | null>,\n onReset: (event: Event) => void,\n): void {\n const handleReset = useEffectEvent((event: Event) => {\n onReset(event);\n });\n const subscription = useRef<Subscription | null>(null);\n\n // No deps on purpose: follows the form owner after every commit (a changed `form` attribute, the\n // element moved to another form), but re-subscribes only when the owner actually changed.\n useLayoutEffect(() => {\n const form = ref.current?.form ?? null;\n const current = subscription.current;\n if (current !== null && current.form === form) return;\n if (current !== null) current.form.removeEventListener(\"reset\", current.listener);\n subscription.current = null;\n if (form === null) return;\n const listener = (event: Event) => {\n queueMicrotask(() => {\n if (event.defaultPrevented) return;\n handleReset(event);\n });\n };\n form.addEventListener(\"reset\", listener);\n subscription.current = { form, listener };\n });\n\n // The only place the subscription is torn down: unmount (and StrictMode's simulated unmount,\n // after which the effect above subscribes again).\n useLayoutEffect(\n () => () => {\n const current = subscription.current;\n if (current !== null) current.form.removeEventListener(\"reset\", current.listener);\n subscription.current = null;\n },\n [],\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAyBA,SAAgB,aACd,KACA,
|
|
1
|
+
{"version":3,"file":"use-form-reset.mjs","names":[],"sources":["../../src/internal/use-form-reset.ts"],"sourcesContent":["\"use client\";\nimport { useEffectEvent, useLayoutEffect, useRef } from \"react\";\nimport type { RefObject } from \"react\";\n\n/** A form-associated element whose form owner the hook follows. */\nexport type FormAssociatedElement = HTMLInputElement | HTMLFieldSetElement | HTMLSelectElement;\n\ninterface Subscription {\n readonly form: HTMLFormElement;\n readonly listener: (event: Event) => void;\n}\n\n/**\n * Stable `reset` subscription on the form owner of `ref.current` (conventions 3.8).\n *\n * - The owner (`element.form`, which honours the `form` attribute) is re-read after every commit,\n * but the listener is moved only when the owner changed. It is never removed and re-added on an\n * ordinary re-render: React's automatic reset after a successful `<form action>` runs in the\n * commit's mutation phase, between an effect's cleanup and its next run, so a subscription that\n * followed every commit would miss it (rule 4).\n * - The `reset` event fires before the reset algorithm, so `onReset` is called in a microtask,\n * when the inputs hold their reset values (rule 5), and only if no listener cancelled the reset\n * (rule 7).\n * - `onReset` always sees the latest committed render's values (`useEffectEvent`).\n */\nexport function useFormReset(\n ref: RefObject<FormAssociatedElement | null>,\n onReset: (event: Event) => void,\n /**\n * Called synchronously from the `reset` event, before the reset algorithm runs: the last\n * moment to write a reset target the browser will restore (Combobox re-applies its mirrored\n * `defaultValue`, which react-dom's attribute syncing overwrites after every keystroke).\n */\n onResetEvent?: (event: Event) => void,\n): void {\n const handleReset = useEffectEvent((event: Event) => {\n onReset(event);\n });\n const handleResetEvent = useEffectEvent((event: Event) => {\n onResetEvent?.(event);\n });\n const subscription = useRef<Subscription | null>(null);\n\n // No deps on purpose: follows the form owner after every commit (a changed `form` attribute, the\n // element moved to another form), but re-subscribes only when the owner actually changed.\n useLayoutEffect(() => {\n const form = ref.current?.form ?? null;\n const current = subscription.current;\n if (current !== null && current.form === form) return;\n if (current !== null) current.form.removeEventListener(\"reset\", current.listener);\n subscription.current = null;\n if (form === null) return;\n const listener = (event: Event) => {\n handleResetEvent(event);\n queueMicrotask(() => {\n if (event.defaultPrevented) return;\n handleReset(event);\n });\n };\n form.addEventListener(\"reset\", listener);\n subscription.current = { form, listener };\n });\n\n // The only place the subscription is torn down: unmount (and StrictMode's simulated unmount,\n // after which the effect above subscribes again).\n useLayoutEffect(\n () => () => {\n const current = subscription.current;\n if (current !== null) current.form.removeEventListener(\"reset\", current.listener);\n subscription.current = null;\n },\n [],\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAyBA,SAAgB,aACd,KACA,SAMA,cACM;CACN,MAAM,cAAc,gBAAgB,UAAiB;EACnD,QAAQ,KAAK;CACf,CAAC;CACD,MAAM,mBAAmB,gBAAgB,UAAiB;EACxD,eAAe,KAAK;CACtB,CAAC;CACD,MAAM,eAAe,OAA4B,IAAI;CAIrD,sBAAsB;EACpB,MAAM,OAAO,IAAI,SAAS,QAAQ;EAClC,MAAM,UAAU,aAAa;EAC7B,IAAI,YAAY,QAAQ,QAAQ,SAAS,MAAM;EAC/C,IAAI,YAAY,MAAM,QAAQ,KAAK,oBAAoB,SAAS,QAAQ,QAAQ;EAChF,aAAa,UAAU;EACvB,IAAI,SAAS,MAAM;EACnB,MAAM,YAAY,UAAiB;GACjC,iBAAiB,KAAK;GACtB,qBAAqB;IACnB,IAAI,MAAM,kBAAkB;IAC5B,YAAY,KAAK;GACnB,CAAC;EACH;EACA,KAAK,iBAAiB,SAAS,QAAQ;EACvC,aAAa,UAAU;GAAE;GAAM;EAAS;CAC1C,CAAC;CAID,4BACc;EACV,MAAM,UAAU,aAAa;EAC7B,IAAI,YAAY,MAAM,QAAQ,KAAK,oBAAoB,SAAS,QAAQ,QAAQ;EAChF,aAAa,UAAU;CACzB,GACA,CAAC,CACH;AACF"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useSyncExternalStore } from "react";
|
|
3
|
+
//#region src/internal/use-hydrated-mount.ts
|
|
4
|
+
const subscribe = () => () => {};
|
|
5
|
+
const clientSnapshot = () => false;
|
|
6
|
+
const serverSnapshot = () => true;
|
|
7
|
+
/**
|
|
8
|
+
* Whether this render hydrates server HTML: `true` in the render that hydrates the component
|
|
9
|
+
* (React uses `useSyncExternalStore`'s server snapshot there, also inside a Suspense boundary that
|
|
10
|
+
* hydrates after its parent) and `false` in a client-created render, including a hydration that
|
|
11
|
+
* fell back to client rendering. After a hydration React re-renders once with the client snapshot
|
|
12
|
+
* (scheduled from a passive effect), so read the value captured by the **mount layout effect**,
|
|
13
|
+
* which belongs to the hydration render (design/components/select.md 6.2).
|
|
14
|
+
*/
|
|
15
|
+
function useHydratedMount() {
|
|
16
|
+
return useSyncExternalStore(subscribe, clientSnapshot, serverSnapshot);
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
export { useHydratedMount };
|
|
20
|
+
|
|
21
|
+
//# sourceMappingURL=use-hydrated-mount.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-hydrated-mount.mjs","names":[],"sources":["../../src/internal/use-hydrated-mount.ts"],"sourcesContent":["\"use client\";\nimport { useSyncExternalStore } from \"react\";\n\nconst subscribe = () => () => {};\nconst clientSnapshot = () => false;\nconst serverSnapshot = () => true;\n\n/**\n * Whether this render hydrates server HTML: `true` in the render that hydrates the component\n * (React uses `useSyncExternalStore`'s server snapshot there, also inside a Suspense boundary that\n * hydrates after its parent) and `false` in a client-created render, including a hydration that\n * fell back to client rendering. After a hydration React re-renders once with the client snapshot\n * (scheduled from a passive effect), so read the value captured by the **mount layout effect**,\n * which belongs to the hydration render (design/components/select.md 6.2).\n */\nexport function useHydratedMount(): boolean {\n return useSyncExternalStore(subscribe, clientSnapshot, serverSnapshot);\n}\n"],"mappings":";;;AAGA,MAAM,wBAAwB,CAAC;AAC/B,MAAM,uBAAuB;AAC7B,MAAM,uBAAuB;;;;;;;;;AAU7B,SAAgB,mBAA4B;CAC1C,OAAO,qBAAqB,WAAW,gBAAgB,cAAc;AACvE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-native-group-reconciliation.mjs","names":[],"sources":["../../src/internal/use-native-group-reconciliation.ts"],"sourcesContent":["\"use client\";\nimport { useCallback, useEffectEvent, useLayoutEffect, useRef } from \"react\";\nimport type { RefObject } from \"react\";\nimport { useFormReset } from \"./use-form-reset.js\";\n\nexport interface UseNativeGroupReconciliationOptions<T> {\n
|
|
1
|
+
{"version":3,"file":"use-native-group-reconciliation.mjs","names":[],"sources":["../../src/internal/use-native-group-reconciliation.ts"],"sourcesContent":["\"use client\";\nimport { useCallback, useEffectEvent, useLayoutEffect, useRef } from \"react\";\nimport type { RefObject } from \"react\";\nimport { useFormReset } from \"./use-form-reset.js\";\n\nexport interface UseNativeGroupReconciliationOptions<T> {\n /**\n * The group's form-associated element: `RadioGroup.Root`'s / `CheckboxGroup.Root`'s\n * `<fieldset>`, or `Select.Root`'s `<select>` (whose options are the members).\n */\n readonly element: RefObject<HTMLFieldSetElement | HTMLSelectElement | null>;\n /** The group's value as the DOM shows it now (`checked` of the members). */\n readonly readCurrent: () => T;\n /** The value the browser resets the group to (`defaultChecked` of the members, rule 5). */\n readonly readDefault: () => T;\n /**\n * What the user intends (rule 1): the latest in-flight dispatch's `next`, else the optimistic\n * value. Use an explicit null check, `const f = latestInFlight(); return f === null ?\n * optimistic : f.next;`, not `?.next ?? optimistic`, which drops an in-flight dispatch of a\n * nullish value (a pending reset to \"nothing checked\").\n */\n readonly base: () => T;\n /**\n * The browser changed the group's value on a residual path and it differs from `base()`.\n * `event` is the `reset` event, or `undefined` for mount reconciliation (the Root's mount, or a\n * member mounting later). The Root dispatches once for the whole group with\n * `reason: \"platform\"`.\n */\n readonly onPlatformChange: (next: T, event: Event | undefined) => void;\n /**\n * Equality between a read value and `base()`. Default `Object.is`, right for a primitive value\n * (RadioGroup's `string | null`); a group with a collection value (CheckboxGroup's\n * `ReadonlySet<string>`) passes structural equality, since every read builds a new object.\n */\n readonly equals?: ((a: T, b: T) => boolean) | undefined;\n}\n\nexport interface UseNativeGroupReconciliationResult {\n /**\n * Stable. Pass it to every member, which hands it to\n * `useNativeChecked({ group: true, onItemMounted })`; the hook calls it from the member's mount\n * layout effect. A member that mounts after the Root's own mount reconciliation (inside a\n * Suspense boundary that hydrates later, selective hydration, or a member added on the client)\n * makes the adapter reconcile the group again: `readCurrent()` vs `base()`, in a microtask, once\n * for all members mounted in the same commit. Members mounting in the Root's own commit are\n * covered by the Root's mount reconciliation (their effects run before the Root's) and are\n * ignored.\n *\n * RadioGroup wiring: `const { onItemMounted } = useNativeGroupReconciliation(...)` in the Root,\n * put `onItemMounted` in the group context, and in each Item\n * `useNativeChecked({ type: \"radio\", group: true, onItemMounted: ctx.onItemMounted, ... })`.\n * The member's element argument is not used here; the Root may use it (dev warnings).\n */\n readonly onItemMounted: (element?: Element | null) => void;\n}\n\n/**\n * Group adapter of the native-input machinery (conventions 3.8): one dispatch per group for the\n * residual browser paths, from the Root rather than once per member.\n *\n * - Mount reconciliation, once, in a layout effect: `readCurrent()` vs `base()`. The members'\n * `useNativeChecked({ group: true })` leaves `checked` alone in their mount commit, so this\n * effect, which runs after the members' (children first), still reads the hydrated DOM.\n * - Late members: a member mounting in a later commit (a Suspense boundary that hydrates after the\n * Root, where a click before hydration changed the DOM) reports through `onItemMounted`, and the\n * group is reconciled again the same way (see `UseNativeGroupReconciliationResult`).\n * - Form reset through `useFormReset` on the container: in a microtask after the reset,\n * `readDefault()` vs `base()`.\n *\n * Values are compared with `equals` (default `Object.is`).\n */\nexport function useNativeGroupReconciliation<T>(\n options: UseNativeGroupReconciliationOptions<T>,\n): UseNativeGroupReconciliationResult {\n const { element } = options;\n\n // The latest committed options, for the late reconciliation, which runs in a microtask outside\n // any effect (so it cannot go through an Effect Event).\n const latest = useRef(options);\n useLayoutEffect(() => {\n latest.current = options;\n });\n\n /** The Root's own mount reconciliation has run (reset by its cleanup, StrictMode included). */\n const rootReconciled = useRef(false);\n /** A late reconciliation is queued; members mounting in the same commit share it. */\n const lateQueued = useRef(false);\n\n const reconcileMount = useEffectEvent(() => {\n if (element.current === null) return;\n reconcile(options, options.readCurrent());\n });\n useLayoutEffect(() => {\n reconcileMount();\n rootReconciled.current = true;\n return () => {\n rootReconciled.current = false;\n };\n }, []);\n\n const onItemMounted = useCallback(() => {\n if (!rootReconciled.current || lateQueued.current) return;\n lateQueued.current = true;\n queueMicrotask(() => {\n lateQueued.current = false;\n if (!rootReconciled.current) return;\n const current = latest.current;\n if (current.element.current === null) return;\n reconcile(current, current.readCurrent());\n });\n }, []);\n\n useFormReset(element, (event) => {\n if (element.current === null) return;\n reconcile(options, options.readDefault(), event);\n });\n\n return { onItemMounted };\n}\n\nfunction reconcile<T>(options: UseNativeGroupReconciliationOptions<T>, next: T, event?: Event) {\n const equals = options.equals ?? Object.is;\n if (!equals(next, options.base())) options.onPlatformChange(next, event);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAuEA,SAAgB,6BACd,SACoC;CACpC,MAAM,EAAE,YAAY;CAIpB,MAAM,SAAS,OAAO,OAAO;CAC7B,sBAAsB;EACpB,OAAO,UAAU;CACnB,CAAC;;CAGD,MAAM,iBAAiB,OAAO,KAAK;;CAEnC,MAAM,aAAa,OAAO,KAAK;CAE/B,MAAM,iBAAiB,qBAAqB;EAC1C,IAAI,QAAQ,YAAY,MAAM;EAC9B,UAAU,SAAS,QAAQ,YAAY,CAAC;CAC1C,CAAC;CACD,sBAAsB;EACpB,eAAe;EACf,eAAe,UAAU;EACzB,aAAa;GACX,eAAe,UAAU;EAC3B;CACF,GAAG,CAAC,CAAC;CAEL,MAAM,gBAAgB,kBAAkB;EACtC,IAAI,CAAC,eAAe,WAAW,WAAW,SAAS;EACnD,WAAW,UAAU;EACrB,qBAAqB;GACnB,WAAW,UAAU;GACrB,IAAI,CAAC,eAAe,SAAS;GAC7B,MAAM,UAAU,OAAO;GACvB,IAAI,QAAQ,QAAQ,YAAY,MAAM;GACtC,UAAU,SAAS,QAAQ,YAAY,CAAC;EAC1C,CAAC;CACH,GAAG,CAAC,CAAC;CAEL,aAAa,UAAU,UAAU;EAC/B,IAAI,QAAQ,YAAY,MAAM;EAC9B,UAAU,SAAS,QAAQ,YAAY,GAAG,KAAK;CACjD,CAAC;CAED,OAAO,EAAE,cAAc;AACzB;AAEA,SAAS,UAAa,SAAiD,MAAS,OAAe;CAE7F,IAAI,EADW,QAAQ,UAAU,OAAO,GAAA,CAC5B,MAAM,QAAQ,KAAK,CAAC,GAAG,QAAQ,iBAAiB,MAAM,KAAK;AACzE"}
|