@sybilion/uilib 1.3.25 → 1.3.26

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.
@@ -4,43 +4,19 @@ import { Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip/Tooltip.js';
4
4
 
5
5
  function TextWithDeferTooltip({ className, children, width, maxWidth, side = 'bottom', overTrigger = false, ...props }) {
6
6
  const [withTooltip, setWithTooltip] = useState(false);
7
- const [tooltipStyles, setTooltipStyles] = useState({});
8
7
  const ref = useRef(null);
9
- const cachedFontSizeRef = useRef(null);
10
8
  const handleMouseEnter = () => {
11
9
  if (!ref.current)
12
10
  return;
13
11
  const isOverflowingHorizontally = ref.current.scrollWidth - ref.current.clientWidth > 3;
14
12
  const isOverflowingVertically = ref.current.scrollHeight - ref.current.clientHeight > 3;
15
13
  if (isOverflowingHorizontally || isOverflowingVertically) {
16
- const styles = {
17
- fontSize: cachedFontSizeRef.current || undefined,
18
- };
19
- if (ref.current) {
20
- const { width: rectWidth, left, top, } = ref.current.getBoundingClientRect();
21
- styles.width = `${width ?? rectWidth}px`;
22
- if (!cachedFontSizeRef.current) {
23
- const { fontSize } = window.getComputedStyle(ref.current);
24
- cachedFontSizeRef.current = fontSize;
25
- styles.fontSize = fontSize;
26
- }
27
- if (overTrigger) {
28
- styles.transform = `translate(${left}px, ${top}px) !important`;
29
- // styles.translateX = `${left}px`;
30
- // styles.translateY = `${top}px`;
31
- }
32
- }
33
- setTooltipStyles(styles);
34
14
  setWithTooltip(true);
35
15
  }
36
16
  };
37
17
  const textElement = (jsx("div", { ref: ref, className: className, onMouseEnter: handleMouseEnter, ...props, children: children }));
38
18
  if (withTooltip) {
39
- const tooltipSide = overTrigger ? 'bottom' : side;
40
- return (jsxs(Tooltip, { open: withTooltip, onOpenChange: setWithTooltip, children: [jsx(TooltipTrigger, { asChild: true, children: textElement }), jsx(TooltipContent, { side: tooltipSide, style: {
41
- ...(maxWidth !== undefined && { maxWidth: `${maxWidth}px` }),
42
- ...tooltipStyles,
43
- }, children: children })] }));
19
+ return (jsxs(Tooltip, { open: withTooltip, onOpenChange: setWithTooltip, children: [jsx(TooltipTrigger, { asChild: true, children: textElement }), jsx(TooltipContent, { side: side, overTrigger: overTrigger, maxWidth: maxWidth, style: width !== undefined ? { width: `${width}px` } : undefined, children: children })] }));
44
20
  }
45
21
  return textElement;
46
22
  }
@@ -1,22 +1,107 @@
1
1
  import { jsx, jsxs } from 'react/jsx-runtime';
2
2
  import cn from 'classnames';
3
+ import { createContext, useRef, useMemo, useContext, useCallback, useLayoutEffect } from 'react';
3
4
  import * as TooltipPrimitive from '@radix-ui/react-tooltip';
4
5
  import S from './Tooltip.styl.js';
5
6
 
7
+ const TooltipContext = createContext(null);
8
+ function composeRefs(...refs) {
9
+ return (node) => {
10
+ for (const ref of refs) {
11
+ if (typeof ref === 'function') {
12
+ ref(node);
13
+ }
14
+ else if (ref) {
15
+ ref.current = node;
16
+ }
17
+ }
18
+ };
19
+ }
20
+ function getPopperWrapper(contentEl) {
21
+ const wrapper = contentEl.parentElement;
22
+ if (!wrapper?.hasAttribute('data-radix-popper-content-wrapper'))
23
+ return null;
24
+ return wrapper;
25
+ }
26
+ const OVER_TRIGGER_OFFSET = { left: -10, top: -6 };
27
+ function applyOverTriggerStyles(contentEl, triggerEl) {
28
+ const wrapper = getPopperWrapper(contentEl);
29
+ if (!wrapper)
30
+ return;
31
+ const rect = triggerEl.getBoundingClientRect();
32
+ const computed = window.getComputedStyle(triggerEl);
33
+ wrapper.style.setProperty('position', 'fixed', 'important');
34
+ wrapper.style.setProperty('left', `${rect.left + OVER_TRIGGER_OFFSET.left}px`, 'important');
35
+ wrapper.style.setProperty('top', `${rect.top + OVER_TRIGGER_OFFSET.top}px`, 'important');
36
+ wrapper.style.setProperty('transform', 'none', 'important');
37
+ wrapper.style.setProperty('min-width', '0', 'important');
38
+ wrapper.style.setProperty('width', `${rect.width}px`, 'important');
39
+ contentEl.style.width = '100%';
40
+ contentEl.style.boxSizing = 'border-box';
41
+ contentEl.style.fontSize = computed.fontSize;
42
+ contentEl.style.lineHeight = computed.lineHeight;
43
+ }
44
+ function clearOverTriggerStyles(contentEl) {
45
+ if (!contentEl)
46
+ return;
47
+ const wrapper = getPopperWrapper(contentEl);
48
+ if (!wrapper)
49
+ return;
50
+ wrapper.style.removeProperty('position');
51
+ wrapper.style.removeProperty('left');
52
+ wrapper.style.removeProperty('top');
53
+ wrapper.style.removeProperty('transform');
54
+ wrapper.style.removeProperty('min-width');
55
+ wrapper.style.removeProperty('width');
56
+ contentEl.style.removeProperty('width');
57
+ contentEl.style.removeProperty('box-sizing');
58
+ contentEl.style.removeProperty('font-size');
59
+ contentEl.style.removeProperty('line-height');
60
+ }
6
61
  function TooltipProvider({ delayDuration = 0, ...props }) {
7
62
  return (jsx(TooltipPrimitive.Provider, { "data-slot": "tooltip-provider", delayDuration: delayDuration, ...props }));
8
63
  }
9
- function Tooltip(props) {
10
- return (jsx(TooltipProvider, { children: jsx(TooltipPrimitive.Root, { "data-slot": "tooltip", ...props }) }));
64
+ function Tooltip({ children, ...props }) {
65
+ const triggerRef = useRef(null);
66
+ const contextValue = useMemo(() => ({ triggerRef }), []);
67
+ return (jsx(TooltipProvider, { children: jsx(TooltipContext.Provider, { value: contextValue, children: jsx(TooltipPrimitive.Root, { "data-slot": "tooltip", ...props, children: children }) }) }));
11
68
  }
12
- function TooltipTrigger({ ...props }) {
13
- return jsx(TooltipPrimitive.Trigger, { "data-slot": "tooltip-trigger", ...props });
69
+ function TooltipTrigger({ ref, ...props }) {
70
+ const context = useContext(TooltipContext);
71
+ return (jsx(TooltipPrimitive.Trigger, { ref: context ? composeRefs(context.triggerRef, ref) : ref, "data-slot": "tooltip-trigger", ...props }));
14
72
  }
15
- function TooltipContent({ className, sideOffset = 0, children, maxWidth, ...props }) {
16
- const style = { ...props.style };
73
+ function TooltipContent({ className, sideOffset = 0, children, maxWidth, overTrigger = false, align, side, avoidCollisions, style: styleProp, ...props }) {
74
+ const context = useContext(TooltipContext);
75
+ const contentRef = useRef(null);
76
+ const updateOverTriggerPosition = useCallback(() => {
77
+ const triggerEl = context?.triggerRef.current;
78
+ const contentEl = contentRef.current;
79
+ if (!overTrigger || !triggerEl || !contentEl)
80
+ return;
81
+ applyOverTriggerStyles(contentEl, triggerEl);
82
+ }, [context, overTrigger]);
83
+ useLayoutEffect(() => {
84
+ if (!overTrigger)
85
+ return;
86
+ let frameId = 0;
87
+ const tick = () => {
88
+ updateOverTriggerPosition();
89
+ frameId = requestAnimationFrame(tick);
90
+ };
91
+ tick();
92
+ window.addEventListener('scroll', updateOverTriggerPosition, true);
93
+ window.addEventListener('resize', updateOverTriggerPosition);
94
+ return () => {
95
+ cancelAnimationFrame(frameId);
96
+ window.removeEventListener('scroll', updateOverTriggerPosition, true);
97
+ window.removeEventListener('resize', updateOverTriggerPosition);
98
+ clearOverTriggerStyles(contentRef.current);
99
+ };
100
+ }, [overTrigger, updateOverTriggerPosition]);
101
+ const style = { ...styleProp };
17
102
  if (maxWidth)
18
103
  style.maxWidth = `${maxWidth}px`;
19
- return (jsx(TooltipPrimitive.Portal, { children: jsxs(TooltipPrimitive.Content, { "data-slot": "tooltip-content", sideOffset: sideOffset, className: cn(S.tooltipContent, className), ...props, style: style, children: [children, jsx(TooltipPrimitive.Arrow, { className: S.tooltipArrow })] }) }));
104
+ return (jsx(TooltipPrimitive.Portal, { children: jsxs(TooltipPrimitive.Content, { ref: contentRef, "data-slot": "tooltip-content", side: overTrigger ? 'bottom' : side, align: overTrigger ? 'start' : align, sideOffset: overTrigger ? 0 : sideOffset, avoidCollisions: overTrigger ? false : avoidCollisions, className: cn(S.tooltipContent, overTrigger && S.tooltipContentOverTrigger, className), ...props, style: style, children: [children, !overTrigger && jsx(TooltipPrimitive.Arrow, { className: S.tooltipArrow })] }) }));
20
105
  }
21
106
 
22
107
  export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
@@ -1,7 +1,7 @@
1
1
  import styleInject from 'style-inject';
2
2
 
3
- var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.Tooltip_tooltipContent__b3pS-{backdrop-filter:blur(10px);background-color:var(--popover);border:1px solid var(--border);border-radius:.375rem;box-shadow:0 10px 15px -3px rgba(0,0,0,.1);color:var(--popover-foreground);font-size:12px;padding:6px 12px;text-wrap:balance;transform-origin:var(--radix-tooltip-content-transform-origin);width:-moz-fit-content;width:fit-content;word-break:break-word;z-index:50}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open],.Tooltip_tooltipContent__b3pS-[data-state=instant-open],.Tooltip_tooltipContent__b3pS-[data-state=open]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=closed]{animation:Tooltip_fade-out__UOBET .1s ease-in,Tooltip_zoom-out__fodOk .1s ease-in}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open][data-side=bottom],.Tooltip_tooltipContent__b3pS-[data-state=instant-open][data-side=bottom],.Tooltip_tooltipContent__b3pS-[data-state=open][data-side=bottom]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out,Tooltip_slide-in-from-top-2__8uuS- .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open][data-side=left],.Tooltip_tooltipContent__b3pS-[data-state=instant-open][data-side=left],.Tooltip_tooltipContent__b3pS-[data-state=open][data-side=left]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out,Tooltip_slide-in-from-right-2__Uu79F .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open][data-side=right],.Tooltip_tooltipContent__b3pS-[data-state=instant-open][data-side=right],.Tooltip_tooltipContent__b3pS-[data-state=open][data-side=right]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out,Tooltip_slide-in-from-left-2__23kHm .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open][data-side=top],.Tooltip_tooltipContent__b3pS-[data-state=instant-open][data-side=top],.Tooltip_tooltipContent__b3pS-[data-state=open][data-side=top]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out,Tooltip_slide-in-from-bottom-2__O-Aa8 .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=closed][data-side=bottom],.Tooltip_tooltipContent__b3pS-[data-state=closed][data-side=left],.Tooltip_tooltipContent__b3pS-[data-state=closed][data-side=right],.Tooltip_tooltipContent__b3pS-[data-state=closed][data-side=top]{animation:Tooltip_fade-out__UOBET .1s ease-in,Tooltip_zoom-out__fodOk .1s ease-in}.Tooltip_tooltipArrow__87DVL{background-color:var(--popover);border-bottom:1px solid var(--border);border-left-width:1px;border-left:0 solid var(--border);border-radius:2px;border-right:1px solid var(--border);border-top-width:1px;border-top:0 solid var(--border);fill:var(--popover);height:10px;transform:translateY(calc(-50% + .5px)) rotate(45deg);width:10px;z-index:50}@keyframes Tooltip_fade-in__ZQqZv{0%{opacity:0}to{opacity:1}}@keyframes Tooltip_fade-out__UOBET{0%{opacity:1}to{opacity:0}}@keyframes Tooltip_zoom-in__SbWQb{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes Tooltip_zoom-out__fodOk{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}@keyframes Tooltip_slide-in-from-top-2__8uuS-{0%{opacity:0;transform:translateY(-.5rem)}to{opacity:1;transform:translateY(0)}}@keyframes Tooltip_slide-in-from-right-2__Uu79F{0%{opacity:0;transform:translateX(.5rem)}to{opacity:1;transform:translateX(0)}}@keyframes Tooltip_slide-in-from-left-2__23kHm{0%{opacity:0;transform:translateX(-.5rem)}to{opacity:1;transform:translateX(0)}}@keyframes Tooltip_slide-in-from-bottom-2__O-Aa8{0%{opacity:0;transform:translateY(.5rem)}to{opacity:1;transform:translateY(0)}}";
4
- var S = {"tooltipContent":"Tooltip_tooltipContent__b3pS-","fade-in":"Tooltip_fade-in__ZQqZv","zoom-in":"Tooltip_zoom-in__SbWQb","fade-out":"Tooltip_fade-out__UOBET","zoom-out":"Tooltip_zoom-out__fodOk","slide-in-from-top-2":"Tooltip_slide-in-from-top-2__8uuS-","slide-in-from-right-2":"Tooltip_slide-in-from-right-2__Uu79F","slide-in-from-left-2":"Tooltip_slide-in-from-left-2__23kHm","slide-in-from-bottom-2":"Tooltip_slide-in-from-bottom-2__O-Aa8","tooltipArrow":"Tooltip_tooltipArrow__87DVL"};
3
+ var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.Tooltip_tooltipContent__b3pS-{backdrop-filter:blur(10px);background-color:var(--popover);border:1px solid var(--border);border-radius:.375rem;box-shadow:0 10px 15px -3px rgba(0,0,0,.1);color:var(--popover-foreground);font-size:12px;padding:6px 12px;text-wrap:balance;transform-origin:var(--radix-tooltip-content-transform-origin);width:-moz-fit-content;width:fit-content;word-break:break-word;z-index:50}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open],.Tooltip_tooltipContent__b3pS-[data-state=instant-open],.Tooltip_tooltipContent__b3pS-[data-state=open]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=closed]{animation:Tooltip_fade-out__UOBET .1s ease-in,Tooltip_zoom-out__fodOk .1s ease-in}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open][data-side=bottom],.Tooltip_tooltipContent__b3pS-[data-state=instant-open][data-side=bottom],.Tooltip_tooltipContent__b3pS-[data-state=open][data-side=bottom]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out,Tooltip_slide-in-from-top-2__8uuS- .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open][data-side=left],.Tooltip_tooltipContent__b3pS-[data-state=instant-open][data-side=left],.Tooltip_tooltipContent__b3pS-[data-state=open][data-side=left]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out,Tooltip_slide-in-from-right-2__Uu79F .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open][data-side=right],.Tooltip_tooltipContent__b3pS-[data-state=instant-open][data-side=right],.Tooltip_tooltipContent__b3pS-[data-state=open][data-side=right]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out,Tooltip_slide-in-from-left-2__23kHm .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open][data-side=top],.Tooltip_tooltipContent__b3pS-[data-state=instant-open][data-side=top],.Tooltip_tooltipContent__b3pS-[data-state=open][data-side=top]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out,Tooltip_slide-in-from-bottom-2__O-Aa8 .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=closed][data-side=bottom],.Tooltip_tooltipContent__b3pS-[data-state=closed][data-side=left],.Tooltip_tooltipContent__b3pS-[data-state=closed][data-side=right],.Tooltip_tooltipContent__b3pS-[data-state=closed][data-side=top]{animation:Tooltip_fade-out__UOBET .1s ease-in,Tooltip_zoom-out__fodOk .1s ease-in}.Tooltip_tooltipContentOverTrigger__VQAU3{box-sizing:border-box;height:auto}.Tooltip_tooltipContentOverTrigger__VQAU3[data-state=delayed-open],.Tooltip_tooltipContentOverTrigger__VQAU3[data-state=instant-open],.Tooltip_tooltipContentOverTrigger__VQAU3[data-state=open]{animation:Tooltip_fade-in__ZQqZv .15s ease-out}.Tooltip_tooltipContentOverTrigger__VQAU3[data-state=closed]{animation:Tooltip_fade-out__UOBET .1s ease-in}.Tooltip_tooltipArrow__87DVL{background-color:var(--popover);border-bottom:1px solid var(--border);border-left-width:1px;border-left:0 solid var(--border);border-radius:2px;border-right:1px solid var(--border);border-top-width:1px;border-top:0 solid var(--border);fill:var(--popover);height:10px;transform:translateY(calc(-50% + .5px)) rotate(45deg);width:10px;z-index:50}@keyframes Tooltip_fade-in__ZQqZv{0%{opacity:0}to{opacity:1}}@keyframes Tooltip_fade-out__UOBET{0%{opacity:1}to{opacity:0}}@keyframes Tooltip_zoom-in__SbWQb{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes Tooltip_zoom-out__fodOk{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}@keyframes Tooltip_slide-in-from-top-2__8uuS-{0%{opacity:0;transform:translateY(-.5rem)}to{opacity:1;transform:translateY(0)}}@keyframes Tooltip_slide-in-from-right-2__Uu79F{0%{opacity:0;transform:translateX(.5rem)}to{opacity:1;transform:translateX(0)}}@keyframes Tooltip_slide-in-from-left-2__23kHm{0%{opacity:0;transform:translateX(-.5rem)}to{opacity:1;transform:translateX(0)}}@keyframes Tooltip_slide-in-from-bottom-2__O-Aa8{0%{opacity:0;transform:translateY(.5rem)}to{opacity:1;transform:translateY(0)}}";
4
+ var S = {"tooltipContent":"Tooltip_tooltipContent__b3pS-","fade-in":"Tooltip_fade-in__ZQqZv","zoom-in":"Tooltip_zoom-in__SbWQb","fade-out":"Tooltip_fade-out__UOBET","zoom-out":"Tooltip_zoom-out__fodOk","slide-in-from-top-2":"Tooltip_slide-in-from-top-2__8uuS-","slide-in-from-right-2":"Tooltip_slide-in-from-right-2__Uu79F","slide-in-from-left-2":"Tooltip_slide-in-from-left-2__23kHm","slide-in-from-bottom-2":"Tooltip_slide-in-from-bottom-2__O-Aa8","tooltipContentOverTrigger":"Tooltip_tooltipContentOverTrigger__VQAU3","tooltipArrow":"Tooltip_tooltipArrow__87DVL"};
5
5
  styleInject(css_248z);
6
6
 
7
7
  export { S as default };
@@ -10,8 +10,7 @@ import '../../ui/Sidebar/Sidebar.js';
10
10
  import 'lucide-react';
11
11
  import '../../ui/Page/Breadcrumbs/Breadcrumbs.styl.js';
12
12
  import '../../ui/Page/pageContext.js';
13
- import '@radix-ui/react-tooltip';
14
- import '../../ui/Tooltip/Tooltip.styl.js';
13
+ import '../../ui/Tooltip/Tooltip.js';
15
14
  import '../../ui/Page/PageHeader/PageHeader.styl.js';
16
15
  import '../../ui/Page/PageEmptyCanvas/PageEmptyCanvas.styl.js';
17
16
  import '../../ui/Page/PageContent/PageContent.styl.js';
@@ -1,6 +1,6 @@
1
1
  import type { TooltipContentProps, TooltipProps, TooltipProviderProps, TooltipTriggerProps } from './Tooltip.types';
2
2
  declare function TooltipProvider({ delayDuration, ...props }: TooltipProviderProps): import("react/jsx-runtime").JSX.Element;
3
- declare function Tooltip(props: TooltipProps): import("react/jsx-runtime").JSX.Element;
4
- declare function TooltipTrigger({ ...props }: TooltipTriggerProps): import("react/jsx-runtime").JSX.Element;
5
- declare function TooltipContent({ className, sideOffset, children, maxWidth, ...props }: TooltipContentProps): import("react/jsx-runtime").JSX.Element;
3
+ declare function Tooltip({ children, ...props }: TooltipProps): import("react/jsx-runtime").JSX.Element;
4
+ declare function TooltipTrigger({ ref, ...props }: TooltipTriggerProps): import("react/jsx-runtime").JSX.Element;
5
+ declare function TooltipContent({ className, sideOffset, children, maxWidth, overTrigger, align, side, avoidCollisions, style: styleProp, ...props }: TooltipContentProps): import("react/jsx-runtime").JSX.Element;
6
6
  export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
@@ -10,4 +10,5 @@ export interface TooltipContentProps extends React.ComponentProps<typeof Tooltip
10
10
  className?: string;
11
11
  sideOffset?: number;
12
12
  maxWidth?: number;
13
+ overTrigger?: boolean;
13
14
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.3.25",
3
+ "version": "1.3.26",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -1,4 +1,4 @@
1
- import { CSSProperties, useRef, useState } from 'react';
1
+ import { useRef, useState } from 'react';
2
2
 
3
3
  import {
4
4
  Tooltip,
@@ -18,9 +18,7 @@ function TextWithDeferTooltip({
18
18
  ...props
19
19
  }: TextWithDeferTooltipProps) {
20
20
  const [withTooltip, setWithTooltip] = useState(false);
21
- const [tooltipStyles, setTooltipStyles] = useState<CSSProperties>({});
22
21
  const ref = useRef<HTMLDivElement>(null);
23
- const cachedFontSizeRef = useRef<string | null>(null);
24
22
 
25
23
  const handleMouseEnter = () => {
26
24
  if (!ref.current) return;
@@ -31,33 +29,6 @@ function TextWithDeferTooltip({
31
29
  ref.current.scrollHeight - ref.current.clientHeight > 3;
32
30
 
33
31
  if (isOverflowingHorizontally || isOverflowingVertically) {
34
- const styles: CSSProperties = {
35
- fontSize: cachedFontSizeRef.current || undefined,
36
- };
37
-
38
- if (ref.current) {
39
- const {
40
- width: rectWidth,
41
- left,
42
- top,
43
- } = ref.current.getBoundingClientRect();
44
-
45
- styles.width = `${width ?? rectWidth}px`;
46
-
47
- if (!cachedFontSizeRef.current) {
48
- const { fontSize } = window.getComputedStyle(ref.current);
49
- cachedFontSizeRef.current = fontSize;
50
- styles.fontSize = fontSize;
51
- }
52
-
53
- if (overTrigger) {
54
- styles.transform = `translate(${left}px, ${top}px) !important`;
55
- // styles.translateX = `${left}px`;
56
- // styles.translateY = `${top}px`;
57
- }
58
- }
59
-
60
- setTooltipStyles(styles);
61
32
  setWithTooltip(true);
62
33
  }
63
34
  };
@@ -74,17 +45,14 @@ function TextWithDeferTooltip({
74
45
  );
75
46
 
76
47
  if (withTooltip) {
77
- const tooltipSide = overTrigger ? 'bottom' : side;
78
-
79
48
  return (
80
49
  <Tooltip open={withTooltip} onOpenChange={setWithTooltip}>
81
50
  <TooltipTrigger asChild>{textElement}</TooltipTrigger>
82
51
  <TooltipContent
83
- side={tooltipSide}
84
- style={{
85
- ...(maxWidth !== undefined && { maxWidth: `${maxWidth}px` }),
86
- ...tooltipStyles,
87
- }}
52
+ side={side}
53
+ overTrigger={overTrigger}
54
+ maxWidth={maxWidth}
55
+ style={width !== undefined ? { width: `${width}px` } : undefined}
88
56
  >
89
57
  {children}
90
58
  </TooltipContent>
@@ -57,6 +57,18 @@
57
57
  &[data-state="closed"][data-side="top"]
58
58
  animation fade-out 0.1s ease-in, zoom-out 0.1s ease-in
59
59
 
60
+ .tooltipContentOverTrigger
61
+ box-sizing border-box
62
+ height auto
63
+
64
+ &[data-state="open"],
65
+ &[data-state="instant-open"],
66
+ &[data-state="delayed-open"]
67
+ animation fade-in 0.15s ease-out
68
+
69
+ &[data-state="closed"]
70
+ animation fade-out 0.1s ease-in
71
+
60
72
  .tooltipArrow
61
73
  z-index 50
62
74
  width 10px
@@ -9,6 +9,7 @@ interface CssExports {
9
9
  'slide-in-from-top-2': string;
10
10
  'tooltipArrow': string;
11
11
  'tooltipContent': string;
12
+ 'tooltipContentOverTrigger': string;
12
13
  'zoom-in': string;
13
14
  'zoom-out': string;
14
15
  }
@@ -1,4 +1,13 @@
1
1
  import cn from 'classnames';
2
+ import {
3
+ type Ref,
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useLayoutEffect,
8
+ useMemo,
9
+ useRef,
10
+ } from 'react';
2
11
 
3
12
  import * as TooltipPrimitive from '@radix-ui/react-tooltip';
4
13
 
@@ -10,6 +19,83 @@ import type {
10
19
  TooltipTriggerProps,
11
20
  } from './Tooltip.types';
12
21
 
22
+ type TooltipContextValue = {
23
+ triggerRef: React.RefObject<HTMLElement | null>;
24
+ };
25
+
26
+ const TooltipContext = createContext<TooltipContextValue | null>(null);
27
+
28
+ function composeRefs<T>(...refs: (Ref<T> | undefined)[]) {
29
+ return (node: T | null) => {
30
+ for (const ref of refs) {
31
+ if (typeof ref === 'function') {
32
+ ref(node);
33
+ } else if (ref) {
34
+ ref.current = node;
35
+ }
36
+ }
37
+ };
38
+ }
39
+
40
+ function getPopperWrapper(contentEl: HTMLElement): HTMLElement | null {
41
+ const wrapper = contentEl.parentElement;
42
+ if (!wrapper?.hasAttribute('data-radix-popper-content-wrapper')) return null;
43
+
44
+ return wrapper;
45
+ }
46
+
47
+ const OVER_TRIGGER_OFFSET = { left: -10, top: -6 };
48
+
49
+ function applyOverTriggerStyles(
50
+ contentEl: HTMLElement,
51
+ triggerEl: HTMLElement,
52
+ ) {
53
+ const wrapper = getPopperWrapper(contentEl);
54
+ if (!wrapper) return;
55
+
56
+ const rect = triggerEl.getBoundingClientRect();
57
+ const computed = window.getComputedStyle(triggerEl);
58
+
59
+ wrapper.style.setProperty('position', 'fixed', 'important');
60
+ wrapper.style.setProperty(
61
+ 'left',
62
+ `${rect.left + OVER_TRIGGER_OFFSET.left}px`,
63
+ 'important',
64
+ );
65
+ wrapper.style.setProperty(
66
+ 'top',
67
+ `${rect.top + OVER_TRIGGER_OFFSET.top}px`,
68
+ 'important',
69
+ );
70
+ wrapper.style.setProperty('transform', 'none', 'important');
71
+ wrapper.style.setProperty('min-width', '0', 'important');
72
+ wrapper.style.setProperty('width', `${rect.width}px`, 'important');
73
+
74
+ contentEl.style.width = '100%';
75
+ contentEl.style.boxSizing = 'border-box';
76
+ contentEl.style.fontSize = computed.fontSize;
77
+ contentEl.style.lineHeight = computed.lineHeight;
78
+ }
79
+
80
+ function clearOverTriggerStyles(contentEl: HTMLElement | null) {
81
+ if (!contentEl) return;
82
+
83
+ const wrapper = getPopperWrapper(contentEl);
84
+ if (!wrapper) return;
85
+
86
+ wrapper.style.removeProperty('position');
87
+ wrapper.style.removeProperty('left');
88
+ wrapper.style.removeProperty('top');
89
+ wrapper.style.removeProperty('transform');
90
+ wrapper.style.removeProperty('min-width');
91
+ wrapper.style.removeProperty('width');
92
+
93
+ contentEl.style.removeProperty('width');
94
+ contentEl.style.removeProperty('box-sizing');
95
+ contentEl.style.removeProperty('font-size');
96
+ contentEl.style.removeProperty('line-height');
97
+ }
98
+
13
99
  function TooltipProvider({
14
100
  delayDuration = 0,
15
101
  ...props
@@ -23,16 +109,31 @@ function TooltipProvider({
23
109
  );
24
110
  }
25
111
 
26
- function Tooltip(props: TooltipProps) {
112
+ function Tooltip({ children, ...props }: TooltipProps) {
113
+ const triggerRef = useRef<HTMLElement | null>(null);
114
+ const contextValue = useMemo(() => ({ triggerRef }), []);
115
+
27
116
  return (
28
117
  <TooltipProvider>
29
- <TooltipPrimitive.Root data-slot="tooltip" {...props} />
118
+ <TooltipContext.Provider value={contextValue}>
119
+ <TooltipPrimitive.Root data-slot="tooltip" {...props}>
120
+ {children}
121
+ </TooltipPrimitive.Root>
122
+ </TooltipContext.Provider>
30
123
  </TooltipProvider>
31
124
  );
32
125
  }
33
126
 
34
- function TooltipTrigger({ ...props }: TooltipTriggerProps) {
35
- return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
127
+ function TooltipTrigger({ ref, ...props }: TooltipTriggerProps) {
128
+ const context = useContext(TooltipContext);
129
+
130
+ return (
131
+ <TooltipPrimitive.Trigger
132
+ ref={context ? composeRefs(context.triggerRef, ref) : ref}
133
+ data-slot="tooltip-trigger"
134
+ {...props}
135
+ />
136
+ );
36
137
  }
37
138
 
38
139
  function TooltipContent({
@@ -40,22 +141,69 @@ function TooltipContent({
40
141
  sideOffset = 0,
41
142
  children,
42
143
  maxWidth,
144
+ overTrigger = false,
145
+ align,
146
+ side,
147
+ avoidCollisions,
148
+ style: styleProp,
43
149
  ...props
44
150
  }: TooltipContentProps) {
45
- const style = { ...props.style };
151
+ const context = useContext(TooltipContext);
152
+ const contentRef = useRef<HTMLDivElement | null>(null);
153
+
154
+ const updateOverTriggerPosition = useCallback(() => {
155
+ const triggerEl = context?.triggerRef.current;
156
+ const contentEl = contentRef.current;
157
+ if (!overTrigger || !triggerEl || !contentEl) return;
158
+
159
+ applyOverTriggerStyles(contentEl, triggerEl);
160
+ }, [context, overTrigger]);
161
+
162
+ useLayoutEffect(() => {
163
+ if (!overTrigger) return;
164
+
165
+ let frameId = 0;
166
+
167
+ const tick = () => {
168
+ updateOverTriggerPosition();
169
+ frameId = requestAnimationFrame(tick);
170
+ };
171
+
172
+ tick();
173
+
174
+ window.addEventListener('scroll', updateOverTriggerPosition, true);
175
+ window.addEventListener('resize', updateOverTriggerPosition);
176
+
177
+ return () => {
178
+ cancelAnimationFrame(frameId);
179
+ window.removeEventListener('scroll', updateOverTriggerPosition, true);
180
+ window.removeEventListener('resize', updateOverTriggerPosition);
181
+ clearOverTriggerStyles(contentRef.current);
182
+ };
183
+ }, [overTrigger, updateOverTriggerPosition]);
184
+
185
+ const style = { ...styleProp };
46
186
  if (maxWidth) style.maxWidth = `${maxWidth}px`;
47
187
 
48
188
  return (
49
189
  <TooltipPrimitive.Portal>
50
190
  <TooltipPrimitive.Content
191
+ ref={contentRef}
51
192
  data-slot="tooltip-content"
52
- sideOffset={sideOffset}
53
- className={cn(S.tooltipContent, className)}
193
+ side={overTrigger ? 'bottom' : side}
194
+ align={overTrigger ? 'start' : align}
195
+ sideOffset={overTrigger ? 0 : sideOffset}
196
+ avoidCollisions={overTrigger ? false : avoidCollisions}
197
+ className={cn(
198
+ S.tooltipContent,
199
+ overTrigger && S.tooltipContentOverTrigger,
200
+ className,
201
+ )}
54
202
  {...props}
55
203
  style={style}
56
204
  >
57
205
  {children}
58
- <TooltipPrimitive.Arrow className={S.tooltipArrow} />
206
+ {!overTrigger && <TooltipPrimitive.Arrow className={S.tooltipArrow} />}
59
207
  </TooltipPrimitive.Content>
60
208
  </TooltipPrimitive.Portal>
61
209
  );
@@ -20,4 +20,5 @@ export interface TooltipContentProps extends React.ComponentProps<
20
20
  className?: string;
21
21
  sideOffset?: number;
22
22
  maxWidth?: number;
23
+ overTrigger?: boolean;
23
24
  }
@@ -4,6 +4,9 @@ import { TextWithDeferTooltip } from '#uilib/components/ui/TextWithDeferTooltip'
4
4
  import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
5
5
  import { DocsHeaderActions } from '../docsHeaderActions';
6
6
 
7
+ const LONG_LABEL =
8
+ 'This is a long label that should overflow and show a tooltip on hover.';
9
+
7
10
  export default function TextWithDeferTooltipPage() {
8
11
  return (
9
12
  <>
@@ -13,16 +16,29 @@ export default function TextWithDeferTooltipPage() {
13
16
  subheader="Tooltip only when text overflows."
14
17
  actions={<DocsHeaderActions />}
15
18
  />
16
- <PageContentSection
17
- style={{
18
- maxWidth: 120,
19
- border: '1px dashed var(--border)',
20
- padding: 8,
21
- }}
22
- >
23
- <TextWithDeferTooltip>
24
- This is a long label that should overflow and show a tooltip on hover.
25
- </TextWithDeferTooltip>
19
+ <PageContentSection>
20
+ <p style={{ margin: '0 0 8px', fontWeight: 600 }}>Default</p>
21
+ <div
22
+ style={{
23
+ maxWidth: 120,
24
+ border: '1px dashed var(--border)',
25
+ padding: 8,
26
+ }}
27
+ >
28
+ <TextWithDeferTooltip>{LONG_LABEL}</TextWithDeferTooltip>
29
+ </div>
30
+ </PageContentSection>
31
+ <PageContentSection>
32
+ <p style={{ margin: '0 0 8px', fontWeight: 600 }}>Over trigger</p>
33
+ <div
34
+ style={{
35
+ maxWidth: 120,
36
+ border: '1px dashed var(--border)',
37
+ padding: 8,
38
+ }}
39
+ >
40
+ <TextWithDeferTooltip overTrigger>{LONG_LABEL}</TextWithDeferTooltip>
41
+ </div>
26
42
  </PageContentSection>
27
43
  </>
28
44
  );
@@ -11,6 +11,9 @@ import { DocsHeaderActions } from '../docsHeaderActions';
11
11
 
12
12
  const TOOLTIP_SIDES = ['left', 'top', 'bottom', 'right'] as const;
13
13
 
14
+ const OVER_TRIGGER_TEXT =
15
+ 'Actual Order volume for Baerlocher MB 301, a compound that includes stearin as a component. MB 301 is typically used in construction-grade applications.';
16
+
14
17
  export default function TooltipPage() {
15
18
  return (
16
19
  <>
@@ -34,6 +37,33 @@ export default function TooltipPage() {
34
37
  ))}
35
38
  </div>
36
39
  </PageContentSection>
40
+ <PageContentSection>
41
+ <p style={{ margin: '0 0 8px', fontWeight: 600 }}>Over trigger</p>
42
+ <div
43
+ style={{
44
+ maxWidth: 280,
45
+ border: '1px dashed var(--border)',
46
+ padding: 8,
47
+ }}
48
+ >
49
+ <Tooltip>
50
+ <TooltipTrigger asChild>
51
+ <div
52
+ style={{
53
+ overflow: 'hidden',
54
+ textOverflow: 'ellipsis',
55
+ whiteSpace: 'nowrap',
56
+ }}
57
+ >
58
+ {OVER_TRIGGER_TEXT}
59
+ </div>
60
+ </TooltipTrigger>
61
+ <TooltipContent overTrigger maxWidth={400}>
62
+ {OVER_TRIGGER_TEXT}
63
+ </TooltipContent>
64
+ </Tooltip>
65
+ </div>
66
+ </PageContentSection>
37
67
  </>
38
68
  );
39
69
  }