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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/README.md +1 -1
  2. package/dist/checkbox-group/checkbox-group-context.mjs +24 -0
  3. package/dist/checkbox-group/checkbox-group-context.mjs.map +1 -0
  4. package/dist/checkbox-group/checkbox-group-item.d.mts +55 -0
  5. package/dist/checkbox-group/checkbox-group-item.d.mts.map +1 -0
  6. package/dist/checkbox-group/checkbox-group-item.mjs +90 -0
  7. package/dist/checkbox-group/checkbox-group-item.mjs.map +1 -0
  8. package/dist/checkbox-group/checkbox-group-label.d.mts +21 -0
  9. package/dist/checkbox-group/checkbox-group-label.d.mts.map +1 -0
  10. package/dist/checkbox-group/checkbox-group-label.mjs +30 -0
  11. package/dist/checkbox-group/checkbox-group-label.mjs.map +1 -0
  12. package/dist/checkbox-group/checkbox-group-parent.d.mts +68 -0
  13. package/dist/checkbox-group/checkbox-group-parent.d.mts.map +1 -0
  14. package/dist/checkbox-group/checkbox-group-parent.mjs +145 -0
  15. package/dist/checkbox-group/checkbox-group-parent.mjs.map +1 -0
  16. package/dist/checkbox-group/checkbox-group-root.d.mts +100 -0
  17. package/dist/checkbox-group/checkbox-group-root.d.mts.map +1 -0
  18. package/dist/checkbox-group/checkbox-group-root.mjs +291 -0
  19. package/dist/checkbox-group/checkbox-group-root.mjs.map +1 -0
  20. package/dist/checkbox-group/index.d.mts +23 -0
  21. package/dist/checkbox-group/index.d.mts.map +1 -0
  22. package/dist/checkbox-group/index.mjs +23 -0
  23. package/dist/checkbox-group/index.mjs.map +1 -0
  24. package/dist/combobox/combobox-clear.d.mts +34 -0
  25. package/dist/combobox/combobox-clear.d.mts.map +1 -0
  26. package/dist/combobox/combobox-clear.mjs +58 -0
  27. package/dist/combobox/combobox-clear.mjs.map +1 -0
  28. package/dist/combobox/combobox-context.mjs +23 -0
  29. package/dist/combobox/combobox-context.mjs.map +1 -0
  30. package/dist/combobox/combobox-group.d.mts +72 -0
  31. package/dist/combobox/combobox-group.d.mts.map +1 -0
  32. package/dist/combobox/combobox-group.mjs +104 -0
  33. package/dist/combobox/combobox-group.mjs.map +1 -0
  34. package/dist/combobox/combobox-input.d.mts +40 -0
  35. package/dist/combobox/combobox-input.d.mts.map +1 -0
  36. package/dist/combobox/combobox-input.mjs +88 -0
  37. package/dist/combobox/combobox-input.mjs.map +1 -0
  38. package/dist/combobox/combobox-label.d.mts +22 -0
  39. package/dist/combobox/combobox-label.d.mts.map +1 -0
  40. package/dist/combobox/combobox-label.mjs +32 -0
  41. package/dist/combobox/combobox-label.mjs.map +1 -0
  42. package/dist/combobox/combobox-list.d.mts +37 -0
  43. package/dist/combobox/combobox-list.d.mts.map +1 -0
  44. package/dist/combobox/combobox-list.mjs +70 -0
  45. package/dist/combobox/combobox-list.mjs.map +1 -0
  46. package/dist/combobox/combobox-option.d.mts +45 -0
  47. package/dist/combobox/combobox-option.d.mts.map +1 -0
  48. package/dist/combobox/combobox-option.mjs +88 -0
  49. package/dist/combobox/combobox-option.mjs.map +1 -0
  50. package/dist/combobox/combobox-popup.d.mts +49 -0
  51. package/dist/combobox/combobox-popup.d.mts.map +1 -0
  52. package/dist/combobox/combobox-popup.mjs +86 -0
  53. package/dist/combobox/combobox-popup.mjs.map +1 -0
  54. package/dist/combobox/combobox-root.d.mts +135 -0
  55. package/dist/combobox/combobox-root.d.mts.map +1 -0
  56. package/dist/combobox/combobox-root.mjs +508 -0
  57. package/dist/combobox/combobox-root.mjs.map +1 -0
  58. package/dist/combobox/combobox-trigger.d.mts +36 -0
  59. package/dist/combobox/combobox-trigger.d.mts.map +1 -0
  60. package/dist/combobox/combobox-trigger.mjs +92 -0
  61. package/dist/combobox/combobox-trigger.mjs.map +1 -0
  62. package/dist/combobox/index.d.mts +36 -0
  63. package/dist/combobox/index.d.mts.map +1 -0
  64. package/dist/combobox/index.mjs +36 -0
  65. package/dist/combobox/index.mjs.map +1 -0
  66. package/dist/dialog/dialog-close.d.mts.map +1 -1
  67. package/dist/dialog/dialog-close.mjs +4 -4
  68. package/dist/dialog/dialog-close.mjs.map +1 -1
  69. package/dist/dialog/dialog-context.mjs.map +1 -1
  70. package/dist/dialog/dialog-popup.mjs +1 -0
  71. package/dist/dialog/dialog-popup.mjs.map +1 -1
  72. package/dist/dialog/dialog-root.mjs +9 -1
  73. package/dist/dialog/dialog-root.mjs.map +1 -1
  74. package/dist/index.d.mts +42 -2
  75. package/dist/index.mjs +6 -1
  76. package/dist/internal/anchor-position.d.mts +7 -0
  77. package/dist/internal/anchor-position.d.mts.map +1 -0
  78. package/dist/internal/anchor-position.mjs +16 -0
  79. package/dist/internal/anchor-position.mjs.map +1 -0
  80. package/dist/internal/platform-attributes.d.mts +18 -1
  81. package/dist/internal/platform-attributes.d.mts.map +1 -1
  82. package/dist/internal/sync-presence.mjs +19 -0
  83. package/dist/internal/sync-presence.mjs.map +1 -0
  84. package/dist/internal/types.d.mts +8 -0
  85. package/dist/internal/types.d.mts.map +1 -1
  86. package/dist/internal/use-action-trigger.mjs +58 -0
  87. package/dist/internal/use-action-trigger.mjs.map +1 -0
  88. package/dist/internal/use-controllable-action.mjs +5 -1
  89. package/dist/internal/use-controllable-action.mjs.map +1 -1
  90. package/dist/internal/use-delayed-pair.mjs +40 -0
  91. package/dist/internal/use-delayed-pair.mjs.map +1 -0
  92. package/dist/internal/use-exit-animation.mjs +55 -0
  93. package/dist/internal/use-exit-animation.mjs.map +1 -0
  94. package/dist/internal/use-form-reset.mjs +5 -1
  95. package/dist/internal/use-form-reset.mjs.map +1 -1
  96. package/dist/internal/use-hydrated-mount.mjs +21 -0
  97. package/dist/internal/use-hydrated-mount.mjs.map +1 -0
  98. package/dist/internal/use-native-group-reconciliation.mjs +2 -2
  99. package/dist/internal/use-native-group-reconciliation.mjs.map +1 -1
  100. package/dist/internal/use-native-select-value.mjs +182 -0
  101. package/dist/internal/use-native-select-value.mjs.map +1 -0
  102. package/dist/internal/use-popover-popup.mjs +274 -0
  103. package/dist/internal/use-popover-popup.mjs.map +1 -0
  104. package/dist/internal/use-popover-root.d.mts +2 -0
  105. package/dist/internal/use-popover-root.mjs +112 -0
  106. package/dist/internal/use-popover-root.mjs.map +1 -0
  107. package/dist/internal/use-popover-trigger.mjs +86 -0
  108. package/dist/internal/use-popover-trigger.mjs.map +1 -0
  109. package/dist/menu/index.d.mts +34 -0
  110. package/dist/menu/index.d.mts.map +1 -0
  111. package/dist/menu/index.mjs +34 -0
  112. package/dist/menu/index.mjs.map +1 -0
  113. package/dist/menu/menu-checkbox-item.d.mts +42 -0
  114. package/dist/menu/menu-checkbox-item.d.mts.map +1 -0
  115. package/dist/menu/menu-checkbox-item.mjs +71 -0
  116. package/dist/menu/menu-checkbox-item.mjs.map +1 -0
  117. package/dist/menu/menu-context.d.mts +1 -0
  118. package/dist/menu/menu-context.mjs +28 -0
  119. package/dist/menu/menu-context.mjs.map +1 -0
  120. package/dist/menu/menu-dom.mjs +37 -0
  121. package/dist/menu/menu-dom.mjs.map +1 -0
  122. package/dist/menu/menu-group.d.mts +48 -0
  123. package/dist/menu/menu-group.d.mts.map +1 -0
  124. package/dist/menu/menu-group.mjs +69 -0
  125. package/dist/menu/menu-group.mjs.map +1 -0
  126. package/dist/menu/menu-hover.mjs +29 -0
  127. package/dist/menu/menu-hover.mjs.map +1 -0
  128. package/dist/menu/menu-item.d.mts +45 -0
  129. package/dist/menu/menu-item.d.mts.map +1 -0
  130. package/dist/menu/menu-item.mjs +54 -0
  131. package/dist/menu/menu-item.mjs.map +1 -0
  132. package/dist/menu/menu-popup.d.mts +69 -0
  133. package/dist/menu/menu-popup.d.mts.map +1 -0
  134. package/dist/menu/menu-popup.mjs +207 -0
  135. package/dist/menu/menu-popup.mjs.map +1 -0
  136. package/dist/menu/menu-radio-group.d.mts +37 -0
  137. package/dist/menu/menu-radio-group.d.mts.map +1 -0
  138. package/dist/menu/menu-radio-group.mjs +72 -0
  139. package/dist/menu/menu-radio-group.mjs.map +1 -0
  140. package/dist/menu/menu-radio-item.d.mts +33 -0
  141. package/dist/menu/menu-radio-item.d.mts.map +1 -0
  142. package/dist/menu/menu-radio-item.mjs +62 -0
  143. package/dist/menu/menu-radio-item.mjs.map +1 -0
  144. package/dist/menu/menu-root.d.mts +54 -0
  145. package/dist/menu/menu-root.d.mts.map +1 -0
  146. package/dist/menu/menu-root.mjs +102 -0
  147. package/dist/menu/menu-root.mjs.map +1 -0
  148. package/dist/menu/menu-submenu-trigger.d.mts +31 -0
  149. package/dist/menu/menu-submenu-trigger.d.mts.map +1 -0
  150. package/dist/menu/menu-submenu-trigger.mjs +92 -0
  151. package/dist/menu/menu-submenu-trigger.mjs.map +1 -0
  152. package/dist/menu/menu-trigger.d.mts +36 -0
  153. package/dist/menu/menu-trigger.d.mts.map +1 -0
  154. package/dist/menu/menu-trigger.mjs +81 -0
  155. package/dist/menu/menu-trigger.mjs.map +1 -0
  156. package/dist/menu/use-menu-item.mjs +54 -0
  157. package/dist/menu/use-menu-item.mjs.map +1 -0
  158. package/dist/popover/index.d.mts +28 -0
  159. package/dist/popover/index.d.mts.map +1 -0
  160. package/dist/popover/index.mjs +28 -0
  161. package/dist/popover/index.mjs.map +1 -0
  162. package/dist/popover/popover-close.d.mts +41 -0
  163. package/dist/popover/popover-close.d.mts.map +1 -0
  164. package/dist/popover/popover-close.mjs +90 -0
  165. package/dist/popover/popover-close.mjs.map +1 -0
  166. package/dist/popover/popover-context.mjs +26 -0
  167. package/dist/popover/popover-context.mjs.map +1 -0
  168. package/dist/popover/popover-description.d.mts +22 -0
  169. package/dist/popover/popover-description.d.mts.map +1 -0
  170. package/dist/popover/popover-description.mjs +28 -0
  171. package/dist/popover/popover-description.mjs.map +1 -0
  172. package/dist/popover/popover-popup.d.mts +82 -0
  173. package/dist/popover/popover-popup.d.mts.map +1 -0
  174. package/dist/popover/popover-popup.mjs +93 -0
  175. package/dist/popover/popover-popup.mjs.map +1 -0
  176. package/dist/popover/popover-root.d.mts +64 -0
  177. package/dist/popover/popover-root.d.mts.map +1 -0
  178. package/dist/popover/popover-root.mjs +39 -0
  179. package/dist/popover/popover-root.mjs.map +1 -0
  180. package/dist/popover/popover-title.d.mts +23 -0
  181. package/dist/popover/popover-title.d.mts.map +1 -0
  182. package/dist/popover/popover-title.mjs +28 -0
  183. package/dist/popover/popover-title.mjs.map +1 -0
  184. package/dist/popover/popover-trigger.d.mts +37 -0
  185. package/dist/popover/popover-trigger.d.mts.map +1 -0
  186. package/dist/popover/popover-trigger.mjs +40 -0
  187. package/dist/popover/popover-trigger.mjs.map +1 -0
  188. package/dist/radio-group/radio-group-item.d.mts.map +1 -1
  189. package/dist/radio-group/radio-group-item.mjs +1 -6
  190. package/dist/radio-group/radio-group-item.mjs.map +1 -1
  191. package/dist/select/index.d.mts +30 -0
  192. package/dist/select/index.d.mts.map +1 -0
  193. package/dist/select/index.mjs +30 -0
  194. package/dist/select/index.mjs.map +1 -0
  195. package/dist/select/select-context.mjs +13 -0
  196. package/dist/select/select-context.mjs.map +1 -0
  197. package/dist/select/select-dom.mjs +44 -0
  198. package/dist/select/select-dom.mjs.map +1 -0
  199. package/dist/select/select-group-label.d.mts +22 -0
  200. package/dist/select/select-group-label.d.mts.map +1 -0
  201. package/dist/select/select-group-label.mjs +39 -0
  202. package/dist/select/select-group-label.mjs.map +1 -0
  203. package/dist/select/select-group.d.mts +28 -0
  204. package/dist/select/select-group.d.mts.map +1 -0
  205. package/dist/select/select-group.mjs +45 -0
  206. package/dist/select/select-group.mjs.map +1 -0
  207. package/dist/select/select-option.d.mts +42 -0
  208. package/dist/select/select-option.d.mts.map +1 -0
  209. package/dist/select/select-option.mjs +65 -0
  210. package/dist/select/select-option.mjs.map +1 -0
  211. package/dist/select/select-root.d.mts +66 -0
  212. package/dist/select/select-root.d.mts.map +1 -0
  213. package/dist/select/select-root.mjs +94 -0
  214. package/dist/select/select-root.mjs.map +1 -0
  215. package/dist/select/select-separator.d.mts +20 -0
  216. package/dist/select/select-separator.d.mts.map +1 -0
  217. package/dist/select/select-separator.mjs +30 -0
  218. package/dist/select/select-separator.mjs.map +1 -0
  219. package/dist/select/select-trigger.d.mts +29 -0
  220. package/dist/select/select-trigger.d.mts.map +1 -0
  221. package/dist/select/select-trigger.mjs +48 -0
  222. package/dist/select/select-trigger.mjs.map +1 -0
  223. package/dist/select/select-value.d.mts +31 -0
  224. package/dist/select/select-value.d.mts.map +1 -0
  225. package/dist/select/select-value.mjs +101 -0
  226. package/dist/select/select-value.mjs.map +1 -0
  227. package/dist/tabs/tabs-context.mjs +2 -1
  228. package/dist/tabs/tabs-context.mjs.map +1 -1
  229. package/dist/tabs/tabs-list.d.mts +1 -1
  230. package/dist/tabs/tabs-list.d.mts.map +1 -1
  231. package/dist/tabs/tabs-list.mjs +17 -9
  232. package/dist/tabs/tabs-list.mjs.map +1 -1
  233. package/dist/tabs/tabs-tab.d.mts +1 -1
  234. package/dist/tabs/tabs-tab.d.mts.map +1 -1
  235. package/dist/tabs/tabs-tab.mjs +6 -3
  236. package/dist/tabs/tabs-tab.mjs.map +1 -1
  237. package/dist/tooltip/tooltip-popup.d.mts.map +1 -1
  238. package/dist/tooltip/tooltip-popup.mjs +2 -7
  239. package/dist/tooltip/tooltip-popup.mjs.map +1 -1
  240. package/dist/tooltip/use-tooltip-interaction.mjs +5 -17
  241. package/dist/tooltip/use-tooltip-interaction.mjs.map +1 -1
  242. package/package.json +6 -1
