|
13 | 13 |
|
14 | 14 | import React, {useEffect, useState} from 'react';
|
15 | 15 |
|
| 16 | +import {flip, offset, shift, useFloating, type VirtualElement} from '@floating-ui/react'; |
16 | 17 | import {pointer} from 'd3-selection';
|
17 |
| -import {usePopper} from 'react-popper'; |
18 | 18 |
|
19 | 19 | interface GraphTooltipProps {
|
20 | 20 | children: React.ReactNode;
|
21 |
| - x?: number; |
22 |
| - y?: number; |
23 | 21 | contextElement: Element | null;
|
24 |
| - isFixed?: boolean; |
25 |
| - virtualContextElement?: boolean; |
26 |
| - isContextMenuOpen?: boolean; |
27 | 22 | }
|
28 | 23 |
|
29 |
| -const virtualElement = { |
30 |
| - getBoundingClientRect: () => |
31 |
| - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions |
32 |
| - ({ |
33 |
| - width: 0, |
34 |
| - height: 0, |
35 |
| - top: 0, |
36 |
| - left: 0, |
37 |
| - right: 0, |
38 |
| - bottom: 0, |
39 |
| - } as DOMRect), |
40 |
| -}; |
41 |
| - |
42 |
| -function generateGetBoundingClientRect(contextElement: Element, x = 0, y = 0): () => DOMRect { |
| 24 | +function createPositionedVirtualElement(contextElement: Element, x = 0, y = 0): VirtualElement { |
43 | 25 | const domRect = contextElement.getBoundingClientRect();
|
44 |
| - return () => |
45 |
| - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions |
46 |
| - ({ |
| 26 | + return { |
| 27 | + getBoundingClientRect: () => ({ |
47 | 28 | width: 0,
|
48 | 29 | height: 0,
|
49 | 30 | top: domRect.y + y,
|
50 | 31 | left: domRect.x + x,
|
51 | 32 | right: domRect.x + x,
|
52 | 33 | bottom: domRect.y + y,
|
53 |
| - } as DOMRect); |
| 34 | + x: domRect.x + x, |
| 35 | + y: domRect.y + y, |
| 36 | + toJSON: () => ({}), |
| 37 | + }), |
| 38 | + }; |
54 | 39 | }
|
55 | 40 |
|
56 |
| -const GraphTooltip = ({ |
57 |
| - children, |
58 |
| - x, |
59 |
| - y, |
60 |
| - contextElement, |
61 |
| - isFixed = false, |
62 |
| - virtualContextElement = true, |
63 |
| - isContextMenuOpen = false, |
64 |
| -}: GraphTooltipProps): React.JSX.Element => { |
65 |
| - const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null); |
| 41 | +const GraphTooltip = ({children, contextElement}: GraphTooltipProps): React.JSX.Element => { |
| 42 | + const [isPositioned, setIsPositioned] = useState(false); |
66 | 43 |
|
67 |
| - const {styles, attributes, ...popperProps} = usePopper( |
68 |
| - virtualContextElement ? virtualElement : contextElement, |
69 |
| - popperElement, |
70 |
| - { |
71 |
| - placement: 'bottom-start', |
72 |
| - strategy: 'absolute', |
73 |
| - modifiers: [ |
74 |
| - { |
75 |
| - name: 'preventOverflow', |
76 |
| - options: { |
77 |
| - tether: false, |
78 |
| - altAxis: true, |
79 |
| - }, |
80 |
| - }, |
81 |
| - { |
82 |
| - name: 'offset', |
83 |
| - options: { |
84 |
| - offset: [30, 30], |
85 |
| - }, |
86 |
| - }, |
87 |
| - ], |
88 |
| - } |
89 |
| - ); |
| 44 | + const {refs, floatingStyles, update} = useFloating({ |
| 45 | + placement: 'bottom-start', |
| 46 | + strategy: 'absolute', |
| 47 | + middleware: [ |
| 48 | + offset({ |
| 49 | + mainAxis: 30, |
| 50 | + crossAxis: 30, |
| 51 | + }), |
| 52 | + flip(), |
| 53 | + shift({ |
| 54 | + padding: 5, |
| 55 | + }), |
| 56 | + ], |
| 57 | + whileElementsMounted: undefined, |
| 58 | + }); |
90 | 59 |
|
91 | 60 | useEffect(() => {
|
92 | 61 | if (contextElement === null) return;
|
93 |
| - const onMouseMove: EventListenerOrEventListenerObject = (e: Event) => { |
94 |
| - if (isContextMenuOpen) { |
95 |
| - return; |
96 |
| - } |
97 | 62 |
|
98 |
| - let tooltipX = x; |
99 |
| - let tooltipY = y; |
100 |
| - if (tooltipX == null || tooltipY == null) { |
101 |
| - const rel = pointer(e); |
102 |
| - tooltipX = rel[0]; |
103 |
| - tooltipY = rel[1]; |
104 |
| - } |
105 |
| - virtualElement.getBoundingClientRect = generateGetBoundingClientRect( |
106 |
| - contextElement, |
107 |
| - tooltipX, |
108 |
| - tooltipY |
109 |
| - ); |
110 |
| - |
111 |
| - void popperProps.update?.(); |
| 63 | + const onMouseMove: EventListenerOrEventListenerObject = (e: Event) => { |
| 64 | + const rel = pointer(e); |
| 65 | + const tooltipX = rel[0]; |
| 66 | + const tooltipY = rel[1]; |
| 67 | + const virtualElement = createPositionedVirtualElement(contextElement, tooltipX, tooltipY); |
| 68 | + refs.setReference(virtualElement); |
| 69 | + setIsPositioned(true); |
| 70 | + update(); |
112 | 71 | };
|
113 | 72 |
|
114 | 73 | contextElement.addEventListener('mousemove', onMouseMove);
|
115 | 74 | return () => {
|
116 | 75 | contextElement.removeEventListener('mousemove', onMouseMove);
|
117 | 76 | };
|
118 |
| - }, [contextElement, popperProps, x, y, isContextMenuOpen]); |
| 77 | + }, [contextElement, update, refs]); |
119 | 78 |
|
120 |
| - return isFixed ? ( |
121 |
| - <>{children}</> |
122 |
| - ) : ( |
123 |
| - <div ref={setPopperElement} style={styles.popper} {...attributes.popper} className="z-50"> |
| 79 | + return ( |
| 80 | + <div |
| 81 | + ref={refs.setFloating} |
| 82 | + style={{ |
| 83 | + ...floatingStyles, |
| 84 | + visibility: !isPositioned ? 'hidden' : 'visible', |
| 85 | + }} |
| 86 | + className="z-50 w-max" |
| 87 | + > |
124 | 88 | {children}
|
125 | 89 | </div>
|
126 | 90 | );
|
|
0 commit comments