@carbonid1/design-system 5.7.6 → 5.7.7
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/Tooltip/Tooltip.js +39 -16
- package/package.json +1 -1
package/dist/Tooltip/Tooltip.js
CHANGED
|
@@ -3,17 +3,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { computeTooltipPlacement, } from '../helpers/tooltipPlacement/tooltipPlacement';
|
|
4
4
|
import { Kbd } from '../Kbd/Kbd';
|
|
5
5
|
import { cloneElement, useCallback, useEffect, useLayoutEffect, useRef, useState, } from 'react';
|
|
6
|
+
import { createPortal } from 'react-dom';
|
|
6
7
|
const DEFAULT_DELAY = 200;
|
|
7
8
|
export const VIEWPORT_MARGIN = 8;
|
|
8
|
-
//
|
|
9
|
-
//
|
|
9
|
+
// Gap in px between the trigger and the tooltip box, baked into the fixed `top`
|
|
10
|
+
// offset below; the flip math subtracts it when deciding whether a side fits.
|
|
10
11
|
const TRIGGER_GAP = 8;
|
|
11
12
|
// Position before paint so an edge tooltip never flashes centered-then-shifted.
|
|
12
13
|
// useLayoutEffect warns and no-ops during SSR, so fall back to useEffect there.
|
|
13
14
|
const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
|
|
14
15
|
export const Tooltip = ({ label, shortcut, position = 'top', delay = DEFAULT_DELAY, maxWidth, disabled, className, children, }) => {
|
|
15
16
|
const [visible, setVisible] = useState(false);
|
|
16
|
-
const [
|
|
17
|
+
const [box, setBox] = useState(null);
|
|
17
18
|
const timeoutRef = useRef(null);
|
|
18
19
|
const wrapperRef = useRef(null);
|
|
19
20
|
const tooltipRef = useRef(null);
|
|
@@ -71,28 +72,50 @@ export const Tooltip = ({ label, shortcut, position = 'top', delay = DEFAULT_DEL
|
|
|
71
72
|
if (!wrapper || !tooltip)
|
|
72
73
|
return;
|
|
73
74
|
const trigger = wrapper.getBoundingClientRect();
|
|
74
|
-
const
|
|
75
|
-
const
|
|
75
|
+
const tip = tooltip.getBoundingClientRect();
|
|
76
|
+
const placement = computeTooltipPlacement({
|
|
76
77
|
trigger,
|
|
77
|
-
tooltip: { width:
|
|
78
|
+
tooltip: { width: tip.width, height: tip.height },
|
|
78
79
|
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
79
80
|
preferredSide: position,
|
|
80
81
|
margin: VIEWPORT_MARGIN,
|
|
81
82
|
gap: TRIGGER_GAP,
|
|
82
83
|
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
const left = trigger.left + trigger.width / 2;
|
|
85
|
+
const top = placement.side === 'top' ? trigger.top - TRIGGER_GAP : trigger.bottom + TRIGGER_GAP;
|
|
86
|
+
// Keep the same object when nothing moved so a no-op resize/scroll doesn't
|
|
87
|
+
// trigger a redundant re-render (the common centered, still case is a no-op).
|
|
88
|
+
setBox(prev => prev &&
|
|
89
|
+
prev.side === placement.side &&
|
|
90
|
+
prev.offsetX === placement.offsetX &&
|
|
91
|
+
prev.left === left &&
|
|
92
|
+
prev.top === top
|
|
93
|
+
? prev
|
|
94
|
+
: { ...placement, left, top });
|
|
86
95
|
};
|
|
87
96
|
measure();
|
|
97
|
+
// Portaled and fixed, the box no longer rides the trigger's scroll container,
|
|
98
|
+
// so re-anchor on scroll (capture, to catch any scrolling ancestor) and resize.
|
|
88
99
|
window.addEventListener('resize', measure, { passive: true });
|
|
89
|
-
|
|
100
|
+
window.addEventListener('scroll', measure, { capture: true, passive: true });
|
|
101
|
+
return () => {
|
|
102
|
+
window.removeEventListener('resize', measure);
|
|
103
|
+
window.removeEventListener('scroll', measure, { capture: true });
|
|
104
|
+
};
|
|
90
105
|
}, [visible, position]);
|
|
91
|
-
const
|
|
92
|
-
|
|
106
|
+
const boxStyle = {
|
|
107
|
+
position: 'fixed',
|
|
108
|
+
left: box?.left ?? 0,
|
|
109
|
+
top: box?.top ?? 0,
|
|
110
|
+
transform: `translate(calc(-50% + ${box?.offsetX ?? 0}px), ${box?.side === 'top' ? '-100%' : '0'})`,
|
|
111
|
+
// Hidden until measured so the first frame never paints at the un-anchored origin.
|
|
112
|
+
visibility: box ? 'visible' : 'hidden',
|
|
113
|
+
...(maxWidth ? { maxWidth } : {}),
|
|
114
|
+
};
|
|
115
|
+
return (_jsxs("div", { ref: wrapperRef, className: `inline-flex${className ? ` ${className}` : ''}`, onMouseEnter: show, onMouseLeave: hide, onPointerDown: hide, onFocus: show, onBlur: hide, children: [cloneElement(children, {
|
|
93
116
|
'aria-label': children.props['aria-label'] ?? label,
|
|
94
|
-
}), visible &&
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
117
|
+
}), visible &&
|
|
118
|
+
!disabled &&
|
|
119
|
+
typeof document !== 'undefined' &&
|
|
120
|
+
createPortal(_jsxs("div", { ref: tooltipRef, role: "tooltip", style: boxStyle, className: `bg-foreground text-background pointer-events-none z-50 flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs shadow-lg ${maxWidth ? 'w-max whitespace-normal' : 'whitespace-nowrap'}`, children: [label, shortcut && (_jsx(Kbd, { keys: shortcut, size: "sm", className: "bg-background/15 border-transparent" }))] }), document.body)] }));
|
|
98
121
|
};
|