@proyecto-viviana/solidaria-components 0.2.9 → 0.3.1

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 +23253 -18564
  139. package/dist/index.js.map +1 -1
  140. package/dist/index.jsx +18116 -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 +251 -176
  186. package/src/NumberField.tsx +139 -143
  187. package/src/Popover.tsx +396 -234
  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 +216 -158
  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 +49 -60
  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/Popover.tsx CHANGED
@@ -16,17 +16,18 @@ import {
16
16
  splitProps,
17
17
  useContext,
18
18
  Show,
19
- } from 'solid-js'
20
- import { Portal, isServer } from 'solid-js/web'
19
+ } from "solid-js";
20
+ import { Portal } from "solid-js/web";
21
21
  import {
22
22
  createOverlayTrigger,
23
23
  createPopover,
24
24
  FocusScope,
25
25
  useUNSAFE_PortalContext,
26
+ visuallyHiddenStyles,
26
27
  type Placement,
27
28
  type PlacementAxis,
28
- } from '@proyecto-viviana/solidaria'
29
- import { createOverlayTriggerState } from '@proyecto-viviana/solid-stately'
29
+ } from "@proyecto-viviana/solidaria";
30
+ import { createOverlayTriggerState } from "@proyecto-viviana/solid-stately";
30
31
  import {
31
32
  type RenderChildren,
32
33
  type ClassNameOrFunction,
@@ -35,160 +36,177 @@ import {
35
36
  useRenderProps,
36
37
  filterDOMProps,
37
38
  dataAttr,
38
- } from './utils'
39
- import { PopoverTriggerContext } from './contexts'
40
-
41
- // ============================================
42
- // TYPES
43
- // ============================================
39
+ useIsHydrated,
40
+ } from "./utils";
41
+ import { DialogTriggerContext, PopoverTriggerContext } from "./contexts";
44
42
 
45
43
  export interface PopoverRenderProps {
46
44
  /**
47
45
  * The name of the component that triggered the popover.
48
46
  */
49
- trigger: string | null
47
+ trigger: string | null;
50
48
  /**
51
49
  * The placement of the popover relative to the trigger.
52
50
  */
53
- placement: PlacementAxis | null
51
+ placement: PlacementAxis | null;
54
52
  /**
55
53
  * Whether the popover is currently entering (for animations).
56
54
  */
57
- isEntering: boolean
55
+ isEntering: boolean;
58
56
  /**
59
57
  * Whether the popover is currently exiting (for animations).
60
58
  */
61
- isExiting: boolean
59
+ isExiting: boolean;
62
60
  }
63
61
 
