feat: better overlayscrollbar

This commit is contained in:
Ou 2024-11-10 23:52:25 +08:00
parent a05f0d54b5
commit d86da2387d
8 changed files with 200 additions and 100 deletions

View File

@ -52,7 +52,6 @@
"md5": "^2.3.0", "md5": "^2.3.0",
"ofetch": "^1.4.1", "ofetch": "^1.4.1",
"overlayscrollbars": "^2.10.0", "overlayscrollbars": "^2.10.0",
"overlayscrollbars-react": "^0.5.6",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-use": "^17.5.1", "react-use": "^17.5.1",

85
pnpm-lock.yaml generated
View File

@ -92,18 +92,12 @@ importers:
md5: md5:
specifier: ^2.3.0 specifier: ^2.3.0
version: 2.3.0 version: 2.3.0
motion:
specifier: ^10.18.0
version: 10.18.0
ofetch: ofetch:
specifier: ^1.4.1 specifier: ^1.4.1
version: 1.4.1 version: 1.4.1
overlayscrollbars: overlayscrollbars:
specifier: ^2.10.0 specifier: ^2.10.0
version: 2.10.0 version: 2.10.0
overlayscrollbars-react:
specifier: ^0.5.6
version: 0.5.6(overlayscrollbars@2.10.0)(react@18.3.1)
react: react:
specifier: ^18.3.1 specifier: ^18.3.1
version: 18.3.1 version: 18.3.1
@ -1549,24 +1543,6 @@ packages:
resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
hasBin: true hasBin: true
'@motionone/animation@10.18.0':
resolution: {integrity: sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw==}
'@motionone/dom@10.18.0':
resolution: {integrity: sha512-bKLP7E0eyO4B2UaHBBN55tnppwRnaE3KFfh3Ps9HhnAkar3Cb69kUCJY9as8LrccVYKgHA+JY5dOQqJLOPhF5A==}
'@motionone/easing@10.18.0':
resolution: {integrity: sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg==}
'@motionone/generators@10.18.0':
resolution: {integrity: sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg==}
'@motionone/types@10.17.1':
resolution: {integrity: sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A==}
'@motionone/utils@10.18.0':
resolution: {integrity: sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw==}
'@napi-rs/pinyin-android-arm-eabi@1.7.5': '@napi-rs/pinyin-android-arm-eabi@1.7.5':
resolution: {integrity: sha512-dnurqJdedbU8D1Ngudf10nXvc4BbVSe4ki9U2LUZoGMDGa069t4c87BHRXvcy2By3YpOozORv9/QTMxAQFVOLg==} resolution: {integrity: sha512-dnurqJdedbU8D1Ngudf10nXvc4BbVSe4ki9U2LUZoGMDGa069t4c87BHRXvcy2By3YpOozORv9/QTMxAQFVOLg==}
engines: {node: '>= 10.0'} engines: {node: '>= 10.0'}
@ -3998,9 +3974,6 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
hey-listen@1.0.8:
resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==}
hookable@5.5.3: hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
@ -4635,9 +4608,6 @@ packages:
mockdate@3.0.5: mockdate@3.0.5:
resolution: {integrity: sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ==} resolution: {integrity: sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ==}
motion@10.18.0:
resolution: {integrity: sha512-MVAZZmwM/cp77BrNe1TxTMldxRPjwBNHheU5aPToqT4rJdZxLiADk58H+a0al5jKLxkB0OdgNq6DiVn11cjvIQ==}
mri@1.2.0: mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -4804,12 +4774,6 @@ packages:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
overlayscrollbars-react@0.5.6:
resolution: {integrity: sha512-E5To04bL5brn9GVCZ36SnfGanxa2I2MDkWoa4Cjo5wol7l+diAgi4DBc983V7l2nOk/OLJ6Feg4kySspQEGDBw==}
peerDependencies:
overlayscrollbars: ^2.0.0
react: '>=16.8.0'
overlayscrollbars@2.10.0: overlayscrollbars@2.10.0:
resolution: {integrity: sha512-diNMeEafWTE0A4GJfwRpdBp2rE/BEvrhptBdBcDu8/UeytWcdCy9Td8tZWnztJeJ26f8/uHCWfPnPUC/dtgJdw==} resolution: {integrity: sha512-diNMeEafWTE0A4GJfwRpdBp2rE/BEvrhptBdBcDu8/UeytWcdCy9Td8tZWnztJeJ26f8/uHCWfPnPUC/dtgJdw==}
@ -7569,41 +7533,6 @@ snapshots:
- encoding - encoding
- supports-color - supports-color
'@motionone/animation@10.18.0':
dependencies:
'@motionone/easing': 10.18.0
'@motionone/types': 10.17.1
'@motionone/utils': 10.18.0
tslib: 2.8.0
'@motionone/dom@10.18.0':
dependencies:
'@motionone/animation': 10.18.0
'@motionone/generators': 10.18.0
'@motionone/types': 10.17.1
'@motionone/utils': 10.18.0
hey-listen: 1.0.8
tslib: 2.8.0
'@motionone/easing@10.18.0':
dependencies:
'@motionone/utils': 10.18.0
tslib: 2.8.0
'@motionone/generators@10.18.0':
dependencies:
'@motionone/types': 10.17.1
'@motionone/utils': 10.18.0
tslib: 2.8.0
'@motionone/types@10.17.1': {}
'@motionone/utils@10.18.0':
dependencies:
'@motionone/types': 10.17.1
hey-listen: 1.0.8
tslib: 2.8.0
'@napi-rs/pinyin-android-arm-eabi@1.7.5': '@napi-rs/pinyin-android-arm-eabi@1.7.5':
optional: true optional: true
@ -10427,8 +10356,6 @@ snapshots:
dependencies: dependencies:
function-bind: 1.1.2 function-bind: 1.1.2
hey-listen@1.0.8: {}
hookable@5.5.3: {} hookable@5.5.3: {}
hosted-git-info@2.8.9: {} hosted-git-info@2.8.9: {}
@ -11045,13 +10972,6 @@ snapshots:
mockdate@3.0.5: {} mockdate@3.0.5: {}
motion@10.18.0:
dependencies:
'@motionone/animation': 10.18.0
'@motionone/dom': 10.18.0
'@motionone/types': 10.17.1
'@motionone/utils': 10.18.0
mri@1.2.0: {} mri@1.2.0: {}
mrmime@2.0.0: {} mrmime@2.0.0: {}
@ -11303,11 +11223,6 @@ snapshots:
type-check: 0.4.0 type-check: 0.4.0
word-wrap: 1.2.5 word-wrap: 1.2.5
overlayscrollbars-react@0.5.6(overlayscrollbars@2.10.0)(react@18.3.1):
dependencies:
overlayscrollbars: 2.10.0
react: 18.3.1
overlayscrollbars@2.10.0: {} overlayscrollbars@2.10.0: {}
p-limit@2.3.0: p-limit@2.3.0:

