@bikiran/utils 2.3.6 → 2.3.8

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.
@@ -6,6 +6,10 @@ type TProps = {
6
6
  align?: "left" | "right" | "top" | "bottom";
7
7
  fillColor?: string;
8
8
  borderColor?: string;
9
+ trigger?: "hover" | "click";
10
+ disabled?: boolean;
11
+ delay?: number;
12
+ offset?: number;
9
13
  };
10
14
  declare const InformationTooltip: FC<TProps>;
11
15
  export default InformationTooltip;
@@ -1,27 +1,191 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState, useRef, useEffect, useCallback } from "react";
3
+ import { createPortal } from "react-dom";
2
4
  import { IconArrow, IconInfo } from "./icons";
3
5
  import styles from "./style/InfoTooltip.module.css";
4
6
  import { cn } from "../../lib/utils/cn";
5
- const InformationTooltip = ({ children, content, className, align = "right", fillColor = "#FFF9DB", borderColor = "#FFE6BA", }) => {
6
- const tooltipClass = cn(styles.tooltipContent, align === "top"
7
- ? styles.topAlign
8
- : align === "bottom"
9
- ? styles.bottomAlign
10
- : align === "left"
11
- ? styles.leftAlign
12
- : styles.rightAlign, className);
13
- const arrowClass = cn(styles.arrow, align === "top"
14
- ? styles.arrowTop
15
- : align === "bottom"
16
- ? styles.arrowBottom
17
- : align === "left"
18
- ? styles.arrowLeft
19
- : styles.arrowRight);
20
- return (_jsxs("div", { className: styles.wrapper, children: [children ? children : _jsx(IconInfo, {}), _jsx("div", { className: tooltipClass, style: {
21
- backgroundColor: fillColor,
22
- borderColor: borderColor,
23
- borderWidth: "1px",
24
- color: "var(--color-primary)", // if you have text-primary color in css vars
25
- }, children: _jsx("span", { children: content }) }), _jsx("div", { className: arrowClass, children: _jsx(IconArrow, { fillColor: fillColor, borderColor: borderColor }) })] }));
7
+ const InformationTooltip = ({ children, content, className, align = "right", fillColor = "#FFF9DB", borderColor = "#FFE6BA", trigger = "hover", disabled = false, delay = 200, offset = 12, }) => {
8
+ const [isVisible, setIsVisible] = useState(false);
9
+ const [position, setPosition] = useState({
10
+ top: 0,
11
+ left: 0,
12
+ finalAlign: align,
13
+ });
14
+ const triggerRef = useRef(null);
15
+ const tooltipRef = useRef(null);
16
+ const timeoutRef = useRef(null);
17
+ // Calculate optimal position based on viewport boundaries
18
+ const calculatePosition = useCallback(() => {
19
+ if (!triggerRef.current)
20
+ return { top: 0, left: 0, finalAlign: align };
21
+ const triggerRect = triggerRef.current.getBoundingClientRect();
22
+ const viewportWidth = window.innerWidth;
23
+ const viewportHeight = window.innerHeight;
24
+ const tooltipWidth = 224; // w-56 = 14rem = 224px
25
+ // Try to get actual tooltip height if available, otherwise use estimate
26
+ let tooltipHeight = 65; // default estimate
27
+ if (tooltipRef.current) {
28
+ const tooltipRect = tooltipRef.current.getBoundingClientRect();
29
+ if (tooltipRect.height > 0) {
30
+ tooltipHeight = tooltipRect.height;
31
+ }
32
+ }
33
+ let finalAlign = align;
34
+ let top = 0;
35
+ let left = 0;
36
+ // Calculate position based on preferred alignment
37
+ switch (align) {
38
+ case "right":
39
+ left = triggerRect.right + offset;
40
+ top = triggerRect.top + triggerRect.height / 2 - tooltipHeight / 2;
41
+ // Check if tooltip would overflow viewport
42
+ if (left + tooltipWidth > viewportWidth) {
43
+ finalAlign = "left";
44
+ left = triggerRect.left - tooltipWidth - offset;
45
+ }
46
+ break;
47
+ case "left":
48
+ left = triggerRect.left - tooltipWidth - offset;
49
+ top = triggerRect.top + triggerRect.height / 2 - tooltipHeight / 2;
50
+ if (left < 0) {
51
+ finalAlign = "right";
52
+ left = triggerRect.right + offset;
53
+ }
54
+ break;
55
+ case "top":
56
+ left = triggerRect.left + triggerRect.width / 2 - tooltipWidth / 2;
57
+ top = triggerRect.top - tooltipHeight + offset / 2; // Reduce gap for closer positioning
58
+ if (top < 0) {
59
+ finalAlign = "bottom";
60
+ top = triggerRect.bottom + offset;
61
+ }
62
+ break;
63
+ case "bottom":
64
+ left = triggerRect.left + triggerRect.width / 2 - tooltipWidth / 2;
65
+ top = triggerRect.bottom + offset;
66
+ if (top + tooltipHeight > viewportHeight) {
67
+ finalAlign = "top";
68
+ top = triggerRect.top - tooltipHeight - offset / 2; // Consistent closer positioning
69
+ }
70
+ break;
71
+ }
72
+ // Ensure tooltip doesn't overflow horizontally
73
+ if (left < 0)
74
+ left = 8;
75
+ if (left + tooltipWidth > viewportWidth)
76
+ left = viewportWidth - tooltipWidth - 8;
77
+ // Ensure tooltip doesn't overflow vertically
78
+ if (top < 0)
79
+ top = 8;
80
+ if (top + tooltipHeight > viewportHeight)
81
+ top = viewportHeight - tooltipHeight - 8;
82
+ return { top, left, finalAlign };
83
+ }, [align, offset]);
84
+ const showTooltip = useCallback(() => {
85
+ if (disabled)
86
+ return;
87
+ if (timeoutRef.current) {
88
+ clearTimeout(timeoutRef.current);
89
+ }
90
+ timeoutRef.current = setTimeout(() => {
91
+ setPosition(calculatePosition());
92
+ setIsVisible(true);
93
+ }, trigger === "hover" ? delay : 0);
94
+ }, [disabled, calculatePosition, delay, trigger]);
95
+ const hideTooltip = useCallback(() => {
96
+ if (timeoutRef.current) {
97
+ clearTimeout(timeoutRef.current);
98
+ }
99
+ if (trigger === "hover") {
100
+ timeoutRef.current = setTimeout(() => {
101
+ setIsVisible(false);
102
+ }, 100);
103
+ }
104
+ else {
105
+ setIsVisible(false);
106
+ }
107
+ }, [trigger]);
108
+ const handleClick = useCallback(() => {
109
+ if (disabled)
110
+ return;
111
+ if (trigger === "click") {
112
+ if (isVisible) {
113
+ hideTooltip();
114
+ }
115
+ else {
116
+ showTooltip();
117
+ }
118
+ }
119
+ }, [disabled, trigger, isVisible, showTooltip, hideTooltip]);
120
+ // Handle click outside
121
+ useEffect(() => {
122
+ const handleClickOutside = (event) => {
123
+ if (trigger === "click" &&
124
+ isVisible &&
125
+ triggerRef.current &&
126
+ tooltipRef.current &&
127
+ !triggerRef.current.contains(event.target) &&
128
+ !tooltipRef.current.contains(event.target)) {
129
+ hideTooltip();
130
+ }
131
+ };
132
+ document.addEventListener("mousedown", handleClickOutside);
133
+ return () => document.removeEventListener("mousedown", handleClickOutside);
134
+ }, [trigger, isVisible, hideTooltip]);
135
+ // Handle escape key
136
+ useEffect(() => {
137
+ const handleEscape = (event) => {
138
+ if (event.key === "Escape" && isVisible) {
139
+ hideTooltip();
140
+ }
141
+ };
142
+ document.addEventListener("keydown", handleEscape);
143
+ return () => document.removeEventListener("keydown", handleEscape);
144
+ }, [isVisible, hideTooltip]);
145
+ // Recalculate position on scroll/resize
146
+ useEffect(() => {
147
+ const handleReposition = () => {
148
+ if (isVisible) {
149
+ setPosition(calculatePosition());
150
+ }
151
+ };
152
+ window.addEventListener("scroll", handleReposition, true);
153
+ window.addEventListener("resize", handleReposition);
154
+ return () => {
155
+ window.removeEventListener("scroll", handleReposition, true);
156
+ window.removeEventListener("resize", handleReposition);
157
+ };
158
+ }, [isVisible, calculatePosition]);
159
+ // Cleanup timeout on unmount
160
+ useEffect(() => {
161
+ return () => {
162
+ if (timeoutRef.current) {
163
+ clearTimeout(timeoutRef.current);
164
+ }
165
+ };
166
+ }, []);
167
+ const tooltipClass = cn(styles.tooltipPortal, className);
168
+ const arrowClass = cn(styles.arrowPortal, styles[`arrow${position.finalAlign.charAt(0).toUpperCase() +
169
+ position.finalAlign.slice(1)}`]);
170
+ const triggerProps = Object.assign(Object.assign({}, (trigger === "hover" && {
171
+ onMouseEnter: showTooltip,
172
+ onMouseLeave: hideTooltip,
173
+ onFocus: showTooltip,
174
+ onBlur: hideTooltip,
175
+ })), (trigger === "click" && {
176
+ onClick: handleClick,
177
+ }));
178
+ return (_jsxs(_Fragment, { children: [_jsx("div", Object.assign({ ref: triggerRef, className: cn(styles.wrapper, disabled && styles.disabled) }, triggerProps, { tabIndex: trigger === "click" ? 0 : undefined, role: trigger === "click" ? "button" : undefined, "aria-describedby": isVisible ? "tooltip" : undefined, children: children ? children : _jsx(IconInfo, {}) })), isVisible &&
179
+ !disabled &&
180
+ createPortal(_jsxs("div", { ref: tooltipRef, id: "tooltip", role: "tooltip", className: tooltipClass, style: {
181
+ position: "fixed",
182
+ top: position.top,
183
+ left: position.left,
184
+ backgroundColor: fillColor,
185
+ borderColor: borderColor,
186
+ borderWidth: "1px",
187
+ color: "var(--color-primary)",
188
+ zIndex: 9999,
189
+ }, onMouseEnter: trigger === "hover" ? showTooltip : undefined, onMouseLeave: trigger === "hover" ? hideTooltip : undefined, children: [_jsx("span", { children: content }), _jsx("div", { className: arrowClass, children: _jsx(IconArrow, { fillColor: fillColor, borderColor: borderColor }) })] }), document.body)] }));
26
190
  };
27
191
  export default InformationTooltip;
@@ -4,6 +4,14 @@
4
4
  @apply relative inline-flex items-center cursor-pointer;
5
5
  }
