UNPKG

react-vertical-feed

Version:

A React component for creating vertical media feeds (videos) similar to TikTok or Instagram

112 lines (109 loc) 5.09 kB
import { jsxs, jsx } from 'react/jsx-runtime'; import { useRef, useState, useCallback, useEffect, useMemo } from 'react'; const VerticalFeed = ({ items, onEndReached, loadingComponent, errorComponent, className, style, onItemVisible, onItemHidden, onItemClick, threshold = 0.75, scrollBehavior = 'smooth', renderItemOverlay, }) => { const containerRef = useRef(null); const [loadingStates, setLoadingStates] = useState({}); const [errorStates, setErrorStates] = useState({}); const handleMediaLoad = useCallback((index) => { setLoadingStates(prev => ({ ...prev, [index]: false })); }, []); const handleMediaError = useCallback((index) => { setErrorStates(prev => ({ ...prev, [index]: true })); setLoadingStates(prev => ({ ...prev, [index]: false })); }, []); useEffect(() => { const observer = new IntersectionObserver(entries => { entries.forEach(entry => { const index = parseInt(entry.target.getAttribute('data-index') || '0', 10); const item = items[index]; if (entry.isIntersecting) { const video = entry.target.querySelector('video'); if (video) { video.play().catch(error => { console.error('Error playing video:', error); }); } onItemVisible?.(item, index); } else { const video = entry.target.querySelector('video'); if (video) { video.pause(); } onItemHidden?.(item, index); } }); }, { threshold, }); const mediaElements = containerRef.current?.querySelectorAll('[data-index]') || []; mediaElements.forEach(media => observer.observe(media)); return () => { observer.disconnect(); }; }, [items, onItemVisible, onItemHidden, threshold]); const handleScroll = useCallback(() => { if (!containerRef.current || !onEndReached) return; const { scrollTop, scrollHeight, clientHeight } = containerRef.current; if (scrollTop + clientHeight >= scrollHeight - 100) { onEndReached(); } }, [onEndReached]); const handleKeyDown = useCallback((e) => { if (!containerRef.current) return; const { scrollTop, clientHeight } = containerRef.current; const scrollAmount = clientHeight; if (!containerRef.current.scrollTo) return; switch (e.key) { case 'ArrowDown': containerRef.current.scrollTo({ top: scrollTop + scrollAmount, behavior: scrollBehavior, }); break; case 'ArrowUp': containerRef.current.scrollTo({ top: scrollTop - scrollAmount, behavior: scrollBehavior, }); break; } }, [scrollBehavior]); const defaultRenderItem = useCallback((item, index) => { const isLoading = loadingStates[index] ?? true; const hasError = errorStates[index] ?? false; return (jsxs("div", { "data-index": index, onClick: () => onItemClick?.(item, index), style: { height: '100vh', scrollSnapAlign: 'start', position: 'relative', cursor: onItemClick ? 'pointer' : 'default', }, role: "region", "aria-label": `video ${index + 1}`, children: [isLoading && loadingComponent, hasError && errorComponent, jsx("video", { src: item.src, muted: item.muted ?? true, playsInline: item.playsInline ?? true, controls: item.controls ?? false, autoPlay: item.autoPlay ?? true, onLoadedData: () => handleMediaLoad(index), onError: () => handleMediaError(index), style: { width: '100%', height: '100%', objectFit: 'cover', display: isLoading || hasError ? 'none' : 'block', } }), renderItemOverlay && renderItemOverlay(item, index)] }, item.id || index)); }, [ loadingStates, errorStates, loadingComponent, errorComponent, handleMediaLoad, handleMediaError, onItemClick, renderItemOverlay, ]); const mediaElements = useMemo(() => items.map((item, index) => defaultRenderItem(item, index)), [items, defaultRenderItem]); return (jsx("div", { ref: containerRef, onScroll: handleScroll, onKeyDown: handleKeyDown, tabIndex: 0, role: "feed", "aria-label": "Vertical video feed", className: className, style: { height: '100vh', overflowY: 'scroll', scrollSnapType: 'y mandatory', outline: 'none', ...style, }, children: mediaElements })); }; export { VerticalFeed }; //# sourceMappingURL=index.esm.js.map