@@ -0,0 +1,101 @@
1
+ "use client";
2
+ import { useRender } from "../internal/use-render.mjs";
3
+ import { useFormReset } from "../internal/use-form-reset.mjs";
4
+ import { useSelectContext } from "./select-context.mjs";
5
+ import { useSelectPartRef } from "./select-dom.mjs";
6
+ import { useLayoutEffect, useMemo } from "react";
7
+ //#region src/select/select-value.tsx
8
+ const isDev = process.env.NODE_ENV !== "production";
9
+ /** Where the browser clones options into `<selectedcontent>` (Chromium 135+, Safari 27). */
10
+ function supportsSelectedContent() {
11
+ return typeof globalThis !== "undefined" && "HTMLSelectedContentElement" in globalThis;
12
+ }
13
+ /**
14
+ * Whether a mutation record changes what `<selectedcontent>` shows: a record inside the selected
15
+ * option. Attribute records on the option element itself are ignored (option attributes are not
16
+ * cloned, and the library writes `data-*` there); records inside `<selectedcontent>` are never
17
+ * inside an option, so the refresh's own clone does not loop.
18
+ */
19
+ function touchesSelected(record, selected) {
20
+ if (record.type === "attributes" && record.target === selected) return false;
21
+ return selected.contains(record.target);
22
+ }
23
+ /** Re-clones the selected option into `<selectedcontent>`. */
24
+ function refresh(select) {
25
+ if (select.selectedIndex === -1) return;
26
+ select.value = select.value;
27
+ }
28
+ /**
29
+ * `<selectedcontent>`: the browser clones the selected option's content into it (inside
30
+ * `Select.Trigger`). It has no `children`. Rendered with `suppressHydrationWarning` and an empty
31
+ * `dangerouslySetInnerHTML` so that hydration accepts the content the parser cloned before React
32
+ * ran (design/components/select.md 3.3).
33
+ *
34
+ * Also keeps it fresh: the platform re-clones only on selection changes, so when React changes
35
+ * the selected option's content (a label, a state function, a Suspense boundary resolving inside
36
+ * it), a `MutationObserver` re-assigns `select.value = select.value`, which re-clones without
37
+ * events and without touching the selection or an open picker (6.3). The same refresh follows a
38
+ * form reset, which Chromium does not re-clone for. Not installed where
39
+ * `HTMLSelectedContentElement` is undefined (Firefox today, jsdom): nothing is cloned there.
40
+ *
41
+ * Owned keys: `dangerouslySetInnerHTML`, `suppressHydrationWarning`, `children`.
42
+ */
43
+ function SelectValue({ className, style, render, ...rest }) {
44
+ const ctx = useSelectContext("Select.Value");
45
+ const { ref, node } = useSelectPartRef("Select.Value", "selectedcontent");
46
+ useLayoutEffect(() => {
47
+ const el = node.current;
48
+ if (el === null) return;
49
+ const select = el.closest("select");
50
+ if (select === null) {
51
+ if (isDev) console.error("Select.Value: rendered outside the Select.Root's <select>; put it inside Select.Trigger, the select's first child.");
52
+ return;
53
+ }
54
+ if (!supportsSelectedContent()) return;
55
+ const observer = new MutationObserver((records) => {
56
+ const selected = select.options[select.selectedIndex];
57
+ if (selected === void 0) return;
58
+ if (records.some((record) => touchesSelected(record, selected))) refresh(select);
59
+ });
60
+ observer.observe(select, {
61
+ childList: true,
62
+ characterData: true,
63
+ attributes: true,
64
+ subtree: true
65
+ });
66
+ return () => {
67
+ observer.disconnect();
68
+ };
69
+ }, [node]);
70
+ const selectRef = useMemo(() => ({ get current() {
71
+ return node.current?.closest("select") ?? null;
72
+ } }), [node]);
73
+ useFormReset(selectRef, () => {
74
+ const select = selectRef.current;
75
+ if (select !== null && supportsSelectedContent()) refresh(select);
76
+ });
77
+ const state = {
78
+ value: ctx.optimistic,
79
+ pending: ctx.pending,
80
+ disabled: ctx.disabled
81
+ };
82
+ return useRender({
83
+ defaultElement: "selectedcontent",
84
+ render,
85
+ state,
86
+ className,
87
+ style,
88
+ children: void 0,
89
+ props: rest,
90
+ libraryProps: {
91
+ suppressHydrationWarning: true,
92
+ dangerouslySetInnerHTML: { __html: "" }
93
+ },
94
+ ref
95
+ });
96
+ }
97
+ SelectValue.displayName = "SelectValue";
98
+ //#endregion
99
+ export { SelectValue };
100
+
101
+ //# sourceMappingURL=select-value.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"select-value.mjs","names":[],"sources":["../../src/select/select-value.tsx"],"sourcesContent":["\"use client\";\nimport { useLayoutEffect, useMemo } from \"react\";\nimport type { ComponentPropsWithRef, CSSProperties } from \"react\";\nimport type { RenderProp, StateValue } from \"../internal/types.js\";\nimport { useFormReset } from \"../internal/use-form-reset.js\";\nimport { useRender } from \"../internal/use-render.js\";\nimport { useSelectContext } from \"./select-context.js\";\nimport { useSelectPartRef } from \"./select-dom.js\";\nimport type { SelectRootState } from \"./select-root.js\";\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\nexport interface SelectValueProps extends Omit<\n ComponentPropsWithRef<\"selectedcontent\">,\n \"children\" | \"dangerouslySetInnerHTML\" | \"className\" | \"style\"\n> {\n className?: StateValue<string | undefined, SelectRootState>;\n style?: StateValue<CSSProperties | undefined, SelectRootState>;\n /** Replace the rendered element. Must keep a `<selectedcontent>`. */\n render?: RenderProp<ComponentPropsWithRef<\"selectedcontent\">, SelectRootState>;\n}\n\n/** Where the browser clones options into `<selectedcontent>` (Chromium 135+, Safari 27). */\nfunction supportsSelectedContent(): boolean {\n return typeof globalThis !== \"undefined\" && \"HTMLSelectedContentElement\" in globalThis;\n}\n\n/**\n * Whether a mutation record changes what `<selectedcontent>` shows: a record inside the selected\n * option. Attribute records on the option element itself are ignored (option attributes are not\n * cloned, and the library writes `data-*` there); records inside `<selectedcontent>` are never\n * inside an option, so the refresh's own clone does not loop.\n */\nfunction touchesSelected(record: MutationRecord, selected: HTMLOptionElement): boolean {\n if (record.type === \"attributes\" && record.target === selected) return false;\n return selected.contains(record.target);\n}\n\n/** Re-clones the selected option into `<selectedcontent>`. */\nfunction refresh(select: HTMLSelectElement): void {\n // Nothing selected: assigning the value would select the placeholder (probe case 7).\n if (select.selectedIndex === -1) return;\n // The assignment is the re-clone trigger; it fires no event and keeps the selection.\n const current = select.value;\n select.value = current;\n}\n\n/**\n * `<selectedcontent>`: the browser clones the selected option's content into it (inside\n * `Select.Trigger`). It has no `children`. Rendered with `suppressHydrationWarning` and an empty\n * `dangerouslySetInnerHTML` so that hydration accepts the content the parser cloned before React\n * ran (design/components/select.md 3.3).\n *\n * Also keeps it fresh: the platform re-clones only on selection changes, so when React changes\n * the selected option's content (a label, a state function, a Suspense boundary resolving inside\n * it), a `MutationObserver` re-assigns `select.value = select.value`, which re-clones without\n * events and without touching the selection or an open picker (6.3). The same refresh follows a\n * form reset, which Chromium does not re-clone for. Not installed where\n * `HTMLSelectedContentElement` is undefined (Firefox today, jsdom): nothing is cloned there.\n *\n * Owned keys: `dangerouslySetInnerHTML`, `suppressHydrationWarning`, `children`.\n */\nexport function SelectValue({ className, style, render, ...rest }: SelectValueProps) {\n const ctx = useSelectContext(\"Select.Value\");\n const { ref, node } = useSelectPartRef<HTMLElement>(\"Select.Value\", \"selectedcontent\");\n\n useLayoutEffect(() => {\n const el = node.current;\n if (el === null) return;\n const select = el.closest(\"select\");\n if (select === null) {\n if (isDev) {\n console.error(\n \"Select.Value: rendered outside the Select.Root's <select>; put it inside \" +\n \"Select.Trigger, the select's first child.\",\n );\n }\n return;\n }\n if (!supportsSelectedContent()) return;\n const observer = new MutationObserver((records) => {\n const selected = select.options[select.selectedIndex];\n if (selected === undefined) return;\n if (records.some((record) => touchesSelected(record, selected))) refresh(select);\n });\n observer.observe(select, {\n childList: true,\n characterData: true,\n attributes: true,\n subtree: true,\n });\n return () => {\n observer.disconnect();\n };\n }, [node]);\n\n // A form reset changes the selection without re-cloning (Chromium 156: `form.reset()` leaves\n // the old option's content in <selectedcontent>; memo/developer/select.md). `useFormReset`\n // listens on the select's form owner itself (so neither `stopPropagation()` on the form nor a\n // shadow root hides the event), follows a changed owner or `form` attribute, and calls back in\n // a microtask after an uncancelled reset. React's own reset after a form action is a\n // `form.reset()` too.\n const selectRef = useMemo(\n () => ({\n get current(): HTMLSelectElement | null {\n return node.current?.closest(\"select\") ?? null;\n },\n }),\n [node],\n );\n useFormReset(selectRef, () => {\n const select = selectRef.current;\n if (select !== null && supportsSelectedContent()) refresh(select);\n });\n\n const state: SelectRootState = {\n value: ctx.optimistic,\n pending: ctx.pending,\n disabled: ctx.disabled,\n };\n return useRender<ComponentPropsWithRef<\"selectedcontent\">, SelectRootState>({\n defaultElement: \"selectedcontent\",\n render,\n state,\n className,\n style,\n children: undefined,\n props: rest,\n libraryProps: {\n suppressHydrationWarning: true,\n dangerouslySetInnerHTML: { __html: \"\" },\n },\n ref,\n });\n}\n\nSelectValue.displayName = \"SelectValue\";\n"],"mappings":";;;;;;;AAUA,MAAM,QAAQ,QAAQ,IAAI,aAAa;;AAavC,SAAS,0BAAmC;CAC1C,OAAO,OAAO,eAAe,eAAe,gCAAgC;AAC9E;;;;;;;AAQA,SAAS,gBAAgB,QAAwB,UAAsC;CACrF,IAAI,OAAO,SAAS,gBAAgB,OAAO,WAAW,UAAU,OAAO;CACvE,OAAO,SAAS,SAAS,OAAO,MAAM;AACxC;;AAGA,SAAS,QAAQ,QAAiC;CAEhD,IAAI,OAAO,kBAAkB,IAAI;CAGjC,OAAO,QADS,OAAO;AAEzB;;;;;;;;;;;;;;;;AAiBA,SAAgB,YAAY,EAAE,WAAW,OAAO,QAAQ,GAAG,QAA0B;CACnF,MAAM,MAAM,iBAAiB,cAAc;CAC3C,MAAM,EAAE,KAAK,SAAS,iBAA8B,gBAAgB,iBAAiB;CAErF,sBAAsB;EACpB,MAAM,KAAK,KAAK;EAChB,IAAI,OAAO,MAAM;EACjB,MAAM,SAAS,GAAG,QAAQ,QAAQ;EAClC,IAAI,WAAW,MAAM;GACnB,IAAI,OACF,QAAQ,MACN,oHAEF;GAEF;EACF;EACA,IAAI,CAAC,wBAAwB,GAAG;EAChC,MAAM,WAAW,IAAI,kBAAkB,YAAY;GACjD,MAAM,WAAW,OAAO,QAAQ,OAAO;GACvC,IAAI,aAAa,KAAA,GAAW;GAC5B,IAAI,QAAQ,MAAM,WAAW,gBAAgB,QAAQ,QAAQ,CAAC,GAAG,QAAQ,MAAM;EACjF,CAAC;EACD,SAAS,QAAQ,QAAQ;GACvB,WAAW;GACX,eAAe;GACf,YAAY;GACZ,SAAS;EACX,CAAC;EACD,aAAa;GACX,SAAS,WAAW;EACtB;CACF,GAAG,CAAC,IAAI,CAAC;CAQT,MAAM,YAAY,eACT,EACL,IAAI,UAAoC;EACtC,OAAO,KAAK,SAAS,QAAQ,QAAQ,KAAK;CAC5C,EACF,IACA,CAAC,IAAI,CACP;CACA,aAAa,iBAAiB;EAC5B,MAAM,SAAS,UAAU;EACzB,IAAI,WAAW,QAAQ,wBAAwB,GAAG,QAAQ,MAAM;CAClE,CAAC;CAED,MAAM,QAAyB;EAC7B,OAAO,IAAI;EACX,SAAS,IAAI;EACb,UAAU,IAAI;CAChB;CACA,OAAO,UAAqE;EAC1E,gBAAgB;EAChB;EACA;EACA;EACA;EACA,UAAU,KAAA;EACV,OAAO;EACP,cAAc;GACZ,0BAA0B;GAC1B,yBAAyB,EAAE,QAAQ,GAAG;EACxC;EACA;CACF,CAAC;AACH;AAEA,YAAY,cAAc"}
@@ -2,12 +2,13 @@
2
2
  import { createContext, use } from "react";
