@funstack/ui 0.1.0-alpha.0 → 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/README.md +1 -1
- package/dist/checkbox-group/checkbox-group-context.mjs +24 -0
- package/dist/checkbox-group/checkbox-group-context.mjs.map +1 -0
- package/dist/checkbox-group/checkbox-group-item.d.mts +55 -0
- package/dist/checkbox-group/checkbox-group-item.d.mts.map +1 -0
- package/dist/checkbox-group/checkbox-group-item.mjs +90 -0
- package/dist/checkbox-group/checkbox-group-item.mjs.map +1 -0
- package/dist/checkbox-group/checkbox-group-label.d.mts +21 -0
- package/dist/checkbox-group/checkbox-group-label.d.mts.map +1 -0
- package/dist/checkbox-group/checkbox-group-label.mjs +30 -0
- package/dist/checkbox-group/checkbox-group-label.mjs.map +1 -0
- package/dist/checkbox-group/checkbox-group-parent.d.mts +68 -0
- package/dist/checkbox-group/checkbox-group-parent.d.mts.map +1 -0
- package/dist/checkbox-group/checkbox-group-parent.mjs +145 -0
- package/dist/checkbox-group/checkbox-group-parent.mjs.map +1 -0
- package/dist/checkbox-group/checkbox-group-root.d.mts +100 -0
- package/dist/checkbox-group/checkbox-group-root.d.mts.map +1 -0
- package/dist/checkbox-group/checkbox-group-root.mjs +291 -0
- package/dist/checkbox-group/checkbox-group-root.mjs.map +1 -0
- package/dist/checkbox-group/index.d.mts +23 -0
- package/dist/checkbox-group/index.d.mts.map +1 -0
- package/dist/checkbox-group/index.mjs +23 -0
- package/dist/checkbox-group/index.mjs.map +1 -0
- 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/dialog/dialog-close.d.mts.map +1 -1
- package/dist/dialog/dialog-close.mjs +4 -4
- package/dist/dialog/dialog-close.mjs.map +1 -1
- package/dist/dialog/dialog-context.mjs.map +1 -1
- package/dist/dialog/dialog-popup.mjs +1 -0
- package/dist/dialog/dialog-popup.mjs.map +1 -1
- package/dist/dialog/dialog-root.mjs +9 -1
- package/dist/dialog/dialog-root.mjs.map +1 -1
- package/dist/index.d.mts +42 -2
- package/dist/index.mjs +6 -1
- package/dist/internal/anchor-position.d.mts +7 -0
- package/dist/internal/anchor-position.d.mts.map +1 -0
- package/dist/internal/anchor-position.mjs +16 -0
- package/dist/internal/anchor-position.mjs.map +1 -0
- package/dist/internal/platform-attributes.d.mts +18 -1
- package/dist/internal/platform-attributes.d.mts.map +1 -1
- package/dist/internal/sync-presence.mjs +19 -0
- package/dist/internal/sync-presence.mjs.map +1 -0
- package/dist/internal/types.d.mts +8 -0
- package/dist/internal/types.d.mts.map +1 -1
- package/dist/internal/use-action-trigger.mjs +58 -0
- package/dist/internal/use-action-trigger.mjs.map +1 -0
- package/dist/internal/use-controllable-action.mjs +5 -1
- package/dist/internal/use-controllable-action.mjs.map +1 -1
- package/dist/internal/use-delayed-pair.mjs +40 -0
- package/dist/internal/use-delayed-pair.mjs.map +1 -0
- package/dist/internal/use-exit-animation.mjs +55 -0
- package/dist/internal/use-exit-animation.mjs.map +1 -0
- 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 +2 -2
- 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/internal/use-popover-popup.mjs +274 -0
- package/dist/internal/use-popover-popup.mjs.map +1 -0
- package/dist/internal/use-popover-root.d.mts +2 -0
- package/dist/internal/use-popover-root.mjs +112 -0
- package/dist/internal/use-popover-root.mjs.map +1 -0
- package/dist/internal/use-popover-trigger.mjs +86 -0
- package/dist/internal/use-popover-trigger.mjs.map +1 -0
- package/dist/menu/index.d.mts +34 -0
- package/dist/menu/index.d.mts.map +1 -0
- package/dist/menu/index.mjs +34 -0
- package/dist/menu/index.mjs.map +1 -0
- package/dist/menu/menu-checkbox-item.d.mts +42 -0
- package/dist/menu/menu-checkbox-item.d.mts.map +1 -0
- package/dist/menu/menu-checkbox-item.mjs +71 -0
- package/dist/menu/menu-checkbox-item.mjs.map +1 -0
- package/dist/menu/menu-context.d.mts +1 -0
- package/dist/menu/menu-context.mjs +28 -0
- package/dist/menu/menu-context.mjs.map +1 -0
- package/dist/menu/menu-dom.mjs +37 -0
- package/dist/menu/menu-dom.mjs.map +1 -0
- package/dist/menu/menu-group.d.mts +48 -0
- package/dist/menu/menu-group.d.mts.map +1 -0
- package/dist/menu/menu-group.mjs +69 -0
- package/dist/menu/menu-group.mjs.map +1 -0
- package/dist/menu/menu-hover.mjs +29 -0
- package/dist/menu/menu-hover.mjs.map +1 -0
- package/dist/menu/menu-item.d.mts +45 -0
- package/dist/menu/menu-item.d.mts.map +1 -0
- package/dist/menu/menu-item.mjs +54 -0
- package/dist/menu/menu-item.mjs.map +1 -0
- package/dist/menu/menu-popup.d.mts +69 -0
- package/dist/menu/menu-popup.d.mts.map +1 -0
- package/dist/menu/menu-popup.mjs +207 -0
- package/dist/menu/menu-popup.mjs.map +1 -0
- package/dist/menu/menu-radio-group.d.mts +37 -0
- package/dist/menu/menu-radio-group.d.mts.map +1 -0
- package/dist/menu/menu-radio-group.mjs +72 -0
- package/dist/menu/menu-radio-group.mjs.map +1 -0
- package/dist/menu/menu-radio-item.d.mts +33 -0
- package/dist/menu/menu-radio-item.d.mts.map +1 -0
- package/dist/menu/menu-radio-item.mjs +62 -0
- package/dist/menu/menu-radio-item.mjs.map +1 -0
- package/dist/menu/menu-root.d.mts +54 -0
- package/dist/menu/menu-root.d.mts.map +1 -0
- package/dist/menu/menu-root.mjs +102 -0
- package/dist/menu/menu-root.mjs.map +1 -0
- package/dist/menu/menu-submenu-trigger.d.mts +31 -0
- package/dist/menu/menu-submenu-trigger.d.mts.map +1 -0
- package/dist/menu/menu-submenu-trigger.mjs +92 -0
- package/dist/menu/menu-submenu-trigger.mjs.map +1 -0
- package/dist/menu/menu-trigger.d.mts +36 -0
- package/dist/menu/menu-trigger.d.mts.map +1 -0
- package/dist/menu/menu-trigger.mjs +81 -0
- package/dist/menu/menu-trigger.mjs.map +1 -0
- package/dist/menu/use-menu-item.mjs +54 -0
- package/dist/menu/use-menu-item.mjs.map +1 -0
- package/dist/popover/index.d.mts +28 -0
- package/dist/popover/index.d.mts.map +1 -0
- package/dist/popover/index.mjs +28 -0
- package/dist/popover/index.mjs.map +1 -0
- package/dist/popover/popover-close.d.mts +41 -0
- package/dist/popover/popover-close.d.mts.map +1 -0
- package/dist/popover/popover-close.mjs +90 -0
- package/dist/popover/popover-close.mjs.map +1 -0
- package/dist/popover/popover-context.mjs +26 -0
- package/dist/popover/popover-context.mjs.map +1 -0
- package/dist/popover/popover-description.d.mts +22 -0
- package/dist/popover/popover-description.d.mts.map +1 -0
- package/dist/popover/popover-description.mjs +28 -0
- package/dist/popover/popover-description.mjs.map +1 -0
- package/dist/popover/popover-popup.d.mts +82 -0
- package/dist/popover/popover-popup.d.mts.map +1 -0
- package/dist/popover/popover-popup.mjs +93 -0
- package/dist/popover/popover-popup.mjs.map +1 -0
- package/dist/popover/popover-root.d.mts +64 -0
- package/dist/popover/popover-root.d.mts.map +1 -0
- package/dist/popover/popover-root.mjs +39 -0
- package/dist/popover/popover-root.mjs.map +1 -0
- package/dist/popover/popover-title.d.mts +23 -0
- package/dist/popover/popover-title.d.mts.map +1 -0
- package/dist/popover/popover-title.mjs +28 -0
- package/dist/popover/popover-title.mjs.map +1 -0
- package/dist/popover/popover-trigger.d.mts +37 -0
- package/dist/popover/popover-trigger.d.mts.map +1 -0
- package/dist/popover/popover-trigger.mjs +40 -0
- package/dist/popover/popover-trigger.mjs.map +1 -0
- package/dist/radio-group/radio-group-item.d.mts.map +1 -1
- package/dist/radio-group/radio-group-item.mjs +1 -6
- package/dist/radio-group/radio-group-item.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 +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/dist/tabs/tabs-context.mjs +2 -1
- package/dist/tabs/tabs-context.mjs.map +1 -1
- package/dist/tabs/tabs-list.d.mts +1 -1
- package/dist/tabs/tabs-list.d.mts.map +1 -1
- package/dist/tabs/tabs-list.mjs +17 -9
- package/dist/tabs/tabs-list.mjs.map +1 -1
- package/dist/tabs/tabs-tab.d.mts +1 -1
- package/dist/tabs/tabs-tab.d.mts.map +1 -1
- package/dist/tabs/tabs-tab.mjs +6 -3
- package/dist/tabs/tabs-tab.mjs.map +1 -1
- package/dist/tooltip/tooltip-popup.d.mts.map +1 -1
- package/dist/tooltip/tooltip-popup.mjs +2 -7
- package/dist/tooltip/tooltip-popup.mjs.map +1 -1
- package/dist/tooltip/use-tooltip-interaction.mjs +5 -17
- package/dist/tooltip/use-tooltip-interaction.mjs.map +1 -1
- package/package.json +6 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createActionDetails } from "./action-details.mjs";
|
|
3
|
+
import { transitionTypes } from "./transition-types.mjs";
|
|
4
|
+
import { useControllableAction } from "./use-controllable-action.mjs";
|
|
5
|
+
import { useDevWarning } from "./use-dev-warning.mjs";
|
|
6
|
+
import { useCallback, useId, useInsertionEffect, useRef, useState } from "react";
|
|
7
|
+
//#region src/internal/use-popover-root.ts
|
|
8
|
+
function isPopoverOpen(element) {
|
|
9
|
+
if (element === null) return false;
|
|
10
|
+
try {
|
|
11
|
+
return element.matches(":popover-open");
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** How long a recorded close intent survives when the popover did not close (design 2.3). */
|
|
17
|
+
function clearLater(intentRef, intent, popupRef) {
|
|
18
|
+
setTimeout(() => {
|
|
19
|
+
if (intentRef.current === intent && isPopoverOpen(popupRef.current)) intentRef.current = void 0;
|
|
20
|
+
}, 0);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The Root of a popover-like component without its public props (design 14.1): mode detection
|
|
24
|
+
* (once, from the first render), `useControllableAction<boolean>`, ids, refs, close intents and
|
|
25
|
+
* the intent-aware `requestOpen` / `requestClose` / `notifyClosed`.
|
|
26
|
+
*/
|
|
27
|
+
function usePopoverRoot(options) {
|
|
28
|
+
const { component, part, open, defaultOpen, openChangeAction, details, onDispatch } = options;
|
|
29
|
+
const isLibraryNow = options.mode === "library" || open !== void 0 || defaultOpen !== void 0 || openChangeAction !== void 0;
|
|
30
|
+
const [mode] = useState(isLibraryNow ? "library" : "static");
|
|
31
|
+
const isLibrary = mode === "library";
|
|
32
|
+
useDevWarning(isLibrary !== isLibraryNow, `${part}: the mode changed after mount (\`open\`, \`defaultOpen\` and \`openChangeAction\` were all absent on one render and present on another). Switching between JS-free and library mode is not supported.`);
|
|
33
|
+
const { committed, optimistic, pending, dispatch, latestInFlight } = useControllableAction({
|
|
34
|
+
controlled: isLibrary ? open : void 0,
|
|
35
|
+
defaultValue: isLibrary ? defaultOpen : void 0,
|
|
36
|
+
initialValue: false,
|
|
37
|
+
action: isLibrary ? openChangeAction : void 0,
|
|
38
|
+
transitionTypes: (next) => transitionTypes(component, next ? "open" : "close"),
|
|
39
|
+
warn: {
|
|
40
|
+
component: part,
|
|
41
|
+
prop: "open"
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
const id = useId();
|
|
45
|
+
const triggerRef = useRef(null);
|
|
46
|
+
const popupRef = useRef(null);
|
|
47
|
+
const closeIntent = useRef(void 0);
|
|
48
|
+
const committedOptimistic = useRef(optimistic);
|
|
49
|
+
useInsertionEffect(() => {
|
|
50
|
+
committedOptimistic.current = optimistic;
|
|
51
|
+
});
|
|
52
|
+
const generation = useRef(0);
|
|
53
|
+
const openGeneration = useCallback(() => generation.current, []);
|
|
54
|
+
const nextOpenGeneration = useCallback(() => {
|
|
55
|
+
generation.current++;
|
|
56
|
+
}, []);
|
|
57
|
+
const recordCloseIntent = useCallback((intent) => {
|
|
58
|
+
closeIntent.current = intent;
|
|
59
|
+
if (intent !== void 0) clearLater(closeIntent, intent, popupRef);
|
|
60
|
+
}, []);
|
|
61
|
+
const takeCloseIntent = useCallback(() => {
|
|
62
|
+
const intent = closeIntent.current;
|
|
63
|
+
closeIntent.current = void 0;
|
|
64
|
+
return intent;
|
|
65
|
+
}, []);
|
|
66
|
+
const intended = () => latestInFlight()?.next ?? committedOptimistic.current;
|
|
67
|
+
const buildDetails = (event, reason) => details !== void 0 ? details(event, reason) : createActionDetails(event, { reason });
|
|
68
|
+
const send = (next, event, reason) => {
|
|
69
|
+
const built = buildDetails(event, reason);
|
|
70
|
+
if (next) nextOpenGeneration();
|
|
71
|
+
onDispatch?.(next, built);
|
|
72
|
+
dispatch(next, built);
|
|
73
|
+
};
|
|
74
|
+
const requestOpen = (event, reason) => {
|
|
75
|
+
if (!isLibrary || intended()) return;
|
|
76
|
+
send(true, event, reason);
|
|
77
|
+
};
|
|
78
|
+
const requestClose = (event, reason) => {
|
|
79
|
+
if (!isLibrary || !intended()) return;
|
|
80
|
+
send(false, event, reason);
|
|
81
|
+
};
|
|
82
|
+
const notifyClosed = (event, reason) => {
|
|
83
|
+
if (!isLibrary || !intended()) return;
|
|
84
|
+
send(false, event, reason);
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
mode,
|
|
88
|
+
ids: {
|
|
89
|
+
popup: id,
|
|
90
|
+
title: `${id}-title`,
|
|
91
|
+
description: `${id}-description`
|
|
92
|
+
},
|
|
93
|
+
committed: isLibrary && committed,
|
|
94
|
+
optimistic: isLibrary && optimistic,
|
|
95
|
+
pending: isLibrary && pending,
|
|
96
|
+
intended,
|
|
97
|
+
openGeneration,
|
|
98
|
+
nextOpenGeneration,
|
|
99
|
+
requestOpen,
|
|
100
|
+
requestClose,
|
|
101
|
+
notifyClosed,
|
|
102
|
+
latestInFlight,
|
|
103
|
+
triggerRef,
|
|
104
|
+
popupRef,
|
|
105
|
+
recordCloseIntent,
|
|
106
|
+
takeCloseIntent
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
//#endregion
|
|
110
|
+
export { usePopoverRoot };
|
|
111
|
+
|
|
112
|
+
//# sourceMappingURL=use-popover-root.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-popover-root.mjs","names":[],"sources":["../../src/internal/use-popover-root.ts"],"sourcesContent":["\"use client\";\nimport { useCallback, useId, useInsertionEffect, useRef, useState } from \"react\";\nimport type { RefObject } from \"react\";\nimport { createActionDetails } from \"./action-details.js\";\nimport { transitionTypes } from \"./transition-types.js\";\nimport type { ActionDetails, ChangeAction, ChangeReason } from \"./types.js\";\nimport { useControllableAction } from \"./use-controllable-action.js\";\nimport { useDevWarning } from \"./use-dev-warning.js\";\n\n/**\n * \"static\": JS-free mode, the browser owns open and close through Invoker Commands. \"library\":\n * opening and part-initiated closing are library-owned (transitions keyed on committed state);\n * dismissals are the browser's and notified (design/components/popover.md section 2).\n */\nexport type PopoverMode = \"static\" | \"library\";\n\n/**\n * What caused the next close, recorded just before it happens (design 2.3). The `toggle` event\n * carries no source; without an intent a close is a `\"dismiss\"`.\n */\nexport interface PopoverCloseIntent {\n readonly reason: ChangeReason;\n readonly event: Event | undefined;\n}\n\n/** The `details` every popover-like `openChangeAction` receives. */\nexport interface PopoverLikeDetails extends ActionDetails {\n readonly reason: ChangeReason;\n}\n\n/**\n * The Root's state and machinery, shared by Popover and Menu (design 14). The component puts it\n * in its own context; `usePopoverTrigger` and `usePopoverPopup` take it as their first argument.\n */\nexport interface PopoverRootValue {\n readonly mode: PopoverMode;\n /** `useId`-based ids: the popup, its Title and its Description. */\n readonly ids: { readonly popup: string; readonly title: string; readonly description: string };\n /** Committed open state (library mode); `false` in JS-free mode. */\n readonly committed: boolean;\n /** Optimistic open state (library mode); `false` in JS-free mode. */\n readonly optimistic: boolean;\n /** The Root's transition is pending (library mode). */\n readonly pending: boolean;\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 counter that grows with every open (a requested open, or the popup being shown). A part\n * whose close runs after an `await` records it first and drops the close when it changed: the\n * user dismissed and re-opened meanwhile (Popover.Close, design 3.4). Handlers only.\n */\n readonly openGeneration: () => number;\n /** Advance `openGeneration` (the Popup's `beforetoggle` for an open). */\n readonly nextOpenGeneration: () => void;\n /**\n * Open through the Root's transition (library mode): a Trigger click, a foreign open caught at\n * `beforetoggle`, or mount reconciliation. A no-op when `true` is already intended.\n */\n readonly requestOpen: (event: Event | undefined, reason: ChangeReason) => void;\n /**\n * A library-owned close (library mode): a Trigger click while open, a Close part, Menu's item\n * activation. `openChangeAction(false)` is awaited and the popup hides in the commit. A no-op\n * when `false` is already intended.\n */\n readonly requestClose: (event: Event | undefined, reason: ChangeReason) => void;\n /**\n * The browser hid the popup (a dismissal or a foreign invoker): dispatch the `false`\n * notification unless `false` is already intended (the library's own echo, or a dismissal\n * during a pending part-initiated close).\n */\n readonly notifyClosed: (event: Event | undefined, reason: ChangeReason) => void;\n /** From `useControllableAction`: the latest in-flight intent, or `null`. */\n readonly latestInFlight: () => { readonly next: boolean } | null;\n /** The registered Trigger: `showPopover({ source })` and the focus-return fallback. */\n readonly triggerRef: RefObject<HTMLElement | null>;\n /** The popup element, set by the Popup's ref; how Close reaches `hidePopover()`. */\n readonly popupRef: RefObject<HTMLElement | null>;\n /** Record what causes the next close. Cleared after a task if the popover did not close. */\n readonly recordCloseIntent: (intent: PopoverCloseIntent | undefined) => void;\n /** Read and clear the recorded intent (the Popup's `toggle` listener). */\n readonly takeCloseIntent: () => PopoverCloseIntent | undefined;\n}\n\nexport interface UsePopoverRootOptions<Details extends PopoverLikeDetails> {\n /** Transition types and messages: `\"popover\"` gives `fsui-popover`, `fsui-popover-open`, … */\n readonly component: string;\n /** Public name of the Root part for development warnings (`\"Popover.Root\"`). */\n readonly part: string;\n readonly open: boolean | undefined;\n readonly defaultOpen: boolean | undefined;\n readonly openChangeAction: ChangeAction<boolean, Details> | undefined;\n /**\n * Builds the `details` of a dispatch. Default: `createActionDetails(event, { reason })`. A\n * component with more fields (Menu) passes its own.\n */\n readonly details?: (event: Event | undefined, reason: ChangeReason) => Details;\n /**\n * `\"library\"` forces library mode whatever the props (Menu, which has no JS-free mode). Default:\n * detected from `open` / `defaultOpen` / `openChangeAction` on the first render.\n */\n readonly mode?: \"library\";\n /**\n * Called synchronously for every dispatch, before the action runs (Menu: cancel hover timers,\n * read the open cause).\n */\n readonly onDispatch?: (next: boolean, details: Details) => void;\n}\n\nfunction isPopoverOpen(element: Element | null): boolean {\n if (element === null) return false;\n try {\n return element.matches(\":popover-open\");\n } catch {\n return false;\n }\n}\n\n/** How long a recorded close intent survives when the popover did not close (design 2.3). */\nfunction clearLater(\n intentRef: { current: PopoverCloseIntent | undefined },\n intent: PopoverCloseIntent,\n popupRef: { current: HTMLElement | null },\n): void {\n setTimeout(() => {\n // Closed: its `toggle` event (queued as a task) consumes the intent. Still open: the close\n // was cancelled or invalid, so the intent is stale.\n if (intentRef.current === intent && isPopoverOpen(popupRef.current)) {\n intentRef.current = undefined;\n }\n }, 0);\n}\n\n/**\n * The Root of a popover-like component without its public props (design 14.1): mode detection\n * (once, from the first render), `useControllableAction<boolean>`, ids, refs, close intents and\n * the intent-aware `requestOpen` / `requestClose` / `notifyClosed`.\n */\nexport function usePopoverRoot<Details extends PopoverLikeDetails>(\n options: UsePopoverRootOptions<Details>,\n): PopoverRootValue {\n const { component, part, open, defaultOpen, openChangeAction, details, onDispatch } = options;\n\n // Mode is decided once, from the first render's props (design 2).\n const isLibraryNow =\n options.mode === \"library\" ||\n open !== undefined ||\n defaultOpen !== undefined ||\n openChangeAction !== undefined;\n const [mode] = useState<PopoverMode>(isLibraryNow ? \"library\" : \"static\");\n const isLibrary = mode === \"library\";\n useDevWarning(\n isLibrary !== isLibraryNow,\n `${part}: the mode changed after mount (\\`open\\`, \\`defaultOpen\\` and \\`openChangeAction\\` ` +\n \"were all absent on one render and present on another). Switching between JS-free and \" +\n \"library mode is not supported.\",\n );\n\n // Hooks cannot be conditional: JS-free mode calls the hook too and never dispatches.\n const { committed, optimistic, pending, dispatch, latestInFlight } = useControllableAction<\n boolean,\n Details\n >({\n controlled: isLibrary ? open : undefined,\n defaultValue: isLibrary ? defaultOpen : undefined,\n initialValue: false,\n action: isLibrary ? openChangeAction : undefined,\n transitionTypes: (next) => transitionTypes(component, next ? \"open\" : \"close\"),\n warn: { component: part, prop: \"open\" },\n });\n\n const id = useId();\n const triggerRef = useRef<HTMLElement | null>(null);\n const popupRef = useRef<HTMLElement | null>(null);\n const closeIntent = useRef<PopoverCloseIntent | undefined>(undefined);\n\n // The optimistic value of the last commit, for callers that run after an `await` (a Close\n // action's continuation) and would otherwise read the value of the render they were created in.\n // An insertion effect runs in the mutation phase, before any layout effect of the same commit\n // (a child's included), so `intended()` is current there too.\n const committedOptimistic = useRef(optimistic);\n useInsertionEffect(() => {\n committedOptimistic.current = optimistic;\n });\n const generation = useRef(0);\n const openGeneration = useCallback(() => generation.current, []);\n const nextOpenGeneration = useCallback(() => {\n generation.current++;\n }, []);\n\n const recordCloseIntent = useCallback((intent: PopoverCloseIntent | undefined) => {\n closeIntent.current = intent;\n if (intent !== undefined) clearLater(closeIntent, intent, popupRef);\n }, []);\n const takeCloseIntent = useCallback(() => {\n const intent = closeIntent.current;\n closeIntent.current = undefined;\n return intent;\n }, []);\n\n // Conventions 3.8 rule 1: judge no-ops against what the user already intends. That also\n // deduplicates the StrictMode re-run of the mount reconciliation and recognises the library's\n // own `hidePopover()` echoing back through `toggle`.\n const intended = (): boolean => latestInFlight()?.next ?? committedOptimistic.current;\n\n const buildDetails = (event: Event | undefined, reason: ChangeReason): Details =>\n details !== undefined\n ? details(event, reason)\n : (createActionDetails(event, { reason }) as unknown as Details);\n\n const send = (next: boolean, event: Event | undefined, reason: ChangeReason): void => {\n const built = buildDetails(event, reason);\n if (next) nextOpenGeneration();\n onDispatch?.(next, built);\n dispatch(next, built);\n };\n\n const requestOpen = (event: Event | undefined, reason: ChangeReason): void => {\n if (!isLibrary || intended()) return;\n send(true, event, reason);\n };\n\n const requestClose = (event: Event | undefined, reason: ChangeReason): void => {\n if (!isLibrary || !intended()) return;\n send(false, event, reason);\n };\n\n // Same guard as `requestClose`; a separate name because the callers mean different things.\n const notifyClosed = (event: Event | undefined, reason: ChangeReason): void => {\n if (!isLibrary || !intended()) return;\n send(false, event, reason);\n };\n\n // Not memoised: `dispatch` is a new function on every render (as Dialog), so parts re-render\n // with the Root, which they do anyway when its state changes.\n return {\n mode,\n ids: { popup: id, title: `${id}-title`, description: `${id}-description` },\n committed: isLibrary && committed,\n optimistic: isLibrary && optimistic,\n pending: isLibrary && pending,\n intended,\n openGeneration,\n nextOpenGeneration,\n requestOpen,\n requestClose,\n notifyClosed,\n latestInFlight,\n triggerRef,\n popupRef,\n recordCloseIntent,\n takeCloseIntent,\n };\n}\n"],"mappings":";;;;;;;AAgHA,SAAS,cAAc,SAAkC;CACvD,IAAI,YAAY,MAAM,OAAO;CAC7B,IAAI;EACF,OAAO,QAAQ,QAAQ,eAAe;CACxC,QAAQ;EACN,OAAO;CACT;AACF;;AAGA,SAAS,WACP,WACA,QACA,UACM;CACN,iBAAiB;EAGf,IAAI,UAAU,YAAY,UAAU,cAAc,SAAS,OAAO,GAChE,UAAU,UAAU,KAAA;CAExB,GAAG,CAAC;AACN;;;;;;AAOA,SAAgB,eACd,SACkB;CAClB,MAAM,EAAE,WAAW,MAAM,MAAM,aAAa,kBAAkB,SAAS,eAAe;CAGtF,MAAM,eACJ,QAAQ,SAAS,aACjB,SAAS,KAAA,KACT,gBAAgB,KAAA,KAChB,qBAAqB,KAAA;CACvB,MAAM,CAAC,QAAQ,SAAsB,eAAe,YAAY,QAAQ;CACxE,MAAM,YAAY,SAAS;CAC3B,cACE,cAAc,cACd,GAAG,KAAK,uMAGV;CAGA,MAAM,EAAE,WAAW,YAAY,SAAS,UAAU,mBAAmB,sBAGnE;EACA,YAAY,YAAY,OAAO,KAAA;EAC/B,cAAc,YAAY,cAAc,KAAA;EACxC,cAAc;EACd,QAAQ,YAAY,mBAAmB,KAAA;EACvC,kBAAkB,SAAS,gBAAgB,WAAW,OAAO,SAAS,OAAO;EAC7E,MAAM;GAAE,WAAW;GAAM,MAAM;EAAO;CACxC,CAAC;CAED,MAAM,KAAK,MAAM;CACjB,MAAM,aAAa,OAA2B,IAAI;CAClD,MAAM,WAAW,OAA2B,IAAI;CAChD,MAAM,cAAc,OAAuC,KAAA,CAAS;CAMpE,MAAM,sBAAsB,OAAO,UAAU;CAC7C,yBAAyB;EACvB,oBAAoB,UAAU;CAChC,CAAC;CACD,MAAM,aAAa,OAAO,CAAC;CAC3B,MAAM,iBAAiB,kBAAkB,WAAW,SAAS,CAAC,CAAC;CAC/D,MAAM,qBAAqB,kBAAkB;EAC3C,WAAW;CACb,GAAG,CAAC,CAAC;CAEL,MAAM,oBAAoB,aAAa,WAA2C;EAChF,YAAY,UAAU;EACtB,IAAI,WAAW,KAAA,GAAW,WAAW,aAAa,QAAQ,QAAQ;CACpE,GAAG,CAAC,CAAC;CACL,MAAM,kBAAkB,kBAAkB;EACxC,MAAM,SAAS,YAAY;EAC3B,YAAY,UAAU,KAAA;EACtB,OAAO;CACT,GAAG,CAAC,CAAC;CAKL,MAAM,iBAA0B,eAAe,CAAC,EAAE,QAAQ,oBAAoB;CAE9E,MAAM,gBAAgB,OAA0B,WAC9C,YAAY,KAAA,IACR,QAAQ,OAAO,MAAM,IACpB,oBAAoB,OAAO,EAAE,OAAO,CAAC;CAE5C,MAAM,QAAQ,MAAe,OAA0B,WAA+B;EACpF,MAAM,QAAQ,aAAa,OAAO,MAAM;EACxC,IAAI,MAAM,mBAAmB;EAC7B,aAAa,MAAM,KAAK;EACxB,SAAS,MAAM,KAAK;CACtB;CAEA,MAAM,eAAe,OAA0B,WAA+B;EAC5E,IAAI,CAAC,aAAa,SAAS,GAAG;EAC9B,KAAK,MAAM,OAAO,MAAM;CAC1B;CAEA,MAAM,gBAAgB,OAA0B,WAA+B;EAC7E,IAAI,CAAC,aAAa,CAAC,SAAS,GAAG;EAC/B,KAAK,OAAO,OAAO,MAAM;CAC3B;CAGA,MAAM,gBAAgB,OAA0B,WAA+B;EAC7E,IAAI,CAAC,aAAa,CAAC,SAAS,GAAG;EAC/B,KAAK,OAAO,OAAO,MAAM;CAC3B;CAIA,OAAO;EACL;EACA,KAAK;GAAE,OAAO;GAAI,OAAO,GAAG,GAAG;GAAS,aAAa,GAAG,GAAG;EAAc;EACzE,WAAW,aAAa;EACxB,YAAY,aAAa;EACzB,SAAS,aAAa;EACtB;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { dataAttr } from "./data-attr.mjs";
|
|
3
|
+
import { rendersButton } from "./use-render.mjs";
|
|
4
|
+
import { useCallback } from "react";
|
|
5
|
+
//#region src/internal/use-popover-trigger.ts
|
|
6
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
7
|
+
function isPopoverOpen(element) {
|
|
8
|
+
if (element === null) return false;
|
|
9
|
+
try {
|
|
10
|
+
return element.matches(":popover-open");
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The Trigger of a popover-like component (design/components/popover.md 14.3):
|
|
17
|
+
* `button[commandfor][command="toggle-popover"]` with `aria-expanded`. In JS-free mode the
|
|
18
|
+
* browser runs the command and the Popup keeps `aria-expanded` current on the element. In library
|
|
19
|
+
* mode the click is cancelled (so the command does not run) and toggles against what the user
|
|
20
|
+
* already intends: an open through `requestOpen`, a library-owned close through `requestClose`.
|
|
21
|
+
*
|
|
22
|
+
* Menu adds `aria-haspopup="menu"` and its own key handling on top.
|
|
23
|
+
*/
|
|
24
|
+
function usePopoverTrigger(ctx, options) {
|
|
25
|
+
const { part, disabled, render, handlers, activate, disabledAs = "native" } = options;
|
|
26
|
+
const isLibrary = ctx.mode === "library";
|
|
27
|
+
const { ids, triggerRef, popupRef } = ctx;
|
|
28
|
+
const state = {
|
|
29
|
+
open: ctx.optimistic,
|
|
30
|
+
pending: ctx.pending,
|
|
31
|
+
disabled
|
|
32
|
+
};
|
|
33
|
+
const ref = useCallback((element) => {
|
|
34
|
+
if (isDev && !isLibrary && element.tagName !== "BUTTON") console.error(`${part}: rendered a <${element.tagName.toLowerCase()}> in JS-free mode. Only a <button> runs \`command="toggle-popover"\`; this trigger will not open the popover. Render a <button>, or put the component in library mode (\`defaultOpen={false}\`).`);
|
|
35
|
+
triggerRef.current = element;
|
|
36
|
+
if (!isLibrary && popupRef.current !== null) element.setAttribute("aria-expanded", String(isPopoverOpen(popupRef.current)));
|
|
37
|
+
return () => {
|
|
38
|
+
if (triggerRef.current === element) triggerRef.current = null;
|
|
39
|
+
};
|
|
40
|
+
}, [
|
|
41
|
+
isLibrary,
|
|
42
|
+
part,
|
|
43
|
+
triggerRef,
|
|
44
|
+
popupRef
|
|
45
|
+
]);
|
|
46
|
+
const onClick = (event) => {
|
|
47
|
+
event.preventDefault();
|
|
48
|
+
if (disabled) return;
|
|
49
|
+
if (activate !== void 0) {
|
|
50
|
+
activate(event);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const native = event.nativeEvent;
|
|
54
|
+
if (ctx.intended()) ctx.requestClose(native, "activate");
|
|
55
|
+
else ctx.requestOpen(native, "activate");
|
|
56
|
+
};
|
|
57
|
+
const merged = { ...handlers };
|
|
58
|
+
if (isLibrary) {
|
|
59
|
+
const extra = handlers?.onClick;
|
|
60
|
+
merged.onClick = extra === void 0 ? onClick : (event) => {
|
|
61
|
+
extra(event);
|
|
62
|
+
onClick(event);
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const ariaDisabled = disabledAs === "aria";
|
|
66
|
+
return {
|
|
67
|
+
state,
|
|
68
|
+
libraryProps: {
|
|
69
|
+
...rendersButton(render) && { type: "button" },
|
|
70
|
+
disabled: ariaDisabled ? void 0 : disabled,
|
|
71
|
+
...ariaDisabled && { "aria-disabled": disabled ? true : void 0 },
|
|
72
|
+
commandfor: ids.popup,
|
|
73
|
+
command: "toggle-popover",
|
|
74
|
+
"aria-expanded": isLibrary ? state.open : false,
|
|
75
|
+
...merged,
|
|
76
|
+
"data-open": dataAttr(state.open),
|
|
77
|
+
"data-pending": dataAttr(state.pending),
|
|
78
|
+
"data-disabled": dataAttr((isLibrary || ariaDisabled) && disabled)
|
|
79
|
+
},
|
|
80
|
+
ref
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
export { usePopoverTrigger };
|
|
85
|
+
|
|
86
|
+
//# sourceMappingURL=use-popover-trigger.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-popover-trigger.mjs","names":[],"sources":["../../src/internal/use-popover-trigger.ts"],"sourcesContent":["\"use client\";\nimport { useCallback } from \"react\";\nimport type { ComponentPropsWithRef, DOMAttributes, MouseEvent } from \"react\";\nimport { dataAttr } from \"./data-attr.js\";\nimport type { RenderProp } from \"./types.js\";\nimport { rendersButton } from \"./use-render.js\";\nimport type { DataAttributes } from \"./use-render.js\";\nimport type { PopoverRootValue } from \"./use-popover-root.js\";\n\nexport interface PopoverTriggerHookState {\n /** Optimistic open state (library mode); `false` in JS-free mode. */\n readonly open: boolean;\n /** The Root's transition is pending (an open, a part-initiated close or a notification). */\n readonly pending: boolean;\n readonly disabled: boolean;\n}\n\n/** Event handlers a component layers onto the Trigger's own (Menu's keys and hover intent). */\nexport type PopoverTriggerHandlers = Pick<\n DOMAttributes<HTMLButtonElement>,\n \"onClick\" | \"onKeyDown\" | \"onPointerMove\" | \"onPointerEnter\" | \"onPointerLeave\"\n>;\n\nexport interface UsePopoverTriggerOptions<State> {\n /** Public name of the part for development warnings (`\"Popover.Trigger\"`). */\n readonly part: string;\n readonly disabled: boolean;\n readonly render: RenderProp<ComponentPropsWithRef<\"button\">, State> | undefined;\n /**\n * Extra handlers, merged in front of the Trigger's own: for a key both define, the extra one\n * runs first (the consumer's pass-through handler still runs before both, conventions 1.2).\n */\n readonly handlers?: PopoverTriggerHandlers;\n /**\n * What a click does in library mode, after the command has been cancelled and `disabled`\n * checked. Default: toggle against `intended()` (`requestOpen` / `requestClose`). Menu's\n * SubmenuTrigger opens, or moves focus into an already open submenu, and never closes.\n */\n readonly activate?: (event: MouseEvent<HTMLElement>) => void;\n /**\n * How `disabled` is rendered. `\"native\"` (default): the `disabled` attribute, so the button is\n * no invoker at all. `\"aria\"`: `aria-disabled=\"true\"` and no `disabled` (a menu item stays\n * focusable); the click handler still refuses.\n */\n readonly disabledAs?: \"native\" | \"aria\";\n}\n\nexport interface PopoverTriggerHook {\n readonly state: PopoverTriggerHookState;\n /** Owned props (conventions 1.2) for `useRender`'s `libraryProps`. */\n readonly libraryProps: ComponentPropsWithRef<\"button\"> & DataAttributes;\n /** Registers the element as the Root's Trigger; pass as `useRender`'s `ref`. */\n readonly ref: (element: HTMLElement) => () => void;\n}\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\nfunction isPopoverOpen(element: Element | null): boolean {\n if (element === null) return false;\n try {\n return element.matches(\":popover-open\");\n } catch {\n return false;\n }\n}\n\n/**\n * The Trigger of a popover-like component (design/components/popover.md 14.3):\n * `button[commandfor][command=\"toggle-popover\"]` with `aria-expanded`. In JS-free mode the\n * browser runs the command and the Popup keeps `aria-expanded` current on the element. In library\n * mode the click is cancelled (so the command does not run) and toggles against what the user\n * already intends: an open through `requestOpen`, a library-owned close through `requestClose`.\n *\n * Menu adds `aria-haspopup=\"menu\"` and its own key handling on top.\n */\nexport function usePopoverTrigger<State>(\n ctx: PopoverRootValue,\n options: UsePopoverTriggerOptions<State>,\n): PopoverTriggerHook {\n const { part, disabled, render, handlers, activate, disabledAs = \"native\" } = options;\n const isLibrary = ctx.mode === \"library\";\n const { ids, triggerRef, popupRef } = ctx;\n\n const state: PopoverTriggerHookState = {\n open: ctx.optimistic,\n // Opens, part-initiated closes and dismissal notifications all belong to the Trigger (4.2).\n pending: ctx.pending,\n disabled,\n };\n\n const ref = useCallback(\n (element: HTMLElement) => {\n if (isDev && !isLibrary && element.tagName !== \"BUTTON\") {\n console.error(\n `${part}: rendered a <${element.tagName.toLowerCase()}> in JS-free mode. Only a ` +\n '<button> runs `command=\"toggle-popover\"`; this trigger will not open the popover. ' +\n \"Render a <button>, or put the component in library mode (`defaultOpen={false}`).\",\n );\n }\n triggerRef.current = element;\n // JS-free mode: the DOM owns `aria-expanded` (design 9). A Trigger that mounts after the\n // Popup (or is re-created) picks up the current state here.\n if (!isLibrary && popupRef.current !== null) {\n element.setAttribute(\"aria-expanded\", String(isPopoverOpen(popupRef.current)));\n }\n return () => {\n if (triggerRef.current === element) triggerRef.current = null;\n };\n },\n [isLibrary, part, triggerRef, popupRef],\n );\n\n const onClick = (event: MouseEvent<HTMLElement>) => {\n // Cancelling the click cancels the command (it runs as the button's activation behaviour,\n // only if the click was not cancelled). Enter and Space synthesise the same click.\n event.preventDefault();\n if (disabled) return;\n if (activate !== undefined) {\n activate(event);\n return;\n }\n const native = event.nativeEvent;\n if (ctx.intended()) ctx.requestClose(native, \"activate\");\n else ctx.requestOpen(native, \"activate\");\n };\n\n // The component's handlers first, then the Trigger's (design/components/menu.md 14). The click\n // handler is only owned when installed, so a render element keeps its own `onClick` in JS-free\n // mode.\n const merged: PopoverTriggerHandlers = { ...handlers };\n if (isLibrary) {\n const extra = handlers?.onClick;\n merged.onClick =\n extra === undefined\n ? onClick\n : (event) => {\n extra(event);\n onClick(event);\n };\n }\n const ariaDisabled = disabledAs === \"aria\";\n\n const libraryProps: ComponentPropsWithRef<\"button\"> & DataAttributes = {\n // `type` only applies to a <button>; on any other element the key is not owned.\n ...(rendersButton(render) && { type: \"button\" as const }),\n // Owned: a render element must not re-enable a disabled Trigger (in JS-free mode the browser\n // would run its command). With `disabledAs: \"aria\"` it is owned as absent.\n disabled: ariaDisabled ? undefined : disabled,\n ...(ariaDisabled && { \"aria-disabled\": disabled ? true : undefined }),\n commandfor: ids.popup,\n command: \"toggle-popover\",\n // Library mode: optimistic. JS-free mode: \"false\" in React; the Popup's `toggle` listener\n // writes the DOM value, and React never rewrites an unchanged prop.\n \"aria-expanded\": isLibrary ? state.open : false,\n ...merged,\n \"data-open\": dataAttr(state.open),\n \"data-pending\": dataAttr(state.pending),\n \"data-disabled\": dataAttr((isLibrary || ariaDisabled) && disabled),\n };\n\n return { state, libraryProps, ref };\n}\n"],"mappings":";;;;;AAuDA,MAAM,QAAQ,QAAQ,IAAI,aAAa;AAEvC,SAAS,cAAc,SAAkC;CACvD,IAAI,YAAY,MAAM,OAAO;CAC7B,IAAI;EACF,OAAO,QAAQ,QAAQ,eAAe;CACxC,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;;AAWA,SAAgB,kBACd,KACA,SACoB;CACpB,MAAM,EAAE,MAAM,UAAU,QAAQ,UAAU,UAAU,aAAa,aAAa;CAC9E,MAAM,YAAY,IAAI,SAAS;CAC/B,MAAM,EAAE,KAAK,YAAY,aAAa;CAEtC,MAAM,QAAiC;EACrC,MAAM,IAAI;EAEV,SAAS,IAAI;EACb;CACF;CAEA,MAAM,MAAM,aACT,YAAyB;EACxB,IAAI,SAAS,CAAC,aAAa,QAAQ,YAAY,UAC7C,QAAQ,MACN,GAAG,KAAK,gBAAgB,QAAQ,QAAQ,YAAY,EAAE,iMAGxD;EAEF,WAAW,UAAU;EAGrB,IAAI,CAAC,aAAa,SAAS,YAAY,MACrC,QAAQ,aAAa,iBAAiB,OAAO,cAAc,SAAS,OAAO,CAAC,CAAC;EAE/E,aAAa;GACX,IAAI,WAAW,YAAY,SAAS,WAAW,UAAU;EAC3D;CACF,GACA;EAAC;EAAW;EAAM;EAAY;CAAQ,CACxC;CAEA,MAAM,WAAW,UAAmC;EAGlD,MAAM,eAAe;EACrB,IAAI,UAAU;EACd,IAAI,aAAa,KAAA,GAAW;GAC1B,SAAS,KAAK;GACd;EACF;EACA,MAAM,SAAS,MAAM;EACrB,IAAI,IAAI,SAAS,GAAG,IAAI,aAAa,QAAQ,UAAU;OAClD,IAAI,YAAY,QAAQ,UAAU;CACzC;CAKA,MAAM,SAAiC,EAAE,GAAG,SAAS;CACrD,IAAI,WAAW;EACb,MAAM,QAAQ,UAAU;EACxB,OAAO,UACL,UAAU,KAAA,IACN,WACC,UAAU;GACT,MAAM,KAAK;GACX,QAAQ,KAAK;EACf;CACR;CACA,MAAM,eAAe,eAAe;CAoBpC,OAAO;EAAE;EAAO,cAAA;GAhBd,GAAI,cAAc,MAAM,KAAK,EAAE,MAAM,SAAkB;GAGvD,UAAU,eAAe,KAAA,IAAY;GACrC,GAAI,gBAAgB,EAAE,iBAAiB,WAAW,OAAO,KAAA,EAAU;GACnE,YAAY,IAAI;GAChB,SAAS;GAGT,iBAAiB,YAAY,MAAM,OAAO;GAC1C,GAAG;GACH,aAAa,SAAS,MAAM,IAAI;GAChC,gBAAgB,SAAS,MAAM,OAAO;GACtC,iBAAiB,UAAU,aAAa,iBAAiB,QAAQ;EAGxC;EAAG;CAAI;AACpC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import "../internal/platform-attributes.mjs";
|
|
2
|
+
import { MenuItem, MenuItemDetails, MenuItemProps, MenuItemState } from "./menu-item.mjs";
|
|
3
|
+
import { MenuCheckboxItem, MenuCheckboxItemProps, MenuCheckboxItemState } from "./menu-checkbox-item.mjs";
|
|
4
|
+
import { MenuGroup, MenuGroupLabel, MenuGroupLabelProps, MenuGroupLabelState, MenuGroupProps, MenuGroupState, MenuSeparator, MenuSeparatorProps, MenuSeparatorState } from "./menu-group.mjs";
|
|
5
|
+
import { MenuAlign, MenuPopup, MenuPopupProps, MenuPopupState, MenuSide } from "./menu-popup.mjs";
|
|
6
|
+
import { MenuRadioGroup, MenuRadioGroupProps, MenuRadioGroupState } from "./menu-radio-group.mjs";
|
|
7
|
+
import { MenuRadioItem, MenuRadioItemProps, MenuRadioItemState } from "./menu-radio-item.mjs";
|
|
8
|
+
import { MenuOpenChangeDetails, MenuRoot, MenuRootProps, MenuRootState } from "./menu-root.mjs";
|
|
9
|
+
import { MenuTrigger, MenuTriggerProps, MenuTriggerState } from "./menu-trigger.mjs";
|
|
10
|
+
import { MenuSubmenuTrigger, MenuSubmenuTriggerProps, MenuSubmenuTriggerState } from "./menu-submenu-trigger.mjs";
|
|
11
|
+
//#region src/menu/index.d.ts
|
|
12
|
+
/**
|
|
13
|
+
* Menu button (WAI-ARIA APG Menu Button + Menu) on `popover="auto"`: `Root` (no DOM) > `Trigger`
|
|
14
|
+
* (`button[aria-haspopup=menu]`) + `Popup` (`div[popover=auto][role=menu]`) > `Item`,
|
|
15
|
+
* `CheckboxItem`, `RadioGroup` > `RadioItem`, `Group` > `GroupLabel`, `Separator`, and submenus
|
|
16
|
+
* (a `Root` inside a `Popup`, opened by its `SubmenuTrigger`). An item's `action` runs in its own
|
|
17
|
+
* transition with `data-pending` on the item, and the whole menu closes when it settles.
|
|
18
|
+
*/
|
|
19
|
+
export declare const Menu: {
|
|
20
|
+
readonly Root: typeof MenuRoot;
|
|
21
|
+
readonly Trigger: typeof MenuTrigger;
|
|
22
|
+
readonly Popup: typeof MenuPopup;
|
|
23
|
+
readonly Item: typeof MenuItem;
|
|
24
|
+
readonly CheckboxItem: typeof MenuCheckboxItem;
|
|
25
|
+
readonly RadioGroup: typeof MenuRadioGroup;
|
|
26
|
+
readonly RadioItem: typeof MenuRadioItem;
|
|
27
|
+
readonly Group: typeof MenuGroup;
|
|
28
|
+
readonly GroupLabel: typeof MenuGroupLabel;
|
|
29
|
+
readonly Separator: typeof MenuSeparator;
|
|
30
|
+
readonly SubmenuTrigger: typeof MenuSubmenuTrigger;
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
export type { MenuAlign, MenuCheckboxItemProps, MenuCheckboxItemState, MenuGroupLabelProps, MenuGroupLabelState, MenuGroupProps, MenuGroupState, MenuItemDetails, MenuItemProps, MenuItemState, MenuOpenChangeDetails, MenuPopupProps, MenuPopupState, MenuRadioGroupProps, MenuRadioGroupState, MenuRadioItemProps, MenuRadioItemState, MenuRootProps, MenuRootState, MenuSeparatorProps, MenuSeparatorState, MenuSide, MenuSubmenuTriggerProps, MenuSubmenuTriggerState, MenuTriggerProps, MenuTriggerState };
|
|
34
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/menu/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;qBAoBa;WACX,aAAI;WACJ,gBAAO;WACP,cAAK;WACL,aAAI;WACJ,qBAAY;WACZ,mBAAU;WACV,kBAAS;WACT,cAAK;WACL,mBAAU;WACV,kBAAS;WACT,uBAAc"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { MenuCheckboxItem } from "./menu-checkbox-item.mjs";
|
|
2
|
+
import { MenuGroup, MenuGroupLabel, MenuSeparator } from "./menu-group.mjs";
|
|
3
|
+
import { MenuItem } from "./menu-item.mjs";
|
|
4
|
+
import { MenuPopup } from "./menu-popup.mjs";
|
|
5
|
+
import { MenuRadioGroup } from "./menu-radio-group.mjs";
|
|
6
|
+
import { MenuRadioItem } from "./menu-radio-item.mjs";
|
|
7
|
+
import { MenuRoot } from "./menu-root.mjs";
|
|
8
|
+
import { MenuTrigger } from "./menu-trigger.mjs";
|
|
9
|
+
import { MenuSubmenuTrigger } from "./menu-submenu-trigger.mjs";
|
|
10
|
+
//#region src/menu/index.ts
|
|
11
|
+
/**
|
|
12
|
+
* Menu button (WAI-ARIA APG Menu Button + Menu) on `popover="auto"`: `Root` (no DOM) > `Trigger`
|
|
13
|
+
* (`button[aria-haspopup=menu]`) + `Popup` (`div[popover=auto][role=menu]`) > `Item`,
|
|
14
|
+
* `CheckboxItem`, `RadioGroup` > `RadioItem`, `Group` > `GroupLabel`, `Separator`, and submenus
|
|
15
|
+
* (a `Root` inside a `Popup`, opened by its `SubmenuTrigger`). An item's `action` runs in its own
|
|
16
|
+
* transition with `data-pending` on the item, and the whole menu closes when it settles.
|
|
17
|
+
*/
|
|
18
|
+
const Menu = {
|
|
19
|
+
Root: MenuRoot,
|
|
20
|
+
Trigger: MenuTrigger,
|
|
21
|
+
Popup: MenuPopup,
|
|
22
|
+
Item: MenuItem,
|
|
23
|
+
CheckboxItem: MenuCheckboxItem,
|
|
24
|
+
RadioGroup: MenuRadioGroup,
|
|
25
|
+
RadioItem: MenuRadioItem,
|
|
26
|
+
Group: MenuGroup,
|
|
27
|
+
GroupLabel: MenuGroupLabel,
|
|
28
|
+
Separator: MenuSeparator,
|
|
29
|
+
SubmenuTrigger: MenuSubmenuTrigger
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
export { Menu };
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/menu/index.ts"],"sourcesContent":["// Ships the platform attribute typings (`command`, `commandfor`, `focusgroup`) with this subpath\n// (conventions 7.4). Type-only: the built JS has no import of it.\nimport \"../internal/platform-attributes.js\";\nimport { MenuCheckboxItem } from \"./menu-checkbox-item.js\";\nimport { MenuGroup, MenuGroupLabel, MenuSeparator } from \"./menu-group.js\";\nimport { MenuItem } from \"./menu-item.js\";\nimport { MenuPopup } from \"./menu-popup.js\";\nimport { MenuRadioGroup } from \"./menu-radio-group.js\";\nimport { MenuRadioItem } from \"./menu-radio-item.js\";\nimport { MenuRoot } from \"./menu-root.js\";\nimport { MenuSubmenuTrigger } from \"./menu-submenu-trigger.js\";\nimport { MenuTrigger } from \"./menu-trigger.js\";\n\n/**\n * Menu button (WAI-ARIA APG Menu Button + Menu) on `popover=\"auto\"`: `Root` (no DOM) > `Trigger`\n * (`button[aria-haspopup=menu]`) + `Popup` (`div[popover=auto][role=menu]`) > `Item`,\n * `CheckboxItem`, `RadioGroup` > `RadioItem`, `Group` > `GroupLabel`, `Separator`, and submenus\n * (a `Root` inside a `Popup`, opened by its `SubmenuTrigger`). An item's `action` runs in its own\n * transition with `data-pending` on the item, and the whole menu closes when it settles.\n */\nexport const Menu = {\n Root: MenuRoot,\n Trigger: MenuTrigger,\n Popup: MenuPopup,\n Item: MenuItem,\n CheckboxItem: MenuCheckboxItem,\n RadioGroup: MenuRadioGroup,\n RadioItem: MenuRadioItem,\n Group: MenuGroup,\n GroupLabel: MenuGroupLabel,\n Separator: MenuSeparator,\n SubmenuTrigger: MenuSubmenuTrigger,\n} as const;\n\nexport type { MenuRootProps, MenuRootState, MenuOpenChangeDetails } from \"./menu-root.js\";\nexport type { MenuTriggerProps, MenuTriggerState } from \"./menu-trigger.js\";\nexport type { MenuPopupProps, MenuPopupState, MenuSide, MenuAlign } from \"./menu-popup.js\";\nexport type { MenuItemProps, MenuItemState, MenuItemDetails } from \"./menu-item.js\";\nexport type { MenuCheckboxItemProps, MenuCheckboxItemState } from \"./menu-checkbox-item.js\";\nexport type { MenuRadioGroupProps, MenuRadioGroupState } from \"./menu-radio-group.js\";\nexport type { MenuRadioItemProps, MenuRadioItemState } from \"./menu-radio-item.js\";\nexport type {\n MenuGroupProps,\n MenuGroupState,\n MenuGroupLabelProps,\n MenuGroupLabelState,\n MenuSeparatorProps,\n MenuSeparatorState,\n} from \"./menu-group.js\";\nexport type { MenuSubmenuTriggerProps, MenuSubmenuTriggerState } from \"./menu-submenu-trigger.js\";\n"],"mappings":";;;;;;;;;;;;;;;;;AAoBA,MAAa,OAAO;CAClB,MAAM;CACN,SAAS;CACT,OAAO;CACP,MAAM;CACN,cAAc;CACd,YAAY;CACZ,WAAW;CACX,OAAO;CACP,YAAY;CACZ,WAAW;CACX,gBAAgB;AAClB"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ChangeAction, RenderProp, StateValue } from "../internal/types.mjs";
|
|
2
|
+
import { MenuItemProps } from "./menu-item.mjs";
|
|
3
|
+
import { CSSProperties, ComponentPropsWithRef, ReactNode } from "react";
|
|
4
|
+
//#region src/menu/menu-checkbox-item.d.ts
|
|
5
|
+
export interface MenuCheckboxItemState {
|
|
6
|
+
/** Optimistic checked state (`aria-checked`). */
|
|
7
|
+
readonly checked: boolean;
|
|
8
|
+
/** `checkedChangeAction` is in flight. */
|
|
9
|
+
readonly pending: boolean;
|
|
10
|
+
readonly disabled: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface MenuCheckboxItemProps extends Omit<MenuItemProps, "action" | "closeOnActivate" | "className" | "style" | "children" | "render"> {
|
|
13
|
+
/** Controlled checked state. `undefined` means uncontrolled. */
|
|
14
|
+
checked?: boolean;
|
|
15
|
+
/** Initial checked state when uncontrolled. Default `false`. */
|
|
16
|
+
defaultChecked?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Awaited inside the item's transition; `aria-checked` flips at once and rolls back on
|
|
19
|
+
* rejection (which reaches the nearest Error Boundary above the item). May be a Server
|
|
20
|
+
* Function.
|
|
21
|
+
*/
|
|
22
|
+
checkedChangeAction?: ChangeAction<boolean>;
|
|
23
|
+
/** Close the whole menu once the change settles. Default `false`: the menu stays open. */
|
|
24
|
+
closeOnActivate?: boolean;
|
|
25
|
+
className?: StateValue<string | undefined, MenuCheckboxItemState>;
|
|
26
|
+
style?: StateValue<CSSProperties | undefined, MenuCheckboxItemState>;
|
|
27
|
+
children?: StateValue<ReactNode, MenuCheckboxItemState>;
|
|
28
|
+
render?: RenderProp<ComponentPropsWithRef<"button">, MenuCheckboxItemState>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A checkable menu item: `<button type="button" role="menuitemcheckbox" aria-checked>`. Click,
|
|
32
|
+
* Enter or Space toggles it through its own transition with optimistic `aria-checked` /
|
|
33
|
+
* `data-checked` and `data-pending`; the menu stays open unless `closeOnActivate`.
|
|
34
|
+
*
|
|
35
|
+
* Owned keys (conventions 1.2): those of `Menu.Item`, plus `aria-checked` and `data-checked`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function MenuCheckboxItem({ checked, defaultChecked, checkedChangeAction, closeOnActivate, disabled, label, className, style, children, render, ...rest }: MenuCheckboxItemProps): ReactNode;
|
|
38
|
+
export declare namespace MenuCheckboxItem {
|
|
39
|
+
var displayName: string;
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
//# sourceMappingURL=menu-checkbox-item.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menu-checkbox-item.d.mts","names":[],"sources":["../../src/menu/menu-checkbox-item.tsx"],"mappings":";;;;iBAaiB;;WAEN;;WAEA;WACA;;iBAGM,8BAA8B,KAC7C;;EAIA;;EAEA;;;;;;EAMA,sBAAsB;;EAEtB;EACA,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;EAC9C,WAAW,WAAW,WAAW;EACjC,SAAS,WAAW,iCAAiC;;;;;;;;;wBAUvC,mBACd,SACA,gBACA,qBACA,iBACA,UACA,OACA,WACA,OACA,UACA,WACG,QACF,wBAAqB;yBAZR"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createActionDetails } from "../internal/action-details.mjs";
|
|
3
|
+
import { dataAttr } from "../internal/data-attr.mjs";
|
|
4
|
+
import { transitionTypes } from "../internal/transition-types.mjs";
|
|
5
|
+
import { useControllableAction } from "../internal/use-controllable-action.mjs";
|
|
6
|
+
import { rendersButton, useRender } from "../internal/use-render.mjs";
|
|
7
|
+
import { closeStackLater, useMenuContext } from "./menu-context.mjs";
|
|
8
|
+
import { useMenuItem } from "./use-menu-item.mjs";
|
|
9
|
+
import { useRef } from "react";
|
|
10
|
+
//#region src/menu/menu-checkbox-item.tsx
|
|
11
|
+
/**
|
|
12
|
+
* A checkable menu item: `<button type="button" role="menuitemcheckbox" aria-checked>`. Click,
|
|
13
|
+
* Enter or Space toggles it through its own transition with optimistic `aria-checked` /
|
|
14
|
+
* `data-checked` and `data-pending`; the menu stays open unless `closeOnActivate`.
|
|
15
|
+
*
|
|
16
|
+
* Owned keys (conventions 1.2): those of `Menu.Item`, plus `aria-checked` and `data-checked`.
|
|
17
|
+
*/
|
|
18
|
+
function MenuCheckboxItem({ checked, defaultChecked, checkedChangeAction, closeOnActivate = false, disabled = false, label, className, style, children, render, ...rest }) {
|
|
19
|
+
const menu = useMenuContext("Menu.CheckboxItem");
|
|
20
|
+
const after = useRef(/* @__PURE__ */ new WeakMap());
|
|
21
|
+
const { optimistic, pending, dispatch } = useControllableAction({
|
|
22
|
+
controlled: checked,
|
|
23
|
+
defaultValue: defaultChecked,
|
|
24
|
+
initialValue: false,
|
|
25
|
+
action: async (next, details) => {
|
|
26
|
+
await checkedChangeAction?.(next, details);
|
|
27
|
+
after.current.get(details)?.();
|
|
28
|
+
},
|
|
29
|
+
transitionTypes: (next) => transitionTypes("menu", next ? "check" : "uncheck"),
|
|
30
|
+
warn: {
|
|
31
|
+
component: "Menu.CheckboxItem",
|
|
32
|
+
prop: "checked"
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
const { libraryProps } = useMenuItem({
|
|
36
|
+
part: "Menu.CheckboxItem",
|
|
37
|
+
role: "menuitemcheckbox",
|
|
38
|
+
disabled,
|
|
39
|
+
label,
|
|
40
|
+
activate: (event) => {
|
|
41
|
+
const details = createActionDetails(event, {});
|
|
42
|
+
if (closeOnActivate) after.current.set(details, closeStackLater(menu, event));
|
|
43
|
+
dispatch(!optimistic, details);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return useRender({
|
|
47
|
+
defaultElement: "button",
|
|
48
|
+
render,
|
|
49
|
+
state: {
|
|
50
|
+
checked: optimistic,
|
|
51
|
+
pending,
|
|
52
|
+
disabled
|
|
53
|
+
},
|
|
54
|
+
className,
|
|
55
|
+
style,
|
|
56
|
+
children,
|
|
57
|
+
props: rest,
|
|
58
|
+
libraryProps: {
|
|
59
|
+
...rendersButton(render) && { type: "button" },
|
|
60
|
+
...libraryProps,
|
|
61
|
+
"aria-checked": optimistic,
|
|
62
|
+
"data-checked": dataAttr(optimistic),
|
|
63
|
+
"data-pending": dataAttr(pending)
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
MenuCheckboxItem.displayName = "MenuCheckboxItem";
|
|
68
|
+
//#endregion
|
|
69
|
+
export { MenuCheckboxItem };
|
|
70
|
+
|
|
71
|
+
//# sourceMappingURL=menu-checkbox-item.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menu-checkbox-item.mjs","names":[],"sources":["../../src/menu/menu-checkbox-item.tsx"],"sourcesContent":["\"use client\";\nimport { useRef } from \"react\";\nimport type { ComponentPropsWithRef, CSSProperties, ReactNode } from \"react\";\nimport { createActionDetails } from \"../internal/action-details.js\";\nimport { dataAttr } from \"../internal/data-attr.js\";\nimport { transitionTypes } from \"../internal/transition-types.js\";\nimport type { ActionDetails, ChangeAction, RenderProp, StateValue } from \"../internal/types.js\";\nimport { useControllableAction } from \"../internal/use-controllable-action.js\";\nimport { rendersButton, useRender } from \"../internal/use-render.js\";\nimport { closeStackLater, useMenuContext } from \"./menu-context.js\";\nimport type { MenuItemProps } from \"./menu-item.js\";\nimport { useMenuItem } from \"./use-menu-item.js\";\n\nexport interface MenuCheckboxItemState {\n /** Optimistic checked state (`aria-checked`). */\n readonly checked: boolean;\n /** `checkedChangeAction` is in flight. */\n readonly pending: boolean;\n readonly disabled: boolean;\n}\n\nexport interface MenuCheckboxItemProps extends Omit<\n MenuItemProps,\n \"action\" | \"closeOnActivate\" | \"className\" | \"style\" | \"children\" | \"render\"\n> {\n /** Controlled checked state. `undefined` means uncontrolled. */\n checked?: boolean;\n /** Initial checked state when uncontrolled. Default `false`. */\n defaultChecked?: boolean;\n /**\n * Awaited inside the item's transition; `aria-checked` flips at once and rolls back on\n * rejection (which reaches the nearest Error Boundary above the item). May be a Server\n * Function.\n */\n checkedChangeAction?: ChangeAction<boolean>;\n /** Close the whole menu once the change settles. Default `false`: the menu stays open. */\n closeOnActivate?: boolean;\n className?: StateValue<string | undefined, MenuCheckboxItemState>;\n style?: StateValue<CSSProperties | undefined, MenuCheckboxItemState>;\n children?: StateValue<ReactNode, MenuCheckboxItemState>;\n render?: RenderProp<ComponentPropsWithRef<\"button\">, MenuCheckboxItemState>;\n}\n\n/**\n * A checkable menu item: `<button type=\"button\" role=\"menuitemcheckbox\" aria-checked>`. Click,\n * Enter or Space toggles it through its own transition with optimistic `aria-checked` /\n * `data-checked` and `data-pending`; the menu stays open unless `closeOnActivate`.\n *\n * Owned keys (conventions 1.2): those of `Menu.Item`, plus `aria-checked` and `data-checked`.\n */\nexport function MenuCheckboxItem({\n checked,\n defaultChecked,\n checkedChangeAction,\n closeOnActivate = false,\n disabled = false,\n label,\n className,\n style,\n children,\n render,\n ...rest\n}: MenuCheckboxItemProps) {\n const menu = useMenuContext(\"Menu.CheckboxItem\");\n // The close to run after a dispatch's action resolved, keyed by the dispatch's details.\n const after = useRef(new WeakMap<object, () => void>());\n const { optimistic, pending, dispatch } = useControllableAction<boolean>({\n controlled: checked,\n defaultValue: defaultChecked,\n initialValue: false,\n action: async (next, details) => {\n await checkedChangeAction?.(next, details);\n after.current.get(details)?.();\n },\n transitionTypes: (next) => transitionTypes(\"menu\", next ? \"check\" : \"uncheck\"),\n warn: { component: \"Menu.CheckboxItem\", prop: \"checked\" },\n });\n\n const { libraryProps } = useMenuItem({\n part: \"Menu.CheckboxItem\",\n role: \"menuitemcheckbox\",\n disabled,\n label,\n activate: (event) => {\n const details = createActionDetails(event, {}) as ActionDetails;\n if (closeOnActivate) after.current.set(details, closeStackLater(menu, event));\n dispatch(!optimistic, details);\n },\n });\n\n const state: MenuCheckboxItemState = { checked: optimistic, pending, disabled };\n\n return useRender<ComponentPropsWithRef<\"button\">, MenuCheckboxItemState>({\n defaultElement: \"button\",\n render,\n state,\n className,\n style,\n children,\n props: rest,\n libraryProps: {\n ...(rendersButton(render) && { type: \"button\" as const }),\n ...libraryProps,\n \"aria-checked\": optimistic,\n \"data-checked\": dataAttr(optimistic),\n \"data-pending\": dataAttr(pending),\n },\n });\n}\n\nMenuCheckboxItem.displayName = \"MenuCheckboxItem\";\n"],"mappings":";;;;;;;;;;;;;;;;;AAkDA,SAAgB,iBAAiB,EAC/B,SACA,gBACA,qBACA,kBAAkB,OAClB,WAAW,OACX,OACA,WACA,OACA,UACA,QACA,GAAG,QACqB;CACxB,MAAM,OAAO,eAAe,mBAAmB;CAE/C,MAAM,QAAQ,uBAAO,IAAI,QAA4B,CAAC;CACtD,MAAM,EAAE,YAAY,SAAS,aAAa,sBAA+B;EACvE,YAAY;EACZ,cAAc;EACd,cAAc;EACd,QAAQ,OAAO,MAAM,YAAY;GAC/B,MAAM,sBAAsB,MAAM,OAAO;GACzC,MAAM,QAAQ,IAAI,OAAO,CAAC,GAAG;EAC/B;EACA,kBAAkB,SAAS,gBAAgB,QAAQ,OAAO,UAAU,SAAS;EAC7E,MAAM;GAAE,WAAW;GAAqB,MAAM;EAAU;CAC1D,CAAC;CAED,MAAM,EAAE,iBAAiB,YAAY;EACnC,MAAM;EACN,MAAM;EACN;EACA;EACA,WAAW,UAAU;GACnB,MAAM,UAAU,oBAAoB,OAAO,CAAC,CAAC;GAC7C,IAAI,iBAAiB,MAAM,QAAQ,IAAI,SAAS,gBAAgB,MAAM,KAAK,CAAC;GAC5E,SAAS,CAAC,YAAY,OAAO;EAC/B;CACF,CAAC;CAID,OAAO,UAAkE;EACvE,gBAAgB;EAChB;EACA,OAAA;GALqC,SAAS;GAAY;GAAS;EAK/D;EACJ;EACA;EACA;EACA,OAAO;EACP,cAAc;GACZ,GAAI,cAAc,MAAM,KAAK,EAAE,MAAM,SAAkB;GACvD,GAAG;GACH,gBAAgB;GAChB,gBAAgB,SAAS,UAAU;GACnC,gBAAgB,SAAS,OAAO;EAClC;CACF,CAAC;AACH;AAEA,iBAAiB,cAAc"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "../internal/use-popover-root.mjs";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, use } from "react";
|
|
3
|
+
//#region src/menu/menu-context.ts
|
|
4
|
+
const MenuContext = createContext(null);
|
|
5
|
+
/**
|
|
6
|
+
* The close an activated item runs once its action settled: the whole stack, through the
|
|
7
|
+
* top-level Root (design 2.2). Recorded at activation: if the menu was dismissed and opened again
|
|
8
|
+
* meanwhile, the close belongs to the previous opening and does nothing (as `useActionTrigger`).
|
|
9
|
+
*/
|
|
10
|
+
function closeStackLater(menu, event) {
|
|
11
|
+
const { popover } = menu.root;
|
|
12
|
+
const generation = popover.openGeneration();
|
|
13
|
+
return () => {
|
|
14
|
+
if (popover.openGeneration() === generation) popover.requestClose(event, "activate");
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function useMenuContext(part) {
|
|
18
|
+
const context = use(MenuContext);
|
|
19
|
+
if (context === null) throw new Error(`${part} must be rendered inside a Menu.Root.`);
|
|
20
|
+
return context;
|
|
21
|
+
}
|
|
22
|
+
const MenuListContext = createContext(null);
|
|
23
|
+
const MenuGroupContext = createContext(null);
|
|
24
|
+
const MenuRadioGroupContext = createContext(null);
|
|
25
|
+
//#endregion
|
|
26
|
+
export { MenuContext, MenuGroupContext, MenuListContext, MenuRadioGroupContext, closeStackLater, useMenuContext };
|
|
27
|
+
|
|
28
|
+
//# sourceMappingURL=menu-context.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menu-context.mjs","names":[],"sources":["../../src/menu/menu-context.ts"],"sourcesContent":["\"use client\";\nimport { createContext, use } from \"react\";\nimport type { DelayedPair } from \"../internal/use-delayed-pair.js\";\nimport type { PopoverRootValue } from \"../internal/use-popover-root.js\";\n\n/**\n * Where focus goes when the menu opens (design/components/menu.md 6.1): the first enabled item,\n * the last one (ArrowUp on the Trigger), or nowhere (a hover-opened submenu keeps focus on its\n * SubmenuTrigger).\n */\nexport type MenuOpenCause = \"first\" | \"last\" | \"none\";\n\n/** How the open being shown should place focus (design 6.1; review H1). */\nexport interface MenuOpenFocus {\n readonly cause: MenuOpenCause;\n /**\n * The open came from a pointer click (`detail > 0`): the item is focused without the focus\n * ring where the engine supports `focus({ focusVisible: false })`.\n */\n readonly pointer: boolean;\n}\n\n/** One menu level: what its Root provides to its Trigger, Popup and items. */\nexport interface MenuContextValue {\n /** The shared popover machinery (`usePopoverRoot`). */\n readonly popover: PopoverRootValue;\n /** This Root is nested in a `Menu.Popup`. */\n readonly submenu: boolean;\n /** The top-level menu: item activation closes the whole stack through it (design 2.2). */\n readonly root: MenuContextValue;\n /** The enclosing menu of a submenu; `null` at the top level. */\n readonly parent: MenuContextValue | null;\n readonly loop: boolean;\n readonly delay: number;\n readonly closeDelay: number;\n /** The Trigger's id (`aria-labelledby` of the Popup). */\n readonly triggerId: string;\n /** Record the focus target of the next open (and whether a pointer asked for it). */\n readonly setOpenCause: (cause: MenuOpenCause, pointer?: boolean) => void;\n /** Read and clear the focus target of the open being shown (default `\"first\"`, keyboard). */\n readonly takeOpenCause: () => MenuOpenFocus;\n /** Submenu hover-intent timers, shared by the SubmenuTrigger and the Popup. */\n readonly timers: DelayedPair;\n}\n\nexport const MenuContext = createContext<MenuContextValue | null>(null);\n\n/**\n * The close an activated item runs once its action settled: the whole stack, through the\n * top-level Root (design 2.2). Recorded at activation: if the menu was dismissed and opened again\n * meanwhile, the close belongs to the previous opening and does nothing (as `useActionTrigger`).\n */\nexport function closeStackLater(menu: MenuContextValue, event: Event): () => void {\n const { popover } = menu.root;\n const generation = popover.openGeneration();\n return () => {\n if (popover.openGeneration() === generation) popover.requestClose(event, \"activate\");\n };\n}\n\nexport function useMenuContext(part: string): MenuContextValue {\n const context = use(MenuContext);\n if (context === null) {\n throw new Error(`${part} must be rendered inside a Menu.Root.`);\n }\n return context;\n}\n\n/** What a `Menu.Popup` tells the items rendered in it. */\nexport interface MenuListContextValue {\n /** The menu this popup belongs to. */\n readonly menu: MenuContextValue;\n /**\n * `experimental_focusgroup` is on and the engine implements it: items render no `tabindex`\n * (Chromium leaves `tabindex=\"-1\"` elements out of a focusgroup) and the JS arrow keys step\n * aside.\n */\n readonly nativeFocusgroup: boolean;\n /** A typeahead buffer is active: Space is a character, not an activation (design 5.1). */\n readonly typingAhead: () => boolean;\n}\n\nexport const MenuListContext = createContext<MenuListContextValue | null>(null);\n\n/** `Menu.Group` / `Menu.RadioGroup`: the id their `Menu.GroupLabel` renders. */\nexport interface MenuGroupContextValue {\n readonly labelId: string;\n}\n\nexport const MenuGroupContext = createContext<MenuGroupContextValue | null>(null);\n\n/** `Menu.RadioGroup`'s state for its `Menu.RadioItem`s. */\nexport interface MenuRadioGroupContextValue {\n /** Optimistic value. */\n readonly value: string | null;\n readonly pending: boolean;\n /**\n * Dispatch `value` through the group's transition; `then` runs once the change action\n * resolved (an item's `closeOnActivate`).\n */\n readonly select: (value: string, event: Event, then: (() => void) | undefined) => void;\n}\n\nexport const MenuRadioGroupContext = createContext<MenuRadioGroupContextValue | null>(null);\n"],"mappings":";;;AA6CA,MAAa,cAAc,cAAuC,IAAI;;;;;;AAOtE,SAAgB,gBAAgB,MAAwB,OAA0B;CAChF,MAAM,EAAE,YAAY,KAAK;CACzB,MAAM,aAAa,QAAQ,eAAe;CAC1C,aAAa;EACX,IAAI,QAAQ,eAAe,MAAM,YAAY,QAAQ,aAAa,OAAO,UAAU;CACrF;AACF;AAEA,SAAgB,eAAe,MAAgC;CAC7D,MAAM,UAAU,IAAI,WAAW;CAC/B,IAAI,YAAY,MACd,MAAM,IAAI,MAAM,GAAG,KAAK,sCAAsC;CAEhE,OAAO;AACT;AAgBA,MAAa,kBAAkB,cAA2C,IAAI;AAO9E,MAAa,mBAAmB,cAA4C,IAAI;AAchF,MAAa,wBAAwB,cAAiD,IAAI"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
//#region src/menu/menu-dom.ts
|
|
3
|
+
const ITEM_SELECTOR = "[role=\"menuitem\"], [role=\"menuitemcheckbox\"], [role=\"menuitemradio\"]";
|
|
4
|
+
/** The items of `popup` in DOM order, without the items of its submenus. Disabled included. */
|
|
5
|
+
function menuItems(popup) {
|
|
6
|
+
return Array.from(popup.querySelectorAll(ITEM_SELECTOR)).filter((item) => item.closest("[role=\"menu\"]") === popup);
|
|
7
|
+
}
|
|
8
|
+
function isMenuItem(element, popup) {
|
|
9
|
+
return element.matches(ITEM_SELECTOR) && element.closest("[role=\"menu\"]") === popup;
|
|
10
|
+
}
|
|
11
|
+
function isDisabledItem(item) {
|
|
12
|
+
return item.getAttribute("aria-disabled") === "true";
|
|
13
|
+
}
|
|
14
|
+
/** The first (or last) enabled item; the first (or last) item if all are disabled. */
|
|
15
|
+
function edgeItem(popup, which) {
|
|
16
|
+
const items = menuItems(popup);
|
|
17
|
+
const ordered = which === "first" ? items : [...items].reverse();
|
|
18
|
+
return ordered.find((item) => !isDisabledItem(item)) ?? ordered[0] ?? null;
|
|
19
|
+
}
|
|
20
|
+
/** Keyboard focus: the ring shows even where the engine's heuristic would not (as Tabs). */
|
|
21
|
+
function focusItem(item) {
|
|
22
|
+
item.focus({ focusVisible: true });
|
|
23
|
+
}
|
|
24
|
+
/** The text typeahead matches: the item's `label` prop, else its text content. */
|
|
25
|
+
function typeaheadText(item) {
|
|
26
|
+
return (item.getAttribute("data-typeahead") ?? item.textContent ?? "").trim().toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
function isRtl(element) {
|
|
29
|
+
return getComputedStyle(element).direction === "rtl";
|
|
30
|
+
}
|
|
31
|
+
function hasModifier(event) {
|
|
32
|
+
return event.altKey || event.ctrlKey || event.metaKey;
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
export { edgeItem, focusItem, hasModifier, isDisabledItem, isMenuItem, isRtl, menuItems, typeaheadText };
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=menu-dom.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menu-dom.mjs","names":[],"sources":["../../src/menu/menu-dom.ts"],"sourcesContent":["\"use client\";\n// DOM helpers shared by the Menu parts. Items are read from the DOM when they are needed (a key\n// press, an open), never registered (design/components/menu.md 13 and 14).\n\nconst ITEM_SELECTOR = '[role=\"menuitem\"], [role=\"menuitemcheckbox\"], [role=\"menuitemradio\"]';\n\n/** The items of `popup` in DOM order, without the items of its submenus. Disabled included. */\nexport function menuItems(popup: Element): HTMLElement[] {\n return Array.from(popup.querySelectorAll<HTMLElement>(ITEM_SELECTOR)).filter(\n (item) => item.closest('[role=\"menu\"]') === popup,\n );\n}\n\nexport function isMenuItem(element: Element, popup: Element): boolean {\n return element.matches(ITEM_SELECTOR) && element.closest('[role=\"menu\"]') === popup;\n}\n\nexport function isDisabledItem(item: Element): boolean {\n return item.getAttribute(\"aria-disabled\") === \"true\";\n}\n\n/** The first (or last) enabled item; the first (or last) item if all are disabled. */\nexport function edgeItem(popup: Element, which: \"first\" | \"last\"): HTMLElement | null {\n const items = menuItems(popup);\n const ordered = which === \"first\" ? items : [...items].reverse();\n return ordered.find((item) => !isDisabledItem(item)) ?? ordered[0] ?? null;\n}\n\n/** Keyboard focus: the ring shows even where the engine's heuristic would not (as Tabs). */\nexport function focusItem(item: HTMLElement): void {\n item.focus({ focusVisible: true } as FocusOptions);\n}\n\n/** The text typeahead matches: the item's `label` prop, else its text content. */\nexport function typeaheadText(item: Element): string {\n return (item.getAttribute(\"data-typeahead\") ?? item.textContent ?? \"\").trim().toLowerCase();\n}\n\nexport function isRtl(element: Element): boolean {\n return getComputedStyle(element).direction === \"rtl\";\n}\n\nexport function hasModifier(event: { altKey: boolean; ctrlKey: boolean; metaKey: boolean }) {\n return event.altKey || event.ctrlKey || event.metaKey;\n}\n"],"mappings":";;AAIA,MAAM,gBAAgB;;AAGtB,SAAgB,UAAU,OAA+B;CACvD,OAAO,MAAM,KAAK,MAAM,iBAA8B,aAAa,CAAC,CAAC,CAAC,QACnE,SAAS,KAAK,QAAQ,iBAAe,MAAM,KAC9C;AACF;AAEA,SAAgB,WAAW,SAAkB,OAAyB;CACpE,OAAO,QAAQ,QAAQ,aAAa,KAAK,QAAQ,QAAQ,iBAAe,MAAM;AAChF;AAEA,SAAgB,eAAe,MAAwB;CACrD,OAAO,KAAK,aAAa,eAAe,MAAM;AAChD;;AAGA,SAAgB,SAAS,OAAgB,OAA6C;CACpF,MAAM,QAAQ,UAAU,KAAK;CAC7B,MAAM,UAAU,UAAU,UAAU,QAAQ,CAAC,GAAG,KAAK,CAAC,CAAC,QAAQ;CAC/D,OAAO,QAAQ,MAAM,SAAS,CAAC,eAAe,IAAI,CAAC,KAAK,QAAQ,MAAM;AACxE;;AAGA,SAAgB,UAAU,MAAyB;CACjD,KAAK,MAAM,EAAE,cAAc,KAAK,CAAiB;AACnD;;AAGA,SAAgB,cAAc,MAAuB;CACnD,QAAQ,KAAK,aAAa,gBAAgB,KAAK,KAAK,eAAe,GAAA,CAAI,KAAK,CAAC,CAAC,YAAY;AAC5F;AAEA,SAAgB,MAAM,SAA2B;CAC/C,OAAO,iBAAiB,OAAO,CAAC,CAAC,cAAc;AACjD;AAEA,SAAgB,YAAY,OAAgE;CAC1F,OAAO,MAAM,UAAU,MAAM,WAAW,MAAM;AAChD"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { RenderProp, StateValue } from "../internal/types.mjs";
|
|
2
|
+
import { CSSProperties, ComponentPropsWithRef, ReactNode } from "react";
|
|
3
|
+
//#region src/menu/menu-group.d.ts
|
|
4
|
+
/** `Menu.Group`, `Menu.GroupLabel` and `Menu.Separator` have no state. */
|
|
5
|
+
export type MenuGroupState = Record<string, never>;
|
|
6
|
+
export type MenuGroupLabelState = Record<string, never>;
|
|
7
|
+
export type MenuSeparatorState = Record<string, never>;
|
|
8
|
+
export interface MenuGroupProps extends Omit<ComponentPropsWithRef<"div">, "role" | "className" | "style" | "children"> {
|
|
9
|
+
className?: StateValue<string | undefined, MenuGroupState>;
|
|
10
|
+
style?: StateValue<CSSProperties | undefined, MenuGroupState>;
|
|
11
|
+
children?: StateValue<ReactNode, MenuGroupState>;
|
|
12
|
+
render?: RenderProp<ComponentPropsWithRef<"div">, MenuGroupState>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A labelled set of items: `<div role="group" aria-labelledby>` named by the `Menu.GroupLabel`
|
|
16
|
+
* inside it. Owned keys: `role`, `aria-labelledby` (token-merged).
|
|
17
|
+
*/
|
|
18
|
+
export declare function MenuGroup({ className, style, children, render, ...rest }: MenuGroupProps): import("react").JSX.Element;
|
|
19
|
+
export declare namespace MenuGroup {
|
|
20
|
+
var displayName: string;
|
|
21
|
+
}
|
|
22
|
+
export interface MenuGroupLabelProps extends Omit<ComponentPropsWithRef<"div">, "id" | "className" | "style" | "children"> {
|
|
23
|
+
className?: StateValue<string | undefined, MenuGroupLabelState>;
|
|
24
|
+
style?: StateValue<CSSProperties | undefined, MenuGroupLabelState>;
|
|
25
|
+
children?: StateValue<ReactNode, MenuGroupLabelState>;
|
|
26
|
+
render?: RenderProp<ComponentPropsWithRef<"div">, MenuGroupLabelState>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The name of the enclosing `Menu.Group` or `Menu.RadioGroup`: a `<div>` with the id the group's
|
|
30
|
+
* `aria-labelledby` references. Owned key: `id`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function MenuGroupLabel({ className, style, children, render, ...rest }: MenuGroupLabelProps): ReactNode;
|
|
33
|
+
export declare namespace MenuGroupLabel {
|
|
34
|
+
var displayName: string;
|
|
35
|
+
}
|
|
36
|
+
export interface MenuSeparatorProps extends Omit<ComponentPropsWithRef<"hr">, "className" | "style" | "children"> {
|
|
37
|
+
className?: StateValue<string | undefined, MenuSeparatorState>;
|
|
38
|
+
style?: StateValue<CSSProperties | undefined, MenuSeparatorState>;
|
|
39
|
+
/** Another element must carry `role="separator"` itself. */
|
|
40
|
+
render?: RenderProp<ComponentPropsWithRef<"hr">, MenuSeparatorState>;
|
|
41
|
+
}
|
|
42
|
+
/** A divider between items: `<hr>` (implicit `role="separator"`). Owns no keys. */
|
|
43
|
+
export declare function MenuSeparator({ className, style, render, ...rest }: MenuSeparatorProps): ReactNode;
|
|
44
|
+
export declare namespace MenuSeparator {
|
|
45
|
+
var displayName: string;
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
//# sourceMappingURL=menu-group.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menu-group.d.mts","names":[],"sources":["../../src/menu/menu-group.tsx"],"mappings":";;;;YASY,iBAAiB;YACjB,sBAAsB;YACtB,qBAAqB;iBAIhB,uBAAuB,KACtC;EAGA,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;EAC9C,WAAW,WAAW,WAAW;EACjC,SAAS,WAAW,8BAA8B;;;;;;wBAOpC,YAAY,WAAW,OAAO,UAAU,WAAW,QAAQ,iCAAc,IAAA;yBAAzE;;;iBAiBC,4BAA4B,KAC3C;EAGA,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;EAC9C,WAAW,WAAW,WAAW;EACjC,SAAS,WAAW,8BAA8B;;;;;;wBAOpC,iBACd,WACA,OACA,UACA,WACG,QACF,sBAAmB;yBANN;;;iBA0BC,2BAA2B,KAC1C;EAGA,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;;EAE9C,SAAS,WAAW,6BAA6B;;;wBAInC,gBAAgB,WAAW,OAAO,WAAW,QAAQ,qBAAkB;yBAAvE"}
|