@nizivo/react 0.1.0 → 0.2.0

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 (238) hide show
  1. package/THIRD_PARTY_NOTICES +12 -5
  2. package/dist/_engines/react-aria/dialog.client.js +16 -24
  3. package/dist/_engines/react-aria/drawer.client.js +8 -0
  4. package/dist/_engines/react-aria/file-upload.client.js +50 -0
  5. package/dist/_engines/react-aria/menu.client.js +17 -13
  6. package/dist/_engines/react-aria/open-state.client.js +53 -0
  7. package/dist/_engines/react-aria/outside-press.client.js +61 -0
  8. package/dist/_engines/react-aria/overlay.client.js +6 -1
  9. package/dist/_engines/react-aria/popover.client.js +148 -0
  10. package/dist/_engines/react-aria/popup.client.js +31 -5
  11. package/dist/_engines/react-aria/select.client.js +22 -12
  12. package/dist/_engines/react-aria/toast.client.js +38 -0
  13. package/dist/_engines/react-aria/tooltip.client.js +11 -11
  14. package/dist/_internal/data-status.d.ts +3 -0
  15. package/dist/_internal/data-status.js +13 -0
  16. package/dist/_internal/glyphs.js +54 -1
  17. package/dist/_internal/modal.client.js +54 -0
  18. package/dist/_internal/nested-focus.js +33 -0
  19. package/dist/alert/alert-dismiss.client.d.ts +1 -1
  20. package/dist/alert/alert-dismiss.client.js +3 -1
  21. package/dist/app-shell/app-shell.d.ts +24 -0
  22. package/dist/app-shell/app-shell.js +56 -0
  23. package/dist/app-shell/index.d.ts +2 -0
  24. package/dist/app-shell/index.js +1 -0
  25. package/dist/avatar/avatar.d.ts +96 -0
  26. package/dist/avatar/avatar.js +94 -0
  27. package/dist/avatar/index.d.ts +2 -0
  28. package/dist/avatar/index.js +1 -0
  29. package/dist/banner/banner-dismiss.client.d.ts +32 -0
  30. package/dist/banner/banner-dismiss.client.js +38 -0
  31. package/dist/banner/banner.d.ts +34 -0
  32. package/dist/banner/banner.js +47 -0
  33. package/dist/banner/index.d.ts +3 -0
  34. package/dist/banner/index.js +3 -0
  35. package/dist/breadcrumb/breadcrumb.d.ts +46 -0
  36. package/dist/breadcrumb/breadcrumb.js +46 -0
  37. package/dist/breadcrumb/breadcrumb.parts.d.ts +6 -0
  38. package/dist/breadcrumb/breadcrumb.parts.js +9 -0
  39. package/dist/breadcrumb/index.d.ts +3 -0
  40. package/dist/breadcrumb/index.js +3 -0
  41. package/dist/button/button.d.ts +3 -2
  42. package/dist/card/card.d.ts +9 -2
  43. package/dist/chip-group/chip-group.client.d.ts +88 -0
  44. package/dist/chip-group/chip-group.client.js +157 -0
  45. package/dist/chip-group/index.d.ts +2 -0
  46. package/dist/chip-group/index.js +1 -0
  47. package/dist/choice-card/choice-card.client.d.ts +66 -0
  48. package/dist/choice-card/choice-card.client.js +138 -0
  49. package/dist/choice-card/index.d.ts +2 -0
  50. package/dist/choice-card/index.js +1 -0
  51. package/dist/config/config.client.d.ts +16 -1
  52. package/dist/config/config.client.js +18 -3
  53. package/dist/config/index.d.ts +2 -2
  54. package/dist/contracts/index.d.ts +1 -1
  55. package/dist/css/app-shell.css +2 -0
  56. package/dist/css/avatar.css +2 -0
  57. package/dist/css/banner.css +2 -0
  58. package/dist/css/base.css +1 -1
  59. package/dist/css/breadcrumb.css +2 -0
  60. package/dist/css/card.css +1 -1
  61. package/dist/css/chip-group.css +2 -0
  62. package/dist/css/choice-card.css +2 -0
  63. package/dist/css/delta.css +1 -1
  64. package/dist/css/drawer.css +2 -0
  65. package/dist/css/file-upload.css +2 -0
  66. package/dist/css/form.css +2 -0
  67. package/dist/css/heading.css +1 -1
  68. package/dist/css/kpi-tile.css +2 -0
  69. package/dist/css/metric.css +2 -0
  70. package/dist/css/mobile-tab-bar.css +2 -0
  71. package/dist/css/mobile-top-bar.css +2 -0
  72. package/dist/css/motion.css +2 -0
  73. package/dist/css/nav-item.css +2 -0
  74. package/dist/css/number-value.css +2 -0
  75. package/dist/css/page-header.css +2 -0
  76. package/dist/css/pagination.css +2 -0
  77. package/dist/css/password-input.css +2 -0
  78. package/dist/css/popover.css +2 -0
  79. package/dist/css/ranked-list.css +2 -0
  80. package/dist/css/search-field.css +2 -0
  81. package/dist/css/sidebar.css +2 -0
  82. package/dist/css/split.css +2 -0
  83. package/dist/css/sticky-bar.css +2 -0
  84. package/dist/css/table-selection.css +2 -0
  85. package/dist/css/table.css +2 -0
  86. package/dist/css/text.css +1 -1
  87. package/dist/css/toast.css +2 -0
  88. package/dist/css/top-bar.css +2 -0
  89. package/dist/css/trend-card.css +2 -0
  90. package/dist/dialog/dialog.client.d.ts +1 -1
  91. package/dist/dialog/dialog.client.js +15 -37
  92. package/dist/drawer/drawer.client.d.ts +25 -0
  93. package/dist/drawer/drawer.client.js +154 -0
  94. package/dist/drawer/drawer.contract.d.ts +98 -0
  95. package/dist/drawer/drawer.parts.d.ts +6 -0
  96. package/dist/drawer/drawer.parts.js +16 -0
  97. package/dist/drawer/index.d.ts +5 -0
  98. package/dist/drawer/index.js +3 -0
  99. package/dist/field/field-scope.client.js +9 -2
  100. package/dist/file-upload/file-upload.client.d.ts +9 -0
  101. package/dist/file-upload/file-upload.client.js +406 -0
  102. package/dist/file-upload/file-upload.contract.d.ts +112 -0
  103. package/dist/file-upload/index.d.ts +3 -0
  104. package/dist/file-upload/index.js +1 -0
  105. package/dist/file-upload/seam.d.ts +1 -0
  106. package/dist/form/form.client.d.ts +38 -0
  107. package/dist/form/form.client.js +127 -0
  108. package/dist/form/form.d.ts +36 -0
  109. package/dist/form/form.js +29 -0
  110. package/dist/form/index.d.ts +4 -0
  111. package/dist/form/index.js +4 -0
  112. package/dist/form/submit-button.client.d.ts +20 -0
  113. package/dist/form/submit-button.client.js +19 -0
  114. package/dist/heading/heading.d.ts +4 -1
  115. package/dist/i18n/format-label.d.ts +24 -0
  116. package/dist/i18n/format-label.js +19 -0
  117. package/dist/i18n/index.d.ts +2 -0
  118. package/dist/i18n/index.js +1 -0
  119. package/dist/icon-button/icon-button.client.js +1 -1
  120. package/dist/icon-button/icon-button.js +1 -1
  121. package/dist/index.d.ts +84 -5
  122. package/dist/index.js +49 -4
  123. package/dist/kpi-tile/index.d.ts +2 -0
  124. package/dist/kpi-tile/index.js +1 -0
  125. package/dist/kpi-tile/kpi-tile.d.ts +30 -0
  126. package/dist/kpi-tile/kpi-tile.js +48 -0
  127. package/dist/locales/en-US.d.ts +77 -0
  128. package/dist/locales/en-US.js +83 -0
  129. package/dist/menu/menu.contract.d.ts +1 -1
  130. package/dist/metric/index.d.ts +2 -0
  131. package/dist/metric/index.js +1 -0
  132. package/dist/metric/metric.d.ts +72 -0
  133. package/dist/metric/metric.js +116 -0
  134. package/dist/mobile-tab-bar/index.d.ts +4 -0
  135. package/dist/mobile-tab-bar/index.js +4 -0
  136. package/dist/mobile-tab-bar/mobile-tab-bar-inset.client.js +38 -0
  137. package/dist/mobile-tab-bar/mobile-tab-bar-more.client.d.ts +26 -0
  138. package/dist/mobile-tab-bar/mobile-tab-bar-more.client.js +60 -0
  139. package/dist/mobile-tab-bar/mobile-tab-bar.d.ts +39 -0
  140. package/dist/mobile-tab-bar/mobile-tab-bar.js +54 -0
  141. package/dist/mobile-tab-bar/mobile-tab-bar.parts.d.ts +6 -0
  142. package/dist/mobile-tab-bar/mobile-tab-bar.parts.js +9 -0
  143. package/dist/mobile-top-bar/index.d.ts +2 -0
  144. package/dist/mobile-top-bar/index.js +1 -0
  145. package/dist/mobile-top-bar/mobile-top-bar.d.ts +17 -0
  146. package/dist/mobile-top-bar/mobile-top-bar.js +31 -0
  147. package/dist/motion/count-up.client.d.ts +39 -0
  148. package/dist/motion/count-up.client.js +109 -0
  149. package/dist/motion/index.d.ts +2 -0
  150. package/dist/motion/index.js +1 -0
  151. package/dist/nav-item/index.d.ts +2 -0
  152. package/dist/nav-item/index.js +1 -0
  153. package/dist/nav-item/nav-item.d.ts +35 -0
  154. package/dist/nav-item/nav-item.js +50 -0
  155. package/dist/number-value/index.d.ts +2 -0
  156. package/dist/number-value/index.js +1 -0
  157. package/dist/number-value/number-value.d.ts +25 -0
  158. package/dist/number-value/number-value.js +26 -0
  159. package/dist/page-header/index.d.ts +2 -0
  160. package/dist/page-header/index.js +1 -0
  161. package/dist/page-header/page-header.d.ts +19 -0
  162. package/dist/page-header/page-header.js +39 -0
  163. package/dist/pagination/index.d.ts +2 -0
  164. package/dist/pagination/index.js +1 -0
  165. package/dist/pagination/pagination.d.ts +74 -0
  166. package/dist/pagination/pagination.js +136 -0
  167. package/dist/password-input/index.d.ts +2 -0
  168. package/dist/password-input/index.js +1 -0
  169. package/dist/password-input/password-input.client.d.ts +52 -0
  170. package/dist/password-input/password-input.client.js +106 -0
  171. package/dist/popover/index.d.ts +5 -0
  172. package/dist/popover/index.js +3 -0
  173. package/dist/popover/popover.client.d.ts +20 -0
  174. package/dist/popover/popover.client.js +113 -0
  175. package/dist/popover/popover.contract.d.ts +88 -0
  176. package/dist/popover/popover.parts.d.ts +6 -0
  177. package/dist/popover/popover.parts.js +13 -0
  178. package/dist/ranked-list/index.d.ts +3 -0
  179. package/dist/ranked-list/index.js +3 -0
  180. package/dist/ranked-list/ranked-list.d.ts +85 -0
  181. package/dist/ranked-list/ranked-list.js +153 -0
  182. package/dist/ranked-list/ranked-list.parts.d.ts +6 -0
  183. package/dist/ranked-list/ranked-list.parts.js +10 -0
  184. package/dist/search-field/index.d.ts +2 -0
  185. package/dist/search-field/index.js +1 -0
  186. package/dist/search-field/search-field.client.d.ts +55 -0
  187. package/dist/search-field/search-field.client.js +136 -0
  188. package/dist/select/select.contract.d.ts +1 -1
  189. package/dist/sidebar/index.d.ts +4 -0
  190. package/dist/sidebar/index.js +4 -0
  191. package/dist/sidebar/sidebar-item.client.d.ts +10 -0
  192. package/dist/sidebar/sidebar-item.client.js +55 -0
  193. package/dist/sidebar/sidebar.d.ts +45 -0
  194. package/dist/sidebar/sidebar.js +62 -0
  195. package/dist/sidebar/sidebar.parts.d.ts +6 -0
  196. package/dist/sidebar/sidebar.parts.js +11 -0
  197. package/dist/split/index.d.ts +3 -0
  198. package/dist/split/index.js +3 -0
  199. package/dist/split/split.d.ts +28 -0
  200. package/dist/split/split.js +33 -0
  201. package/dist/split/split.parts.d.ts +6 -0
  202. package/dist/split/split.parts.js +10 -0
  203. package/dist/sticky-bar/index.d.ts +2 -0
  204. package/dist/sticky-bar/index.js +1 -0
  205. package/dist/sticky-bar/sticky-bar.d.ts +16 -0
  206. package/dist/sticky-bar/sticky-bar.js +26 -0
  207. package/dist/styles.css +66 -7
  208. package/dist/table/index.d.ts +3 -0
  209. package/dist/table/index.js +3 -0
  210. package/dist/table/table.d.ts +101 -0
  211. package/dist/table/table.js +121 -0
  212. package/dist/table/table.parts.d.ts +6 -0
  213. package/dist/table/table.parts.js +18 -0
  214. package/dist/table-selection/index.d.ts +3 -0
  215. package/dist/table-selection/index.js +3 -0
  216. package/dist/table-selection/table-selection.client.d.ts +14 -0
  217. package/dist/table-selection/table-selection.client.js +84 -0
  218. package/dist/table-selection/table-selection.d.ts +34 -0
  219. package/dist/table-selection/table-selection.js +50 -0
  220. package/dist/text/text.d.ts +13 -2
  221. package/dist/toast/index.d.ts +4 -0
  222. package/dist/toast/index.js +3 -0
  223. package/dist/toast/seam.d.ts +2 -0
  224. package/dist/toast/store.d.ts +19 -0
  225. package/dist/toast/store.js +136 -0
  226. package/dist/toast/toast.contract.d.ts +56 -0
  227. package/dist/toast/toaster.client.d.ts +10 -0
  228. package/dist/toast/toaster.client.js +305 -0
  229. package/dist/tooltip/tooltip.contract.d.ts +1 -1
  230. package/dist/top-bar/index.d.ts +2 -0
  231. package/dist/top-bar/index.js +1 -0
  232. package/dist/top-bar/top-bar.d.ts +17 -0
  233. package/dist/top-bar/top-bar.js +35 -0
  234. package/dist/trend-card/index.d.ts +2 -0
  235. package/dist/trend-card/index.js +1 -0
  236. package/dist/trend-card/trend-card.d.ts +34 -0
  237. package/dist/trend-card/trend-card.js +74 -0
  238. package/package.json +167 -4
