@versini/ui-tooltip 5.3.2 → 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.
- package/dist/index.js +190 -74
- package/package.json +3 -4
package/dist/index.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
@versini/ui-tooltip v5.
|
|
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
|
|
25
|
-
|
|
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("
|
|
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("
|
|
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
|
|
91
|
-
const
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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:
|
|
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:
|
|
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
|
+
"version": "5.4.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Arno Versini",
|
|
6
6
|
"publishConfig": {
|
|
@@ -41,13 +41,12 @@
|
|
|
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
|
-
"tailwindcss": "4.
|
|
46
|
+
"tailwindcss": "4.2.0"
|
|
48
47
|
},
|
|
49
48
|
"sideEffects": [
|
|
50
49
|
"**/*.css"
|
|
51
50
|
],
|
|
52
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "ee67ae957439fc613daf2610a3643b45b6702320"
|
|
53
52
|
}
|