@carbonid1/design-system 5.7.5 → 5.7.6
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.
|
@@ -11,5 +11,6 @@ type TooltipProps = {
|
|
|
11
11
|
'aria-label'?: string;
|
|
12
12
|
}>;
|
|
13
13
|
};
|
|
14
|
+
export declare const VIEWPORT_MARGIN = 8;
|
|
14
15
|
export declare const Tooltip: ({ label, shortcut, position, delay, maxWidth, disabled, className, children, }: TooltipProps) => import("react/jsx-runtime").JSX.Element;
|
|
15
16
|
export {};
|
package/dist/Tooltip/Tooltip.js
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { computeTooltipPlacement, } from '../helpers/tooltipPlacement/tooltipPlacement';
|
|
3
4
|
import { Kbd } from '../Kbd/Kbd';
|
|
4
|
-
import { cloneElement, useCallback, useEffect, useRef, useState } from 'react';
|
|
5
|
+
import { cloneElement, useCallback, useEffect, useLayoutEffect, useRef, useState, } from 'react';
|
|
5
6
|
const DEFAULT_DELAY = 200;
|
|
7
|
+
export const VIEWPORT_MARGIN = 8;
|
|
8
|
+
// Must stay in sync with the mb-2 / mt-2 (0.5rem = 8px) classes below — the flip
|
|
9
|
+
// math subtracts this gap, so a mismatch would mis-predict whether a side fits.
|
|
10
|
+
const TRIGGER_GAP = 8;
|
|
11
|
+
// Position before paint so an edge tooltip never flashes centered-then-shifted.
|
|
12
|
+
// useLayoutEffect warns and no-ops during SSR, so fall back to useEffect there.
|
|
13
|
+
const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
|
|
6
14
|
export const Tooltip = ({ label, shortcut, position = 'top', delay = DEFAULT_DELAY, maxWidth, disabled, className, children, }) => {
|
|
7
15
|
const [visible, setVisible] = useState(false);
|
|
16
|
+
const [placement, setPlacement] = useState({ side: position, offsetX: 0 });
|
|
8
17
|
const timeoutRef = useRef(null);
|
|
9
18
|
const wrapperRef = useRef(null);
|
|
19
|
+
const tooltipRef = useRef(null);
|
|
10
20
|
const show = useCallback(() => {
|
|
11
21
|
if (disabled)
|
|
12
22
|
return;
|
|
@@ -52,8 +62,37 @@ export const Tooltip = ({ label, shortcut, position = 'top', delay = DEFAULT_DEL
|
|
|
52
62
|
document.addEventListener('pointermove', handlePointerMove, { passive: true });
|
|
53
63
|
return () => document.removeEventListener('pointermove', handlePointerMove);
|
|
54
64
|
}, [visible, hide]);
|
|
55
|
-
|
|
65
|
+
useIsomorphicLayoutEffect(() => {
|
|
66
|
+
if (!visible)
|
|
67
|
+
return;
|
|
68
|
+
const measure = () => {
|
|
69
|
+
const wrapper = wrapperRef.current;
|
|
70
|
+
const tooltip = tooltipRef.current;
|
|
71
|
+
if (!wrapper || !tooltip)
|
|
72
|
+
return;
|
|
73
|
+
const trigger = wrapper.getBoundingClientRect();
|
|
74
|
+
const box = tooltip.getBoundingClientRect();
|
|
75
|
+
const next = computeTooltipPlacement({
|
|
76
|
+
trigger,
|
|
77
|
+
tooltip: { width: box.width, height: box.height },
|
|
78
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
79
|
+
preferredSide: position,
|
|
80
|
+
margin: VIEWPORT_MARGIN,
|
|
81
|
+
gap: TRIGGER_GAP,
|
|
82
|
+
});
|
|
83
|
+
// Keep the same object when nothing moved so a show or a no-op resize
|
|
84
|
+
// doesn't trigger a redundant re-render (the common centered case is a no-op).
|
|
85
|
+
setPlacement(prev => (prev.side === next.side && prev.offsetX === next.offsetX ? prev : next));
|
|
86
|
+
};
|
|
87
|
+
measure();
|
|
88
|
+
window.addEventListener('resize', measure, { passive: true });
|
|
89
|
+
return () => window.removeEventListener('resize', measure);
|
|
90
|
+
}, [visible, position]);
|
|
91
|
+
const positionClasses = placement.side === 'top' ? 'bottom-full mb-2' : 'top-full mt-2';
|
|
56
92
|
return (_jsxs("div", { ref: wrapperRef, className: `relative inline-flex${className ? ` ${className}` : ''}`, onMouseEnter: show, onMouseLeave: hide, onPointerDown: hide, onFocus: show, onBlur: hide, children: [cloneElement(children, {
|
|
57
93
|
'aria-label': children.props['aria-label'] ?? label,
|
|
58
|
-
}), visible && !disabled && (_jsxs("div", { role: "tooltip", style:
|
|
94
|
+
}), visible && !disabled && (_jsxs("div", { ref: tooltipRef, role: "tooltip", style: {
|
|
95
|
+
transform: `translateX(calc(-50% + ${placement.offsetX}px))`,
|
|
96
|
+
...(maxWidth ? { maxWidth } : {}),
|
|
97
|
+
}, className: `absolute left-1/2 z-50 ${maxWidth ? 'w-max whitespace-normal' : 'whitespace-nowrap'} bg-foreground text-background pointer-events-none flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs shadow-lg ${positionClasses}`, children: [label, shortcut && (_jsx(Kbd, { keys: shortcut, size: "sm", className: "bg-background/15 border-transparent" }))] }))] }));
|
|
59
98
|
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
type Side = 'top' | 'bottom';
|
|
2
|
+
type Rect = {
|
|
3
|
+
left: number;
|
|
4
|
+
top: number;
|
|
5
|
+
bottom: number;
|
|
6
|
+
width: number;
|
|
7
|
+
};
|
|
8
|
+
type Size = {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
};
|
|
12
|
+
type TooltipPlacementInput = {
|
|
13
|
+
/** Trigger (wrapper) rect, viewport-relative — the tooltip is centered on its horizontal midpoint. */
|
|
14
|
+
trigger: Rect;
|
|
15
|
+
/** Measured tooltip box, taken at its un-shifted centered position. */
|
|
16
|
+
tooltip: Size;
|
|
17
|
+
viewport: Size;
|
|
18
|
+
/** Side the caller asked for; kept unless that side lacks room. */
|
|
19
|
+
preferredSide: Side;
|
|
20
|
+
/** Minimum gap to keep between the tooltip and each viewport edge. */
|
|
21
|
+
margin: number;
|
|
22
|
+
/** Gap between the trigger and the tooltip on the chosen side. */
|
|
23
|
+
gap: number;
|
|
24
|
+
};
|
|
25
|
+
export type TooltipPlacement = {
|
|
26
|
+
side: Side;
|
|
27
|
+
/** Pixels to shift the centered tooltip horizontally; 0 leaves it centered. */
|
|
28
|
+
offsetX: number;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Decide where a centered tooltip should sit so it stays within the viewport:
|
|
32
|
+
* shift it horizontally when an edge would cross `margin`, and flip the vertical
|
|
33
|
+
* side when the preferred one lacks room. Pure — the component feeds it measured
|
|
34
|
+
* rects and applies the result. On a viewport too narrow to fit the box, the left
|
|
35
|
+
* edge wins (LTR readers lose the tail, not the start).
|
|
36
|
+
*/
|
|
37
|
+
export declare const computeTooltipPlacement: ({ trigger, tooltip, viewport, preferredSide, margin, gap, }: TooltipPlacementInput) => TooltipPlacement;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide where a centered tooltip should sit so it stays within the viewport:
|
|
3
|
+
* shift it horizontally when an edge would cross `margin`, and flip the vertical
|
|
4
|
+
* side when the preferred one lacks room. Pure — the component feeds it measured
|
|
5
|
+
* rects and applies the result. On a viewport too narrow to fit the box, the left
|
|
6
|
+
* edge wins (LTR readers lose the tail, not the start).
|
|
7
|
+
*/
|
|
8
|
+
export const computeTooltipPlacement = ({ trigger, tooltip, viewport, preferredSide, margin, gap, }) => {
|
|
9
|
+
const triggerCenterX = trigger.left + trigger.width / 2;
|
|
10
|
+
const centeredLeft = triggerCenterX - tooltip.width / 2;
|
|
11
|
+
const centeredRight = centeredLeft + tooltip.width;
|
|
12
|
+
let offsetX = 0;
|
|
13
|
+
if (centeredRight > viewport.width - margin)
|
|
14
|
+
offsetX = viewport.width - margin - centeredRight;
|
|
15
|
+
if (centeredLeft + offsetX < margin)
|
|
16
|
+
offsetX = margin - centeredLeft;
|
|
17
|
+
const spaceAbove = trigger.top;
|
|
18
|
+
const spaceBelow = viewport.height - trigger.bottom;
|
|
19
|
+
const needed = tooltip.height + gap + margin;
|
|
20
|
+
let side = preferredSide;
|
|
21
|
+
if (preferredSide === 'top' && spaceAbove < needed && spaceBelow > spaceAbove)
|
|
22
|
+
side = 'bottom';
|
|
23
|
+
if (preferredSide === 'bottom' && spaceBelow < needed && spaceAbove > spaceBelow)
|
|
24
|
+
side = 'top';
|
|
25
|
+
return { side, offsetX };
|
|
26
|
+
};
|