@proyecto-viviana/solidaria-components 0.2.5 → 0.2.9

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 (194) hide show
  1. package/LICENSE +21 -0
  2. package/dist/ActionBar.d.ts +71 -0
  3. package/dist/ActionBar.d.ts.map +1 -0
  4. package/dist/ActionGroup.d.ts +74 -0
  5. package/dist/ActionGroup.d.ts.map +1 -0
  6. package/dist/Alert.d.ts +70 -0
  7. package/dist/Alert.d.ts.map +1 -0
  8. package/dist/Breadcrumbs.d.ts +10 -2
  9. package/dist/Breadcrumbs.d.ts.map +1 -1
  10. package/dist/Button.d.ts +4 -0
  11. package/dist/Button.d.ts.map +1 -1
  12. package/dist/Calendar.d.ts +13 -0
  13. package/dist/Calendar.d.ts.map +1 -1
  14. package/dist/Checkbox.d.ts +2 -2
  15. package/dist/Checkbox.d.ts.map +1 -1
  16. package/dist/Collection.d.ts +125 -0
  17. package/dist/Collection.d.ts.map +1 -0
  18. package/dist/Color.d.ts +114 -2
  19. package/dist/Color.d.ts.map +1 -1
  20. package/dist/ColorEditor.d.ts +42 -0
  21. package/dist/ColorEditor.d.ts.map +1 -0
  22. package/dist/ComboBox.d.ts +64 -0
  23. package/dist/ComboBox.d.ts.map +1 -1
  24. package/dist/ContextualHelpTrigger.d.ts +40 -0
  25. package/dist/ContextualHelpTrigger.d.ts.map +1 -0
  26. package/dist/DateField.d.ts +27 -2
  27. package/dist/DateField.d.ts.map +1 -1
  28. package/dist/DatePicker.d.ts +67 -2
  29. package/dist/DatePicker.d.ts.map +1 -1
  30. package/dist/Dialog.d.ts.map +1 -1
  31. package/dist/Disclosure.d.ts +2 -0
  32. package/dist/Disclosure.d.ts.map +1 -1
  33. package/dist/DragAndDrop.d.ts +80 -0
  34. package/dist/DragAndDrop.d.ts.map +1 -0
  35. package/dist/DragPreview.d.ts +14 -0
  36. package/dist/DragPreview.d.ts.map +1 -0
  37. package/dist/DropZone.d.ts +27 -0
  38. package/dist/DropZone.d.ts.map +1 -0
  39. package/dist/FieldError.d.ts +23 -0
  40. package/dist/FieldError.d.ts.map +1 -0
  41. package/dist/FileTrigger.d.ts +26 -0
  42. package/dist/FileTrigger.d.ts.map +1 -0
  43. package/dist/Focusable.d.ts +27 -0
  44. package/dist/Focusable.d.ts.map +1 -0
  45. package/dist/Form.d.ts +27 -0
  46. package/dist/Form.d.ts.map +1 -0
  47. package/dist/GridList.d.ts +40 -1
  48. package/dist/GridList.d.ts.map +1 -1
  49. package/dist/Icon.d.ts +57 -0
  50. package/dist/Icon.d.ts.map +1 -0
  51. package/dist/Keyboard.d.ts +13 -0
  52. package/dist/Keyboard.d.ts.map +1 -0
  53. package/dist/Link.d.ts.map +1 -1
  54. package/dist/ListBox.d.ts +43 -1
  55. package/dist/ListBox.d.ts.map +1 -1
  56. package/dist/ListDropTargetDelegate.d.ts +38 -0
  57. package/dist/ListDropTargetDelegate.d.ts.map +1 -0
  58. package/dist/Menu.d.ts +20 -2
  59. package/dist/Menu.d.ts.map +1 -1
  60. package/dist/Meter.d.ts +2 -2
  61. package/dist/Meter.d.ts.map +1 -1
  62. package/dist/Modal.d.ts +2 -0
  63. package/dist/Modal.d.ts.map +1 -1
  64. package/dist/NumberField.d.ts +2 -0
  65. package/dist/NumberField.d.ts.map +1 -1
  66. package/dist/Popover.d.ts +4 -2
  67. package/dist/Popover.d.ts.map +1 -1
  68. package/dist/Pressable.d.ts +27 -0
  69. package/dist/Pressable.d.ts.map +1 -0
  70. package/dist/ProgressBar.d.ts +2 -2
  71. package/dist/ProgressBar.d.ts.map +1 -1
  72. package/dist/RadioGroup.d.ts.map +1 -1
  73. package/dist/RangeCalendar.d.ts +5 -0
  74. package/dist/RangeCalendar.d.ts.map +1 -1
  75. package/dist/RouterProvider.d.ts +75 -0
  76. package/dist/RouterProvider.d.ts.map +1 -0
  77. package/dist/SearchField.d.ts +2 -3
  78. package/dist/SearchField.d.ts.map +1 -1
  79. package/dist/Select.d.ts +11 -0
  80. package/dist/Select.d.ts.map +1 -1
  81. package/dist/SelectionIndicator.d.ts +30 -0
  82. package/dist/SelectionIndicator.d.ts.map +1 -0
  83. package/dist/SharedElementTransition.d.ts +39 -0
  84. package/dist/SharedElementTransition.d.ts.map +1 -0
  85. package/dist/Slider.d.ts +6 -3
  86. package/dist/Slider.d.ts.map +1 -1
  87. package/dist/Table.d.ts +39 -0
  88. package/dist/Table.d.ts.map +1 -1
  89. package/dist/Tabs.d.ts +4 -3
  90. package/dist/Tabs.d.ts.map +1 -1
  91. package/dist/TagGroup.d.ts +12 -2
  92. package/dist/TagGroup.d.ts.map +1 -1
  93. package/dist/Text.d.ts +10 -0
  94. package/dist/Text.d.ts.map +1 -0
  95. package/dist/TextField.d.ts +4 -0
  96. package/dist/TextField.d.ts.map +1 -1
  97. package/dist/TimeField.d.ts +26 -1
  98. package/dist/TimeField.d.ts.map +1 -1
  99. package/dist/Toast.d.ts.map +1 -1
  100. package/dist/ToggleButton.d.ts +30 -0
  101. package/dist/ToggleButton.d.ts.map +1 -0
  102. package/dist/ToggleButtonGroup.d.ts +33 -0
  103. package/dist/ToggleButtonGroup.d.ts.map +1 -0
  104. package/dist/Toolbar.d.ts.map +1 -1
  105. package/dist/Tooltip.d.ts +9 -0
  106. package/dist/Tooltip.d.ts.map +1 -1
  107. package/dist/Tree.d.ts +44 -2
  108. package/dist/Tree.d.ts.map +1 -1
  109. package/dist/Virtualizer.d.ts +61 -0
  110. package/dist/Virtualizer.d.ts.map +1 -0
  111. package/dist/VirtualizerLayouts.d.ts +82 -0
  112. package/dist/VirtualizerLayouts.d.ts.map +1 -0
  113. package/dist/VisuallyHidden.d.ts +3 -1
  114. package/dist/VisuallyHidden.d.ts.map +1 -1
  115. package/dist/contexts.d.ts +1 -0
  116. package/dist/contexts.d.ts.map +1 -1
  117. package/dist/index.d.ts +57 -25
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +13961 -5946
  120. package/dist/index.js.map +1 -7
  121. package/dist/index.ssr.js +9612 -2401
  122. package/dist/index.ssr.js.map +1 -7
  123. package/dist/useDragAndDrop.d.ts +93 -0
  124. package/dist/useDragAndDrop.d.ts.map +1 -0
  125. package/dist/utils.d.ts +7 -1
  126. package/dist/utils.d.ts.map +1 -1
  127. package/dist/virtualizer/Layout.d.ts +79 -0
  128. package/dist/virtualizer/Layout.d.ts.map +1 -0
  129. package/package.json +8 -6
  130. package/src/ActionBar.tsx +248 -0
  131. package/src/ActionGroup.tsx +285 -0
  132. package/src/Alert.tsx +177 -0
  133. package/src/Autocomplete.tsx +1 -1
  134. package/src/Breadcrumbs.tsx +103 -17
  135. package/src/Button.tsx +65 -21
  136. package/src/Calendar.tsx +179 -53
  137. package/src/Checkbox.tsx +1 -2
  138. package/src/Collection.tsx +341 -0
  139. package/src/Color.tsx +652 -34
  140. package/src/ColorEditor.tsx +231 -0
  141. package/src/ComboBox.tsx +315 -81
  142. package/src/ContextualHelpTrigger.tsx +183 -0
  143. package/src/DateField.tsx +93 -19
  144. package/src/DatePicker.tsx +495 -25
  145. package/src/Dialog.tsx +40 -9
  146. package/src/Disclosure.tsx +33 -27
  147. package/src/DragAndDrop.tsx +334 -0
  148. package/src/DragPreview.tsx +45 -0
  149. package/src/DropZone.tsx +213 -0
  150. package/src/FieldError.tsx +67 -0
  151. package/src/FileTrigger.tsx +83 -0
  152. package/src/Focusable.tsx +106 -0
  153. package/src/Form.tsx +85 -0
  154. package/src/GridList.tsx +379 -41
  155. package/src/Icon.tsx +154 -0
  156. package/src/Keyboard.tsx +26 -0
  157. package/src/Link.tsx +14 -1
  158. package/src/ListBox.tsx +484 -33
  159. package/src/ListDropTargetDelegate.ts +282 -0
  160. package/src/Menu.tsx +388 -35
  161. package/src/Meter.tsx +7 -3
  162. package/src/Modal.tsx +32 -4
  163. package/src/NumberField.tsx +163 -43
  164. package/src/Popover.tsx +136 -180
  165. package/src/Pressable.tsx +108 -0
  166. package/src/ProgressBar.tsx +7 -3
  167. package/src/RadioGroup.tsx +35 -25
  168. package/src/RangeCalendar.tsx +100 -68
  169. package/src/RouterProvider.tsx +240 -0
  170. package/src/SearchField.tsx +142 -34
  171. package/src/Select.tsx +221 -73
  172. package/src/SelectionIndicator.tsx +105 -0
  173. package/src/SharedElementTransition.tsx +258 -0
  174. package/src/Slider.tsx +16 -6
  175. package/src/Table.tsx +417 -57
  176. package/src/Tabs.tsx +68 -35
  177. package/src/TagGroup.tsx +121 -36
  178. package/src/Text.tsx +18 -0
  179. package/src/TextField.tsx +25 -8
  180. package/src/TimeField.tsx +101 -151
  181. package/src/Toast.tsx +108 -14
  182. package/src/ToggleButton.tsx +159 -0
  183. package/src/ToggleButtonGroup.tsx +136 -0
  184. package/src/Toolbar.tsx +14 -8
  185. package/src/Tooltip.tsx +108 -19
  186. package/src/Tree.tsx +1143 -87
  187. package/src/Virtualizer.tsx +702 -0
  188. package/src/VirtualizerLayouts.ts +265 -0
  189. package/src/VisuallyHidden.tsx +15 -21
  190. package/src/contexts.ts +1 -0
  191. package/src/index.ts +1057 -620
  192. package/src/useDragAndDrop.ts +351 -0
  193. package/src/utils.tsx +37 -3
  194. package/src/virtualizer/Layout.ts +200 -0