64
62
  export interface PopoverProps extends SlotProps {
65
63
  /** The children of the component - can be JSX or render function. */
66
- children?: RenderChildren<PopoverRenderProps>
64
+ children?: RenderChildren<PopoverRenderProps>;
67
65
  /** The CSS className for the element. */
68
- class?: ClassNameOrFunction<PopoverRenderProps>
66
+ class?: ClassNameOrFunction<PopoverRenderProps>;
69
67
  /** The inline style for the element. */
70
- style?: StyleOrFunction<PopoverRenderProps>
68
+ style?: StyleOrFunction<PopoverRenderProps>;
71
69
  /**
72
70
  * The name of the component that triggered the popover.
73
71
  */
74
- trigger?: string
72
+ trigger?: string;
75
73
  /**
76
74
  * The ref for the element which the popover positions itself with respect to.
77
75
  * Required when used standalone (not within a trigger component).
78
76
  */
79
- triggerRef?: () => Element | null
77
+ triggerRef?: () => Element | null;
80
78
  /**
81
79
  * The placement of the element with respect to its anchor element.
82
80
  * @default 'bottom'
83
81
  */
84
- placement?: Placement
82
+ placement?: Placement;
85
83
  /**
86
84
  * The placement padding that should be applied between the element and its
87
85
  * surrounding container.
88
86
  * @default 12
89
87
  */
90
- containerPadding?: number
88
+ containerPadding?: number;
91
89
  /**
92
90
  * The additional offset applied along the main axis between the element and its
93
91
  * anchor element.
94
92
  * @default 8
95
93
  */
96
- offset?: number
94
+ offset?: number;
97
95
  /**
98
96
  * The additional offset applied along the cross axis between the element and its
99
97
  * anchor element.
100
98
  * @default 0
101
99
  */
102
- crossOffset?: number
100
+ crossOffset?: number;
103
101
  /**
104
102
  * Whether the element should flip its orientation when there is insufficient room.
105
103
  * @default true
106
104
  */
107
- shouldFlip?: boolean
105
+ shouldFlip?: boolean;
106
+ /**
107
+ * The max height of the popover.
108
+ */
109
+ maxHeight?: number;
110
+ /**
111
+ * A boundary element for placement calculations.
112
+ */
113
+ boundaryElement?: Element;
114
+ /**
115
+ * A ref for the popover arrow element.
116
+ */
117
+ arrowRef?: () => Element | null;
118
+ /**
119
+ * A ref for the scrollable popover element.
120
+ */
121
+ scrollRef?: () => Element | null;
108
122
  /**
109
123
  * Whether the popover is non-modal (allows interaction outside).
110
124
  */
111
- isNonModal?: boolean
125
+ isNonModal?: boolean;
112
126
  /**
113
127
  * Whether pressing Escape to close should be disabled.
114
128
  */
115
- isKeyboardDismissDisabled?: boolean
129
+ isKeyboardDismissDisabled?: boolean;
116
130
  /**
117
131
  * Filter for which outside interactions should close the popover.
118
132
  */
119
- shouldCloseOnInteractOutside?: (element: Element) => boolean
133
+ shouldCloseOnInteractOutside?: (element: Element) => boolean;
120
134
  /** Whether the popover is open (controlled). */
121
- isOpen?: boolean
135
+ isOpen?: boolean;
122
136
  /** Whether the popover opens by default (uncontrolled). */
123
- defaultOpen?: boolean
137
+ defaultOpen?: boolean;
124
138
  /** Handler called when the popover's open state changes. */
125
- onOpenChange?: (isOpen: boolean) => void
139
+ onOpenChange?: (isOpen: boolean) => void;
140
+ /**
141
+ * Whether focus should move to the popover container on open.
142
+ * @default true
143
+ */
144
+ autoFocus?: boolean;
126
145
  /** Whether the popover is entering (for animations). */
127
- isEntering?: boolean
146
+ isEntering?: boolean;
128
147
  /** Whether the popover is exiting (for animations). */
129
- isExiting?: boolean
148
+ isExiting?: boolean;
130
149
  }
131
150
 
132
151
  export interface PopoverTriggerProps {
133
152
  /** The children - should include a trigger and popover content. */
134
- children: JSX.Element
153
+ children: JSX.Element;
135
154
  /** Whether the popover is open (controlled). */
136
- isOpen?: boolean
155
+ isOpen?: boolean;
137
156
  /** Whether the popover is open by default (uncontrolled). */
138
- defaultOpen?: boolean
157
+ defaultOpen?: boolean;
139
158
  /** Callback when open state changes. */
140
- onOpenChange?: (isOpen: boolean) => void
159
+ onOpenChange?: (isOpen: boolean) => void;
141
160
  }
142
161
 
143
- // ============================================
144
- // CONTEXTS
145
- // ============================================
146
-
147
- // Re-export from shared contexts
148
- export { PopoverTriggerContext, usePopoverTrigger, type PopoverTriggerContextValue } from './contexts'
162
+ export {
163
+ PopoverTriggerContext,
164
+ usePopoverTrigger,
165
+ type PopoverTriggerContextValue,
166
+ } from "./contexts";
149
167
 
150
168
  interface PopoverContextValue {
151
- placement: () => PlacementAxis | null
152
- arrowProps: () => JSX.HTMLAttributes<HTMLElement>
169
+ placement: () => PlacementAxis | null;
170
+ arrowProps: () => JSX.HTMLAttributes<HTMLElement>;
153
171
  }
154
172
 
155
- // Internal context for placement + arrow
156
- export const PopoverContext = createContext<PopoverContextValue | null>(null)
173
+ export const PopoverContext = createContext<PopoverContextValue | null>(null);
174
+ const PopoverGroupContext = createContext<(() => HTMLElement | null) | null>(null);
157
175
 
158
- // ============================================
159
- // POPOVER TRIGGER COMPONENT
160
- // ============================================
176
+ function PopoverDismissButton(props: { onDismiss: () => void }): JSX.Element {
177
+ return (
178
+ <button
179
+ type="button"
180
+ aria-label="Dismiss"
181
+ tabIndex={-1}
182
+ onClick={props.onDismiss}
183
+ style={visuallyHiddenStyles}
184
+ />
185
+ );
186
+ }
161
187
 
