From 04e62ad2b63dc4295606c456e0497808f0fb39fc Mon Sep 17 00:00:00 2001 From: Danny Delott Date: Mon, 10 Mar 2025 10:26:13 -0700 Subject: [PATCH 1/7] Install floating-ui --- apps/hyperdrive-trading/package.json | 2 ++ yarn.lock | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/apps/hyperdrive-trading/package.json b/apps/hyperdrive-trading/package.json index d96fe9300..9ff9fb49a 100644 --- a/apps/hyperdrive-trading/package.json +++ b/apps/hyperdrive-trading/package.json @@ -76,6 +76,8 @@ "react-hot-toast": "^2.4.0", "react-loading-skeleton": "^3.3.1", "react-use": "^17.4.2", + "@floating-ui/react": "0.27.5", + "@floating-ui/react-dom": "2.1.2", "rollbar": "^2.26.4", "semver": "^7.6.3", "viem": "^2.22.19", diff --git a/yarn.lock b/yarn.lock index e6a019742..bc34b5505 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3390,6 +3390,13 @@ "@floating-ui/core" "^1.6.0" "@floating-ui/utils" "^0.2.5" +"@floating-ui/react-dom@2.1.2", "@floating-ui/react-dom@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.2.tgz#a1349bbf6a0e5cb5ded55d023766f20a4d439a31" + integrity sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A== + dependencies: + "@floating-ui/dom" "^1.0.0" + "@floating-ui/react-dom@^2.0.0", "@floating-ui/react-dom@^2.1.1": version "2.1.1" resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.1.tgz#cca58b6b04fc92b4c39288252e285e0422291fb0" @@ -3397,6 +3404,15 @@ dependencies: "@floating-ui/dom" "^1.0.0" +"@floating-ui/react@0.27.5": + version "0.27.5" + resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.27.5.tgz#27a6e63a8ef35eb8712ef304a154ea706da26814" + integrity sha512-BX3jKxo39Ba05pflcQmqPPwc0qdNsdNi/eweAFtoIdrJWNen2sVEWMEac3i6jU55Qfx+lOcdMNKYn2CtWmlnOQ== + dependencies: + "@floating-ui/react-dom" "^2.1.2" + "@floating-ui/utils" "^0.2.9" + tabbable "^6.0.0" + "@floating-ui/react@^0.26.16": version "0.26.23" resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.26.23.tgz#28985e5ce482c34f347f28076f11267e47a933bd" @@ -3416,6 +3432,11 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.7.tgz#d0ece53ce99ab5a8e37ebdfe5e32452a2bfc073e" integrity sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA== +"@floating-ui/utils@^0.2.9": + version "0.2.9" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.9.tgz#50dea3616bc8191fb8e112283b49eaff03e78429" + integrity sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg== + "@google-cloud/paginator@^5.0.0": version "5.0.2" resolved "https://registry.yarnpkg.com/@google-cloud/paginator/-/paginator-5.0.2.tgz#86ad773266ce9f3b82955a8f75e22cd012ccc889" From 6763330dac365f4a2f7809803a8636c2a0062aa8 Mon Sep 17 00:00:00 2001 From: Danny Delott Date: Mon, 10 Mar 2025 10:27:01 -0700 Subject: [PATCH 2/7] Remove unused prop --- .../ui/hyperdrive/lp/AddLiquidityForm/AddLiquidityForm.tsx | 1 - .../src/ui/rewards/RewardsTooltip/RewardsTooltip.tsx | 7 ------- 2 files changed, 8 deletions(-) diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/lp/AddLiquidityForm/AddLiquidityForm.tsx b/apps/hyperdrive-trading/src/ui/hyperdrive/lp/AddLiquidityForm/AddLiquidityForm.tsx index a56499f3b..762dd6bb8 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/lp/AddLiquidityForm/AddLiquidityForm.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/lp/AddLiquidityForm/AddLiquidityForm.tsx @@ -525,7 +525,6 @@ function LpApyStat({ hyperdrive }: { hyperdrive: HyperdriveConfig }) { ) : rewards?.length ? ( ): ReactNode { const appConfig = useAppConfigForConnectedChain(); From 7599f742848d277dedfe9f7b61b7ae9aca4a7699 Mon Sep 17 00:00:00 2001 From: Danny Delott Date: Mon, 10 Mar 2025 11:04:03 -0700 Subject: [PATCH 3/7] Refactor RewardsTooltip to floating-ui tooltip --- .../ui/base/components/Popover/Popover.tsx | 252 ++++++++++++++++++ .../rewards/RewardsTooltip/RewardsTooltip.tsx | 43 +-- 2 files changed, 274 insertions(+), 21 deletions(-) create mode 100644 apps/hyperdrive-trading/src/ui/base/components/Popover/Popover.tsx diff --git a/apps/hyperdrive-trading/src/ui/base/components/Popover/Popover.tsx b/apps/hyperdrive-trading/src/ui/base/components/Popover/Popover.tsx new file mode 100644 index 000000000..576d54e59 --- /dev/null +++ b/apps/hyperdrive-trading/src/ui/base/components/Popover/Popover.tsx @@ -0,0 +1,252 @@ +/* + * This component file is lifted from https://floating-ui.com/docs/popover#reusable-popover-component + */ + +/* eslint-disable react/prop-types */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable react-refresh/only-export-components */ +import { + autoUpdate, + flip, + FloatingFocusManager, + FloatingPortal, + offset, + Placement, + shift, + useClick, + useDismiss, + useFloating, + useId, + useInteractions, + useMergeRefs, + useRole, +} from "@floating-ui/react"; +import "@floating-ui/react-dom"; +import * as React from "react"; + +interface PopoverOptions { + initialOpen?: boolean; + placement?: Placement; + modal?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; +} + +export function usePopover({ + initialOpen = false, + placement = "bottom", + modal, + open: controlledOpen, + onOpenChange: setControlledOpen, +}: PopoverOptions = {}) { + const [uncontrolledOpen, setUncontrolledOpen] = React.useState(initialOpen); + const [labelId, setLabelId] = React.useState(); + const [descriptionId, setDescriptionId] = React.useState< + string | undefined + >(); + + const open = controlledOpen ?? uncontrolledOpen; + const setOpen = setControlledOpen ?? setUncontrolledOpen; + + const data = useFloating({ + placement, + open, + onOpenChange: setOpen, + whileElementsMounted: autoUpdate, + middleware: [ + offset(5), + flip({ + crossAxis: placement.includes("-"), + fallbackAxisSideDirection: "end", + padding: 5, + }), + shift({ padding: 5 }), + ], + }); + + const context = data.context; + + const click = useClick(context, { + enabled: controlledOpen == null, + }); + const dismiss = useDismiss(context); + const role = useRole(context); + + const interactions = useInteractions([click, dismiss, role]); + + return React.useMemo( + () => ({ + open, + setOpen, + ...interactions, + ...data, + modal, + labelId, + descriptionId, + setLabelId, + setDescriptionId, + }), + [open, setOpen, interactions, data, modal, labelId, descriptionId], + ); +} + +type ContextType = + | (ReturnType & { + setLabelId: React.Dispatch>; + setDescriptionId: React.Dispatch< + React.SetStateAction + >; + }) + | null; + +const PopoverContext = React.createContext(null); + +export function usePopoverContext() { + const context = React.useContext(PopoverContext); + + if (context == null) { + throw new Error("Popover components must be wrapped in "); + } + + return context; +} + +export function Popover({ + children, + modal = false, + ...restOptions +}: { + children: React.ReactNode; +} & PopoverOptions) { + // This can accept any props as options, e.g. `placement`, + // or other positioning options. + const popover = usePopover({ modal, ...restOptions }); + return ( + + {children} + + ); +} + +interface PopoverTriggerProps { + children: React.ReactNode; + asChild?: boolean; +} + +export const PopoverTrigger = React.forwardRef< + HTMLElement, + React.HTMLProps & PopoverTriggerProps +>(function PopoverTrigger({ children, asChild = false, ...props }, propRef) { + const context = usePopoverContext(); + const childrenRef = (children as any).ref; + const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef]); + + // `asChild` allows the user to pass any element as the anchor + if (asChild && React.isValidElement(children)) { + return React.cloneElement( + children, + context.getReferenceProps({ + ref, + ...props, + ...children.props, + "data-state": context.open ? "open" : "closed", + }), + ); + } + + return ( + + ); +}); + +export const PopoverContent = React.forwardRef< + HTMLDivElement, + React.HTMLProps +>(function PopoverContent({ style, ...props }, propRef) { + const { context: floatingContext, ...context } = usePopoverContext(); + const ref = useMergeRefs([context.refs.setFloating, propRef]); + + if (!floatingContext.open) { + return null; + } + + return ( + + +
+ {props.children} +
+
+
+ ); +}); + +export const PopoverHeading = React.forwardRef< + HTMLHeadingElement, + React.HTMLProps +>(function PopoverHeading(props, ref) { + const { setLabelId } = usePopoverContext(); + const id = useId(); + + // Only sets `aria-labelledby` on the Popover root element + // if this component is mounted inside it. + React.useLayoutEffect(() => { + setLabelId(id); + return () => setLabelId(undefined); + }, [id, setLabelId]); + + return ( +

+ {props.children} +

+ ); +}); + +export const PopoverDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLProps +>(function PopoverDescription(props, ref) { + const { setDescriptionId } = usePopoverContext(); + const id = useId(); + + // Only sets `aria-describedby` on the Popover root element + // if this component is mounted inside it. + React.useLayoutEffect(() => { + setDescriptionId(id); + return () => setDescriptionId(undefined); + }, [id, setDescriptionId]); + + return

; +}); + +export const PopoverClose = React.forwardRef< + HTMLButtonElement, + React.ButtonHTMLAttributes +>(function PopoverClose(props, ref) { + const { setOpen } = usePopoverContext(); + return ( +