@vuu-ui/vuu-popups 2.1.18 → 2.1.19-beta.2
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/package.json +10 -13
- package/src/index.js +4 -0
- package/src/popup/Popup.js +29 -0
- package/src/popup/getPositionRelativeToAnchor.js +47 -0
- package/src/popup/index.js +2 -0
- package/src/popup/useAnchoredPosition.js +41 -0
- package/src/popup-menu/PopupMenu.css.js +11 -0
- package/src/popup-menu/PopupMenu.js +67 -0
- package/src/popup-menu/index.js +1 -0
- package/src/popup-menu/usePopupMenu.js +72 -0
- package/src/portal/Portal.css.js +27 -0
- package/src/portal/Portal.js +62 -0
- package/src/portal/index.js +1 -0
- package/src/tooltip/Tooltip.css.js +76 -0
- package/src/tooltip/Tooltip.js +41 -0
- package/src/tooltip/index.js +2 -0
- package/src/tooltip/useTooltip.js +91 -0
- package/src/tooltip/useTooltipAnchoredPosition.js +102 -0
- package/cjs/index.js +0 -18
- package/cjs/index.js.map +0 -1
- package/cjs/popup/Popup.css.js +0 -6
- package/cjs/popup/Popup.css.js.map +0 -1
- package/cjs/popup/Popup.js +0 -38
- package/cjs/popup/Popup.js.map +0 -1
- package/cjs/popup/getPositionRelativeToAnchor.js +0 -43
- package/cjs/popup/getPositionRelativeToAnchor.js.map +0 -1
- package/cjs/popup/useAnchoredPosition.js +0 -58
- package/cjs/popup/useAnchoredPosition.js.map +0 -1
- package/cjs/popup-menu/PopupMenu.css.js +0 -6
- package/cjs/popup-menu/PopupMenu.css.js.map +0 -1
- package/cjs/popup-menu/PopupMenu.js +0 -100
- package/cjs/popup-menu/PopupMenu.js.map +0 -1
- package/cjs/popup-menu/usePopupMenu.js +0 -100
- package/cjs/popup-menu/usePopupMenu.js.map +0 -1
- package/cjs/portal/Portal.css.js +0 -6
- package/cjs/portal/Portal.css.js.map +0 -1
- package/cjs/portal/Portal.js +0 -73
- package/cjs/portal/Portal.js.map +0 -1
- package/cjs/tooltip/Tooltip.css.js +0 -6
- package/cjs/tooltip/Tooltip.css.js.map +0 -1
- package/cjs/tooltip/Tooltip.js +0 -56
- package/cjs/tooltip/Tooltip.js.map +0 -1
- package/cjs/tooltip/useTooltip.js +0 -106
- package/cjs/tooltip/useTooltip.js.map +0 -1
- package/cjs/tooltip/useTooltipAnchoredPosition.js +0 -117
- package/cjs/tooltip/useTooltipAnchoredPosition.js.map +0 -1
- package/esm/index.js +0 -7
- package/esm/index.js.map +0 -1
- package/esm/popup/Popup.css.js +0 -4
- package/esm/popup/Popup.css.js.map +0 -1
- package/esm/popup/Popup.js +0 -36
- package/esm/popup/Popup.js.map +0 -1
- package/esm/popup/getPositionRelativeToAnchor.js +0 -41
- package/esm/popup/getPositionRelativeToAnchor.js.map +0 -1
- package/esm/popup/useAnchoredPosition.js +0 -56
- package/esm/popup/useAnchoredPosition.js.map +0 -1
- package/esm/popup-menu/PopupMenu.css.js +0 -4
- package/esm/popup-menu/PopupMenu.css.js.map +0 -1
- package/esm/popup-menu/PopupMenu.js +0 -98
- package/esm/popup-menu/PopupMenu.js.map +0 -1
- package/esm/popup-menu/usePopupMenu.js +0 -98
- package/esm/popup-menu/usePopupMenu.js.map +0 -1
- package/esm/portal/Portal.css.js +0 -4
- package/esm/portal/Portal.css.js.map +0 -1
- package/esm/portal/Portal.js +0 -71
- package/esm/portal/Portal.js.map +0 -1
- package/esm/tooltip/Tooltip.css.js +0 -4
- package/esm/tooltip/Tooltip.css.js.map +0 -1
- package/esm/tooltip/Tooltip.js +0 -54
- package/esm/tooltip/Tooltip.js.map +0 -1
- package/esm/tooltip/useTooltip.js +0 -104
- package/esm/tooltip/useTooltip.js.map +0 -1
- package/esm/tooltip/useTooltipAnchoredPosition.js +0 -115
- package/esm/tooltip/useTooltipAnchoredPosition.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.1.
|
|
2
|
+
"version": "2.1.19-beta.2",
|
|
3
3
|
"description": "VUU popup components - Context Menu, Dialog etc",
|
|
4
4
|
"author": "heswell",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
6
7
|
"dependencies": {
|
|
7
8
|
"@salt-ds/core": "1.54.1",
|
|
8
9
|
"@salt-ds/styles": "0.2.1",
|
|
9
10
|
"@salt-ds/window": "0.1.1",
|
|
10
|
-
"@vuu-ui/vuu-context-menu": "2.1.
|
|
11
|
-
"@vuu-ui/vuu-data-types": "2.1.
|
|
12
|
-
"@vuu-ui/vuu-layout": "2.1.
|
|
13
|
-
"@vuu-ui/vuu-utils": "2.1.
|
|
14
|
-
"@vuu-ui/vuu-ui-controls": "2.1.
|
|
11
|
+
"@vuu-ui/vuu-context-menu": "2.1.19-beta.2",
|
|
12
|
+
"@vuu-ui/vuu-data-types": "2.1.19-beta.2",
|
|
13
|
+
"@vuu-ui/vuu-layout": "2.1.19-beta.2",
|
|
14
|
+
"@vuu-ui/vuu-utils": "2.1.19-beta.2",
|
|
15
|
+
"@vuu-ui/vuu-ui-controls": "2.1.19-beta.2"
|
|
15
16
|
},
|
|
16
17
|
"peerDependencies": {
|
|
17
18
|
"clsx": "^2.0.0",
|
|
@@ -21,20 +22,16 @@
|
|
|
21
22
|
"sideEffects": false,
|
|
22
23
|
"files": [
|
|
23
24
|
"README.md",
|
|
24
|
-
"
|
|
25
|
-
"cjs",
|
|
25
|
+
"src",
|
|
26
26
|
"/types"
|
|
27
27
|
],
|
|
28
28
|
"exports": {
|
|
29
29
|
".": {
|
|
30
|
-
"
|
|
31
|
-
"import": "./esm/index.js",
|
|
30
|
+
"import": "./src/index.js",
|
|
32
31
|
"types": "./types/index.d.ts"
|
|
33
32
|
}
|
|
34
33
|
},
|
|
35
|
-
"
|
|
36
|
-
"module": "esm/index.js",
|
|
34
|
+
"module": "index.js",
|
|
37
35
|
"name": "@vuu-ui/vuu-popups",
|
|
38
|
-
"type": "module",
|
|
39
36
|
"types": "types/index.d.ts"
|
|
40
37
|
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { useComponentCssInjection } from "@salt-ds/styles";
|
|
4
|
+
import { useWindow } from "@salt-ds/window";
|
|
5
|
+
import { useAnchoredPosition } from "./useAnchoredPosition.js";
|
|
6
|
+
import Popup from "./Popup.css";
|
|
7
|
+
const PopupComponent = ({ children, className, anchorElement, minWidth, offsetLeft, offsetTop, placement, position: positionProp })=>{
|
|
8
|
+
const targetWindow = useWindow();
|
|
9
|
+
useComponentCssInjection({
|
|
10
|
+
testId: "vuu-popup",
|
|
11
|
+
css: Popup,
|
|
12
|
+
window: targetWindow
|
|
13
|
+
});
|
|
14
|
+
const { popupRef, position } = useAnchoredPosition({
|
|
15
|
+
anchorElement,
|
|
16
|
+
minWidth,
|
|
17
|
+
offsetLeft,
|
|
18
|
+
offsetTop,
|
|
19
|
+
placement,
|
|
20
|
+
position: positionProp
|
|
21
|
+
});
|
|
22
|
+
return void 0 === position ? null : /*#__PURE__*/ jsx("div", {
|
|
23
|
+
className: clsx("vuuPortal", className),
|
|
24
|
+
ref: popupRef,
|
|
25
|
+
style: position,
|
|
26
|
+
children: children
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
export { PopupComponent };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const getPositionRelativeToAnchor = (anchorElement, placement, offsetLeft, offsetTop, minWidth, dimensions)=>{
|
|
2
|
+
const { bottom, height, left, right, top, width } = anchorElement.getBoundingClientRect();
|
|
3
|
+
switch(placement){
|
|
4
|
+
case "below":
|
|
5
|
+
return {
|
|
6
|
+
left: left + offsetLeft,
|
|
7
|
+
top: bottom + offsetTop
|
|
8
|
+
};
|
|
9
|
+
case "right":
|
|
10
|
+
return {
|
|
11
|
+
left: right + offsetLeft,
|
|
12
|
+
top: top + offsetTop
|
|
13
|
+
};
|
|
14
|
+
case "below-center":
|
|
15
|
+
return {
|
|
16
|
+
left: left + width / 2 + offsetLeft,
|
|
17
|
+
top: bottom + offsetTop
|
|
18
|
+
};
|
|
19
|
+
case "below-right":
|
|
20
|
+
return {
|
|
21
|
+
left: left,
|
|
22
|
+
minWidth,
|
|
23
|
+
top: bottom + offsetTop
|
|
24
|
+
};
|
|
25
|
+
case "below-full-width":
|
|
26
|
+
return {
|
|
27
|
+
left: left + offsetLeft,
|
|
28
|
+
minWidth,
|
|
29
|
+
top: bottom + offsetTop,
|
|
30
|
+
width
|
|
31
|
+
};
|
|
32
|
+
case "center":
|
|
33
|
+
if (dimensions) return {
|
|
34
|
+
left: width / 2 - dimensions.width / 2 + offsetLeft,
|
|
35
|
+
top: height / 2 - dimensions.height / 2 + offsetTop,
|
|
36
|
+
visibility: "visible"
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
left: width / 2 + offsetLeft,
|
|
40
|
+
top: height / 2 + offsetTop,
|
|
41
|
+
visibility: "hidden"
|
|
42
|
+
};
|
|
43
|
+
default:
|
|
44
|
+
throw Error(`Popup getPositionRelativeToAnchor non-supported placement value ${placement}`);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
export { getPositionRelativeToAnchor };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useCallback, useLayoutEffect, useRef, useState } from "react";
|
|
2
|
+
import { getPositionRelativeToAnchor } from "./getPositionRelativeToAnchor.js";
|
|
3
|
+
const useAnchoredPosition = ({ anchorElement, minWidth, offsetLeft = 0, offsetTop = 0, placement, position: positionProp })=>{
|
|
4
|
+
const popupRef = useRef(null);
|
|
5
|
+
const [position, setPosition] = useState(positionProp);
|
|
6
|
+
useLayoutEffect(()=>{
|
|
7
|
+
if ("absolute" === placement && positionProp) setPosition(positionProp);
|
|
8
|
+
else if (anchorElement.current && "auto" !== placement) {
|
|
9
|
+
const dimensions = null === popupRef.current ? void 0 : popupRef.current.getBoundingClientRect();
|
|
10
|
+
const position = getPositionRelativeToAnchor(anchorElement.current, placement, offsetLeft, offsetTop, minWidth, dimensions);
|
|
11
|
+
setPosition(position);
|
|
12
|
+
}
|
|
13
|
+
}, [
|
|
14
|
+
anchorElement,
|
|
15
|
+
minWidth,
|
|
16
|
+
offsetLeft,
|
|
17
|
+
offsetTop,
|
|
18
|
+
placement,
|
|
19
|
+
positionProp
|
|
20
|
+
]);
|
|
21
|
+
const popupCallbackRef = useCallback((el)=>{
|
|
22
|
+
popupRef.current = el;
|
|
23
|
+
if (el && "center" === placement && anchorElement.current) {
|
|
24
|
+
const { height, width } = el.getBoundingClientRect();
|
|
25
|
+
setPosition(getPositionRelativeToAnchor(anchorElement.current, placement, offsetLeft, offsetTop, void 0, {
|
|
26
|
+
height,
|
|
27
|
+
width
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
}, [
|
|
31
|
+
anchorElement,
|
|
32
|
+
offsetLeft,
|
|
33
|
+
offsetTop,
|
|
34
|
+
placement
|
|
35
|
+
]);
|
|
36
|
+
return {
|
|
37
|
+
position,
|
|
38
|
+
popupRef: "center" === placement ? popupCallbackRef : void 0
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
export { useAnchoredPosition };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from "@salt-ds/core";
|
|
3
|
+
import { useComponentCssInjection } from "@salt-ds/styles";
|
|
4
|
+
import { useWindow } from "@salt-ds/window";
|
|
5
|
+
import { Icon, IconButton } from "@vuu-ui/vuu-ui-controls";
|
|
6
|
+
import { useId } from "@vuu-ui/vuu-utils";
|
|
7
|
+
import clsx from "clsx";
|
|
8
|
+
import { usePopupMenu } from "./usePopupMenu.js";
|
|
9
|
+
import PopupMenu from "./PopupMenu.css";
|
|
10
|
+
const classBase = "vuuPopupMenu";
|
|
11
|
+
const PopupMenu_PopupMenu = ({ anchorElement, "aria-label": ariaLabel, appearance = "transparent", className, disabled = false, label, icon = label ? "chevron-down" : "more-vert", id: idProp, menuActionHandler, menuBuilder, menuClassName, menuLocation = "header", menuOptions, onMenuClose, onMenuOpen, popupPlacement = "below-right", sentiment = "neutral", tabIndex = 0, ...htmlAttributes })=>{
|
|
12
|
+
const targetWindow = useWindow();
|
|
13
|
+
useComponentCssInjection({
|
|
14
|
+
testId: "vuu-popup-menu",
|
|
15
|
+
css: PopupMenu,
|
|
16
|
+
window: targetWindow
|
|
17
|
+
});
|
|
18
|
+
const id = useId(idProp);
|
|
19
|
+
const { ariaAttributes, buttonProps, menuOpen, rootRef } = usePopupMenu({
|
|
20
|
+
anchorElement,
|
|
21
|
+
"aria-label": ariaLabel,
|
|
22
|
+
id,
|
|
23
|
+
menuActionHandler,
|
|
24
|
+
menuBuilder,
|
|
25
|
+
menuClassName,
|
|
26
|
+
menuLocation,
|
|
27
|
+
onMenuClose,
|
|
28
|
+
onMenuOpen,
|
|
29
|
+
menuOptions,
|
|
30
|
+
popupPlacement,
|
|
31
|
+
tabIndex
|
|
32
|
+
});
|
|
33
|
+
if (label) return /*#__PURE__*/ jsxs(Button, {
|
|
34
|
+
...htmlAttributes,
|
|
35
|
+
...ariaAttributes,
|
|
36
|
+
...buttonProps,
|
|
37
|
+
appearance: appearance,
|
|
38
|
+
className: clsx(classBase, className, `${classBase}-withCaption`, {
|
|
39
|
+
"saltButton-active": menuOpen
|
|
40
|
+
}),
|
|
41
|
+
disabled: disabled,
|
|
42
|
+
ref: rootRef,
|
|
43
|
+
sentiment: sentiment,
|
|
44
|
+
children: [
|
|
45
|
+
icon ? /*#__PURE__*/ jsx(Icon, {
|
|
46
|
+
name: icon
|
|
47
|
+
}) : null,
|
|
48
|
+
label
|
|
49
|
+
]
|
|
50
|
+
});
|
|
51
|
+
if (icon) return /*#__PURE__*/ jsx(IconButton, {
|
|
52
|
+
...htmlAttributes,
|
|
53
|
+
...ariaAttributes,
|
|
54
|
+
...buttonProps,
|
|
55
|
+
appearance: appearance,
|
|
56
|
+
className: clsx(classBase, className, {
|
|
57
|
+
"saltButton-active": menuOpen
|
|
58
|
+
}),
|
|
59
|
+
disabled: disabled,
|
|
60
|
+
icon: icon,
|
|
61
|
+
ref: rootRef,
|
|
62
|
+
sentiment: sentiment
|
|
63
|
+
});
|
|
64
|
+
console.error("PopupMenu must have a label or an icon (or both)");
|
|
65
|
+
return null;
|
|
66
|
+
};
|
|
67
|
+
export { PopupMenu_PopupMenu as PopupMenu };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./PopupMenu.js";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from "react";
|
|
2
|
+
import { getPositionRelativeToAnchor } from "../popup/getPositionRelativeToAnchor.js";
|
|
3
|
+
import { useContextMenu } from "@vuu-ui/vuu-context-menu";
|
|
4
|
+
const usePopupMenu = ({ anchorElement, "aria-label": ariaLabel = "Popup menu", id, menuActionHandler, menuBuilder, menuLocation, menuOptions, onMenuClose, onMenuOpen, popupPlacement, tabIndex })=>{
|
|
5
|
+
const [menuOpen, _setMenuOpen] = useState(false);
|
|
6
|
+
const suppressShowMenuRef = useRef(false);
|
|
7
|
+
const rootRef = useRef(null);
|
|
8
|
+
const setMenuOpen = useCallback((isOpen)=>{
|
|
9
|
+
_setMenuOpen(isOpen);
|
|
10
|
+
if (isOpen) {
|
|
11
|
+
onMenuOpen?.();
|
|
12
|
+
setTimeout(()=>{
|
|
13
|
+
const firstOption = document.activeElement?.querySelector(".saltMenuItem");
|
|
14
|
+
firstOption?.focus();
|
|
15
|
+
}, 40);
|
|
16
|
+
} else suppressShowMenuRef.current = false;
|
|
17
|
+
}, [
|
|
18
|
+
onMenuOpen
|
|
19
|
+
]);
|
|
20
|
+
const showContextMenu = useContextMenu(menuBuilder, menuActionHandler);
|
|
21
|
+
const handleMenuOpenChange = useCallback((isOpen)=>{
|
|
22
|
+
if (!isOpen) {
|
|
23
|
+
setMenuOpen(false);
|
|
24
|
+
onMenuClose?.();
|
|
25
|
+
}
|
|
26
|
+
}, [
|
|
27
|
+
onMenuClose,
|
|
28
|
+
setMenuOpen
|
|
29
|
+
]);
|
|
30
|
+
const showMenu = useCallback((e)=>{
|
|
31
|
+
if (suppressShowMenuRef.current) suppressShowMenuRef.current = false;
|
|
32
|
+
else {
|
|
33
|
+
suppressShowMenuRef.current = true;
|
|
34
|
+
const anchorEl = anchorElement?.current ?? rootRef.current;
|
|
35
|
+
if (anchorEl) {
|
|
36
|
+
const { left: x, top: y } = getPositionRelativeToAnchor(anchorEl, popupPlacement, 0, 0);
|
|
37
|
+
setMenuOpen(true);
|
|
38
|
+
showContextMenu(e, menuLocation, menuOptions, {
|
|
39
|
+
onOpenChange: handleMenuOpenChange,
|
|
40
|
+
x,
|
|
41
|
+
y
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}, [
|
|
46
|
+
anchorElement,
|
|
47
|
+
popupPlacement,
|
|
48
|
+
setMenuOpen,
|
|
49
|
+
showContextMenu,
|
|
50
|
+
menuLocation,
|
|
51
|
+
menuOptions,
|
|
52
|
+
handleMenuOpenChange
|
|
53
|
+
]);
|
|
54
|
+
const ariaAttributes = {
|
|
55
|
+
"aria-controls": menuOpen ? `${id}-menu` : void 0,
|
|
56
|
+
"aria-label": ariaLabel,
|
|
57
|
+
"aria-expanded": menuOpen,
|
|
58
|
+
"aria-haspopup": "menu"
|
|
59
|
+
};
|
|
60
|
+
const buttonProps = {
|
|
61
|
+
id,
|
|
62
|
+
onClick: showMenu,
|
|
63
|
+
tabIndex
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
ariaAttributes,
|
|
67
|
+
buttonProps,
|
|
68
|
+
menuOpen,
|
|
69
|
+
rootRef
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
export { usePopupMenu };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const css = `
|
|
2
|
+
.vuuPortal {
|
|
3
|
+
z-index: 100;
|
|
4
|
+
position: absolute;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.vuuPortal:not(.vuuPopup) {
|
|
8
|
+
background-color: var(--salt-container-primary-background);
|
|
9
|
+
border-color: var(--vuuMenuList-borderColor, var(--salt-container-primary-borderColor));
|
|
10
|
+
border-style: solid;
|
|
11
|
+
border-width: 1px;
|
|
12
|
+
border-radius: 4px;
|
|
13
|
+
padding: 4px 0;
|
|
14
|
+
box-shadow: 0 6px 12px #0000002d;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.vuuPortal:has(.vuuDropdown-popup-component.vuuList-empty) {
|
|
18
|
+
display: none;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#vuu-portal-root {
|
|
22
|
+
z-index: 1000;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
`;
|
|
27
|
+
export default css;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useThemeAttributes } from "@vuu-ui/vuu-utils";
|
|
2
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
import { useComponentCssInjection } from "@salt-ds/styles";
|
|
5
|
+
import { useWindow } from "@salt-ds/window";
|
|
6
|
+
import Portal from "./Portal.css";
|
|
7
|
+
function getContainer(container) {
|
|
8
|
+
return "function" == typeof container ? container() : container;
|
|
9
|
+
}
|
|
10
|
+
const DEFAULT_ID = [
|
|
11
|
+
"vuu-dialog-portal-root",
|
|
12
|
+
"vuu-portal-root"
|
|
13
|
+
];
|
|
14
|
+
const getFirstAvailableElementById = (id)=>{
|
|
15
|
+
if (!Array.isArray(id)) return document.getElementById(id);
|
|
16
|
+
for (const i of id){
|
|
17
|
+
const element = document.getElementById(i);
|
|
18
|
+
if (element) return element;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
};
|
|
22
|
+
const Portal_Portal = ({ children, container: containerProp = document.body, id = DEFAULT_ID, onRender, open = true, themeAttributes })=>{
|
|
23
|
+
const targetWindow = useWindow();
|
|
24
|
+
useComponentCssInjection({
|
|
25
|
+
testId: "vuu-portal",
|
|
26
|
+
css: Portal,
|
|
27
|
+
window: targetWindow
|
|
28
|
+
});
|
|
29
|
+
const [mounted, setMounted] = useState(false);
|
|
30
|
+
const portalRef = useRef(null);
|
|
31
|
+
const container = getContainer(containerProp) ?? document.body;
|
|
32
|
+
const [themeClass, densityClass, dataMode] = useThemeAttributes(themeAttributes);
|
|
33
|
+
useLayoutEffect(()=>{
|
|
34
|
+
const root = getFirstAvailableElementById(id);
|
|
35
|
+
if (root) portalRef.current = root;
|
|
36
|
+
else {
|
|
37
|
+
portalRef.current = document.createElement("div");
|
|
38
|
+
portalRef.current.id = "string" == typeof id ? id : id.length > 0 ? id.at(-1) : "vuu-portal-root";
|
|
39
|
+
}
|
|
40
|
+
const el = portalRef.current;
|
|
41
|
+
if (!container.contains(el)) container.appendChild(el);
|
|
42
|
+
el.classList.add(themeClass, densityClass);
|
|
43
|
+
el.dataset.mode = dataMode;
|
|
44
|
+
setMounted(true);
|
|
45
|
+
}, [
|
|
46
|
+
id,
|
|
47
|
+
container,
|
|
48
|
+
themeClass,
|
|
49
|
+
densityClass,
|
|
50
|
+
dataMode
|
|
51
|
+
]);
|
|
52
|
+
useLayoutEffect(()=>{
|
|
53
|
+
requestAnimationFrame(()=>{
|
|
54
|
+
onRender?.();
|
|
55
|
+
});
|
|
56
|
+
}, [
|
|
57
|
+
onRender
|
|
58
|
+
]);
|
|
59
|
+
if (open && mounted && portalRef.current && children) return /*#__PURE__*/ createPortal(children, portalRef.current);
|
|
60
|
+
return null;
|
|
61
|
+
};
|
|
62
|
+
export { Portal_Portal as Portal };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Portal.js";
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const css = `
|
|
2
|
+
.vuuTooltip {
|
|
3
|
+
--tooltip-align: flex-start;
|
|
4
|
+
--tooltip-background: var(--salt-actionable-bold-background-hover);
|
|
5
|
+
--tooltip-border: var(--salt-actionable-bold-background-hover);
|
|
6
|
+
--tooltip-color: var(--salt-actionable-bold-foreground-hover);
|
|
7
|
+
--tooltip-justify: flex-start;
|
|
8
|
+
--tooltip-top: auto;
|
|
9
|
+
--tooltip-right: auto;
|
|
10
|
+
--tooltip-bottom: auto;
|
|
11
|
+
--tooltip-left: auto;
|
|
12
|
+
align-items: var(--tooltip-align);
|
|
13
|
+
justify-content: var(--tooltip-justify);
|
|
14
|
+
z-index: 100;
|
|
15
|
+
transition: opacity .15s ease-in;
|
|
16
|
+
display: flex;
|
|
17
|
+
position: absolute;
|
|
18
|
+
|
|
19
|
+
&.vuuHidden {
|
|
20
|
+
opacity: 0;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.vuuTooltip-content {
|
|
25
|
+
background-color: var(--tooltip-background);
|
|
26
|
+
border-color: var(--tooltip-border);
|
|
27
|
+
border-radius: var(--vuuTooltip-borderRadius, 6px);
|
|
28
|
+
color: var(--tooltip-color);
|
|
29
|
+
white-space: nowrap;
|
|
30
|
+
border-style: solid;
|
|
31
|
+
border-width: 1px;
|
|
32
|
+
padding: 2px 8px;
|
|
33
|
+
line-height: 24px;
|
|
34
|
+
box-shadow: 0 4px 4px #00000040;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.vuuTooltip:before {
|
|
38
|
+
background-color: var(--tooltip-border);
|
|
39
|
+
content: " ";
|
|
40
|
+
z-index: -1;
|
|
41
|
+
width: 12px;
|
|
42
|
+
height: 12px;
|
|
43
|
+
display: block;
|
|
44
|
+
position: absolute;
|
|
45
|
+
transform: rotate(45deg);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.vuuTooltip[data-align="above"]:before {
|
|
49
|
+
bottom: -6px;
|
|
50
|
+
left: calc(50% - 6px);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.vuuTooltip[data-align="below"]:before {
|
|
54
|
+
top: -6px;
|
|
55
|
+
left: calc(50% - 6px);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.vuuTooltip[data-align="right"]:before {
|
|
59
|
+
top: calc(50% - 6px);
|
|
60
|
+
left: -6px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.vuuTooltip[data-align="left"]:before {
|
|
64
|
+
top: calc(50% - 6px);
|
|
65
|
+
right: -6px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.vuuTooltip-error {
|
|
69
|
+
--tooltip-background: var(--vuu-color-red-50);
|
|
70
|
+
--tooltip-color: white;
|
|
71
|
+
color: #fff;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
`;
|
|
76
|
+
export default css;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Portal } from "../portal/index.js";
|
|
3
|
+
import { useTooltipAnchoredPosition } from "./useTooltipAnchoredPosition.js";
|
|
4
|
+
import clsx from "clsx";
|
|
5
|
+
import { useComponentCssInjection } from "@salt-ds/styles";
|
|
6
|
+
import { useWindow } from "@salt-ds/window";
|
|
7
|
+
import Tooltip from "./Tooltip.css";
|
|
8
|
+
const classBase = "vuuTooltip";
|
|
9
|
+
const Tooltip_Tooltip = ({ anchorElement, children, className, id, onMouseEnter, onMouseLeave, placement: placementProp, status, style: styleProp })=>{
|
|
10
|
+
const targetWindow = useWindow();
|
|
11
|
+
useComponentCssInjection({
|
|
12
|
+
testId: "vuu-tooltip",
|
|
13
|
+
css: Tooltip,
|
|
14
|
+
window: targetWindow
|
|
15
|
+
});
|
|
16
|
+
const ref = useTooltipAnchoredPosition({
|
|
17
|
+
anchorElement,
|
|
18
|
+
placement: placementProp
|
|
19
|
+
});
|
|
20
|
+
return /*#__PURE__*/ jsx(Portal, {
|
|
21
|
+
children: /*#__PURE__*/ jsx("div", {
|
|
22
|
+
className: clsx(classBase, className, "vuuHidden", {
|
|
23
|
+
[`${classBase}-error`]: "error" === status
|
|
24
|
+
}),
|
|
25
|
+
id: id,
|
|
26
|
+
ref: ref,
|
|
27
|
+
style: {
|
|
28
|
+
...styleProp,
|
|
29
|
+
left: 0,
|
|
30
|
+
top: 0
|
|
31
|
+
},
|
|
32
|
+
children: /*#__PURE__*/ jsx("span", {
|
|
33
|
+
className: `${classBase}-content`,
|
|
34
|
+
onMouseEnter: onMouseEnter,
|
|
35
|
+
onMouseLeave: onMouseLeave,
|
|
36
|
+
children: children
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
export { Tooltip_Tooltip as Tooltip };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { queryClosest, useId } from "@vuu-ui/vuu-utils";
|
|
2
|
+
import { useCallback, useRef, useState } from "react";
|
|
3
|
+
const useTooltip = ({ anchorQuery = "*", id: idProp, placement = [
|
|
4
|
+
"right",
|
|
5
|
+
"above",
|
|
6
|
+
"below"
|
|
7
|
+
], tooltipContent })=>{
|
|
8
|
+
const hideTooltipRef = useRef(void 0);
|
|
9
|
+
const isHoveringRef = useRef(false);
|
|
10
|
+
const anchorElementRef = useRef(null);
|
|
11
|
+
const mouseEnterTimerRef = useRef(void 0);
|
|
12
|
+
const mouseLeaveTimerRef = useRef(void 0);
|
|
13
|
+
const [tooltipProps, setTooltipProps] = useState();
|
|
14
|
+
const id = useId(idProp);
|
|
15
|
+
const escapeListener = useCallback((evt)=>{
|
|
16
|
+
if ("Escape" === evt.key) hideTooltipRef.current?.();
|
|
17
|
+
}, []);
|
|
18
|
+
hideTooltipRef.current = useCallback(()=>{
|
|
19
|
+
setTooltipProps(void 0);
|
|
20
|
+
document.removeEventListener("keydown", escapeListener);
|
|
21
|
+
}, [
|
|
22
|
+
escapeListener
|
|
23
|
+
]);
|
|
24
|
+
const handleMouseEnterTooltip = useCallback(()=>{
|
|
25
|
+
window.clearTimeout(mouseLeaveTimerRef.current);
|
|
26
|
+
}, []);
|
|
27
|
+
const handleMouseLeaveTooltip = useCallback(()=>{
|
|
28
|
+
hideTooltipRef.current?.();
|
|
29
|
+
}, []);
|
|
30
|
+
const hideTooltip = useCallback((defer = 0)=>{
|
|
31
|
+
if (mouseEnterTimerRef.current) {
|
|
32
|
+
window.clearTimeout(mouseEnterTimerRef.current);
|
|
33
|
+
mouseEnterTimerRef.current = void 0;
|
|
34
|
+
} else if (hideTooltipRef.current) if (0 === defer) hideTooltipRef.current();
|
|
35
|
+
else mouseLeaveTimerRef.current = window.setTimeout(hideTooltipRef.current, defer);
|
|
36
|
+
}, []);
|
|
37
|
+
const showTooltip = useCallback((ref = anchorElementRef)=>{
|
|
38
|
+
const { current: anchorEl } = ref;
|
|
39
|
+
if (anchorEl) {
|
|
40
|
+
setTooltipProps({
|
|
41
|
+
anchorElement: ref,
|
|
42
|
+
children: tooltipContent,
|
|
43
|
+
id: `${id}-tooltip`,
|
|
44
|
+
onMouseEnter: handleMouseEnterTooltip,
|
|
45
|
+
onMouseLeave: handleMouseLeaveTooltip,
|
|
46
|
+
placement: placement
|
|
47
|
+
});
|
|
48
|
+
document.addEventListener("keydown", escapeListener);
|
|
49
|
+
}
|
|
50
|
+
mouseEnterTimerRef.current = void 0;
|
|
51
|
+
hideTooltip(isHoveringRef.current ? 3000 : 1000);
|
|
52
|
+
}, [
|
|
53
|
+
escapeListener,
|
|
54
|
+
handleMouseEnterTooltip,
|
|
55
|
+
handleMouseLeaveTooltip,
|
|
56
|
+
hideTooltip,
|
|
57
|
+
id,
|
|
58
|
+
placement,
|
|
59
|
+
tooltipContent
|
|
60
|
+
]);
|
|
61
|
+
const handleMouseEnter = useCallback((evt)=>{
|
|
62
|
+
isHoveringRef.current = true;
|
|
63
|
+
const el = queryClosest(evt.target, anchorQuery);
|
|
64
|
+
if (el) {
|
|
65
|
+
console.log(`el ${el.classList}`);
|
|
66
|
+
anchorElementRef.current = el;
|
|
67
|
+
mouseEnterTimerRef.current = window.setTimeout(showTooltip, 800);
|
|
68
|
+
}
|
|
69
|
+
}, [
|
|
70
|
+
anchorQuery,
|
|
71
|
+
showTooltip
|
|
72
|
+
]);
|
|
73
|
+
const handleMouseLeave = useCallback(()=>{
|
|
74
|
+
isHoveringRef.current = false;
|
|
75
|
+
hideTooltip(200);
|
|
76
|
+
}, [
|
|
77
|
+
hideTooltip
|
|
78
|
+
]);
|
|
79
|
+
const anchorProps = {
|
|
80
|
+
"aria-describedby": `${id}-tooltip`,
|
|
81
|
+
onMouseEnter: handleMouseEnter,
|
|
82
|
+
onMouseLeave: handleMouseLeave
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
anchorProps,
|
|
86
|
+
hideTooltip,
|
|
87
|
+
showTooltip,
|
|
88
|
+
tooltipProps
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
export { useTooltip };
|