@versini/ui-tooltip 5.3.3 → 5.4.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 (2) hide show
  1. package/dist/index.js +190 -74
  2. package/package.json +2 -3
package/dist/index.js CHANGED
@@ -1,10 +1,9 @@
1
1
  /*!
2
- @versini/ui-tooltip v5.3.3
2
+ @versini/ui-tooltip v5.4.0
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
7
- import { arrow as dom_arrow, computePosition, flip, offset, shift } from "@floating-ui/dom";
8
7
  import { useClickOutside } from "@versini/ui-hooks/use-click-outside";
9
8
  import { useInterval } from "@versini/ui-hooks/use-interval";
10
9
  import { useCallback, useEffect, useRef, useState } from "react";
@@ -20,12 +19,121 @@ const TOOLTIP_ARROW_CLASSNAME = "av-tooltip-arrow";
20
19
 
21
20
 
22
21
 
23
-
24
- const roundInteger = (value, suffix = "px")=>{
25
- return typeof value === "undefined" ? "" : `${Math.round(value)}${suffix}`;
22
+ const ARROW_PADDING = 4;
23
+ const calculateTooltipPosition = (triggerRect, tooltipRect, placement, sideOffset, viewportWidth, viewportHeight)=>{
24
+ /* v8 ignore start - split always returns non-empty for valid placements */ const side = placement.split("-")[0] || "top";
25
+ /* v8 ignore stop */ const alignment = placement.includes("-") ? placement.split("-")[1] : "center";
26
+ let top = 0;
27
+ let left = 0;
28
+ let resolvedSide = side;
29
+ // Calculate base position based on side
30
+ switch(side){
31
+ case "bottom":
32
+ top = triggerRect.bottom + sideOffset;
33
+ break;
34
+ case "top":
35
+ top = triggerRect.top - tooltipRect.height - sideOffset;
36
+ break;
37
+ case "right":
38
+ left = triggerRect.right + sideOffset;
39
+ break;
40
+ case "left":
41
+ left = triggerRect.left - tooltipRect.width - sideOffset;
42
+ break;
43
+ }
44
+ // Calculate alignment
45
+ if (side === "bottom" || side === "top") {
46
+ switch(alignment){
47
+ case "start":
48
+ left = triggerRect.left;
49
+ break;
50
+ case "end":
51
+ left = triggerRect.right - tooltipRect.width;
52
+ break;
53
+ default:
54
+ left = triggerRect.left + (triggerRect.width - tooltipRect.width) / 2;
55
+ break;
56
+ }
57
+ } else {
58
+ switch(alignment){
59
+ case "start":
60
+ top = triggerRect.top;
61
+ break;
62
+ case "end":
63
+ top = triggerRect.bottom - tooltipRect.height;
64
+ break;
65
+ default:
66
+ top = triggerRect.top + (triggerRect.height - tooltipRect.height) / 2;
67
+ break;
68
+ }
69
+ }
70
+ // Auto-flip if overflowing viewport
71
+ /* v8 ignore start - auto-flip with fallback when flipped position also overflows */ if (side === "bottom" && top + tooltipRect.height > viewportHeight) {
72
+ const flippedTop = triggerRect.top - tooltipRect.height - sideOffset;
73
+ if (flippedTop >= 0) {
74
+ top = flippedTop;
75
+ resolvedSide = "top";
76
+ }
77
+ } else if (side === "top" && top < 0) {
78
+ const flippedTop = triggerRect.bottom + sideOffset;
79
+ if (flippedTop + tooltipRect.height <= viewportHeight) {
80
+ top = flippedTop;
81
+ resolvedSide = "bottom";
82
+ }
83
+ } else if (side === "right" && left + tooltipRect.width > viewportWidth) {
84
+ const flippedLeft = triggerRect.left - tooltipRect.width - sideOffset;
85
+ if (flippedLeft >= 0) {
86
+ left = flippedLeft;
87
+ resolvedSide = "left";
88
+ }
89
+ } else if (side === "left" && left < 0) {
90
+ const flippedLeft = triggerRect.right + sideOffset;
91
+ if (flippedLeft + tooltipRect.width <= viewportWidth) {
92
+ left = flippedLeft;
93
+ resolvedSide = "right";
94
+ }
95
+ }
96
+ /* v8 ignore stop */ // Clamp to viewport bounds
97
+ left = Math.max(0, Math.min(left, viewportWidth - tooltipRect.width));
98
+ top = Math.max(0, Math.min(top, viewportHeight - tooltipRect.height));
99
+ return {
100
+ top,
101
+ left,
102
+ resolvedSide
103
+ };
104
+ };
105
+ const calculateArrowPosition = (triggerRect, tooltipTop, tooltipLeft, tooltipWidth, tooltipHeight, resolvedSide, arrowWidth, arrowHeight)=>{
106
+ const staticSideMap = {
107
+ top: "bottom",
108
+ bottom: "top",
109
+ left: "right",
110
+ right: "left"
111
+ };
112
+ const staticSide = staticSideMap[resolvedSide];
113
+ let arrowLeft = "";
114
+ let arrowTop = "";
115
+ if (resolvedSide === "top" || resolvedSide === "bottom") {
116
+ const triggerCenterX = triggerRect.left + triggerRect.width / 2;
117
+ const rawArrowLeft = triggerCenterX - tooltipLeft - arrowWidth / 2;
118
+ const clampedArrowLeft = Math.max(ARROW_PADDING, Math.min(rawArrowLeft, tooltipWidth - arrowWidth - ARROW_PADDING));
119
+ arrowLeft = `${Math.round(clampedArrowLeft)}px`;
120
+ } else {
121
+ const triggerCenterY = triggerRect.top + triggerRect.height / 2;
122
+ const rawArrowTop = triggerCenterY - tooltipTop - arrowHeight / 2;
123
+ const clampedArrowTop = Math.max(ARROW_PADDING, Math.min(rawArrowTop, tooltipHeight - arrowHeight - ARROW_PADDING));
124
+ arrowTop = `${Math.round(clampedArrowTop)}px`;
125
+ }
126
+ return {
127
+ arrowTop,
128
+ arrowLeft,
129
+ arrowRight: "",
130
+ arrowBottom: "",
131
+ staticSide,
132
+ staticSideValue: `-${Math.round(arrowHeight / 2)}px`
133
+ };
26
134
  };
