@proyecto-viviana/solidaria-components 0.2.5 → 0.3.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 (225) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -272
  3. package/dist/ActionBar.d.ts +79 -0
  4. package/dist/ActionBar.d.ts.map +1 -0
  5. package/dist/ActionGroup.d.ts +74 -0
  6. package/dist/ActionGroup.d.ts.map +1 -0
  7. package/dist/Alert.d.ts +70 -0
  8. package/dist/Alert.d.ts.map +1 -0
  9. package/dist/Autocomplete.d.ts +5 -5
  10. package/dist/Autocomplete.d.ts.map +1 -1
  11. package/dist/Breadcrumbs.d.ts +27 -8
  12. package/dist/Breadcrumbs.d.ts.map +1 -1
  13. package/dist/Button.d.ts +28 -5
  14. package/dist/Button.d.ts.map +1 -1
  15. package/dist/Calendar.d.ts +51 -7
  16. package/dist/Calendar.d.ts.map +1 -1
  17. package/dist/Checkbox.d.ts +33 -8
  18. package/dist/Checkbox.d.ts.map +1 -1
  19. package/dist/Collection.d.ts +130 -0
  20. package/dist/Collection.d.ts.map +1 -0
  21. package/dist/Color.d.ts +210 -9
  22. package/dist/Color.d.ts.map +1 -1
  23. package/dist/ColorEditor.d.ts +42 -0
  24. package/dist/ColorEditor.d.ts.map +1 -0
  25. package/dist/ComboBox.d.ts +146 -16
  26. package/dist/ComboBox.d.ts.map +1 -1
  27. package/dist/ContextualHelpTrigger.d.ts +40 -0
  28. package/dist/ContextualHelpTrigger.d.ts.map +1 -0
  29. package/dist/DateField.d.ts +35 -8
  30. package/dist/DateField.d.ts.map +1 -1
  31. package/dist/DatePicker.d.ts +101 -5
  32. package/dist/DatePicker.d.ts.map +1 -1
  33. package/dist/DateRangePickerContext.d.ts +30 -0
  34. package/dist/DateRangePickerContext.d.ts.map +1 -0
  35. package/dist/Dialog.d.ts +5 -5
  36. package/dist/Dialog.d.ts.map +1 -1
  37. package/dist/Disclosure.d.ts +25 -5
  38. package/dist/Disclosure.d.ts.map +1 -1
  39. package/dist/DragAndDrop.d.ts +80 -0
  40. package/dist/DragAndDrop.d.ts.map +1 -0
  41. package/dist/DragPreview.d.ts +14 -0
  42. package/dist/DragPreview.d.ts.map +1 -0
  43. package/dist/DropZone.d.ts +27 -0
  44. package/dist/DropZone.d.ts.map +1 -0
  45. package/dist/FieldError.d.ts +27 -0
  46. package/dist/FieldError.d.ts.map +1 -0
  47. package/dist/FileTrigger.d.ts +26 -0
  48. package/dist/FileTrigger.d.ts.map +1 -0
  49. package/dist/Focusable.d.ts +27 -0
  50. package/dist/Focusable.d.ts.map +1 -0
  51. package/dist/Form.d.ts +41 -0
  52. package/dist/Form.d.ts.map +1 -0
  53. package/dist/GridList.d.ts +69 -10
  54. package/dist/GridList.d.ts.map +1 -1
  55. package/dist/HiddenDateInput.d.ts +26 -0
  56. package/dist/HiddenDateInput.d.ts.map +1 -0
  57. package/dist/HiddenTimeInput.d.ts +25 -0
  58. package/dist/HiddenTimeInput.d.ts.map +1 -0
  59. package/dist/Icon.d.ts +57 -0
  60. package/dist/Icon.d.ts.map +1 -0
  61. package/dist/Keyboard.d.ts +13 -0
  62. package/dist/Keyboard.d.ts.map +1 -0
  63. package/dist/Landmark.d.ts +3 -3
  64. package/dist/Landmark.d.ts.map +1 -1
  65. package/dist/Link.d.ts +10 -4
  66. package/dist/Link.d.ts.map +1 -1
  67. package/dist/ListBox.d.ts +73 -11
  68. package/dist/ListBox.d.ts.map +1 -1
  69. package/dist/ListDropTargetDelegate.d.ts +38 -0
  70. package/dist/ListDropTargetDelegate.d.ts.map +1 -0
  71. package/dist/Menu.d.ts +79 -10
  72. package/dist/Menu.d.ts.map +1 -1
  73. package/dist/Meter.d.ts +4 -4
  74. package/dist/Meter.d.ts.map +1 -1
  75. package/dist/Modal.d.ts +6 -4
  76. package/dist/Modal.d.ts.map +1 -1
  77. package/dist/NumberField.d.ts +10 -12
  78. package/dist/NumberField.d.ts.map +1 -1
  79. package/dist/Popover.d.ts +32 -7
  80. package/dist/Popover.d.ts.map +1 -1
  81. package/dist/Pressable.d.ts +27 -0
  82. package/dist/Pressable.d.ts.map +1 -0
  83. package/dist/ProgressBar.d.ts +6 -4
  84. package/dist/ProgressBar.d.ts.map +1 -1
  85. package/dist/RadioGroup.d.ts +43 -9
  86. package/dist/RadioGroup.d.ts.map +1 -1
  87. package/dist/RangeCalendar.d.ts +39 -7
  88. package/dist/RangeCalendar.d.ts.map +1 -1
  89. package/dist/RouterProvider.d.ts +75 -0
  90. package/dist/RouterProvider.d.ts.map +1 -0
  91. package/dist/SearchField.d.ts +23 -21
  92. package/dist/SearchField.d.ts.map +1 -1
  93. package/dist/Select.d.ts +48 -7
  94. package/dist/Select.d.ts.map +1 -1
  95. package/dist/SelectionIndicator.d.ts +30 -0
  96. package/dist/SelectionIndicator.d.ts.map +1 -0
  97. package/dist/Separator.d.ts +9 -3
  98. package/dist/Separator.d.ts.map +1 -1
  99. package/dist/SharedElementTransition.d.ts +41 -0
  100. package/dist/SharedElementTransition.d.ts.map +1 -0
  101. package/dist/Slider.d.ts +15 -8
  102. package/dist/Slider.d.ts.map +1 -1
  103. package/dist/StepList.d.ts +90 -0
  104. package/dist/StepList.d.ts.map +1 -0
  105. package/dist/Switch.d.ts +11 -5
  106. package/dist/Switch.d.ts.map +1 -1
  107. package/dist/Table.d.ts +222 -19
  108. package/dist/Table.d.ts.map +1 -1
  109. package/dist/Tabs.d.ts +47 -10
  110. package/dist/Tabs.d.ts.map +1 -1
  111. package/dist/TagGroup.d.ts +22 -10
  112. package/dist/TagGroup.d.ts.map +1 -1
  113. package/dist/Text.d.ts +10 -0
  114. package/dist/Text.d.ts.map +1 -0
  115. package/dist/TextField.d.ts +19 -11
  116. package/dist/TextField.d.ts.map +1 -1
  117. package/dist/TimeField.d.ts +32 -7
  118. package/dist/TimeField.d.ts.map +1 -1
  119. package/dist/Toast.d.ts +29 -14
  120. package/dist/Toast.d.ts.map +1 -1
  121. package/dist/ToggleButton.d.ts +36 -0
  122. package/dist/ToggleButton.d.ts.map +1 -0
  123. package/dist/ToggleButtonGroup.d.ts +33 -0
  124. package/dist/ToggleButtonGroup.d.ts.map +1 -0
  125. package/dist/Toolbar.d.ts +7 -3
  126. package/dist/Toolbar.d.ts.map +1 -1
  127. package/dist/Tooltip.d.ts +58 -7
  128. package/dist/Tooltip.d.ts.map +1 -1
  129. package/dist/Tree.d.ts +102 -11
  130. package/dist/Tree.d.ts.map +1 -1
  131. package/dist/Virtualizer.d.ts +61 -0
  132. package/dist/Virtualizer.d.ts.map +1 -0
  133. package/dist/VirtualizerLayouts.d.ts +82 -0
  134. package/dist/VirtualizerLayouts.d.ts.map +1 -0
  135. package/dist/VisuallyHidden.d.ts +4 -2
  136. package/dist/VisuallyHidden.d.ts.map +1 -1
  137. package/dist/contexts.d.ts +6 -1
  138. package/dist/contexts.d.ts.map +1 -1
  139. package/dist/index.d.ts +73 -39
  140. package/dist/index.d.ts.map +1 -1
  141. package/dist/index.js +23342 -10644
  142. package/dist/index.js.map +1 -7
  143. package/dist/index.jsx +18110 -0
  144. package/dist/index.jsx.map +1 -0
  145. package/dist/useDragAndDrop.d.ts +93 -0
  146. package/dist/useDragAndDrop.d.ts.map +1 -0
  147. package/dist/utils.d.ts +8 -2
  148. package/dist/utils.d.ts.map +1 -1
  149. package/dist/virtualizer/Layout.d.ts +79 -0
  150. package/dist/virtualizer/Layout.d.ts.map +1 -0
  151. package/package.json +33 -32
  152. package/src/ActionBar.tsx +251 -0
  153. package/src/ActionGroup.tsx +277 -0
  154. package/src/Alert.tsx +152 -0
  155. package/src/Autocomplete.tsx +39 -44
  156. package/src/Breadcrumbs.tsx +227 -72
  157. package/src/Button.tsx +315 -74
  158. package/src/Calendar.tsx +347 -141
  159. package/src/Checkbox.tsx +414 -123
  160. package/src/Collection.tsx +350 -0
  161. package/src/Color.tsx +1325 -284
  162. package/src/ColorEditor.tsx +213 -0
  163. package/src/ComboBox.tsx +644 -245
  164. package/src/ContextualHelpTrigger.tsx +195 -0
  165. package/src/DateField.tsx +274 -106
  166. package/src/DatePicker.tsx +892 -111
  167. package/src/DateRangePickerContext.tsx +44 -0
  168. package/src/Dialog.tsx +173 -104
  169. package/src/Disclosure.tsx +158 -105
  170. package/src/DragAndDrop.tsx +340 -0
  171. package/src/DragPreview.tsx +47 -0
  172. package/src/DropZone.tsx +233 -0
  173. package/src/FieldError.tsx +89 -0
  174. package/src/FileTrigger.tsx +83 -0
  175. package/src/Focusable.tsx +103 -0
  176. package/src/Form.tsx +140 -0
  177. package/src/GridList.tsx +542 -128
  178. package/src/HiddenDateInput.tsx +153 -0
  179. package/src/HiddenTimeInput.tsx +133 -0
  180. package/src/Icon.tsx +133 -0
  181. package/src/Keyboard.tsx +26 -0
  182. package/src/Landmark.tsx +37 -63
  183. package/src/Link.tsx +132 -69
  184. package/src/ListBox.tsx +656 -106
  185. package/src/ListDropTargetDelegate.ts +283 -0
  186. package/src/Menu.tsx +1234 -132
  187. package/src/Meter.tsx +44 -58
  188. package/src/Modal.tsx +262 -166
  189. package/src/NumberField.tsx +267 -151
  190. package/src/Popover.tsx +452 -343
  191. package/src/Pressable.tsx +108 -0
  192. package/src/ProgressBar.tsx +54 -59
  193. package/src/RadioGroup.tsx +533 -121
  194. package/src/RangeCalendar.tsx +249 -150
  195. package/src/RouterProvider.tsx +223 -0
  196. package/src/SearchField.tsx +460 -133
  197. package/src/Select.tsx +804 -233
  198. package/src/SelectionIndicator.tsx +108 -0
  199. package/src/Separator.tsx +47 -49
  200. package/src/SharedElementTransition.tsx +264 -0
  201. package/src/Slider.tsx +148 -98
  202. package/src/StepList.tsx +272 -0
  203. package/src/Switch.tsx +93 -46
  204. package/src/Table.tsx +1551 -225
  205. package/src/Tabs.tsx +377 -123
  206. package/src/TagGroup.tsx +233 -135
  207. package/src/Text.tsx +18 -0
  208. package/src/TextField.tsx +413 -86
  209. package/src/TimeField.tsx +232 -222
  210. package/src/Toast.tsx +306 -160
  211. package/src/ToggleButton.tsx +169 -0
  212. package/src/ToggleButtonGroup.tsx +141 -0
  213. package/src/Toolbar.tsx +61 -70
  214. package/src/Tooltip.tsx +473 -116
  215. package/src/Tree.tsx +1514 -175
  216. package/src/Virtualizer.tsx +730 -0
  217. package/src/VirtualizerLayouts.ts +280 -0
  218. package/src/VisuallyHidden.tsx +32 -38
  219. package/src/contexts.ts +29 -36
  220. package/src/index.ts +972 -620
  221. package/src/useDragAndDrop.ts +367 -0
  222. package/src/utils.tsx +69 -50
  223. package/src/virtualizer/Layout.ts +192 -0
  224. package/dist/index.ssr.js +0 -9785
  225. package/dist/index.ssr.js.map +0 -7