6
6
 
7
+ .wrapper:focus {
8
+ @apply outline-none;
9
+ }
10
+
11
+ .disabled {
12
+ @apply cursor-not-allowed opacity-50;
13
+ }
14
+
7
15
  .tooltipContent {
8
16
  @apply absolute rounded-[15px] text-sm w-56 px-4 py-3 shadow-md hidden z-10 transition-all duration-300 ease-in-out;
9
17
  }
@@ -20,43 +28,29 @@
20
28
  @apply block;
21
29
  }
22
30
 
23
- /* Alignment positions for tooltipContent */
24
- .rightAlign {
25
- left: calc(100% + 24%);
26
- @apply top-1/2 transform -translate-y-1/2;
31
+ /* Portal-based tooltip styles */
32
+ .tooltipPortal {
33
+ @apply rounded-[15px] text-sm w-56 px-4 py-3 shadow-lg border border-solid transition-all duration-200 ease-in-out;
34
+ @apply opacity-100 visible relative;
27
35
  }
28
36
 
29
- .leftAlign {
30
- right: calc(100% + 24%);
31
- @apply top-1/2 transform -translate-y-1/2;
37
+ .arrowPortal {
38
+ @apply absolute w-6 h-3;
32
39
  }
33
40
 
34
- .topAlign {
35
- bottom: calc(100% + 19%);
36
- @apply left-1/2 transform -translate-x-1/2;
37
- }
38
-
39
- .bottomAlign {
40
- top: calc(100% + 20%);
41
- @apply left-1/2 transform -translate-x-1/2;
42
- }
43
-
44
- /* Arrow positions */
41
+ /* Arrow positions for portal tooltips */
45
42
  .arrowRight {
46
- left: calc(100% - 26px);
47
- @apply top-1/2 transform rotate-90 -translate-x-[100%];
43
+ @apply left-[-18px] top-1/2 transform -translate-y-1/2 rotate-90;
48
44
  }
49
45
 
50
46
  .arrowLeft {
51
- right: calc(100% - 27px);
52
-
53
- @apply top-1/2 transform rotate-90 translate-x-[100%];
47
+ @apply right-[-18px] top-1/2 transform -translate-y-1/2 rotate-[270deg];
54
48
  }
55
49
 
56
50
  .arrowTop {
57
- @apply left-1/2 bottom-[5px] transform -translate-x-1/2 rotate-0;
51
+ @apply left-1/2 bottom-[-12px] transform -translate-x-1/2 rotate-0;
58
52
  }
59
53
 
60
54
  .arrowBottom {
61
- @apply left-4 top-2 transform rotate-180 -translate-x-1/2;
55
+ @apply left-1/2 top-[-12px] transform -translate-x-1/2 rotate-180;
62
56
  }
@@ -23,7 +23,11 @@ const TooltipUserInfo = ({ user, ImageComponent, redirectClick, }) => {
23
23
  document.addEventListener("mousedown", handleClickOutside);
24
24
  return () => document.removeEventListener("mousedown", handleClickOutside);
25
25
  }, [setShow]);
26
- return (_jsxs("div", { className: cn(style.toolTipParentComp, "parentComp"), ref: tooltipRef, children: [_jsx("div", { onClick: () => setShow(show === (user === null || user === void 0 ? void 0 : user.id) ? null : user === null || user === void 0 ? void 0 : user.id), className: cn(style.imageDiv, "imageDiv"), children: (user === null || user === void 0 ? void 0 : user.photoUrl) ? (_jsx(ImageComponent, { src: user === null || user === void 0 ? void 0 : user.photoUrl, alt: "user", width: 0, height: 0, sizes: "100vw", className: cn(style.userImage, "userImage") })) : (_jsx(IconUser, {})) }), _jsxs("button", { className: cn(style.btnRedirect, "btnRedirect", show === (user === null || user === void 0 ? void 0 : user.id) ? style.show : "show" // Show when active
26
+ return (_jsxs("div", { className: cn(style.toolTipParentComp, "parentComp"), ref: tooltipRef, children: [_jsx("div", { onClick: (e) => {
27
+ e.stopPropagation();
28
+ e.preventDefault();
29
+ setShow(show === (user === null || user === void 0 ? void 0 : user.id) ? null : user === null || user === void 0 ? void 0 : user.id);
30
+ }, className: cn(style.imageDiv, "imageDiv"), children: (user === null || user === void 0 ? void 0 : user.photoUrl) ? (_jsx(ImageComponent, { src: user === null || user === void 0 ? void 0 : user.photoUrl, alt: "user", width: 0, height: 0, sizes: "100vw", className: cn(style.userImage, "userImage") })) : (_jsx(IconUser, {})) }), _jsxs("button", { className: cn(style.btnRedirect, "btnRedirect", show === (user === null || user === void 0 ? void 0 : user.id) ? style.show : "show" // Show when active
27
31
  ), onClick: handleRedirectClick, children: [_jsxs("div", { className: cn(style.showImg, "showImg"), children: [(user === null || user === void 0 ? void 0 : user.photoUrl) ? (_jsx(ImageComponent, { src: user === null || user === void 0 ? void 0 : user.photoUrl, alt: "user", width: 0, height: 0, sizes: "100vw", className: cn(style.userImage, "userImage") })) : (_jsx(IconUser, {})), _jsxs("div", { children: [_jsx("div", { className: cn(style.displayName, "displayName"), children: (user === null || user === void 0 ? void 0 : user.displayName) || "-----" }), _jsx("div", { className: cn(style.email, "email"), children: (user === null || user === void 0 ? void 0 : user.email) || "-----" })] })] }), _jsx("div", { className: cn(style.tooltipArrow, "tooltipArrow") })] })] }));
28
32
  };
29
33
  export default TooltipUserInfo;
@@ -5,7 +5,7 @@
5
5
  @apply cursor-pointer;
6
6
  }
7
7
  .userImage {
8
- @apply size-7 xl:size-10 rounded-full;
8
+ @apply size-7 xl:size-10 rounded-full !flex-shrink-0;
9
9
  }
10
10
  .btnRedirect {
11
11
  @apply absolute left-1/2 bottom-full mb-3 -translate-x-1/2 z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none transition-all duration-200 opacity-0 scale-95 pointer-events-none;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bikiran/utils",
3
- "version": "2.3.6",
3
+ "version": "2.3.8",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [