@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/Tooltip.tsx CHANGED
@@ -15,19 +15,19 @@ import {
15
15
  createEffect,
16
16
  onCleanup,
17
17
  Show,
18
- } from 'solid-js';
19
- import { isServer } from 'solid-js/web';
18
+ } from "solid-js";
19
+ import { isServer } from "solid-js/web";
20
20
  import {
21
21
  createTooltipTriggerState,
22
22
  type TooltipTriggerState,
23
23
  type TooltipTriggerProps as StateProps,
24
- } from '@proyecto-viviana/solid-stately';
24
+ } from "@proyecto-viviana/solid-stately";
25
25
  import {
26
26
  createTooltip,
27
27
  createTooltipTrigger,
28
28
  type TooltipTriggerProps as AriaProps,
29
29
  OverlayContainer,
30
- } from '@proyecto-viviana/solidaria';
30
+ } from "@proyecto-viviana/solidaria";
31
31
  import {
32
32
  type RenderChildren,
33
33
  type ClassNameOrFunction,
@@ -35,11 +35,7 @@ import {
35
35
  type SlotProps,
36
36
  useRenderProps,
37
37
  filterDOMProps,
38
- } from './utils';
39
-
40
- // ============================================
41
- // TYPES
42
- // ============================================
38
+ } from "./utils";
43
39
 
