diff --git a/src/content/Components/AnimatedList/AnimatedList.jsx b/src/content/Components/AnimatedList/AnimatedList.jsx index adb1c364..f10316f6 100644 --- a/src/content/Components/AnimatedList/AnimatedList.jsx +++ b/src/content/Components/AnimatedList/AnimatedList.jsx @@ -1,4 +1,4 @@ -import { useRef, useState, useEffect } from 'react'; +import { useRef, useState, useEffect, useCallback } from 'react'; import { motion, useInView } from 'motion/react'; import './AnimatedList.css'; @@ -53,12 +53,26 @@ const AnimatedList = ({ const [topGradientOpacity, setTopGradientOpacity] = useState(0); const [bottomGradientOpacity, setBottomGradientOpacity] = useState(1); - const handleScroll = e => { + const handleItemMouseEnter = useCallback(index => { + setSelectedIndex(index); + }, []); + + const handleItemClick = useCallback( + (item, index) => { + setSelectedIndex(index); + if (onItemSelect) { + onItemSelect(item, index); + } + }, + [onItemSelect] + ); + + const handleScroll = useCallback(e => { const { scrollTop, scrollHeight, clientHeight } = e.target; setTopGradientOpacity(Math.min(scrollTop / 50, 1)); const bottomDistance = scrollHeight - (scrollTop + clientHeight); setBottomGradientOpacity(scrollHeight <= clientHeight ? 0 : Math.min(bottomDistance / 50, 1)); - }; + }, []); useEffect(() => { if (!enableArrowNavigation) return; @@ -115,13 +129,8 @@ const AnimatedList = ({ key={index} delay={0.1} index={index} - onMouseEnter={() => setSelectedIndex(index)} - onClick={() => { - setSelectedIndex(index); - if (onItemSelect) { - onItemSelect(item, index); - } - }} + onMouseEnter={() => handleItemMouseEnter(index)} + onClick={() => handleItemClick(item, index)} >
{item}
diff --git a/src/tailwind/Components/AnimatedList/AnimatedList.jsx b/src/tailwind/Components/AnimatedList/AnimatedList.jsx index 8fa67fde..21814ade 100644 --- a/src/tailwind/Components/AnimatedList/AnimatedList.jsx +++ b/src/tailwind/Components/AnimatedList/AnimatedList.jsx @@ -1,6 +1,9 @@ import { useRef, useState, useEffect } from 'react'; import { motion, useInView } from 'motion/react'; +import { useRef, useState, useEffect, useCallback } from 'react'; +import { motion, useInView } from 'motion/react'; + const AnimatedItem = ({ children, delay = 0, index, onMouseEnter, onClick }) => { const ref = useRef(null); const inView = useInView(ref, { amount: 0.5, triggerOnce: false }); @@ -20,6 +23,164 @@ const AnimatedItem = ({ children, delay = 0, index, onMouseEnter, onClick }) => ); }; +const AnimatedList = ({ + items = [ + 'Item 1', + 'Item 2', + 'Item 3', + 'Item 4', + 'Item 5', + 'Item 6', + 'Item 7', + 'Item 8', + 'Item 9', + 'Item 10', + 'Item 11', + 'Item 12', + 'Item 13', + 'Item 14', + 'Item 15' + ], + onItemSelect, + showGradients = true, + enableArrowNavigation = true, + className = '', + itemClassName = '', + displayScrollbar = true, + initialSelectedIndex = -1 +}) => { + const listRef = useRef(null); + const [selectedIndex, setSelectedIndex] = useState(initialSelectedIndex); + const [keyboardNav, setKeyboardNav] = useState(false); + const [topGradientOpacity, setTopGradientOpacity] = useState(0); + const [bottomGradientOpacity, setBottomGradientOpacity] = useState(1); + + const handleItemMouseEnter = useCallback((index) => { + setSelectedIndex(index); + }, []); + + const handleItemClick = useCallback((item, index) => { + setSelectedIndex(index); + if (onItemSelect) { + onItemSelect(item, index); + } + }, [onItemSelect]); + + const handleScroll = useCallback((e) => { + const { scrollTop, scrollHeight, clientHeight } = e.target; + setTopGradientOpacity(Math.min(scrollTop / 50, 1)); + const bottomDistance = scrollHeight - (scrollTop + clientHeight); + setBottomGradientOpacity(scrollHeight <= clientHeight ? 0 : Math.min(bottomDistance / 50, 1)); + }, []); + + useEffect(() => { + if (!enableArrowNavigation) return; + const handleKeyDown = e => { + if (e.key === 'ArrowDown' || (e.key === 'Tab' && !e.shiftKey)) { + e.preventDefault(); + setKeyboardNav(true); + setSelectedIndex(prev => Math.min(prev + 1, items.length - 1)); + } else if (e.key === 'ArrowUp' || (e.key === 'Tab' && e.shiftKey)) { + e.preventDefault(); + setKeyboardNav(true); + setSelectedIndex(prev => Math.max(prev - 1, 0)); + } else if (e.key === 'Enter') { + if (selectedIndex >= 0 && selectedIndex < items.length) { + e.preventDefault(); + if (onItemSelect) { + onItemSelect(items[selectedIndex], selectedIndex); + } + } + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [items, selectedIndex, onItemSelect, enableArrowNavigation]); + + useEffect(() => { + if (!keyboardNav || selectedIndex < 0 || !listRef.current) return; + const container = listRef.current; + const selectedItem = container.querySelector(`[data-index="${selectedIndex}"]`); + if (selectedItem) { + const extraMargin = 50; + const containerScrollTop = container.scrollTop; + const containerHeight = container.clientHeight; + const itemTop = selectedItem.offsetTop; + const itemBottom = itemTop + selectedItem.offsetHeight; + if (itemTop < containerScrollTop + extraMargin) { + container.scrollTo({ top: itemTop - extraMargin, behavior: 'smooth' }); + } else if (itemBottom > containerScrollTop + containerHeight - extraMargin) { + container.scrollTo({ + top: itemBottom - containerHeight + extraMargin, + behavior: 'smooth' + }); + } + } + setKeyboardNav(false); + }, [selectedIndex, keyboardNav]); + + return ( +{item}
+{item}
diff --git a/src/ts-tailwind/Components/AnimatedList/AnimatedList.tsx b/src/ts-tailwind/Components/AnimatedList/AnimatedList.tsx index b60e756e..b93e22b1 100644 --- a/src/ts-tailwind/Components/AnimatedList/AnimatedList.tsx +++ b/src/ts-tailwind/Components/AnimatedList/AnimatedList.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState, useEffect, ReactNode, MouseEventHandler, UIEvent } from 'react'; +import React, { useRef, useState, useEffect, useCallback, ReactNode, MouseEventHandler, UIEvent } from 'react'; import { motion, useInView } from 'motion/react'; interface AnimatedItemProps { @@ -71,6 +71,20 @@ const AnimatedList: React.FC{item}