162
188
  /**
163
189
  * A PopoverTrigger opens a popover when a trigger element is pressed.
164
190
  * Children should include a trigger element (e.g. Button) and the Popover.
165
191
  */
166
192
  export function PopoverTrigger(props: PopoverTriggerProps): JSX.Element {
167
- const [local] = splitProps(props, ['isOpen', 'defaultOpen', 'onOpenChange'])
193
+ const [local] = splitProps(props, ["isOpen", "defaultOpen", "onOpenChange"]);
168
194
 
169
- // Create overlay trigger state
170
195
  const state = createOverlayTriggerState({
171
196
  get isOpen() {
172
- return local.isOpen
197
+ return local.isOpen;
173
198
  },
174
199
  get defaultOpen() {
175
- return local.defaultOpen
200
+ return local.defaultOpen;
176
201
  },
177
202
  onOpenChange: local.onOpenChange,
178
- })
203
+ });
179
204
 
180
- // Ref for the trigger element
181
- let triggerRef: HTMLElement | null = null
182
- const triggerId = createUniqueId()
205
+ let triggerRef: HTMLElement | null = null;
206
+ const triggerId = createUniqueId();
183
207
 
184
- // Create overlay trigger (for side effects like scroll close)
185
- createOverlayTrigger(
186
- { type: 'dialog' },
187
- state,
188
- () => triggerRef
189
- )
208
+ const triggerAria = createOverlayTrigger({ type: "dialog" }, state, () => triggerRef);
190
209
 
191
- // Context value
192
210
  const contextValue = createMemo(() => ({
193
211
  state: {
194
212
  isOpen: () => state.isOpen(),
@@ -198,299 +216,443 @@ export function PopoverTrigger(props: PopoverTriggerProps): JSX.Element {
198
216
  },
199
217
  triggerRef: () => triggerRef,
200
218
  setTriggerRef: (el: HTMLElement | null) => {
201
- if (!el) return
219
+ if (!el) return;
202
220
  if (!triggerRef || !triggerRef.isConnected) {
203
- triggerRef = el
221
+ triggerRef = el;
204
222
  }
205
223
  },
206
224
  triggerId,
207
- trigger: 'PopoverTrigger',
208
- }))
225
+ triggerProps: triggerAria.triggerProps,
226
+ overlayProps: triggerAria.overlayProps,
227
+ trigger: "PopoverTrigger",
228
+ }));
209
229
 
210
230
  return (
211
231
  <PopoverTriggerContext.Provider value={contextValue()}>
212
232
  {props.children}
213
233
  </PopoverTriggerContext.Provider>
214
- )
234
+ );
215
235
  }
216
236
 
217
- // ============================================
218
- // POPOVER COMPONENT
219
- // ============================================
220
-
221
237
  /**
222
238
  * A popover is an overlay element positioned relative to a trigger.
223
239
  */