package/src/Modal.tsx CHANGED
@@ -15,13 +15,14 @@ import {
15
15
  splitProps,
16
16
  Show,
17
17
  useContext,
18
- } from 'solid-js'
19
- import { Portal, isServer } from 'solid-js/web'
18
+ } from "solid-js";
19
+ import { Portal, isServer } from "solid-js/web";
20
20
  import {
21
21
  createInteractOutside,
22
22
  ariaHideOutside,
23
23
  FocusScope,
24
- } from '@proyecto-viviana/solidaria'
24
+ useUNSAFE_PortalContext,
25
+ } from "@proyecto-viviana/solidaria";
25
26
  import {
26
27
  type RenderChildren,
27
28
  type ClassNameOrFunction,
@@ -29,197 +30,234 @@ import {
29
30
  useRenderProps,
30
31
  filterDOMProps,
31
32
  dataAttr,
32
- } from './utils'
33
+ useIsHydrated,
34
+ } from "./utils";
33
35
  import {
34
36
  DialogTriggerContext,
35
37
  OverlayTriggerStateContext,
36
38
  type OverlayTriggerState,
37
- } from './contexts'
38
-
39
- // ============================================
40
- // INTERNAL CONTEXT
41
- // ============================================
39
+ } from "./contexts";
42
40
 