@@ -0,0 +1,136 @@
1
+ /**
2
+ * ToggleButtonGroup component for solidaria-components.
3
+ *
4
+ * Groups toggle buttons with single/multiple selection state.
5
+ * Parity target: react-aria-components/src/ToggleButtonGroup.tsx
6
+ */
7
+
8
+ import { type JSX, createContext, createMemo, splitProps, useContext } from 'solid-js';
9
+ import {
10
+ createToggleButtonGroup,
11
+ mergeProps,
12
+ } from '@proyecto-viviana/solidaria';
13
+ import {
14
+ createToggleGroupState,
15
+ type Key,
16
+ type ToggleGroupState,
17
+ } from '@proyecto-viviana/solid-stately';
18
+ import {
19
+ type ClassNameOrFunction,
20
+ type StyleOrFunction,
21
+ type RenderChildren,
22
+ type SlotProps,
23
+ useRenderProps,
24
+ filterDOMProps,
25
+ } from './utils';
26
+
27
+ export interface ToggleButtonGroupRenderProps {
28
+ orientation: 'horizontal' | 'vertical';
29
+ isDisabled: boolean;
30
+ state: ToggleGroupState;
31
+ }
32
+
33
+ export interface ToggleButtonGroupProps
34
+ extends Omit<JSX.HTMLAttributes<HTMLDivElement>, 'children' | 'class' | 'style' | 'onSelectionChange'>,
35
+ SlotProps {
36
+ selectionMode?: 'single' | 'multiple';
37
+ disallowEmptySelection?: boolean;
38
+ selectedKeys?: Iterable<Key>;
39
+ defaultSelectedKeys?: Iterable<Key>;
40
+ onSelectionChange?: (keys: Set<Key>) => void;
41
+ orientation?: 'horizontal' | 'vertical';
42
+ isDisabled?: boolean;
43
+ children?: RenderChildren<ToggleButtonGroupRenderProps>;
44
+ class?: ClassNameOrFunction<ToggleButtonGroupRenderProps>;
45
+ style?: StyleOrFunction<ToggleButtonGroupRenderProps>;
46
+ }
47
+
48
+ export const ToggleButtonGroupContext = createContext<ToggleButtonGroupProps | null>(null);
49
+ export const ToggleButtonGroupStateContext = createContext<ToggleGroupState | null>(null);
50
+ export const ToggleGroupStateContext = ToggleButtonGroupStateContext;
51
+ export type ToggleButtonGroupStateContextValue = ToggleGroupState;
52
+
53
+ export function ToggleButtonGroup(props: ToggleButtonGroupProps): JSX.Element {
54
+ const [local, domProps] = splitProps(props, [
55
+ 'selectionMode',
56
+ 'disallowEmptySelection',
57
+ 'selectedKeys',
58
+ 'defaultSelectedKeys',
59
+ 'onSelectionChange',
60
+ 'orientation',
61
+ 'isDisabled',
62
+ 'children',
63
+ 'class',
64
+ 'style',
65
+ 'slot',
66
+ 'aria-label',
67
+ 'aria-labelledby',
68
+ ]);
69
+
70
+ const state = createToggleGroupState(() => ({
71
+ selectionMode: local.selectionMode,
72
+ disallowEmptySelection: local.disallowEmptySelection,
73
+ selectedKeys: local.selectedKeys,
74
+ defaultSelectedKeys: local.defaultSelectedKeys,
75
+ onSelectionChange: local.onSelectionChange,
76
+ isDisabled: !!local.isDisabled,
77
+ }));
78
+
79
+ const { groupProps } = createToggleButtonGroup(
80
+ {
81
+ get orientation() {
82
+ return local.orientation;
83
+ },
84
+ get isDisabled() {
85
+ return !!local.isDisabled;
86
+ },
87
+ get 'aria-label'() {
88
+ return local['aria-label'];
89
+ },
90
+ get 'aria-labelledby'() {
91
+ return local['aria-labelledby'];
92
+ },
93
+ },
94
+ state
95
+ );
96
+
97
+ const renderProps = useRenderProps(
98
+ {
99
+ children: local.children,
100
+ class: local.class,
101
+ style: local.style,
102
+ defaultClassName: 'solidaria-ToggleButtonGroup',
103
+ },
104
+ () => ({
105
+ orientation: local.orientation ?? 'horizontal',
106
+ isDisabled: !!local.isDisabled,
107
+ state,
108
+ })
109
+ );
110
+
111
+ const filteredDomProps = createMemo(() => filterDOMProps(domProps, { global: true }));
112
+ const mergedGroupProps = createMemo(() =>
113
+ mergeProps(filteredDomProps(), groupProps as Record<string, unknown>)
114
+ );
115
+
116
+ return (
117
+ <div
118
+ {...(mergedGroupProps() as JSX.HTMLAttributes<HTMLDivElement>)}
119
+ class={renderProps.class()}
120
+ style={renderProps.style()}
121
+ slot={local.slot}
122
+ data-orientation={local.orientation ?? 'horizontal'}
123
+ data-disabled={local.isDisabled || undefined}
124
+ >
125
+ <ToggleButtonGroupContext.Provider value={props}>
126
+ <ToggleButtonGroupStateContext.Provider value={state}>
127
+ {renderProps.renderChildren()}
128
+ </ToggleButtonGroupStateContext.Provider>
129
+ </ToggleButtonGroupContext.Provider>
130
+ </div>
131
+ );
132
+ }
133
+
134
+ export function useToggleButtonGroupStateContext(): ToggleGroupState | null {
135
+ return useContext(ToggleButtonGroupStateContext);
136
+ }
package/src/Toolbar.tsx CHANGED
@@ -100,15 +100,21 @@ export function Toolbar(props: ToolbarProps): JSX.Element {
100
100
  return {}
101
101
  }
