Skip to content

Commit 6dc9a41

Browse files
authored
Install floating-ui, refactor RewardsTooltip (#1822)
* Install floating-ui * Remove unused prop * Refactor RewardsTooltip to floating-ui tooltip * Build wasm pacakges * Refactor PoolStat, remove radix-ui * Fix default active styling * Use previous default placement
1 parent e91760f commit 6dc9a41

File tree

14 files changed

+787
-708
lines changed

14 files changed

+787
-708
lines changed

apps/hyperdrive-trading/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
"@delvtech/hyperdrive-js": "^0.0.2",
3737
"@headlessui/react": "^2.1.5",
3838
"@heroicons/react": "^2.0.16",
39-
"@radix-ui/react-tooltip": "^1.1.8",
4039
"@rainbow-me/rainbowkit": "^2.2.3",
4140
"@rollbar/react": "^0.11.1",
4241
"@tanstack/query-core": "^4.36.1",
@@ -76,6 +75,7 @@
7675
"react-hot-toast": "^2.4.0",
7776
"react-loading-skeleton": "^3.3.1",
7877
"react-use": "^17.4.2",
78+
"@floating-ui/react": "0.27.5",
7979
"rollbar": "^2.26.4",
8080
"semver": "^7.6.3",
8181
"viem": "^2.22.19",
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
/*
2+
* This component file is lifted from https://floating-ui.com/docs/popover#reusable-popover-component
3+
*/
4+
5+
/* eslint-disable react/prop-types */
6+
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
7+
import {
8+
autoUpdate,
9+
flip,
10+
FloatingFocusManager,
11+
FloatingPortal,
12+
offset,
13+
Placement,
14+
safePolygon,
15+
shift,
16+
useDismiss,
17+
useFloating,
18+
useHover,
19+
useId,
20+
useInteractions,
21+
useMergeRefs,
22+
useRole,
23+
} from "@floating-ui/react";
24+
import * as React from "react";
25+
26+
interface PopoverOptions {
27+
initialOpen?: boolean;
28+
placement?: Placement;
29+
modal?: boolean;
30+
open?: boolean;
31+
onOpenChange?: (open: boolean) => void;
32+
}
33+
34+
function usePopover({
35+
initialOpen = false,
36+
placement = "top",
37+
modal,
38+
open: controlledOpen,
39+
onOpenChange: setControlledOpen,
40+
}: PopoverOptions = {}) {
41+
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(initialOpen);
42+
const [labelId, setLabelId] = React.useState<string | undefined>();
43+
const [descriptionId, setDescriptionId] = React.useState<
44+
string | undefined
45+
>();
46+
47+
const open = controlledOpen ?? uncontrolledOpen;
48+
const setOpen = setControlledOpen ?? setUncontrolledOpen;
49+
50+
const data = useFloating({
51+
placement,
52+
open,
53+
onOpenChange: setOpen,
54+
whileElementsMounted: autoUpdate,
55+
middleware: [
56+
offset(5),
57+
flip({
58+
crossAxis: placement.includes("-"),
59+
fallbackAxisSideDirection: "end",
60+
padding: 5,
61+
}),
62+
shift({ padding: 5 }),
63+
],
64+
});
65+
66+
const context = data.context;
67+
68+
const hover = useHover(context, {
69+
// allows the popover to remain open while user's cursor is inside it
70+
// https://floating-ui.com/docs/usehover#handleclose
71+
handleClose: safePolygon(),
72+
});
73+
const dismiss = useDismiss(context);
74+
const role = useRole(context);
75+
76+
const interactions = useInteractions([hover, dismiss, role]);
77+
78+
return React.useMemo(
79+
() => ({
80+
open,
81+
setOpen,
82+
...interactions,
83+
...data,
84+
modal,
85+
labelId,
86+
descriptionId,
87+
setLabelId,
88+
setDescriptionId,
89+
}),
90+
[open, setOpen, interactions, data, modal, labelId, descriptionId],
91+
);
92+
}
93+
94+
type ContextType =
95+
| (ReturnType<typeof usePopover> & {
96+
setLabelId: React.Dispatch<React.SetStateAction<string | undefined>>;
97+
setDescriptionId: React.Dispatch<
98+
React.SetStateAction<string | undefined>
99+
>;
100+
})
101+
| null;
102+
103+
const PopoverContext = React.createContext<ContextType>(null);
104+
105+
function usePopoverContext() {
106+
const context = React.useContext(PopoverContext);
107+
108+
if (context == null) {
109+
throw new Error("Popover components must be wrapped in <Popover />");
110+
}
111+
112+
return context;
113+
}
114+
115+
export function Popover({
116+
children,
117+
modal = false,
118+
...restOptions
119+
}: {
120+
children: React.ReactNode;
121+
} & PopoverOptions) {
122+
// This can accept any props as options, e.g. `placement`,
123+
// or other positioning options.
124+
const popover = usePopover({ modal, ...restOptions });
125+
return (
126+
<PopoverContext.Provider value={popover}>
127+
{children}
128+
</PopoverContext.Provider>
129+
);
130+
}
131+
132+
interface PopoverTriggerProps {
133+
children: React.ReactNode;
134+
asChild?: boolean;
135+
}
136+
137+
export const PopoverTrigger = React.forwardRef<
138+
HTMLElement,
139+
React.HTMLProps<HTMLElement> & PopoverTriggerProps
140+
>(function PopoverTrigger({ children, asChild = false, ...props }, propRef) {
141+
const context = usePopoverContext();
142+
const childrenRef = (children as any).ref;
143+
const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef]);
144+
145+
// `asChild` allows the user to pass any element as the anchor
146+
if (asChild && React.isValidElement(children)) {
147+
return React.cloneElement(
148+
children,
149+
context.getReferenceProps({
150+
ref,
151+
...props,
152+
...children.props,
153+
"data-state": context.open ? "open" : "closed",
154+
}),
155+
);
156+
}
157+
158+
return (
159+
<button
160+
ref={ref}
161+
type="button"
162+
// The user can style the trigger based on the state
163+
data-state={context.open ? "open" : "closed"}
164+
{...context.getReferenceProps(props)}
165+
>
166+
{children}
167+
</button>
168+
);
169+
});
170+
171+
export const PopoverContent = React.forwardRef<
172+
HTMLDivElement,
173+
React.HTMLProps<HTMLDivElement>
174+
>(function PopoverContent({ style, ...props }, propRef) {
175+
const { context: floatingContext, ...context } = usePopoverContext();
176+
const ref = useMergeRefs([context.refs.setFloating, propRef]);
177+
178+
if (!floatingContext.open) {
179+
return null;
180+
}
181+
182+
return (
183+
<FloatingPortal>
184+
<FloatingFocusManager
185+
initialFocus={
186+
/* Don't set an initial focus element prevents focus styling from
187+
* appearing inside the popover when opened */
188+
-1
189+
}
190+
context={floatingContext}
191+
modal={context.modal}
192+
>
193+
<div
194+
ref={ref}
195+
style={{ ...context.floatingStyles, ...style }}
196+
aria-labelledby={context.labelId}
197+
aria-describedby={context.descriptionId}
198+
{...context.getFloatingProps(props)}
199+
>
200+
{props.children}
201+
</div>
202+
</FloatingFocusManager>
203+
</FloatingPortal>
204+
);
205+
});
206+
207+
export const PopoverHeading = React.forwardRef<
208+
HTMLHeadingElement,
209+
React.HTMLProps<HTMLHeadingElement>
210+
>(function PopoverHeading(props, ref) {
211+
const { setLabelId } = usePopoverContext();
212+
const id = useId();
213+
214+
// Only sets `aria-labelledby` on the Popover root element
215+
// if this component is mounted inside it.
216+
React.useLayoutEffect(() => {
217+
setLabelId(id);
218+
return () => setLabelId(undefined);
219+
}, [id, setLabelId]);
220+
221+
return (
222+
<h2 {...props} ref={ref} id={id}>
223+
{props.children}
224+
</h2>
225+
);
226+
});
227+
228+
export const PopoverDescription = React.forwardRef<
229+
HTMLParagraphElement,
230+
React.HTMLProps<HTMLParagraphElement>
231+
>(function PopoverDescription(props, ref) {
232+
const { setDescriptionId } = usePopoverContext();
233+
const id = useId();
234+
235+
// Only sets `aria-describedby` on the Popover root element
236+
// if this component is mounted inside it.
237+
React.useLayoutEffect(() => {
238+
setDescriptionId(id);
239+
return () => setDescriptionId(undefined);
240+
}, [id, setDescriptionId]);
241+
242+
return <p {...props} ref={ref} id={id} />;
243+
});
244+
245+
export const PopoverClose = React.forwardRef<
246+
HTMLButtonElement,
247+
React.ButtonHTMLAttributes<HTMLButtonElement>
248+
>(function PopoverClose(props, ref) {
249+
const { setOpen } = usePopoverContext();
250+
return (
251+
<button
252+
type="button"
253+
ref={ref}
254+
{...props}
255+
onClick={(event) => {
256+
props.onClick?.(event);
257+
setOpen(false);
258+
}}
259+
/>
260+
);
261+
});