43
41
  /**
44
42
  * Internal context to signal that Modal is wrapped in ModalOverlay.
45
43
  * When present, Modal should not create its own Portal.
46
44
  */
47
45
  interface InternalModalContextValue {
48
- isDismissable?: boolean
49
- isKeyboardDismissDisabled?: boolean
46
+ isDismissable?: boolean;
47
+ isKeyboardDismissDisabled?: boolean;
50
48
  }
51
49
 
52
- const InternalModalContext = createContext<InternalModalContextValue | null>(null)
50
+ const InternalModalContext = createContext<InternalModalContextValue | null>(null);
51
+
52
+ // Stack of visible modals, used to ensure only the top-most modal dismisses on Escape/outside interaction.
53
+ const visibleModals: Array<() => Element | null> = [];
53
54
 
54
- // ============================================
55
- // TYPES
56
- // ============================================
55
+ function pruneDisconnectedModals() {
56
+ for (let index = visibleModals.length - 1; index >= 0; index -= 1) {
57
+ const element = visibleModals[index]?.();
58
+ if (!element?.isConnected) {
59
+ visibleModals.splice(index, 1);
60
+ }
61
+ }
62
+ }
57
63
 
58
64
  export interface ModalRenderProps {
59
65
  /** Whether the modal is currently entering (for animations). */
60
- isEntering: boolean
66
+ isEntering: boolean;
61
67
  /** Whether the modal is currently exiting (for animations). */
62
- isExiting: boolean
68
+ isExiting: boolean;
63
69
  }