44
40
  export interface TooltipRenderProps {
45
41
  /** Whether the tooltip is currently entering (for animations). */
@@ -47,15 +43,36 @@ export interface TooltipRenderProps {
47
43
  /** Whether the tooltip is currently exiting (for animations). */
48
44
  isExiting: boolean;
49
45
  /** The placement of the tooltip relative to the trigger. */
50
- placement: 'top' | 'bottom' | 'left' | 'right' | null;
46
+ placement: TooltipResolvedPlacement | null;
51
47
  }
52
48
 
49
+ export type TooltipPlacement = "top" | "bottom" | "left" | "right" | "start" | "end";
50
+ export type TooltipResolvedPlacement = "top" | "bottom" | "left" | "right";
51
+
53
52
  export interface TooltipTriggerComponentProps extends StateProps, AriaProps {
54
53
  /** The children of the tooltip trigger (trigger element and tooltip). */
55
54
  children: JSX.Element;
55
+ /** The placement of the tooltip relative to the trigger. */
56
+ placement?: TooltipPlacement;
57
+ /** The placement padding between the tooltip and viewport edge. */
58
+ containerPadding?: number;
59
+ /** The additional offset along the cross axis. */
60
+ crossOffset?: number;
61
+ /** Whether the tooltip should flip when there is insufficient room. */
62
+ shouldFlip?: boolean;
56
63
  }
57
64
 
58
65
  export interface TooltipProps extends SlotProps {
66
+ /** The element id. */
67
+ id?: string;
68
+ /** Custom aria-label for the tooltip. */
69
+ "aria-label"?: string;
70
+ /** ID of an element that labels the tooltip. */
71
+ "aria-labelledby"?: string;
72
+ /** ID of an element that describes the tooltip. */
73
+ "aria-describedby"?: string;
74
+ /** ID of an element that provides details for the tooltip. */
75
+ "aria-details"?: string;
59
76
  /** The children of the tooltip. A function may be provided to receive render props. */
60
77
  children?: RenderChildren<TooltipRenderProps>;
61
78
  /** The CSS className for the element. */
@@ -67,29 +84,43 @@ export interface TooltipProps extends SlotProps {
67
84
  /** Whether the tooltip is open by default (uncontrolled). */
68
85
  defaultOpen?: boolean;
69
86
  /** The placement of the tooltip relative to the trigger. */
70
- placement?: 'top' | 'bottom' | 'left' | 'right';
71
- /** Whether to render the tooltip in a portal. */
87
+ placement?: TooltipPlacement;
88
+ /** The placement padding between the tooltip and viewport edge. */
89
+ containerPadding?: number;
90
+ /** The additional offset along the cross axis. */
91
+ crossOffset?: number;
92
+ /** Whether the tooltip should flip when there is insufficient room. */
72
93
  shouldFlip?: boolean;
94
+ /** The offset between the tooltip and trigger. */
95
+ offset?: number;
96
+ /** The arrow size used to keep the arrow overlapping the trigger. */
97
+ arrowSize?: number;
98
+ /** The padding between the arrow and tooltip edge. */
99
+ arrowBoundaryOffset?: number;
100
+ /** Whether the tooltip should be disabled. */
101
+ isDisabled?: boolean;
102
+ /** The element language. */
103
+ lang?: string;
104
+ /** The text direction. */
105
+ dir?: "ltr" | "rtl";
73
106
  }
74
107
 
75
- // ============================================
76
- // CONTEXT
77
- // ============================================
78
-
79
108
  interface TooltipTriggerContextValue {
80
109
  state: TooltipTriggerState;
81
- tooltipProps: { id: string };
110
+ tooltipProps: { readonly id: string };
111
+ setTooltipId: (id: string | undefined) => void;
82
112
  triggerRef: () => HTMLElement | null | undefined;
113
+ placement: () => TooltipPlacement | undefined;
114
+ containerPadding: () => number | undefined;
115
+ crossOffset: () => number | undefined;
116
+ shouldFlip: () => boolean | undefined;
117
+ isDisabled: () => boolean | undefined;
83
118
  }
84
119
 
85
120
  const TooltipTriggerContext = createContext<TooltipTriggerContextValue | null>(null);
86
121
  export const TooltipContext = TooltipTriggerContext;
87
122
  export const TooltipTriggerStateContext = createContext<TooltipTriggerState | null>(null);
88
123
 
89
- // ============================================
90
- // COMPONENTS
91
- // ============================================
92
-
93
124
  /**
94
125
  * TooltipTrigger wraps around a trigger element and a Tooltip.
95
126
  * It handles opening and closing the Tooltip when the user hovers
@@ -105,42 +136,68 @@ export const TooltipTriggerStateContext = createContext<TooltipTriggerState | nu
105
136
  */
106
137
  export const TooltipTrigger: ParentComponent<TooltipTriggerComponentProps> = (props) => {
107
138
  let triggerRef: HTMLElement | null = null;
139
+ const [tooltipId, setTooltipId] = createSignal<string | undefined>();
108
140
 
109
141
  const state = createTooltipTriggerState({
110
- get delay() { return props.delay; },
111
- get closeDelay() { return props.closeDelay; },
112
- get isOpen() { return props.isOpen; },
113
- get defaultOpen() { return props.defaultOpen; },
114
- get onOpenChange() { return props.onOpenChange; },
142
+ get delay() {
143
+ return props.delay;
144
+ },
145
+ get closeDelay() {
146
+ return props.closeDelay;
147
+ },
148
+ get isOpen() {
149
+ return props.isOpen;
150
+ },
151
+ get defaultOpen() {
152
+ return props.defaultOpen;
153
+ },
154
+ get onOpenChange() {
155
+ return props.onOpenChange;
156
+ },
115
157
  });
116
158
 
117
159
  const { triggerProps, tooltipProps } = createTooltipTrigger(
118
160
  {
119
- get isDisabled() { return props.isDisabled; },
120
- get trigger() { return props.trigger; },
121
- get shouldCloseOnPress() { return props.shouldCloseOnPress; },
161
+ get isDisabled() {
162
+ return props.isDisabled;
163
+ },
164
+ get trigger() {
165
+ return props.trigger;
166
+ },
167
+ get shouldCloseOnPress() {
168
+ return props.shouldCloseOnPress;
169
+ },
170
+ get tooltipId() {
171
+ return tooltipId();
172
+ },
122
173
  },
123
174
  state,
124
- () => triggerRef
175
+ () => triggerRef,
125
176
  );
126
177
 
127
178
  const context: TooltipTriggerContextValue = {
128
179
  state,
129
180
  tooltipProps,
181
+ setTooltipId,
130
182
  triggerRef: () => triggerRef,
183
+ placement: () => props.placement,
184
+ containerPadding: () => props.containerPadding,
185
+ crossOffset: () => props.crossOffset,
186
+ shouldFlip: () => props.shouldFlip,
187
+ isDisabled: () => props.isDisabled,
131
188
  };
132
189
 
133
- // Clone children and inject trigger props into the first child
134
190
  const processChildren = () => {
135
191
  const children = props.children;
136
192
  if (Array.isArray(children)) {
137
- // First child is the trigger, rest are tooltip(s)
138
193
  const [trigger, ...rest] = children;
139
194
  return (
140
195
  <>
141
196
  <TriggerWrapper
142
197
  triggerProps={triggerProps}
143
- ref={(el) => { triggerRef = el; }}
198
+ ref={(el) => {
199
+ triggerRef = el;
200
+ }}
144
201
  >
145
202
  {trigger}
146
203
  </TriggerWrapper>
@@ -167,13 +224,72 @@ const TriggerWrapper: ParentComponent<{
167
224
  triggerProps: JSX.HTMLAttributes<HTMLElement>;
168
225
  ref: (el: HTMLElement) => void;
169
226
  }> = (props) => {
170
- // Get the child element and clone it with trigger props
171
227
  const child = () => props.children as JSX.Element;
228
+ const [triggerElement, setTriggerElement] = createSignal<HTMLElement | null>(null);
229
+
230
+ createEffect(() => {
231
+ const element = triggerElement();
232
+ if (!element) {
233
+ return;
234
+ }
235
+
236
+ const triggerProps = props.triggerProps as Record<string, unknown>;
237
+ const describedBy = triggerProps["aria-describedby"] as string | undefined;
238
+ if (describedBy) {
239
+ element.setAttribute("aria-describedby", describedBy);
240
+ } else {
241
+ element.removeAttribute("aria-describedby");
242
+ }
243
+
244
+ const listeners: Array<[string, EventListener]> = [];
245
+ const eventProps = [
246
+ ["onFocus", "focus"],
247
+ ["onBlur", "blur"],
248
+ ["onPointerEnter", "pointerenter"],
249
+ ["onPointerLeave", "pointerleave"],
250
+ ["onPointerOver", "pointerover"],
251
+ ["onPointerOut", "pointerout"],
252
+ ["onMouseEnter", "mouseenter"],
253
+ ["onMouseLeave", "mouseleave"],
254
+ ["onTouchStart", "touchstart"],
255
+ ["onPointerDown", "pointerdown"],
256
+ ["onKeyDown", "keydown"],
257
+ ] as const;
258
+
259
+ for (const [propName, eventName] of eventProps) {
260
+ const handler = triggerProps[propName];
261
+ if (typeof handler === "function") {
262
+ const listener = handler as EventListener;
263
+ element.addEventListener(eventName, listener);
264
+ listeners.push([eventName, listener]);
265
+ }
266
+ }
267
+
268
+ onCleanup(() => {
269
+ for (const [eventName, listener] of listeners) {
270
+ element.removeEventListener(eventName, listener);
271
+ }
272
+ if (describedBy && element.getAttribute("aria-describedby") === describedBy) {
273
+ element.removeAttribute("aria-describedby");
274
+ }
275
+ });
276
+ });
172
277
 
173
278
  // We wrap in a span with display:contents to not affect layout.
174
279
  // However, display:contents makes getBoundingClientRect return zeros,
175
280
  // so we pass a ref callback that finds the first actual element child.
176
281
  const handleRef = (span: HTMLSpanElement) => {
282
+ const findElementChild = (el: Element): HTMLElement | null => {
283
+ for (const child of el.children) {
284
+ if (child instanceof HTMLElement) {
285
+ return child;
286
+ }
287
+ const found = findElementChild(child);
288
+ if (found) return found;
289
+ }
290
+ return null;
291
+ };
292
+
177
293
  // Find the first element child that has dimensions (not display:contents)
178
294
  const findVisibleChild = (el: Element): HTMLElement | null => {
179
295
  if (el instanceof HTMLElement) {
@@ -181,7 +297,6 @@ const TriggerWrapper: ParentComponent<{
181
297
  if (rect.width > 0 && rect.height > 0) {
182
298
  return el;
183
299
  }
184
- // Check children
185
300
  for (const child of el.children) {
186
301
  const found = findVisibleChild(child);
187
302
  if (found) return found;
@@ -193,31 +308,28 @@ const TriggerWrapper: ParentComponent<{
193
308
  // Use requestAnimationFrame to ensure children are rendered and have dimensions
194
309
  // This is necessary because SolidJS may not have computed child layout yet
195
310
  const resolveRef = () => {
311
+ const elementChild = findElementChild(span);
196
312
  const visibleChild = findVisibleChild(span);
197
- if (visibleChild) {
198
- props.ref(visibleChild);
199
- } else {
200
- // Fallback to span itself
201
- props.ref(span);
202
- }
313
+ setTriggerElement(elementChild ?? visibleChild ?? span);
314
+ props.ref(visibleChild ?? elementChild ?? span);
203
315
  };
204
316
 
205
- // Try immediately first (in case layout is already computed)
317
+ const elementChild = findElementChild(span);
318
+ if (elementChild) {
319
+ setTriggerElement(elementChild);
320
+ }
321
+
206
322
  const immediateChild = findVisibleChild(span);
207
323
  if (immediateChild) {
324
+ setTriggerElement(elementChild ?? immediateChild);
208
325
  props.ref(immediateChild);
209
326
  } else {
210
- // Defer to next frame when layout should be computed
211
327
  requestAnimationFrame(resolveRef);
212
328
  }
213
329
  };
214
330
 
215
331
  return (
216
- <span
217
- {...props.triggerProps}
218
- ref={handleRef}
219
- style={{ display: 'contents' }}
220
- >
332
+ <span ref={handleRef} style={{ display: "contents" }}>
221
333
  {child()}
222
334
  </span>
223
335
  );
@@ -237,30 +349,47 @@ const TriggerWrapper: ParentComponent<{
237
349
  export function Tooltip(props: TooltipProps): JSX.Element {
238
350
  const context = useContext(TooltipTriggerContext);
239
351
 
240
- // Support standalone usage
352
+ createEffect(() => {
353
+ context?.setTooltipId(props.id);
354
+ onCleanup(() => {
355
+ context?.setTooltipId(undefined);
356
+ });
357
+ });
358
+
241
359
  const localState = createTooltipTriggerState({
242
- get isOpen() { return props.isOpen; },
243
- get defaultOpen() { return props.defaultOpen; },
360
+ get isOpen() {
361
+ return props.isOpen;
362
+ },
363
+ get defaultOpen() {
364
+ return props.defaultOpen;
365
+ },
244
366
  });
245
367
 
246
368
  const state = () => context?.state ?? localState;
247
- const placement = () => props.placement ?? 'top';
369
+ const placement = () => props.placement ?? context?.placement() ?? "top";
370
+ const containerPadding = () => props.containerPadding ?? context?.containerPadding() ?? 12;
371
+ const crossOffset = () => props.crossOffset ?? context?.crossOffset() ?? 0;
372
+ const shouldFlip = () => props.shouldFlip ?? context?.shouldFlip() ?? true;
373
+ const offset = () => props.offset ?? 9;
374
+ const arrowSize = () => props.arrowSize ?? 0;
375
+ const arrowBoundaryOffset = () => props.arrowBoundaryOffset ?? 0;
376
+ const isDisabled = () => props.isDisabled ?? context?.isDisabled() ?? false;
248
377
 
249
- const isOpen = () => state().isOpen();
378
+ const isOpen = () => !isDisabled() && state().isOpen();
250
379
 
251
380
  // Exit animation state machine: 'closed' | 'open' | 'exiting'
252
381
  // Keeps the tooltip mounted during exit animation so CSS transitions can play.
253
- const [exitState, setExitState] = createSignal<'closed' | 'open' | 'exiting'>(
254
- isOpen() ? 'open' : 'closed'
382
+ const [exitState, setExitState] = createSignal<"closed" | "open" | "exiting">(
383
+ isOpen() ? "open" : "closed",
255
384
  );
256
385
 
257
386
  createEffect(() => {
258
387
  const open = isOpen();
259
388
  const current = exitState();
260
- if (current === 'open' && !open) {
261
- setExitState('exiting');
262
- } else if ((current === 'closed' || current === 'exiting') && open) {
263
- setExitState('open');
389
+ if (current === "open" && !open) {
390
+ setExitState("exiting");
391
+ } else if ((current === "closed" || current === "exiting") && open) {
392
+ setExitState("open");
264
393
  }
265
394
  });
266
395
 
@@ -269,26 +398,32 @@ export function Tooltip(props: TooltipProps): JSX.Element {
269
398
 
270
399
  // When exiting, wait for CSS animations to finish, then set state to closed
271
400
  createEffect(() => {
272
- if (exitState() !== 'exiting') return;
401
+ if (exitState() !== "exiting") return;
273
402
  const el = tooltipEl();
274
- if (!el || !('getAnimations' in el)) {
275
- setExitState('closed');
403
+ if (!el || !("getAnimations" in el)) {
404
+ setExitState("closed");
276
405
  return;
277
406
  }
278
407
  const animations = el.getAnimations();
279
408
  if (animations.length === 0) {
280
- setExitState('closed');
409
+ setExitState("closed");
281
410
  return;
282
411
  }
283
412
  let canceled = false;
284
413
  Promise.all(animations.map((a) => a.finished))
285
- .then(() => { if (!canceled) setExitState((s) => s === 'exiting' ? 'closed' : s); })
286
- .catch(() => { if (!canceled) setExitState((s) => s === 'exiting' ? 'closed' : s); });
287
- onCleanup(() => { canceled = true; });
414
+ .then(() => {
415
+ if (!canceled) setExitState((s) => (s === "exiting" ? "closed" : s));
416
+ })
417
+ .catch(() => {
418
+ if (!canceled) setExitState((s) => (s === "exiting" ? "closed" : s));
419
+ });
420
+ onCleanup(() => {
421
+ canceled = true;
422
+ });
288
423
  });
289
424
 
290
- const shouldRender = () => isOpen() || exitState() === 'exiting';
291
- const isExiting = () => exitState() === 'exiting';
425
+ const shouldRender = () => isOpen() || exitState() === "exiting";
426
+ const isExiting = () => exitState() === "exiting";
292
427
 
293
428
  return (
294
429
  <Show when={shouldRender()}>
@@ -297,6 +432,12 @@ export function Tooltip(props: TooltipProps): JSX.Element {
297
432
  state={state()}
298
433
  contextTooltipProps={context?.tooltipProps ?? {}}
299
434
  placement={placement()}
435
+ containerPadding={containerPadding()}
436
+ crossOffset={crossOffset()}
437
+ shouldFlip={shouldFlip()}
438
+ offset={offset()}
439
+ arrowSize={arrowSize()}
440
+ arrowBoundaryOffset={arrowBoundaryOffset()}
300
441
  triggerRef={context?.triggerRef ?? (() => null)}
301
442
  isExiting={isExiting()}
302
443
  onTooltipRef={setTooltipEl}
@@ -311,12 +452,18 @@ export function Tooltip(props: TooltipProps): JSX.Element {
311
452
  function TooltipContent(
312
453
  props: TooltipProps & {
313
454
  state: TooltipTriggerState;
314
- contextTooltipProps: { id?: string };
315
- placement: 'top' | 'bottom' | 'left' | 'right';
455
+ contextTooltipProps: { readonly id?: string };
456
+ placement: TooltipPlacement;
457
+ containerPadding: number;
458
+ crossOffset: number;
459
+ shouldFlip: boolean;
460
+ offset: number;
461
+ arrowSize: number;
462
+ arrowBoundaryOffset: number;
316
463
  triggerRef: () => HTMLElement | null | undefined;
317
464
  isExiting: boolean;
318
465
  onTooltipRef: (el: HTMLDivElement | null) => void;
319
- }
466
+ },
320
467
  ): JSX.Element {
321
468
  if (isServer) {
322
469
  return null as unknown as JSX.Element;
@@ -325,15 +472,17 @@ function TooltipContent(
325
472
  let tooltipRef!: HTMLDivElement;
326
473
  const { tooltipProps: ariaTooltipProps } = createTooltip({}, props.state);
327
474
 
328
- // Signal to track position styles
329
475
  // Start visible at 0,0 and update position asynchronously
330
476
  // This ensures the tooltip is immediately accessible (for screen readers and tests)
331
477
  // while the visual position gets calculated
332
478
  const [positionStyles, setPositionStyles] = createSignal({
333
- top: '0px',
334
- left: '0px',
335
- visibility: 'visible' as 'hidden' | 'visible',
479
+ top: "0px",
480
+ left: "0px",
481
+ visibility: "visible" as "hidden" | "visible",
336
482
  });
483
+ const [renderedPlacement, setRenderedPlacement] = createSignal<TooltipResolvedPlacement>(
484
+ resolvePlacement(props.placement),
485
+ );
337
486
 
338
487
  // Enter animation state: starts true on mount, clears after first animation frame.
339
488
  // Uses getAnimations() to detect CSS animations/transitions - if none exist (JSDOM,
@@ -342,7 +491,7 @@ function TooltipContent(
342
491
 
343
492
  createEffect(() => {
344
493
  if (!isEntering()) return;
345
- if (!tooltipRef || !('getAnimations' in tooltipRef)) {
494
+ if (!tooltipRef || !("getAnimations" in tooltipRef)) {
346
495
  setIsEntering(false);
347
496
  return;
348
497
  }
@@ -359,15 +508,21 @@ function TooltipContent(
359
508
  }
360
509
  let canceled = false;
361
510
  Promise.all(animations.map((a) => a.finished))
362
- .then(() => { if (!canceled) setIsEntering(false); })
363
- .catch(() => { if (!canceled) setIsEntering(false); });
364
- onCleanup(() => { canceled = true; });
511
+ .then(() => {
512
+ if (!canceled) setIsEntering(false);
513
+ })
514
+ .catch(() => {
515
+ if (!canceled) setIsEntering(false);
516
+ });
517
+ onCleanup(() => {
518
+ canceled = true;
519
+ });
365
520
  });
366
521
 
367
522
  const values = createMemo<TooltipRenderProps>(() => ({
368
523
  isEntering: isEntering(),
369
524
  isExiting: props.isExiting,
370
- placement: props.placement,
525
+ placement: renderedPlacement(),
371
526
  }));
372
527
 
373
528
  const renderProps = useRenderProps(
@@ -375,13 +530,13 @@ function TooltipContent(
375
530
  class: props.class,
376
531
  style: props.style,
377
532
  children: props.children,
378
- defaultClassName: 'solidaria-Tooltip',
533
+ defaultClassName: "solidaria-Tooltip",
379
534
  },
380
- values
535
+ values,
381
536
  );
382
537
 
383
- // Calculate position based on trigger element
384
- // Using position: fixed so we use viewport coordinates directly from getBoundingClientRect
538
+ // Position the overlay in document coordinates, matching React Aria's
539
+ // absolute overlay positioning when the portal container is the document.
385
540
  // Returns true if position was successfully updated, false if we need to retry
386
541
  const updatePosition = (): boolean => {
387
542
  const triggerEl = props.triggerRef();
@@ -398,45 +553,81 @@ function TooltipContent(
398
553
  // when the element might be positioned off-screen initially
399
554
  const tooltipWidth = tooltipRef.offsetWidth;
400
555
  const tooltipHeight = tooltipRef.offsetHeight;
401
- const offset = 8; // Gap between trigger and tooltip
556
+ const viewportWidth = window.innerWidth;
557
+ const viewportHeight = window.innerHeight;
558
+ const containerPadding = props.containerPadding;
559
+ const crossOffset = props.crossOffset;
560
+ const placement = maybeFlipPlacement(
561
+ resolvePlacement(props.placement),
562
+ triggerRect,
563
+ tooltipWidth,
564
+ tooltipHeight,
565
+ viewportWidth,
566
+ viewportHeight,
567
+ containerPadding,
568
+ props.offset,
569
+ props.shouldFlip,
570
+ );
402
571
 
403
572
  let top = 0;
404
573
  let left = 0;
405
574
 
406
- // Using viewport coordinates for position: fixed
407
- switch (props.placement) {
408
- case 'top':
409
- top = triggerRect.top - tooltipHeight - offset;
410
- left = triggerRect.left + (triggerRect.width - tooltipWidth) / 2;
575
+ switch (placement) {
576
+ case "top":
577
+ top = triggerRect.top - tooltipHeight - props.offset;
578
+ left = triggerRect.left + (triggerRect.width - tooltipWidth) / 2 + crossOffset;
411
579
  break;
412
- case 'bottom':
413
- top = triggerRect.bottom + offset;
414
- left = triggerRect.left + (triggerRect.width - tooltipWidth) / 2;
580
+ case "bottom":
581
+ top = triggerRect.bottom + props.offset;
582
+ left = triggerRect.left + (triggerRect.width - tooltipWidth) / 2 + crossOffset;
415
583
  break;
416
- case 'left':
417
- top = triggerRect.top + (triggerRect.height - tooltipHeight) / 2;
418
- left = triggerRect.left - tooltipWidth - offset;
584
+ case "left":
585
+ top = triggerRect.top + (triggerRect.height - tooltipHeight) / 2 + crossOffset;
586
+ left = triggerRect.left - tooltipWidth - props.offset;
419
587
  break;
420
- case 'right':
421
- top = triggerRect.top + (triggerRect.height - tooltipHeight) / 2;
422
- left = triggerRect.right + offset;
588
+ case "right":
589
+ top = triggerRect.top + (triggerRect.height - tooltipHeight) / 2 + crossOffset;
590
+ left = triggerRect.right + props.offset;
423
591
  break;
424
592
  }
425
593
 
594
+ if (placement === "top" || placement === "bottom") {
595
+ left = clamp(
596
+ left,
597
+ triggerRect.left - tooltipWidth + props.arrowSize + props.arrowBoundaryOffset,
598
+ triggerRect.left + triggerRect.width - props.arrowSize - props.arrowBoundaryOffset,
599
+ );
600
+ left = clamp(left, containerPadding, viewportWidth - tooltipWidth - containerPadding);
601
+ } else {
602
+ top = clamp(
603
+ top,
604
+ triggerRect.top - tooltipHeight + props.arrowSize + props.arrowBoundaryOffset,
605
+ triggerRect.top + triggerRect.height - props.arrowSize - props.arrowBoundaryOffset,
606
+ );
607
+ top = clamp(top, containerPadding, viewportHeight - tooltipHeight - containerPadding);
608
+ }
609
+ setRenderedPlacement(placement);
426
610
  setPositionStyles({
427
- top: `${top}px`,
428
- left: `${left}px`,
429
- visibility: 'visible',
611
+ top: `${Math.floor(top + window.scrollY)}px`,
612
+ left: `${Math.floor(left + window.scrollX)}px`,
613
+ visibility: "visible",
430
614
  });
431
615
 
432
616
  return true;
433
617
  };
434
618
 
435
- // Set up positioning effect - runs when trigger ref is available.
436
- // Tracks pending rAF/setTimeout IDs so they can be canceled on cleanup.
619
+ // Set up positioning and scroll-close effects. Positioning retries while the
620
+ // trigger ref resolves, and pending rAF/setTimeout IDs are canceled on cleanup.
437
621
  createEffect(() => {
438
- const trigger = props.triggerRef();
439
- if (!trigger) return;
622
+ // Track positioning inputs synchronously so updates from controlled route
623
+ // props reschedule measurement even though layout reads happen in rAF.
624
+ props.placement;
625
+ props.containerPadding;
626
+ props.crossOffset;
627
+ props.shouldFlip;
628
+ props.offset;
629
+ props.arrowSize;
630
+ props.arrowBoundaryOffset;
440
631
 
441
632
  let retryCount = 0;
442
633
  const maxRetries = 5;
@@ -455,30 +646,51 @@ function TooltipContent(
455
646
 
456
647
  pendingRaf = requestAnimationFrame(tryUpdatePosition);
457
648
 
458
- // Update on scroll/resize
459
- window.addEventListener('scroll', updatePosition, true);
460
- window.addEventListener('resize', updatePosition);
649
+ const closeOnScroll = (event: Event) => {
650
+ const trigger = props.triggerRef();
651
+ const target = event.target;
652
+ if (!trigger || (target instanceof Node && !target.contains(trigger))) {
653
+ return;
654
+ }
655
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
656
+ return;
657
+ }
658
+
659
+ props.state.close(true);
660
+ };
661
+
662
+ window.addEventListener("scroll", closeOnScroll, true);
663
+ window.addEventListener("resize", updatePosition);
461
664
 
462
665
  onCleanup(() => {
463
666
  if (pendingRaf) cancelAnimationFrame(pendingRaf);
464
667
  if (pendingTimeout) clearTimeout(pendingTimeout);
465
- window.removeEventListener('scroll', updatePosition, true);
466
- window.removeEventListener('resize', updatePosition);
668
+ window.removeEventListener("scroll", closeOnScroll, true);
669
+ window.removeEventListener("resize", updatePosition);
467
670
  });
468
671
  });
469
672
 
470
- // Filter to only valid DOM props (aria-*, data-*, events)
471
- const domProps = filterDOMProps(props);
673
+ const domProps = filterDOMProps(props, { global: true });
674
+ const tooltipId = () => props.contextTooltipProps.id ?? (domProps as { id?: string }).id;
472
675
 
473
676
  // Extract ref from ariaTooltipProps to avoid type conflicts (SolidJS ref types are element-specific)
474
677
  const { ref: _ariaRef, ...cleanAriaProps } = ariaTooltipProps as Record<string, unknown>;
475
678
 
476
679
  const setRef = (el: HTMLDivElement) => {
477
680
  tooltipRef = el;
681
+ if (!props.dir) {
682
+ el.dir = getDocumentDirection();
683
+ }
684
+ if (!props.lang) {
685
+ const documentLang = document.documentElement.lang;
686
+ const navigatorLang = typeof navigator !== "undefined" ? navigator.language : "";
687
+ el.lang = documentLang.includes("-")
688
+ ? documentLang
689
+ : navigatorLang || documentLang || "en-US";
690
+ }
478
691
  props.onTooltipRef(el);
479
692
  };
480
693
 
481
- // Clean up ref on unmount
482
694
  onCleanup(() => {
483
695
  props.onTooltipRef(null);
484
696
  });
@@ -487,18 +699,18 @@ function TooltipContent(
487
699
  <OverlayContainer>
488
700
  <div
489
701
  {...domProps}
490
- {...props.contextTooltipProps}
491
702
  {...cleanAriaProps}
703
+ id={tooltipId()}
492
704
  role="tooltip"
493
705
  ref={setRef}
494
706
  class={renderProps.class()}
495
707
  style={{
496
- position: 'fixed',
497
- 'z-index': 100000,
708
+ position: "absolute",
709
+ "z-index": 100000,
498
710
  ...positionStyles(),
499
711
  ...renderProps.style(),
500
712
  }}
501
- data-placement={props.placement}
713
+ data-placement={renderedPlacement()}
502
714
  data-entering={isEntering() || undefined}
503
715
  data-exiting={props.isExiting || undefined}
504
716
  >
@@ -508,5 +720,61 @@ function TooltipContent(
508
720
  );
509
721
  }
510
722
 
511
- // Re-export types
723
+ function resolvePlacement(placement: TooltipPlacement): TooltipResolvedPlacement {
724
+ if (placement === "start") {
725
+ return getDocumentDirection() === "rtl" ? "right" : "left";
726
+ }
727
+ if (placement === "end") {
728
+ return getDocumentDirection() === "rtl" ? "left" : "right";
729
+ }
730
+ return placement;
731
+ }
732
+
733
+ function getDocumentDirection(): "ltr" | "rtl" {
734
+ if (typeof document === "undefined") {
735
+ return "ltr";
736
+ }
737
+
738
+ return document.documentElement.dir === "rtl" ? "rtl" : "ltr";
739
+ }
740
+
741
+ function maybeFlipPlacement(
742
+ placement: TooltipResolvedPlacement,
743
+ triggerRect: DOMRect,
744
+ tooltipWidth: number,
745
+ tooltipHeight: number,
746
+ viewportWidth: number,
747
+ viewportHeight: number,
748
+ containerPadding: number,
749
+ offset: number,
750
+ shouldFlip: boolean,
751
+ ): TooltipResolvedPlacement {
752
+ if (!shouldFlip) {
753
+ return placement;
754
+ }
755
+
756
+ switch (placement) {
757
+ case "top":
758
+ return triggerRect.top - tooltipHeight - offset < containerPadding ? "bottom" : placement;
759
+ case "bottom":
760
+ return triggerRect.bottom + tooltipHeight + offset > viewportHeight - containerPadding
761
+ ? "top"
762
+ : placement;
763
+ case "left":
764
+ return triggerRect.left - tooltipWidth - offset < containerPadding ? "right" : placement;
765
+ case "right":
766
+ return triggerRect.right + tooltipWidth + offset > viewportWidth - containerPadding
767
+ ? "left"
768
+ : placement;
769
+ }
770
+ }
771
+
772
+ function clamp(value: number, min: number, max: number): number {
773
+ if (max < min) {
774
+ return min;
775
+ }
776
+
777
+ return Math.min(Math.max(value, min), max);
778
+ }
779
+
512
780
  export type { TooltipTriggerState };