refactor: optimize scroll progress handling in Description component for smoother user experience

This commit is contained in:
yessenia
2026-02-11 05:03:26 +08:00
parent 2b62862467
commit 36f42ec0a9

View File

@ -2,7 +2,7 @@
import type { MotionValue } from 'motion/react'
import { useTranslation } from '#i18n'
import { motion, useMotionValue, useSpring, useTransform } from 'motion/react'
import { motion, useMotionValue, useTransform } from 'motion/react'
import { useEffect, useLayoutEffect, useRef } from 'react'
import marketPlaceBg from '@/public/marketplace/hero-bg.jpg'
import marketplaceGradientNoise from '@/public/marketplace/hero-gradient-noise.svg'
@ -36,10 +36,10 @@ export const Description = ({
const heroSubtitleKey = isTemplatesView ? 'marketplace.templatesHeroSubtitle' : 'marketplace.pluginsHeroSubtitle'
const rafRef = useRef<number | null>(null)
const lastProgressRef = useRef(0)
const maxScrollableRef = useRef(1)
const titleRef = useRef<HTMLDivElement | null>(null)
const progress = useMotionValue(0)
const titleHeight = useMotionValue(0)
const smoothProgress = useSpring(progress, { stiffness: 260, damping: 34 })
useLayoutEffect(() => {
const node = titleRef.current
@ -74,7 +74,17 @@ export const Description = ({
rafRef.current = requestAnimationFrame(() => {
const scrollTop = Math.round(container.scrollTop)
const heightDelta = container.scrollHeight - container.clientHeight
const effectiveMaxScroll = Math.max(1, Math.min(MAX_SCROLL, heightDelta))
// Keep collapse threshold stable during the same scroll session.
// If we recompute with shrinking scrollHeight on every frame, progress can stay stuck at 1
// until scrollTop reaches 0, which feels like "cannot scroll to top".
const observedScrollable = Math.max(1, heightDelta)
const shouldResetThreshold = scrollTop === 0
if (shouldResetThreshold)
maxScrollableRef.current = observedScrollable
else
maxScrollableRef.current = Math.max(maxScrollableRef.current, observedScrollable)
const effectiveMaxScroll = Math.max(1, Math.min(MAX_SCROLL, maxScrollableRef.current))
const rawProgress = Math.min(Math.max(scrollTop / effectiveMaxScroll, 0), 1)
const snappedProgress = rawProgress >= 0.95
? 1
@ -102,16 +112,16 @@ export const Description = ({
}, [progress, scrollContainerId])
// Calculate interpolated values
const contentOpacity = useTransform(smoothProgress, [0, 1], [1, 0])
const contentScale = useTransform(smoothProgress, [0, 1], [1, 0.9])
const contentOpacity = useTransform(progress, [0, 1], [1, 0])
const contentScale = useTransform(progress, [0, 1], [1, 0.9])
const titleMaxHeight: MotionValue<number> = useTransform(
[smoothProgress, titleHeight],
[progress, titleHeight],
(values: number[]) => values[1] * (1 - values[0]),
)
const tabsMarginTop = useTransform(smoothProgress, [0, 1], [48, marketplaceNav ? 16 : 0])
const titleMarginTop = useTransform(smoothProgress, [0, 1], [marketplaceNav ? 80 : 0, 0])
const paddingTop = useTransform(smoothProgress, [0, 1], [marketplaceNav ? COLLAPSED_PADDING_TOP : EXPANDED_PADDING_TOP, COLLAPSED_PADDING_TOP])
const paddingBottom = useTransform(smoothProgress, [0, 1], [EXPANDED_PADDING_BOTTOM, COLLAPSED_PADDING_BOTTOM])
const tabsMarginTop = useTransform(progress, [0, 1], [48, marketplaceNav ? 16 : 0])
const titleMarginTop = useTransform(progress, [0, 1], [marketplaceNav ? 80 : 0, 0])
const paddingTop = useTransform(progress, [0, 1], [marketplaceNav ? COLLAPSED_PADDING_TOP : EXPANDED_PADDING_TOP, COLLAPSED_PADDING_TOP])
const paddingBottom = useTransform(progress, [0, 1], [EXPANDED_PADDING_BOTTOM, COLLAPSED_PADDING_BOTTOM])
return (
<motion.div
@ -122,6 +132,7 @@ export const Description = ({
style={{
paddingTop,
paddingBottom,
overflowAnchor: 'none',
}}
>
{/* Blue base background */}