224
240
  export function Popover(props: PopoverProps): JSX.Element {
225
- if (isServer) {
226
- // On the server, return null - popovers should not render during SSR
227
- return null as unknown as JSX.Element
228
- }
229
-
241
+ // Note: do NOT early-return on the server. Returning `null` on the server and a
242
+ // full <Show>/<Portal> tree on the client desyncs Solid's hydration walk (the
243
+ // server emits no marker for the <Show>), which surfaces as "Hydration Mismatch /
244
+ // getNextElement" in the parent (e.g. Picker). Instead, run the same structure on
245
+ // both and gate the Portal on `useIsHydrated()` so the overlay only mounts on the
246
+ // client after hydration — the server + first client render both produce an empty
247
+ // <Show> marker, so hydration aligns.
230
248
  const [local, rest] = splitProps(props, [
231
- 'class',
232
- 'style',
233
- 'trigger',
234
- 'triggerRef',
235
- 'placement',
236
- 'containerPadding',
237
- 'offset',
238
- 'crossOffset',
239
- 'shouldFlip',
240
- 'isNonModal',
241
- 'isKeyboardDismissDisabled',
242
- 'shouldCloseOnInteractOutside',
243
- 'isOpen',
244
- 'defaultOpen',
245
- 'onOpenChange',
246
- 'isEntering',
247
- 'isExiting',
248
- ])
249
-
250
- let popoverRef!: HTMLDivElement
251
-
252
- // Get trigger context if available
253
- const triggerContext = useContext(PopoverTriggerContext)
254
-
255
- // Internal state for uncontrolled mode
256
- const [internalOpen, setInternalOpen] = createSignal(local.defaultOpen ?? false)
257
-
258
- // Determine if open
249
+ "class",
250
+ "style",
251
+ "trigger",
252
+ "triggerRef",
253
+ "placement",
254
+ "containerPadding",
255
+ "offset",
256
+ "crossOffset",
257
+ "shouldFlip",
258
+ "maxHeight",
259
+ "boundaryElement",
260
+ "arrowRef",
261
+ "scrollRef",
262
+ "isNonModal",
263
+ "isKeyboardDismissDisabled",
264
+ "shouldCloseOnInteractOutside",
265
+ "isOpen",
266
+ "defaultOpen",
267
+ "onOpenChange",
268
+ "autoFocus",
269
+ "isEntering",
270
+ "isExiting",
271
+ ]);
272
+
273
+ let popoverRef!: HTMLDivElement;
274
+ const [groupRef, setGroupRef] = createSignal<HTMLDivElement | null>(null);
275
+ // False on the server and during hydration; true after onMount. Gates the Portal
276
+ // so overlay content only ever renders client-side, post-hydration.
277
+ const isHydrated = useIsHydrated();
278
+
279
+ const triggerContext = useContext(PopoverTriggerContext);
280
+ const dialogTriggerContext = useContext(DialogTriggerContext);
281
+ const popoverGroupContext = useContext(PopoverGroupContext);
282
+ const resolvedTrigger = () =>
283
+ local.trigger ??
284
+ triggerContext?.trigger ??
285
+ (dialogTriggerContext ? "DialogTrigger" : undefined);
286
+ const isSubPopover = () => resolvedTrigger() === "SubmenuTrigger" && popoverGroupContext != null;
287
+
288
+ const [internalOpen, setInternalOpen] = createSignal(local.defaultOpen ?? false);
289
+
259
290
  const isOpen = (): boolean => {
260
- if (local.isOpen !== undefined) return local.isOpen
291
+ if (local.isOpen !== undefined) return local.isOpen;
261
292
  if (triggerContext) {
262
- return triggerContext.state.isOpen()
293
+ return triggerContext.state.isOpen();
263
294
  }
264
- return internalOpen()
265
- }
295
+ if (dialogTriggerContext) {
296
+ return dialogTriggerContext.state.isOpen();
297
+ }
298
+ return internalOpen();
299
+ };
266
300
 
267
301
  const close = () => {
268
302
  if (local.isOpen !== undefined) {
269
- local.onOpenChange?.(false)
303
+ local.onOpenChange?.(false);
270
304
  } else if (triggerContext) {
271
- triggerContext.state.close()
272
- local.onOpenChange?.(false)
305
+ triggerContext.state.close();
306
+ local.onOpenChange?.(false);
307
+ } else if (dialogTriggerContext) {
308
+ dialogTriggerContext.state.close();
309
+ local.onOpenChange?.(false);
273
310
  } else {
274
- setInternalOpen(false)
275
- local.onOpenChange?.(false)
311
+ setInternalOpen(false);
312
+ local.onOpenChange?.(false);
276
313
  }
277
- }
314
+ };
278
315
 
279
- // Get trigger ref
280
316
  const getTriggerRef = () => {
281
- if (local.triggerRef) return local.triggerRef()
282
- if (triggerContext) return triggerContext.triggerRef()
283
- return null
284
- }
317
+ if (local.triggerRef) return local.triggerRef();
318
+ if (triggerContext) return triggerContext.triggerRef();
319
+ if (dialogTriggerContext) return dialogTriggerContext.triggerRef();
320
+ return null;
321
+ };
285
322
 
