@sybilion/uilib 1.3.25 → 1.3.27

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,147 @@
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
+ const TRIGGER_TEXT_STYLE_PROPS = [
28
+ 'font-family',
29
+ 'font-size',
30
+ 'font-weight',
31
+ 'font-style',
32
+ 'font-variant',
33
+ 'font-stretch',
34
+ 'line-height',
35
+ 'letter-spacing',
36
+ 'word-spacing',
37
+ 'text-transform',
38
+ 'text-indent',
39
+ 'text-decoration-line',
40
+ 'text-decoration-color',
41
+ 'text-decoration-style',
42
+ 'text-decoration-thickness',
43
+ 'text-underline-offset',
44
+ 'font-feature-settings',
45
+ 'font-variation-settings',
46
+ 'font-kerning',
47
+ 'font-optical-sizing',
48
+ 'font-synthesis',
49
+ 'font-variant-numeric',
50
+ 'font-variant-ligatures',
51
+ 'font-variant-caps',
52
+ 'font-variant-east-asian',
53
+ 'tab-size',
54
+ 'color',
55
+ 'word-break',
56
+ 'overflow-wrap',
57
+ 'hyphens',
58
+ ];
59
+ function applyTriggerTextStyles(contentEl, computed) {
60
+ for (const prop of TRIGGER_TEXT_STYLE_PROPS) {
61
+ contentEl.style.setProperty(prop, computed.getPropertyValue(prop));
62
+ }
63
+ }
64
+ function clearTriggerTextStyles(contentEl) {
65
+ for (const prop of TRIGGER_TEXT_STYLE_PROPS) {
66
+ contentEl.style.removeProperty(prop);
67
+ }
68
+ }
69
+ function applyOverTriggerStyles(contentEl, triggerEl) {
70
+ const wrapper = getPopperWrapper(contentEl);
71
+ if (!wrapper)
72
+ return;
73
+ const rect = triggerEl.getBoundingClientRect();
74
+ const computed = window.getComputedStyle(triggerEl);
75
+ wrapper.style.setProperty('position', 'fixed', 'important');
76
+ wrapper.style.setProperty('left', `${rect.left + OVER_TRIGGER_OFFSET.left}px`, 'important');
77
+ wrapper.style.setProperty('top', `${rect.top + OVER_TRIGGER_OFFSET.top}px`, 'important');
78
+ wrapper.style.setProperty('transform', 'none', 'important');
79
+ wrapper.style.setProperty('min-width', '0', 'important');
80
+ wrapper.style.setProperty('width', `${rect.width}px`, 'important');
81
+ contentEl.style.width = '100%';
82
+ contentEl.style.boxSizing = 'border-box';
83
+ applyTriggerTextStyles(contentEl, computed);
84
+ }
85
+ function clearOverTriggerStyles(contentEl) {
86
+ if (!contentEl)
87
+ return;
88
+ const wrapper = getPopperWrapper(contentEl);
89
+ if (!wrapper)
90
+ return;
91
+ wrapper.style.removeProperty('position');
92
+ wrapper.style.removeProperty('left');
93
+ wrapper.style.removeProperty('top');
94
+ wrapper.style.removeProperty('transform');
95
+ wrapper.style.removeProperty('min-width');
96
+ wrapper.style.removeProperty('width');
97
+ contentEl.style.removeProperty('width');
98
+ contentEl.style.removeProperty('box-sizing');
99
+ clearTriggerTextStyles(contentEl);
100
+ }
6
101
  function TooltipProvider({ delayDuration = 0, ...props }) {
7
102
  return (jsx(TooltipPrimitive.Provider, { "data-slot": "tooltip-provider", delayDuration: delayDuration, ...props }));
8
103
  }
9
- function Tooltip(props) {
10
- return (jsx(TooltipProvider, { children: jsx(TooltipPrimitive.Root, { "data-slot": "tooltip", ...props }) }));
104
+ function Tooltip({ children, ...props }) {
105
+ const triggerRef = useRef(null);
106
+ const contextValue = useMemo(() => ({ triggerRef }), []);
107
+ return (jsx(TooltipProvider, { children: jsx(TooltipContext.Provider, { value: contextValue, children: jsx(TooltipPrimitive.Root, { "data-slot": "tooltip", ...props, children: children }) }) }));
11
108
  }
12
- function TooltipTrigger({ ...props }) {
13
- return jsx(TooltipPrimitive.Trigger, { "data-slot": "tooltip-trigger", ...props });
109
+ function TooltipTrigger({ ref, ...props }) {
110
+ const context = useContext(TooltipContext);
111
+ return (jsx(TooltipPrimitive.Trigger, { ref: context ? composeRefs(context.triggerRef, ref) : ref, "data-slot": "tooltip-trigger", ...props }));
14
112
  }
15
- function TooltipContent({ className, sideOffset = 0, children, maxWidth, ...props }) {
16
- const style = { ...props.style };
113
+ function TooltipContent({ className, sideOffset = 0, children, maxWidth, overTrigger = false, align, side, avoidCollisions, style: styleProp, ...props }) {
114
+ const context = useContext(TooltipContext);
115
+ const contentRef = useRef(null);
116
+ const updateOverTriggerPosition = useCallback(() => {
117
+ const triggerEl = context?.triggerRef.current;
118
+ const contentEl = contentRef.current;
119
+ if (!overTrigger || !triggerEl || !contentEl)
120
+ return;
121
+ applyOverTriggerStyles(contentEl, triggerEl);
122
+ }, [context, overTrigger]);
123
+ useLayoutEffect(() => {
124
+ if (!overTrigger)
125
+ return;
126
+ let frameId = 0;
127
+ const tick = () => {
128
+ updateOverTriggerPosition();
129
+ frameId = requestAnimationFrame(tick);
130
+ };
131
+ tick();
132
+ window.addEventListener('scroll', updateOverTriggerPosition, true);
133
+ window.addEventListener('resize', updateOverTriggerPosition);
134
+ return () => {
135
+ cancelAnimationFrame(frameId);
136
+ window.removeEventListener('scroll', updateOverTriggerPosition, true);
137
+ window.removeEventListener('resize', updateOverTriggerPosition);
138
+ clearOverTriggerStyles(contentRef.current);
139
+ };
140
+ }, [overTrigger, updateOverTriggerPosition]);
141
+ const style = { ...styleProp };
17
142
  if (maxWidth)
18
143
  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 })] }) }));