apps/hyperdrive-trading/src/ui/hyperdrive/lp/AddLiquidityForm/AddLiquidityForm.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,6 @@ function LpApyStat({ hyperdrive }: { hyperdrive: HyperdriveConfig }) {
525525
) : rewards?.length ? (
526526
<RewardsTooltip
527527
position="addLiquidity"
528-
showMiles
529528
hyperdriveAddress={hyperdrive.address}
530529
baseRate={lpApy?.lpApy}
531530
netRate={lpApy?.netLpApy}

apps/hyperdrive-trading/src/ui/main.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { RainbowKitProvider } from "@rainbow-me/rainbowkit";
22
import "@rainbow-me/rainbowkit/styles.css";
33
import "@usecapsule/react-sdk/styles.css";
44

5-
import * as Tooltip from "@radix-ui/react-tooltip";
65
import {
76
ErrorBoundary as RollbarErrorBoundary,
87
Provider as RollbarProvider,
@@ -70,9 +69,7 @@ root.render(
7069
>
7170
<RollbarErrorBoundary>
7271
<RegionInfoProvider>
73-
<Tooltip.Provider>
74-
<App />
75-
</Tooltip.Provider>
72+
<App />
7673
</RegionInfoProvider>
7774
</RollbarErrorBoundary>
7875
</RollbarProvider>

apps/hyperdrive-trading/src/ui/markets/PoolRow/PoolStat.tsx

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
import * as Tooltip from "@radix-ui/react-tooltip";
21
import classNames from "classnames";
32
import { ReactElement, ReactNode } from "react";
43
import Skeleton from "react-loading-skeleton";
4+
import {
5+
Popover,
6+
PopoverContent,
7+
PopoverTrigger,
8+
} from "src/ui/base/components/Popover/Popover";
59

610
export interface PoolStatProps {
711
label: string;
@@ -47,20 +51,14 @@ export function PoolStat({
4751

4852
if (overlay) {
4953
return (
50-
<Tooltip.Root>
51-
<Tooltip.Trigger className="flex w-full items-center whitespace-nowrap">
54+
<Popover>
55+
<PopoverTrigger className="flex w-full items-center whitespace-nowrap">
5256
{poolStat}
53-
</Tooltip.Trigger>
54-
<Tooltip.Portal>
55-
<Tooltip.Content
56-
className="z-20 h-fit w-72 rounded-[0.6rem] bg-base-200 px-3 py-2 shadow-2xl"
57-
sideOffset={5}
58-
collisionPadding={12}
59-
>
60-
{overlay}
61-
</Tooltip.Content>
62-
</Tooltip.Portal>
63-
</Tooltip.Root>
57+
</PopoverTrigger>
58+
<PopoverContent className="z-20 h-fit w-72 rounded-[0.6rem] bg-base-200 px-3 py-2 shadow-2xl">
59+
{overlay}
60+
</PopoverContent>
61+
</Popover>
6462
);
6563
}
6664

0 commit comments

Comments
 (0)