From e821397e6c7940c326442e4950abac2586a11215 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sat, 27 Dec 2025 18:29:10 -0800 Subject: [PATCH] add horizontal scroll to feature carousel (#1123) --- .../feature-carousel/feature-carousel.tsx | 74 +++++++++++++++---- 1 file changed, 58 insertions(+), 16 deletions(-) diff --git a/src/renderer/components/feature-carousel/feature-carousel.tsx b/src/renderer/components/feature-carousel/feature-carousel.tsx index 6921d4038..3de2e6cb5 100644 --- a/src/renderer/components/feature-carousel/feature-carousel.tsx +++ b/src/renderer/components/feature-carousel/feature-carousel.tsx @@ -1,7 +1,7 @@ import type { MouseEvent } from 'react'; import { AnimatePresence, motion } from 'motion/react'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { generatePath, Link } from 'react-router'; import styles from './feature-carousel.module.css'; @@ -209,28 +209,70 @@ export const FeatureCarousel = ({ data, onNearEnd }: FeatureCarouselProps) => { } }, [data, startIndex, itemsPerRow, onNearEnd]); - const handleNext = (e?: MouseEvent) => { - e?.preventDefault(); - e?.stopPropagation(); - if (!data) return; - directionRef.current = { isNext: true }; - setStartIndex((prev) => (prev + itemsPerRow) % data.length); - }; + const handleNext = useCallback( + (e?: MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); + if (!data) return; + directionRef.current = { isNext: true }; + setStartIndex((prev) => (prev + itemsPerRow) % data.length); + }, + [data, itemsPerRow], + ); - const handlePrevious = (e?: MouseEvent) => { - e?.preventDefault(); - e?.stopPropagation(); - if (!data) return; - directionRef.current = { isNext: false }; - setStartIndex((prev) => (prev - itemsPerRow + data.length) % data.length); - }; + const handlePrevious = useCallback( + (e?: MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); + if (!data) return; + directionRef.current = { isNext: false }; + setStartIndex((prev) => (prev - itemsPerRow + data.length) % data.length); + }, + [data, itemsPerRow], + ); + + const canNavigate = data && data.length > itemsPerRow; + + const wheelCooldownRef = useRef(0); + const wheelThreshold = 10; + const wheelCooldownMs = 250; + + const handleWheel = useCallback( + (event: React.WheelEvent) => { + if (!canNavigate || !data) { + return; + } + + if (!event.shiftKey) { + return; + } + + const now = Date.now(); + const elapsed = now - wheelCooldownRef.current; + + const horizontalDelta = Math.abs(event.deltaY); + + if (horizontalDelta < wheelThreshold || elapsed < wheelCooldownMs) { + return; + } + + if (event.deltaY > 0) { + wheelCooldownRef.current = now; + handleNext(); + } else if (event.deltaY < 0) { + wheelCooldownRef.current = now; + handlePrevious(); + } + }, + [canNavigate, data, handleNext, handlePrevious, wheelCooldownMs, wheelThreshold], + ); if (!data || data.length === 0) { return null; } return ( -
+