286
323
  const popoverAria = createPopover(
287
324
  {
288
325
  triggerRef: getTriggerRef,
289
326
  popoverRef: () => popoverRef ?? null,
327
+ groupRef: () => (isSubPopover() ? (popoverGroupContext?.() ?? null) : groupRef()),
290
328
  get placement() {
291
- return local.placement
329
+ return local.placement;
292
330
  },
293
331
  get containerPadding() {
294
- return local.containerPadding
332
+ return local.containerPadding;
295
333
  },
296
334
  get offset() {
297
- return local.offset ?? 8
335
+ return local.offset ?? 8;
298
336
  },
299
337
  get crossOffset() {
300
- return local.crossOffset
338
+ return local.crossOffset;
301
339
  },
302
340
  get shouldFlip() {
303
- return local.shouldFlip
341
+ return local.shouldFlip;
342
+ },
343
+ get maxHeight() {
344
+ return local.maxHeight;
345
+ },
346
+ get boundaryElement() {
347
+ return local.boundaryElement;
348
+ },
349
+ get arrowRef() {
350
+ return local.arrowRef;
351
+ },
352
+ get scrollRef() {
353
+ return local.scrollRef;
304
354
  },
305
355
  get isNonModal() {
306
- return local.isNonModal
356
+ return local.isNonModal;
307
357
  },
308
358
  get isKeyboardDismissDisabled() {
309
- return local.isKeyboardDismissDisabled
359
+ return local.isKeyboardDismissDisabled;
310
360
  },
311
361
  get shouldCloseOnInteractOutside() {
312
- return local.shouldCloseOnInteractOutside
362
+ return local.shouldCloseOnInteractOutside;
313
363
  },
314
364
  get trigger() {
315
- return local.trigger ?? triggerContext?.trigger
365
+ return resolvedTrigger();
316
366
  },
317
367
  },
318
368
  {
319
369
  isOpen,
320
370
  open: () => {
321
371
  if (local.isOpen !== undefined) {
322
- local.onOpenChange?.(true)
372
+ local.onOpenChange?.(true);
323
373
  } else if (triggerContext) {
324
- triggerContext.state.open()
325
- local.onOpenChange?.(true)
374
+ triggerContext.state.open();
375
+ local.onOpenChange?.(true);
376
+ } else if (dialogTriggerContext) {
377
+ dialogTriggerContext.state.open();
378
+ local.onOpenChange?.(true);
326
379
  } else {
327
- setInternalOpen(true)
328
- local.onOpenChange?.(true)
380
+ setInternalOpen(true);
381
+ local.onOpenChange?.(true);
329
382
  }
330
383
  },
331
384
  close,
332
385
  toggle: () => {
333
- if (isOpen()) close()
386
+ if (isOpen()) close();
334
387
  else if (local.isOpen !== undefined) {
335
- local.onOpenChange?.(true)
388
+ local.onOpenChange?.(true);
336
389
  } else if (triggerContext) {
337
- triggerContext.state.toggle()
390
+ triggerContext.state.toggle();
391
+ } else if (dialogTriggerContext) {
392
+ dialogTriggerContext.state.toggle();
338
393
  } else {
339
- setInternalOpen(true)
340
- local.onOpenChange?.(true)
394
+ setInternalOpen(true);
395
+ local.onOpenChange?.(true);
341
396
  }
342
397
  },
343
- }
344
- )
398
+ },
399
+ );
345
400
 
346
- // Render props values
347
401
  const renderValues = createMemo<PopoverRenderProps>(() => ({
348
- trigger: local.trigger ?? triggerContext?.trigger ?? null,
402
+ trigger: resolvedTrigger() ?? null,
349
403
  placement: popoverAria.placement(),
350
404
  isEntering: local.isEntering ?? false,
351
405
  isExiting: local.isExiting ?? false,
352
- }))
406
+ }));
353
407
 