102
102
 
103
- // Merge slot props with explicit props
104
- const mergedAriaProps = createMemo(() => ({
105
- orientation: ariaProps.orientation,
106
- 'aria-label': ariaProps['aria-label'] ?? slotProps()['aria-label'] as string | undefined,
107
- 'aria-labelledby': ariaProps['aria-labelledby'],
108
- }))
109
-
110
103
  // Create toolbar aria props
111
- const { toolbarProps, orientation } = createToolbar(mergedAriaProps())
104
+ const { toolbarProps, orientation } = createToolbar({
105
+ get orientation() {
106
+ return ariaProps.orientation
107
+ },
108
+ get 'aria-label'() {
109
+ return (
110
+ (ariaProps['aria-label'] as string | undefined) ??
111
+ (slotProps()['aria-label'] as string | undefined)
112
+ )
113
+ },
114
+ get 'aria-labelledby'() {
115
+ return ariaProps['aria-labelledby'] as string | undefined
116
+ },
117
+ })
112
118
 
113
119
  // Render props values
114
120
  const renderValues = createMemo<ToolbarRenderProps>(() => ({
package/src/Tooltip.tsx CHANGED
@@ -83,6 +83,8 @@ interface TooltipTriggerContextValue {
83
83
  }
84
84
 
85
85
  const TooltipTriggerContext = createContext<TooltipTriggerContextValue | null>(null);
86
+ export const TooltipContext = TooltipTriggerContext;
87
+ export const TooltipTriggerStateContext = createContext<TooltipTriggerState | null>(null);
86
88
 
87
89
  // ============================================
88
90
  // COMPONENTS
@@ -150,9 +152,11 @@ export const TooltipTrigger: ParentComponent<TooltipTriggerComponentProps> = (pr
150
152
  };
151
153
 
152
154
  return (
153
- <TooltipTriggerContext.Provider value={context}>
154
- {processChildren()}
155
- </TooltipTriggerContext.Provider>
155
+ <TooltipTriggerStateContext.Provider value={state}>
156
+ <TooltipTriggerContext.Provider value={context}>
157
+ {processChildren()}
158
+ </TooltipTriggerContext.Provider>
159
+ </TooltipTriggerStateContext.Provider>
156
160
  );
157
161
  };
158
162
 
@@ -242,17 +246,60 @@ export function Tooltip(props: TooltipProps): JSX.Element {
242
246
  const state = () => context?.state ?? localState;
243
247
  const placement = () => props.placement ?? 'top';
244
248
 
245
- // Only render when open
246
249
  const isOpen = () => state().isOpen();
247
250
 
251
+ // Exit animation state machine: 'closed' | 'open' | 'exiting'
252
+ // Keeps the tooltip mounted during exit animation so CSS transitions can play.
253
+ const [exitState, setExitState] = createSignal<'closed' | 'open' | 'exiting'>(
254
+ isOpen() ? 'open' : 'closed'
255
+ );
256
+
257
+ createEffect(() => {
258
+ const open = isOpen();
259
+ const current = exitState();
260
+ if (current === 'open' && !open) {
261
+ setExitState('exiting');
262
+ } else if ((current === 'closed' || current === 'exiting') && open) {
263
+ setExitState('open');
264
+ }
265
+ });
266
+
267
+ // Signal for the tooltip ref so we can observe exit animations
268
+ const [tooltipEl, setTooltipEl] = createSignal<HTMLDivElement | null>(null);
269
+
270
+ // When exiting, wait for CSS animations to finish, then set state to closed
271
+ createEffect(() => {
272
+ if (exitState() !== 'exiting') return;
273
+ const el = tooltipEl();
274
+ if (!el || !('getAnimations' in el)) {
275
+ setExitState('closed');
276
+ return;
277
+ }
278
+ const animations = el.getAnimations();
279
+ if (animations.length === 0) {
280
+ setExitState('closed');
281
+ return;
282
+ }
283
+ let canceled = false;
284
+ 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; });
288
+ });
289
+
290
+ const shouldRender = () => isOpen() || exitState() === 'exiting';
291
+ const isExiting = () => exitState() === 'exiting';
292
+
248
293
  return (
249
- <Show when={isOpen()}>
294
+ <Show when={shouldRender()}>
250
295
  <TooltipContent
251
296
  {...props}
252
297
  state={state()}
253
298
  contextTooltipProps={context?.tooltipProps ?? {}}
254
299
  placement={placement()}
255
300
  triggerRef={context?.triggerRef ?? (() => null)}
301
+ isExiting={isExiting()}
302
+ onTooltipRef={setTooltipEl}
256
303
  />
257
304
  </Show>
258
305
  );
@@ -267,6 +314,8 @@ function TooltipContent(
267
314
  contextTooltipProps: { id?: string };
268
315
  placement: 'top' | 'bottom' | 'left' | 'right';
269
316
  triggerRef: () => HTMLElement | null | undefined;
317
+ isExiting: boolean;
318
+ onTooltipRef: (el: HTMLDivElement | null) => void;
270
319
  }
271
320
  ): JSX.Element {
272
321
  if (isServer) {
@@ -286,9 +335,38 @@ function TooltipContent(
286
335
  visibility: 'visible' as 'hidden' | 'visible',
287
336
  });
288
337
 
338
+ // Enter animation state: starts true on mount, clears after first animation frame.
339
+ // Uses getAnimations() to detect CSS animations/transitions - if none exist (JSDOM,
340
+ // no CSS defined, reduced-motion), clears immediately.
341
+ const [isEntering, setIsEntering] = createSignal(true);
342
+
343
+ createEffect(() => {
344
+ if (!isEntering()) return;
345
+ if (!tooltipRef || !('getAnimations' in tooltipRef)) {
346
+ setIsEntering(false);
347
+ return;
348
+ }
349
+ // Cancel any premature CSS transitions triggered before layout
350
+ for (const anim of tooltipRef.getAnimations()) {
351
+ if (anim instanceof CSSTransition) {
352
+ anim.cancel();
353
+ }
354
+ }
355
+ const animations = tooltipRef.getAnimations();
356
+ if (animations.length === 0) {
357
+ setIsEntering(false);
358
+ return;
359
+ }
360
+ let canceled = false;
361
+ Promise.all(animations.map((a) => a.finished))
362
+ .then(() => { if (!canceled) setIsEntering(false); })
363
+ .catch(() => { if (!canceled) setIsEntering(false); });
364
+ onCleanup(() => { canceled = true; });
365
+ });
366
+
289
367
  const values = createMemo<TooltipRenderProps>(() => ({
290
- isEntering: false, // TODO: animation support
291
- isExiting: false,
368
+ isEntering: isEntering(),
369
+ isExiting: props.isExiting,
292
370
  placement: props.placement,
293
371
  }));
294
372
 
@@ -354,37 +432,36 @@ function TooltipContent(
354
432
  return true;
355
433
  };
356
434
 
357
- // Set up positioning effect - runs when trigger ref is available
435
+ // Set up positioning effect - runs when trigger ref is available.
436
+ // Tracks pending rAF/setTimeout IDs so they can be canceled on cleanup.
358
437
  createEffect(() => {
359
438
  const trigger = props.triggerRef();
360
439
  if (!trigger) return;
361
440
 
362
- // Initial position calculation - use requestAnimationFrame to ensure
363
- // the element is rendered and has proper dimensions
364
- // We may need multiple frames if the trigger ref hasn't resolved yet
365
441
  let retryCount = 0;
366
442
  const maxRetries = 5;
443
+ let pendingRaf = 0;
444
+ let pendingTimeout = 0;
367
445
 
368
446
  const tryUpdatePosition = () => {
447
+ pendingRaf = 0;
448
+ pendingTimeout = 0;
369
449
  const success = updatePosition();
370
450
  if (!success && retryCount < maxRetries) {
371
451
  retryCount++;
372
- // In JSDOM, requestAnimationFrame may not trigger layout properly
373
- // Use setTimeout for more reliable deferral across environments
374
- setTimeout(tryUpdatePosition, 16); // ~60fps
452
+ pendingTimeout = window.setTimeout(tryUpdatePosition, 16);
375
453
  }
376
- // If all retries fail, tooltip stays at 0,0 (test environments)
377
- // The tooltip is visible by default, so it remains accessible
378
454
  };
379
455
 
380
- // Initial attempt - use rAF for real browsers, then fall back to timeout retries
381
- requestAnimationFrame(tryUpdatePosition);
456
+ pendingRaf = requestAnimationFrame(tryUpdatePosition);
382
457
 
383
458
  // Update on scroll/resize
384
459
  window.addEventListener('scroll', updatePosition, true);
385
460
  window.addEventListener('resize', updatePosition);
386
461
 
387
462
  onCleanup(() => {
463
+ if (pendingRaf) cancelAnimationFrame(pendingRaf);
464
+ if (pendingTimeout) clearTimeout(pendingTimeout);
388
465
  window.removeEventListener('scroll', updatePosition, true);
389
466
  window.removeEventListener('resize', updatePosition);
390
467
  });
@@ -396,6 +473,16 @@ function TooltipContent(
396
473
  // Extract ref from ariaTooltipProps to avoid type conflicts (SolidJS ref types are element-specific)
397
474
  const { ref: _ariaRef, ...cleanAriaProps } = ariaTooltipProps as Record<string, unknown>;
398
475
 
476
+ const setRef = (el: HTMLDivElement) => {
477
+ tooltipRef = el;
478
+ props.onTooltipRef(el);
479
+ };
480
+
481
+ // Clean up ref on unmount
482
+ onCleanup(() => {
483
+ props.onTooltipRef(null);
484
+ });
485
+
399
486
  return (
400
487
  <OverlayContainer>
401
488
  <div
@@ -403,7 +490,7 @@ function TooltipContent(
403
490
  {...props.contextTooltipProps}
404
491
  {...cleanAriaProps}
405
492
  role="tooltip"
406
- ref={tooltipRef}
493
+ ref={setRef}
407
494
  class={renderProps.class()}
408
495
  style={{
409
496
  position: 'fixed',
@@ -412,6 +499,8 @@ function TooltipContent(
412
499
  ...renderProps.style(),
413
500
  }}
414
501
  data-placement={props.placement}
502
+ data-entering={isEntering() || undefined}
503
+ data-exiting={props.isExiting || undefined}
415
504
  >
416
505
  {renderProps.renderChildren()}
417
506
  </div>