From 09be4f19c8192ad19e3eb9aa88a867d3c29b47ea Mon Sep 17 00:00:00 2001 From: chefBingbong <133646395+ChefBingbong@users.noreply.github.com> Date: Fri, 7 Jul 2023 09:28:04 +0100 Subject: [PATCH] fix: tooltip refactor for buyCrypto entry (#7266) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 🤖 Generated by Copilot at f745f90 ### Summary 📝📱💰 This pull request enhances the `WalletInfo` component to help users buy more crypto, refactors the `useTooltip` hook to use `lodash` and improve performance, and updates the FAQ content for the `BuyCrypto` view. It also adds new types for the `useTooltip` hook. > _We're sailing on the crypto sea, me hearties, yo ho ho_ > _We're buying more BNB when our balance is low_ > _We're fixing up the `useTooltip` hook, with `debounce` and `deviceActions`_ > _We're moving all the FAQ items to a separate file, to avoid distractions_ ### Walkthrough * Add `InfoFilledIcon` component to `WalletInfo` component and adjust layout and logic of `buyCryptoTooltip` component ([link](https://github.com/pancakeswap/pancake-frontend/pull/7266/files?diff=unified&w=0#diff-667fa9ffb5f38ba848d89c70e2b060c60ce67fee0185391f612eb425169911beR15), [link](https://github.com/pancakeswap/pancake-frontend/pull/7266/files?diff=unified&w=0#diff-667fa9ffb5f38ba848d89c70e2b060c60ce67fee0185391f612eb425169911beL71-R86), [link](https://github.com/pancakeswap/pancake-frontend/pull/7266/files?diff=unified&w=0#diff-667fa9ffb5f38ba848d89c70e2b060c60ce67fee0185391f612eb425169911beL175-R182), [link](https://github.com/pancakeswap/pancake-frontend/pull/7266/files?diff=unified&w=0#diff-667fa9ffb5f38ba848d89c70e2b060c60ce67fee0185391f612eb425169911beL183-R192)) * Remove FAQ item from `BuyCrypto` view and move FAQ items to separate file ([link](https://github.com/pancakeswap/pancake-frontend/pull/7266/files?diff=unified&w=0#diff-2999e60a5b9510d1e96d02fe7ecd6283d03a0363972374f5d433eb7ecd52b60fL51-L73)) * Add `debounce` function and `DeviceAction` and `Devices` types to `useTooltip` hook and simplify event handling logic ([link](https://github.com/pancakeswap/pancake-frontend/pull/7266/files?diff=unified&w=0#diff-184e2bdd1dd0d58dab95d4efa5b52f7abd0c62c7833b5dec7f4347f21500d861R24-R33), [link](https://github.com/pancakeswap/pancake-frontend/pull/7266/files?diff=unified&w=0#diff-6fe052fda58ce75c3e945093975d8c35309b36402839fb6166f8cfadde2aa35eL7-R12), [link](https://github.com/pancakeswap/pancake-frontend/pull/7266/files?diff=unified&w=0#diff-6fe052fda58ce75c3e945093975d8c35309b36402839fb6166f8cfadde2aa35eR26-R36), [link](https://github.com/pancakeswap/pancake-frontend/pull/7266/files?diff=unified&w=0#diff-6fe052fda58ce75c3e945093975d8c35309b36402839fb6166f8cfadde2aa35eL52-L53), [link](https://github.com/pancakeswap/pancake-frontend/pull/7266/files?diff=unified&w=0#diff-6fe052fda58ce75c3e945093975d8c35309b36402839fb6166f8cfadde2aa35eL59-R91), [link](https://github.com/pancakeswap/pancake-frontend/pull/7266/files?diff=unified&w=0#diff-6fe052fda58ce75c3e945093975d8c35309b36402839fb6166f8cfadde2aa35eL96-R104), [link](https://github.com/pancakeswap/pancake-frontend/pull/7266/files?diff=unified&w=0#diff-6fe052fda58ce75c3e945093975d8c35309b36402839fb6166f8cfadde2aa35eL110-R111), [link](https://github.com/pancakeswap/pancake-frontend/pull/7266/files?diff=unified&w=0#diff-6fe052fda58ce75c3e945093975d8c35309b36402839fb6166f8cfadde2aa35eL125-R142), [link](https://github.com/pancakeswap/pancake-frontend/pull/7266/files?diff=unified&w=0#diff-6fe052fda58ce75c3e945093975d8c35309b36402839fb6166f8cfadde2aa35eR231)) --- .../components/Menu/UserMenu/WalletInfo.tsx | 26 +++-- .../localization/src/config/translations.json | 1 + packages/uikit/src/hooks/useTooltip/types.ts | 10 ++ .../uikit/src/hooks/useTooltip/useTooltip.tsx | 110 ++++++++---------- 4 files changed, 79 insertions(+), 68 deletions(-) diff --git a/apps/web/src/components/Menu/UserMenu/WalletInfo.tsx b/apps/web/src/components/Menu/UserMenu/WalletInfo.tsx index 7749dcf057353..d04effb430424 100644 --- a/apps/web/src/components/Menu/UserMenu/WalletInfo.tsx +++ b/apps/web/src/components/Menu/UserMenu/WalletInfo.tsx @@ -11,7 +11,7 @@ import { FlexGap, useTooltip, TooltipText, - InfoIcon, + InfoFilledIcon, } from '@pancakeswap/uikit' import { ChainId, WNATIVE } from '@pancakeswap/sdk' import { FetchStatus } from 'config/constants/types' @@ -68,15 +68,21 @@ const WalletInfo: React.FC = ({ hasLowNativeBalance, onDismiss targetRef: buyCryptoTargetRef, } = useTooltip( <> - - - {t('%currency% Balance Low. You need %currency% for transaction fees.', { - currency: native?.symbol, - })} - + + + + {t('%currency% Balance Low. You need %currency% for transaction fees.', { + currency: native?.symbol, + })} + + onDismiss?.()}> + + + , { + isInPortal: false, placement: isMobile ? 'top' : 'bottom', trigger: isMobile ? 'focus' : 'hover', ...(isMobile && { manualVisible: mobileTooltipShow }), @@ -172,7 +178,7 @@ const WalletInfo: React.FC = ({ hasLowNativeBalance, onDismiss fontWeight={Number(bnbBalance?.data?.value) === 0 ? 'bold' : 'normal'} color={Number(bnbBalance?.data?.value) === 0 ? 'warning' : 'normal'} > - {bnbBalance?.data?.value && formatBigInt(bnbBalance?.data?.value ?? 0n, 6)} + {formatBigInt(bnbBalance?.data?.value ?? 0n, 6)} = ({ hasLowNativeBalance, onDismiss display="flex" style={{ justifyContent: 'center' }} > - {Number(bnbBalance?.data?.value) === 0 ? : null} + {Number(bnbBalance?.data?.value) === 0 ? ( + + ) : null} {buyCryptoTooltipVisible && (!isMobile || mobileTooltipShow) && buyCryptoTooltip} diff --git a/packages/localization/src/config/translations.json b/packages/localization/src/config/translations.json index 22a89980ddf8e..573acd75c3c5b 100644 --- a/packages/localization/src/config/translations.json +++ b/packages/localization/src/config/translations.json @@ -2602,6 +2602,7 @@ "PancakeSwap Now Live on Polygon zkEVM!": "PancakeSwap Now Live on Polygon zkEVM!", "Polygon zkEVM is LIVE!": "Polygon zkEVM is LIVE!", "Swap and provide liquidity on Polygon zkEVM now": "Swap and provide liquidity on Polygon zkEVM now", + "Buy %currency%": "Buy %currency%", "Caution - METIS Token": "Caution - METIS Token", "Please exercise due caution when trading / providing liquidity for the METIS token. The protocol was recently affected by the": "Please exercise due caution when trading / providing liquidity for the METIS token. The protocol was recently affected by the", "PolyNetwork Exploit.": "PolyNetwork Exploit.", diff --git a/packages/uikit/src/hooks/useTooltip/types.ts b/packages/uikit/src/hooks/useTooltip/types.ts index cba7d8f5196f8..ed0c1cbb2e847 100644 --- a/packages/uikit/src/hooks/useTooltip/types.ts +++ b/packages/uikit/src/hooks/useTooltip/types.ts @@ -21,3 +21,13 @@ export interface TooltipOptions { } export type TriggerType = "click" | "hover" | "focus"; + +export interface DeviceAction { + start: string; + end: string; +} + +export enum Devices { + touchDevice = "touchDevice", + nonTouchDevice = "nonTouchDevice", +} diff --git a/packages/uikit/src/hooks/useTooltip/useTooltip.tsx b/packages/uikit/src/hooks/useTooltip/useTooltip.tsx index b20f8e0bdfafc..60c3c9e03ee07 100644 --- a/packages/uikit/src/hooks/useTooltip/useTooltip.tsx +++ b/packages/uikit/src/hooks/useTooltip/useTooltip.tsx @@ -1,14 +1,15 @@ import { AnimatePresence, Variants, LazyMotion, domAnimation } from "framer-motion"; -import React, { useCallback, useEffect, useRef, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { createPortal } from "react-dom"; import { usePopper } from "react-popper"; import { isMobile } from "react-device-detect"; import { DefaultTheme, ThemeProvider, useTheme } from "styled-components"; +import debounce from "lodash/debounce"; import { dark, light } from "../../theme"; import getPortalRoot from "../../util/getPortalRoot"; import isTouchDevice from "../../util/isTouchDevice"; import { Arrow, StyledTooltip } from "./StyledTooltip"; -import { TooltipOptions, TooltipRefs } from "./types"; +import { DeviceAction, Devices, TooltipOptions, TooltipRefs } from "./types"; const animationVariants: Variants = { initial: { opacity: 0 }, @@ -22,6 +23,17 @@ const animationMap = { exit: "exit", }; +const deviceActions: { [device in Devices]: DeviceAction } = { + [Devices.touchDevice]: { + start: "touchstart", + end: "touchend", + }, + [Devices.nonTouchDevice]: { + start: "mouseenter", + end: "mouseleave", + }, +}; + const invertTheme = (currentTheme: DefaultTheme) => { if (currentTheme.isDark) { return light; @@ -49,65 +61,54 @@ const useTooltip = (content: React.ReactNode, options?: TooltipOptions): Tooltip const [arrowElement, setArrowElement] = useState(null); const [visible, setVisible] = useState(manualVisible); - const isHoveringOverTooltip = useRef(false); - const hideTimeoutRef = useRef(); useEffect(() => { setVisible(manualVisible); }, [manualVisible]); + // eslint-disable-next-line react-hooks/exhaustive-deps + const debouncedHide = useCallback( + debounce(() => { + setVisible(false); + }, hideTimeout), + [hideTimeout] + ); + // using lodash debounce we can get rid of hideTimeout cleanups + // loadash's debounce handles cleanup it its implementation const hideTooltip = useCallback( (e: Event) => { if (manualVisible) return; - const hide = () => { - if (!avoidToStopPropagation) { - e.stopPropagation(); - e.preventDefault(); - } - - setVisible(false); - }; - + if (!avoidToStopPropagation) { + e.stopPropagation(); + e.preventDefault(); + } if (trigger === "hover") { - if (hideTimeoutRef.current) { - window.clearTimeout(hideTimeoutRef.current); - } - if (e.target === tooltipElement) { - isHoveringOverTooltip.current = false; - } - if (!isHoveringOverTooltip.current) { - hideTimeoutRef.current = window.setTimeout(() => { - if (!isHoveringOverTooltip.current) { - hide(); - } - }, hideTimeout); - } + debouncedHide(); } else { - hide(); + setVisible(false); } }, - [manualVisible, trigger, avoidToStopPropagation, tooltipElement, hideTimeout] + [manualVisible, trigger, debouncedHide, avoidToStopPropagation] ); const showTooltip = useCallback( (e: Event) => { setVisible(true); if (trigger === "hover") { - if (e.target === targetElement) { - // If we were about to close the tooltip and got back to it - // then prevent closing it. - clearTimeout(hideTimeoutRef.current); - } - if (e.target === tooltipElement) { - isHoveringOverTooltip.current = true; - } + // we dont need to make a inTooltipRef anymore, when we leave + // the target, hide tooltip is called for leaving the target, but show tooltip + // is called for entering the tooltip. since we enact a delay in hidetooltip, + // by the time the dylay is over lodash debounce will be cancelled until we leave the + // tooltip calling hidetooltip onece again to close. clever method jackson pointed me + // onto. saves a lot of nedless states and refs and listeners + debouncedHide.cancel(); } if (!avoidToStopPropagation) { e.stopPropagation(); e.preventDefault(); } }, - [tooltipElement, targetElement, trigger, avoidToStopPropagation] + [trigger, avoidToStopPropagation, debouncedHide] ); const toggleTooltip = useCallback( @@ -122,32 +123,23 @@ const useTooltip = (content: React.ReactNode, options?: TooltipOptions): Tooltip useEffect(() => { if (targetElement === null || trigger !== "hover" || manualVisible) return undefined; - if (isTouchDevice()) { - targetElement.addEventListener("touchstart", showTooltip); - targetElement.addEventListener("touchend", hideTooltip); - } else { - targetElement.addEventListener("mouseenter", showTooltip); - targetElement.addEventListener("mouseleave", hideTooltip); - } - return () => { - targetElement.removeEventListener("touchstart", showTooltip); - targetElement.removeEventListener("touchend", hideTooltip); - targetElement.removeEventListener("mouseenter", showTooltip); - targetElement.removeEventListener("mouseleave", showTooltip); - }; - }, [trigger, targetElement, hideTooltip, showTooltip, manualVisible]); + const eventHandlers = isTouchDevice() ? deviceActions.touchDevice : deviceActions.nonTouchDevice; - // Keep tooltip open when cursor moves from the targetElement to the tooltip - useEffect(() => { - if (tooltipElement === null || trigger !== "hover" || manualVisible) return undefined; + [targetElement, tooltipElement].forEach((element) => { + element?.addEventListener(eventHandlers.start, showTooltip); + element?.addEventListener(eventHandlers.end, hideTooltip); + }); - tooltipElement.addEventListener("mouseenter", showTooltip); - tooltipElement.addEventListener("mouseleave", hideTooltip); return () => { - tooltipElement.removeEventListener("mouseenter", showTooltip); - tooltipElement.removeEventListener("mouseleave", hideTooltip); + [targetElement, tooltipElement].forEach((element) => { + element?.removeEventListener(eventHandlers.start, showTooltip); + element?.removeEventListener(eventHandlers.end, hideTooltip); + debouncedHide.cancel(); + }); }; - }, [trigger, tooltipElement, hideTooltip, showTooltip, manualVisible]); + }, [trigger, targetElement, hideTooltip, showTooltip, manualVisible, tooltipElement, debouncedHide]); + + // no longer need the extra useeffect // Trigger = click useEffect(() => {