mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
fix(journey/mobile): eliminate carousel scroll stutter on mobile
- Defer activeIndex updates until scrolling settles (150ms debounce) instead of updating every RAF — mid-swipe card resize (240→320px) caused layout reflow on every frame, which is the main stutter source - Switch scrollSnapType from 'proximity' to 'mandatory' for reliable browser-native snapping without needing a JS re-center pass - Remove scroll-smooth CSS class (conflicts with mandatory snap) - Remove the post-settle scrollIntoView call (mandatory snap handles it) - Drop the now-unused activeIndexRef Closes #818
This commit is contained in:
@@ -53,9 +53,6 @@ export default function MobileMapTimeline({
|
|||||||
})
|
})
|
||||||
}, [entries])
|
}, [entries])
|
||||||
const cardRefs = useRef<Map<number, HTMLDivElement>>(new Map())
|
const cardRefs = useRef<Map<number, HTMLDivElement>>(new Map())
|
||||||
const activeIndexRef = useRef(activeIndex)
|
|
||||||
useEffect(() => { activeIndexRef.current = activeIndex }, [activeIndex])
|
|
||||||
|
|
||||||
// Sync map focus when carousel scrolls (with guard for uninitialized map)
|
// Sync map focus when carousel scrolls (with guard for uninitialized map)
|
||||||
const syncMapToCarousel = useCallback((index: number) => {
|
const syncMapToCarousel = useCallback((index: number) => {
|
||||||
const entry = entries[index]
|
const entry = entries[index]
|
||||||
@@ -90,29 +87,19 @@ export default function MobileMapTimeline({
|
|||||||
})
|
})
|
||||||
}, [syncMapToCarousel])
|
}, [syncMapToCarousel])
|
||||||
|
|
||||||
// Track scroll; debounce to re-center the active card when the user stops.
|
// Defer all state updates until scrolling settles — updating activeIndex
|
||||||
|
// mid-swipe resizes cards (240→320px), causing layout reflow every frame.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = carouselRef.current
|
const el = carouselRef.current
|
||||||
if (!el || entries.length === 0) return
|
if (!el || entries.length === 0) return
|
||||||
let rafId: number | null = null
|
|
||||||
let settleTimer: number | null = null
|
let settleTimer: number | null = null
|
||||||
const onScroll = () => {
|
const onScroll = () => {
|
||||||
if (rafId != null) return
|
|
||||||
rafId = requestAnimationFrame(() => {
|
|
||||||
pickNearestCard()
|
|
||||||
rafId = null
|
|
||||||
})
|
|
||||||
if (settleTimer != null) window.clearTimeout(settleTimer)
|
if (settleTimer != null) window.clearTimeout(settleTimer)
|
||||||
settleTimer = window.setTimeout(() => {
|
settleTimer = window.setTimeout(pickNearestCard, 150)
|
||||||
// Ensure the active card sits at the center once the user settles.
|
|
||||||
const card = cardRefs.current.get(activeIndexRef.current)
|
|
||||||
card?.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' })
|
|
||||||
}, 180)
|
|
||||||
}
|
}
|
||||||
el.addEventListener('scroll', onScroll, { passive: true })
|
el.addEventListener('scroll', onScroll, { passive: true })
|
||||||
return () => {
|
return () => {
|
||||||
el.removeEventListener('scroll', onScroll)
|
el.removeEventListener('scroll', onScroll)
|
||||||
if (rafId != null) cancelAnimationFrame(rafId)
|
|
||||||
if (settleTimer != null) window.clearTimeout(settleTimer)
|
if (settleTimer != null) window.clearTimeout(settleTimer)
|
||||||
}
|
}
|
||||||
}, [entries.length, pickNearestCard])
|
}, [entries.length, pickNearestCard])
|
||||||
@@ -210,9 +197,9 @@ export default function MobileMapTimeline({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={carouselRef}
|
ref={carouselRef}
|
||||||
className="flex gap-3 overflow-x-auto px-4 pb-3 pt-1 scroll-smooth"
|
className="flex gap-3 overflow-x-auto px-4 pb-3 pt-1"
|
||||||
style={{
|
style={{
|
||||||
scrollSnapType: 'x proximity',
|
scrollSnapType: 'x mandatory',
|
||||||
WebkitOverflowScrolling: 'touch',
|
WebkitOverflowScrolling: 'touch',
|
||||||
scrollbarWidth: 'none',
|
scrollbarWidth: 'none',
|
||||||
msOverflowStyle: 'none',
|
msOverflowStyle: 'none',
|
||||||
|
|||||||
Reference in New Issue
Block a user