3
3
  //#region src/tabs/tabs-context.ts
4
4
  const TabsContext = createContext(null);
5
+ const TabsListContext = createContext(null);
5
6
  function useTabsContext(part) {
6
7
  const context = use(TabsContext);
7
8
  if (context === null) throw new Error(`${part} must be rendered inside a Tabs.Root.`);
8
9
  return context;
9
10
  }
10
11
  //#endregion
11
- export { TabsContext, useTabsContext };
12
+ export { TabsContext, TabsListContext, useTabsContext };
12
13
 
13
14
  //# sourceMappingURL=tabs-context.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"tabs-context.mjs","names":[],"sources":["../../src/tabs/tabs-context.ts"],"sourcesContent":["\"use client\";\nimport { createContext, use } from \"react\";\n\nexport type TabsOrientation = \"horizontal\" | \"vertical\";\nexport type TabsActivationMode = \"automatic\" | \"manual\";\n\n/** Why a Tabs change was dispatched (design section 3.1). */\nexport type TabsChangeReason = \"activate\" | \"platform\";\n\nexport interface TabsContextValue {\n readonly rootId: string;\n /** Committed value: Panels render this. */\n readonly committed: string | null;\n /** Optimistic value: Tabs and the Indicator render this. */\n readonly optimistic: string | null;\n readonly pending: boolean;\n readonly orientation: TabsOrientation;\n readonly activationMode: TabsActivationMode;\n readonly loop: boolean;\n readonly disabled: boolean;\n /** Dispatches a change. No-op if `value` is the optimistic value. Never checks `disabled`. */\n readonly select: (value: string, event: Event | undefined, reason: TabsChangeReason) => void;\n}\n\nexport const TabsContext = createContext<TabsContextValue | null>(null);\n\nexport function useTabsContext(part: string): TabsContextValue {\n const context = use(TabsContext);\n if (context === null) {\n throw new Error(`${part} must be rendered inside a Tabs.Root.`);\n }\n return context;\n}\n"],"mappings":";;;AAwBA,MAAa,cAAc,cAAuC,IAAI;AAEtE,SAAgB,eAAe,MAAgC;CAC7D,MAAM,UAAU,IAAI,WAAW;CAC/B,IAAI,YAAY,MACd,MAAM,IAAI,MAAM,GAAG,KAAK,sCAAsC;CAEhE,OAAO;AACT"}
1
+ {"version":3,"file":"tabs-context.mjs","names":[],"sources":["../../src/tabs/tabs-context.ts"],"sourcesContent":["\"use client\";\nimport { createContext, use } from \"react\";\n\nexport type TabsOrientation = \"horizontal\" | \"vertical\";\nexport type TabsActivationMode = \"automatic\" | \"manual\";\n\n/** Why a Tabs change was dispatched (design section 3.1). */\nexport type TabsChangeReason = \"activate\" | \"platform\";\n\nexport interface TabsContextValue {\n readonly rootId: string;\n /** Committed value: Panels render this. */\n readonly committed: string | null;\n /** Optimistic value: Tabs and the Indicator render this. */\n readonly optimistic: string | null;\n readonly pending: boolean;\n readonly orientation: TabsOrientation;\n readonly activationMode: TabsActivationMode;\n readonly loop: boolean;\n readonly disabled: boolean;\n /** Dispatches a change. No-op if `value` is the optimistic value. Never checks `disabled`. */\n readonly select: (value: string, event: Event | undefined, reason: TabsChangeReason) => void;\n}\n\nexport const TabsContext = createContext<TabsContextValue | null>(null);\n\n/** What a Tabs.List tells its Tabs about `experimental_focusgroup`. */\nexport interface TabsListContextValue {\n /** `experimental_focusgroup` is on: the selected Tab carries `focusgroupstart`. */\n readonly focusgroup: boolean;\n /**\n * The flag is on and the engine implements `focusgroup` (detected in an effect, so false during\n * SSR and hydration). Unselected Tabs then drop `tabindex=\"-1\"`: Chromium leaves an element with\n * a negative tabindex out of the focusgroup, so roving tabindex would disable the native arrows.\n */\n readonly nativeFocusgroup: boolean;\n}\n\nexport const TabsListContext = createContext<TabsListContextValue | null>(null);\n\nexport function useTabsContext(part: string): TabsContextValue {\n const context = use(TabsContext);\n if (context === null) {\n throw new Error(`${part} must be rendered inside a Tabs.Root.`);\n }\n return context;\n}\n"],"mappings":";;;AAwBA,MAAa,cAAc,cAAuC,IAAI;AActE,MAAa,kBAAkB,cAA2C,IAAI;AAE9E,SAAgB,eAAe,MAAgC;CAC7D,MAAM,UAAU,IAAI,WAAW;CAC/B,IAAI,YAAY,MACd,MAAM,IAAI,MAAM,GAAG,KAAK,sCAAsC;CAEhE,OAAO;AACT"}
@@ -26,7 +26,7 @@ export interface TabsListProps extends Omit<ComponentPropsWithRef<"div">, "onKey
26
26
  * `aria-orientation`, `data-orientation`, `data-disabled`, `tabIndex`, `onKeyDown`, `onFocus`,
27
27
  * and `focusgroup` when `experimental_focusgroup` is on.
28
28
  */