64
70
 
65
71
  export interface ModalOverlayProps {
66
72
  /** The children of the component - can be JSX or render function. */
67
- children?: RenderChildren<ModalRenderProps>
73
+ children?: RenderChildren<ModalRenderProps>;
68
74
  /** The CSS className for the element. */
69
- class?: ClassNameOrFunction<ModalRenderProps>
75
+ class?: ClassNameOrFunction<ModalRenderProps>;
70
76
  /** The inline style for the element. */
71
- style?: StyleOrFunction<ModalRenderProps>
77
+ style?: StyleOrFunction<ModalRenderProps>;
72
78
  /** Whether the modal is open (controlled). */
73
- isOpen?: boolean
79
+ isOpen?: boolean;
74
80
  /** Whether the modal opens by default (uncontrolled). */
75
- defaultOpen?: boolean
81
+ defaultOpen?: boolean;
76
82
  /** Handler called when the modal's open state changes. */
77
- onOpenChange?: (isOpen: boolean) => void
83
+ onOpenChange?: (isOpen: boolean) => void;
78
84
  /** Whether clicking outside the modal closes it. */
79
- isDismissable?: boolean
85
+ isDismissable?: boolean;
80
86
  /** Whether pressing Escape closes the modal. */
81
- isKeyboardDismissDisabled?: boolean
87
+ isKeyboardDismissDisabled?: boolean;
82
88
  /** Whether the modal is entering (for animations). */
83
- isEntering?: boolean
89
+ isEntering?: boolean;
84
90
  /** Whether the modal is exiting (for animations). */
85
- isExiting?: boolean
91
+ isExiting?: boolean;
86
92
  }
87
93
 
88
94
  export interface ModalProps extends ModalOverlayProps {}
89
95
 
90
- // Re-export from contexts for backwards compatibility
91
- export { OverlayTriggerStateContext, type OverlayTriggerState } from './contexts'
92
- export { useOverlayTriggerState } from './contexts'
93
-
94
- // ============================================
95
- // MODAL OVERLAY COMPONENT
96
- // ============================================
96
+ export { OverlayTriggerStateContext, type OverlayTriggerState } from "./contexts";
97
+ export { useOverlayTriggerState } from "./contexts";
98
+ export const ModalContext = OverlayTriggerStateContext;
97
99
 
98
100
  /**
99
101
  * ModalOverlay is the backdrop/underlay behind a modal.
100
102
  * It handles click-outside dismissal and provides styling hooks.
101
103
  */
102
104
  export function ModalOverlay(props: ModalOverlayProps): JSX.Element {
103
- if (isServer) {
104
- return <>{props.children}</>
105
- }
105
+ // Do NOT early-return on the server: rendering children bare on the server and a
106
+ // <Show>/<Portal> overlay on the client desyncs hydration. Run the same structure
107
+ // on both and gate the Portal on useIsHydrated() (see Popover for the rationale).
108
+ const isHydrated = useIsHydrated();
106
109
 
107
110
  // IMPORTANT: Don't destructure or access props.children early!
108
111
  // In SolidJS, children are lazily evaluated. Accessing them before
109
112
  // the context provider renders causes them to evaluate outside the context.
110
113
  // See: https://github.com/solidjs/solid/issues/182
111
114
  const [local, rest] = splitProps(props, [
112
- 'class',
113
- 'style',
114
- 'isOpen',
115
- 'defaultOpen',
116
- 'onOpenChange',
117
- 'isDismissable',
118
- 'isKeyboardDismissDisabled',
119
- 'isEntering',
120
- 'isExiting',
121
- ])
115
+ "class",
116
+ "style",
117
+ "isOpen",
118
+ "defaultOpen",
119
+ "onOpenChange",
120
+ "isDismissable",
121
+ "isKeyboardDismissDisabled",
122
+ "isEntering",
123
+ "isExiting",
124
+ ]);
122
125
 
