mirror of
https://github.com/ourongxing/newsnow.git
synced 2025-03-17 00:54:49 +08:00
feat: support go to top button
This commit is contained in:
parent
dfabc1bc42
commit
303f7b136a
@ -59,3 +59,8 @@ export const currentSectionAtom = atom((get) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
export type Update<T> = T | ((prev: T) => T)
|
export type Update<T> = T | ((prev: T) => T)
|
||||||
|
|
||||||
|
export const goToTopAtom = atom({
|
||||||
|
ok: false,
|
||||||
|
fn: undefined as (() => void) | undefined,
|
||||||
|
})
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import type { UseOverlayScrollbarsParams } from "overlayscrollbars-react"
|
import type { UseOverlayScrollbarsParams } from "overlayscrollbars-react"
|
||||||
import { useOverlayScrollbars } from "overlayscrollbars-react"
|
import { useOverlayScrollbars } from "overlayscrollbars-react"
|
||||||
import type { HTMLProps, PropsWithChildren } from "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 { defu } from "defu"
|
||||||
|
import { useSetAtom } from "jotai"
|
||||||
|
import { goToTopAtom } from "~/atoms"
|
||||||
|
|
||||||
type Props = HTMLProps<HTMLDivElement> & UseOverlayScrollbarsParams
|
type Props = HTMLProps<HTMLDivElement> & UseOverlayScrollbarsParams
|
||||||
const defaultScrollbarParams: UseOverlayScrollbarsParams = {
|
const defaultScrollbarParams: UseOverlayScrollbarsParams = {
|
||||||
@ -41,3 +43,63 @@ export function OverlayScrollbar({ children, options, events, defer, ...props }:
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function GlobalOverlayScrollbar({ children, ...props }: PropsWithChildren<HTMLProps<HTMLDivElement>>) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
const lastScroll = useRef(0)
|
||||||
|
const timer = useRef<any>()
|
||||||
|
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 (
|
||||||
|
<div ref={ref} {...props}>
|
||||||
|
<div>{children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -7,7 +7,7 @@ import type { SourceID } from "@shared/types"
|
|||||||
import { Homepage, Version } from "@shared/consts"
|
import { Homepage, Version } from "@shared/consts"
|
||||||
import logo from "~/assets/icon.svg"
|
import logo from "~/assets/icon.svg"
|
||||||
import { useDark } from "~/hooks/useDark"
|
import { useDark } from "~/hooks/useDark"
|
||||||
import { currentSectionAtom, refetchSourcesAtom } from "~/atoms"
|
import { currentSectionAtom, goToTopAtom, refetchSourcesAtom } from "~/atoms"
|
||||||
|
|
||||||
function ThemeToggle() {
|
function ThemeToggle() {
|
||||||
const { toggleDark } = useDark()
|
const { toggleDark } = useDark()
|
||||||
@ -21,6 +21,18 @@ function ThemeToggle() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function GoTop() {
|
||||||
|
const { ok, fn: goToTop } = useAtomValue(goToTopAtom)
|
||||||
|
return (
|
||||||
|
ok && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="i-ph:arrow-fat-up-duotone btn-pure"
|
||||||
|
onClick={goToTop}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
export function GithubIcon() {
|
export function GithubIcon() {
|
||||||
return <a className="i-ph-github-logo-duotone inline-block btn-pure" href={Homepage} />
|
return <a className="i-ph-github-logo-duotone inline-block btn-pure" href={Homepage} />
|
||||||
}
|
}
|
||||||
@ -70,6 +82,7 @@ export function Header() {
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span className="flex gap-2 items-center text-xl text-primary-600 dark:text-primary ">
|
<span className="flex gap-2 items-center text-xl text-primary-600 dark:text-primary ">
|
||||||
|
<GoTop />
|
||||||
<RefreshButton />
|
<RefreshButton />
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
<GithubIcon />
|
<GithubIcon />
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
|
import "~/styles/globals.css"
|
||||||
|
import "virtual:uno.css"
|
||||||
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router"
|
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router"
|
||||||
import { TanStackRouterDevtools } from "@tanstack/router-devtools"
|
import { TanStackRouterDevtools } from "@tanstack/router-devtools"
|
||||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
|
||||||
import "~/styles/globals.css"
|
|
||||||
import "virtual:uno.css"
|
|
||||||
import type { QueryClient } from "@tanstack/react-query"
|
import type { QueryClient } from "@tanstack/react-query"
|
||||||
import { Author, Homepage } from "@shared/consts"
|
import { Author, Homepage } from "@shared/consts"
|
||||||
import clsx from "clsx"
|
import clsx from "clsx"
|
||||||
import { Header } from "~/components/header"
|
import { Header } from "~/components/header"
|
||||||
import { useOnReload } from "~/hooks/useOnReload"
|
import { useOnReload } from "~/hooks/useOnReload"
|
||||||
import { OverlayScrollbar } from "~/components/common/overlay-scrollbar"
|
import { GlobalOverlayScrollbar } from "~/components/common/overlay-scrollbar"
|
||||||
|
|
||||||
export const Route = createRootRouteWithContext<{
|
export const Route = createRootRouteWithContext<{
|
||||||
queryClient: QueryClient
|
queryClient: QueryClient
|
||||||
@ -28,8 +28,8 @@ function RootComponent() {
|
|||||||
useOnReload()
|
useOnReload()
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OverlayScrollbar className={clsx([
|
<GlobalOverlayScrollbar className={clsx([
|
||||||
"h-full overflow-x-auto relative px-4",
|
"h-full overflow-x-auto px-4",
|
||||||
"md:(px-10)",
|
"md:(px-10)",
|
||||||
"lg:(px-24)",
|
"lg:(px-24)",
|
||||||
])}
|
])}
|
||||||
@ -59,7 +59,7 @@ function RootComponent() {
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</footer>
|
</footer>
|
||||||
</OverlayScrollbar>
|
</GlobalOverlayScrollbar>
|
||||||
{import.meta.env.DEV && (
|
{import.meta.env.DEV && (
|
||||||
<>
|
<>
|
||||||
<ReactQueryDevtools buttonPosition="bottom-left" />
|
<ReactQueryDevtools buttonPosition="bottom-left" />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user