354
- // Resolve render props
355
408
  const renderProps = useRenderProps(
356
409
  {
357
- children: props.children,
410
+ // Read children lazily. The popover content is gated behind
411
+ // `<Show when={isHydrated() && …}>` below, so it must NOT be instantiated
412
+ // during the component body. An eager `children: props.children` reads the
413
+ // child getter at object-construction time, building the content template
414
+ // (and walking `getNextElement`) before the gate — which the server, with
415
+ // the gate closed, never emitted → hydration mismatch. The getter defers
416
+ // the read until `renderChildren()` runs inside the gated overlay.
417
+ get children() {
418
+ return props.children;
419
+ },
358
420
  class: local.class,
359
421
  style: local.style,
360
- defaultClassName: 'solidaria-Popover',
422
+ defaultClassName: "solidaria-Popover",
361
423
  },
362
- renderValues
363
- )
364
-
365
- // Filter DOM props
366
- const domProps = createMemo(() => filterDOMProps(rest as Record<string, unknown>, { global: true }))
424
+ renderValues,
425
+ );
426
+
427
+ const [triggerWidth, setTriggerWidth] = createSignal<string | undefined>();
428
+ const hasExplicitTriggerWidth = () => {
429
+ const style = renderProps.style() as (JSX.CSSProperties & Record<string, unknown>) | undefined;
430
+ return style?.["--trigger-width"] != null;
431
+ };
432
+ const updateTriggerWidth = () => {
433
+ const trigger = getTriggerRef();
434
+ if (!trigger || hasExplicitTriggerWidth()) return;
435
+ setTriggerWidth(`${trigger.getBoundingClientRect().width}px`);
436
+ };
437
+ createEffect(() => {
438
+ if (!isOpen()) return;
439
+ updateTriggerWidth();
440
+
441
+ const trigger = getTriggerRef();
442
+ if (!trigger || hasExplicitTriggerWidth() || typeof ResizeObserver === "undefined") return;
443
+
444
+ const observer = new ResizeObserver(updateTriggerWidth);
445
+ observer.observe(trigger);
446
+ onCleanup(() => observer.disconnect());
447
+ });
448
+
449
+ const domProps = createMemo(() =>
450
+ filterDOMProps(rest as Record<string, unknown>, { global: true }),
451
+ );
452
+ const overlayId = () => {
453
+ const restId = (rest as Record<string, unknown>).id as string | undefined;
454
+ return (
455
+ restId ??
456
+ (triggerContext?.overlayProps?.id as string | undefined) ??
457
+ (dialogTriggerContext?.overlayProps?.id as string | undefined)
458
+ );
459
+ };
367
460
 
368
- // Remove style/ref from spread props to avoid collisions
369
461
  const cleanPopoverProps = () => {
370
- const { style: _style, ref: _ref, ...remaining } = popoverAria.popoverProps as Record<string, unknown>
371
- return remaining
372
- }
462
+ const {
463
+ style: _style,
464
+ ref: _ref,
465
+ ...remaining
466
+ } = popoverAria.popoverProps as Record<string, unknown>;
467
+ return remaining;
468
+ };
373
469
 
374
470
  const mergedStyle = (): JSX.CSSProperties => {
375
- const ariaStyle = (popoverAria.popoverProps as Record<string, unknown>).style as JSX.CSSProperties | undefined
376
- const renderStyle = renderProps.style() || {}
471
+ const ariaStyle = (popoverAria.popoverProps as Record<string, unknown>).style as
472
+ | JSX.CSSProperties
473
+ | undefined;
474
+ const renderStyle = (renderProps.style() || {}) as JSX.CSSProperties & Record<string, unknown>;
377
475
  return {
378
- ...(ariaStyle ?? {}),
476
+ ...ariaStyle,
379
477
  ...renderStyle,
478
+ "--trigger-width": renderStyle["--trigger-width"] ?? triggerWidth(),
479
+ };
480
+ };
481
+
482
+ const shouldBeDialog = () => !local.isNonModal || resolvedTrigger() === "SubmenuTrigger";
483
+ const portalContext = useUNSAFE_PortalContext();
484
+ const portalContainer = () => {
485
+ if (isSubPopover()) {
486
+ return popoverGroupContext?.() ?? portalContext.getContainer?.() ?? undefined;
380
487
  }
381
- }
382
-
383
- // Check if we should render with dialog role
384
- const shouldBeDialog = () => !local.isNonModal
385
- const portalContext = useUNSAFE_PortalContext()
386
- const portalContainer = () => portalContext.getContainer?.() ?? undefined
488
+ return portalContext.getContainer?.() ?? undefined;
489
+ };
387
490
 
