@funstack/ui 0.1.0-alpha.2 → 0.1.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/dist/accordion/accordion-context.mjs.map +1 -1
  2. package/dist/accordion/accordion-item.mjs +18 -4
  3. package/dist/accordion/accordion-item.mjs.map +1 -1
  4. package/dist/accordion/accordion-panel.mjs +2 -3
  5. package/dist/accordion/accordion-panel.mjs.map +1 -1
  6. package/dist/accordion/accordion-root.d.mts.map +1 -1
  7. package/dist/accordion/accordion-root.mjs +32 -14
  8. package/dist/accordion/accordion-root.mjs.map +1 -1
  9. package/dist/accordion/accordion-trigger.mjs +4 -5
  10. package/dist/accordion/accordion-trigger.mjs.map +1 -1
  11. package/dist/checkbox/checkbox.d.mts +5 -3
  12. package/dist/checkbox/checkbox.d.mts.map +1 -1
  13. package/dist/checkbox/checkbox.mjs +25 -9
  14. package/dist/checkbox/checkbox.mjs.map +1 -1
  15. package/dist/checkbox-group/checkbox-group-context.mjs.map +1 -1
  16. package/dist/checkbox-group/checkbox-group-item.d.mts +2 -1
  17. package/dist/checkbox-group/checkbox-group-item.d.mts.map +1 -1
  18. package/dist/checkbox-group/checkbox-group-item.mjs +3 -1
  19. package/dist/checkbox-group/checkbox-group-item.mjs.map +1 -1
  20. package/dist/checkbox-group/checkbox-group-root.d.mts +5 -2
  21. package/dist/checkbox-group/checkbox-group-root.d.mts.map +1 -1
  22. package/dist/checkbox-group/checkbox-group-root.mjs +24 -5
  23. package/dist/checkbox-group/checkbox-group-root.mjs.map +1 -1
  24. package/dist/combobox/combobox-context.mjs.map +1 -1
  25. package/dist/combobox/combobox-input.d.mts +3 -1
  26. package/dist/combobox/combobox-input.d.mts.map +1 -1
  27. package/dist/combobox/combobox-input.mjs +16 -2
  28. package/dist/combobox/combobox-input.mjs.map +1 -1
  29. package/dist/combobox/combobox-list.mjs +1 -1
  30. package/dist/combobox/combobox-list.mjs.map +1 -1
  31. package/dist/combobox/combobox-root.d.mts.map +1 -1
  32. package/dist/combobox/combobox-root.mjs +19 -4
  33. package/dist/combobox/combobox-root.mjs.map +1 -1
  34. package/dist/dialog/dialog-close.d.mts +17 -10
  35. package/dist/dialog/dialog-close.d.mts.map +1 -1
  36. package/dist/dialog/dialog-close.mjs +31 -20
  37. package/dist/dialog/dialog-close.mjs.map +1 -1
  38. package/dist/dialog/dialog-context.mjs.map +1 -1
  39. package/dist/dialog/dialog-form.d.mts +12 -5
  40. package/dist/dialog/dialog-form.d.mts.map +1 -1
  41. package/dist/dialog/dialog-form.mjs +16 -28
  42. package/dist/dialog/dialog-form.mjs.map +1 -1
  43. package/dist/dialog/dialog-popup.d.mts +10 -4
  44. package/dist/dialog/dialog-popup.d.mts.map +1 -1
  45. package/dist/dialog/dialog-popup.mjs +106 -41
  46. package/dist/dialog/dialog-popup.mjs.map +1 -1
  47. package/dist/dialog/dialog-root.d.mts +14 -8
  48. package/dist/dialog/dialog-root.d.mts.map +1 -1
  49. package/dist/dialog/dialog-root.mjs +66 -11
  50. package/dist/dialog/dialog-root.mjs.map +1 -1
  51. package/dist/dialog/dialog-trigger.d.mts +3 -2
  52. package/dist/dialog/dialog-trigger.d.mts.map +1 -1
  53. package/dist/dialog/dialog-trigger.mjs.map +1 -1
  54. package/dist/dialog/index.d.mts +4 -2
  55. package/dist/dialog/index.d.mts.map +1 -1
  56. package/dist/dialog/index.mjs +4 -2
  57. package/dist/dialog/index.mjs.map +1 -1
  58. package/dist/field/field-control.d.mts +33 -0
  59. package/dist/field/field-control.d.mts.map +1 -0
  60. package/dist/field/field-control.mjs +68 -0
  61. package/dist/field/field-control.mjs.map +1 -0
  62. package/dist/field/field-description.d.mts +24 -0
  63. package/dist/field/field-description.d.mts.map +1 -0
  64. package/dist/field/field-description.mjs +34 -0
  65. package/dist/field/field-description.mjs.map +1 -0
  66. package/dist/field/field-error.d.mts +30 -0
  67. package/dist/field/field-error.d.mts.map +1 -0
  68. package/dist/field/field-error.mjs +42 -0
  69. package/dist/field/field-error.mjs.map +1 -0
  70. package/dist/field/field-label.d.mts +25 -0
  71. package/dist/field/field-label.d.mts.map +1 -0
  72. package/dist/field/field-label.mjs +46 -0
  73. package/dist/field/field-label.mjs.map +1 -0
  74. package/dist/field/field-root.d.mts +53 -0
  75. package/dist/field/field-root.d.mts.map +1 -0
  76. package/dist/field/field-root.mjs +317 -0
  77. package/dist/field/field-root.mjs.map +1 -0
  78. package/dist/field/index.d.mts +18 -0
  79. package/dist/field/index.d.mts.map +1 -0
  80. package/dist/field/index.mjs +17 -0
  81. package/dist/field/index.mjs.map +1 -0
  82. package/dist/form/form-root.d.mts +58 -0
  83. package/dist/form/form-root.d.mts.map +1 -0
  84. package/dist/form/form-root.mjs +146 -0
  85. package/dist/form/form-root.mjs.map +1 -0
  86. package/dist/form/form-submit.d.mts +31 -0
  87. package/dist/form/form-submit.d.mts.map +1 -0
  88. package/dist/form/form-submit.mjs +52 -0
  89. package/dist/form/form-submit.mjs.map +1 -0
  90. package/dist/form/index.d.mts +11 -0
  91. package/dist/form/index.d.mts.map +1 -0
  92. package/dist/form/index.mjs +11 -0
  93. package/dist/form/index.mjs.map +1 -0
  94. package/dist/index.d.mts +27 -1
  95. package/dist/index.mjs +6 -1
  96. package/dist/internal/content-focus-probe.mjs +42 -0
  97. package/dist/internal/content-focus-probe.mjs.map +1 -0
  98. package/dist/internal/field-context.d.mts +19 -0
  99. package/dist/internal/field-context.d.mts.map +1 -0
  100. package/dist/internal/field-context.mjs +14 -0
  101. package/dist/internal/field-context.mjs.map +1 -0
  102. package/dist/internal/focus-first-invalid.mjs +67 -0
  103. package/dist/internal/focus-first-invalid.mjs.map +1 -0
  104. package/dist/internal/form-status-watcher.mjs +31 -0
  105. package/dist/internal/form-status-watcher.mjs.map +1 -0
  106. package/dist/internal/modal-registry.mjs +39 -0
  107. package/dist/internal/modal-registry.mjs.map +1 -0
  108. package/dist/internal/types.d.mts +3 -1
  109. package/dist/internal/types.d.mts.map +1 -1
  110. package/dist/internal/use-controllable-action.mjs +33 -15
  111. package/dist/internal/use-controllable-action.mjs.map +1 -1
  112. package/dist/internal/use-field-control.mjs +107 -0
  113. package/dist/internal/use-field-control.mjs.map +1 -0
  114. package/dist/internal/use-form-owner-event.mjs +44 -0
  115. package/dist/internal/use-form-owner-event.mjs.map +1 -0
  116. package/dist/internal/use-form-reset.mjs +10 -33
  117. package/dist/internal/use-form-reset.mjs.map +1 -1
  118. package/dist/internal/use-latest.mjs +32 -0
  119. package/dist/internal/use-latest.mjs.map +1 -0
  120. package/dist/internal/use-modal-registration.mjs +47 -0
  121. package/dist/internal/use-modal-registration.mjs.map +1 -0
  122. package/dist/internal/use-native-group-reconciliation.mjs +6 -5
  123. package/dist/internal/use-native-group-reconciliation.mjs.map +1 -1
  124. package/dist/internal/use-native-select-value.mjs +7 -5
  125. package/dist/internal/use-native-select-value.mjs.map +1 -1
  126. package/dist/internal/use-popover-popup.mjs +8 -22
  127. package/dist/internal/use-popover-popup.mjs.map +1 -1
  128. package/dist/internal/use-popover-root.mjs +58 -23
  129. package/dist/internal/use-popover-root.mjs.map +1 -1
  130. package/dist/internal/validity.d.mts +24 -0
  131. package/dist/internal/validity.d.mts.map +1 -0
  132. package/dist/internal/validity.mjs +108 -0
  133. package/dist/internal/validity.mjs.map +1 -0
  134. package/dist/menu/menu-group.d.mts.map +1 -1
  135. package/dist/menu/menu-group.mjs +3 -2
  136. package/dist/menu/menu-group.mjs.map +1 -1
  137. package/dist/menu/menu-radio-group.mjs +9 -4
  138. package/dist/menu/menu-radio-group.mjs.map +1 -1
  139. package/dist/menu/menu-root.mjs.map +1 -1
  140. package/dist/radio-group/radio-group-root.d.mts +4 -2
  141. package/dist/radio-group/radio-group-root.d.mts.map +1 -1
  142. package/dist/radio-group/radio-group-root.mjs +34 -8
  143. package/dist/radio-group/radio-group-root.mjs.map +1 -1
  144. package/dist/select/select-group.d.mts.map +1 -1
  145. package/dist/select/select-group.mjs +6 -4
  146. package/dist/select/select-group.mjs.map +1 -1
  147. package/dist/select/select-root.d.mts +4 -2
  148. package/dist/select/select-root.d.mts.map +1 -1
  149. package/dist/select/select-root.mjs +26 -6
  150. package/dist/select/select-root.mjs.map +1 -1
  151. package/dist/slider/index.d.mts +32 -0
  152. package/dist/slider/index.d.mts.map +1 -0
  153. package/dist/slider/index.mjs +27 -0
  154. package/dist/slider/index.mjs.map +1 -0
  155. package/dist/slider/slider-arithmetic.mjs +120 -0
  156. package/dist/slider/slider-arithmetic.mjs.map +1 -0
  157. package/dist/slider/slider-context.mjs +13 -0
  158. package/dist/slider/slider-context.mjs.map +1 -0
  159. package/dist/slider/slider-indicator.d.mts +22 -0
  160. package/dist/slider/slider-indicator.d.mts.map +1 -0
  161. package/dist/slider/slider-indicator.mjs +36 -0
  162. package/dist/slider/slider-indicator.mjs.map +1 -0
  163. package/dist/slider/slider-label.d.mts +23 -0
  164. package/dist/slider/slider-label.d.mts.map +1 -0
  165. package/dist/slider/slider-label.mjs +44 -0
  166. package/dist/slider/slider-label.mjs.map +1 -0
  167. package/dist/slider/slider-root.d.mts +111 -0
  168. package/dist/slider/slider-root.d.mts.map +1 -0
  169. package/dist/slider/slider-root.mjs +582 -0
  170. package/dist/slider/slider-root.mjs.map +1 -0
  171. package/dist/slider/slider-thumb.d.mts +58 -0
  172. package/dist/slider/slider-thumb.d.mts.map +1 -0
  173. package/dist/slider/slider-thumb.mjs +145 -0
  174. package/dist/slider/slider-thumb.mjs.map +1 -0
  175. package/dist/slider/slider-track.d.mts +29 -0
  176. package/dist/slider/slider-track.d.mts.map +1 -0
  177. package/dist/slider/slider-track.mjs +54 -0
  178. package/dist/slider/slider-track.mjs.map +1 -0
  179. package/dist/slider/slider-value.d.mts +34 -0
  180. package/dist/slider/slider-value.d.mts.map +1 -0
  181. package/dist/slider/slider-value.mjs +38 -0
  182. package/dist/slider/slider-value.mjs.map +1 -0
  183. package/dist/switch/switch.d.mts +5 -3
  184. package/dist/switch/switch.d.mts.map +1 -1
  185. package/dist/switch/switch.mjs +24 -8
  186. package/dist/switch/switch.mjs.map +1 -1
  187. package/dist/tabs/tabs-root.mjs +13 -3
  188. package/dist/tabs/tabs-root.mjs.map +1 -1
  189. package/dist/toast/index.d.mts +25 -0
  190. package/dist/toast/index.d.mts.map +1 -0
  191. package/dist/toast/index.mjs +26 -0
  192. package/dist/toast/index.mjs.map +1 -0
  193. package/dist/toast/toast-action.d.mts +39 -0
  194. package/dist/toast/toast-action.d.mts.map +1 -0
  195. package/dist/toast/toast-action.mjs +64 -0
  196. package/dist/toast/toast-action.mjs.map +1 -0
  197. package/dist/toast/toast-close.d.mts +25 -0
  198. package/dist/toast/toast-close.d.mts.map +1 -0
  199. package/dist/toast/toast-close.mjs +46 -0
  200. package/dist/toast/toast-close.mjs.map +1 -0
  201. package/dist/toast/toast-context.mjs +19 -0
  202. package/dist/toast/toast-context.mjs.map +1 -0
  203. package/dist/toast/toast-description.d.mts +24 -0
  204. package/dist/toast/toast-description.d.mts.map +1 -0
  205. package/dist/toast/toast-description.mjs +35 -0
  206. package/dist/toast/toast-description.mjs.map +1 -0
  207. package/dist/toast/toast-root.d.mts +43 -0
  208. package/dist/toast/toast-root.d.mts.map +1 -0
  209. package/dist/toast/toast-root.mjs +204 -0
  210. package/dist/toast/toast-root.mjs.map +1 -0
  211. package/dist/toast/toast-title.d.mts +24 -0
  212. package/dist/toast/toast-title.d.mts.map +1 -0
  213. package/dist/toast/toast-title.mjs +33 -0
  214. package/dist/toast/toast-title.mjs.map +1 -0
  215. package/dist/toast/toast-viewport.d.mts +52 -0
  216. package/dist/toast/toast-viewport.d.mts.map +1 -0
  217. package/dist/toast/toast-viewport.mjs +621 -0
  218. package/dist/toast/toast-viewport.mjs.map +1 -0
  219. package/dist/toast/toast.d.mts +119 -0
  220. package/dist/toast/toast.d.mts.map +1 -0
  221. package/dist/toast/toast.mjs +153 -0
  222. package/dist/toast/toast.mjs.map +1 -0
  223. package/package.json +5 -1