View File

@ -159,7 +159,7 @@ function NewsCard({ id, setHandleRef }: NewsCardProps) {
options={{ options={{
overflow: { x: "hidden" }, overflow: { x: "hidden" },
}} }}
defer={false} defer
> >
<div className={$("transition-opacity-500", isFetching && "op-20")}> <div className={$("transition-opacity-500", isFetching && "op-20")}>
{!!data?.items?.length && (sources[id].type === "hottest" ? <NewsListHot items={data.items} /> : <NewsListTimeLine items={data.items} />)} {!!data?.items?.length && (sources[id].type === "hottest" ? <NewsListHot items={data.items} /> : <NewsListTimeLine items={data.items} />)}

View File

@ -1,9 +1,10 @@
import type { UseOverlayScrollbarsParams } from "overlayscrollbars-react"
import { useOverlayScrollbars } from "overlayscrollbars-react"
import type { HTMLProps, PropsWithChildren } from "react" import type { HTMLProps, PropsWithChildren } from "react"
import { defu } from "defu" import { defu } from "defu"
import { useMount } from "react-use" import { useMount } from "react-use"
import { useOverlayScrollbars } from "./useOverlayScrollbars"
import type { UseOverlayScrollbarsParams } from "./useOverlayScrollbars"
import { goToTopAtom } from "~/atoms" import { goToTopAtom } from "~/atoms"
import "./style.css"
type Props = HTMLProps<HTMLDivElement> & UseOverlayScrollbarsParams type Props = HTMLProps<HTMLDivElement> & UseOverlayScrollbarsParams
const defaultScrollbarParams: UseOverlayScrollbarsParams = { const defaultScrollbarParams: UseOverlayScrollbarsParams = {
@ -15,7 +16,7 @@ const defaultScrollbarParams: UseOverlayScrollbarsParams = {
defer: true, defer: true,
} }
export function OverlayScrollbar({ disabled, children, options, events, defer, ...props }: PropsWithChildren<Props>) { export function OverlayScrollbar({ disabled, children, options, events, defer, className, ...props }: PropsWithChildren<Props>) {
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
const scrollbarParams = useMemo(() => defu<UseOverlayScrollbarsParams, Array<UseOverlayScrollbarsParams> >({ const scrollbarParams = useMemo(() => defu<UseOverlayScrollbarsParams, Array<UseOverlayScrollbarsParams> >({
options, options,
@ -23,7 +24,7 @@ export function OverlayScrollbar({ disabled, children, options, events, defer, .
defer, defer,
}, defaultScrollbarParams), [options, events, defer]) }, defaultScrollbarParams), [options, events, defer])
const [initialize] = useOverlayScrollbars(scrollbarParams) const [initialize, instance] = useOverlayScrollbars(scrollbarParams)
useMount(() => { useMount(() => {
if (!disabled) { if (!disabled) {
@ -37,15 +38,25 @@ export function OverlayScrollbar({ disabled, children, options, events, defer, .
} }
}) })
useEffect(() => {
if (ref.current) {
if (instance && instance?.state().destroyed) {
ref.current.classList.remove("scrollbar-hidden")
} else {
ref.current.classList.add("scrollbar-hidden")
}
}
}, [instance])
return ( return (
<div ref={ref} {...props}> <div ref={ref} {...props} className={$("overflow-auto scrollbar-hidden", className)}>
{/* 只能有一个 element */} {/* 只能有一个 element */}
<div>{children}</div> <div>{children}</div>
</div> </div>
) )
} }
export function GlobalOverlayScrollbar({ children, ...props }: PropsWithChildren<HTMLProps<HTMLDivElement>>) { export function GlobalOverlayScrollbar({ children, className, ...props }: PropsWithChildren<HTMLProps<HTMLDivElement>>) {
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
const lastTrigger = useRef(0) const lastTrigger = useRef(0)
const timer = useRef<any>() const timer = useRef<any>()
@ -67,7 +78,7 @@ export function GlobalOverlayScrollbar({ children, ...props }: PropsWithChildren
) )
} }
}, [setGoToTop]) }, [setGoToTop])
const [initialize] = useOverlayScrollbars({ const [initialize, instance] = useOverlayScrollbars({
options: { options: {
scrollbars: { scrollbars: {
autoHide: "scroll", autoHide: "scroll",
@ -76,7 +87,7 @@ export function GlobalOverlayScrollbar({ children, ...props }: PropsWithChildren
events: { events: {
scroll: (_, e) => onScroll(e), scroll: (_, e) => onScroll(e),
}, },
defer: false, defer: true,
}) })
useMount(() => { useMount(() => {
@ -95,8 +106,18 @@ export function GlobalOverlayScrollbar({ children, ...props }: PropsWithChildren
} }
}) })
useEffect(() => {
if (ref.current) {
if (instance && instance?.state().destroyed) {
ref.current.classList.remove("scrollbar-hidden")
} else {
ref.current?.classList.add("scrollbar-hidden")
}
}
}, [instance])
return ( return (
<div ref={ref} {...props}> <div ref={ref} {...props} className={$("overflow-auto scrollbar-hidden", className)}>
<div>{children}</div> <div>{children}</div>
</div> </div>
) )

View File

@ -0,0 +1,12 @@
::-webkit-scrollbar-thumb {
border-radius: 8px;
-webkit-border-radius: 8px;
}
.scrollbar-hidden {
scrollbar-width: none;
}
.scrollbar-hidden::-webkit-scrollbar {
width: 0px;
height: 0px;
}

View File

@ -0,0 +1,152 @@
import type { ComponentPropsWithoutRef, ElementRef, ElementType, ForwardedRef } from "react"
import { useEffect, useMemo, useRef } from "react"
import { OverlayScrollbars } from "overlayscrollbars"
import type { EventListeners, InitializationTarget, PartialOptions } from "overlayscrollbars"
type OverlayScrollbarsComponentBaseProps<T extends ElementType = "div"> =
ComponentPropsWithoutRef<T> & {
/** Tag of the root element. */
element?: T
/** OverlayScrollbars options. */
options?: PartialOptions | false | null
/** OverlayScrollbars events. */
events?: EventListeners | false | null
/** Whether to defer the initialization to a point in time when the browser is idle. (or to the next frame if `window.requestIdleCallback` is not supported) */
defer?: boolean | IdleRequestOptions
}
type OverlayScrollbarsComponentProps<T extends ElementType = "div"> =
OverlayScrollbarsComponentBaseProps<T> & {
ref?: ForwardedRef<OverlayScrollbarsComponentRef<T>>
}
interface OverlayScrollbarsComponentRef<T extends ElementType = "div"> {
/** Returns the OverlayScrollbars instance or null if not initialized. */
osInstance: () => OverlayScrollbars | null
/** Returns the root element. */
getElement: () => ElementRef<T> | null
}
type Defer = [
requestDefer: (callback: () => any, options?: OverlayScrollbarsComponentProps["defer"]) => void,
cancelDefer: () => void,
]
export interface UseOverlayScrollbarsParams {
/** OverlayScrollbars options. */
options?: OverlayScrollbarsComponentProps["options"]
/** OverlayScrollbars events. */
events?: OverlayScrollbarsComponentProps["events"]
/** Whether to defer the initialization to a point in time when the browser is idle. (or to the next frame if `window.requestIdleCallback` is not supported) */
defer?: OverlayScrollbarsComponentProps["defer"]
}
export type UseOverlayScrollbarsInitialization = (target: InitializationTarget) => void
export type UseOverlayScrollbarsInstance = () => ReturnType<
OverlayScrollbarsComponentRef["osInstance"]
>
function createDefer(): Defer {
let idleId: number
let rafId: number
const wnd = window
const idleSupported = typeof wnd.requestIdleCallback === "function"
const rAF = wnd.requestAnimationFrame
const cAF = wnd.cancelAnimationFrame
const rIdle = idleSupported ? wnd.requestIdleCallback : rAF
const cIdle = idleSupported ? wnd.cancelIdleCallback : cAF
const clear = () => {
cIdle(idleId)
cAF(rafId)
}
return [
(callback, options) => {
clear()
idleId = rIdle(
idleSupported
? () => {
clear()
// inside idle its best practice to use rAF to change DOM for best performance
rafId = rAF(callback)
}
: callback,
typeof options === "object" ? options : { timeout: 2233 },
)
},
clear,
]
}
/**
* Hook for advanced usage of OverlayScrollbars. (When the OverlayScrollbarsComponent is not enough)
* @param params Parameters for customization.
* @returns A tuple with two values:
* The first value is the initialization function, it takes one argument which is the `InitializationTarget`.
* The second value is a function which returns the current OverlayScrollbars instance or `null` if not initialized.
*/
export function useOverlayScrollbars(params?: UseOverlayScrollbarsParams): [UseOverlayScrollbarsInitialization, OverlayScrollbars | null ] {
const { options, events, defer } = params || {}
const [requestDefer, cancelDefer] = useMemo<Defer>(createDefer, [])
// const instanceRef = useRef<ReturnType<UseOverlayScrollbarsInstance>>(null)
const [instance, setInstance] = useState<ReturnType<UseOverlayScrollbarsInstance>>(null)
const deferRef = useRef(defer)
const optionsRef = useRef(options)
const eventsRef = useRef(events)
useEffect(() => {
deferRef.current = defer
}, [defer])
useEffect(() => {
optionsRef.current = options
if (OverlayScrollbars.valid(instance)) {
instance.options(options || {}, true)
}
}, [options, instance])
useEffect(() => {
eventsRef.current = events
if (OverlayScrollbars.valid(instance)) {
instance.on(events || {}, true)
}
}, [events, instance])
useEffect(
() => () => {
cancelDefer()
instance?.destroy()
},
[cancelDefer, instance, setInstance],
)
return useMemo(
() => [
(target) => {
// if already initialized do nothing
const presentInstance = instance
if (OverlayScrollbars.valid(presentInstance)) {
return
}
const currDefer = deferRef.current
const currOptions = optionsRef.current || {}
const currEvents = eventsRef.current || {}
const init = () => {
setInstance(OverlayScrollbars(target, currOptions, currEvents))
}
if (currDefer) {
requestDefer(init, currDefer)
} else {
init()
}
},
instance,
],
[instance, requestDefer],
)
}

View File

@ -86,7 +86,7 @@ export function SearchBar() {
placeholder="搜索你想要的" placeholder="搜索你想要的"
/> />
<div className="md:flex pt-2"> <div className="md:flex pt-2">
<OverlayScrollbar defer={false} className="overflow-y-auto md:min-w-275px"> <OverlayScrollbar defer className="overflow-y-auto md:min-w-275px">
<Command.List> <Command.List>
<Command.Empty> Github issue </Command.Empty> <Command.Empty> Github issue </Command.Empty>
{ {

View File

@ -4,13 +4,14 @@ import react from "@vitejs/plugin-react-swc"
import { TanStackRouterVite } from "@tanstack/router-plugin/vite" import { TanStackRouterVite } from "@tanstack/router-plugin/vite"
import unocss from "unocss/vite" import unocss from "unocss/vite"
import unimport from "unimport/unplugin" import unimport from "unimport/unplugin"
import dotenv from "dotenv"
import nitro from "./nitro.config" import nitro from "./nitro.config"
import { projectDir } from "./shared/dir" import { projectDir } from "./shared/dir"
import pwa from "./pwa.config" import pwa from "./pwa.config"
// dotenv.config({ dotenv.config({
// path: join(projectDir, ".env.server"), path: join(projectDir, ".env.server"),
// }) })
export default defineConfig({ export default defineConfig({
resolve: { resolve: {