123
126
  // Get state from DialogTrigger context if available
124
- const dialogTriggerContext = useContext(DialogTriggerContext)
127
+ const dialogTriggerContext = useContext(DialogTriggerContext);
125
128
 
126
129
  // Internal state for uncontrolled mode
127
- const [internalOpen, setInternalOpen] = createSignal(local.defaultOpen ?? false)
130
+ const [internalOpen, setInternalOpen] = createSignal(local.defaultOpen ?? false);
128
131
 
129
132
  // Determine if open (controlled > DialogTrigger context > uncontrolled)
130
133
  const isOpen = (): boolean => {
131
- if (local.isOpen !== undefined) return local.isOpen
132
- if (dialogTriggerContext) return dialogTriggerContext.state.isOpen()
133
- return internalOpen()
134
- }
134
+ if (local.isOpen !== undefined) return local.isOpen;
135
+ if (dialogTriggerContext) return dialogTriggerContext.state.isOpen();
136
+ return internalOpen();
137
+ };
135
138
 
136
139
  const close = () => {
137
140
  if (local.isOpen !== undefined) {
138
- local.onOpenChange?.(false)
141
+ local.onOpenChange?.(false);
139
142
  } else if (dialogTriggerContext) {
140
- dialogTriggerContext.state.close()
141
- local.onOpenChange?.(false)
143
+ dialogTriggerContext.state.close();
144
+ local.onOpenChange?.(false);
142
145
  } else {
143
- setInternalOpen(false)
144
- local.onOpenChange?.(false)
146
+ setInternalOpen(false);
147
+ local.onOpenChange?.(false);
145
148
  }
146
- }
149
+ };
147
150
 
148
151
  const open = () => {
149
152
  if (local.isOpen !== undefined) {
150
- local.onOpenChange?.(true)
153
+ local.onOpenChange?.(true);
151
154
  } else if (dialogTriggerContext) {
152
- dialogTriggerContext.state.open()
153
- local.onOpenChange?.(true)
155
+ dialogTriggerContext.state.open();
156
+ local.onOpenChange?.(true);
154
157
  } else {
155
- setInternalOpen(true)
156
- local.onOpenChange?.(true)
158
+ setInternalOpen(true);
159
+ local.onOpenChange?.(true);
157
160
  }
158
- }
161
+ };
159
162
 
160
163
  const toggle = () => {
161
164
  if (isOpen()) {
162
- close()
165
+ close();
163
166
  } else {
164
- open()
167
+ open();
165
168
  }
166
- }
169
+ };
167
170
 
168
- // Create overlay trigger state for context
169
171
  const state: OverlayTriggerState = {
170
- get isOpen() { return isOpen() },
172
+ get isOpen() {
173
+ return isOpen();
174
+ },
171
175
  open,
172
176
  close,
173
177
  toggle,
174
- }
178
+ };
175
179
 
176
- // Render props values
177
180
  const renderValues = createMemo<ModalRenderProps>(() => ({
178
181
  isEntering: local.isEntering ?? false,
179
182
  isExiting: local.isExiting ?? false,
180
- }))
183
+ }));
181
184
 
182
185
  // Resolve render props - don't pass children, we'll render props.children directly
183
186
  const renderProps = useRenderProps(
184
187
  {
185
188
  class: local.class,
186
189
  style: local.style,
187
- defaultClassName: 'solidaria-ModalOverlay',
190
+ defaultClassName: "solidaria-ModalOverlay",
188
191
  },
189
- renderValues
190
- )
192
+ renderValues,
193
+ );
191
194
 
192
- // Filter DOM props
193
- const domProps = createMemo(() => filterDOMProps(rest as Record<string, unknown>, { global: true }))
195
+ const domProps = createMemo(() =>
196
+ filterDOMProps(rest as Record<string, unknown>, { global: true }),
197
+ );
194
198
 
195
199
  // Internal context value to signal Modal that it's wrapped