27
135
  const getTooltipBaseClasses = ()=>{
28
- return clsx("absolute top-0 left-0 w-max py-1 px-2 rounded-sm text-sm z-50");
136
+ return clsx("fixed w-max py-1 px-2 rounded-sm text-sm z-50");
29
137
  };
30
138
  const getTooltipBackgroundColorClasses = ({ mode })=>{
31
139
  return clsx({
@@ -53,7 +161,7 @@ const getTooltipArrowBaseClasses = ()=>{
53
161
  return clsx("absolute", "size-2", "transform rotate-45");
54
162
  };
55
163
  const getTooltipClasses = ({ mode, className, tooltipClassName, arrowClassName })=>{
56
- const wrapper = clsx("relative inline-block", className);
164
+ const wrapper = clsx("inline-block", className);
57
165
  const tooltip = clsx(TOOLTIP_CLASSNAME, getTooltipBaseClasses(), getTooltipBackgroundColorClasses({
58
166
  mode
59
167
  }), getTooltipTextCopyColorClasses({
@@ -78,6 +186,68 @@ const getAnimationStyles = ({ animationDuration = 300 })=>{
78
186
 
79
187
 
80
188
 
189
+ function useTooltipPosition({ triggerRef, tooltipRef, arrowRef, placement, sideOffset, animationDuration, isOpen }) {
190
+ const [isPositioned, setIsPositioned] = useState(false);
191
+ const updatePosition = useCallback(()=>{
192
+ /* v8 ignore start - positioning requires real DOM layout */ if (!triggerRef.current || !tooltipRef.current || !arrowRef.current) {
193
+ return;
194
+ }
195
+ const triggerRect = triggerRef.current.getBoundingClientRect();
196
+ const tooltipRect = tooltipRef.current.getBoundingClientRect();
197
+ const viewportWidth = window.innerWidth;
198
+ const viewportHeight = window.innerHeight;
199
+ const { top, left, resolvedSide } = calculateTooltipPosition(triggerRect, tooltipRect, placement, sideOffset, viewportWidth, viewportHeight);
200
+ // Use offsetWidth/offsetHeight to get the untransformed arrow size,
201
+ // since the arrow has transform:rotate(45deg) which inflates
202
+ // getBoundingClientRect() dimensions.
203
+ const arrowWidth = arrowRef.current.offsetWidth;
204
+ const arrowHeight = arrowRef.current.offsetHeight;
205
+ const arrowPosition = calculateArrowPosition(triggerRect, top, left, tooltipRect.width, tooltipRect.height, resolvedSide, arrowWidth, arrowHeight);
206
+ const animationStyles = getAnimationStyles({
207
+ animationDuration
208
+ });
209
+ Object.assign(tooltipRef.current.style, {
210
+ position: "fixed",
211
+ top: `${Math.round(top)}px`,
212
+ left: `${Math.round(left)}px`,
213
+ margin: "0",
214
+ ...animationStyles
215
+ });
216
+ Object.assign(arrowRef.current.style, {
217
+ left: arrowPosition.arrowLeft,
218
+ top: arrowPosition.arrowTop,
219
+ right: arrowPosition.arrowRight,
220
+ bottom: arrowPosition.arrowBottom,
221
+ [arrowPosition.staticSide]: arrowPosition.staticSideValue
222
+ });
223
+ setIsPositioned(true);
224
+ /* v8 ignore stop */ }, [
225
+ triggerRef,
226
+ tooltipRef,
227
+ arrowRef,
228
+ placement,
229
+ sideOffset,
230
+ animationDuration
231
+ ]);
232
+ useEffect(()=>{
233
+ if (isOpen) {
234
+ requestAnimationFrame(()=>{
235
+ updatePosition();
236
+ });
237
+ } else {
238
+ setIsPositioned(false);
239
+ }
240
+ }, [
241
+ isOpen,
242
+ updatePosition
243
+ ]);
244
+ return {
245
+ isPositioned
246
+ };
247
+ }
248
+
249
+
250
+
81
251
 
82
252
 
83
253
 
@@ -87,12 +257,11 @@ const Tooltip = ({ trigger, label, placement = "top", mode = "system", animation
87
257
  const referenceRef = useClickOutside(()=>{
88
258
  /* v8 ignore start */ delayedRestartTooltip.stop(), setDisabled(false);
89
259
  /* v8 ignore stop */ });
90
- const floatingRef = useRef(null);
91
- const floatingArrowRef = useRef(null);
260
+ const tooltipRef = useRef(null);
261
+ const arrowRef = useRef(null);
92
262
  const showTimeoutRef = useRef(null);
93
263
  const [showTooltip, setShowTooltip] = useState(false);
94
264
  const [disabled, setDisabled] = useState(false);
95
- const [isPositioned, setIsPositioned] = useState(false);
96
265
  /* v8 ignore start - delayed interval callback */ const delayedRestartTooltip = useInterval(()=>{
97
266
  setDisabled(false);
98
267
  }, DEFAULT_DEACTIVATION_DELAY);
@@ -102,69 +271,16 @@ const Tooltip = ({ trigger, label, placement = "top", mode = "system", animation
102
271
  tooltipClassName,
103
272
  arrowClassName
104
273
  });
105
- const animationStyles = getAnimationStyles({
106
- animationDuration
107
- });
108
- /* v8 ignore start - async floating UI positioning */ const updatePosition = useCallback(async ()=>{
109
- if (referenceRef.current && floatingRef.current && floatingArrowRef.current) {
110
- const { x, y, middlewareData, placement: newPlacement } = await computePosition(referenceRef.current, floatingRef.current, {
111
- placement,
112
- middleware: [
113
- offset(10),
114
- flip({
115
- crossAxis: placement.includes("-"),
116
- fallbackAxisSideDirection: "start"
117
- }),
118
- shift({
119
- padding: 5
120
- }),
121
- dom_arrow({
122
- element: floatingArrowRef.current
123
- })
124
- ]
125
- });
126
- if (floatingRef?.current?.style) {
127
- Object.assign(floatingRef.current.style, {
128
- left: roundInteger(x),
129
- top: roundInteger(y),
130
- ...animationStyles
131
- });
132
- }
133
- const staticSide = {
134
- top: "bottom",
135
- right: "left",
136
- bottom: "top",
137
- left: "right"
138
- }[newPlacement.split("-")[0]];
139
- if (floatingArrowRef?.current?.style) {
140
- Object.assign(floatingArrowRef.current.style, {
141
- left: roundInteger(middlewareData.arrow?.x),
142
- top: roundInteger(middlewareData.arrow?.y),
143
- right: "",
144
- bottom: "",
145
- [staticSide]: "-4px"
146
- });
147
- }
148
- setIsPositioned(true);
149
- }
150
- }, [
274
+ const { isPositioned } = useTooltipPosition({
275
+ triggerRef: referenceRef,
276
+ tooltipRef,
277
+ arrowRef,
151
278
  placement,
152
- animationStyles,
153
- referenceRef
154
- ]);
155
- /* v8 ignore stop */ /* v8 ignore start - async effect handler */ useEffect(()=>{
156
- (async ()=>{
157
- if (showTooltip) {
158
- await updatePosition();
159
- } else {
160
- setIsPositioned(false);
161
- }
162
- })();
163
- }, [
164
- updatePosition,
165
- showTooltip
166
- ]);
167
- /* v8 ignore stop */ useEffect(()=>{
279
+ sideOffset: 10,
280
+ animationDuration,
281
+ isOpen: showTooltip
282
+ });
283
+ useEffect(()=>{
168
284
  /* v8 ignore start - cleanup function */ return ()=>{
169
285
  if (showTimeoutRef.current) {
170
286
  clearTimeout(showTimeoutRef.current);
@@ -212,7 +328,7 @@ const Tooltip = ({ trigger, label, placement = "top", mode = "system", animation
212
328
  trigger,
213
329
  showTooltip && /*#__PURE__*/ jsxs("div", {
214
330
  role: "tooltip",
215
- ref: floatingRef,
331
+ ref: tooltipRef,
216
332
  className: tooltipClasses.tooltip,
217
333
  style: {
218
334
  opacity: isPositioned ? undefined : 0
@@ -220,7 +336,7 @@ const Tooltip = ({ trigger, label, placement = "top", mode = "system", animation
220
336
  children: [
221
337
  label,
222
338
  /*#__PURE__*/ jsx("div", {
223
- ref: floatingArrowRef,
339
+ ref: arrowRef,
224
340
  className: tooltipClasses.arrow
225
341
  })
226
342
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@versini/ui-tooltip",
3
- "version": "5.3.3",
3
+ "version": "5.4.0",
4
4
  "license": "MIT",
5
5
  "author": "Arno Versini",
6
6
  "publishConfig": {
@@ -41,7 +41,6 @@
41
41
  "@versini/ui-types": "8.3.0"
42
42
  },
43
43
  "dependencies": {
44
- "@floating-ui/dom": "^1.7.5",
45
44
  "@versini/ui-hooks": "6.1.1",
46
45
  "clsx": "2.1.1",
47
46
  "tailwindcss": "4.2.0"
@@ -49,5 +48,5 @@
49
48
  "sideEffects": [
50
49
  "**/*.css"
51
50
  ],
52
- "gitHead": "9129610bbd6d91a2bc6cac41b0ccf8430a13fa41"
51
+ "gitHead": "ee67ae957439fc613daf2610a3643b45b6702320"
53
52
  }