29
- export declare function TabsList({ experimental_focusgroup, className, style, children, render, ...rest }: TabsListProps): ReactNode;
29
+ export declare function TabsList({ experimental_focusgroup, className, style, children, render, ...rest }: TabsListProps): import("react").JSX.Element;
30
30
  export declare namespace TabsList {
31
31
  var displayName: string;
32
32
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tabs-list.d.mts","names":[],"sources":["../../src/tabs/tabs-list.tsx"],"mappings":";;;;iBAgBiB;WACN,aAAa;WACb;;iBAGM,sBAAsB,KACrC;;;;;;EAQA;EACA,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;EAC9C,WAAW,WAAW,WAAW;EACjC,SAAS,WAAW,8BAA8B;;;;;;;;;;wBA4CpC,WACd,yBACA,WACA,OACA,UACA,WACG,QACF,gBAAa;yBAPA"}
1
+ {"version":3,"file":"tabs-list.d.mts","names":[],"sources":["../../src/tabs/tabs-list.tsx"],"mappings":";;;;iBAgBiB;WACN,aAAa;WACb;;iBAGM,sBAAsB,KACrC;;;;;;EAQA;EACA,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;EAC9C,WAAW,WAAW,WAAW;EACjC,SAAS,WAAW,8BAA8B;;;;;;;;;;wBA+CpC,WACd,yBACA,WACA,OACA,UACA,WACG,QACF,gCAAa,IAAA;yBAPA"}
@@ -2,8 +2,9 @@
2
2
  import { dataAttr } from "../internal/data-attr.mjs";
3
3
  import { useRender } from "../internal/use-render.mjs";
4
4
  import { useDevWarning } from "../internal/use-dev-warning.mjs";
5
- import { useTabsContext } from "./tabs-context.mjs";
6
- import { useEffect, useLayoutEffect, useRef, useState } from "react";
5
+ import { TabsListContext, useTabsContext } from "./tabs-context.mjs";
6
+ import { useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
7
+ import { jsx } from "react/jsx-runtime";
7
8
  //#region src/tabs/tabs-list.tsx
8
9
  function resolveKey(key, orientation, rtl) {
9
10
  switch (key) {
@@ -23,6 +24,8 @@ function tabsOf(list) {
23
24
  function focusTab(tab) {
24
25
  tab.focus({ focusVisible: true });
25
26
  }
27
+ /** Engine capabilities never change while the page lives. */
28
+ const subscribeNever = () => () => {};
26
29
  /**
27
30
  * The `role="tablist"` container. Owns arrow-key / Home / End navigation between Tabs: it only
28
31
  * moves focus; automatic activation happens in the Tab's focus handler.
@@ -33,10 +36,11 @@ function focusTab(tab) {
33
36
  */
34
37
  function TabsList({ experimental_focusgroup = false, className, style, children, render, ...rest }) {
35
38
  const { orientation, loop, disabled, optimistic } = useTabsContext("Tabs.List");
36
- const nativeFocusgroup = useRef(false);
37
- useEffect(() => {
38
- nativeFocusgroup.current = experimental_focusgroup && "focusgroup" in HTMLElement.prototype;
39
- }, [experimental_focusgroup]);
39
+ const nativeFocusgroup = useSyncExternalStore(subscribeNever, () => experimental_focusgroup && "focusGroup" in HTMLElement.prototype, () => false);
40
+ const listContext = useMemo(() => ({
41
+ focusgroup: experimental_focusgroup,
42
+ nativeFocusgroup
43
+ }), [experimental_focusgroup, nativeFocusgroup]);
40
44
  const listRef = useRef(null);
41
45
  const [unmatched, setUnmatched] = useState(false);
42
46
  useLayoutEffect(() => {
@@ -62,7 +66,7 @@ function TabsList({ experimental_focusgroup = false, className, style, children,
62
66
  const rtl = orientation === "horizontal" && getComputedStyle(list).direction === "rtl";
63
67
  const move = resolveKey(event.key, orientation, rtl);
64
68
  if (move === null) return;
65
- if (nativeFocusgroup.current) return;
69
+ if (nativeFocusgroup) return;
66
70
  event.preventDefault();
67
71
  const tabs = tabsOf(list);
68
72
  if (tabs.length === 0) return;
@@ -93,8 +97,8 @@ function TabsList({ experimental_focusgroup = false, className, style, children,
93
97
  const first = tabs.find((tab) => tab.getAttribute("aria-disabled") !== "true") ?? tabs[0];
94
98
  if (first !== void 0) focusTab(first);
95
99
  };
96
- const focusgroup = experimental_focusgroup ? `tablist ${orientation === "vertical" ? "block" : "inline"} ${loop ? "wrap" : "nowrap"}` : void 0;
97
- return useRender({
100
+ const focusgroup = experimental_focusgroup ? `tablist ${orientation === "vertical" ? "block" : "inline"} ${loop ? "wrap" : "nowrap"} nomemory` : void 0;
101
+ const element = useRender({
98
102
  defaultElement: "div",
99
103
  render,
100
104
  state,
@@ -114,6 +118,10 @@ function TabsList({ experimental_focusgroup = false, className, style, children,
114
118
  },
115
119
  ref: listRef
116
120
  });
121
+ return /* @__PURE__ */ jsx(TabsListContext, {
122
+ value: listContext,
123
+ children: element
124
+ });
117
125
  }
118
126
  TabsList.displayName = "TabsList";
119
127
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"tabs-list.mjs","names":[],"sources":["../../src/tabs/tabs-list.tsx"],"sourcesContent":["\"use client\";\nimport { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport type {\n ComponentPropsWithRef,\n CSSProperties,\n FocusEvent,\n KeyboardEvent,\n ReactNode,\n} from \"react\";\nimport { dataAttr } from \"../internal/data-attr.js\";\nimport type { RenderProp, StateValue } from \"../internal/types.js\";\nimport { useDevWarning } from \"../internal/use-dev-warning.js\";\nimport { useRender } from \"../internal/use-render.js\";\nimport { useTabsContext } from \"./tabs-context.js\";\nimport type { TabsOrientation } from \"./tabs-context.js\";\n\nexport interface TabsListState {\n readonly orientation: TabsOrientation;\n readonly disabled: boolean;\n}\n\nexport interface TabsListProps extends Omit<\n ComponentPropsWithRef<\"div\">,\n \"onKeyDown\" | \"onFocus\" | \"className\" | \"style\" | \"children\"\n> {\n /**\n * Render `focusgroup=\"tablist …\"` and, where the browser implements it, let the browser handle\n * arrow keys, Home and End. Chromium 150+ only; elsewhere the JS path stays active.\n * Default `false`.\n */\n experimental_focusgroup?: boolean;\n className?: StateValue<string | undefined, TabsListState>;\n style?: StateValue<CSSProperties | undefined, TabsListState>;\n children?: StateValue<ReactNode, TabsListState>;\n render?: RenderProp<ComponentPropsWithRef<\"div\">, TabsListState>;\n}\n\ntype Move = \"next\" | \"prev\" | \"first\" | \"last\";\n\nfunction resolveKey(key: string, orientation: TabsOrientation, rtl: boolean): Move | null {\n switch (key) {\n case \"Home\":\n return \"first\";\n case \"End\":\n return \"last\";\n case \"ArrowRight\":\n return orientation === \"horizontal\" ? (rtl ? \"prev\" : \"next\") : null;\n case \"ArrowLeft\":\n return orientation === \"horizontal\" ? (rtl ? \"next\" : \"prev\") : null;\n case \"ArrowDown\":\n return orientation === \"vertical\" ? \"next\" : null;\n case \"ArrowUp\":\n return orientation === \"vertical\" ? \"prev\" : null;\n default:\n return null;\n }\n}\n\n/** The tabs that belong to `list` (not to a nested tablist), in DOM order. Disabled included. */\nfunction tabsOf(list: Element): HTMLElement[] {\n return Array.from(list.querySelectorAll<HTMLElement>('[role=\"tab\"]')).filter(\n (tab) => tab.closest('[role=\"tablist\"]') === list,\n );\n}\n\nfunction focusTab(tab: HTMLElement): void {\n // `focusVisible` shows the ring even where the engine's heuristic would not (survey: adopt).\n tab.focus({ focusVisible: true } as FocusOptions);\n}\n\n/**\n * The `role=\"tablist\"` container. Owns arrow-key / Home / End navigation between Tabs: it only\n * moves focus; automatic activation happens in the Tab's focus handler.\n *\n * Owned keys (a `render` element cannot override them, conventions 1.2): `role`,\n * `aria-orientation`, `data-orientation`, `data-disabled`, `tabIndex`, `onKeyDown`, `onFocus`,\n * and `focusgroup` when `experimental_focusgroup` is on.\n */\nexport function TabsList({\n experimental_focusgroup = false,\n className,\n style,\n children,\n render,\n ...rest\n}: TabsListProps) {\n const ctx = useTabsContext(\"Tabs.List\");\n const { orientation, loop, disabled, optimistic } = ctx;\n\n // Feature detection only when the flag is on, and only in an effect (conventions 8): markup\n // never differs between server and client.\n const nativeFocusgroup = useRef(false);\n useEffect(() => {\n nativeFocusgroup.current = experimental_focusgroup && \"focusgroup\" in HTMLElement.prototype;\n }, [experimental_focusgroup]);\n\n // A value that matches no Tab (a controlled `value` for a route without a tab, design 11.5)\n // leaves no tab with tabindex=0, so the List takes over as the tab stop exactly as for `null`.\n // Read from the DOM (Tabs register nowhere) whenever the value or the List's children change; a\n // Tab added or removed by a component below that re-renders on its own is picked up on the\n // List's next render. The state only changes when the answer does.\n const listRef = useRef<HTMLElement | null>(null);\n const [unmatched, setUnmatched] = useState(false);\n useLayoutEffect(() => {\n const list = listRef.current;\n const next =\n optimistic !== null &&\n list !== null &&\n !tabsOf(list).some((tab) => tab.getAttribute(\"tabindex\") === \"0\");\n if (next !== unmatched) setUnmatched(next);\n }, [optimistic, children, unmatched]);\n useDevWarning(\n unmatched,\n `Tabs: the value ${JSON.stringify(optimistic)} matches no Tabs.Tab. No tab is selected; the ` +\n \"tablist itself is the tab stop. Pass null to select nothing on purpose.\",\n );\n const listIsTabStop = optimistic === null || unmatched;\n\n const state: TabsListState = { orientation, disabled };\n\n const onKeyDown = (event: KeyboardEvent<HTMLElement>) => {\n if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) return;\n const list = event.currentTarget;\n const target = event.target as Element;\n if (target.getAttribute(\"role\") !== \"tab\" || target.closest('[role=\"tablist\"]') !== list) {\n return;\n }\n const rtl = orientation === \"horizontal\" && getComputedStyle(list).direction === \"rtl\";\n const move = resolveKey(event.key, orientation, rtl);\n if (move === null) return;\n // The browser moves focus itself where `focusgroup` is implemented.\n if (nativeFocusgroup.current) return;\n\n event.preventDefault();\n const tabs = tabsOf(list);\n if (tabs.length === 0) return;\n const index = tabs.indexOf(target as HTMLElement);\n const last = tabs.length - 1;\n let nextIndex: number;\n switch (move) {\n case \"first\":\n nextIndex = 0;\n break;\n case \"last\":\n nextIndex = last;\n break;\n case \"next\":\n nextIndex = index >= last ? (loop ? 0 : last) : index + 1;\n break;\n case \"prev\":\n nextIndex = index <= 0 ? (loop ? last : 0) : index - 1;\n break;\n }\n const next = tabs[nextIndex];\n if (next !== undefined && next !== target) focusTab(next);\n };\n\n // With no selected tab (value null or matching no Tab) no Tab has tabindex=0, so the list\n // itself is the tab stop and hands focus to the first enabled tab (design section 8). Only when\n // focus arrives from outside the list: Shift+Tab from a tab inside would otherwise bounce\n // straight back. The redirect never selects: the Tab only dispatches on focus that moves\n // between tabs.\n const onFocus = (event: FocusEvent<HTMLElement>) => {\n const list = event.currentTarget;\n if (event.target !== list || !listIsTabStop) return;\n const from = event.relatedTarget;\n if (from instanceof Node && list.contains(from)) return;\n const tabs = tabsOf(list);\n const first = tabs.find((tab) => tab.getAttribute(\"aria-disabled\") !== \"true\") ?? tabs[0];\n if (first !== undefined) focusTab(first);\n };\n\n const focusgroup = experimental_focusgroup\n ? `tablist ${orientation === \"vertical\" ? \"block\" : \"inline\"} ${loop ? \"wrap\" : \"nowrap\"}`\n : undefined;\n\n return useRender<ComponentPropsWithRef<\"div\">, TabsListState>({\n defaultElement: \"div\",\n render,\n state,\n className,\n style,\n children,\n props: rest,\n libraryProps: {\n role: \"tablist\",\n \"aria-orientation\": orientation,\n \"data-orientation\": orientation,\n \"data-disabled\": dataAttr(disabled),\n tabIndex: listIsTabStop ? 0 : undefined,\n // Owned only while the flag is on; otherwise the key is omitted, not `undefined`, so it is not\n // owned (conventions 1.2).\n ...(focusgroup !== undefined && { focusgroup }),\n onKeyDown,\n onFocus,\n },\n ref: listRef,\n });\n}\n\nTabsList.displayName = \"TabsList\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"tabs-list.mjs","names":[],"sources":["../../src/tabs/tabs-list.tsx"],"sourcesContent":["\"use client\";\nimport { useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport type {\n ComponentPropsWithRef,\n CSSProperties,\n FocusEvent,\n KeyboardEvent,\n ReactNode,\n} from \"react\";\nimport { dataAttr } from \"../internal/data-attr.js\";\nimport type { RenderProp, StateValue } from \"../internal/types.js\";\nimport { useDevWarning } from \"../internal/use-dev-warning.js\";\nimport { useRender } from \"../internal/use-render.js\";\nimport { TabsListContext, useTabsContext } from \"./tabs-context.js\";\nimport type { TabsListContextValue, TabsOrientation } from \"./tabs-context.js\";\n\nexport interface TabsListState {\n readonly orientation: TabsOrientation;\n readonly disabled: boolean;\n}\n\nexport interface TabsListProps extends Omit<\n ComponentPropsWithRef<\"div\">,\n \"onKeyDown\" | \"onFocus\" | \"className\" | \"style\" | \"children\"\n> {\n /**\n * Render `focusgroup=\"tablist …\"` and, where the browser implements it, let the browser handle\n * arrow keys, Home and End. Chromium 150+ only; elsewhere the JS path stays active.\n * Default `false`.\n */\n experimental_focusgroup?: boolean;\n className?: StateValue<string | undefined, TabsListState>;\n style?: StateValue<CSSProperties | undefined, TabsListState>;\n children?: StateValue<ReactNode, TabsListState>;\n render?: RenderProp<ComponentPropsWithRef<\"div\">, TabsListState>;\n}\n\ntype Move = \"next\" | \"prev\" | \"first\" | \"last\";\n\nfunction resolveKey(key: string, orientation: TabsOrientation, rtl: boolean): Move | null {\n switch (key) {\n case \"Home\":\n return \"first\";\n case \"End\":\n return \"last\";\n case \"ArrowRight\":\n return orientation === \"horizontal\" ? (rtl ? \"prev\" : \"next\") : null;\n case \"ArrowLeft\":\n return orientation === \"horizontal\" ? (rtl ? \"next\" : \"prev\") : null;\n case \"ArrowDown\":\n return orientation === \"vertical\" ? \"next\" : null;\n case \"ArrowUp\":\n return orientation === \"vertical\" ? \"prev\" : null;\n default:\n return null;\n }\n}\n\n/** The tabs that belong to `list` (not to a nested tablist), in DOM order. Disabled included. */\nfunction tabsOf(list: Element): HTMLElement[] {\n return Array.from(list.querySelectorAll<HTMLElement>('[role=\"tab\"]')).filter(\n (tab) => tab.closest('[role=\"tablist\"]') === list,\n );\n}\n\nfunction focusTab(tab: HTMLElement): void {\n // `focusVisible` shows the ring even where the engine's heuristic would not (survey: adopt).\n tab.focus({ focusVisible: true } as FocusOptions);\n}\n\n/** Engine capabilities never change while the page lives. */\nconst subscribeNever = () => () => {};\n\n/**\n * The `role=\"tablist\"` container. Owns arrow-key / Home / End navigation between Tabs: it only\n * moves focus; automatic activation happens in the Tab's focus handler.\n *\n * Owned keys (a `render` element cannot override them, conventions 1.2): `role`,\n * `aria-orientation`, `data-orientation`, `data-disabled`, `tabIndex`, `onKeyDown`, `onFocus`,\n * and `focusgroup` when `experimental_focusgroup` is on.\n */\nexport function TabsList({\n experimental_focusgroup = false,\n className,\n style,\n children,\n render,\n ...rest\n}: TabsListProps) {\n const ctx = useTabsContext(\"Tabs.List\");\n const { orientation, loop, disabled, optimistic } = ctx;\n\n // Feature detection only when the flag is on, and never during SSR or hydration (conventions\n // 8): the server snapshot is `false`, so hydration markup matches the server and React re-renders\n // with the client answer afterwards. The IDL attribute is camel-cased `focusGroup`\n // (`\"focusgroup\" in HTMLElement.prototype` is always false, issue #43). The Tabs read it too:\n // they drop `tabindex=\"-1\"` on the native path (TabsListContextValue).\n const nativeFocusgroup = useSyncExternalStore(\n subscribeNever,\n () => experimental_focusgroup && \"focusGroup\" in HTMLElement.prototype,\n () => false,\n );\n const listContext = useMemo<TabsListContextValue>(\n () => ({ focusgroup: experimental_focusgroup, nativeFocusgroup }),\n [experimental_focusgroup, nativeFocusgroup],\n );\n\n // A value that matches no Tab (a controlled `value` for a route without a tab, design 11.5)\n // leaves no tab with tabindex=0, so the List takes over as the tab stop exactly as for `null`.\n // Read from the DOM (Tabs register nowhere) whenever the value or the List's children change; a\n // Tab added or removed by a component below that re-renders on its own is picked up on the\n // List's next render. The state only changes when the answer does.\n const listRef = useRef<HTMLElement | null>(null);\n const [unmatched, setUnmatched] = useState(false);\n useLayoutEffect(() => {\n const list = listRef.current;\n const next =\n optimistic !== null &&\n list !== null &&\n !tabsOf(list).some((tab) => tab.getAttribute(\"tabindex\") === \"0\");\n if (next !== unmatched) setUnmatched(next);\n }, [optimistic, children, unmatched]);\n useDevWarning(\n unmatched,\n `Tabs: the value ${JSON.stringify(optimistic)} matches no Tabs.Tab. No tab is selected; the ` +\n \"tablist itself is the tab stop. Pass null to select nothing on purpose.\",\n );\n const listIsTabStop = optimistic === null || unmatched;\n\n const state: TabsListState = { orientation, disabled };\n\n const onKeyDown = (event: KeyboardEvent<HTMLElement>) => {\n if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) return;\n const list = event.currentTarget;\n const target = event.target as Element;\n if (target.getAttribute(\"role\") !== \"tab\" || target.closest('[role=\"tablist\"]') !== list) {\n return;\n }\n const rtl = orientation === \"horizontal\" && getComputedStyle(list).direction === \"rtl\";\n const move = resolveKey(event.key, orientation, rtl);\n if (move === null) return;\n // The browser moves focus itself where `focusgroup` is implemented.\n if (nativeFocusgroup) return;\n\n event.preventDefault();\n const tabs = tabsOf(list);\n if (tabs.length === 0) return;\n const index = tabs.indexOf(target as HTMLElement);\n const last = tabs.length - 1;\n let nextIndex: number;\n switch (move) {\n case \"first\":\n nextIndex = 0;\n break;\n case \"last\":\n nextIndex = last;\n break;\n case \"next\":\n nextIndex = index >= last ? (loop ? 0 : last) : index + 1;\n break;\n case \"prev\":\n nextIndex = index <= 0 ? (loop ? last : 0) : index - 1;\n break;\n }\n const next = tabs[nextIndex];\n if (next !== undefined && next !== target) focusTab(next);\n };\n\n // With no selected tab (value null or matching no Tab) no Tab has tabindex=0, so the list\n // itself is the tab stop and hands focus to the first enabled tab (design section 8). Only when\n // focus arrives from outside the list: Shift+Tab from a tab inside would otherwise bounce\n // straight back. The redirect never selects: the Tab only dispatches on focus that moves\n // between tabs.\n const onFocus = (event: FocusEvent<HTMLElement>) => {\n const list = event.currentTarget;\n if (event.target !== list || !listIsTabStop) return;\n const from = event.relatedTarget;\n if (from instanceof Node && list.contains(from)) return;\n const tabs = tabsOf(list);\n const first = tabs.find((tab) => tab.getAttribute(\"aria-disabled\") !== \"true\") ?? tabs[0];\n if (first !== undefined) focusTab(first);\n };\n\n const focusgroup = experimental_focusgroup\n ? // `nomemory` + `focusgroupstart` on the selected Tab: Tab always enters on the selected tab\n // (APG), not on the last focused one, which differs in manual activation.\n `tablist ${orientation === \"vertical\" ? \"block\" : \"inline\"} ${loop ? \"wrap\" : \"nowrap\"} nomemory`\n : undefined;\n\n const element = useRender<ComponentPropsWithRef<\"div\">, TabsListState>({\n defaultElement: \"div\",\n render,\n state,\n className,\n style,\n children,\n props: rest,\n libraryProps: {\n role: \"tablist\",\n \"aria-orientation\": orientation,\n \"data-orientation\": orientation,\n \"data-disabled\": dataAttr(disabled),\n tabIndex: listIsTabStop ? 0 : undefined,\n // Owned only while the flag is on; otherwise the key is omitted, not `undefined`, so it is not\n // owned (conventions 1.2).\n ...(focusgroup !== undefined && { focusgroup }),\n onKeyDown,\n onFocus,\n },\n ref: listRef,\n });\n\n return <TabsListContext value={listContext}>{element}</TabsListContext>;\n}\n\nTabsList.displayName = \"TabsList\";\n"],"mappings":"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"}
@@ -38,7 +38,7 @@ export interface TabsTabProps extends Omit<ComponentPropsWithRef<"button">, "onC
38
38
  * roving `tabindex`) and carries `anchor-name` for the Indicator.
39
39
  *
40
40
  * Owned keys (conventions 1.2): `role`, `id`, `aria-selected`, `aria-controls` (token-merged),
41
- * `aria-disabled`, `tabIndex`, `data-selected`, `data-pending`, `data-disabled`,
41
+ * `aria-disabled`, `tabIndex`, `focusgroupstart` (with `experimental_focusgroup`), `data-selected`, `data-pending`, `data-disabled`,
42
42
  * `data-orientation`, `onClick`, `onKeyDown`, `onFocus`.
43
43
  */
44
44
  export declare function TabsTab({ value, disabled: disabledProp, transition, className, style, children, render, ...rest }: TabsTabProps): ReactNode;
@@ -1 +1 @@
1
- {"version":3,"file":"tabs-tab.d.mts","names":[],"sources":["../../src/tabs/tabs-tab.tsx"],"mappings":";;;;iBAkBiB;WACN;;WAEA;;WAEA;WACA;WACA,aAAa;;iBAGP,qBAAqB,KACpC;;EAIA;;EAEA;;;;;;;;;;;EAWA,aAAa;EACb,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;;EAE9C,WAAW,WAAW,WAAW;EACjC,SAAS,WAAW,iCAAiC;;;;;;;;;;wBA2BvC,UACd,OACA,UAAU,cACV,YACA,WACA,OACA,UACA,WACG,QACF,eAAY;yBATC"}
1
+ {"version":3,"file":"tabs-tab.d.mts","names":[],"sources":["../../src/tabs/tabs-tab.tsx"],"mappings":";;;;iBAmBiB;WACN;;WAEA;;WAEA;WACA;WACA,aAAa;;iBAGP,qBAAqB,KACpC;;EAIA;;EAEA;;;;;;;;;;;EAWA,aAAa;EACb,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;;EAE9C,WAAW,WAAW,WAAW;EACjC,SAAS,WAAW,iCAAiC;;;;;;;;;;wBA2BvC,UACd,OACA,UAAU,cACV,YACA,WACA,OACA,UACA,WACG,QACF,eAAY;yBATC"}
@@ -2,8 +2,9 @@
2
2
  import { useTransitionBoundary } from "../internal/transition-boundary.mjs";
3
3
  import { dataAttr } from "../internal/data-attr.mjs";
4
4
  import { rendersButton, resolveStateValue, useRender } from "../internal/use-render.mjs";
5
- import { useTabsContext } from "./tabs-context.mjs";
5
+ import { TabsListContext, useTabsContext } from "./tabs-context.mjs";
6
6
  import { anchorName, panelId, tabId } from "./tabs-ids.mjs";
7
+ import { use } from "react";
7
8
  //#region src/tabs/tabs-tab.tsx
8
9
  function isActivationKey(event) {
9
10
  return (event.key === "Enter" || event.key === " ") && !event.altKey && !event.ctrlKey && !event.metaKey;
@@ -19,11 +20,12 @@ function movedWithinList(event) {
19
20
  * roving `tabindex`) and carries `anchor-name` for the Indicator.
20
21
  *
21
22
  * Owned keys (conventions 1.2): `role`, `id`, `aria-selected`, `aria-controls` (token-merged),
22
- * `aria-disabled`, `tabIndex`, `data-selected`, `data-pending`, `data-disabled`,
23
+ * `aria-disabled`, `tabIndex`, `focusgroupstart` (with `experimental_focusgroup`), `data-selected`, `data-pending`, `data-disabled`,
23
24
  * `data-orientation`, `onClick`, `onKeyDown`, `onFocus`.
24
25
  */
25
26
  function TabsTab({ value, disabled: disabledProp = false, transition, className, style, children, render, ...rest }) {
26
27
  const ctx = useTabsContext("Tabs.Tab");
28
+ const listCtx = use(TabsListContext);
27
29
  const selected = ctx.optimistic === value;
28
30
  const pending = ctx.pending && selected;
29
31
  const disabled = ctx.disabled || disabledProp;
@@ -55,7 +57,8 @@ function TabsTab({ value, disabled: disabledProp = false, transition, className,
55
57
  "aria-selected": selected,
56
58
  "aria-controls": panelId(ctx.rootId, value),
57
59
  "aria-disabled": disabled ? true : void 0,
58
- tabIndex: selected ? 0 : -1,
60
+ tabIndex: selected ? 0 : listCtx?.nativeFocusgroup ? void 0 : -1,
61
+ ...listCtx?.focusgroup === true && { focusgroupstart: selected ? "" : void 0 },
59
62
  "data-selected": dataAttr(selected),
60
63
  "data-pending": dataAttr(pending),
61
64
  "data-disabled": dataAttr(disabled),
@@ -1 +1 @@
1
- {"version":3,"file":"tabs-tab.mjs","names":[],"sources":["../../src/tabs/tabs-tab.tsx"],"sourcesContent":["\"use client\";\nimport type {\n ComponentPropsWithRef,\n CSSProperties,\n FocusEvent,\n KeyboardEvent,\n MouseEvent,\n ReactNode,\n ViewTransitionClass,\n} from \"react\";\nimport { useTransitionBoundary } from \"../internal/transition-boundary.js\";\nimport { dataAttr } from \"../internal/data-attr.js\";\nimport type { RenderProp, StateValue } from \"../internal/types.js\";\nimport { rendersButton, resolveStateValue, useRender } from \"../internal/use-render.js\";\nimport { useTabsContext } from \"./tabs-context.js\";\nimport type { TabsOrientation } from \"./tabs-context.js\";\nimport { anchorName, panelId, tabId } from \"./tabs-ids.js\";\n\nexport interface TabsTabState {\n readonly value: string;\n /** Optimistic: true as soon as the user selects this tab, before the action settles. */\n readonly selected: boolean;\n /** The Root is pending and this tab is the optimistic target. */\n readonly pending: boolean;\n readonly disabled: boolean;\n readonly orientation: TabsOrientation;\n}\n\nexport interface TabsTabProps extends Omit<\n ComponentPropsWithRef<\"button\">,\n \"onClick\" | \"onKeyDown\" | \"onFocus\" | \"id\" | \"value\" | \"className\" | \"style\" | \"children\"\n> {\n /** The value this tab selects. Must be unique within the Root. */\n value: string;\n /** Focusable but inert: `aria-disabled=\"true\"`, never dispatches. */\n disabled?: boolean;\n /**\n * Forwarded to the Tab's `<ViewTransition default>`. Default: no boundary. Fires only on the commit\n * that settles the action (or rolls it back), never on the optimistic selection; animate the\n * selection itself with a CSS `transition` on `[data-selected]`.\n *\n * Static: without `transition` the part renders no `<ViewTransition>`; changing it between\n * `undefined` and a value remounts the element. Any value, `\"none\"` included, makes a mount or\n * `<Activity>` reveal of this part inside a transition start a document view transition: the\n * page cross-fades for about 250 ms and the next transition commit waits for it (issue #35).\n */\n transition?: ViewTransitionClass;\n className?: StateValue<string | undefined, TabsTabState>;\n style?: StateValue<CSSProperties | undefined, TabsTabState>;\n /** Rendered from optimistic state; must not suspend. */\n children?: StateValue<ReactNode, TabsTabState>;\n render?: RenderProp<ComponentPropsWithRef<\"button\">, TabsTabState>;\n}\n\nfunction isActivationKey(event: KeyboardEvent): boolean {\n return (\n (event.key === \"Enter\" || event.key === \" \") &&\n !event.altKey &&\n !event.ctrlKey &&\n !event.metaKey\n );\n}\n\n/** Focus came from another element inside the tab's own tablist (not from the list itself). */\nfunction movedWithinList(event: FocusEvent<HTMLElement>): boolean {\n const list = event.currentTarget.closest('[role=\"tablist\"]');\n const from = event.relatedTarget;\n return list !== null && from instanceof Node && from !== list && list.contains(from);\n}\n\n/**\n * A `button[role=tab]`. Renders the optimistic selection (`aria-selected`, `data-selected`,\n * roving `tabindex`) and carries `anchor-name` for the Indicator.\n *\n * Owned keys (conventions 1.2): `role`, `id`, `aria-selected`, `aria-controls` (token-merged),\n * `aria-disabled`, `tabIndex`, `data-selected`, `data-pending`, `data-disabled`,\n * `data-orientation`, `onClick`, `onKeyDown`, `onFocus`.\n */\nexport function TabsTab({\n value,\n disabled: disabledProp = false,\n transition,\n className,\n style,\n children,\n render,\n ...rest\n}: TabsTabProps) {\n const ctx = useTabsContext(\"Tabs.Tab\");\n const selected = ctx.optimistic === value;\n const pending = ctx.pending && selected;\n const disabled = ctx.disabled || disabledProp;\n const anchor = anchorName(ctx.rootId, value);\n\n const state: TabsTabState = {\n value,\n selected,\n pending,\n disabled,\n orientation: ctx.orientation,\n };\n\n const element = useRender<ComponentPropsWithRef<\"button\">, TabsTabState>({\n defaultElement: \"button\",\n render,\n state,\n className,\n // `anchor-name` sits under the consumer's style: the consumer wins (design 13.5).\n style: (s) => ({ anchorName: anchor, ...resolveStateValue(style, s) }),\n children,\n // `type=\"button\"` only on a real button (no form submission); an `<a>` must not get it.\n props: rendersButton(render) ? { ...rest, type: \"button\" } : rest,\n libraryProps: {\n role: \"tab\",\n id: tabId(ctx.rootId, value),\n \"aria-selected\": selected,\n \"aria-controls\": panelId(ctx.rootId, value),\n \"aria-disabled\": disabled ? true : undefined,\n tabIndex: selected ? 0 : -1,\n \"data-selected\": dataAttr(selected),\n \"data-pending\": dataAttr(pending),\n \"data-disabled\": dataAttr(disabled),\n \"data-orientation\": ctx.orientation,\n onClick: (event: MouseEvent<HTMLButtonElement>) => {\n if (disabled) return;\n ctx.select(value, event.nativeEvent, \"activate\");\n },\n // Enter and Space are handled on keydown so a `render`ed non-button element (a link)\n // behaves the same; `preventDefault` also stops the native click / navigation.\n onKeyDown: (event: KeyboardEvent<HTMLButtonElement>) => {\n if (!isActivationKey(event)) return;\n event.preventDefault();\n if (disabled) return;\n ctx.select(value, event.nativeEvent, \"activate\");\n },\n // Automatic activation is selection-follows-focus (design 4.1), but only for focus that\n // moves between tabs inside this list (the arrow-key path, the native `focusgroup` path,\n // programmatic movement). Focus entering from outside never selects: the Tab key, window\n // re-activation, focus restore, `autoFocus`, and the List's own redirect when no tab is the\n // tab stop. A click still selects through `onClick`.\n onFocus: (event: FocusEvent<HTMLButtonElement>) => {\n if (ctx.activationMode !== \"automatic\" || disabled || selected) return;\n if (!movedWithinList(event)) return;\n ctx.select(value, event.nativeEvent, \"activate\");\n },\n },\n });\n\n return useTransitionBoundary(\"Tabs.Tab\", transition, element);\n}\n\nTabsTab.displayName = \"TabsTab\";\n"],"mappings":";;;;;;;AAsDA,SAAS,gBAAgB,OAA+B;CACtD,QACG,MAAM,QAAQ,WAAW,MAAM,QAAQ,QACxC,CAAC,MAAM,UACP,CAAC,MAAM,WACP,CAAC,MAAM;AAEX;;AAGA,SAAS,gBAAgB,OAAyC;CAChE,MAAM,OAAO,MAAM,cAAc,QAAQ,oBAAkB;CAC3D,MAAM,OAAO,MAAM;CACnB,OAAO,SAAS,QAAQ,gBAAgB,QAAQ,SAAS,QAAQ,KAAK,SAAS,IAAI;AACrF;;;;;;;;;AAUA,SAAgB,QAAQ,EACtB,OACA,UAAU,eAAe,OACzB,YACA,WACA,OACA,UACA,QACA,GAAG,QACY;CACf,MAAM,MAAM,eAAe,UAAU;CACrC,MAAM,WAAW,IAAI,eAAe;CACpC,MAAM,UAAU,IAAI,WAAW;CAC/B,MAAM,WAAW,IAAI,YAAY;CACjC,MAAM,SAAS,WAAW,IAAI,QAAQ,KAAK;CAE3C,MAAM,QAAsB;EAC1B;EACA;EACA;EACA;EACA,aAAa,IAAI;CACnB;CAEA,MAAM,UAAU,UAAyD;EACvE,gBAAgB;EAChB;EACA;EACA;EAEA,QAAQ,OAAO;GAAE,YAAY;GAAQ,GAAG,kBAAkB,OAAO,CAAC;EAAE;EACpE;EAEA,OAAO,cAAc,MAAM,IAAI;GAAE,GAAG;GAAM,MAAM;EAAS,IAAI;EAC7D,cAAc;GACZ,MAAM;GACN,IAAI,MAAM,IAAI,QAAQ,KAAK;GAC3B,iBAAiB;GACjB,iBAAiB,QAAQ,IAAI,QAAQ,KAAK;GAC1C,iBAAiB,WAAW,OAAO,KAAA;GACnC,UAAU,WAAW,IAAI;GACzB,iBAAiB,SAAS,QAAQ;GAClC,gBAAgB,SAAS,OAAO;GAChC,iBAAiB,SAAS,QAAQ;GAClC,oBAAoB,IAAI;GACxB,UAAU,UAAyC;IACjD,IAAI,UAAU;IACd,IAAI,OAAO,OAAO,MAAM,aAAa,UAAU;GACjD;GAGA,YAAY,UAA4C;IACtD,IAAI,CAAC,gBAAgB,KAAK,GAAG;IAC7B,MAAM,eAAe;IACrB,IAAI,UAAU;IACd,IAAI,OAAO,OAAO,MAAM,aAAa,UAAU;GACjD;GAMA,UAAU,UAAyC;IACjD,IAAI,IAAI,mBAAmB,eAAe,YAAY,UAAU;IAChE,IAAI,CAAC,gBAAgB,KAAK,GAAG;IAC7B,IAAI,OAAO,OAAO,MAAM,aAAa,UAAU;GACjD;EACF;CACF,CAAC;CAED,OAAO,sBAAsB,YAAY,YAAY,OAAO;AAC9D;AAEA,QAAQ,cAAc"}
1
+ {"version":3,"file":"tabs-tab.mjs","names":[],"sources":["../../src/tabs/tabs-tab.tsx"],"sourcesContent":["\"use client\";\nimport { use } from \"react\";\nimport type {\n ComponentPropsWithRef,\n CSSProperties,\n FocusEvent,\n KeyboardEvent,\n MouseEvent,\n ReactNode,\n ViewTransitionClass,\n} from \"react\";\nimport { useTransitionBoundary } from \"../internal/transition-boundary.js\";\nimport { dataAttr } from \"../internal/data-attr.js\";\nimport type { RenderProp, StateValue } from \"../internal/types.js\";\nimport { rendersButton, resolveStateValue, useRender } from \"../internal/use-render.js\";\nimport { TabsListContext, useTabsContext } from \"./tabs-context.js\";\nimport type { TabsOrientation } from \"./tabs-context.js\";\nimport { anchorName, panelId, tabId } from \"./tabs-ids.js\";\n\nexport interface TabsTabState {\n readonly value: string;\n /** Optimistic: true as soon as the user selects this tab, before the action settles. */\n readonly selected: boolean;\n /** The Root is pending and this tab is the optimistic target. */\n readonly pending: boolean;\n readonly disabled: boolean;\n readonly orientation: TabsOrientation;\n}\n\nexport interface TabsTabProps extends Omit<\n ComponentPropsWithRef<\"button\">,\n \"onClick\" | \"onKeyDown\" | \"onFocus\" | \"id\" | \"value\" | \"className\" | \"style\" | \"children\"\n> {\n /** The value this tab selects. Must be unique within the Root. */\n value: string;\n /** Focusable but inert: `aria-disabled=\"true\"`, never dispatches. */\n disabled?: boolean;\n /**\n * Forwarded to the Tab's `<ViewTransition default>`. Default: no boundary. Fires only on the commit\n * that settles the action (or rolls it back), never on the optimistic selection; animate the\n * selection itself with a CSS `transition` on `[data-selected]`.\n *\n * Static: without `transition` the part renders no `<ViewTransition>`; changing it between\n * `undefined` and a value remounts the element. Any value, `\"none\"` included, makes a mount or\n * `<Activity>` reveal of this part inside a transition start a document view transition: the\n * page cross-fades for about 250 ms and the next transition commit waits for it (issue #35).\n */\n transition?: ViewTransitionClass;\n className?: StateValue<string | undefined, TabsTabState>;\n style?: StateValue<CSSProperties | undefined, TabsTabState>;\n /** Rendered from optimistic state; must not suspend. */\n children?: StateValue<ReactNode, TabsTabState>;\n render?: RenderProp<ComponentPropsWithRef<\"button\">, TabsTabState>;\n}\n\nfunction isActivationKey(event: KeyboardEvent): boolean {\n return (\n (event.key === \"Enter\" || event.key === \" \") &&\n !event.altKey &&\n !event.ctrlKey &&\n !event.metaKey\n );\n}\n\n/** Focus came from another element inside the tab's own tablist (not from the list itself). */\nfunction movedWithinList(event: FocusEvent<HTMLElement>): boolean {\n const list = event.currentTarget.closest('[role=\"tablist\"]');\n const from = event.relatedTarget;\n return list !== null && from instanceof Node && from !== list && list.contains(from);\n}\n\n/**\n * A `button[role=tab]`. Renders the optimistic selection (`aria-selected`, `data-selected`,\n * roving `tabindex`) and carries `anchor-name` for the Indicator.\n *\n * Owned keys (conventions 1.2): `role`, `id`, `aria-selected`, `aria-controls` (token-merged),\n * `aria-disabled`, `tabIndex`, `focusgroupstart` (with `experimental_focusgroup`), `data-selected`, `data-pending`, `data-disabled`,\n * `data-orientation`, `onClick`, `onKeyDown`, `onFocus`.\n */\nexport function TabsTab({\n value,\n disabled: disabledProp = false,\n transition,\n className,\n style,\n children,\n render,\n ...rest\n}: TabsTabProps) {\n const ctx = useTabsContext(\"Tabs.Tab\");\n const listCtx = use(TabsListContext);\n const selected = ctx.optimistic === value;\n const pending = ctx.pending && selected;\n const disabled = ctx.disabled || disabledProp;\n const anchor = anchorName(ctx.rootId, value);\n\n const state: TabsTabState = {\n value,\n selected,\n pending,\n disabled,\n orientation: ctx.orientation,\n };\n\n const element = useRender<ComponentPropsWithRef<\"button\">, TabsTabState>({\n defaultElement: \"button\",\n render,\n state,\n className,\n // `anchor-name` sits under the consumer's style: the consumer wins (design 13.5).\n style: (s) => ({ anchorName: anchor, ...resolveStateValue(style, s) }),\n children,\n // `type=\"button\"` only on a real button (no form submission); an `<a>` must not get it.\n props: rendersButton(render) ? { ...rest, type: \"button\" } : rest,\n libraryProps: {\n role: \"tab\",\n id: tabId(ctx.rootId, value),\n \"aria-selected\": selected,\n \"aria-controls\": panelId(ctx.rootId, value),\n \"aria-disabled\": disabled ? true : undefined,\n // Roving tabindex, except on the native `focusgroup` path, where the browser owns the tab\n // stop and a negative tabindex would take the Tab out of the group (TabsListContextValue).\n tabIndex: selected ? 0 : listCtx?.nativeFocusgroup ? undefined : -1,\n // Always rendered with the flag (inert where unsupported), so markup is stable.\n ...(listCtx?.focusgroup === true && { focusgroupstart: selected ? \"\" : undefined }),\n \"data-selected\": dataAttr(selected),\n \"data-pending\": dataAttr(pending),\n \"data-disabled\": dataAttr(disabled),\n \"data-orientation\": ctx.orientation,\n onClick: (event: MouseEvent<HTMLButtonElement>) => {\n if (disabled) return;\n ctx.select(value, event.nativeEvent, \"activate\");\n },\n // Enter and Space are handled on keydown so a `render`ed non-button element (a link)\n // behaves the same; `preventDefault` also stops the native click / navigation.\n onKeyDown: (event: KeyboardEvent<HTMLButtonElement>) => {\n if (!isActivationKey(event)) return;\n event.preventDefault();\n if (disabled) return;\n ctx.select(value, event.nativeEvent, \"activate\");\n },\n // Automatic activation is selection-follows-focus (design 4.1), but only for focus that\n // moves between tabs inside this list (the arrow-key path, the native `focusgroup` path,\n // programmatic movement). Focus entering from outside never selects: the Tab key, window\n // re-activation, focus restore, `autoFocus`, and the List's own redirect when no tab is the\n // tab stop. A click still selects through `onClick`.\n onFocus: (event: FocusEvent<HTMLButtonElement>) => {\n if (ctx.activationMode !== \"automatic\" || disabled || selected) return;\n if (!movedWithinList(event)) return;\n ctx.select(value, event.nativeEvent, \"activate\");\n },\n },\n });\n\n return useTransitionBoundary(\"Tabs.Tab\", transition, element);\n}\n\nTabsTab.displayName = \"TabsTab\";\n"],"mappings":";;;;;;;;AAuDA,SAAS,gBAAgB,OAA+B;CACtD,QACG,MAAM,QAAQ,WAAW,MAAM,QAAQ,QACxC,CAAC,MAAM,UACP,CAAC,MAAM,WACP,CAAC,MAAM;AAEX;;AAGA,SAAS,gBAAgB,OAAyC;CAChE,MAAM,OAAO,MAAM,cAAc,QAAQ,oBAAkB;CAC3D,MAAM,OAAO,MAAM;CACnB,OAAO,SAAS,QAAQ,gBAAgB,QAAQ,SAAS,QAAQ,KAAK,SAAS,IAAI;AACrF;;;;;;;;;AAUA,SAAgB,QAAQ,EACtB,OACA,UAAU,eAAe,OACzB,YACA,WACA,OACA,UACA,QACA,GAAG,QACY;CACf,MAAM,MAAM,eAAe,UAAU;CACrC,MAAM,UAAU,IAAI,eAAe;CACnC,MAAM,WAAW,IAAI,eAAe;CACpC,MAAM,UAAU,IAAI,WAAW;CAC/B,MAAM,WAAW,IAAI,YAAY;CACjC,MAAM,SAAS,WAAW,IAAI,QAAQ,KAAK;CAE3C,MAAM,QAAsB;EAC1B;EACA;EACA;EACA;EACA,aAAa,IAAI;CACnB;CAEA,MAAM,UAAU,UAAyD;EACvE,gBAAgB;EAChB;EACA;EACA;EAEA,QAAQ,OAAO;GAAE,YAAY;GAAQ,GAAG,kBAAkB,OAAO,CAAC;EAAE;EACpE;EAEA,OAAO,cAAc,MAAM,IAAI;GAAE,GAAG;GAAM,MAAM;EAAS,IAAI;EAC7D,cAAc;GACZ,MAAM;GACN,IAAI,MAAM,IAAI,QAAQ,KAAK;GAC3B,iBAAiB;GACjB,iBAAiB,QAAQ,IAAI,QAAQ,KAAK;GAC1C,iBAAiB,WAAW,OAAO,KAAA;GAGnC,UAAU,WAAW,IAAI,SAAS,mBAAmB,KAAA,IAAY;GAEjE,GAAI,SAAS,eAAe,QAAQ,EAAE,iBAAiB,WAAW,KAAK,KAAA,EAAU;GACjF,iBAAiB,SAAS,QAAQ;GAClC,gBAAgB,SAAS,OAAO;GAChC,iBAAiB,SAAS,QAAQ;GAClC,oBAAoB,IAAI;GACxB,UAAU,UAAyC;IACjD,IAAI,UAAU;IACd,IAAI,OAAO,OAAO,MAAM,aAAa,UAAU;GACjD;GAGA,YAAY,UAA4C;IACtD,IAAI,CAAC,gBAAgB,KAAK,GAAG;IAC7B,MAAM,eAAe;IACrB,IAAI,UAAU;IACd,IAAI,OAAO,OAAO,MAAM,aAAa,UAAU;GACjD;GAMA,UAAU,UAAyC;IACjD,IAAI,IAAI,mBAAmB,eAAe,YAAY,UAAU;IAChE,IAAI,CAAC,gBAAgB,KAAK,GAAG;IAC7B,IAAI,OAAO,OAAO,MAAM,aAAa,UAAU;GACjD;EACF;CACF,CAAC;CAED,OAAO,sBAAsB,YAAY,YAAY,OAAO;AAC9D;AAEA,QAAQ,cAAc"}
@@ -1 +1 @@
1
- {"version":3,"file":"tooltip-popup.d.mts","names":[],"sources":["../../src/tooltip/tooltip-popup.tsx"],"mappings":";;;YAQY;YACA;iBAEK;WACN;WACA,MAAM;WACN,OAAO;;iBAGD,0BAA0B,KACzC;;;;;;EAQA,OAAO;;EAEP,QAAQ;;;;;;;EAOR;EACA,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;;;;;EAK9C,WAAW,WAAW,WAAW;EACjC,SAAS,WAAW,8BAA8B;;;;;;;;;;wBA2BpC,eACd,MACA,OACA,mBACA,WACA,OACA,UACA,WACG,QACF,oBAAiB;yBATJ"}
1
+ {"version":3,"file":"tooltip-popup.d.mts","names":[],"sources":["../../src/tooltip/tooltip-popup.tsx"],"mappings":";;;YASY;YACA;iBAEK;WACN;WACA,MAAM;WACN,OAAO;;iBAGD,0BAA0B,KACzC;;;;;;EAQA,OAAO;;EAEP,QAAQ;;;;;;;EAOR;EACA,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;;;;;EAK9C,WAAW,WAAW,WAAW;EACjC,SAAS,WAAW,8BAA8B;;;;;;;;;;wBAWpC,eACd,MACA,OACA,mBACA,WACA,OACA,UACA,WACG,QACF,oBAAiB;yBATJ"}
@@ -1,14 +1,9 @@
1
1
  "use client";
2
2
  import { dataAttr } from "../internal/data-attr.mjs";
3
3
  import { resolveStateValue, useRender } from "../internal/use-render.mjs";
4
+ import { POSITION_TRY_FALLBACKS, positionArea } from "../internal/anchor-position.mjs";
4
5
  import { useTooltipContext } from "./tooltip-context.mjs";
5
6
  //#region src/tooltip/tooltip-popup.tsx
6
- const FALLBACKS = "flip-block, flip-inline, flip-block flip-inline";
7
- /** Design section 7.1: the `position-area` for each side and alignment. */
8
- function positionArea(side, align) {
9
- if (align === "center") return side;
10
- return `${side} ${side === "top" || side === "bottom" ? align === "start" ? "span-right" : "span-left" : align === "start" ? "span-bottom" : "span-top"}`;
11
- }
12
7
  /**
13
8
  * The tooltip content: a `div` with `popover` and `role="tooltip"`, shown in the top layer and
14
9
  * anchored to the trigger implicitly (`interestfor` or `showPopover({ source })`). Open state is
@@ -41,7 +36,7 @@ function TooltipPopup({ side = "top", align = "center", experimental_hint = fals
41
36
  className,
42
37
  style: side === null ? style : (s) => ({
43
38
  positionArea: positionArea(side, align),
44
- positionTryFallbacks: FALLBACKS,
39
+ positionTryFallbacks: POSITION_TRY_FALLBACKS,
45
40
  ...resolveStateValue(style, s)
46
41
  }),
47
42
  children,
@@ -1 +1 @@
1
- {"version":3,"file":"tooltip-popup.mjs","names":[],"sources":["../../src/tooltip/tooltip-popup.tsx"],"sourcesContent":["\"use client\";\nimport type { ComponentPropsWithRef, CSSProperties, ReactNode } from \"react\";\nimport { dataAttr } from \"../internal/data-attr.js\";\nimport type { RenderProp, StateValue } from \"../internal/types.js\";\nimport { resolveStateValue, useRender } from \"../internal/use-render.js\";\nimport type { DataAttributes } from \"../internal/use-render.js\";\nimport { useTooltipContext } from \"./tooltip-context.js\";\n\nexport type TooltipSide = \"top\" | \"bottom\" | \"left\" | \"right\";\nexport type TooltipAlign = \"start\" | \"center\" | \"end\";\n\nexport interface TooltipPopupState {\n readonly disabled: boolean;\n readonly side: TooltipSide | null;\n readonly align: TooltipAlign;\n}\n\nexport interface TooltipPopupProps extends Omit<\n ComponentPropsWithRef<\"div\">,\n \"id\" | \"popover\" | \"role\" | \"className\" | \"style\" | \"children\"\n> {\n /**\n * Placement relative to the trigger through inline `position-area`, with\n * `position-try-fallbacks` flipping it when it does not fit. Default `\"top\"`. `null` emits no\n * inline positioning (and no `data-side` / `data-align`), so a stylesheet owns placement.\n */\n side?: TooltipSide | null;\n /** Alignment along the side. Default `\"center\"`. Ignored when `side` is `null`. */\n align?: TooltipAlign;\n /**\n * Experimental (Chrome 151+, Firefox 153+; Safari 27.0 treats it as `\"manual\"`). Renders\n * `popover=\"hint\"` instead of `popover=\"manual\"`: native light dismiss, and the tooltip can\n * open over an `auto` popover (a menu) without closing it. The JS Escape / pointerdown handling\n * stays in place for engines that do not know `hint`. Default `false`.\n */\n experimental_hint?: boolean;\n className?: StateValue<string | undefined, TooltipPopupState>;\n style?: StateValue<CSSProperties | undefined, TooltipPopupState>;\n /**\n * Rendered at mount (hidden by the popover's `display: none`), not at open time. Tooltip\n * content should be static; if it must suspend, put the `<Suspense>` boundary inside the Popup.\n */\n children?: StateValue<ReactNode, TooltipPopupState>;\n render?: RenderProp<ComponentPropsWithRef<\"div\">, TooltipPopupState>;\n}\n\nconst FALLBACKS = \"flip-block, flip-inline, flip-block flip-inline\";\n\n/** Design section 7.1: the `position-area` for each side and alignment. */\nfunction positionArea(side: TooltipSide, align: TooltipAlign): string {\n if (align === \"center\") return side;\n const vertical = side === \"top\" || side === \"bottom\";\n const span = vertical\n ? align === \"start\"\n ? \"span-right\"\n : \"span-left\"\n : align === \"start\"\n ? \"span-bottom\"\n : \"span-top\";\n return `${side} ${span}`;\n}\n\n/**\n * The tooltip content: a `div` with `popover` and `role=\"tooltip\"`, shown in the top layer and\n * anchored to the trigger implicitly (`interestfor` or `showPopover({ source })`). Open state is\n * browser-owned: style it with `:popover-open` and animate it with `@starting-style` in CSS.\n *\n * Owned keys (conventions 1.2): `id`, `popover`, `role`, `data-disabled`, and `data-side` /\n * `data-align` unless `side` is `null`.\n */\nexport function TooltipPopup({\n side = \"top\",\n align = \"center\",\n experimental_hint = false,\n className,\n style,\n children,\n render,\n ...rest\n}: TooltipPopupProps) {\n const ctx = useTooltipContext(\"Tooltip.Popup\");\n const state: TooltipPopupState = { disabled: ctx.disabled, side, align };\n\n const libraryProps: ComponentPropsWithRef<\"div\"> & DataAttributes = {\n id: ctx.popupId,\n popover: experimental_hint ? \"hint\" : \"manual\",\n role: \"tooltip\",\n \"data-disabled\": dataAttr(ctx.disabled),\n // Not owned with `side={null}`: the stylesheet (or a render element) may set them.\n ...(side !== null && { \"data-side\": side, \"data-align\": align }),\n };\n\n return useRender<ComponentPropsWithRef<\"div\">, TooltipPopupState>({\n defaultElement: \"div\",\n render,\n state,\n className,\n // The inline placement sits under the consumer's style: the consumer wins (design 3).\n style:\n side === null\n ? style\n : (s) => ({\n positionArea: positionArea(side, align),\n positionTryFallbacks: FALLBACKS,\n ...resolveStateValue(style, s),\n }),\n children,\n props: rest,\n libraryProps,\n ref: ctx.setPopupElement,\n });\n}\n\nTooltipPopup.displayName = \"TooltipPopup\";\n"],"mappings":";;;;;AA8CA,MAAM,YAAY;;AAGlB,SAAS,aAAa,MAAmB,OAA6B;CACpE,IAAI,UAAU,UAAU,OAAO;CAS/B,OAAO,GAAG,KAAK,GARE,SAAS,SAAS,SAAS,WAExC,UAAU,UACR,eACA,cACF,UAAU,UACR,gBACA;AAER;;;;;;;;;AAUA,SAAgB,aAAa,EAC3B,OAAO,OACP,QAAQ,UACR,oBAAoB,OACpB,WACA,OACA,UACA,QACA,GAAG,QACiB;CACpB,MAAM,MAAM,kBAAkB,eAAe;CAC7C,MAAM,QAA2B;EAAE,UAAU,IAAI;EAAU;EAAM;CAAM;CAEvE,MAAM,eAA8D;EAClE,IAAI,IAAI;EACR,SAAS,oBAAoB,SAAS;EACtC,MAAM;EACN,iBAAiB,SAAS,IAAI,QAAQ;EAEtC,GAAI,SAAS,QAAQ;GAAE,aAAa;GAAM,cAAc;EAAM;CAChE;CAEA,OAAO,UAA2D;EAChE,gBAAgB;EAChB;EACA;EACA;EAEA,OACE,SAAS,OACL,SACC,OAAO;GACN,cAAc,aAAa,MAAM,KAAK;GACtC,sBAAsB;GACtB,GAAG,kBAAkB,OAAO,CAAC;EAC/B;EACN;EACA,OAAO;EACP;EACA,KAAK,IAAI;CACX,CAAC;AACH;AAEA,aAAa,cAAc"}
1
+ {"version":3,"file":"tooltip-popup.mjs","names":[],"sources":["../../src/tooltip/tooltip-popup.tsx"],"sourcesContent":["\"use client\";\nimport type { ComponentPropsWithRef, CSSProperties, ReactNode } from \"react\";\nimport { POSITION_TRY_FALLBACKS, positionArea } from \"../internal/anchor-position.js\";\nimport { dataAttr } from \"../internal/data-attr.js\";\nimport type { RenderProp, StateValue } from \"../internal/types.js\";\nimport { resolveStateValue, useRender } from \"../internal/use-render.js\";\nimport type { DataAttributes } from \"../internal/use-render.js\";\nimport { useTooltipContext } from \"./tooltip-context.js\";\n\nexport type TooltipSide = \"top\" | \"bottom\" | \"left\" | \"right\";\nexport type TooltipAlign = \"start\" | \"center\" | \"end\";\n\nexport interface TooltipPopupState {\n readonly disabled: boolean;\n readonly side: TooltipSide | null;\n readonly align: TooltipAlign;\n}\n\nexport interface TooltipPopupProps extends Omit<\n ComponentPropsWithRef<\"div\">,\n \"id\" | \"popover\" | \"role\" | \"className\" | \"style\" | \"children\"\n> {\n /**\n * Placement relative to the trigger through inline `position-area`, with\n * `position-try-fallbacks` flipping it when it does not fit. Default `\"top\"`. `null` emits no\n * inline positioning (and no `data-side` / `data-align`), so a stylesheet owns placement.\n */\n side?: TooltipSide | null;\n /** Alignment along the side. Default `\"center\"`. Ignored when `side` is `null`. */\n align?: TooltipAlign;\n /**\n * Experimental (Chrome 151+, Firefox 153+; Safari 27.0 treats it as `\"manual\"`). Renders\n * `popover=\"hint\"` instead of `popover=\"manual\"`: native light dismiss, and the tooltip can\n * open over an `auto` popover (a menu) without closing it. The JS Escape / pointerdown handling\n * stays in place for engines that do not know `hint`. Default `false`.\n */\n experimental_hint?: boolean;\n className?: StateValue<string | undefined, TooltipPopupState>;\n style?: StateValue<CSSProperties | undefined, TooltipPopupState>;\n /**\n * Rendered at mount (hidden by the popover's `display: none`), not at open time. Tooltip\n * content should be static; if it must suspend, put the `<Suspense>` boundary inside the Popup.\n */\n children?: StateValue<ReactNode, TooltipPopupState>;\n render?: RenderProp<ComponentPropsWithRef<\"div\">, TooltipPopupState>;\n}\n\n/**\n * The tooltip content: a `div` with `popover` and `role=\"tooltip\"`, shown in the top layer and\n * anchored to the trigger implicitly (`interestfor` or `showPopover({ source })`). Open state is\n * browser-owned: style it with `:popover-open` and animate it with `@starting-style` in CSS.\n *\n * Owned keys (conventions 1.2): `id`, `popover`, `role`, `data-disabled`, and `data-side` /\n * `data-align` unless `side` is `null`.\n */\nexport function TooltipPopup({\n side = \"top\",\n align = \"center\",\n experimental_hint = false,\n className,\n style,\n children,\n render,\n ...rest\n}: TooltipPopupProps) {\n const ctx = useTooltipContext(\"Tooltip.Popup\");\n const state: TooltipPopupState = { disabled: ctx.disabled, side, align };\n\n const libraryProps: ComponentPropsWithRef<\"div\"> & DataAttributes = {\n id: ctx.popupId,\n popover: experimental_hint ? \"hint\" : \"manual\",\n role: \"tooltip\",\n \"data-disabled\": dataAttr(ctx.disabled),\n // Not owned with `side={null}`: the stylesheet (or a render element) may set them.\n ...(side !== null && { \"data-side\": side, \"data-align\": align }),\n };\n\n return useRender<ComponentPropsWithRef<\"div\">, TooltipPopupState>({\n defaultElement: \"div\",\n render,\n state,\n className,\n // The inline placement sits under the consumer's style: the consumer wins (design 3).\n style:\n side === null\n ? style\n : (s) => ({\n positionArea: positionArea(side, align),\n positionTryFallbacks: POSITION_TRY_FALLBACKS,\n ...resolveStateValue(style, s),\n }),\n children,\n props: rest,\n libraryProps,\n ref: ctx.setPopupElement,\n });\n}\n\nTooltipPopup.displayName = \"TooltipPopup\";\n"],"mappings":";;;;;;;;;;;;;;AAuDA,SAAgB,aAAa,EAC3B,OAAO,OACP,QAAQ,UACR,oBAAoB,OACpB,WACA,OACA,UACA,QACA,GAAG,QACiB;CACpB,MAAM,MAAM,kBAAkB,eAAe;CAC7C,MAAM,QAA2B;EAAE,UAAU,IAAI;EAAU;EAAM;CAAM;CAEvE,MAAM,eAA8D;EAClE,IAAI,IAAI;EACR,SAAS,oBAAoB,SAAS;EACtC,MAAM;EACN,iBAAiB,SAAS,IAAI,QAAQ;EAEtC,GAAI,SAAS,QAAQ;GAAE,aAAa;GAAM,cAAc;EAAM;CAChE;CAEA,OAAO,UAA2D;EAChE,gBAAgB;EAChB;EACA;EACA;EAEA,OACE,SAAS,OACL,SACC,OAAO;GACN,cAAc,aAAa,MAAM,KAAK;GACtC,sBAAsB;GACtB,GAAG,kBAAkB,OAAO,CAAC;EAC/B;EACN;EACA,OAAO;EACP;EACA,KAAK,IAAI;CACX,CAAC;AACH;AAEA,aAAa,cAAc"}
@@ -1,4 +1,5 @@
1
1
  "use client";
2
+ import { createDelayedPair } from "../internal/use-delayed-pair.mjs";
2
3
  import { useEffect, useEffectEvent } from "react";
3
4
  //#region src/tooltip/use-tooltip-interaction.ts
4
5
  /**
@@ -29,13 +30,8 @@ function useTooltipInteraction(trigger, popup, options) {
29
30
  if (trigger === null) return;
30
31
  if (interestForRequested && trigger.hasAttribute("interestfor") && "interestForElement" in trigger) return;
31
32
  const doc = trigger.ownerDocument;
32
- let timer;
33
- const clear = () => {
34
- if (timer !== void 0) {
35
- clearTimeout(timer);
36
- timer = void 0;
37
- }
38
- };
33
+ const timers = createDelayedPair();
34
+ const clear = timers.cancel;
39
35
  const show = () => {
40
36
  if (isOpen(popup) || !popup.isConnected || !trigger.isConnected) return;
41
37
  popup.showPopover({ source: trigger });
@@ -47,18 +43,10 @@ function useTooltipInteraction(trigger, popup, options) {
47
43
  listenDocument(false);
48
44
  };
49
45
  const showLater = () => {
50
- clear();
51
- timer = setTimeout(() => {
52
- timer = void 0;
53
- show();
54
- }, getDelays().delay);
46
+ timers.open(getDelays().delay, show);
55
47
  };
56
48
  const hideLater = () => {
57
- clear();
58
- timer = setTimeout(() => {
59
- timer = void 0;
60
- hide();
61
- }, getDelays().closeDelay);
49
+ timers.close(getDelays().closeDelay, hide);
62
50
  };
63
51
  const hideNow = () => {
64
52
  clear();
@@ -1 +1 @@
1
- {"version":3,"file":"use-tooltip-interaction.mjs","names":[],"sources":["../../src/tooltip/use-tooltip-interaction.ts"],"sourcesContent":["\"use client\";\nimport { useEffect, useEffectEvent } from \"react\";\n\nexport interface TooltipInteractionOptions {\n readonly delay: number;\n readonly closeDelay: number;\n readonly disabled: boolean;\n /** The Trigger renders `interestfor` (flag on and not disabled). Re-runs the detection. */\n readonly interestForRequested: boolean;\n}\n\n/**\n * The cross-engine JS path of design section 2.2: hover / keyboard focus with delays,\n * `showPopover({ source })`, Escape and pointerdown-anywhere. Listeners go through\n * `addEventListener` (conventions 7.4) so they can be feature-detected away: where the trigger\n * renders `interestfor` and the element implements `interestForElement`, the browser owns the\n * whole interaction and nothing is attached.\n *\n * Open state is browser-owned (conventions 3.5): every decision reads\n * `popup.matches(\":popover-open\")`, never a copy. The document listeners are attached right after\n * `showPopover()` (the `toggle` event is queued as a task, so waiting for it would leave a window\n * in which an Escape or pointerdown does nothing) and detached when `toggle` reports a close,\n * including closes the library did not perform (native light dismiss of `popover=\"hint\"`).\n */\nexport function useTooltipInteraction(\n trigger: HTMLElement | null,\n popup: HTMLElement | null,\n options: TooltipInteractionOptions,\n): void {\n const getDelays = useEffectEvent(() => ({\n delay: options.delay,\n closeDelay: options.closeDelay,\n }));\n const { disabled, interestForRequested } = options;\n\n useEffect(() => {\n if (popup === null) return;\n // Disabling while open hides the popup on both paths (design section 13, Disabled row). On the\n // JS path the previous run's cleanup already did; on the native path nothing else would, and\n // removing `interestfor` does not close an open popover.\n if (disabled) {\n if (isOpen(popup)) popup.hidePopover();\n return;\n }\n if (trigger === null) return;\n // Native path (conventions 8): only when the flag put `interestfor` on the element, and per\n // element, so a `render`ed `<span>` (no `interestForElement`) keeps the JS path. Nothing is\n // attached here, Escape included: the browser's interest handling and the `hint` close watcher\n // decide, so in Chromium an Escape that closes the tooltip may also close an enclosing modal\n // `<dialog>` (the library cannot `preventDefault()` a keydown it never sees).\n if (\n interestForRequested &&\n trigger.hasAttribute(\"interestfor\") &&\n \"interestForElement\" in trigger\n ) {\n return;\n }\n\n const doc = trigger.ownerDocument;\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const clear = () => {\n if (timer !== undefined) {\n clearTimeout(timer);\n timer = undefined;\n }\n };\n const show = () => {\n if (isOpen(popup) || !popup.isConnected || !trigger.isConnected) return;\n popup.showPopover({ source: trigger });\n listenDocument(true);\n };\n const hide = () => {\n // A removed popover is already hidden (`isOpen` is false), so this never throws.\n if (!isOpen(popup)) return;\n popup.hidePopover();\n listenDocument(false);\n };\n const showLater = () => {\n clear();\n timer = setTimeout(() => {\n timer = undefined;\n show();\n }, getDelays().delay);\n };\n const hideLater = () => {\n clear();\n timer = setTimeout(() => {\n timer = undefined;\n hide();\n }, getDelays().closeDelay);\n };\n const hideNow = () => {\n clear();\n hide();\n };\n\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key !== \"Escape\") return;\n clear();\n // Escape while closed passes through untouched.\n if (!isOpen(popup)) return;\n hide();\n // This Escape was the tooltip's: an enclosing modal `<dialog>` or `popover=\"auto\"` menu\n // stays open (the close request sees a canceled keydown). A second Escape reaches them.\n event.preventDefault();\n };\n function listenDocument(listen: boolean) {\n if (listen) {\n doc.addEventListener(\"keydown\", onKeyDown);\n doc.addEventListener(\"pointerdown\", hideNow, true);\n } else {\n doc.removeEventListener(\"keydown\", onKeyDown);\n doc.removeEventListener(\"pointerdown\", hideNow, true);\n }\n }\n // Follows opens and closes the library did not perform; reads the DOM, not `newState`, so a\n // toggle task that is stale by the time it runs cannot detach the listeners of an open popup.\n const onToggle = () => {\n listenDocument(isOpen(popup));\n };\n const onTriggerEnter = (event: PointerEvent) => {\n // No hover on touch; the ARIA relation still works for screen-reader focus (design 5.3).\n if (event.pointerType !== \"touch\") showLater();\n };\n const onTriggerFocus = () => {\n // Pointer focus (a click) does not open; keyboard focus does (design 5.2).\n if (trigger.matches(\":focus-visible\")) showLater();\n };\n // The document listeners exist only while the popup is open, so before that the trigger\n // itself cancels a pending open: a click or Escape before `delay` elapses must not open the\n // tooltip afterwards. Once open, the document listeners do the hiding.\n const onTriggerKeyDown = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") clear();\n };\n\n trigger.addEventListener(\"pointerenter\", onTriggerEnter);\n trigger.addEventListener(\"pointerleave\", hideLater);\n trigger.addEventListener(\"pointerdown\", clear);\n trigger.addEventListener(\"keydown\", onTriggerKeyDown);\n trigger.addEventListener(\"focus\", onTriggerFocus);\n trigger.addEventListener(\"blur\", hideLater);\n popup.addEventListener(\"pointerenter\", clear);\n popup.addEventListener(\"pointerleave\", hideLater);\n popup.addEventListener(\"toggle\", onToggle);\n\n return () => {\n hideNow();\n listenDocument(false);\n trigger.removeEventListener(\"pointerenter\", onTriggerEnter);\n trigger.removeEventListener(\"pointerleave\", hideLater);\n trigger.removeEventListener(\"pointerdown\", clear);\n trigger.removeEventListener(\"keydown\", onTriggerKeyDown);\n trigger.removeEventListener(\"focus\", onTriggerFocus);\n trigger.removeEventListener(\"blur\", hideLater);\n popup.removeEventListener(\"pointerenter\", clear);\n popup.removeEventListener(\"pointerleave\", hideLater);\n popup.removeEventListener(\"toggle\", onToggle);\n };\n }, [trigger, popup, disabled, interestForRequested]);\n}\n\n/** The popover's top-layer state, read from the DOM. A removed popover is never open. */\nfunction isOpen(popup: HTMLElement): boolean {\n return popup.isConnected && popup.matches(\":popover-open\");\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-tooltip-interaction.mjs","names":[],"sources":["../../src/tooltip/use-tooltip-interaction.ts"],"sourcesContent":["\"use client\";\nimport { useEffect, useEffectEvent } from \"react\";\nimport { createDelayedPair } from \"../internal/use-delayed-pair.js\";\n\nexport interface TooltipInteractionOptions {\n readonly delay: number;\n readonly closeDelay: number;\n readonly disabled: boolean;\n /** The Trigger renders `interestfor` (flag on and not disabled). Re-runs the detection. */\n readonly interestForRequested: boolean;\n}\n\n/**\n * The cross-engine JS path of design section 2.2: hover / keyboard focus with delays,\n * `showPopover({ source })`, Escape and pointerdown-anywhere. Listeners go through\n * `addEventListener` (conventions 7.4) so they can be feature-detected away: where the trigger\n * renders `interestfor` and the element implements `interestForElement`, the browser owns the\n * whole interaction and nothing is attached.\n *\n * Open state is browser-owned (conventions 3.5): every decision reads\n * `popup.matches(\":popover-open\")`, never a copy. The document listeners are attached right after\n * `showPopover()` (the `toggle` event is queued as a task, so waiting for it would leave a window\n * in which an Escape or pointerdown does nothing) and detached when `toggle` reports a close,\n * including closes the library did not perform (native light dismiss of `popover=\"hint\"`).\n */\nexport function useTooltipInteraction(\n trigger: HTMLElement | null,\n popup: HTMLElement | null,\n options: TooltipInteractionOptions,\n): void {\n const getDelays = useEffectEvent(() => ({\n delay: options.delay,\n closeDelay: options.closeDelay,\n }));\n const { disabled, interestForRequested } = options;\n\n useEffect(() => {\n if (popup === null) return;\n // Disabling while open hides the popup on both paths (design section 13, Disabled row). On the\n // JS path the previous run's cleanup already did; on the native path nothing else would, and\n // removing `interestfor` does not close an open popover.\n if (disabled) {\n if (isOpen(popup)) popup.hidePopover();\n return;\n }\n if (trigger === null) return;\n // Native path (conventions 8): only when the flag put `interestfor` on the element, and per\n // element, so a `render`ed `<span>` (no `interestForElement`) keeps the JS path. Nothing is\n // attached here, Escape included: the browser's interest handling and the `hint` close watcher\n // decide, so in Chromium an Escape that closes the tooltip may also close an enclosing modal\n // `<dialog>` (the library cannot `preventDefault()` a keydown it never sees).\n if (\n interestForRequested &&\n trigger.hasAttribute(\"interestfor\") &&\n \"interestForElement\" in trigger\n ) {\n return;\n }\n\n const doc = trigger.ownerDocument;\n // One pending open or close at a time (the timer helper shared with Menu's hover intent).\n const timers = createDelayedPair();\n const clear = timers.cancel;\n const show = () => {\n if (isOpen(popup) || !popup.isConnected || !trigger.isConnected) return;\n popup.showPopover({ source: trigger });\n listenDocument(true);\n };\n const hide = () => {\n // A removed popover is already hidden (`isOpen` is false), so this never throws.\n if (!isOpen(popup)) return;\n popup.hidePopover();\n listenDocument(false);\n };\n const showLater = () => {\n timers.open(getDelays().delay, show);\n };\n const hideLater = () => {\n timers.close(getDelays().closeDelay, hide);\n };\n const hideNow = () => {\n clear();\n hide();\n };\n\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key !== \"Escape\") return;\n clear();\n // Escape while closed passes through untouched.\n if (!isOpen(popup)) return;\n hide();\n // This Escape was the tooltip's: an enclosing modal `<dialog>` or `popover=\"auto\"` menu\n // stays open (the close request sees a canceled keydown). A second Escape reaches them.\n event.preventDefault();\n };\n function listenDocument(listen: boolean) {\n if (listen) {\n doc.addEventListener(\"keydown\", onKeyDown);\n doc.addEventListener(\"pointerdown\", hideNow, true);\n } else {\n doc.removeEventListener(\"keydown\", onKeyDown);\n doc.removeEventListener(\"pointerdown\", hideNow, true);\n }\n }\n // Follows opens and closes the library did not perform; reads the DOM, not `newState`, so a\n // toggle task that is stale by the time it runs cannot detach the listeners of an open popup.\n const onToggle = () => {\n listenDocument(isOpen(popup));\n };\n const onTriggerEnter = (event: PointerEvent) => {\n // No hover on touch; the ARIA relation still works for screen-reader focus (design 5.3).\n if (event.pointerType !== \"touch\") showLater();\n };\n const onTriggerFocus = () => {\n // Pointer focus (a click) does not open; keyboard focus does (design 5.2).\n if (trigger.matches(\":focus-visible\")) showLater();\n };\n // The document listeners exist only while the popup is open, so before that the trigger\n // itself cancels a pending open: a click or Escape before `delay` elapses must not open the\n // tooltip afterwards. Once open, the document listeners do the hiding.\n const onTriggerKeyDown = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") clear();\n };\n\n trigger.addEventListener(\"pointerenter\", onTriggerEnter);\n trigger.addEventListener(\"pointerleave\", hideLater);\n trigger.addEventListener(\"pointerdown\", clear);\n trigger.addEventListener(\"keydown\", onTriggerKeyDown);\n trigger.addEventListener(\"focus\", onTriggerFocus);\n trigger.addEventListener(\"blur\", hideLater);\n popup.addEventListener(\"pointerenter\", clear);\n popup.addEventListener(\"pointerleave\", hideLater);\n popup.addEventListener(\"toggle\", onToggle);\n\n return () => {\n hideNow();\n listenDocument(false);\n trigger.removeEventListener(\"pointerenter\", onTriggerEnter);\n trigger.removeEventListener(\"pointerleave\", hideLater);\n trigger.removeEventListener(\"pointerdown\", clear);\n trigger.removeEventListener(\"keydown\", onTriggerKeyDown);\n trigger.removeEventListener(\"focus\", onTriggerFocus);\n trigger.removeEventListener(\"blur\", hideLater);\n popup.removeEventListener(\"pointerenter\", clear);\n popup.removeEventListener(\"pointerleave\", hideLater);\n popup.removeEventListener(\"toggle\", onToggle);\n };\n }, [trigger, popup, disabled, interestForRequested]);\n}\n\n/** The popover's top-layer state, read from the DOM. A removed popover is never open. */\nfunction isOpen(popup: HTMLElement): boolean {\n return popup.isConnected && popup.matches(\":popover-open\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAyBA,SAAgB,sBACd,SACA,OACA,SACM;CACN,MAAM,YAAY,sBAAsB;EACtC,OAAO,QAAQ;EACf,YAAY,QAAQ;CACtB,EAAE;CACF,MAAM,EAAE,UAAU,yBAAyB;CAE3C,gBAAgB;EACd,IAAI,UAAU,MAAM;EAIpB,IAAI,UAAU;GACZ,IAAI,OAAO,KAAK,GAAG,MAAM,YAAY;GACrC;EACF;EACA,IAAI,YAAY,MAAM;EAMtB,IACE,wBACA,QAAQ,aAAa,aAAa,KAClC,wBAAwB,SAExB;EAGF,MAAM,MAAM,QAAQ;EAEpB,MAAM,SAAS,kBAAkB;EACjC,MAAM,QAAQ,OAAO;EACrB,MAAM,aAAa;GACjB,IAAI,OAAO,KAAK,KAAK,CAAC,MAAM,eAAe,CAAC,QAAQ,aAAa;GACjE,MAAM,YAAY,EAAE,QAAQ,QAAQ,CAAC;GACrC,eAAe,IAAI;EACrB;EACA,MAAM,aAAa;GAEjB,IAAI,CAAC,OAAO,KAAK,GAAG;GACpB,MAAM,YAAY;GAClB,eAAe,KAAK;EACtB;EACA,MAAM,kBAAkB;GACtB,OAAO,KAAK,UAAU,CAAC,CAAC,OAAO,IAAI;EACrC;EACA,MAAM,kBAAkB;GACtB,OAAO,MAAM,UAAU,CAAC,CAAC,YAAY,IAAI;EAC3C;EACA,MAAM,gBAAgB;GACpB,MAAM;GACN,KAAK;EACP;EAEA,MAAM,aAAa,UAAyB;GAC1C,IAAI,MAAM,QAAQ,UAAU;GAC5B,MAAM;GAEN,IAAI,CAAC,OAAO,KAAK,GAAG;GACpB,KAAK;GAGL,MAAM,eAAe;EACvB;EACA,SAAS,eAAe,QAAiB;GACvC,IAAI,QAAQ;IACV,IAAI,iBAAiB,WAAW,SAAS;IACzC,IAAI,iBAAiB,eAAe,SAAS,IAAI;GACnD,OAAO;IACL,IAAI,oBAAoB,WAAW,SAAS;IAC5C,IAAI,oBAAoB,eAAe,SAAS,IAAI;GACtD;EACF;EAGA,MAAM,iBAAiB;GACrB,eAAe,OAAO,KAAK,CAAC;EAC9B;EACA,MAAM,kBAAkB,UAAwB;GAE9C,IAAI,MAAM,gBAAgB,SAAS,UAAU;EAC/C;EACA,MAAM,uBAAuB;GAE3B,IAAI,QAAQ,QAAQ,gBAAgB,GAAG,UAAU;EACnD;EAIA,MAAM,oBAAoB,UAAyB;GACjD,IAAI,MAAM,QAAQ,UAAU,MAAM;EACpC;EAEA,QAAQ,iBAAiB,gBAAgB,cAAc;EACvD,QAAQ,iBAAiB,gBAAgB,SAAS;EAClD,QAAQ,iBAAiB,eAAe,KAAK;EAC7C,QAAQ,iBAAiB,WAAW,gBAAgB;EACpD,QAAQ,iBAAiB,SAAS,cAAc;EAChD,QAAQ,iBAAiB,QAAQ,SAAS;EAC1C,MAAM,iBAAiB,gBAAgB,KAAK;EAC5C,MAAM,iBAAiB,gBAAgB,SAAS;EAChD,MAAM,iBAAiB,UAAU,QAAQ;EAEzC,aAAa;GACX,QAAQ;GACR,eAAe,KAAK;GACpB,QAAQ,oBAAoB,gBAAgB,cAAc;GAC1D,QAAQ,oBAAoB,gBAAgB,SAAS;GACrD,QAAQ,oBAAoB,eAAe,KAAK;GAChD,QAAQ,oBAAoB,WAAW,gBAAgB;GACvD,QAAQ,oBAAoB,SAAS,cAAc;GACnD,QAAQ,oBAAoB,QAAQ,SAAS;GAC7C,MAAM,oBAAoB,gBAAgB,KAAK;GAC/C,MAAM,oBAAoB,gBAAgB,SAAS;GACnD,MAAM,oBAAoB,UAAU,QAAQ;EAC9C;CACF,GAAG;EAAC;EAAS;EAAO;EAAU;CAAoB,CAAC;AACrD;;AAGA,SAAS,OAAO,OAA6B;CAC3C,OAAO,MAAM,eAAe,MAAM,QAAQ,eAAe;AAC3D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funstack/ui",
3
- "version": "0.1.0-alpha.0",
3
+ "version": "0.1.0-alpha.2",
4
4
  "description": "Headless UI library for React — the first Async React-native UI library",
5
5
  "keywords": [
6
6
  "components",
@@ -31,8 +31,13 @@
31
31
  ".": "./dist/index.mjs",
32
32
  "./accordion": "./dist/accordion/index.mjs",
33
33
  "./checkbox": "./dist/checkbox/index.mjs",
34
+ "./checkbox-group": "./dist/checkbox-group/index.mjs",
35
+ "./combobox": "./dist/combobox/index.mjs",
34
36
  "./dialog": "./dist/dialog/index.mjs",
37
+ "./menu": "./dist/menu/index.mjs",
38
+ "./popover": "./dist/popover/index.mjs",
35
39
  "./radio-group": "./dist/radio-group/index.mjs",
40
+ "./select": "./dist/select/index.mjs",
36
41
  "./switch": "./dist/switch/index.mjs",
37
42
  "./tabs": "./dist/tabs/index.mjs",
38
43
  "./toggle": "./dist/toggle/index.mjs",