196
200
  const internalModalContext: InternalModalContextValue = {
197
201
  isDismissable: local.isDismissable,
198
202
  isKeyboardDismissDisabled: local.isKeyboardDismissDisabled,
199
- }
203
+ };
204
+ const portalContext = useUNSAFE_PortalContext();
205
+ const portalContainer = () => portalContext.getContainer?.() ?? undefined;
206
+ let overlayRef!: HTMLDivElement;
207
+
208
+ const isTopMostModalInOverlay = () => {
209
+ pruneDisconnectedModals();
210
+ const topMostModal = visibleModals[visibleModals.length - 1]?.();
211
+ return !topMostModal || overlayRef?.contains(topMostModal);
212
+ };
213
+
214
+ const handleOverlayPointerDown: JSX.EventHandler<HTMLDivElement, PointerEvent> = (event) => {
215
+ if (local.isDismissable && event.target === event.currentTarget && isTopMostModalInOverlay()) {
216
+ close();
217
+ }
218
+ };
219
+
220
+ createEffect(() => {
221
+ if (!isOpen() || local.isKeyboardDismissDisabled) return;
222
+
223
+ const handleKeyDown = (event: KeyboardEvent) => {
224
+ if (event.key === "Escape" && !event.isComposing && isTopMostModalInOverlay()) {
225
+ event.preventDefault();
226
+ event.stopPropagation();
227
+ close();
228
+ }
229
+ };
230
+
231
+ document.addEventListener("keydown", handleKeyDown, true);
232
+ onCleanup(() => {
233
+ document.removeEventListener("keydown", handleKeyDown, true);
234
+ });
235
+ });
200
236
 
201
237
  // Resolve children - handle both static JSX and render functions
202
238
  // IMPORTANT: We access props.children directly (not local.children) to preserve
203
239
  // lazy evaluation inside context providers
204
240
  const resolveChildren = () => {
205
- const children = props.children
206
- if (typeof children === 'function') {
207
- return (children as (props: ModalRenderProps) => JSX.Element)(renderValues())
241
+ const children = props.children;
242
+ if (typeof children === "function") {
243
+ return (children as (props: ModalRenderProps) => JSX.Element)(renderValues());
208
244
  }
209
- return children
210
- }
245
+ return children;
246
+ };
211
247
 
212
248
  return (
213
- <Show when={isOpen() || local.isExiting}>
214
- <Portal>
249
+ <Show when={isHydrated() && (isOpen() || local.isExiting)}>
250
+ <Portal mount={portalContainer()}>
215
251
  <OverlayTriggerStateContext.Provider value={state}>
216
252
  <InternalModalContext.Provider value={internalModalContext}>
217
253
  <div
218
254
  {...domProps()}
255
+ ref={overlayRef}
219
256
  class={renderProps.class()}
220
257
  style={renderProps.style()}
221
258
  data-entering={dataAttr(local.isEntering)}
222
259
  data-exiting={dataAttr(local.isExiting)}
260
+ onPointerDown={handleOverlayPointerDown}
223
261
  >
224
262
  {resolveChildren()}
225
263
  </div>
@@ -227,13 +265,9 @@ export function ModalOverlay(props: ModalOverlayProps): JSX.Element {
227
265
  </OverlayTriggerStateContext.Provider>
228
266
  </Portal>
229
267
  </Show>
230
- )
268
+ );
231
269
  }
232
270
 
233
- // ============================================
234
- // MODAL COMPONENT
235
- // ============================================
236
-
237
271
  /**
238
272
  * Modal is a dialog container that manages focus trapping, scroll prevention,
239
273
  * aria-hiding of content outside, and dismissal.
@@ -249,7 +283,7 @@ export function ModalOverlay(props: ModalOverlayProps): JSX.Element {
249
283
  export function Modal(props: ModalProps): JSX.Element {
250
284
  // Check for InternalModalContext which signals we're inside a rendered ModalOverlay
251
285
  // This works because ModalContent is rendered INSIDE ModalOverlay's Show/Portal
252
- return <ModalContentWithAutoOverlay {...props} />
286
+ return <ModalContentWithAutoOverlay {...props} />;
253
287
  }
254
288
 
255
289
  /**
@@ -259,17 +293,17 @@ export function Modal(props: ModalProps): JSX.Element {
259
293
  */
260
294
  function ModalContentWithAutoOverlay(props: ModalProps): JSX.Element {
261
295
  const [overlayProps, modalProps] = splitProps(props, [
262
- 'isOpen',
263
- 'defaultOpen',
264
- 'onOpenChange',
265
- 'isDismissable',
266
- 'isKeyboardDismissDisabled',
267
- 'isEntering',
268
- 'isExiting',
269
- ])
296
+ "isOpen",
297
+ "defaultOpen",
298
+ "onOpenChange",
299
+ "isDismissable",
300
+ "isKeyboardDismissDisabled",
301
+ "isEntering",
302
+ "isExiting",
303
+ ]);
270
304
 
271
305
  // Check for InternalModalContext - if present, we're inside a ModalOverlay
272
- const internalContext = useContext(InternalModalContext)
306
+ const internalContext = useContext(InternalModalContext);
273
307
 
274
308
  // If wrapped in ModalOverlay, just render the content
275
309
  if (internalContext) {
@@ -277,14 +311,14 @@ function ModalContentWithAutoOverlay(props: ModalProps): JSX.Element {
277
311
  <ModalContent {...modalProps} internalContext={internalContext}>
278
312
  {props.children}
279
313
  </ModalContent>
280
- )
314
+ );
281
315
  }
282
316
 