388
- // Ensure Escape handling works even when popover content has no focusable children.
491
+ // Match React Aria Components: focus the popover container only when no
492
+ // descendant has already moved focus during mount.
389
493
  createEffect(() => {
390
- if (!isOpen() || !shouldBeDialog()) return
391
- if (!popoverRef) return
392
- if (document.activeElement !== popoverRef) {
393
- popoverRef.focus()
494
+ if (!isOpen() || !shouldBeDialog()) return;
495
+ if ((local.autoFocus ?? true) === false) return;
496
+ if (!popoverRef) return;
497
+ if (resolvedTrigger() === "SubmenuTrigger") return;
498
+
499
+ let timeout: number | undefined;
500
+ let frame: number | undefined;
501
+
502
+ const focusIfNeeded = () => {
503
+ if (!isOpen() || !shouldBeDialog()) return;
504
+ if (!popoverRef || resolvedTrigger() === "SubmenuTrigger") return;
505
+ if (document.activeElement === popoverRef || popoverRef.contains(document.activeElement)) {
506
+ return;
507
+ }
508
+ popoverRef.focus();
509
+ };
510
+
511
+ const scheduleFocus = () => {
512
+ timeout = window.setTimeout(focusIfNeeded, 0);
513
+ };
514
+
515
+ if (typeof window.requestAnimationFrame === "function") {
516
+ frame = window.requestAnimationFrame(scheduleFocus);
517
+ } else {
518
+ scheduleFocus();
394
519
  }
395
- })
520
+
521
+ onCleanup(() => {
522
+ if (frame !== undefined) {
523
+ window.cancelAnimationFrame(frame);
524
+ }
525
+ if (timeout !== undefined) {
526
+ window.clearTimeout(timeout);
527
+ }
528
+ });
529
+ });
396
530
 
397
531
  // Fallback Escape handling for environments where focus is not moved into the popover.
398
532
  createEffect(() => {
399
- if (!isOpen()) return
400
- if (local.isKeyboardDismissDisabled) return
533
+ if (!isOpen()) return;
534
+ if (local.isKeyboardDismissDisabled) return;
401
535
 
402
536
  const onKeyDown = (event: KeyboardEvent) => {
403
- if (event.key !== 'Escape') return
404
- if (event.defaultPrevented) return
405
- close()
406
- }
407
-
408
- document.addEventListener('keydown', onKeyDown)
409
- onCleanup(() => document.removeEventListener('keydown', onKeyDown))
410
- })
537
+ if (event.key !== "Escape") return;
538
+ if (event.defaultPrevented) return;
539
+ close();
540
+ };
541
+
542
+ document.addEventListener("keydown", onKeyDown);
543
+ onCleanup(() => document.removeEventListener("keydown", onKeyDown));
544
+ });
545
+
546
+ const overlay = () => (
547
+ <PopoverContext.Provider
548
+ value={{ placement: popoverAria.placement, arrowProps: () => popoverAria.arrowProps }}
549
+ >
550
+ <FocusScope contain={shouldBeDialog()} restoreFocus>
551
+ <div
552
+ {...domProps()}
553
+ {...cleanPopoverProps()}
554
+ ref={popoverRef}
555
+ id={overlayId()}
556
+ role={shouldBeDialog() ? "dialog" : undefined}
557
+ tabIndex={shouldBeDialog() ? -1 : undefined}
558
+ class={renderProps.class()}
559
+ style={mergedStyle()}
560
+ data-trigger={resolvedTrigger()}
561
+ data-placement={popoverAria.placement()}
562
+ data-entering={dataAttr(local.isEntering)}
563
+ data-exiting={dataAttr(local.isExiting)}
564
+ >
565
+ <Show when={!local.isNonModal}>
566
+ <PopoverDismissButton onDismiss={close} />
567
+ </Show>
568
+ {renderProps.renderChildren()}
569
+ <PopoverDismissButton onDismiss={close} />
570
+ </div>
571
+ </FocusScope>
572
+ </PopoverContext.Provider>
573
+ );
574
+
575
+ const underlay = () => (
576
+ <div
577
+ data-testid="underlay"
578
+ {...(popoverAria.underlayProps as unknown as JSX.HTMLAttributes<HTMLDivElement>)}
579
+ style={{ position: "fixed", inset: 0 }}
580
+ />
581
+ );
411
582
 
