From 303f7b136a6538dd2ab1b514ada407d53c396bdb Mon Sep 17 00:00:00 2001 From: Ou Date: Sun, 13 Oct 2024 01:32:32 +0800 Subject: [PATCH] feat: support go to top button --- src/atoms.ts | 5 ++ src/components/common/overlay-scrollbar.tsx | 64 ++++++++++++++++++++- src/components/header.tsx | 15 ++++- src/routes/__root.tsx | 12 ++-- 4 files changed, 88 insertions(+), 8 deletions(-) diff --git a/src/atoms.ts b/src/atoms.ts index 987ee56..ca2537d 100644 --- a/src/atoms.ts +++ b/src/atoms.ts @@ -59,3 +59,8 @@ export const currentSectionAtom = atom((get) => { }) export type Update = T | ((prev: T) => T) + +export const goToTopAtom = atom({ + ok: false, + fn: undefined as (() => void) | undefined, +}) diff --git a/src/components/common/overlay-scrollbar.tsx b/src/components/common/overlay-scrollbar.tsx index 838cfe5..cc98940 100644 --- a/src/components/common/overlay-scrollbar.tsx +++ b/src/components/common/overlay-scrollbar.tsx @@ -1,8 +1,10 @@ import type { UseOverlayScrollbarsParams } from "overlayscrollbars-react" import { useOverlayScrollbars } from "overlayscrollbars-react" import type { HTMLProps, PropsWithChildren } from "react" -import { useEffect, useMemo, useRef } from "react" +import { useCallback, useEffect, useMemo, useRef } from "react" import { defu } from "defu" +import { useSetAtom } from "jotai" +import { goToTopAtom } from "~/atoms" type Props = HTMLProps & UseOverlayScrollbarsParams const defaultScrollbarParams: UseOverlayScrollbarsParams = { @@ -41,3 +43,63 @@ export function OverlayScrollbar({ children, options, events, defer, ...props }: ) } + +export function GlobalOverlayScrollbar({ children, ...props }: PropsWithChildren>) { + const ref = useRef(null) + const lastScroll = useRef(0) + const timer = useRef() + const setGoToTop = useSetAtom(goToTopAtom) + const onScroll = useCallback((e: Event) => { + const now = Date.now() + if (now - lastScroll.current > 50) { + lastScroll.current = now + clearTimeout(timer.current) + timer.current = setTimeout( + () => { + const el = e.target as HTMLElement + setGoToTop({ + ok: el.scrollTop > 100, + fn: () => el.scrollTo({ top: 0, behavior: "smooth" }), + }) + }, + 500, + ) + } + }, [setGoToTop]) + const [initialize] = useOverlayScrollbars({ + options: { + scrollbars: { + autoHide: "scroll", + }, + }, + events: { + scroll: (_, e) => onScroll(e), + }, + defer: true, + }) + + useEffect(() => { + initialize({ + target: ref.current!, + cancel: { + nativeScrollbarsOverlaid: true, + }, + }) + }, [initialize]) + + useEffect(() => { + const el = ref.current + if (el) { + ref.current?.addEventListener("scroll", onScroll) + return () => { + el?.removeEventListener("scroll", onScroll) + } + } + }, [onScroll]) + + return ( +
+
{children}
+
+ ) +} diff --git a/src/components/header.tsx b/src/components/header.tsx index ababbfa..cd6d6b5 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -7,7 +7,7 @@ import type { SourceID } from "@shared/types" import { Homepage, Version } from "@shared/consts" import logo from "~/assets/icon.svg" import { useDark } from "~/hooks/useDark" -import { currentSectionAtom, refetchSourcesAtom } from "~/atoms" +import { currentSectionAtom, goToTopAtom, refetchSourcesAtom } from "~/atoms" function ThemeToggle() { const { toggleDark } = useDark() @@ -21,6 +21,18 @@ function ThemeToggle() { ) } +function GoTop() { + const { ok, fn: goToTop } = useAtomValue(goToTopAtom) + return ( + ok && ( +