@@ -7,13 +7,20 @@ The internal glyphs in dist/_internal/glyphs.js reuse the SVG path data of these
7
7
  Lucide icons:
8
8
 
9
9
  loader-circle (the Button and IconButton pending spinner)
10
+ chevron-right (the Breadcrumb separator and the Pagination previous/next marks)
10
11
  chevrons-up-down (the Select trigger indicator)
11
- circle-x (the Field, Fieldset and InlineError error mark)
12
+ circle-x (the Field, Fieldset and InlineError error mark, the critical Toast mark)
13
+ circle-check (the success Toast mark)
14
+ info (the neutral Toast mark)
12
15
  x (the AlertDismiss close mark)
13
16
  check (the Checkbox checked mark)
14
17
  minus (the Checkbox indeterminate dash)
18
+ eye (the PasswordInput toggle while the password is hidden)
19
+ eye-off (the PasswordInput toggle while the password is shown)
20
+ search (the SearchField magnifier)
21
+ ellipsis (the MobileTabBar More mark)
15
22
 
16
- Lucide's licence covers all six:
23
+ Lucide's licence covers all thirteen:
17
24
 
18
25
  ISC License
19
26
 
@@ -34,9 +41,9 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
34
41
  --------------------------------------------------------------------------------
35
42
  Feather (https://feathericons.com)
36
43
 
37
- circle-x (Feather's x-circle), x, check and minus are on Lucide's list of icons
38
- derived from the Feather project, so their path data is also under Feather's
39
- licence:
44
+ chevron-right, circle-x (Feather's x-circle), info, x, check, minus, search and
45
+ ellipsis (Feather's more-horizontal) are on Lucide's list of icons derived from the
46
+ Feather project, so their path data is also under Feather's licence:
40
47
 
41
48
  The MIT License (MIT)
42
49
 
@@ -1,25 +1,26 @@
1
1
  "use client";
2
2
  import { mergeProps } from "../../contracts/merge-props.js";
3
- import { details } from "../../contracts/details.js";
4
3
  import { stateAttrs } from "../../contracts/state.js";
5
4
  import { defined, engineDom } from "./dom.js";
6
5
  import { EngineLocale } from "./locale.client.js";
7
6
  import { OverlayScope, TOP_LAYER } from "./overlay.client.js";
8
- import { ReturnFocus, useTriggerRef } from "./popup.client.js";
9
- import { createElement, use, useEffect, useRef } from "react";
7
+ import { useOpenState } from "./open-state.client.js";
8
+ import { InitialFocus, ReturnFocus, useTriggerRef } from "./popup.client.js";
9
+ import { OutsidePress } from "./outside-press.client.js";
10
+ import { createElement, use, useRef } from "react";
10
11
  import { jsx, jsxs } from "react/jsx-runtime";
11
12
  import { Button, Dialog, DialogTrigger, Heading, Modal, ModalOverlay, OverlayTriggerStateContext, Text } from "react-aria-components";
12
13
  //#region src/_engines/react-aria/dialog.client.tsx
13
14
  /** A box-less wrapper: popups portaled into it position against the positioner. */
14
15
  const NESTED_STYLE = { display: "contents" };
15
- const Root = ({ open, defaultOpen, onOpenChange, children }) => /* @__PURE__ */ jsx(EngineLocale, { children: /* @__PURE__ */ jsx(DialogTrigger, {
16
- ...defined({
17
- isOpen: open,
18
- defaultOpen
19
- }),
20
- onOpenChange: (isOpen) => onOpenChange?.(isOpen, details("unknown")),
21
- children
22
- }) });
16
+ const Root = ({ open, defaultOpen, onOpenChange, children }) => {
17
+ const [isOpen, setOpen, key] = useOpenState(open, defaultOpen, onOpenChange);
18
+ return /* @__PURE__ */ jsx(EngineLocale, { children: /* @__PURE__ */ jsx(DialogTrigger, {
19
+ isOpen,
20
+ onOpenChange: setOpen,
21
+ children
22
+ }, key) });
23
+ };
23
24
  const Trigger = ({ render, children, ...dom }) => {
24
25
  const open = use(OverlayTriggerStateContext)?.isOpen ?? false;
25
26
  return /* @__PURE__ */ jsx(Button, {
@@ -35,19 +36,6 @@ const Trigger = ({ render, children, ...dom }) => {
35
36
  children
36
37
  });
37
38
  };
38
- /**
39
- * Moves focus to the leaf's initial-focus element. A child of the content element, so its effect runs
40
- * before React Aria's own focus-the-dialog effect (a parent's), which then sees focus inside and
41
- * leaves it; and after FocusScope recorded the trigger to return focus to.
42
- */
43
- function InitialFocus({ content, resolve }) {
44
- useEffect(() => {
45
- const element = content.current;
46
- if (element === null || element.contains(element.ownerDocument.activeElement)) return;
47
- resolve(element)?.focus();
48
- }, [content, resolve]);
49
- return null;
50
- }
51
39
  const Popup = ({ role, backdrop, container, positioner, backdropProps, contentProps, initialFocus, returnFocus, "aria-label": label, "aria-labelledby": labelledBy, "aria-describedby": describedBy, render, children, ...dom }) => {
52
40
  const nestedRef = useRef(null);
53
41
  const contentRef = useRef(null);
@@ -99,6 +87,10 @@ const Popup = ({ role, backdrop, container, positioner, backdropProps, contentPr
99
87
  popup: contentRef,
100
88
  target: returnFocus
101
89
  }),
90
+ /* @__PURE__ */ jsx(OutsidePress, {
91
+ popup: contentRef,
92
+ dismissable: role !== "alertdialog"
93
+ }),
102
94
  children
103
95
  ]
104
96
  })
@@ -0,0 +1,8 @@
1
+ "use client";
2
+ import "./dialog.client.js";
3
+ import { useLocale } from "react-aria-components";
4
+ //#region src/_engines/react-aria/drawer.client.tsx
5
+ /** The engine's writing direction, from the locale every engine root provides (§9.3 rule 5). */
6
+ const useDirection = () => useLocale().direction;
7
+ //#endregion
8
+ export { useDirection };
@@ -0,0 +1,50 @@
1
+ "use client";
2
+ import { useRef, useState } from "react";
3
+ //#region src/_engines/react-aria/file-upload.client.tsx
4
+ const carriesFiles = (event) => event.dataTransfer?.types.includes("Files") ?? false;
5
+ function useDropTarget(options, _ref) {
6
+ const [over, setOver] = useState(false);
7
+ const depth = useRef(0);
8
+ if (options.disabled) return {
9
+ dropProps: {
10
+ onDragOver: refuse,
11
+ onDrop: refuse
12
+ },
13
+ isDropTarget: false
14
+ };
15
+ return {
16
+ dropProps: {
17
+ onDragEnter(event) {
18
+ if (!carriesFiles(event)) return;
19
+ event.preventDefault();
20
+ depth.current += 1;
21
+ setOver(true);
22
+ },
23
+ onDragOver(event) {
24
+ if (!carriesFiles(event)) return;
25
+ event.preventDefault();
26
+ if (event.dataTransfer !== null) event.dataTransfer.dropEffect = "copy";
27
+ },
28
+ onDragLeave(event) {
29
+ if (!carriesFiles(event)) return;
30
+ depth.current = Math.max(0, depth.current - 1);
31
+ if (depth.current === 0) setOver(false);
32
+ },
33
+ onDrop(event) {
34
+ if (!carriesFiles(event)) return;
35
+ event.preventDefault();
36
+ depth.current = 0;
37
+ setOver(false);
38
+ const files = [...event.dataTransfer?.files ?? []];
39
+ if (files.length > 0) options.onDrop(files, event.currentTarget);
40
+ }
41
+ },
42
+ isDropTarget: over
43
+ };
44
+ }
45
+ function refuse(event) {
46
+ event.preventDefault();
47
+ if (event.dataTransfer !== null) event.dataTransfer.dropEffect = "none";
48
+ }
49
+ //#endregion
50
+ export { useDropTarget };
@@ -5,7 +5,9 @@ import { stateAttrs } from "../../contracts/state.js";
5
5
  import { defined, engineDom } from "./dom.js";
6
6
  import { EngineLocale } from "./locale.client.js";
7
7
  import { popupStyle, resolveSide, toPlacement } from "./overlay.client.js";
8
+ import { useOpenState } from "./open-state.client.js";
8
9
  import { ReturnFocus, useTabOut, useTriggerRef } from "./popup.client.js";
10
+ import { OutsidePress } from "./outside-press.client.js";
9
11
  import { Children, createContext, createElement, isValidElement, use, useRef, useState } from "react";
10
12
  import { jsx, jsxs } from "react/jsx-runtime";
11
13
  import { Button, Header, Menu, MenuItem, MenuSection, MenuTrigger, Popover, RootMenuTriggerStateContext, Separator, useLocale } from "react-aria-components";
@@ -40,16 +42,14 @@ function itemValues(children, out = []) {
40
42
  }
41
43
  const Root = ({ open, defaultOpen, onOpenChange, children }) => {
42
44
  const [store] = useState(() => /* @__PURE__ */ new Map());
45
+ const [isOpen, setOpen, key] = useOpenState(open, defaultOpen, onOpenChange);
43
46
  return /* @__PURE__ */ jsx(EngineLocale, { children: /* @__PURE__ */ jsx(Kept, {
44
47
  value: store,
45
48
  children: /* @__PURE__ */ jsx(MenuTrigger, {
46
- ...defined({
47
- isOpen: open,
48
- defaultOpen
49
- }),
50
- onOpenChange: (isOpen) => onOpenChange?.(isOpen, details("unknown")),
49
+ isOpen,
50
+ onOpenChange: setOpen,
51
51
  children
52
- })
52
+ }, key)
53
53
  }) });
54
54
  };
55
55
  const Trigger = ({ render, children, ...dom }) => {
@@ -102,13 +102,17 @@ const Popup = ({ side, align, sideOffset, container, list, render, children, ...
102
102
  });
103
103
  return render(mergeProps(engine, dom, attrs, { ref: popup }), s);
104
104
  },
105
- children: [/* @__PURE__ */ jsx(ReturnFocus, {
106
- trigger,
107
- popup
108
- }), /* @__PURE__ */ jsx(Menu, {
109
- render: (p) => createElement("div", mergeProps(listDom(p, list), list, { onKeyDown: tabOut })),
110
- children
111
- })]
105
+ children: [
106
+ /* @__PURE__ */ jsx(ReturnFocus, {
107
+ trigger,
108
+ popup
109
+ }),
110
+ /* @__PURE__ */ jsx(OutsidePress, { popup }),
111
+ /* @__PURE__ */ jsx(Menu, {
112
+ render: (p) => createElement("div", mergeProps(listDom(p, list), list, { onKeyDown: tabOut })),
113
+ children
114
+ })
115
+ ]
112
116
  });