283
317
  // For standalone usage, wrap in ModalOverlay
284
318
  const standaloneContext: InternalModalContextValue = {
285
319
  isDismissable: overlayProps.isDismissable,
286
320
  isKeyboardDismissDisabled: overlayProps.isKeyboardDismissDisabled,
287
- }
321
+ };
288
322
 
289
323
  return (
290
324
  <ModalOverlay {...overlayProps}>
@@ -292,127 +326,170 @@ function ModalContentWithAutoOverlay(props: ModalProps): JSX.Element {
292
326
  {props.children}
293
327
  </ModalContent>
294
328
  </ModalOverlay>
295
- )
329
+ );
296
330
  }
297
331
 
298
332
  /**
299
333
  * Internal component that renders the actual modal content.
300
334
  * Used by both standalone Modal and Modal wrapped in ModalOverlay.
301
335
  */
302
- function ModalContent(props: ModalProps & { internalContext: InternalModalContextValue }): JSX.Element {
336
+ function ModalContent(
337
+ props: ModalProps & { internalContext: InternalModalContextValue },
338
+ ): JSX.Element {
303
339
  if (isServer) {
304
- return <>{props.children}</>
340
+ return <>{props.children}</>;
305
341
  }
306
342
 
307
343
  const [local, rest] = splitProps(props, [
308
- 'children',
309
- 'class',
310
- 'style',
311
- 'isOpen',
312
- 'defaultOpen',
313
- 'onOpenChange',
314
- 'isDismissable',
315
- 'isKeyboardDismissDisabled',
316
- 'isEntering',
317
- 'isExiting',
318
- 'internalContext',
319
- ])
320
-
321
- let modalRef!: HTMLDivElement
344
+ "children",
345
+ "class",
346
+ "style",
347
+ "isOpen",
348
+ "defaultOpen",
349
+ "onOpenChange",
350
+ "isDismissable",
351
+ "isKeyboardDismissDisabled",
352
+ "isEntering",
353
+ "isExiting",
354
+ "internalContext",
355
+ ]);
356
+
357
+ let modalRef!: HTMLDivElement;
358
+ const modalRefAccessor = () => modalRef ?? null;
322
359
 
323
360
  // Get state from parent OverlayTriggerStateContext (provided by ModalOverlay)
324
- const parentState = useContext(OverlayTriggerStateContext)
361
+ const parentState = useContext(OverlayTriggerStateContext);
325
362
 
326
363
  // Get dismissable settings from internal context (set by ModalOverlay)
327
- const isDismissable = () => local.internalContext?.isDismissable ?? local.isDismissable
328
- const isKeyboardDismissDisabled = () => local.internalContext?.isKeyboardDismissDisabled ?? local.isKeyboardDismissDisabled
364
+ const isDismissable = () => local.internalContext?.isDismissable ?? local.isDismissable;
365
+ const isKeyboardDismissDisabled = () =>
366
+ local.internalContext?.isKeyboardDismissDisabled ?? local.isKeyboardDismissDisabled;
329
367
 
330
368
  // Determine if open from parent state
331
369
  const isOpen = (): boolean => {
332
- if (local.isOpen !== undefined) return local.isOpen
333
- return parentState?.isOpen ?? false
334
- }
370
+ if (local.isOpen !== undefined) return local.isOpen;
371
+ return parentState?.isOpen ?? false;
372
+ };
373
+
374
+ // Keep this modal in a global stack so nested modals dismiss in top-down order.
375
+ createEffect(() => {
376
+ if (!isOpen()) return;
377
+
378
+ pruneDisconnectedModals();
379
+ if (!visibleModals.includes(modalRefAccessor)) {
380
+ visibleModals.push(modalRefAccessor);
381
+ }
382
+
383
+ onCleanup(() => {
384
+ const index = visibleModals.indexOf(modalRefAccessor);
385
+ if (index >= 0) {
386
+ visibleModals.splice(index, 1);
387
+ }
388
+ });
389
+ });
390
+
391
+ const isTopMostModal = () => {
392
+ pruneDisconnectedModals();
393
+ return visibleModals[visibleModals.length - 1] === modalRefAccessor;
394
+ };
335
395
 
336
396
  const close = () => {
337
397
  if (local.isOpen !== undefined) {
338
- local.onOpenChange?.(false)
398
+ local.onOpenChange?.(false);
339
399
  } else {
340
- parentState?.close()
400
+ parentState?.close();
341
401
  }
342
- }
402
+ };
343
403
 
344
404
  // Prevent scroll when modal is open
345
405
  createEffect(() => {
346
- if (!isOpen()) return
406
+ if (!isOpen()) return;
347
407
 
348
- // Set overflow hidden on html element
349
- const html = document.documentElement
350
- const prevOverflow = html.style.overflow
351
- html.style.overflow = 'hidden'
408
+ const html = document.documentElement;
409
+ const prevOverflow = html.style.overflow;
410
+ html.style.overflow = "hidden";
352
411
 
353
412
  onCleanup(() => {
354
- html.style.overflow = prevOverflow
355
- })
356
- })
413
+ html.style.overflow = prevOverflow;
414
+ });
415
+ });
357
416
 