144
+ 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
145
  }
21
146
 
22
147
  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.27",
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,129 @@ 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
+ const TRIGGER_TEXT_STYLE_PROPS = [
50
+ 'font-family',
51
+ 'font-size',
52
+ 'font-weight',
53
+ 'font-style',
54
+ 'font-variant',
55
+ 'font-stretch',
56
+ 'line-height',
57
+ 'letter-spacing',
58
+ 'word-spacing',
59
+ 'text-transform',
60
+ 'text-indent',
61
+ 'text-decoration-line',
62
+ 'text-decoration-color',
63
+ 'text-decoration-style',
64
+ 'text-decoration-thickness',
65
+ 'text-underline-offset',
66
+ 'font-feature-settings',
67
+ 'font-variation-settings',
68
+ 'font-kerning',
69
+ 'font-optical-sizing',
70
+ 'font-synthesis',
71
+ 'font-variant-numeric',
72
+ 'font-variant-ligatures',
73
+ 'font-variant-caps',
74
+ 'font-variant-east-asian',
75
+ 'tab-size',
76
+ 'color',
77
+ 'word-break',
78
+ 'overflow-wrap',
79
+ 'hyphens',
80
+ ] as const;
81
+
82
+ function applyTriggerTextStyles(
83
+ contentEl: HTMLElement,
84
+ computed: CSSStyleDeclaration,
85
+ ) {
86
+ for (const prop of TRIGGER_TEXT_STYLE_PROPS) {
87
+ contentEl.style.setProperty(prop, computed.getPropertyValue(prop));
88
+ }
89
+ }
90
+
91
+ function clearTriggerTextStyles(contentEl: HTMLElement) {
92
+ for (const prop of TRIGGER_TEXT_STYLE_PROPS) {
93
+ contentEl.style.removeProperty(prop);
94
+ }
95
+ }
96
+
97
+ function applyOverTriggerStyles(
98
+ contentEl: HTMLElement,
99
+ triggerEl: HTMLElement,
100
+ ) {
101
+ const wrapper = getPopperWrapper(contentEl);
102
+ if (!wrapper) return;
103
+
104
+ const rect = triggerEl.getBoundingClientRect();
105
+ const computed = window.getComputedStyle(triggerEl);
106
+
107
+ wrapper.style.setProperty('position', 'fixed', 'important');
108
+ wrapper.style.setProperty(
109
+ 'left',
110
+ `${rect.left + OVER_TRIGGER_OFFSET.left}px`,
111
+ 'important',
112
+ );
113
+ wrapper.style.setProperty(
114
+ 'top',
115
+ `${rect.top + OVER_TRIGGER_OFFSET.top}px`,
116
+ 'important',
117
+ );
118
+ wrapper.style.setProperty('transform', 'none', 'important');
119
+ wrapper.style.setProperty('min-width', '0', 'important');
120
+ wrapper.style.setProperty('width', `${rect.width}px`, 'important');
121
+
122
+ contentEl.style.width = '100%';
123
+ contentEl.style.boxSizing = 'border-box';
124
+ applyTriggerTextStyles(contentEl, computed);
125
+ }
126
+
127
+ function clearOverTriggerStyles(contentEl: HTMLElement | null) {
128
+ if (!contentEl) return;
129
+
130
+ const wrapper = getPopperWrapper(contentEl);
131
+ if (!wrapper) return;
132
+
133
+ wrapper.style.removeProperty('position');
134
+ wrapper.style.removeProperty('left');
135
+ wrapper.style.removeProperty('top');
136
+ wrapper.style.removeProperty('transform');
137
+ wrapper.style.removeProperty('min-width');
138
+ wrapper.style.removeProperty('width');
139
+
140
+ contentEl.style.removeProperty('width');
141
+ contentEl.style.removeProperty('box-sizing');
142
+ clearTriggerTextStyles(contentEl);
143
+ }
144
+
13
145
  function TooltipProvider({
14
146
  delayDuration = 0,
15
147
  ...props
@@ -23,16 +155,31 @@ function TooltipProvider({
23
155
  );
24
156
  }
25
157
 
26
- function Tooltip(props: TooltipProps) {
158
+ function Tooltip({ children, ...props }: TooltipProps) {
159
+ const triggerRef = useRef<HTMLElement | null>(null);
160
+ const contextValue = useMemo(() => ({ triggerRef }), []);
161
+
27
162
  return (
28
163
  <TooltipProvider>
29
- <TooltipPrimitive.Root data-slot="tooltip" {...props} />
164
+ <TooltipContext.Provider value={contextValue}>
165
+ <TooltipPrimitive.Root data-slot="tooltip" {...props}>
166
+ {children}
167
+ </TooltipPrimitive.Root>
168
+ </TooltipContext.Provider>
30
169
  </TooltipProvider>
31
170
  );
32
171
  }
33
172
 
34
- function TooltipTrigger({ ...props }: TooltipTriggerProps) {
35
- return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
173
+ function TooltipTrigger({ ref, ...props }: TooltipTriggerProps) {
174
+ const context = useContext(TooltipContext);
175
+
176
+ return (
177
+ <TooltipPrimitive.Trigger
178
+ ref={context ? composeRefs(context.triggerRef, ref) : ref}
179
+ data-slot="tooltip-trigger"
180
+ {...props}
181
+ />
182
+ );
36
183
  }
37
184
 
38
185
  function TooltipContent({
@@ -40,22 +187,69 @@ function TooltipContent({
40
187
  sideOffset = 0,
41
188
  children,
42
189
  maxWidth,
190
+ overTrigger = false,
191
+ align,
192
+ side,
193
+ avoidCollisions,
194
+ style: styleProp,
43
195
  ...props
44
196
  }: TooltipContentProps) {
45
- const style = { ...props.style };
197
+ const context = useContext(TooltipContext);
198
+ const contentRef = useRef<HTMLDivElement | null>(null);
199
+
200
+ const updateOverTriggerPosition = useCallback(() => {
201
+ const triggerEl = context?.triggerRef.current;
202
+ const contentEl = contentRef.current;
203
+ if (!overTrigger || !triggerEl || !contentEl) return;
204
+
205
+ applyOverTriggerStyles(contentEl, triggerEl);
206
+ }, [context, overTrigger]);
207
+
208
+ useLayoutEffect(() => {
209
+ if (!overTrigger) return;
210
+
211
+ let frameId = 0;
212
+
213
+ const tick = () => {
214
+ updateOverTriggerPosition();
215
+ frameId = requestAnimationFrame(tick);
216
+ };
217
+
218
+ tick();
219
+
220
+ window.addEventListener('scroll', updateOverTriggerPosition, true);
221
+ window.addEventListener('resize', updateOverTriggerPosition);
222
+
223
+ return () => {
224
+ cancelAnimationFrame(frameId);
225
+ window.removeEventListener('scroll', updateOverTriggerPosition, true);
226
+ window.removeEventListener('resize', updateOverTriggerPosition);
227
+ clearOverTriggerStyles(contentRef.current);
228
+ };
229
+ }, [overTrigger, updateOverTriggerPosition]);
230
+
231
+ const style = { ...styleProp };
46
232
  if (maxWidth) style.maxWidth = `${maxWidth}px`;
47
233
 
48
234
  return (
49
235
  <TooltipPrimitive.Portal>
50
236
  <TooltipPrimitive.Content
237
+ ref={contentRef}
51
238
  data-slot="tooltip-content"
52
- sideOffset={sideOffset}
53
- className={cn(S.tooltipContent, className)}
239
+ side={overTrigger ? 'bottom' : side}
240
+ align={overTrigger ? 'start' : align}
241
+ sideOffset={overTrigger ? 0 : sideOffset}
242
+ avoidCollisions={overTrigger ? false : avoidCollisions}
243
+ className={cn(
244
+ S.tooltipContent,
245
+ overTrigger && S.tooltipContentOverTrigger,
246
+ className,
247
+ )}
54
248
  {...props}
55
249
  style={style}
56
250
  >
57
251
  {children}
58
- <TooltipPrimitive.Arrow className={S.tooltipArrow} />
252
+ {!overTrigger && <TooltipPrimitive.Arrow className={S.tooltipArrow} />}
59
253
  </TooltipPrimitive.Content>
60
254
  </TooltipPrimitive.Portal>
61
255
  );
@@ -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,34 @@ 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
+ fontWeight: 600,
57
+ }}
58
+ >
59
+ {OVER_TRIGGER_TEXT}
60
+ </div>
61
+ </TooltipTrigger>
62
+ <TooltipContent overTrigger maxWidth={400}>
63
+ {OVER_TRIGGER_TEXT}
64
+ </TooltipContent>
65
+ </Tooltip>
66
+ </div>
67
+ </PageContentSection>
37
68
  </>
38
69
  );
39
70
  }