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