From fff92bb0f989252ffd97ba63b801029f83277992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=9F=83=E6=8B=89?= Date: Sat, 14 Dec 2024 20:53:31 +0800 Subject: [PATCH] wip: update --- .../core/Tooltip/Tooltip.module.scss | 79 ++++++++--- src/components/core/Tooltip/Tooltip.tsx | 130 ++++++++++++------ .../widgets/ChallengeCard/ChallengeCard.tsx | 1 + src/layouts/Base/Base.module.scss | 14 ++ src/layouts/Base/Base.tsx | 19 ++- src/layouts/Default/Default.tsx | 10 +- src/stores/tooltip.ts | 11 ++ 7 files changed, 193 insertions(+), 71 deletions(-) create mode 100644 src/stores/tooltip.ts diff --git a/src/components/core/Tooltip/Tooltip.module.scss b/src/components/core/Tooltip/Tooltip.module.scss index cfd7218..fb9ffbb 100644 --- a/src/components/core/Tooltip/Tooltip.module.scss +++ b/src/components/core/Tooltip/Tooltip.module.scss @@ -1,36 +1,81 @@ .root { - position: fixed; + position: absolute; background: #333; color: #fff; padding: 8px 12px; border-radius: 12px; font-size: 14px; white-space: nowrap; - z-index: 9999; + z-index: 1; transition: opacity 0.2s ease, visibility 0.2s ease; + inset: 0px auto auto 0px; + margin: 0; + user-select: none; - &[data-position="top"] { - transform: translate(-50%, -100%); - margin-bottom: var(--tooltip-offset, 8px); - } + &[data-arrow="true"] { + &[data-position="top"] { + &::after { + content: ""; + position: absolute; + bottom: 0; + left: 50%; + transform: translate(-50%, 100%); + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid #333; + } + } - &[data-position="bottom"] { - transform: translate(-50%, 0%); - margin-top: var(--tooltip-offset, 8px); - } + &[data-position="bottom"] { + &::before { + content: ""; + position: absolute; + top: 0; + left: 50%; + transform: translate(-50%, -100%); + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 5px solid #333; + } + } - &[data-position="left"] { - transform: translate(-100%, -50%); - margin-right: var(--tooltip-offset, 8px); - } + &[data-position="left"] { + &::before { + content: ""; + position: absolute; + top: 50%; + right: 0; + transform: translate(100%, -50%); + width: 0; + height: 0; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 5px solid #333; + } + } - &[data-position="right"] { - transform: translate(0%, -50%); - margin-left: var(--tooltip-offset, 8px); + &[data-position="right"] { + &::after { + content: ""; + position: absolute; + top: 50%; + left: 0; + transform: translate(-100%, -50%); + width: 0; + height: 0; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-right: 5px solid #333; + } + } } &.enter { diff --git a/src/components/core/Tooltip/Tooltip.tsx b/src/components/core/Tooltip/Tooltip.tsx index b2585f3..c53092c 100644 --- a/src/components/core/Tooltip/Tooltip.tsx +++ b/src/components/core/Tooltip/Tooltip.tsx @@ -11,6 +11,7 @@ import { CSSTransition } from "react-transition-group"; import { Box, BoxProps } from "../Box"; import clsx from "clsx"; import { createPortal } from "react-dom"; +import { useTooltipStore } from "@/stores/tooltip"; export interface TooltipProps extends Omit { content?: React.ReactNode; @@ -25,12 +26,14 @@ export function Tooltip(props: TooltipProps) { content, position = "top", offset = 8, + hasArrow = false, children, className, style, ...rest } = props; + const tooltipStore = useTooltipStore(); const triggerRef = useRef(null); const contentRef = useRef(null); const isHovered = useHover(triggerRef); @@ -38,78 +41,115 @@ export function Tooltip(props: TooltipProps) { const [tooltipStyle, setTooltipStyle] = useState({}); useEffect(() => { - if (!isHovered || !triggerRef.current || !contentRef.current) return; + if ( + !isHovered || + !triggerRef.current || + !contentRef.current || + !tooltipStore?.portal + ) + return; - const newStyle: CSSProperties = {}; + const triggerRect = triggerRef.current.getBoundingClientRect(); + const contentRect = contentRef.current.getBoundingClientRect(); + const portalRect = tooltipStore.portal.getBoundingClientRect(); - const triggerRect = triggerRef?.current?.getBoundingClientRect(); - const contentRect = contentRef?.current?.getBoundingClientRect(); + const newStyle: CSSProperties = {}; switch (position) { case "top": newStyle.top = - triggerRect?.top! - contentRect?.height! - offset; + triggerRect.top - + portalRect.top - + contentRect.height - + offset; newStyle.left = - triggerRect?.left! + - (triggerRect?.width! - contentRect?.width!) / 2; + triggerRect.left - + portalRect.left + + (triggerRect.width - contentRect.width) / 2; + break; + case "right": + newStyle.top = + triggerRect.top - + portalRect.top + + (triggerRect.height - contentRect.height) / 2; + newStyle.left = triggerRect.right - portalRect.left + offset; break; case "bottom": - newStyle.top = triggerRect?.bottom! + offset; + newStyle.top = triggerRect.bottom - portalRect.top + offset; newStyle.left = - triggerRect?.left! + - (triggerRect?.width! - contentRect?.width!) / 2; + triggerRect.left - + portalRect.left + + (triggerRect.width - contentRect.width) / 2; break; case "left": newStyle.top = - triggerRect?.top! + - (triggerRect?.height! - contentRect?.height!) / 2; + triggerRect.top - + portalRect.top + + (triggerRect.height - contentRect.height) / 2; newStyle.left = - triggerRect?.left! - contentRect?.width! - offset; + triggerRect.left - + portalRect.left - + contentRect.width - + offset; break; - case "right": - newStyle.top = - triggerRect?.top! + - (triggerRect?.height! - contentRect?.height!) / 2; - newStyle.left = triggerRect?.right! + offset; + default: break; } - newStyle.position = "fixed"; + newStyle.top = Math.max(0, Number(newStyle.top) || 0); + newStyle.left = Math.max(0, Number(newStyle.left) || 0); + setTooltipStyle(newStyle); - }, [isHovered, position, offset]); + }, [isHovered, position, offset, tooltipStore?.portal]); return ( <> {cloneElement(children, { ref: triggerRef, })} - {createPortal( - - - {content} - - , - document.body - )} + + {content} + {hasArrow && ( +
+ )} + + , + tooltipStore?.portal + )} ); } diff --git a/src/components/widgets/ChallengeCard/ChallengeCard.tsx b/src/components/widgets/ChallengeCard/ChallengeCard.tsx index 965d8a7..10a1b0b 100644 --- a/src/components/widgets/ChallengeCard/ChallengeCard.tsx +++ b/src/components/widgets/ChallengeCard/ChallengeCard.tsx @@ -69,6 +69,7 @@ export function ChallengeCard(props: ChallengeCard) { {Number(status?.solved_times) > 0 && ( diff --git a/src/layouts/Base/Base.module.scss b/src/layouts/Base/Base.module.scss index e69de29..c132872 100644 --- a/src/layouts/Base/Base.module.scss +++ b/src/layouts/Base/Base.module.scss @@ -0,0 +1,14 @@ +.wrapper { + overflow: auto; + height: 100vh; + width: 100%; + z-index: 0; + transition: all 0.2s ease-in-out; + display: flex; + flex-direction: column; + + main { + position: relative; + flex: 1; + } +} diff --git a/src/layouts/Base/Base.tsx b/src/layouts/Base/Base.tsx index 47804e0..37a6ef7 100644 --- a/src/layouts/Base/Base.tsx +++ b/src/layouts/Base/Base.tsx @@ -1,17 +1,34 @@ import { ErrorFallback } from "@/components/utils/ErrorFallback/ErrorFallback"; import { Toaster } from "@/components/widgets/Toaster"; +import { useTooltipStore } from "@/stores/tooltip"; import globalRouter from "@/utils/globalRouter"; +import { useEffect, useRef } from "react"; import { ErrorBoundary } from "react-error-boundary"; import { Outlet, useNavigate } from "react-router"; +import styles from "./Base.module.scss"; +import { Box } from "@/components/core"; export function Base() { const navigate = useNavigate(); globalRouter.navigate = navigate; + const tooltipStore = useTooltipStore(); + const ref = useRef(null); + + useEffect(() => { + if (ref.current) { + tooltipStore.setPortal(ref.current); + } + }, [ref.current]); + return ( <> - + +
+ +
+
diff --git a/src/layouts/Default/Default.tsx b/src/layouts/Default/Default.tsx index 6d0da74..9b11087 100644 --- a/src/layouts/Default/Default.tsx +++ b/src/layouts/Default/Default.tsx @@ -1,17 +1,11 @@ import { Navbar } from "@/components/widgets/Navbar"; -import styles from "./Default.module.scss"; import { Outlet } from "react-router"; -import { Box } from "@/components/core"; export function Default() { return ( <> -
- - - - -
+ + ); } diff --git a/src/stores/tooltip.ts b/src/stores/tooltip.ts new file mode 100644 index 0000000..43d1376 --- /dev/null +++ b/src/stores/tooltip.ts @@ -0,0 +1,11 @@ +import { create } from "zustand"; + +interface TooltipState { + portal?: HTMLDivElement; + setPortal: (el: HTMLDivElement) => void; +} + +export const useTooltipStore = create()((set) => ({ + porta: undefined, + setPortal: (el) => set({ portal: el }), +}));