@@ -1,6 +1,7 @@
1
1
  "use client";
2
+ import { useContentFocusProbe } from "./content-focus-probe.mjs";
2
3
  import { exitAnimations, useExitAnimation } from "./use-exit-animation.mjs";
3
- import { createElement, useCallback, useEffect, useEffectEvent, useInsertionEffect, useLayoutEffect, useRef, useState } from "react";
4
+ import { useCallback, useEffect, useEffectEvent, useInsertionEffect, useLayoutEffect, useRef, useState } from "react";
4
5
  //#region src/internal/use-popover-popup.ts
5
6
  const isDev = process.env.NODE_ENV !== "production";
6
7
  function isPopoverOpen(element) {
@@ -42,15 +43,6 @@ function insideHiddenPopover(element) {
42
43
  return false;
43
44
  }
44
45
  /**
45
- * Records, from its layout-effect cleanup, whether focus is inside the popup when the content
46
- * goes away: React runs a deleted component's layout cleanups before it detaches the host nodes,
47
- * so `document.activeElement` still names the focused element there.
48
- */
49
- function ContentFocusProbe({ onDetach, children }) {
50
- useLayoutEffect(() => onDetach, [onDetach]);
51
- return children;
52
- }
53
- /**
54
46
  * Everything in a popover-like Popup except rendering (design/components/popover.md 14.2): mount
55
47
  * reconciliation, the apply effect, the native `beforetoggle` / `toggle` / `command` listeners,
56
48
  * close intents, the focus-return fallback, the JS-free `aria-expanded` sync, the deferred
@@ -64,7 +56,6 @@ function usePopoverPopup(ctx, options) {
64
56
  const opening = useRef(false);
65
57
  const awaitingClose = useRef(false);
66
58
  const focusWasInside = useRef(false);
67
- const contentHadFocus = useRef(false);
68
59
  const warnedFocus = useRef(false);
69
60
  const exit = useExitAnimation();
70
61
  const [prevCommitted, setPrevCommitted] = useState(ctx.committed);
@@ -72,13 +63,8 @@ function usePopoverPopup(ctx, options) {
72
63
  setPrevCommitted(ctx.committed);
73
64
  if (isLibrary && prevCommitted && transition === void 0) exit.holdDuringRender();
74
65
  }
75
- const recordContentFocus = useCallback(() => {
76
- const element = popupRef.current;
77
- if (element === null) return;
78
- const active = element.ownerDocument.activeElement;
79
- if (active !== null && active !== element && element.contains(active)) contentHadFocus.current = true;
80
- }, [popupRef]);
81
- const wrapContent = useCallback((children) => createElement(ContentFocusProbe, { onDetach: recordContentFocus }, children), [recordContentFocus]);
66
+ const contentProbe = useContentFocusProbe(popupRef);
67
+ const { wrapContent } = contentProbe;
82
68
  const syncExpanded = (element) => {
83
69
  triggerRef.current?.setAttribute("aria-expanded", String(isPopoverOpen(element)));
84
70
  };
@@ -153,7 +139,7 @@ function usePopoverPopup(ctx, options) {
153
139
  if (focusWasInside.current) returnFocus(element);
154
140
  }
155
141
  }
156
- if (ctx.committed && isPopoverOpen(element)) contentHadFocus.current = false;
142
+ if (ctx.committed && isPopoverOpen(element)) contentProbe.forget();
157
143
  if (exit.held && !ctx.committed && !exit.waiting() && !awaitingClose.current && !isPopoverOpen(element)) exit.release();
158
144
  });
159
145
  const onBeforeToggle = (event) => {
@@ -165,12 +151,12 @@ function usePopoverPopup(ctx, options) {
165
151
  event.preventDefault();
166
152
  ctx.requestOpen(event, "activate");
167
153
  } else ctx.nextOpenGeneration();
168
- contentHadFocus.current = false;
154
+ contentProbe.forget();
169
155
  } else {
170
156
  awaitingClose.current = true;
171
157
  const active = element.ownerDocument.activeElement;
172
- focusWasInside.current = active !== null && element.contains(active) || contentHadFocus.current;
173
- contentHadFocus.current = false;
158
+ focusWasInside.current = active !== null && element.contains(active) || contentProbe.hadFocus();
159
+ contentProbe.forget();
174
160
  }
175
161
  onBeforeToggleOption?.(toggle);
176
162
  };
@@ -1 +1 @@
1
- {"version":3,"file":"use-popover-popup.mjs","names":[],"sources":["../../src/internal/use-popover-popup.ts"],"sourcesContent":["\"use client\";\nimport {\n createElement,\n useCallback,\n useEffect,\n useEffectEvent,\n useInsertionEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from \"react\";\nimport type { ReactNode, RefObject, ViewTransitionClass } from \"react\";\nimport { exitAnimations, useExitAnimation } from \"./use-exit-animation.js\";\nimport type { PopoverRootValue } from \"./use-popover-root.js\";\n\nexport interface UsePopoverPopupOptions {\n /** Public name of the part for development warnings (`\"Popover.Popup\"`). */\n readonly part: string;\n /** Element to focus on open: a ref, or a selector resolved inside the popup (design 6.1). */\n readonly initialFocus: RefObject<HTMLElement | null> | string | undefined;\n /**\n * Replaces `initialFocus` with a function called right before each library open (Menu: the\n * first or last item by open cause). `null` leaves the choice to the platform. A\n * `QuietFocusTarget` asks for focus without the focus ring (a pointer-opened menu).\n */\n readonly resolveInitialFocus?: (popup: HTMLElement) => HTMLElement | QuietFocusTarget | null;\n readonly keepMounted: boolean | undefined;\n /** The consumer's `transition`; decides how a library-driven close keeps its content (4.4). */\n readonly transition: ViewTransitionClass | undefined;\n /** Runs after the library's own `beforetoggle` handling (Menu: focus bookkeeping). */\n readonly onBeforeToggle?: (event: ToggleEvent) => void;\n /** Runs after the library's own `toggle` handling (Menu: close the stack on Tab-out). */\n readonly onToggle?: (event: ToggleEvent) => void;\n}\n\n/**\n * An initial focus target opened by a pointer: where the engine reads `focus({ focusVisible })`,\n * the library focuses it with `focusVisible: false` right after `showPopover()` instead of\n * writing `autofocus` (WebKit shows the ring for an `autofocus` target after a click; review H1).\n * Elsewhere it is an ordinary target.\n */\nexport interface QuietFocusTarget {\n readonly element: HTMLElement;\n readonly focusVisible: false;\n}\n\nexport interface PopoverPopupHook {\n /** Callback ref for the popup element; pass as `useRender`'s `ref`. */\n readonly setElement: (element: HTMLElement) => () => void;\n /**\n * Whether the children are shown: committed open, or held while the hide animates (4.4).\n * Library mode only; JS-free mode always renders them.\n */\n readonly visible: boolean;\n /**\n * Wraps the library-mode content: call it only while `visible` (`visible ? wrapContent(x) :\n * null`), or inside the `<Activity>` for `keepMounted`. When a close commit unmounts the\n * content while it holds focus (a library-driven close with `transition`, where the focused\n * element is gone before `hidePopover()` runs and the platform restores nothing), the hide\n * that follows returns focus to the Trigger (design 6.2).\n */\n readonly wrapContent: (children: ReactNode) => ReactNode;\n}\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\nfunction isPopoverOpen(element: Element): boolean {\n try {\n return element.matches(\":popover-open\");\n } catch {\n return false;\n }\n}\n\nlet focusVisibleOption: boolean | undefined;\n\n/**\n * The engine reads the `focusVisible` member of `focus()`'s options (Chromium 145, Firefox 104,\n * Safari 18.4). Detected once through a getter: the bindings convert the dictionary before the\n * method runs, so a detached element is enough and nothing is focused.\n */\nfunction supportsFocusVisibleOption(doc: Document): boolean {\n if (focusVisibleOption === undefined) {\n let read = false;\n try {\n doc.createElement(\"div\").focus({\n get focusVisible() {\n read = true;\n return false;\n },\n } as FocusOptions);\n } catch {\n read = false;\n }\n focusVisibleOption = read;\n }\n return focusVisibleOption;\n}\n\n/** Resolves `initialFocus` against the popup (a ref, or a selector inside the popup). */\nfunction resolveInitialFocus(\n popup: HTMLElement,\n initialFocus: RefObject<HTMLElement | null> | string,\n): HTMLElement | null {\n if (typeof initialFocus === \"string\") return popup.querySelector<HTMLElement>(initialFocus);\n return initialFocus.current;\n}\n\n/** Whether `element` sits inside a popover that is not shown (an outer popup that closed). */\nfunction insideHiddenPopover(element: Element): boolean {\n for (let node = element.parentElement; node !== null; node = node.parentElement) {\n if (node.hasAttribute(\"popover\") && !isPopoverOpen(node)) return true;\n }\n return false;\n}\n\n/**\n * Records, from its layout-effect cleanup, whether focus is inside the popup when the content\n * goes away: React runs a deleted component's layout cleanups before it detaches the host nodes,\n * so `document.activeElement` still names the focused element there.\n */\nfunction ContentFocusProbe({\n onDetach,\n children,\n}: {\n onDetach: () => void;\n children?: ReactNode;\n}): ReactNode {\n useLayoutEffect(() => onDetach, [onDetach]);\n return children;\n}\n\n/**\n * Everything in a popover-like Popup except rendering (design/components/popover.md 14.2): mount\n * reconciliation, the apply effect, the native `beforetoggle` / `toggle` / `command` listeners,\n * close intents, the focus-return fallback, the JS-free `aria-expanded` sync, the deferred\n * unmount while a hide animates, and `initialFocus`. Shared by Popover and Menu.\n */\nexport function usePopoverPopup(\n ctx: PopoverRootValue,\n options: UsePopoverPopupOptions,\n): PopoverPopupHook {\n const { part, initialFocus, transition, onBeforeToggle: onBeforeToggleOption } = options;\n const onToggleOption = options.onToggle;\n const isLibrary = ctx.mode === \"library\";\n const { popupRef, triggerRef } = ctx;\n\n // The opening `beforetoggle` lets this one through: the library's own `showPopover()`.\n const opening = useRef(false);\n // Set by the synchronous `beforetoggle` of a close, cleared by `toggle` a task later: the\n // popup is hidden but its `toggle` has not arrived, so the apply effect must not re-open it\n // from a commit that lands in between (design 2.5).\n const awaitingClose = useRef(false);\n // Whether focus was inside the popup when its last close began (design 6.2 rule 1).\n const focusWasInside = useRef(false);\n // Whether the content unmounted while holding focus since the popup was last shown (H1).\n const contentHadFocus = useRef(false);\n const warnedFocus = useRef(false);\n\n // --- `held`: children stay mounted while the hide animates (design 4.4) -------------------\n\n const exit = useExitAnimation();\n\n // A library-driven close commits `false` first and hides in the layout effect, so without\n // `transition` the children are held through the close commit (\"previous value in state\").\n // With `transition` the close commit unmounts them inside the view transition, whose old\n // snapshot carries the content out.\n const [prevCommitted, setPrevCommitted] = useState(ctx.committed);\n if (prevCommitted !== ctx.committed) {\n setPrevCommitted(ctx.committed);\n if (isLibrary && prevCommitted && transition === undefined) exit.holdDuringRender();\n }\n\n const recordContentFocus = useCallback(() => {\n const element = popupRef.current;\n if (element === null) return;\n const active = element.ownerDocument.activeElement;\n if (active !== null && active !== element && element.contains(active)) {\n contentHadFocus.current = true;\n }\n }, [popupRef]);\n\n const wrapContent = useCallback(\n (children: ReactNode): ReactNode =>\n createElement(ContentFocusProbe, { onDetach: recordContentFocus }, children),\n [recordContentFocus],\n );\n\n const syncExpanded = (element: HTMLElement): void => {\n // JS-free mode only (design 9): read the DOM, not `newState`, so a stale task cannot lie.\n triggerRef.current?.setAttribute(\"aria-expanded\", String(isPopoverOpen(element)));\n };\n\n /**\n * Design 6.2 rule 2, for a close that began with focus inside: when focus is now on `<body>`\n * or still inside the hidden popup, focus the Trigger (connected, not inside a hidden\n * popover), else blur what holds focus.\n */\n const returnFocus = (element: HTMLElement): void => {\n const doc = element.ownerDocument;\n const active = doc.activeElement;\n if (active !== null && active !== doc.body && !element.contains(active)) return;\n const trigger = triggerRef.current;\n if (trigger !== null && trigger.isConnected && !insideHiddenPopover(trigger)) {\n trigger.focus();\n } else if (active instanceof HTMLElement && active !== doc.body) {\n active.blur();\n }\n };\n\n // --- Opening ------------------------------------------------------------------------------\n\n const resolveFocusTarget = (element: HTMLElement): HTMLElement | QuietFocusTarget | null => {\n if (options.resolveInitialFocus !== undefined) {\n const resolved = options.resolveInitialFocus(element);\n if (resolved === null) return null;\n const target = resolved instanceof HTMLElement ? resolved : resolved.element;\n return element.contains(target) ? resolved : null;\n }\n if (initialFocus === undefined) return null;\n const target = resolveInitialFocus(element, initialFocus);\n if (target !== null && element.contains(target)) return target;\n if (isDev && !warnedFocus.current) {\n warnedFocus.current = true;\n console.error(\n target === null\n ? `${part}: \\`initialFocus\\` resolved to no element; the platform picks the focus target.`\n : `${part}: \\`initialFocus\\` is outside the popup; the platform picks the focus target.`,\n );\n }\n return null;\n };\n\n const showPopup = (element: HTMLElement): void => {\n const resolved = resolveFocusTarget(element);\n const target =\n resolved === null || resolved instanceof HTMLElement ? resolved : resolved.element;\n // A pointer open asks for no focus ring: `focus({ focusVisible: false })` after the show,\n // where the engine reads that option (QuietFocusTarget).\n const quiet =\n target !== null &&\n !(resolved instanceof HTMLElement) &&\n supportsFocusVisibleOption(element.ownerDocument);\n // Otherwise the popover focusing steps honour `autofocus` (and run the dialog focusing steps\n // for a <dialog>, in every engine); React drops `autoFocus` on client-rendered content.\n // Never `.focus()` (6.1) except for the quiet target above.\n const added = target !== null && !quiet && !target.hasAttribute(\"autofocus\");\n if (added) target.setAttribute(\"autofocus\", \"\");\n opening.current = true;\n try {\n const source = triggerRef.current;\n element.showPopover(source !== null && source.isConnected ? { source } : undefined);\n } finally {\n opening.current = false;\n if (added) target.removeAttribute(\"autofocus\");\n }\n if (quiet && isPopoverOpen(element) && element.contains(target)) {\n target.focus({ focusVisible: false } as FocusOptions);\n }\n };\n\n // Mount reconciliation (design 2.4): a click before hydration opened the popover natively.\n // Runs before the apply effect; the dispatch makes the apply effect wait for it.\n const reconcileOnMount = useEffectEvent(() => {\n const element = popupRef.current;\n if (element === null) return;\n if (!isLibrary) {\n syncExpanded(element);\n return;\n }\n if (isPopoverOpen(element)) ctx.requestOpen(undefined, \"platform\");\n });\n useLayoutEffect(() => {\n reconcileOnMount();\n }, []);\n\n // The apply effect (design 2.5): after every commit, once nothing is in flight, the DOM\n // follows committed state. One effect covers the normal open, `defaultOpen`, a controlled open\n // or close, a library-driven close after `openChangeAction(false)` settled, the re-open of a\n // refused dismissal, and the close after a refused or failed pre-hydration open.\n useLayoutEffect(() => {\n if (!isLibrary) return;\n const element = popupRef.current;\n if (element === null || !element.isConnected) return;\n if (!ctx.pending && ctx.latestInFlight() === null) {\n const shown = isPopoverOpen(element);\n if (ctx.committed && !shown) {\n if (!awaitingClose.current) showPopup(element);\n } else if (!ctx.committed && shown) {\n // An echo: the `toggle` listener finds `false` intended and skips the dispatch.\n ctx.recordCloseIntent(undefined);\n element.hidePopover();\n // The close commit may have unmounted the focused content before the hide (H1): the\n // platform restores focus only from inside the popover, so do it now.\n if (focusWasInside.current) returnFocus(element);\n }\n }\n // Content that unmounted while the popup stays open (not a close): forget it.\n if (ctx.committed && isPopoverOpen(element)) contentHadFocus.current = false;\n // A hold the close commit set (or a dismissal's notification commit) that no hide animation\n // is going to release: the popup is already hidden and its `toggle` has been handled.\n if (\n exit.held &&\n !ctx.committed &&\n !exit.waiting() &&\n !awaitingClose.current &&\n !isPopoverOpen(element)\n ) {\n exit.release();\n }\n });\n\n // --- Native listeners (conventions 7.4; filtered on target: React 19.3.1 emulates bubbling\n // of `toggle`, and a nested popover's events reach this element through the real DOM too) ---\n\n const onBeforeToggle = (event: Event): void => {\n const element = popupRef.current;\n if (element === null || event.target !== element) return;\n const toggle = event as ToggleEvent;\n if (toggle.newState === \"open\") {\n // Every open the library did not start itself is routed through the Root (design 2.2):\n // a foreign `show-popover` / `toggle-popover` invoker, a script `showPopover()`.\n if (isLibrary && !opening.current) {\n event.preventDefault();\n ctx.requestOpen(event, \"activate\");\n } else {\n // A Close action that resolves after this open must not close it (design 3.4).\n ctx.nextOpenGeneration();\n }\n contentHadFocus.current = false;\n } else {\n awaitingClose.current = true;\n const active = element.ownerDocument.activeElement;\n focusWasInside.current =\n (active !== null && element.contains(active)) || contentHadFocus.current;\n contentHadFocus.current = false;\n }\n onBeforeToggleOption?.(toggle);\n };\n\n const onToggle = (event: Event): void => {\n const element = popupRef.current;\n if (element === null || event.target !== element) return;\n awaitingClose.current = false;\n if (!isLibrary) syncExpanded(element);\n\n if (!isPopoverOpen(element)) {\n // Focus return for what the platform leaves out (design 6.2): a nested popover restores\n // nothing, and an opener that is gone leaves focus inside the hidden popup.\n const wasInside = focusWasInside.current;\n focusWasInside.current = false;\n if (wasInside) returnFocus(element);\n\n const intent = ctx.takeCloseIntent();\n if (isLibrary) {\n // `false` already intended: the library's own hide (or a dismissal during a pending\n // part-initiated close). With `transition` such a close needs no hold (4.4).\n const echo = !ctx.intended();\n if (!echo || transition === undefined) {\n const animations = exitAnimations(element);\n if (animations.length > 0) {\n // Before the notification, so its commit keeps the children (urgent, internal).\n exit.holdUntil(animations);\n } else {\n // Nothing animates (Firefox, or no CSS exit): release a hold now; the commit is\n // invisible, the popup is already `display: none`.\n exit.release();\n }\n }\n ctx.notifyClosed(intent?.event ?? event, intent?.reason ?? \"dismiss\");\n }\n }\n onToggleOption?.(event as ToggleEvent);\n };\n\n const onCommand = (event: Event): void => {\n const element = popupRef.current;\n if (element === null || event.target !== element || event.defaultPrevented) return;\n const command = String((event as Event & { command?: string }).command ?? \"\").toLowerCase();\n // A foreign invoker closing the popup (design 2.3). Opens are caught at `beforetoggle`.\n if (command === \"hide-popover\" || (command === \"toggle-popover\" && isPopoverOpen(element))) {\n ctx.recordCloseIntent({ reason: \"activate\", event });\n }\n };\n\n // The listeners the callback ref attaches call the latest render's handlers. An insertion\n // effect publishes them before refs attach and before any layout effect (whose\n // `hidePopover()` dispatches `beforetoggle` synchronously).\n const handlers = useRef({ onBeforeToggle, onToggle, onCommand });\n useInsertionEffect(() => {\n handlers.current = { onBeforeToggle, onToggle, onCommand };\n });\n\n // --- Element ------------------------------------------------------------------------------\n\n // The listeners attach in the callback ref, so they are in place from the commit that mounts\n // the element (no passive-effect window, no extra render). JS-free mode: there is no \"right\n // before showPopover()\" (the browser runs the command), so the `autofocus` attribute stays on\n // the `initialFocus` target while it is mounted (as Dialog).\n const setElement = useCallback(\n (element: HTMLElement) => {\n if (isDev && element.tagName !== \"DIALOG\" && element.tagName !== \"DIV\") {\n console.error(\n `${part}: expected to render a <dialog> or a <div> but got ` +\n `<${element.tagName.toLowerCase()}>.`,\n );\n }\n popupRef.current = element;\n const listeners: [string, EventListener][] = [\n [\"beforetoggle\", (event) => handlers.current.onBeforeToggle(event)],\n [\"toggle\", (event) => handlers.current.onToggle(event)],\n [\"command\", (event) => handlers.current.onCommand(event)],\n ];\n for (const [type, listener] of listeners) element.addEventListener(type, listener);\n\n let autofocused: HTMLElement | null = null;\n if (!isLibrary && initialFocus !== undefined) {\n const target = resolveInitialFocus(element, initialFocus);\n if (target !== null && element.contains(target) && !target.hasAttribute(\"autofocus\")) {\n target.setAttribute(\"autofocus\", \"\");\n autofocused = target;\n }\n }\n\n return () => {\n for (const [type, listener] of listeners) element.removeEventListener(type, listener);\n autofocused?.removeAttribute(\"autofocus\");\n if (popupRef.current === element) popupRef.current = null;\n };\n },\n [part, popupRef, isLibrary, initialFocus],\n );\n\n // Development check (design 9): `role=\"dialog\"` needs a name once the popup is (or can be)\n // shown.\n const shown = !isLibrary || ctx.committed;\n const warnedName = useRef(false);\n useEffect(() => {\n const element = popupRef.current;\n if (!isDev || element === null || !shown || warnedName.current) return;\n if (element.hasAttribute(\"aria-label\")) return;\n const labelledBy = (element.getAttribute(\"aria-labelledby\") ?? \"\").split(/\\s+/);\n const doc = element.ownerDocument;\n if (labelledBy.some((id) => id !== \"\" && doc.getElementById(id) !== null)) return;\n const role = element.getAttribute(\"role\");\n if (role !== \"dialog\" && role !== \"alertdialog\" && role !== \"menu\") return;\n warnedName.current = true;\n console.error(\n `${part}: the popup has no accessible name. Render a Title, or give the Popup an ` +\n \"`aria-label`.\",\n );\n }, [popupRef, shown, part]);\n\n return { setElement, visible: ctx.committed || exit.held, wrapContent };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-popover-popup.mjs","names":[],"sources":["../../src/internal/use-popover-popup.ts"],"sourcesContent":["\"use client\";\nimport {\n useCallback,\n useEffect,\n useEffectEvent,\n useInsertionEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from \"react\";\nimport type { ReactNode, RefObject, ViewTransitionClass } from \"react\";\nimport { useContentFocusProbe } from \"./content-focus-probe.js\";\nimport { exitAnimations, useExitAnimation } from \"./use-exit-animation.js\";\nimport type { PopoverRootValue } from \"./use-popover-root.js\";\n\nexport interface UsePopoverPopupOptions {\n /** Public name of the part for development warnings (`\"Popover.Popup\"`). */\n readonly part: string;\n /** Element to focus on open: a ref, or a selector resolved inside the popup (design 6.1). */\n readonly initialFocus: RefObject<HTMLElement | null> | string | undefined;\n /**\n * Replaces `initialFocus` with a function called right before each library open (Menu: the\n * first or last item by open cause). `null` leaves the choice to the platform. A\n * `QuietFocusTarget` asks for focus without the focus ring (a pointer-opened menu).\n */\n readonly resolveInitialFocus?: (popup: HTMLElement) => HTMLElement | QuietFocusTarget | null;\n readonly keepMounted: boolean | undefined;\n /** The consumer's `transition`; decides how a library-driven close keeps its content (4.4). */\n readonly transition: ViewTransitionClass | undefined;\n /** Runs after the library's own `beforetoggle` handling (Menu: focus bookkeeping). */\n readonly onBeforeToggle?: (event: ToggleEvent) => void;\n /** Runs after the library's own `toggle` handling (Menu: close the stack on Tab-out). */\n readonly onToggle?: (event: ToggleEvent) => void;\n}\n\n/**\n * An initial focus target opened by a pointer: where the engine reads `focus({ focusVisible })`,\n * the library focuses it with `focusVisible: false` right after `showPopover()` instead of\n * writing `autofocus` (WebKit shows the ring for an `autofocus` target after a click; review H1).\n * Elsewhere it is an ordinary target.\n */\nexport interface QuietFocusTarget {\n readonly element: HTMLElement;\n readonly focusVisible: false;\n}\n\nexport interface PopoverPopupHook {\n /** Callback ref for the popup element; pass as `useRender`'s `ref`. */\n readonly setElement: (element: HTMLElement) => () => void;\n /**\n * Whether the children are shown: committed open, or held while the hide animates (4.4).\n * Library mode only; JS-free mode always renders them.\n */\n readonly visible: boolean;\n /**\n * Wraps the library-mode content: call it only while `visible` (`visible ? wrapContent(x) :\n * null`), or inside the `<Activity>` for `keepMounted`. When a close commit unmounts the\n * content while it holds focus (a library-driven close with `transition`, where the focused\n * element is gone before `hidePopover()` runs and the platform restores nothing), the hide\n * that follows returns focus to the Trigger (design 6.2).\n */\n readonly wrapContent: (children: ReactNode) => ReactNode;\n}\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\nfunction isPopoverOpen(element: Element): boolean {\n try {\n return element.matches(\":popover-open\");\n } catch {\n return false;\n }\n}\n\nlet focusVisibleOption: boolean | undefined;\n\n/**\n * The engine reads the `focusVisible` member of `focus()`'s options (Chromium 145, Firefox 104,\n * Safari 18.4). Detected once through a getter: the bindings convert the dictionary before the\n * method runs, so a detached element is enough and nothing is focused.\n */\nfunction supportsFocusVisibleOption(doc: Document): boolean {\n if (focusVisibleOption === undefined) {\n let read = false;\n try {\n doc.createElement(\"div\").focus({\n get focusVisible() {\n read = true;\n return false;\n },\n } as FocusOptions);\n } catch {\n read = false;\n }\n focusVisibleOption = read;\n }\n return focusVisibleOption;\n}\n\n/** Resolves `initialFocus` against the popup (a ref, or a selector inside the popup). */\nfunction resolveInitialFocus(\n popup: HTMLElement,\n initialFocus: RefObject<HTMLElement | null> | string,\n): HTMLElement | null {\n if (typeof initialFocus === \"string\") return popup.querySelector<HTMLElement>(initialFocus);\n return initialFocus.current;\n}\n\n/** Whether `element` sits inside a popover that is not shown (an outer popup that closed). */\nfunction insideHiddenPopover(element: Element): boolean {\n for (let node = element.parentElement; node !== null; node = node.parentElement) {\n if (node.hasAttribute(\"popover\") && !isPopoverOpen(node)) return true;\n }\n return false;\n}\n\n/**\n * Everything in a popover-like Popup except rendering (design/components/popover.md 14.2): mount\n * reconciliation, the apply effect, the native `beforetoggle` / `toggle` / `command` listeners,\n * close intents, the focus-return fallback, the JS-free `aria-expanded` sync, the deferred\n * unmount while a hide animates, and `initialFocus`. Shared by Popover and Menu.\n */\nexport function usePopoverPopup(\n ctx: PopoverRootValue,\n options: UsePopoverPopupOptions,\n): PopoverPopupHook {\n const { part, initialFocus, transition, onBeforeToggle: onBeforeToggleOption } = options;\n const onToggleOption = options.onToggle;\n const isLibrary = ctx.mode === \"library\";\n const { popupRef, triggerRef } = ctx;\n\n // The opening `beforetoggle` lets this one through: the library's own `showPopover()`.\n const opening = useRef(false);\n // Set by the synchronous `beforetoggle` of a close, cleared by `toggle` a task later: the\n // popup is hidden but its `toggle` has not arrived, so the apply effect must not re-open it\n // from a commit that lands in between (design 2.5).\n const awaitingClose = useRef(false);\n // Whether focus was inside the popup when its last close began (design 6.2 rule 1).\n const focusWasInside = useRef(false);\n const warnedFocus = useRef(false);\n\n // --- `held`: children stay mounted while the hide animates (design 4.4) -------------------\n\n const exit = useExitAnimation();\n\n // A library-driven close commits `false` first and hides in the layout effect, so without\n // `transition` the children are held through the close commit (\"previous value in state\").\n // With `transition` the close commit unmounts them inside the view transition, whose old\n // snapshot carries the content out.\n const [prevCommitted, setPrevCommitted] = useState(ctx.committed);\n if (prevCommitted !== ctx.committed) {\n setPrevCommitted(ctx.committed);\n if (isLibrary && prevCommitted && transition === undefined) exit.holdDuringRender();\n }\n\n // Whether the content unmounted while holding focus since the popup was last shown (H1).\n const contentProbe = useContentFocusProbe(popupRef);\n const { wrapContent } = contentProbe;\n\n const syncExpanded = (element: HTMLElement): void => {\n // JS-free mode only (design 9): read the DOM, not `newState`, so a stale task cannot lie.\n triggerRef.current?.setAttribute(\"aria-expanded\", String(isPopoverOpen(element)));\n };\n\n /**\n * Design 6.2 rule 2, for a close that began with focus inside: when focus is now on `<body>`\n * or still inside the hidden popup, focus the Trigger (connected, not inside a hidden\n * popover), else blur what holds focus.\n */\n const returnFocus = (element: HTMLElement): void => {\n const doc = element.ownerDocument;\n const active = doc.activeElement;\n if (active !== null && active !== doc.body && !element.contains(active)) return;\n const trigger = triggerRef.current;\n if (trigger !== null && trigger.isConnected && !insideHiddenPopover(trigger)) {\n trigger.focus();\n } else if (active instanceof HTMLElement && active !== doc.body) {\n active.blur();\n }\n };\n\n // --- Opening ------------------------------------------------------------------------------\n\n const resolveFocusTarget = (element: HTMLElement): HTMLElement | QuietFocusTarget | null => {\n if (options.resolveInitialFocus !== undefined) {\n const resolved = options.resolveInitialFocus(element);\n if (resolved === null) return null;\n const target = resolved instanceof HTMLElement ? resolved : resolved.element;\n return element.contains(target) ? resolved : null;\n }\n if (initialFocus === undefined) return null;\n const target = resolveInitialFocus(element, initialFocus);\n if (target !== null && element.contains(target)) return target;\n if (isDev && !warnedFocus.current) {\n warnedFocus.current = true;\n console.error(\n target === null\n ? `${part}: \\`initialFocus\\` resolved to no element; the platform picks the focus target.`\n : `${part}: \\`initialFocus\\` is outside the popup; the platform picks the focus target.`,\n );\n }\n return null;\n };\n\n const showPopup = (element: HTMLElement): void => {\n const resolved = resolveFocusTarget(element);\n const target =\n resolved === null || resolved instanceof HTMLElement ? resolved : resolved.element;\n // A pointer open asks for no focus ring: `focus({ focusVisible: false })` after the show,\n // where the engine reads that option (QuietFocusTarget).\n const quiet =\n target !== null &&\n !(resolved instanceof HTMLElement) &&\n supportsFocusVisibleOption(element.ownerDocument);\n // Otherwise the popover focusing steps honour `autofocus` (and run the dialog focusing steps\n // for a <dialog>, in every engine); React drops `autoFocus` on client-rendered content.\n // Never `.focus()` (6.1) except for the quiet target above.\n const added = target !== null && !quiet && !target.hasAttribute(\"autofocus\");\n if (added) target.setAttribute(\"autofocus\", \"\");\n opening.current = true;\n try {\n const source = triggerRef.current;\n element.showPopover(source !== null && source.isConnected ? { source } : undefined);\n } finally {\n opening.current = false;\n if (added) target.removeAttribute(\"autofocus\");\n }\n if (quiet && isPopoverOpen(element) && element.contains(target)) {\n target.focus({ focusVisible: false } as FocusOptions);\n }\n };\n\n // Mount reconciliation (design 2.4): a click before hydration opened the popover natively.\n // Runs before the apply effect; the dispatch makes the apply effect wait for it.\n const reconcileOnMount = useEffectEvent(() => {\n const element = popupRef.current;\n if (element === null) return;\n if (!isLibrary) {\n syncExpanded(element);\n return;\n }\n if (isPopoverOpen(element)) ctx.requestOpen(undefined, \"platform\");\n });\n useLayoutEffect(() => {\n reconcileOnMount();\n }, []);\n\n // The apply effect (design 2.5): after every commit that re-renders the Popup, once nothing is\n // in flight, the DOM follows committed state. Since the Root's value is memoised (#48) an\n // unrelated Root render does not re-render the Popup, so this does not run for it; every path\n // that makes the DOM diverge from state goes through a dispatch, whose `pending` flips re-render\n // the Popup (review of #48, finding 4). Do not rely on an unrelated render to re-apply state.\n // One effect covers the normal open, `defaultOpen`, a controlled open\n // or close, a library-driven close after `openChangeAction(false)` settled, the re-open of a\n // refused dismissal, and the close after a refused or failed pre-hydration open.\n useLayoutEffect(() => {\n if (!isLibrary) return;\n const element = popupRef.current;\n if (element === null || !element.isConnected) return;\n if (!ctx.pending && ctx.latestInFlight() === null) {\n const shown = isPopoverOpen(element);\n if (ctx.committed && !shown) {\n if (!awaitingClose.current) showPopup(element);\n } else if (!ctx.committed && shown) {\n // An echo: the `toggle` listener finds `false` intended and skips the dispatch.\n ctx.recordCloseIntent(undefined);\n element.hidePopover();\n // The close commit may have unmounted the focused content before the hide (H1): the\n // platform restores focus only from inside the popover, so do it now.\n if (focusWasInside.current) returnFocus(element);\n }\n }\n // Content that unmounted while the popup stays open (not a close): forget it.\n if (ctx.committed && isPopoverOpen(element)) contentProbe.forget();\n // A hold the close commit set (or a dismissal's notification commit) that no hide animation\n // is going to release: the popup is already hidden and its `toggle` has been handled.\n if (\n exit.held &&\n !ctx.committed &&\n !exit.waiting() &&\n !awaitingClose.current &&\n !isPopoverOpen(element)\n ) {\n exit.release();\n }\n });\n\n // --- Native listeners (conventions 7.4; filtered on target: React 19.3.1 emulates bubbling\n // of `toggle`, and a nested popover's events reach this element through the real DOM too) ---\n\n const onBeforeToggle = (event: Event): void => {\n const element = popupRef.current;\n if (element === null || event.target !== element) return;\n const toggle = event as ToggleEvent;\n if (toggle.newState === \"open\") {\n // Every open the library did not start itself is routed through the Root (design 2.2):\n // a foreign `show-popover` / `toggle-popover` invoker, a script `showPopover()`.\n if (isLibrary && !opening.current) {\n event.preventDefault();\n ctx.requestOpen(event, \"activate\");\n } else {\n // A Close action that resolves after this open must not close it (design 3.4).\n ctx.nextOpenGeneration();\n }\n contentProbe.forget();\n } else {\n awaitingClose.current = true;\n const active = element.ownerDocument.activeElement;\n focusWasInside.current =\n (active !== null && element.contains(active)) || contentProbe.hadFocus();\n contentProbe.forget();\n }\n onBeforeToggleOption?.(toggle);\n };\n\n const onToggle = (event: Event): void => {\n const element = popupRef.current;\n if (element === null || event.target !== element) return;\n awaitingClose.current = false;\n if (!isLibrary) syncExpanded(element);\n\n if (!isPopoverOpen(element)) {\n // Focus return for what the platform leaves out (design 6.2): a nested popover restores\n // nothing, and an opener that is gone leaves focus inside the hidden popup.\n const wasInside = focusWasInside.current;\n focusWasInside.current = false;\n if (wasInside) returnFocus(element);\n\n const intent = ctx.takeCloseIntent();\n if (isLibrary) {\n // `false` already intended: the library's own hide (or a dismissal during a pending\n // part-initiated close). With `transition` such a close needs no hold (4.4).\n const echo = !ctx.intended();\n if (!echo || transition === undefined) {\n const animations = exitAnimations(element);\n if (animations.length > 0) {\n // Before the notification, so its commit keeps the children (urgent, internal).\n exit.holdUntil(animations);\n } else {\n // Nothing animates (Firefox, or no CSS exit): release a hold now; the commit is\n // invisible, the popup is already `display: none`.\n exit.release();\n }\n }\n ctx.notifyClosed(intent?.event ?? event, intent?.reason ?? \"dismiss\");\n }\n }\n onToggleOption?.(event as ToggleEvent);\n };\n\n const onCommand = (event: Event): void => {\n const element = popupRef.current;\n if (element === null || event.target !== element || event.defaultPrevented) return;\n const command = String((event as Event & { command?: string }).command ?? \"\").toLowerCase();\n // A foreign invoker closing the popup (design 2.3). Opens are caught at `beforetoggle`.\n if (command === \"hide-popover\" || (command === \"toggle-popover\" && isPopoverOpen(element))) {\n ctx.recordCloseIntent({ reason: \"activate\", event });\n }\n };\n\n // The listeners the callback ref attaches call the latest render's handlers. An insertion\n // effect publishes them before refs attach and before any layout effect (whose\n // `hidePopover()` dispatches `beforetoggle` synchronously).\n const handlers = useRef({ onBeforeToggle, onToggle, onCommand });\n useInsertionEffect(() => {\n handlers.current = { onBeforeToggle, onToggle, onCommand };\n });\n\n // --- Element ------------------------------------------------------------------------------\n\n // The listeners attach in the callback ref, so they are in place from the commit that mounts\n // the element (no passive-effect window, no extra render). JS-free mode: there is no \"right\n // before showPopover()\" (the browser runs the command), so the `autofocus` attribute stays on\n // the `initialFocus` target while it is mounted (as Dialog).\n const setElement = useCallback(\n (element: HTMLElement) => {\n if (isDev && element.tagName !== \"DIALOG\" && element.tagName !== \"DIV\") {\n console.error(\n `${part}: expected to render a <dialog> or a <div> but got ` +\n `<${element.tagName.toLowerCase()}>.`,\n );\n }\n popupRef.current = element;\n const listeners: [string, EventListener][] = [\n [\"beforetoggle\", (event) => handlers.current.onBeforeToggle(event)],\n [\"toggle\", (event) => handlers.current.onToggle(event)],\n [\"command\", (event) => handlers.current.onCommand(event)],\n ];\n for (const [type, listener] of listeners) element.addEventListener(type, listener);\n\n let autofocused: HTMLElement | null = null;\n if (!isLibrary && initialFocus !== undefined) {\n const target = resolveInitialFocus(element, initialFocus);\n if (target !== null && element.contains(target) && !target.hasAttribute(\"autofocus\")) {\n target.setAttribute(\"autofocus\", \"\");\n autofocused = target;\n }\n }\n\n return () => {\n for (const [type, listener] of listeners) element.removeEventListener(type, listener);\n autofocused?.removeAttribute(\"autofocus\");\n if (popupRef.current === element) popupRef.current = null;\n };\n },\n [part, popupRef, isLibrary, initialFocus],\n );\n\n // Development check (design 9): `role=\"dialog\"` needs a name once the popup is (or can be)\n // shown.\n const shown = !isLibrary || ctx.committed;\n const warnedName = useRef(false);\n useEffect(() => {\n const element = popupRef.current;\n if (!isDev || element === null || !shown || warnedName.current) return;\n if (element.hasAttribute(\"aria-label\")) return;\n const labelledBy = (element.getAttribute(\"aria-labelledby\") ?? \"\").split(/\\s+/);\n const doc = element.ownerDocument;\n if (labelledBy.some((id) => id !== \"\" && doc.getElementById(id) !== null)) return;\n const role = element.getAttribute(\"role\");\n if (role !== \"dialog\" && role !== \"alertdialog\" && role !== \"menu\") return;\n warnedName.current = true;\n console.error(\n `${part}: the popup has no accessible name. Render a Title, or give the Popup an ` +\n \"`aria-label`.\",\n );\n }, [popupRef, shown, part]);\n\n return { setElement, visible: ctx.committed || exit.held, wrapContent };\n}\n"],"mappings":"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"}
@@ -1,9 +1,10 @@
1
1
  "use client";
2
2
  import { createActionDetails } from "./action-details.mjs";
3
3
  import { transitionTypes } from "./transition-types.mjs";
4
+ import { useLatest } from "./use-latest.mjs";
4
5
  import { useControllableAction } from "./use-controllable-action.mjs";
5
6
  import { useDevWarning } from "./use-dev-warning.mjs";
6
- import { useCallback, useId, useInsertionEffect, useRef, useState } from "react";
7
+ import { useCallback, useId, useMemo, useRef, useState } from "react";
7
8
  //#region src/internal/use-popover-root.ts
8
9
  function isPopoverOpen(element) {
9
10
  if (element === null) return false;
@@ -45,10 +46,7 @@ function usePopoverRoot(options) {
45
46
  const triggerRef = useRef(null);
46
47
  const popupRef = useRef(null);
47
48
  const closeIntent = useRef(void 0);
48
- const committedOptimistic = useRef(optimistic);
49
- useInsertionEffect(() => {
50
- committedOptimistic.current = optimistic;
51
- });
49
+ const committedOptimistic = useLatest(optimistic);
52
50
  const generation = useRef(0);
53
51
  const openGeneration = useCallback(() => generation.current, []);
54
52
  const nextOpenGeneration = useCallback(() => {
@@ -63,33 +61,54 @@ function usePopoverRoot(options) {
63
61
  closeIntent.current = void 0;
64
62
  return intent;
65
63
  }, []);
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);
64
+ const intended = useCallback(() => latestInFlight()?.next ?? committedOptimistic.current, [latestInFlight, committedOptimistic]);
65
+ const callbacks = useLatest({
66
+ details,
67
+ onDispatch
68
+ });
69
+ const send = useCallback((next, event, reason) => {
70
+ const { details, onDispatch } = callbacks.current;
71
+ const built = details !== void 0 ? details(event, reason) : createActionDetails(event, { reason });
70
72
  if (next) nextOpenGeneration();
71
73
  onDispatch?.(next, built);
72
74
  dispatch(next, built);
73
- };
74
- const requestOpen = (event, reason) => {
75
+ }, [
76
+ callbacks,
77
+ nextOpenGeneration,
78
+ dispatch
79
+ ]);
80
+ const requestOpen = useCallback((event, reason) => {
75
81
  if (!isLibrary || intended()) return;
76
82
  send(true, event, reason);
77
- };
78
- const requestClose = (event, reason) => {
83
+ }, [
84
+ isLibrary,
85
+ intended,
86
+ send
87
+ ]);
88
+ const requestClose = useCallback((event, reason) => {
79
89
  if (!isLibrary || !intended()) return;
80
90
  send(false, event, reason);
81
- };
82
- const notifyClosed = (event, reason) => {
91
+ }, [
92
+ isLibrary,
93
+ intended,
94
+ send
95
+ ]);
96
+ const notifyClosed = useCallback((event, reason) => {
83
97
  if (!isLibrary || !intended()) return;
84
98
  send(false, event, reason);
85
- };
86
- return {
99
+ }, [
100
+ isLibrary,
101
+ intended,
102
+ send
103
+ ]);
104
+ const ids = useMemo(() => ({
105
+ popup: id,
106
+ title: `${id}-title`,
107
+ description: `${id}-description`
108
+ }), [id]);
109
+ return useMemo(() => ({
87
110
  mode,
88
- ids: {
89
- popup: id,
90
- title: `${id}-title`,
91
- description: `${id}-description`
92
- },
111
+ ids,
93
112
  committed: isLibrary && committed,
94
113
  optimistic: isLibrary && optimistic,
95
114
  pending: isLibrary && pending,
@@ -104,7 +123,23 @@ function usePopoverRoot(options) {
104
123
  popupRef,
105
124
  recordCloseIntent,
106
125
  takeCloseIntent
107
- };
126
+ }), [
127
+ mode,
128
+ ids,
129
+ isLibrary,
130
+ committed,
131
+ optimistic,
132
+ pending,
133
+ intended,
134
+ openGeneration,
135
+ nextOpenGeneration,
136
+ requestOpen,
137
+ requestClose,
138
+ notifyClosed,
139
+ latestInFlight,
140
+ recordCloseIntent,
141
+ takeCloseIntent
142
+ ]);
108
143
  }
109
144
  //#endregion
110
145
  export { usePopoverRoot };
@@ -1 +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"}
1
+ {"version":3,"file":"use-popover-root.mjs","names":[],"sources":["../../src/internal/use-popover-root.ts"],"sourcesContent":["\"use client\";\nimport { useCallback, useId, useMemo, 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\";\nimport { useLatest } from \"./use-latest.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 = useLatest(optimistic);\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 = useCallback(\n (): boolean => latestInFlight()?.next ?? committedOptimistic.current,\n [latestInFlight, committedOptimistic],\n );\n\n // `details` and `onDispatch` are usually inline functions: read the committed render's through a\n // ref so that the functions below, and with them the returned value, stay stable (#48).\n const callbacks = useLatest({ details, onDispatch });\n\n const send = useCallback(\n (next: boolean, event: Event | undefined, reason: ChangeReason): void => {\n const { details, onDispatch } = callbacks.current;\n const built =\n details !== undefined\n ? details(event, reason)\n : (createActionDetails(event, { reason }) as unknown as Details);\n if (next) nextOpenGeneration();\n onDispatch?.(next, built);\n dispatch(next, built);\n },\n [callbacks, nextOpenGeneration, dispatch],\n );\n\n const requestOpen = useCallback(\n (event: Event | undefined, reason: ChangeReason): void => {\n if (!isLibrary || intended()) return;\n send(true, event, reason);\n },\n [isLibrary, intended, send],\n );\n\n const requestClose = useCallback(\n (event: Event | undefined, reason: ChangeReason): void => {\n if (!isLibrary || !intended()) return;\n send(false, event, reason);\n },\n [isLibrary, intended, send],\n );\n\n // Same guard as `requestClose`; a separate name because the callers mean different things.\n const notifyClosed = useCallback(\n (event: Event | undefined, reason: ChangeReason): void => {\n if (!isLibrary || !intended()) return;\n send(false, event, reason);\n },\n [isLibrary, intended, send],\n );\n\n const ids = useMemo(\n () => ({ popup: id, title: `${id}-title`, description: `${id}-description` }),\n [id],\n );\n\n // Memoised: every function above is stable, so the value changes only with the open state and\n // pending, and the components' context values built on it are reused otherwise (#48).\n return useMemo(\n () => ({\n mode,\n ids,\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 mode,\n ids,\n isLibrary,\n committed,\n optimistic,\n pending,\n intended,\n openGeneration,\n nextOpenGeneration,\n requestOpen,\n requestClose,\n notifyClosed,\n latestInFlight,\n recordCloseIntent,\n takeCloseIntent,\n ],\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ //#region src/internal/validity.d.ts
2
+ /** A field error: a message, several messages, or nothing (`""`, `[]`, `null`, `undefined`). */
3
+ export type FieldErrorValue = string | readonly string[] | null | undefined;
4
+ /** Field errors keyed by field `name` (usually `state.errors` from `useActionState`). */
5
+ export type FormErrors = Readonly<Record<string, FieldErrorValue>>;
6
+ /**
7
+ * A plain, serialisable snapshot of the control's `ValidityState` (all `false` and
8
+ * `valid: true` on the server and before the first read).
9
+ */
10
+ export interface FieldValidity {
11
+ readonly valueMissing: boolean;
12
+ readonly typeMismatch: boolean;
13
+ readonly patternMismatch: boolean;
14
+ readonly tooLong: boolean;
15
+ readonly tooShort: boolean;
16
+ readonly rangeUnderflow: boolean;
17
+ readonly rangeOverflow: boolean;
18
+ readonly stepMismatch: boolean;
19
+ readonly badInput: boolean;
20
+ readonly customError: boolean;
21
+ readonly valid: boolean;
22
+ }
23
+ //#endregion
24
+ //# sourceMappingURL=validity.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validity.d.mts","names":[],"sources":["../../src/internal/validity.ts"],"mappings":";;YAIY;;YAGA,aAAa,SAAS,eAAe;;;;;iBAMhC;WACN;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA"}
@@ -0,0 +1,108 @@
1
+ //#region src/internal/validity.ts
2
+ const VALIDITY_KEYS = [
3
+ "valueMissing",
4
+ "typeMismatch",
5
+ "patternMismatch",
6
+ "tooLong",
7
+ "tooShort",
8
+ "rangeUnderflow",
9
+ "rangeOverflow",
10
+ "stepMismatch",
11
+ "badInput",
12
+ "customError",
13
+ "valid"
14
+ ];
15
+ const VALID = Object.freeze({
16
+ valueMissing: false,
17
+ typeMismatch: false,
18
+ patternMismatch: false,
19
+ tooLong: false,
20
+ tooShort: false,
21
+ rangeUnderflow: false,
22
+ rangeOverflow: false,
23
+ stepMismatch: false,
24
+ badInput: false,
25
+ customError: false,
26
+ valid: true
27
+ });
28
+ function snapshotValidity(validity) {
29
+ const result = {};
30
+ for (const key of VALIDITY_KEYS) result[key] = validity[key];
31
+ return result;
32
+ }
33
+ function validityEquals(a, b) {
34
+ return VALIDITY_KEYS.every((key) => a[key] === b[key]);
35
+ }
36
+ function arraysEqual(a, b) {
37
+ return a.length === b.length && a.every((v, i) => v === b[i]);
38
+ }
39
+ const NONE = Object.freeze([]);
40
+ /** `""`, `[]`, `null` and `undefined` mean no error; empty strings inside an array are dropped. */
41
+ function normaliseErrorValue(value) {
42
+ if (value === null || value === void 0 || value === "") return NONE;
43
+ if (typeof value === "string") return [value];
44
+ const list = value.filter((v) => typeof v === "string" && v !== "");
45
+ return list.length === 0 ? NONE : list;
46
+ }
47
+ /** Some entry of `errors` is a non-empty error. */
48
+ function hasAnyError(errors) {
49
+ if (errors === void 0) return false;
50
+ return Object.values(errors).some((v) => normaliseErrorValue(v).length > 0);
51
+ }
52
+ let userInvalidSupport;
53
+ /**
54
+ * Whether the environment implements `:user-invalid` (every engine the library supports does).
55
+ * Decided once per module, so the read is not on a hot path.
56
+ *
57
+ * Detected with `CSS.supports("selector(:user-invalid)")` rather than by `matches()` throwing:
58
+ * jsdom 30's selector engine knows the name and silently answers `false` (it does not throw), and
59
+ * its `CSS.supports` reports `false`, so the fallback engages there and nowhere else
60
+ * (memo/developer/field-form.md).
61
+ */
62
+ function supportsUserInvalid() {
63
+ if (userInvalidSupport === void 0) try {
64
+ userInvalidSupport = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("selector(:user-invalid)");
65
+ } catch {
66
+ userInvalidSupport = false;
67
+ }
68
+ return userInvalidSupport;
69
+ }
70
+ function isValidatable(el) {
71
+ return el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement;
72
+ }
73
+ /**
74
+ * Where `validity`, `validationMessage`, `:user-invalid` and `setCustomValidity` are read and
75
+ * written (design 7.1): the element itself when it is a form control, else the first named
76
+ * `willValidate` member of a `<fieldset>` (a group's first enabled item, not a CheckboxGroup
77
+ * Parent, which has no name), else the first `input` / `select` / `textarea` inside.
78
+ */
79
+ function validityElementOf(el) {
80
+ if (el === null) return null;
81
+ if (isValidatable(el)) return el;
82
+ const controls = (el instanceof HTMLFieldSetElement ? [...el.elements] : [...el.querySelectorAll("input, select, textarea")]).filter(isValidatable);
83
+ return controls.find((c) => c.willValidate && c.name !== "") ?? controls.find((c) => c.willValidate) ?? controls[0] ?? null;
84
+ }
85
+ /**
86
+ * What the Field focuses (design 2.5): the control itself, or for a group the checked radio if
87
+ * any, else the first enabled named member (a group's first item, not a CheckboxGroup Parent,
88
+ * which has no name), else the first enabled member.
89
+ */
90
+ function focusTargetOf(el) {
91
+ if (el === null) return null;
92
+ if (isValidatable(el)) return el;
93
+ const members = (el instanceof HTMLFieldSetElement ? [...el.elements] : [...el.querySelectorAll("input, select, textarea, button")]).filter((m) => m instanceof HTMLElement && !m.disabled && m.type !== "hidden");
94
+ return members.find((m) => m instanceof HTMLInputElement && m.type === "radio" && m.checked) ?? members.find((m) => isValidatable(m) && m.name !== "") ?? members.find(isValidatable) ?? members[0] ?? null;
95
+ }
96
+ /** Sorts elements into document order. */
97
+ function sortByDocumentOrder(items, elementOf) {
98
+ return [...items].sort((a, b) => {
99
+ const position = elementOf(a).compareDocumentPosition(elementOf(b));
100
+ if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
101
+ if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1;
102
+ return 0;
103
+ });
104
+ }
105
+ //#endregion
106
+ export { VALID, arraysEqual, focusTargetOf, hasAnyError, normaliseErrorValue, snapshotValidity, sortByDocumentOrder, supportsUserInvalid, validityElementOf, validityEquals };
107
+
108
+ //# sourceMappingURL=validity.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validity.mjs","names":[],"sources":["../../src/internal/validity.ts"],"sourcesContent":["// Directive-free helpers for Field and Form (design/components/field-form.md 2, 14): the validity\n// snapshot, error-value normalisation, the `:user-invalid` read and the element resolution.\n\n/** A field error: a message, several messages, or nothing (`\"\"`, `[]`, `null`, `undefined`). */\nexport type FieldErrorValue = string | readonly string[] | null | undefined;\n\n/** Field errors keyed by field `name` (usually `state.errors` from `useActionState`). */\nexport type FormErrors = Readonly<Record<string, FieldErrorValue>>;\n\n/**\n * A plain, serialisable snapshot of the control's `ValidityState` (all `false` and\n * `valid: true` on the server and before the first read).\n */\nexport interface FieldValidity {\n readonly valueMissing: boolean;\n readonly typeMismatch: boolean;\n readonly patternMismatch: boolean;\n readonly tooLong: boolean;\n readonly tooShort: boolean;\n readonly rangeUnderflow: boolean;\n readonly rangeOverflow: boolean;\n readonly stepMismatch: boolean;\n readonly badInput: boolean;\n readonly customError: boolean;\n readonly valid: boolean;\n}\n\nconst VALIDITY_KEYS = [\n \"valueMissing\",\n \"typeMismatch\",\n \"patternMismatch\",\n \"tooLong\",\n \"tooShort\",\n \"rangeUnderflow\",\n \"rangeOverflow\",\n \"stepMismatch\",\n \"badInput\",\n \"customError\",\n \"valid\",\n] as const satisfies readonly (keyof FieldValidity)[];\n\nexport const VALID: FieldValidity = Object.freeze({\n valueMissing: false,\n typeMismatch: false,\n patternMismatch: false,\n tooLong: false,\n tooShort: false,\n rangeUnderflow: false,\n rangeOverflow: false,\n stepMismatch: false,\n badInput: false,\n customError: false,\n valid: true,\n});\n\nexport function snapshotValidity(validity: ValidityState): FieldValidity {\n const result = {} as Record<keyof FieldValidity, boolean>;\n for (const key of VALIDITY_KEYS) result[key] = validity[key];\n return result;\n}\n\nexport function validityEquals(a: FieldValidity, b: FieldValidity): boolean {\n return VALIDITY_KEYS.every((key) => a[key] === b[key]);\n}\n\nexport function arraysEqual(a: readonly string[], b: readonly string[]): boolean {\n return a.length === b.length && a.every((v, i) => v === b[i]);\n}\n\nconst NONE: readonly string[] = Object.freeze([]);\n\n/** `\"\"`, `[]`, `null` and `undefined` mean no error; empty strings inside an array are dropped. */\nexport function normaliseErrorValue(value: FieldErrorValue): readonly string[] {\n if (value === null || value === undefined || value === \"\") return NONE;\n if (typeof value === \"string\") return [value];\n const list = value.filter((v) => typeof v === \"string\" && v !== \"\");\n return list.length === 0 ? NONE : list;\n}\n\n/** Some entry of `errors` is a non-empty error. */\nexport function hasAnyError(errors: FormErrors | undefined): boolean {\n if (errors === undefined) return false;\n return Object.values(errors).some((v) => normaliseErrorValue(v).length > 0);\n}\n\nlet userInvalidSupport: boolean | undefined;\n\n/**\n * Whether the environment implements `:user-invalid` (every engine the library supports does).\n * Decided once per module, so the read is not on a hot path.\n *\n * Detected with `CSS.supports(\"selector(:user-invalid)\")` rather than by `matches()` throwing:\n * jsdom 30's selector engine knows the name and silently answers `false` (it does not throw), and\n * its `CSS.supports` reports `false`, so the fallback engages there and nowhere else\n * (memo/developer/field-form.md).\n */\nexport function supportsUserInvalid(): boolean {\n if (userInvalidSupport === undefined) {\n try {\n userInvalidSupport =\n typeof CSS !== \"undefined\" &&\n typeof CSS.supports === \"function\" &&\n CSS.supports(\"selector(:user-invalid)\");\n } catch {\n userInvalidSupport = false;\n }\n }\n return userInvalidSupport;\n}\n\n/** An element that takes part in constraint validation. */\nexport type ValidatableElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;\n\nfunction isValidatable(el: Element): el is ValidatableElement {\n return (\n el instanceof HTMLInputElement ||\n el instanceof HTMLSelectElement ||\n el instanceof HTMLTextAreaElement\n );\n}\n\n/**\n * Where `validity`, `validationMessage`, `:user-invalid` and `setCustomValidity` are read and\n * written (design 7.1): the element itself when it is a form control, else the first named\n * `willValidate` member of a `<fieldset>` (a group's first enabled item, not a CheckboxGroup\n * Parent, which has no name), else the first `input` / `select` / `textarea` inside.\n */\nexport function validityElementOf(el: Element | null): ValidatableElement | null {\n if (el === null) return null;\n if (isValidatable(el)) return el;\n const members =\n el instanceof HTMLFieldSetElement\n ? [...el.elements]\n : [...el.querySelectorAll(\"input, select, textarea\")];\n const controls = members.filter(isValidatable);\n return (\n controls.find((c) => c.willValidate && c.name !== \"\") ??\n controls.find((c) => c.willValidate) ??\n controls[0] ??\n null\n );\n}\n\n/**\n * What the Field focuses (design 2.5): the control itself, or for a group the checked radio if\n * any, else the first enabled named member (a group's first item, not a CheckboxGroup Parent,\n * which has no name), else the first enabled member.\n */\nexport function focusTargetOf(el: Element | null): HTMLElement | null {\n if (el === null) return null;\n if (isValidatable(el)) return el;\n const members = (\n el instanceof HTMLFieldSetElement\n ? [...el.elements]\n : [...el.querySelectorAll(\"input, select, textarea, button\")]\n ).filter(\n (m): m is HTMLElement =>\n m instanceof HTMLElement &&\n !(m as HTMLInputElement).disabled &&\n (m as HTMLInputElement).type !== \"hidden\",\n );\n const checkedRadio = members.find(\n (m) => m instanceof HTMLInputElement && m.type === \"radio\" && m.checked,\n );\n return (\n checkedRadio ??\n members.find((m) => isValidatable(m) && m.name !== \"\") ??\n members.find(isValidatable) ??\n members[0] ??\n null\n );\n}\n\n/** Sorts elements into document order. */\nexport function sortByDocumentOrder<T>(items: readonly T[], elementOf: (item: T) => Node): T[] {\n return [...items].sort((a, b) => {\n const position = elementOf(a).compareDocumentPosition(elementOf(b));\n if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1;\n if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1;\n return 0;\n });\n}\n"],"mappings":"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"}
@@ -1 +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"}
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;;;iBAkBC,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"}
@@ -2,7 +2,7 @@
2
2
  import { useRender } from "../internal/use-render.mjs";
3
3
  import { useDevWarning } from "../internal/use-dev-warning.mjs";
4
4
  import { MenuGroupContext } from "./menu-context.mjs";
5
- import { use, useId } from "react";
5
+ import { use, useId, useMemo } from "react";
6
6
  import { jsx } from "react/jsx-runtime";
7
7
  //#region src/menu/menu-group.tsx
8
8
  const EMPTY = {};
@@ -25,8 +25,9 @@ function MenuGroup({ className, style, children, render, ...rest }) {
25
25
  "aria-labelledby": labelId
26
26
  }
27
27
  });
28
+ const context = useMemo(() => ({ labelId }), [labelId]);
28
29
  return /* @__PURE__ */ jsx(MenuGroupContext, {
29
- value: { labelId },
30
+ value: context,
30
31
  children: element
31
32
  });
32
33
  }
@@ -1 +1 @@
1
- {"version":3,"file":"menu-group.mjs","names":[],"sources":["../../src/menu/menu-group.tsx"],"sourcesContent":["\"use client\";\nimport { use, useId } from \"react\";\nimport type { ComponentPropsWithRef, CSSProperties, ReactNode } from \"react\";\nimport type { RenderProp, StateValue } from \"../internal/types.js\";\nimport { useDevWarning } from \"../internal/use-dev-warning.js\";\nimport { useRender } from \"../internal/use-render.js\";\nimport { MenuGroupContext } from \"./menu-context.js\";\n\n/** `Menu.Group`, `Menu.GroupLabel` and `Menu.Separator` have no state. */\nexport type MenuGroupState = Record<string, never>;\nexport type MenuGroupLabelState = Record<string, never>;\nexport type MenuSeparatorState = Record<string, never>;\n\nconst EMPTY: Record<string, never> = {};\n\nexport interface MenuGroupProps extends Omit<\n ComponentPropsWithRef<\"div\">,\n \"role\" | \"className\" | \"style\" | \"children\"\n> {\n className?: StateValue<string | undefined, MenuGroupState>;\n style?: StateValue<CSSProperties | undefined, MenuGroupState>;\n children?: StateValue<ReactNode, MenuGroupState>;\n render?: RenderProp<ComponentPropsWithRef<\"div\">, MenuGroupState>;\n}\n\n/**\n * A labelled set of items: `<div role=\"group\" aria-labelledby>` named by the `Menu.GroupLabel`\n * inside it. Owned keys: `role`, `aria-labelledby` (token-merged).\n */\nexport function MenuGroup({ className, style, children, render, ...rest }: MenuGroupProps) {\n const labelId = `${useId()}-label`;\n const element = useRender<ComponentPropsWithRef<\"div\">, MenuGroupState>({\n defaultElement: \"div\",\n render,\n state: EMPTY,\n className,\n style,\n children,\n props: rest,\n libraryProps: { role: \"group\", \"aria-labelledby\": labelId },\n });\n return <MenuGroupContext value={{ labelId }}>{element}</MenuGroupContext>;\n}\n\nMenuGroup.displayName = \"MenuGroup\";\n\nexport interface MenuGroupLabelProps extends Omit<\n ComponentPropsWithRef<\"div\">,\n \"id\" | \"className\" | \"style\" | \"children\"\n> {\n className?: StateValue<string | undefined, MenuGroupLabelState>;\n style?: StateValue<CSSProperties | undefined, MenuGroupLabelState>;\n children?: StateValue<ReactNode, MenuGroupLabelState>;\n render?: RenderProp<ComponentPropsWithRef<\"div\">, MenuGroupLabelState>;\n}\n\n/**\n * The name of the enclosing `Menu.Group` or `Menu.RadioGroup`: a `<div>` with the id the group's\n * `aria-labelledby` references. Owned key: `id`.\n */\nexport function MenuGroupLabel({\n className,\n style,\n children,\n render,\n ...rest\n}: MenuGroupLabelProps) {\n const group = use(MenuGroupContext);\n useDevWarning(\n group === null,\n \"Menu.GroupLabel: rendered outside a Menu.Group or Menu.RadioGroup; it names nothing.\",\n );\n return useRender<ComponentPropsWithRef<\"div\">, MenuGroupLabelState>({\n defaultElement: \"div\",\n render,\n state: EMPTY,\n className,\n style,\n children,\n props: rest,\n libraryProps: group === null ? {} : { id: group.labelId },\n });\n}\n\nMenuGroupLabel.displayName = \"MenuGroupLabel\";\n\nexport interface MenuSeparatorProps extends Omit<\n ComponentPropsWithRef<\"hr\">,\n \"className\" | \"style\" | \"children\"\n> {\n className?: StateValue<string | undefined, MenuSeparatorState>;\n style?: StateValue<CSSProperties | undefined, MenuSeparatorState>;\n /** Another element must carry `role=\"separator\"` itself. */\n render?: RenderProp<ComponentPropsWithRef<\"hr\">, MenuSeparatorState>;\n}\n\n/** A divider between items: `<hr>` (implicit `role=\"separator\"`). Owns no keys. */\nexport function MenuSeparator({ className, style, render, ...rest }: MenuSeparatorProps) {\n return useRender<ComponentPropsWithRef<\"hr\">, MenuSeparatorState>({\n defaultElement: \"hr\",\n render,\n state: EMPTY,\n className,\n style,\n children: undefined,\n props: rest,\n });\n}\n\nMenuSeparator.displayName = \"MenuSeparator\";\n"],"mappings":";;;;;;;AAaA,MAAM,QAA+B,CAAC;;;;;AAgBtC,SAAgB,UAAU,EAAE,WAAW,OAAO,UAAU,QAAQ,GAAG,QAAwB;CACzF,MAAM,UAAU,GAAG,MAAM,EAAE;CAC3B,MAAM,UAAU,UAAwD;EACtE,gBAAgB;EAChB;EACA,OAAO;EACP;EACA;EACA;EACA,OAAO;EACP,cAAc;GAAE,MAAM;GAAS,mBAAmB;EAAQ;CAC5D,CAAC;CACD,OAAO,oBAAC,kBAAD;EAAkB,OAAO,EAAE,QAAQ;EAAI,UAAA;CAA0B,CAAA;AAC1E;AAEA,UAAU,cAAc;;;;;AAgBxB,SAAgB,eAAe,EAC7B,WACA,OACA,UACA,QACA,GAAG,QACmB;CACtB,MAAM,QAAQ,IAAI,gBAAgB;CAClC,cACE,UAAU,MACV,sFACF;CACA,OAAO,UAA6D;EAClE,gBAAgB;EAChB;EACA,OAAO;EACP;EACA;EACA;EACA,OAAO;EACP,cAAc,UAAU,OAAO,CAAC,IAAI,EAAE,IAAI,MAAM,QAAQ;CAC1D,CAAC;AACH;AAEA,eAAe,cAAc;;AAa7B,SAAgB,cAAc,EAAE,WAAW,OAAO,QAAQ,GAAG,QAA4B;CACvF,OAAO,UAA2D;EAChE,gBAAgB;EAChB;EACA,OAAO;EACP;EACA;EACA,UAAU,KAAA;EACV,OAAO;CACT,CAAC;AACH;AAEA,cAAc,cAAc"}
1
+ {"version":3,"file":"menu-group.mjs","names":[],"sources":["../../src/menu/menu-group.tsx"],"sourcesContent":["\"use client\";\nimport { use, useId, useMemo } from \"react\";\nimport type { ComponentPropsWithRef, CSSProperties, ReactNode } from \"react\";\nimport type { RenderProp, StateValue } from \"../internal/types.js\";\nimport { useDevWarning } from \"../internal/use-dev-warning.js\";\nimport { useRender } from \"../internal/use-render.js\";\nimport { MenuGroupContext } from \"./menu-context.js\";\n\n/** `Menu.Group`, `Menu.GroupLabel` and `Menu.Separator` have no state. */\nexport type MenuGroupState = Record<string, never>;\nexport type MenuGroupLabelState = Record<string, never>;\nexport type MenuSeparatorState = Record<string, never>;\n\nconst EMPTY: Record<string, never> = {};\n\nexport interface MenuGroupProps extends Omit<\n ComponentPropsWithRef<\"div\">,\n \"role\" | \"className\" | \"style\" | \"children\"\n> {\n className?: StateValue<string | undefined, MenuGroupState>;\n style?: StateValue<CSSProperties | undefined, MenuGroupState>;\n children?: StateValue<ReactNode, MenuGroupState>;\n render?: RenderProp<ComponentPropsWithRef<\"div\">, MenuGroupState>;\n}\n\n/**\n * A labelled set of items: `<div role=\"group\" aria-labelledby>` named by the `Menu.GroupLabel`\n * inside it. Owned keys: `role`, `aria-labelledby` (token-merged).\n */\nexport function MenuGroup({ className, style, children, render, ...rest }: MenuGroupProps) {\n const labelId = `${useId()}-label`;\n const element = useRender<ComponentPropsWithRef<\"div\">, MenuGroupState>({\n defaultElement: \"div\",\n render,\n state: EMPTY,\n className,\n style,\n children,\n props: rest,\n libraryProps: { role: \"group\", \"aria-labelledby\": labelId },\n });\n const context = useMemo(() => ({ labelId }), [labelId]);\n return <MenuGroupContext value={context}>{element}</MenuGroupContext>;\n}\n\nMenuGroup.displayName = \"MenuGroup\";\n\nexport interface MenuGroupLabelProps extends Omit<\n ComponentPropsWithRef<\"div\">,\n \"id\" | \"className\" | \"style\" | \"children\"\n> {\n className?: StateValue<string | undefined, MenuGroupLabelState>;\n style?: StateValue<CSSProperties | undefined, MenuGroupLabelState>;\n children?: StateValue<ReactNode, MenuGroupLabelState>;\n render?: RenderProp<ComponentPropsWithRef<\"div\">, MenuGroupLabelState>;\n}\n\n/**\n * The name of the enclosing `Menu.Group` or `Menu.RadioGroup`: a `<div>` with the id the group's\n * `aria-labelledby` references. Owned key: `id`.\n */\nexport function MenuGroupLabel({\n className,\n style,\n children,\n render,\n ...rest\n}: MenuGroupLabelProps) {\n const group = use(MenuGroupContext);\n useDevWarning(\n group === null,\n \"Menu.GroupLabel: rendered outside a Menu.Group or Menu.RadioGroup; it names nothing.\",\n );\n return useRender<ComponentPropsWithRef<\"div\">, MenuGroupLabelState>({\n defaultElement: \"div\",\n render,\n state: EMPTY,\n className,\n style,\n children,\n props: rest,\n libraryProps: group === null ? {} : { id: group.labelId },\n });\n}\n\nMenuGroupLabel.displayName = \"MenuGroupLabel\";\n\nexport interface MenuSeparatorProps extends Omit<\n ComponentPropsWithRef<\"hr\">,\n \"className\" | \"style\" | \"children\"\n> {\n className?: StateValue<string | undefined, MenuSeparatorState>;\n style?: StateValue<CSSProperties | undefined, MenuSeparatorState>;\n /** Another element must carry `role=\"separator\"` itself. */\n render?: RenderProp<ComponentPropsWithRef<\"hr\">, MenuSeparatorState>;\n}\n\n/** A divider between items: `<hr>` (implicit `role=\"separator\"`). Owns no keys. */\nexport function MenuSeparator({ className, style, render, ...rest }: MenuSeparatorProps) {\n return useRender<ComponentPropsWithRef<\"hr\">, MenuSeparatorState>({\n defaultElement: \"hr\",\n render,\n state: EMPTY,\n className,\n style,\n children: undefined,\n props: rest,\n });\n}\n\nMenuSeparator.displayName = \"MenuSeparator\";\n"],"mappings":";;;;;;;AAaA,MAAM,QAA+B,CAAC;;;;;AAgBtC,SAAgB,UAAU,EAAE,WAAW,OAAO,UAAU,QAAQ,GAAG,QAAwB;CACzF,MAAM,UAAU,GAAG,MAAM,EAAE;CAC3B,MAAM,UAAU,UAAwD;EACtE,gBAAgB;EAChB;EACA,OAAO;EACP;EACA;EACA;EACA,OAAO;EACP,cAAc;GAAE,MAAM;GAAS,mBAAmB;EAAQ;CAC5D,CAAC;CACD,MAAM,UAAU,eAAe,EAAE,QAAQ,IAAI,CAAC,OAAO,CAAC;CACtD,OAAO,oBAAC,kBAAD;EAAkB,OAAO;EAAU,UAAA;CAA0B,CAAA;AACtE;AAEA,UAAU,cAAc;;;;;AAgBxB,SAAgB,eAAe,EAC7B,WACA,OACA,UACA,QACA,GAAG,QACmB;CACtB,MAAM,QAAQ,IAAI,gBAAgB;CAClC,cACE,UAAU,MACV,sFACF;CACA,OAAO,UAA6D;EAClE,gBAAgB;EAChB;EACA,OAAO;EACP;EACA;EACA;EACA,OAAO;EACP,cAAc,UAAU,OAAO,CAAC,IAAI,EAAE,IAAI,MAAM,QAAQ;CAC1D,CAAC;AACH;AAEA,eAAe,cAAc;;AAa7B,SAAgB,cAAc,EAAE,WAAW,OAAO,QAAQ,GAAG,QAA4B;CACvF,OAAO,UAA2D;EAChE,gBAAgB;EAChB;EACA,OAAO;EACP;EACA;EACA,UAAU,KAAA;EACV,OAAO;CACT,CAAC;AACH;AAEA,cAAc,cAAc"}