358
417
  // Click outside to close (if dismissable)
359
418
  createEffect(() => {
360
- if (!isOpen() || !isDismissable()) return
419
+ if (!isOpen() || !isDismissable()) return;
361
420
 
362
421
  createInteractOutside({
363
- ref: () => modalRef ?? null,
422
+ ref: modalRefAccessor,
364
423
  onInteractOutside: () => {
365
- close()
424
+ if (isTopMostModal()) {
425
+ close();
426
+ }
366
427
  },
367
428
  isDisabled: false,
368
- })
369
- })
429
+ });
430
+ });
370
431
 
371
432
  // Escape key to close
372
433
  createEffect(() => {
373
- if (!isOpen() || isKeyboardDismissDisabled()) return
434
+ if (!isOpen() || isKeyboardDismissDisabled()) return;
374
435
 
375
436
  const handleKeyDown = (e: KeyboardEvent) => {
376
- if (e.key === 'Escape') {
377
- e.preventDefault()
378
- e.stopPropagation()
379
- close()
437
+ if (e.key === "Escape" && !e.isComposing && isTopMostModal()) {
438
+ e.preventDefault();
439
+ e.stopPropagation();
440
+ close();
380
441
  }
381
- }
442
+ };
382
443
 
383
- document.addEventListener('keydown', handleKeyDown, true)
444
+ document.addEventListener("keydown", handleKeyDown, true);
384
445
  onCleanup(() => {
385
- document.removeEventListener('keydown', handleKeyDown, true)
386
- })
387
- })
446
+ document.removeEventListener("keydown", handleKeyDown, true);
447
+ });
448
+ });
388
449
 
389
450
  // Aria-hide outside content
390
451
  createEffect(() => {
391
- if (!isOpen() || !modalRef) return
452
+ if (!isOpen() || !modalRef) return;
453
+
454
+ let cleanup: (() => void) | undefined;
455
+ let cancelled = false;
456
+ const ownerWindow = modalRef.ownerDocument.defaultView ?? window;
392
457
 
393
- const cleanup = ariaHideOutside([modalRef])
394
- onCleanup(cleanup)
395
- })
458
+ const hideOutside = () => {
459
+ if (cancelled || !modalRef?.isConnected) return;
460
+ cleanup = ariaHideOutside([modalRef]);
461
+ };
462
+
463
+ if (modalRef.isConnected) {
464
+ hideOutside();
465
+ } else {
466
+ ownerWindow.queueMicrotask(hideOutside);
467
+ }
468
+
469
+ onCleanup(() => {
470
+ cancelled = true;
471
+ cleanup?.();
472
+ });
473
+ });
396
474
 
397
- // Render props values
398
475
  const renderValues = createMemo<ModalRenderProps>(() => ({
399
476
  isEntering: local.isEntering ?? false,
400
477
  isExiting: local.isExiting ?? false,
401
- }))
478
+ }));
402
479
 
403
- // Resolve render props
404
480
  const renderProps = useRenderProps(
405
481
  {
406
482
  children: props.children,
407
483
  class: local.class,
408
484
  style: local.style,
409
- defaultClassName: 'solidaria-Modal',
485
+ defaultClassName: "solidaria-Modal",
410
486
  },
411
- renderValues
412
- )
487
+ renderValues,
488
+ );
413
489
 
414
- // Filter DOM props
415
- const domProps = createMemo(() => filterDOMProps(rest as Record<string, unknown>, { global: true }))
490
+ const domProps = createMemo(() =>
491
+ filterDOMProps(rest as Record<string, unknown>, { global: true }),
492
+ );
416
493
 
417
494
  return (
418
495
  <FocusScope contain restoreFocus autoFocus>
@@ -424,10 +501,29 @@ function ModalContent(props: ModalProps & { internalContext: InternalModalContex
424
501
  data-entering={dataAttr(local.isEntering)}
425
502
  data-exiting={dataAttr(local.isExiting)}
426
503
  >
504
+ <Show when={isDismissable()}>
505
+ <button
506
+ type="button"
507
+ aria-label="Dismiss"
508
+ tabIndex={-1}
509
+ onClick={close}
510
+ style={{
511
+ position: "absolute",
512
+ width: "1px",
513
+ height: "1px",
514
+ padding: 0,
515
+ margin: "-1px",
516
+ overflow: "hidden",
517
+ clip: "rect(0, 0, 0, 0)",
518
+ "white-space": "nowrap",
519
+ border: 0,
520
+ }}
521
+ />
522
+ </Show>
427
523
  {renderProps.renderChildren()}
428
524
  </div>
429
525
  </FocusScope>
430
- )
526
+ );
431
527
  }
432
528
 
433
- export default Modal
529
+ export default Modal;