412
583
  return (
413
- <Show when={isOpen() || local.isExiting}>
584
+ <Show when={isHydrated() && (isOpen() || local.isExiting)}>
414
585
  <Portal mount={portalContainer()}>
415
- <PopoverContext.Provider value={{ placement: popoverAria.placement, arrowProps: () => popoverAria.arrowProps }}>
416
- <FocusScope contain={shouldBeDialog()} restoreFocus autoFocus>
417
- <div
418
- {...domProps()}
419
- {...cleanPopoverProps()}
420
- ref={popoverRef}
421
- role={shouldBeDialog() ? 'dialog' : undefined}
422
- tabIndex={shouldBeDialog() ? -1 : undefined}
423
- class={renderProps.class()}
424
- style={mergedStyle()}
425
- data-trigger={local.trigger ?? triggerContext?.trigger}
426
- data-placement={popoverAria.placement()}
427
- data-entering={dataAttr(local.isEntering)}
428
- data-exiting={dataAttr(local.isExiting)}
429
- >
430
- {renderProps.renderChildren()}
586
+ <Show when={!local.isNonModal && !isSubPopover() && isOpen()}>{underlay()}</Show>
587
+ <Show
588
+ when={isSubPopover()}
589
+ fallback={
590
+ <div ref={setGroupRef} style={{ display: "contents" }}>
591
+ <PopoverGroupContext.Provider value={() => groupRef()}>
592
+ {overlay()}
593
+ </PopoverGroupContext.Provider>
431
594
  </div>
432
- </FocusScope>
433
- </PopoverContext.Provider>
595
+ }
596
+ >
597
+ {overlay()}
598
+ </Show>
434
599
  </Portal>
435
600
  </Show>
436
- )
601
+ );
437
602
  }
438
603
 
439
- // ============================================
440
- // OVERLAY ARROW COMPONENT
441
- // ============================================
442
-
443
604
  export interface OverlayArrowProps {
444
605
  /** The children - should be an SVG or element for the arrow. */
445
- children?: JSX.Element | ((placement: PlacementAxis | null) => JSX.Element)
606
+ children?: JSX.Element;
607
+ /** Render function used when Solid children accessors would be ambiguous. */
608
+ render?: () => JSX.Element;
446
609
  /** The CSS className. */
447
- class?: string
610
+ class?: string;
448
611
  /** The inline style. */
449
- style?: JSX.CSSProperties
612
+ style?: JSX.CSSProperties;
450
613
  }
451
614
 
452
615
  /**
453
616
  * An arrow element that points towards the trigger.
454
617
  */
455
618
  export function OverlayArrow(props: OverlayArrowProps): JSX.Element {
456
- const popoverContext = useContext(PopoverContext)
457
- const placement = () => popoverContext?.placement() ?? null
458
-
459
- const cleanArrowProps = () => {
460
- const contextArrowProps = popoverContext?.arrowProps() as Record<string, unknown> | undefined
461
- if (!contextArrowProps) return {}
462
- const { style: _style, ref: _ref, ...rest } = contextArrowProps
463
- return rest
464
- }
619
+ const popoverContext = useContext(PopoverContext);
620
+ const placement = () => popoverContext?.placement() ?? null;
465
621
 
466
622
  const mergedStyle = () => {
467
- const contextStyle = (popoverContext?.arrowProps() as Record<string, unknown> | undefined)?.style as JSX.CSSProperties | undefined
468
- return {
469
- ...(contextStyle ?? {}),
470
- ...(props.style ?? {}),
623
+ const contextStyle = (popoverContext?.arrowProps() as Record<string, unknown> | undefined)
624
+ ?.style as (JSX.CSSProperties & Record<string, unknown>) | undefined;
625
+ const style: JSX.CSSProperties = {};
626
+ if (typeof contextStyle?.left === "string") {
627
+ style.left = contextStyle.left;
471
628
  }
472
- }
473
-
474
- const resolveChildren = () => {
475
- const children = props.children
476
- if (typeof children === 'function') {
477
- return children(placement())
629
+ if (typeof contextStyle?.top === "string") {
630
+ style.top = contextStyle.top;
478
631
  }
479
- return children
480
- }
632
+
633
+ const localStyle =
634
+ props.style &&
635
+ !(typeof CSSStyleDeclaration !== "undefined" && props.style instanceof CSSStyleDeclaration)
636
+ ? props.style
637
+ : undefined;
638
+
639
+ return {
640
+ ...style,
641
+ ...localStyle,
642
+ };
643
+ };
481
644
 
482
645
  return (
483
646
  <div
484
- {...cleanArrowProps()}
485
647
  class={props.class}
486
648
  style={mergedStyle()}
487
649
  data-placement={placement()}
488
650
  aria-hidden="true"
489
651
  role="presentation"
490
652
  >
491
- {resolveChildren()}
653
+ {props.render ? props.render() : props.children}
492
654
  </div>
493
- )
655
+ );
494
656
  }
495
657
 
496
- export default Popover
658
+ export default Popover;