Changes carousel auto scroll to pause when interaction occurs

This commit is contained in:
2026-02-19 16:32:11 +00:00
parent d403e96d34
commit 3ddd4ecdbd
2 changed files with 37 additions and 19 deletions
+2 -2
View File
@@ -72,7 +72,7 @@ export function ChatWidget({ onAction }: ChatWidgetProps) {
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
if (!hasInteracted.current) setShowNudge(true) if (!hasInteracted.current) setShowNudge(true)
}, 12_000) }, 5_000)
return () => clearTimeout(timer) return () => clearTimeout(timer)
}, []) }, [])
@@ -722,7 +722,7 @@ export function ChatWidget({ onAction }: ChatWidgetProps) {
<motion.div <motion.div
initial={prefersReducedMotion ? { opacity: 1 } : { opacity: 0, y: 6 }} initial={prefersReducedMotion ? { opacity: 1 } : { opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0, transition: motionSafeTransition(0.25, 'easeOut') }} animate={{ opacity: 1, y: 0, transition: motionSafeTransition(0.25, 'easeOut') }}
exit={prefersReducedMotion ? { opacity: 1 } : { opacity: 0, y: 4, transition: { duration: 0.2, ease: 'easeIn' } }} exit={{ opacity: 0, transition: { duration: 0 } }}
className="fixed z-[101] right-4 md:right-6 pointer-events-none" className="fixed z-[101] right-4 md:right-6 pointer-events-none"
style={{ style={{
/* Position above button: button-bottom + button-height + gap */ /* Position above button: button-bottom + button-height + gap */
+35 -17
View File
@@ -539,9 +539,26 @@ function ContinuousScrollCarousel() {
: false, : false,
) )
const resumeTimeoutRef = useRef<number>(0) const resumeTimeoutRef = useRef<number>(0)
const resumeTimestampRef = useRef<number | null>(null)
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const cardRefs = useRef<Map<number, HTMLDivElement>>(new Map()) const cardRefs = useRef<Map<number, HTMLDivElement>>(new Map())
const RESUME_DELAY_MS = 10000
const RAMP_DURATION_MS = 2000
const pauseCarousel = useCallback(() => {
isPausedRef.current = true
window.clearTimeout(resumeTimeoutRef.current)
}, [])
const scheduleResume = useCallback(() => {
window.clearTimeout(resumeTimeoutRef.current)
resumeTimeoutRef.current = window.setTimeout(() => {
isPausedRef.current = false
resumeTimestampRef.current = performance.now()
}, RESUME_DELAY_MS)
}, [])
const jumpByCards = useCallback((direction: 1 | -1) => { const jumpByCards = useCallback((direction: 1 | -1) => {
const trackEl = trackRef.current const trackEl = trackRef.current
const firstSetEl = firstSetRef.current const firstSetEl = firstSetRef.current
@@ -553,9 +570,7 @@ function ContinuousScrollCarousel() {
const cardWidth = (viewportWidth - totalGap) / cardsPerView const cardWidth = (viewportWidth - totalGap) / cardsPerView
const jumpPx = cardWidth + gap const jumpPx = cardWidth + gap
// Pause auto-scroll pauseCarousel()
isPausedRef.current = true
window.clearTimeout(resumeTimeoutRef.current)
// Apply CSS transition for smooth jump // Apply CSS transition for smooth jump
if (!prefersReducedMotion) { if (!prefersReducedMotion) {
@@ -579,11 +594,8 @@ function ContinuousScrollCarousel() {
trackEl.addEventListener('transitionend', transitionEnd, { once: true }) trackEl.addEventListener('transitionend', transitionEnd, { once: true })
} }
// Resume auto-scroll after 6s scheduleResume()
resumeTimeoutRef.current = window.setTimeout(() => { }, [viewportWidth, prefersReducedMotion, pauseCarousel, scheduleResume])
isPausedRef.current = false
}, 6000)
}, [viewportWidth, prefersReducedMotion])
useEffect(() => { useEffect(() => {
return () => { return () => {
@@ -629,9 +641,19 @@ function ContinuousScrollCarousel() {
const deltaSeconds = (timestamp - lastTime) / 1000 const deltaSeconds = (timestamp - lastTime) / 1000
lastTime = timestamp lastTime = timestamp
if (!isPausedRef.current) { if (!isPausedRef.current) {
let speedMultiplier = 1
if (resumeTimestampRef.current !== null) {
const elapsed = timestamp - resumeTimestampRef.current
if (elapsed < RAMP_DURATION_MS) {
const t = elapsed / RAMP_DURATION_MS
speedMultiplier = 1 - Math.pow(1 - t, 2)
} else {
resumeTimestampRef.current = null
}
}
const setWidth = firstSetEl.offsetWidth const setWidth = firstSetEl.offsetWidth
if (setWidth > 0) { if (setWidth > 0) {
offsetRef.current += speedPxPerSecond * deltaSeconds offsetRef.current += speedPxPerSecond * speedMultiplier * deltaSeconds
if (offsetRef.current >= setWidth) offsetRef.current -= setWidth if (offsetRef.current >= setWidth) offsetRef.current -= setWidth
trackEl.style.transform = `translate3d(-${offsetRef.current}px, 0, 0)` trackEl.style.transform = `translate3d(-${offsetRef.current}px, 0, 0)`
} }
@@ -655,10 +677,6 @@ function ContinuousScrollCarousel() {
return 214 return 214
}, [viewportWidth]) }, [viewportWidth])
const setPaused = (value: boolean) => {
isPausedRef.current = value
}
const handleArrowKey = useCallback((currentIndex: number, direction: -1 | 1) => { const handleArrowKey = useCallback((currentIndex: number, direction: -1 | 1) => {
const nextIndex = (currentIndex + direction + investigations.length) % investigations.length const nextIndex = (currentIndex + direction + investigations.length) % investigations.length
cardRefs.current.get(nextIndex)?.focus() cardRefs.current.get(nextIndex)?.focus()
@@ -702,12 +720,12 @@ function ContinuousScrollCarousel() {
<div <div
ref={viewportRef} ref={viewportRef}
style={{ overflow: 'hidden' }} style={{ overflow: 'hidden' }}
onMouseEnter={() => setPaused(true)} onMouseEnter={() => pauseCarousel()}
onMouseLeave={() => setPaused(false)} onMouseLeave={() => scheduleResume()}
onFocusCapture={() => setPaused(true)} onFocusCapture={() => pauseCarousel()}
onBlurCapture={(event) => { onBlurCapture={(event) => {
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) { if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
setPaused(false) scheduleResume()
} }
}} }}
> >