113
117
  };
114
118
  const Item = ({ textValue, disabled, onAction, closeOnSelect, href, target, rel, download, hrefLang, ping, referrerPolicy, render, children, ...dom }) => /* @__PURE__ */ jsx(MenuItem, {
@@ -0,0 +1,53 @@
1
+ "use client";
2
+ import { details } from "../../contracts/details.js";
3
+ import { createContext, useLayoutEffect, useRef, useState } from "react";
4
+ //#region src/_engines/react-aria/open-state.client.tsx
5
+ /**
6
+ * The key, for a popup part whose Root keeps other state in the engine (Select's value): that part
7
+ * remounts its popup instead.
8
+ */
9
+ const PopupKey = createContext(0);
10
+ /**
11
+ * `[isOpen, setOpen, key]` for the engine: the consumer's `open`, else the Root's own state, and the
12
+ * key to render the engine's root with.
13
+ */
14
+ function useOpenState(open, defaultOpen, onOpenChange) {
15
+ const [own, setOwn] = useState(defaultOpen ?? false);
16
+ const [key, setKey] = useState(0);
17
+ const controlled = open !== void 0;
18
+ const isOpen = controlled ? open : own;
19
+ const latest = useRef({
20
+ controlled,
21
+ onOpenChange
22
+ });
23
+ useLayoutEffect(() => {
24
+ latest.current = {
25
+ controlled,
26
+ onOpenChange
27
+ };
28
+ });
29
+ const seenClosed = useRef(false);
30
+ const armed = useRef(false);
31
+ useLayoutEffect(() => {
32
+ if (!isOpen) seenClosed.current = true;
33
+ armed.current = isOpen && seenClosed.current;
34
+ }, [isOpen]);
35
+ useLayoutEffect(() => () => {
36
+ if (!armed.current) return;
37
+ armed.current = false;
38
+ if (!latest.current.controlled) setOwn(false);
39
+ setKey((k) => k + 1);
40
+ latest.current.onOpenChange?.(false, details("hidden"));
41
+ }, []);
42
+ const setOpen = (next) => {
43
+ if (!controlled) setOwn(next);
44
+ onOpenChange?.(next, details("unknown"));
45
+ };
46
+ return [
47
+ isOpen,
48
+ setOpen,
49
+ key
50
+ ];
51
+ }
52
+ //#endregion
53
+ export { PopupKey, useOpenState };
@@ -0,0 +1,61 @@
1
+ "use client";
2
+ import { returnAfterClose } from "./popup.client.js";
3
+ import { use, useEffect, useLayoutEffect, useRef } from "react";
4
+ import { OverlayTriggerStateContext } from "react-aria-components";
5
+ //#region src/_engines/react-aria/outside-press.client.tsx
6
+ /**
7
+ * The family's open popups, in open order. Its own list, not popup.client's registry: a Select's
8
+ * hidden collection pass registers and unregisters there under the same ref. Only entries on the
9
+ * page count.
10
+ */
11
+ const STACK = [];
12
+ const onPage = (popup) => popup.current?.isConnected === true;
13
+ /**
14
+ * Outside press for a popup opened inside another overlay (§12.2 Dismiss stack). React Aria's
15
+ * useInteractOutside ignores every press inside an element marked top-layer, and the family's
16
+ * nested-portal containers carry that mark (§12.1 L2), so a press on a nested popup's underlay, or
17
+ * anywhere else in its parent's container, never reached the engine's dismiss logic: a Select or
18
+ * Menu inside a Dialog stayed open (weeks 4 to 12; found in week 13). The family completes it: a
19
+ * press that starts and ends outside `popup`, inside a top-layer element that is not Nizivo's own
20
+ * top layer (the toast region and announcer, `data-nz-top-layer`, dismiss nothing) and not on a
21
+ * tooltip, closes `popup` when it is the topmost. Every other press stays with the engine. The
22
+ * element is read at the press: a Select's popup is attached after this component's first effect.
23
+ */
24
+ function OutsidePress({ popup, dismissable = true }) {
25
+ const context = use(OverlayTriggerStateContext);
26
+ const state = useRef(context);
27
+ useLayoutEffect(() => {
28
+ state.current = context;
29
+ });
30
+ useEffect(() => {
31
+ STACK.push(popup);
32
+ const ours = (event) => {
33
+ const element = popup.current;
34
+ const { target } = event;
35
+ if (!dismissable || event.button > 0 || element === null || !(target instanceof Element) || element.contains(target)) return false;
36
+ const host = target.closest("[data-react-aria-top-layer]");
37
+ return host !== null && host.closest("[data-nz-top-layer]") === null && target.closest("[role=\"tooltip\"]") === null && STACK.findLast(onPage) === popup;
38
+ };
39
+ let started = false;
40
+ const down = (event) => {
41
+ started = ours(event);
42
+ };
43
+ const up = (event) => {
44
+ if (started && ours(event)) {
45
+ returnAfterClose(popup);
46
+ state.current?.close();
47
+ }
48
+ started = false;
49
+ };
50
+ document.addEventListener("pointerdown", down, true);
51
+ document.addEventListener("pointerup", up, true);
52
+ return () => {
53
+ STACK.splice(STACK.lastIndexOf(popup), 1);
54
+ document.removeEventListener("pointerdown", down, true);
55
+ document.removeEventListener("pointerup", up, true);
56
+ };
57
+ }, [popup, dismissable]);
58
+ return null;
59
+ }
60
+ //#endregion
61
+ export { OutsidePress };
@@ -11,6 +11,11 @@ function OverlayScope({ container, children }) {
11
11
  }
12
12
  /** React Aria's inert exemption, as props for an element the adapter renders itself. */
13
13
  const TOP_LAYER = { "data-react-aria-top-layer": "" };
14
+ /** Maps Nizivo's `data-nz-top-layer` to React Aria's inert exemption. */
15
+ function markTopLayer(element) {
16
+ element.setAttribute("data-react-aria-top-layer", "");
17
+ return () => element.removeAttribute("data-react-aria-top-layer");
18
+ }
14
19
  /** Logical side + alignment → a React Aria placement. */
15
20
  function toPlacement(side, align) {
16
21
  if (align === "center") return side;
@@ -38,4 +43,4 @@ function popupStyle(style) {
38
43
  return out;
39
44
  }
40
45
  //#endregion
41
- export { OverlayScope, TOP_LAYER, popupStyle, resolveSide, toPlacement };
46
+ export { OverlayScope, TOP_LAYER, markTopLayer, popupStyle, resolveSide, toPlacement };
@@ -0,0 +1,148 @@
1
+ "use client";
2
+ import { mergeProps } from "../../contracts/merge-props.js";
3
+ import { stateAttrs } from "../../contracts/state.js";
4
+ import { defined, engineDom } from "./dom.js";
5
+ import { EngineLocale } from "./locale.client.js";
6
+ import { OverlayScope, markTopLayer, popupStyle, resolveSide, toPlacement } from "./overlay.client.js";
7
+ import { useOpenState } from "./open-state.client.js";
8
+ import { InitialFocus, ReturnFocus, useTriggerRef } from "./popup.client.js";
9
+ import { OutsidePress } from "./outside-press.client.js";
10
+ import { createElement, use, useCallback, useRef } from "react";
11
+ import { jsx, jsxs } from "react/jsx-runtime";
12
+ import { Button, Dialog, DialogTrigger, Heading, OverlayTriggerStateContext, Popover, Text, useLocale } from "react-aria-components";
13
+ //#region src/_engines/react-aria/popover.client.tsx
14
+ const Root = ({ open, defaultOpen, onOpenChange, children }) => {
15
+ const [isOpen, setOpen, key] = useOpenState(open, defaultOpen, onOpenChange);
16
+ return /* @__PURE__ */ jsx(EngineLocale, { children: /* @__PURE__ */ jsx(DialogTrigger, {
17
+ isOpen,
18
+ onOpenChange: setOpen,
19
+ children
20
+ }, key) });
21
+ };
22
+ const Trigger = ({ render, children, ...dom }) => {
23
+ const open = use(OverlayTriggerStateContext)?.isOpen ?? false;
24
+ return /* @__PURE__ */ jsx(Button, {
25
+ render: (p, rp) => {
26
+ const s = {
27
+ open,
28
+ pressed: rp.isPressed && !open,
29
+ focusVisible: rp.isFocusVisible,
30
+ disabled: rp.isDisabled
31
+ };
32
+ return render(mergeProps(engineDom(p), dom, stateAttrs(s)), s);
33
+ },
34
+ children
35
+ });
36
+ };
37
+ /**
38
+ * The nested-portal container (§12.1 L2): a box-less element right after the popup, in the
39
+ * container the popup portaled into, so overlays opened inside the popover paint above it. It is not
40
+ * inside the popup, which scrolls (a clip) and scales while it enters. The engine hides everything
41
+ * outside a modal popover, so it carries the family's top-layer exemption, as the Dialog's does: a
42
+ * tooltip portaled there stays hoverable (WCAG 1.4.13). Created here, not by React, because the
43
+ * engine owns the popup's portal and nothing can render a sibling into it. A hidden anchor inside
44
+ * the popup makes it with a stable callback ref: in the commit that inserts the popup, so a tooltip
45
+ * shown by the initial focus (a passive effect) already finds it; a ref merged into the popup's own
46
+ * props would be a new function, detached and attached again, on every render.
47
+ */
48
+ function useNestedContainer() {
49
+ const container = useRef(null);
50
+ return {
51
+ container,
52
+ anchor: useCallback((element) => {
53
+ const popup = element?.parentElement;
54
+ if (popup == null) return void 0;
55
+ const nested = popup.ownerDocument.createElement("div");
56
+ nested.style.display = "contents";
57
+ const undo = markTopLayer(nested);
58
+ popup.after(nested);
59
+ container.current = nested;
60
+ return () => {
61
+ undo();
62
+ nested.remove();
63
+ if (container.current === nested) container.current = null;
64
+ };
65
+ }, [])
66
+ };
67
+ }
68
+ const Popup = ({ side, align, sideOffset, container, contentProps, initialFocus, "aria-label": label, "aria-labelledby": labelledBy, "aria-describedby": describedBy, render, children, ...dom }) => {
69
+ const { direction } = useLocale();
70
+ const popup = useRef(null);
71
+ const content = useRef(null);
72
+ const { container: nested, anchor } = useNestedContainer();
73
+ const trigger = useTriggerRef();
74
+ return /* @__PURE__ */ jsxs(Popover, {
75
+ placement: toPlacement(side, align),
76
+ offset: sideOffset,
77
+ ...defined({ UNSTABLE_portalContainer: container ?? void 0 }),
78
+ render: (p, rp) => {
79
+ const s = {
80
+ entering: rp.isEntering,
81
+ exiting: rp.isExiting,
82
+ side: resolveSide(rp.placement, side, direction)
83
+ };
84
+ const engine = {
85
+ ...engineDom(p),
86
+ style: popupStyle(p.style)
87
+ };
88
+ const attrs = stateAttrs({
89
+ open: !rp.isExiting,
90
+ ...s,
91
+ align
92
+ });
93
+ return render(mergeProps(engine, dom, attrs, { ref: popup }), s);
94
+ },
95
+ children: [/* @__PURE__ */ jsx("span", {
96
+ hidden: true,
97
+ ref: anchor
98
+ }), /* @__PURE__ */ jsxs(Dialog, {
99
+ ...defined({
100
+ "aria-label": label,
101
+ "aria-labelledby": labelledBy,
102
+ "aria-describedby": describedBy
103
+ }),
104
+ render: (p) => createElement("section", mergeProps(engineDom(p), contentProps, { ref: content })),
105
+ children: [
106
+ /* @__PURE__ */ jsx(InitialFocus, {
107
+ content,
108
+ resolve: initialFocus
109
+ }),
110
+ /* @__PURE__ */ jsx(ReturnFocus, {
111
+ trigger,
112
+ popup
113
+ }),
114
+ /* @__PURE__ */ jsx(OutsidePress, { popup }),
115
+ /* @__PURE__ */ jsx(OverlayScope, {
116
+ container: nested,
117
+ children
118
+ })
119
+ ]
120
+ })]
121
+ });
122
+ };
123
+ const Title = ({ level, render, children, ...dom }) => /* @__PURE__ */ jsx(Heading, {
124
+ slot: "title",
125
+ level,
126
+ render: (p) => render(mergeProps(engineDom(p), dom), {}),
127
+ children
128
+ });
129
+ const Description = ({ render, children, ...dom }) => /* @__PURE__ */ jsx(Text, {
130
+ slot: "description",
131
+ elementType: "p",
132
+ render: (p) => render(mergeProps(engineDom(p), dom), {}),
133
+ children
134
+ });
135
+ const Close = ({ render, children, ...dom }) => /* @__PURE__ */ jsx(Button, {
136
+ slot: "close",
137
+ render: (p, rp) => {
138
+ const s = {
139
+ pressed: rp.isPressed,
140
+ focusVisible: rp.isFocusVisible,
141
+ disabled: rp.isDisabled
142
+ };
143
+ return render(mergeProps(engineDom(p), dom, stateAttrs(s)), s);
144
+ },
145
+ children
146
+ });
147
+ //#endregion
148
+ export { Close, Description, Popup, Root, Title, Trigger };
@@ -43,6 +43,17 @@ function useTriggerRef() {
43
43
  const ref = typeof context === "object" && context !== null && "triggerRef" in context ? context.triggerRef : void 0;
44
44
  return isElementRef(ref) ? ref : void 0;
45
45
  }
46
+ /**
47
+ * Popups the family closed itself on an outside press (outside-press.client.tsx). The press lands on
48
+ * the popup's underlay, so no control took focus, but a modal parent's FocusScope moves focus to its
49
+ * first control as the popup's content leaves (React Aria's containment fallback), before any
50
+ * restore: the chain runs for these even though focus is no longer on <body> (O-12).
51
+ */
52
+ const CLOSED_OUTSIDE = /* @__PURE__ */ new WeakSet();
53
+ /** Marks `popup` as closed by the family's outside press, for ReturnFocus. */
54
+ function returnAfterClose(popup) {
55
+ CLOSED_OUTSIDE.add(popup);
56
+ }
46
57
  /** The family's open popups and their triggers: the parent-overlay step below. */
47
58
  const OPEN = /* @__PURE__ */ new Map();
48
59
  /**
@@ -96,17 +107,18 @@ function ReturnFocus({ trigger, popup, target = true }) {
96
107
  let handled = false;
97
108
  const onRestore = (event) => {
98
109
  const own = latest.current;
99
- const to = named(own);
100
- if (event.defaultPrevented || mounted(popup) || own !== false && !usable(to)) return;
110
+ const to = own === false ? void 0 : [named(own), element].find(usable);
111
+ if (event.defaultPrevented || mounted(popup) || own !== false && to === void 0) return;
101
112
  event.preventDefault();
102
113
  handled = true;
103
- if (usable(to)) to.focus();
114
+ to?.focus();
104
115
  };
105
116
  doc.addEventListener(ENGINE_RESTORE, onRestore, true);
106
117
  requestAnimationFrame(() => {
107
118
  doc.removeEventListener(ENGINE_RESTORE, onRestore, true);
108
119
  const own = latest.current;
109
- if (handled || !shown.current || own === false || mounted(popup) || (doc.activeElement ?? doc.body) !== doc.body) return;
120
+ const outside = CLOSED_OUTSIDE.delete(popup);
121
+ if (handled || !shown.current || own === false || mounted(popup) || (doc.activeElement ?? doc.body) !== doc.body && !outside) return;
110
122
  const next = [
111
123
  named(own),
112
124
  element,
@@ -123,5 +135,19 @@ function ReturnFocus({ trigger, popup, target = true }) {
123
135
  ]);
124
136
  return null;
125
137
  }
138
+ /**
139
+ * Moves focus to the leaf's initial-focus element (§12.2 Focus on open; Dialog, Drawer, Popover). A
140
+ * child of the content element, so its effect runs before React Aria's own focus-the-dialog effect (a
141
+ * parent's), which then sees focus inside and leaves it; and after FocusScope recorded the trigger to
142
+ * return focus to.
143
+ */
144
+ function InitialFocus({ content, resolve }) {
145
+ useEffect(() => {
146
+ const element = content.current;
147
+ if (element === null || element.contains(element.ownerDocument.activeElement)) return;
148
+ resolve(element)?.focus();
149
+ }, [content, resolve]);
150
+ return null;
151
+ }
126
152
  //#endregion
127
- export { ReturnFocus, useTabOut, useTriggerRef };
153
+ export { InitialFocus, ReturnFocus, returnAfterClose, useTabOut, useTriggerRef };
@@ -5,7 +5,9 @@ import { stateAttrs } from "../../contracts/state.js";
5
5
  import { defined, engineDom } from "./dom.js";
6
6
  import { EngineLocale } from "./locale.client.js";
7
7
  import { popupStyle, resolveSide, toPlacement } from "./overlay.client.js";
8
+ import { PopupKey, useOpenState } from "./open-state.client.js";
8
9
  import { ReturnFocus, useTabOut, useTriggerRef } from "./popup.client.js";
10
+ import { OutsidePress } from "./outside-press.client.js";
9
11
  import { createElement, use, useEffect, useLayoutEffect, useRef } from "react";
10
12
  import { jsx, jsxs } from "react/jsx-runtime";
11
13
  import { Button, Header, ListBox, ListBoxItem, ListBoxSection, Popover, Select, SelectStateContext, SelectValue, Separator, useLocale } from "react-aria-components";
@@ -37,12 +39,11 @@ function FormReset({ root }) {
37
39
  }
38
40
  const Root = ({ value, defaultValue, onValueChange, open, defaultOpen, onOpenChange, name, required, disabled, invalid, placeholder, "aria-label": label, "aria-labelledby": labelledBy, render, children, ...dom }) => {
39
41
  const root = useRef(null);
42
+ const [isOpen, setOpen, popupKey] = useOpenState(open, defaultOpen, onOpenChange);
40
43
  return /* @__PURE__ */ jsx(EngineLocale, { children: /* @__PURE__ */ jsxs(Select, {
41
44
  ...defined({
42
45
  value,
43
46
  defaultValue,
44
- isOpen: open,
45
- defaultOpen,
46
47
  name,
47
48
  isRequired: required,
48
49
  isDisabled: disabled,
@@ -51,8 +52,9 @@ const Root = ({ value, defaultValue, onValueChange, open, defaultOpen, onOpenCha
51
52
  "aria-label": label,
52
53
  "aria-labelledby": labelledBy
53
54
  }),
55
+ isOpen,
56
+ onOpenChange: setOpen,
54
57
  onChange: (key) => onValueChange?.(key == null ? null : String(key), details("unknown")),
55
- onOpenChange: (isOpen) => onOpenChange?.(isOpen, details("unknown")),
56
58
  validationBehavior: "native",
57
59
  render: (p, rp) => {
58
60
  const s = {
@@ -66,7 +68,10 @@ const Root = ({ value, defaultValue, onValueChange, open, defaultOpen, onOpenCha
66
68
  });
67
69
  return render(mergeProps(engineDom(p), dom, attrs, { ref: root }), s);
68
70
  },
69
- children: [children, /* @__PURE__ */ jsx(FormReset, { root })]
71
+ children: [/* @__PURE__ */ jsx(PopupKey, {
72
+ value: popupKey,
73
+ children
74
+ }), /* @__PURE__ */ jsx(FormReset, { root })]
70
75
  }) });
71
76
  };
72
77
  const Trigger = ({ id, "aria-label": label, "aria-describedby": describedBy, render, children, ...dom }) => {
@@ -101,6 +106,7 @@ const Popup = ({ side, align, sideOffset, container, list, render, children, ...
101
106
  const popup = useRef(null);
102
107
  const tabOut = useTabOut(popup);
103
108
  const trigger = useTriggerRef();
109
+ const key = use(PopupKey);
104
110
  return /* @__PURE__ */ jsxs(Popover, {
105
111
  placement: toPlacement(side, align),
106
112
  offset: sideOffset,
@@ -122,14 +128,18 @@ const Popup = ({ side, align, sideOffset, container, list, render, children, ...
122
128
  });
123
129
  return render(mergeProps(engine, dom, attrs, { ref: popup }), s);
124
130
  },
125
- children: [/* @__PURE__ */ jsx(ReturnFocus, {
126
- trigger,
127
- popup
128
- }), /* @__PURE__ */ jsx(ListBox, {
129
- render: (p) => createElement("div", mergeProps(engineDom(p), list, { onKeyDown: tabOut })),
130
- children
131
- })]
132
- });
131
+ children: [
132
+ /* @__PURE__ */ jsx(ReturnFocus, {
133
+ trigger,
134
+ popup
135
+ }),
136
+ /* @__PURE__ */ jsx(OutsidePress, { popup }),
137
+ /* @__PURE__ */ jsx(ListBox, {
138
+ render: (p) => createElement("div", mergeProps(engineDom(p), list, { onKeyDown: tabOut })),
139
+ children
140
+ })
141
+ ]
142
+ }, key);
133
143
  };
134
144
  const Item = ({ value, textValue, disabled, render, children, ...dom }) => /* @__PURE__ */ jsx(ListBoxItem, {
135
145
  id: value,