@redocly/theme 0.64.0 → 0.65.0-next.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/lib/components/Tooltip/AnchorTooltip.js +11 -2
- package/lib/components/Tooltip/JsTooltip.js +28 -47
- package/lib/components/Tooltip/Tooltip.d.ts +1 -1
- package/lib/components/Tooltip/Tooltip.js +5 -2
- package/lib/core/hooks/index.d.ts +1 -0
- package/lib/core/hooks/index.js +1 -0
- package/lib/core/hooks/use-tooltip-fallback-placement.d.ts +15 -0
- package/lib/core/hooks/use-tooltip-fallback-placement.js +46 -0
- package/lib/core/styles/global.js +1 -0
- package/lib/core/types/tooltip.d.ts +3 -1
- package/lib/core/utils/index.d.ts +1 -0
- package/lib/core/utils/index.js +1 -0
- package/lib/core/utils/tooltip-placement.d.ts +32 -0
- package/lib/core/utils/tooltip-placement.js +92 -0
- package/package.json +2 -2
- package/src/components/Tooltip/AnchorTooltip.tsx +20 -3
- package/src/components/Tooltip/JsTooltip.tsx +36 -48
- package/src/components/Tooltip/Tooltip.tsx +14 -3
- package/src/core/hooks/__mocks__/index.ts +1 -0
- package/src/core/hooks/index.ts +1 -0
- package/src/core/hooks/use-tooltip-fallback-placement.ts +73 -0
- package/src/core/styles/global.ts +1 -0
- package/src/core/types/tooltip.ts +4 -1
- package/src/core/utils/index.ts +1 -0
- package/src/core/utils/tooltip-placement.ts +141 -0
|
@@ -38,11 +38,18 @@ const react_1 = __importStar(require("react"));
|
|
|
38
38
|
const styled_components_1 = __importStar(require("styled-components"));
|
|
39
39
|
const hooks_1 = require("../../core/hooks");
|
|
40
40
|
const Portal_1 = require("../../components/Portal/Portal");
|
|
41
|
-
function TooltipComponent({ children, isOpen, tip, withArrow = true, placement = 'top', className = 'default', width, dataTestId, disabled = false, arrowPosition = 'center', onClick, }) {
|
|
41
|
+
function TooltipComponent({ children, isOpen, tip, withArrow = true, placement = 'top', fallbackPlacements, className = 'default', width, dataTestId, disabled = false, arrowPosition = 'center', onClick, }) {
|
|
42
42
|
const tooltipWrapperRef = (0, react_1.useRef)(null);
|
|
43
43
|
const tooltipBodyRef = (0, react_1.useRef)(null);
|
|
44
44
|
const { isOpened, handleOpen, handleClose } = (0, hooks_1.useControl)(isOpen);
|
|
45
45
|
const anchorName = `--tooltip${(0, react_1.useId)().replace(/:/g, '')}`;
|
|
46
|
+
const { activePlacement, activeArrowPosition } = (0, hooks_1.useTooltipFallbackPlacement)({
|
|
47
|
+
isOpened,
|
|
48
|
+
placement,
|
|
49
|
+
arrowPosition,
|
|
50
|
+
fallbackPlacements,
|
|
51
|
+
tooltipBodyRef,
|
|
52
|
+
});
|
|
46
53
|
(0, hooks_1.useOutsideClick)(isOpened ? [tooltipWrapperRef, tooltipBodyRef] : tooltipWrapperRef, handleClose);
|
|
47
54
|
const isControlled = isOpen !== undefined;
|
|
48
55
|
(0, react_1.useEffect)(() => {
|
|
@@ -70,7 +77,9 @@ function TooltipComponent({ children, isOpen, tip, withArrow = true, placement =
|
|
|
70
77
|
return (react_1.default.createElement(TooltipWrapper, Object.assign({ ref: tooltipWrapperRef }, controllers, { className: `tooltip-${className}`, "data-component-name": "Tooltip/Tooltip", anchorName: anchorName }),
|
|
71
78
|
children,
|
|
72
79
|
isOpened && !disabled && (react_1.default.createElement(Portal_1.Portal, null,
|
|
73
|
-
react_1.default.createElement(TooltipBody, { ref: tooltipBodyRef, "data-testid": dataTestId || (typeof tip === 'string' ? tip : ''), placement:
|
|
80
|
+
react_1.default.createElement(TooltipBody, { ref: tooltipBodyRef, "data-testid": dataTestId || (typeof tip === 'string' ? tip : ''), placement: activePlacement, width: width, withArrow: withArrow, arrowPosition: activeArrowPosition === 'left' || activeArrowPosition === 'right'
|
|
81
|
+
? activeArrowPosition
|
|
82
|
+
: 'center', anchorName: anchorName }, tip)))));
|
|
74
83
|
}
|
|
75
84
|
exports.Tooltip = (0, react_1.memo)(TooltipComponent);
|
|
76
85
|
const PLACEMENTS = {
|
|
@@ -38,56 +38,37 @@ const react_1 = __importStar(require("react"));
|
|
|
38
38
|
const styled_components_1 = __importStar(require("styled-components"));
|
|
39
39
|
const hooks_1 = require("../../core/hooks");
|
|
40
40
|
const Portal_1 = require("../../components/Portal/Portal");
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
const utils_1 = require("../../core/utils");
|
|
42
|
+
function TooltipComponent({ children, isOpen, tip, withArrow = true, placement = 'top', fallbackPlacements, className = 'default', width, dataTestId, disabled = false, arrowPosition = 'center', onClick, }) {
|
|
43
|
+
const wrapperRef = (0, react_1.useRef)(null);
|
|
44
|
+
const tooltipBodyRef = (0, react_1.useRef)(null);
|
|
43
45
|
const { isOpened, handleOpen, handleClose } = (0, hooks_1.useControl)(isOpen);
|
|
44
46
|
const [tooltipPosition, setTooltipPosition] = (0, react_1.useState)({ top: 0, left: 0 });
|
|
45
|
-
(0,
|
|
47
|
+
const [activePlacement, setActivePlacement] = (0, react_1.useState)(placement);
|
|
48
|
+
const activeArrowPosition = activePlacement === placement ? arrowPosition : 'center';
|
|
49
|
+
(0, hooks_1.useOutsideClick)(wrapperRef, handleClose);
|
|
46
50
|
const isControlled = isOpen !== undefined;
|
|
47
51
|
const updateTooltipPosition = (0, react_1.useCallback)(() => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
top = rect.bottom;
|
|
67
|
-
if (arrowPosition === 'left') {
|
|
68
|
-
left = rect.left - 24;
|
|
69
|
-
}
|
|
70
|
-
else if (arrowPosition === 'right') {
|
|
71
|
-
left = rect.right + 24;
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
left = rect.left + rect.width / 2;
|
|
75
|
-
}
|
|
76
|
-
break;
|
|
77
|
-
case 'left':
|
|
78
|
-
top = rect.top + rect.height / 2;
|
|
79
|
-
left = rect.left;
|
|
80
|
-
break;
|
|
81
|
-
case 'right':
|
|
82
|
-
top = rect.top + rect.height / 2;
|
|
83
|
-
left = rect.right;
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
setTooltipPosition({ top, left });
|
|
87
|
-
}
|
|
88
|
-
}, [isOpened, placement, arrowPosition]);
|
|
52
|
+
var _a, _b, _c, _d;
|
|
53
|
+
if (!isOpened || !wrapperRef.current)
|
|
54
|
+
return;
|
|
55
|
+
const triggerRect = wrapperRef.current.getBoundingClientRect();
|
|
56
|
+
const tooltipWidth = (_b = (_a = tooltipBodyRef.current) === null || _a === void 0 ? void 0 : _a.offsetWidth) !== null && _b !== void 0 ? _b : 0;
|
|
57
|
+
const tooltipHeight = (_d = (_c = tooltipBodyRef.current) === null || _c === void 0 ? void 0 : _c.offsetHeight) !== null && _d !== void 0 ? _d : 0;
|
|
58
|
+
const resolved = (0, utils_1.resolvePlacement)({
|
|
59
|
+
triggerRect,
|
|
60
|
+
tooltipWidth,
|
|
61
|
+
tooltipHeight,
|
|
62
|
+
placement,
|
|
63
|
+
arrowPosition,
|
|
64
|
+
fallbackPlacements,
|
|
65
|
+
});
|
|
66
|
+
const resolvedArrow = resolved === placement ? arrowPosition : 'center';
|
|
67
|
+
setTooltipPosition((0, utils_1.calcAnchorPoint)(triggerRect, resolved, resolvedArrow));
|
|
68
|
+
setActivePlacement(resolved);
|
|
69
|
+
}, [isOpened, placement, arrowPosition, fallbackPlacements]);
|
|
89
70
|
(0, react_1.useLayoutEffect)(() => {
|
|
90
|
-
if (isOpened &&
|
|
71
|
+
if (isOpened && wrapperRef.current) {
|
|
91
72
|
updateTooltipPosition();
|
|
92
73
|
const handleScroll = () => updateTooltipPosition();
|
|
93
74
|
const handleResize = () => updateTooltipPosition();
|
|
@@ -118,10 +99,10 @@ function TooltipComponent({ children, isOpen, tip, withArrow = true, placement =
|
|
|
118
99
|
onFocus: handleOpen,
|
|
119
100
|
onBlur: handleClose,
|
|
120
101
|
};
|
|
121
|
-
return (react_1.default.createElement(TooltipWrapper, Object.assign({ ref:
|
|
102
|
+
return (react_1.default.createElement(TooltipWrapper, Object.assign({ ref: wrapperRef }, controllers, { className: `tooltip-${className}`, "data-component-name": "Tooltip/Tooltip" }),
|
|
122
103
|
children,
|
|
123
104
|
isOpened && !disabled && (react_1.default.createElement(Portal_1.Portal, null,
|
|
124
|
-
react_1.default.createElement(TooltipBody, { "data-testid": dataTestId || (typeof tip === 'string' ? tip : ''), placement:
|
|
105
|
+
react_1.default.createElement(TooltipBody, { ref: tooltipBodyRef, "data-testid": dataTestId || (typeof tip === 'string' ? tip : ''), placement: activePlacement, width: width, withArrow: withArrow, arrowPosition: activeArrowPosition, style: {
|
|
125
106
|
position: 'fixed',
|
|
126
107
|
top: tooltipPosition.top,
|
|
127
108
|
left: tooltipPosition.left,
|
|
@@ -38,13 +38,16 @@ const react_1 = __importStar(require("react"));
|
|
|
38
38
|
const AnchorTooltip_1 = require("../../components/Tooltip/AnchorTooltip");
|
|
39
39
|
const JsTooltip_1 = require("../../components/Tooltip/JsTooltip");
|
|
40
40
|
const hooks_1 = require("../../core/hooks");
|
|
41
|
+
const utils_1 = require("../../core/utils");
|
|
41
42
|
function TooltipComponent(props) {
|
|
43
|
+
var _a, _b;
|
|
42
44
|
const { useAnchorPositioning } = (0, hooks_1.useThemeHooks)();
|
|
43
45
|
const { isSupported } = useAnchorPositioning();
|
|
46
|
+
const fallbackPlacements = (_a = props.fallbackPlacements) !== null && _a !== void 0 ? _a : (0, utils_1.getDefaultFallbackPlacements)((_b = props.placement) !== null && _b !== void 0 ? _b : 'top');
|
|
44
47
|
if (isSupported) {
|
|
45
|
-
return react_1.default.createElement(AnchorTooltip_1.Tooltip, Object.assign({}, props, { arrowPosition: prepareArrowPosition(props.arrowPosition) }));
|
|
48
|
+
return (react_1.default.createElement(AnchorTooltip_1.Tooltip, Object.assign({}, props, { fallbackPlacements: fallbackPlacements, arrowPosition: prepareArrowPosition(props.arrowPosition) })));
|
|
46
49
|
}
|
|
47
|
-
return react_1.default.createElement(JsTooltip_1.Tooltip, Object.assign({}, props));
|
|
50
|
+
return react_1.default.createElement(JsTooltip_1.Tooltip, Object.assign({}, props, { fallbackPlacements: fallbackPlacements }));
|
|
48
51
|
}
|
|
49
52
|
exports.Tooltip = (0, react_1.memo)(TooltipComponent);
|
|
50
53
|
const prepareArrowPosition = (arrowPosition) => {
|
package/lib/core/hooks/index.js
CHANGED
|
@@ -70,4 +70,5 @@ __exportStar(require("./use-is-truncated"), exports);
|
|
|
70
70
|
__exportStar(require("./use-toast"), exports);
|
|
71
71
|
__exportStar(require("./use-toast-logic"), exports);
|
|
72
72
|
__exportStar(require("./use-banner-telemetry"), exports);
|
|
73
|
+
__exportStar(require("./use-tooltip-fallback-placement"), exports);
|
|
73
74
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RefObject } from 'react';
|
|
2
|
+
import type { TooltipPlacement, TooltipProps } from '../../core/types';
|
|
3
|
+
type TooltipFallbackPlacementParams = {
|
|
4
|
+
isOpened: boolean;
|
|
5
|
+
placement: TooltipPlacement;
|
|
6
|
+
arrowPosition: TooltipProps['arrowPosition'];
|
|
7
|
+
fallbackPlacements: TooltipPlacement[] | undefined;
|
|
8
|
+
tooltipBodyRef: RefObject<HTMLElement | null>;
|
|
9
|
+
};
|
|
10
|
+
type TooltipFallbackPlacementResult = {
|
|
11
|
+
activePlacement: TooltipPlacement;
|
|
12
|
+
activeArrowPosition: TooltipProps['arrowPosition'];
|
|
13
|
+
};
|
|
14
|
+
export declare function useTooltipFallbackPlacement({ isOpened, placement, arrowPosition, fallbackPlacements, tooltipBodyRef, }: TooltipFallbackPlacementParams): TooltipFallbackPlacementResult;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useTooltipFallbackPlacement = useTooltipFallbackPlacement;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
function useTooltipFallbackPlacement({ isOpened, placement, arrowPosition, fallbackPlacements, tooltipBodyRef, }) {
|
|
6
|
+
const [activePlacement, setActivePlacement] = (0, react_1.useState)(placement);
|
|
7
|
+
const wasOpenRef = (0, react_1.useRef)(false);
|
|
8
|
+
const candidateIndexRef = (0, react_1.useRef)(0);
|
|
9
|
+
(0, react_1.useLayoutEffect)(() => {
|
|
10
|
+
if (!isOpened) {
|
|
11
|
+
wasOpenRef.current = false;
|
|
12
|
+
candidateIndexRef.current = 0;
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (!wasOpenRef.current) {
|
|
16
|
+
wasOpenRef.current = true;
|
|
17
|
+
candidateIndexRef.current = 0;
|
|
18
|
+
if (activePlacement !== placement) {
|
|
19
|
+
setActivePlacement(placement);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (!tooltipBodyRef.current || !(fallbackPlacements === null || fallbackPlacements === void 0 ? void 0 : fallbackPlacements.length))
|
|
24
|
+
return;
|
|
25
|
+
const candidates = [placement, ...fallbackPlacements];
|
|
26
|
+
if (candidateIndexRef.current >= candidates.length)
|
|
27
|
+
return;
|
|
28
|
+
const rect = tooltipBodyRef.current.getBoundingClientRect();
|
|
29
|
+
const overflows = rect.left < 0 ||
|
|
30
|
+
rect.top < 0 ||
|
|
31
|
+
rect.right > window.innerWidth ||
|
|
32
|
+
rect.bottom > window.innerHeight;
|
|
33
|
+
if (!overflows)
|
|
34
|
+
return;
|
|
35
|
+
candidateIndexRef.current++;
|
|
36
|
+
if (candidateIndexRef.current < candidates.length) {
|
|
37
|
+
setActivePlacement(candidates[candidateIndexRef.current]);
|
|
38
|
+
}
|
|
39
|
+
else if (activePlacement !== placement) {
|
|
40
|
+
setActivePlacement(placement);
|
|
41
|
+
}
|
|
42
|
+
}, [isOpened, activePlacement, placement, fallbackPlacements, tooltipBodyRef]);
|
|
43
|
+
const activeArrowPosition = activePlacement === placement ? arrowPosition : 'center';
|
|
44
|
+
return { activePlacement, activeArrowPosition };
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=use-tooltip-fallback-placement.js.map
|
|
@@ -1232,6 +1232,7 @@ const replay = (0, styled_components_1.css) `
|
|
|
1232
1232
|
--replay-runtime-expression-color: rgba(54, 90, 249, 1); // @presenter Color
|
|
1233
1233
|
--replay-runtime-expression-bg-color: rgba(54, 90, 249, 0.08); // @presenter Color
|
|
1234
1234
|
--replay-operators-color: rgba(193, 142, 31, 1); // @presenter Color
|
|
1235
|
+
--replay-claude-icon-color: rgba(217, 119, 87, 1); // @presenter Color
|
|
1235
1236
|
|
|
1236
1237
|
--replay-ai-gradient-soft: linear-gradient(62.6deg, rgba(113, 94, 254, 0.16) 0%, rgba(255, 92, 220, 0.16) 100%);
|
|
1237
1238
|
--replay-ai-gradient-disabled: linear-gradient(62.6deg, rgba(113, 94, 254, 0.6) 0%, rgba(255, 92, 220, 0.6) 100%);
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
+
export type TooltipPlacement = 'top' | 'bottom' | 'left' | 'right';
|
|
2
3
|
export type TooltipProps = {
|
|
3
4
|
children?: ReactNode;
|
|
4
5
|
tip: string | ReactNode;
|
|
5
6
|
isOpen?: boolean;
|
|
6
7
|
withArrow?: boolean;
|
|
7
|
-
placement?:
|
|
8
|
+
placement?: TooltipPlacement;
|
|
9
|
+
fallbackPlacements?: TooltipPlacement[];
|
|
8
10
|
className?: string;
|
|
9
11
|
width?: string;
|
|
10
12
|
dataTestId?: string;
|
package/lib/core/utils/index.js
CHANGED
|
@@ -62,4 +62,5 @@ __exportStar(require("./build-revision-url"), exports);
|
|
|
62
62
|
__exportStar(require("./content-segments"), exports);
|
|
63
63
|
__exportStar(require("./custom-catalog-options-casing"), exports);
|
|
64
64
|
__exportStar(require("./get-auto-dismiss-duration"), exports);
|
|
65
|
+
__exportStar(require("./tooltip-placement"), exports);
|
|
65
66
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { TooltipPlacement, TooltipProps } from '../../core/types';
|
|
2
|
+
export declare function getDefaultFallbackPlacements(placement: TooltipPlacement): TooltipPlacement[];
|
|
3
|
+
export declare function calcAnchorPoint(triggerRect: DOMRect, placement: TooltipPlacement, arrowPosition: TooltipProps['arrowPosition']): {
|
|
4
|
+
top: number;
|
|
5
|
+
left: number;
|
|
6
|
+
};
|
|
7
|
+
type FitsInViewportParams = {
|
|
8
|
+
anchor: {
|
|
9
|
+
top: number;
|
|
10
|
+
left: number;
|
|
11
|
+
};
|
|
12
|
+
tooltipWidth: number;
|
|
13
|
+
tooltipHeight: number;
|
|
14
|
+
placement: TooltipPlacement;
|
|
15
|
+
arrowPosition: TooltipProps['arrowPosition'];
|
|
16
|
+
};
|
|
17
|
+
export declare function fitsInViewport({ anchor, tooltipWidth, tooltipHeight, placement, arrowPosition, }: FitsInViewportParams): boolean;
|
|
18
|
+
type ResolvePlacementParams = {
|
|
19
|
+
triggerRect: DOMRect;
|
|
20
|
+
tooltipWidth: number;
|
|
21
|
+
tooltipHeight: number;
|
|
22
|
+
placement: TooltipPlacement;
|
|
23
|
+
arrowPosition: TooltipProps['arrowPosition'];
|
|
24
|
+
fallbackPlacements: TooltipPlacement[] | undefined;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Given the trigger rect, tooltip dimensions, primary placement/arrow, and
|
|
28
|
+
* fallback list, returns the first placement that keeps the tooltip fully
|
|
29
|
+
* inside the viewport. Falls back to the primary when nothing fits.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolvePlacement({ triggerRect, tooltipWidth, tooltipHeight, placement, arrowPosition, fallbackPlacements, }: ResolvePlacementParams): TooltipPlacement;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getDefaultFallbackPlacements = getDefaultFallbackPlacements;
|
|
4
|
+
exports.calcAnchorPoint = calcAnchorPoint;
|
|
5
|
+
exports.fitsInViewport = fitsInViewport;
|
|
6
|
+
exports.resolvePlacement = resolvePlacement;
|
|
7
|
+
const PLACEMENT_MARGIN = 10;
|
|
8
|
+
const COUNTER_CLOCKWISE = ['top', 'left', 'bottom', 'right'];
|
|
9
|
+
function getDefaultFallbackPlacements(placement) {
|
|
10
|
+
const index = COUNTER_CLOCKWISE.indexOf(placement);
|
|
11
|
+
const result = [];
|
|
12
|
+
for (let i = 1; i < COUNTER_CLOCKWISE.length; i++) {
|
|
13
|
+
result.push(COUNTER_CLOCKWISE[(index + i) % COUNTER_CLOCKWISE.length]);
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
function calcAnchorPoint(triggerRect, placement, arrowPosition) {
|
|
18
|
+
const horizontalLeft = () => arrowPosition === 'left'
|
|
19
|
+
? triggerRect.left - 24
|
|
20
|
+
: arrowPosition === 'right'
|
|
21
|
+
? triggerRect.right + 24
|
|
22
|
+
: triggerRect.left + triggerRect.width / 2;
|
|
23
|
+
const verticalTop = () => triggerRect.top + triggerRect.height / 2;
|
|
24
|
+
switch (placement) {
|
|
25
|
+
case 'top':
|
|
26
|
+
return { top: triggerRect.top, left: horizontalLeft() };
|
|
27
|
+
case 'bottom':
|
|
28
|
+
return { top: triggerRect.bottom, left: horizontalLeft() };
|
|
29
|
+
case 'left':
|
|
30
|
+
return { top: verticalTop(), left: triggerRect.left };
|
|
31
|
+
case 'right':
|
|
32
|
+
return { top: verticalTop(), left: triggerRect.right };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function fitsInViewport({ anchor, tooltipWidth, tooltipHeight, placement, arrowPosition, }) {
|
|
36
|
+
const horizontalLeft = () => arrowPosition === 'left'
|
|
37
|
+
? anchor.left
|
|
38
|
+
: arrowPosition === 'right'
|
|
39
|
+
? anchor.left - tooltipWidth
|
|
40
|
+
: anchor.left - tooltipWidth / 2;
|
|
41
|
+
const verticalTop = () => anchor.top - tooltipHeight / 2;
|
|
42
|
+
let top;
|
|
43
|
+
let left;
|
|
44
|
+
switch (placement) {
|
|
45
|
+
case 'top':
|
|
46
|
+
top = anchor.top - tooltipHeight - PLACEMENT_MARGIN;
|
|
47
|
+
left = horizontalLeft();
|
|
48
|
+
break;
|
|
49
|
+
case 'bottom':
|
|
50
|
+
top = anchor.top + PLACEMENT_MARGIN;
|
|
51
|
+
left = horizontalLeft();
|
|
52
|
+
break;
|
|
53
|
+
case 'left':
|
|
54
|
+
top = verticalTop();
|
|
55
|
+
left = anchor.left - tooltipWidth - PLACEMENT_MARGIN;
|
|
56
|
+
break;
|
|
57
|
+
case 'right':
|
|
58
|
+
top = verticalTop();
|
|
59
|
+
left = anchor.left + PLACEMENT_MARGIN;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
return (top >= 0 &&
|
|
63
|
+
left >= 0 &&
|
|
64
|
+
left + tooltipWidth <= window.innerWidth &&
|
|
65
|
+
top + tooltipHeight <= window.innerHeight);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Given the trigger rect, tooltip dimensions, primary placement/arrow, and
|
|
69
|
+
* fallback list, returns the first placement that keeps the tooltip fully
|
|
70
|
+
* inside the viewport. Falls back to the primary when nothing fits.
|
|
71
|
+
*/
|
|
72
|
+
function resolvePlacement({ triggerRect, tooltipWidth, tooltipHeight, placement, arrowPosition, fallbackPlacements, }) {
|
|
73
|
+
if (!(fallbackPlacements === null || fallbackPlacements === void 0 ? void 0 : fallbackPlacements.length) || tooltipWidth === 0 || tooltipHeight === 0) {
|
|
74
|
+
return placement;
|
|
75
|
+
}
|
|
76
|
+
const candidates = [placement, ...fallbackPlacements];
|
|
77
|
+
for (const candidate of candidates) {
|
|
78
|
+
const candidateArrow = candidate === placement ? arrowPosition : 'center';
|
|
79
|
+
const pos = calcAnchorPoint(triggerRect, candidate, candidateArrow);
|
|
80
|
+
if (fitsInViewport({
|
|
81
|
+
anchor: pos,
|
|
82
|
+
tooltipWidth,
|
|
83
|
+
tooltipHeight,
|
|
84
|
+
placement: candidate,
|
|
85
|
+
arrowPosition: candidateArrow,
|
|
86
|
+
})) {
|
|
87
|
+
return candidate;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return placement;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=tooltip-placement.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redocly/theme",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.65.0-next.0",
|
|
4
4
|
"description": "Shared UI components lib",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"theme",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"vitest": "4.0.10",
|
|
64
64
|
"vitest-when": "0.6.2",
|
|
65
65
|
"webpack": "5.105.2",
|
|
66
|
-
"@redocly/realm-asyncapi-sdk": "0.
|
|
66
|
+
"@redocly/realm-asyncapi-sdk": "0.11.0-next.0"
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"@tanstack/react-query": "5.62.3",
|
|
@@ -4,7 +4,11 @@ import styled, { css } from 'styled-components';
|
|
|
4
4
|
import type { JSX, PropsWithChildren } from 'react';
|
|
5
5
|
import type { TooltipProps } from '@redocly/theme/core/types';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
useControl,
|
|
9
|
+
useOutsideClick,
|
|
10
|
+
useTooltipFallbackPlacement,
|
|
11
|
+
} from '@redocly/theme/core/hooks';
|
|
8
12
|
import { Portal } from '@redocly/theme/components/Portal/Portal';
|
|
9
13
|
|
|
10
14
|
type Props = Exclude<TooltipProps, 'arrowPosition'> & {
|
|
@@ -17,6 +21,7 @@ function TooltipComponent({
|
|
|
17
21
|
tip,
|
|
18
22
|
withArrow = true,
|
|
19
23
|
placement = 'top',
|
|
24
|
+
fallbackPlacements,
|
|
20
25
|
className = 'default',
|
|
21
26
|
width,
|
|
22
27
|
dataTestId,
|
|
@@ -29,6 +34,14 @@ function TooltipComponent({
|
|
|
29
34
|
const { isOpened, handleOpen, handleClose } = useControl(isOpen);
|
|
30
35
|
const anchorName = `--tooltip${useId().replace(/:/g, '')}`;
|
|
31
36
|
|
|
37
|
+
const { activePlacement, activeArrowPosition } = useTooltipFallbackPlacement({
|
|
38
|
+
isOpened,
|
|
39
|
+
placement,
|
|
40
|
+
arrowPosition,
|
|
41
|
+
fallbackPlacements,
|
|
42
|
+
tooltipBodyRef,
|
|
43
|
+
});
|
|
44
|
+
|
|
32
45
|
useOutsideClick(isOpened ? [tooltipWrapperRef, tooltipBodyRef] : tooltipWrapperRef, handleClose);
|
|
33
46
|
|
|
34
47
|
const isControlled = isOpen !== undefined;
|
|
@@ -71,10 +84,14 @@ function TooltipComponent({
|
|
|
71
84
|
<TooltipBody
|
|
72
85
|
ref={tooltipBodyRef}
|
|
73
86
|
data-testid={dataTestId || (typeof tip === 'string' ? tip : '')}
|
|
74
|
-
placement={
|
|
87
|
+
placement={activePlacement}
|
|
75
88
|
width={width}
|
|
76
89
|
withArrow={withArrow}
|
|
77
|
-
arrowPosition={
|
|
90
|
+
arrowPosition={
|
|
91
|
+
activeArrowPosition === 'left' || activeArrowPosition === 'right'
|
|
92
|
+
? activeArrowPosition
|
|
93
|
+
: 'center'
|
|
94
|
+
}
|
|
78
95
|
anchorName={anchorName}
|
|
79
96
|
>
|
|
80
97
|
{tip}
|
|
@@ -6,6 +6,7 @@ import type { TooltipProps } from '@redocly/theme/core/types';
|
|
|
6
6
|
|
|
7
7
|
import { useControl, useOutsideClick } from '@redocly/theme/core/hooks';
|
|
8
8
|
import { Portal } from '@redocly/theme/components/Portal/Portal';
|
|
9
|
+
import { calcAnchorPoint, resolvePlacement } from '@redocly/theme/core/utils';
|
|
9
10
|
|
|
10
11
|
function TooltipComponent({
|
|
11
12
|
children,
|
|
@@ -13,6 +14,7 @@ function TooltipComponent({
|
|
|
13
14
|
tip,
|
|
14
15
|
withArrow = true,
|
|
15
16
|
placement = 'top',
|
|
17
|
+
fallbackPlacements,
|
|
16
18
|
className = 'default',
|
|
17
19
|
width,
|
|
18
20
|
dataTestId,
|
|
@@ -20,58 +22,41 @@ function TooltipComponent({
|
|
|
20
22
|
arrowPosition = 'center',
|
|
21
23
|
onClick,
|
|
22
24
|
}: PropsWithChildren<TooltipProps>): JSX.Element {
|
|
23
|
-
const
|
|
25
|
+
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
|
26
|
+
const tooltipBodyRef = useRef<HTMLSpanElement | null>(null);
|
|
24
27
|
const { isOpened, handleOpen, handleClose } = useControl(isOpen);
|
|
25
28
|
const [tooltipPosition, setTooltipPosition] = useState({ top: 0, left: 0 });
|
|
29
|
+
const [activePlacement, setActivePlacement] = useState(placement);
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
const activeArrowPosition = activePlacement === placement ? arrowPosition : 'center';
|
|
32
|
+
|
|
33
|
+
useOutsideClick(wrapperRef, handleClose);
|
|
28
34
|
|
|
29
35
|
const isControlled = isOpen !== undefined;
|
|
30
36
|
|
|
31
|
-
const updateTooltipPosition = useCallback(() => {
|
|
32
|
-
if (isOpened
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (arrowPosition === 'left') {
|
|
52
|
-
left = rect.left - 24;
|
|
53
|
-
} else if (arrowPosition === 'right') {
|
|
54
|
-
left = rect.right + 24;
|
|
55
|
-
} else {
|
|
56
|
-
left = rect.left + rect.width / 2;
|
|
57
|
-
}
|
|
58
|
-
break;
|
|
59
|
-
case 'left':
|
|
60
|
-
top = rect.top + rect.height / 2;
|
|
61
|
-
left = rect.left;
|
|
62
|
-
break;
|
|
63
|
-
case 'right':
|
|
64
|
-
top = rect.top + rect.height / 2;
|
|
65
|
-
left = rect.right;
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
setTooltipPosition({ top, left });
|
|
70
|
-
}
|
|
71
|
-
}, [isOpened, placement, arrowPosition]);
|
|
37
|
+
const updateTooltipPosition = useCallback((): void => {
|
|
38
|
+
if (!isOpened || !wrapperRef.current) return;
|
|
39
|
+
|
|
40
|
+
const triggerRect = wrapperRef.current.getBoundingClientRect();
|
|
41
|
+
const tooltipWidth = tooltipBodyRef.current?.offsetWidth ?? 0;
|
|
42
|
+
const tooltipHeight = tooltipBodyRef.current?.offsetHeight ?? 0;
|
|
43
|
+
|
|
44
|
+
const resolved = resolvePlacement({
|
|
45
|
+
triggerRect,
|
|
46
|
+
tooltipWidth,
|
|
47
|
+
tooltipHeight,
|
|
48
|
+
placement,
|
|
49
|
+
arrowPosition,
|
|
50
|
+
fallbackPlacements,
|
|
51
|
+
});
|
|
52
|
+
const resolvedArrow = resolved === placement ? arrowPosition : 'center';
|
|
53
|
+
|
|
54
|
+
setTooltipPosition(calcAnchorPoint(triggerRect, resolved, resolvedArrow));
|
|
55
|
+
setActivePlacement(resolved);
|
|
56
|
+
}, [isOpened, placement, arrowPosition, fallbackPlacements]);
|
|
72
57
|
|
|
73
58
|
useLayoutEffect(() => {
|
|
74
|
-
if (isOpened &&
|
|
59
|
+
if (isOpened && wrapperRef.current) {
|
|
75
60
|
updateTooltipPosition();
|
|
76
61
|
|
|
77
62
|
const handleScroll = () => updateTooltipPosition();
|
|
@@ -109,7 +94,7 @@ function TooltipComponent({
|
|
|
109
94
|
|
|
110
95
|
return (
|
|
111
96
|
<TooltipWrapper
|
|
112
|
-
ref={
|
|
97
|
+
ref={wrapperRef}
|
|
113
98
|
{...controllers}
|
|
114
99
|
className={`tooltip-${className}`}
|
|
115
100
|
data-component-name="Tooltip/Tooltip"
|
|
@@ -118,11 +103,12 @@ function TooltipComponent({
|
|
|
118
103
|
{isOpened && !disabled && (
|
|
119
104
|
<Portal>
|
|
120
105
|
<TooltipBody
|
|
106
|
+
ref={tooltipBodyRef}
|
|
121
107
|
data-testid={dataTestId || (typeof tip === 'string' ? tip : '')}
|
|
122
|
-
placement={
|
|
108
|
+
placement={activePlacement}
|
|
123
109
|
width={width}
|
|
124
110
|
withArrow={withArrow}
|
|
125
|
-
arrowPosition={
|
|
111
|
+
arrowPosition={activeArrowPosition}
|
|
126
112
|
style={{
|
|
127
113
|
position: 'fixed',
|
|
128
114
|
top: tooltipPosition.top,
|
|
@@ -255,7 +241,9 @@ const TooltipWrapper = styled.div`
|
|
|
255
241
|
display: flex;
|
|
256
242
|
`;
|
|
257
243
|
const TooltipBody = styled.span<
|
|
258
|
-
Pick<Required<TooltipProps>, 'placement' | 'withArrow' | 'arrowPosition'> & {
|
|
244
|
+
Pick<Required<TooltipProps>, 'placement' | 'withArrow' | 'arrowPosition'> & {
|
|
245
|
+
width?: string;
|
|
246
|
+
}
|
|
259
247
|
>`
|
|
260
248
|
display: inline-block;
|
|
261
249
|
|
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
import React, { memo } from 'react';
|
|
2
2
|
|
|
3
|
+
import type { TooltipProps } from '@redocly/theme/core/types';
|
|
4
|
+
|
|
3
5
|
import { Tooltip as AnchorTooltip } from '@redocly/theme/components/Tooltip/AnchorTooltip';
|
|
4
6
|
import { Tooltip as JsTooltip } from '@redocly/theme/components/Tooltip/JsTooltip';
|
|
5
|
-
import { TooltipProps } from '@redocly/theme/core/types';
|
|
6
7
|
import { useThemeHooks } from '@redocly/theme/core/hooks';
|
|
8
|
+
import { getDefaultFallbackPlacements } from '@redocly/theme/core/utils';
|
|
7
9
|
|
|
8
10
|
function TooltipComponent(props: TooltipProps): React.ReactElement {
|
|
9
11
|
const { useAnchorPositioning } = useThemeHooks();
|
|
10
12
|
const { isSupported } = useAnchorPositioning();
|
|
11
13
|
|
|
14
|
+
const fallbackPlacements =
|
|
15
|
+
props.fallbackPlacements ?? getDefaultFallbackPlacements(props.placement ?? 'top');
|
|
16
|
+
|
|
12
17
|
if (isSupported) {
|
|
13
|
-
return
|
|
18
|
+
return (
|
|
19
|
+
<AnchorTooltip
|
|
20
|
+
{...props}
|
|
21
|
+
fallbackPlacements={fallbackPlacements}
|
|
22
|
+
arrowPosition={prepareArrowPosition(props.arrowPosition)}
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
14
25
|
}
|
|
15
|
-
return <JsTooltip {...props} />;
|
|
26
|
+
return <JsTooltip {...props} fallbackPlacements={fallbackPlacements} />;
|
|
16
27
|
}
|
|
17
28
|
|
|
18
29
|
export const Tooltip = memo<TooltipProps>(TooltipComponent);
|
package/src/core/hooks/index.ts
CHANGED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { RefObject } from 'react';
|
|
4
|
+
import type { TooltipPlacement, TooltipProps } from '@redocly/theme/core/types';
|
|
5
|
+
|
|
6
|
+
type TooltipFallbackPlacementParams = {
|
|
7
|
+
isOpened: boolean;
|
|
8
|
+
placement: TooltipPlacement;
|
|
9
|
+
arrowPosition: TooltipProps['arrowPosition'];
|
|
10
|
+
fallbackPlacements: TooltipPlacement[] | undefined;
|
|
11
|
+
tooltipBodyRef: RefObject<HTMLElement | null>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type TooltipFallbackPlacementResult = {
|
|
15
|
+
activePlacement: TooltipPlacement;
|
|
16
|
+
activeArrowPosition: TooltipProps['arrowPosition'];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function useTooltipFallbackPlacement({
|
|
20
|
+
isOpened,
|
|
21
|
+
placement,
|
|
22
|
+
arrowPosition,
|
|
23
|
+
fallbackPlacements,
|
|
24
|
+
tooltipBodyRef,
|
|
25
|
+
}: TooltipFallbackPlacementParams): TooltipFallbackPlacementResult {
|
|
26
|
+
const [activePlacement, setActivePlacement] = useState<TooltipPlacement>(placement);
|
|
27
|
+
const wasOpenRef = useRef(false);
|
|
28
|
+
const candidateIndexRef = useRef(0);
|
|
29
|
+
|
|
30
|
+
useLayoutEffect(() => {
|
|
31
|
+
if (!isOpened) {
|
|
32
|
+
wasOpenRef.current = false;
|
|
33
|
+
candidateIndexRef.current = 0;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!wasOpenRef.current) {
|
|
38
|
+
wasOpenRef.current = true;
|
|
39
|
+
candidateIndexRef.current = 0;
|
|
40
|
+
if (activePlacement !== placement) {
|
|
41
|
+
setActivePlacement(placement);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!tooltipBodyRef.current || !fallbackPlacements?.length) return;
|
|
47
|
+
|
|
48
|
+
const candidates: TooltipPlacement[] = [placement, ...fallbackPlacements];
|
|
49
|
+
|
|
50
|
+
if (candidateIndexRef.current >= candidates.length) return;
|
|
51
|
+
|
|
52
|
+
const rect = tooltipBodyRef.current.getBoundingClientRect();
|
|
53
|
+
const overflows =
|
|
54
|
+
rect.left < 0 ||
|
|
55
|
+
rect.top < 0 ||
|
|
56
|
+
rect.right > window.innerWidth ||
|
|
57
|
+
rect.bottom > window.innerHeight;
|
|
58
|
+
|
|
59
|
+
if (!overflows) return;
|
|
60
|
+
|
|
61
|
+
candidateIndexRef.current++;
|
|
62
|
+
|
|
63
|
+
if (candidateIndexRef.current < candidates.length) {
|
|
64
|
+
setActivePlacement(candidates[candidateIndexRef.current]);
|
|
65
|
+
} else if (activePlacement !== placement) {
|
|
66
|
+
setActivePlacement(placement);
|
|
67
|
+
}
|
|
68
|
+
}, [isOpened, activePlacement, placement, fallbackPlacements, tooltipBodyRef]);
|
|
69
|
+
|
|
70
|
+
const activeArrowPosition = activePlacement === placement ? arrowPosition : 'center';
|
|
71
|
+
|
|
72
|
+
return { activePlacement, activeArrowPosition };
|
|
73
|
+
}
|
|
@@ -1253,6 +1253,7 @@ const replay = css`
|
|
|
1253
1253
|
--replay-runtime-expression-color: rgba(54, 90, 249, 1); // @presenter Color
|
|
1254
1254
|
--replay-runtime-expression-bg-color: rgba(54, 90, 249, 0.08); // @presenter Color
|
|
1255
1255
|
--replay-operators-color: rgba(193, 142, 31, 1); // @presenter Color
|
|
1256
|
+
--replay-claude-icon-color: rgba(217, 119, 87, 1); // @presenter Color
|
|
1256
1257
|
|
|
1257
1258
|
--replay-ai-gradient-soft: linear-gradient(62.6deg, rgba(113, 94, 254, 0.16) 0%, rgba(255, 92, 220, 0.16) 100%);
|
|
1258
1259
|
--replay-ai-gradient-disabled: linear-gradient(62.6deg, rgba(113, 94, 254, 0.6) 0%, rgba(255, 92, 220, 0.6) 100%);
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
|
|
3
|
+
export type TooltipPlacement = 'top' | 'bottom' | 'left' | 'right';
|
|
4
|
+
|
|
3
5
|
export type TooltipProps = {
|
|
4
6
|
children?: ReactNode;
|
|
5
7
|
tip: string | ReactNode;
|
|
6
8
|
isOpen?: boolean;
|
|
7
9
|
withArrow?: boolean;
|
|
8
|
-
placement?:
|
|
10
|
+
placement?: TooltipPlacement;
|
|
11
|
+
fallbackPlacements?: TooltipPlacement[];
|
|
9
12
|
className?: string;
|
|
10
13
|
width?: string;
|
|
11
14
|
dataTestId?: string;
|
package/src/core/utils/index.ts
CHANGED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { TooltipPlacement, TooltipProps } from '@redocly/theme/core/types';
|
|
2
|
+
|
|
3
|
+
const PLACEMENT_MARGIN = 10;
|
|
4
|
+
const COUNTER_CLOCKWISE: TooltipPlacement[] = ['top', 'left', 'bottom', 'right'];
|
|
5
|
+
|
|
6
|
+
export function getDefaultFallbackPlacements(placement: TooltipPlacement): TooltipPlacement[] {
|
|
7
|
+
const index = COUNTER_CLOCKWISE.indexOf(placement);
|
|
8
|
+
const result: TooltipPlacement[] = [];
|
|
9
|
+
for (let i = 1; i < COUNTER_CLOCKWISE.length; i++) {
|
|
10
|
+
result.push(COUNTER_CLOCKWISE[(index + i) % COUNTER_CLOCKWISE.length]);
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function calcAnchorPoint(
|
|
16
|
+
triggerRect: DOMRect,
|
|
17
|
+
placement: TooltipPlacement,
|
|
18
|
+
arrowPosition: TooltipProps['arrowPosition'],
|
|
19
|
+
): { top: number; left: number } {
|
|
20
|
+
const horizontalLeft = (): number =>
|
|
21
|
+
arrowPosition === 'left'
|
|
22
|
+
? triggerRect.left - 24
|
|
23
|
+
: arrowPosition === 'right'
|
|
24
|
+
? triggerRect.right + 24
|
|
25
|
+
: triggerRect.left + triggerRect.width / 2;
|
|
26
|
+
|
|
27
|
+
const verticalTop = (): number => triggerRect.top + triggerRect.height / 2;
|
|
28
|
+
|
|
29
|
+
switch (placement) {
|
|
30
|
+
case 'top':
|
|
31
|
+
return { top: triggerRect.top, left: horizontalLeft() };
|
|
32
|
+
case 'bottom':
|
|
33
|
+
return { top: triggerRect.bottom, left: horizontalLeft() };
|
|
34
|
+
case 'left':
|
|
35
|
+
return { top: verticalTop(), left: triggerRect.left };
|
|
36
|
+
case 'right':
|
|
37
|
+
return { top: verticalTop(), left: triggerRect.right };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type FitsInViewportParams = {
|
|
42
|
+
anchor: { top: number; left: number };
|
|
43
|
+
tooltipWidth: number;
|
|
44
|
+
tooltipHeight: number;
|
|
45
|
+
placement: TooltipPlacement;
|
|
46
|
+
arrowPosition: TooltipProps['arrowPosition'];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export function fitsInViewport({
|
|
50
|
+
anchor,
|
|
51
|
+
tooltipWidth,
|
|
52
|
+
tooltipHeight,
|
|
53
|
+
placement,
|
|
54
|
+
arrowPosition,
|
|
55
|
+
}: FitsInViewportParams): boolean {
|
|
56
|
+
const horizontalLeft = (): number =>
|
|
57
|
+
arrowPosition === 'left'
|
|
58
|
+
? anchor.left
|
|
59
|
+
: arrowPosition === 'right'
|
|
60
|
+
? anchor.left - tooltipWidth
|
|
61
|
+
: anchor.left - tooltipWidth / 2;
|
|
62
|
+
|
|
63
|
+
const verticalTop = (): number => anchor.top - tooltipHeight / 2;
|
|
64
|
+
|
|
65
|
+
let top: number;
|
|
66
|
+
let left: number;
|
|
67
|
+
|
|
68
|
+
switch (placement) {
|
|
69
|
+
case 'top':
|
|
70
|
+
top = anchor.top - tooltipHeight - PLACEMENT_MARGIN;
|
|
71
|
+
left = horizontalLeft();
|
|
72
|
+
break;
|
|
73
|
+
case 'bottom':
|
|
74
|
+
top = anchor.top + PLACEMENT_MARGIN;
|
|
75
|
+
left = horizontalLeft();
|
|
76
|
+
break;
|
|
77
|
+
case 'left':
|
|
78
|
+
top = verticalTop();
|
|
79
|
+
left = anchor.left - tooltipWidth - PLACEMENT_MARGIN;
|
|
80
|
+
break;
|
|
81
|
+
case 'right':
|
|
82
|
+
top = verticalTop();
|
|
83
|
+
left = anchor.left + PLACEMENT_MARGIN;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
top >= 0 &&
|
|
89
|
+
left >= 0 &&
|
|
90
|
+
left + tooltipWidth <= window.innerWidth &&
|
|
91
|
+
top + tooltipHeight <= window.innerHeight
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type ResolvePlacementParams = {
|
|
96
|
+
triggerRect: DOMRect;
|
|
97
|
+
tooltipWidth: number;
|
|
98
|
+
tooltipHeight: number;
|
|
99
|
+
placement: TooltipPlacement;
|
|
100
|
+
arrowPosition: TooltipProps['arrowPosition'];
|
|
101
|
+
fallbackPlacements: TooltipPlacement[] | undefined;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Given the trigger rect, tooltip dimensions, primary placement/arrow, and
|
|
106
|
+
* fallback list, returns the first placement that keeps the tooltip fully
|
|
107
|
+
* inside the viewport. Falls back to the primary when nothing fits.
|
|
108
|
+
*/
|
|
109
|
+
export function resolvePlacement({
|
|
110
|
+
triggerRect,
|
|
111
|
+
tooltipWidth,
|
|
112
|
+
tooltipHeight,
|
|
113
|
+
placement,
|
|
114
|
+
arrowPosition,
|
|
115
|
+
fallbackPlacements,
|
|
116
|
+
}: ResolvePlacementParams): TooltipPlacement {
|
|
117
|
+
if (!fallbackPlacements?.length || tooltipWidth === 0 || tooltipHeight === 0) {
|
|
118
|
+
return placement;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const candidates: TooltipPlacement[] = [placement, ...fallbackPlacements];
|
|
122
|
+
|
|
123
|
+
for (const candidate of candidates) {
|
|
124
|
+
const candidateArrow = candidate === placement ? arrowPosition : 'center';
|
|
125
|
+
const pos = calcAnchorPoint(triggerRect, candidate, candidateArrow);
|
|
126
|
+
|
|
127
|
+
if (
|
|
128
|
+
fitsInViewport({
|
|
129
|
+
anchor: pos,
|
|
130
|
+
tooltipWidth,
|
|
131
|
+
tooltipHeight,
|
|
132
|
+
placement: candidate,
|
|
133
|
+
arrowPosition: candidateArrow,
|
|
134
|
+
})
|
|
135
|
+
) {
|
|
136
|
+
return candidate;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return placement;
|
|
141
|
+
}
|