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