From b77db5a4392963721366dfbd10cf6c6afd61ba8e Mon Sep 17 00:00:00 2001 From: Alex Zoitos Date: Tue, 22 Sep 2020 22:53:19 -0400 Subject: [PATCH] Horizontal scroll buttons (#100) * feat(frontend): horizontal scroll buttons with animations * feat(frontend): scroll + window resize events for buttons on slider --- src/components/Slider/index.tsx | 211 +++++++++++++++++++++++++++++--- 1 file changed, 193 insertions(+), 18 deletions(-) diff --git a/src/components/Slider/index.tsx b/src/components/Slider/index.tsx index 27fbfc3f..97c23191 100644 --- a/src/components/Slider/index.tsx +++ b/src/components/Slider/index.tsx @@ -1,4 +1,6 @@ -import React from 'react'; +import { debounce } from 'lodash'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useSpring } from 'react-spring'; import TitleCard from '../TitleCard'; interface SliderProps { @@ -8,34 +10,207 @@ interface SliderProps { isEmpty: boolean; } +enum Direction { + RIGHT, + LEFT, +} + const Slider: React.FC = ({ sliderKey, items, isLoading, isEmpty, }) => { + const containerRef = useRef(null); + const [scrollPos, setScrollPos] = useState({ isStart: true, isEnd: false }); + + const handleScroll = useCallback(() => { + const scrollWidth = containerRef.current?.scrollWidth ?? 0; + const clientWidth = + containerRef.current?.getBoundingClientRect().width ?? 0; + const scrollPosition = containerRef.current?.scrollLeft ?? 0; + + if (!items || items?.length === 0) { + setScrollPos({ isStart: true, isEnd: true }); + } else if (clientWidth >= scrollWidth) { + setScrollPos({ isStart: true, isEnd: true }); + } else if ( + scrollPosition >= + (containerRef.current?.scrollWidth ?? 0) - clientWidth + ) { + setScrollPos({ isStart: false, isEnd: true }); + } else if (scrollPosition > 0) { + setScrollPos({ isStart: false, isEnd: false }); + } else { + setScrollPos({ isStart: true, isEnd: false }); + } + }, [items]); + + // eslint-disable-next-line react-hooks/exhaustive-deps + const debouncedScroll = useCallback( + debounce(() => handleScroll(), 50), + [handleScroll] + ); + + useEffect(() => { + const handleResize = () => { + debouncedScroll(); + }; + + window.addEventListener('resize', handleResize, { passive: true }); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [debouncedScroll]); + + useEffect(() => { + handleScroll(); + }, [items, handleScroll]); + + const onScroll = () => { + debouncedScroll(); + }; + + const [, setX] = useSpring(() => ({ + from: { x: 0 }, + to: { x: 0 }, + onFrame: (props: { x: number }) => { + if (containerRef.current) { + containerRef.current.scrollLeft = props.x; + } + }, + })); + + const slide = (direction: Direction) => { + const clientWidth = + containerRef.current?.getBoundingClientRect().width ?? 0; + const cardWidth = + containerRef.current?.firstElementChild?.getBoundingClientRect().width ?? + 0; + const scrollPosition = containerRef.current?.scrollLeft ?? 0; + const visibleItems = Math.floor(clientWidth / cardWidth); + const scrollOffset = scrollPosition % cardWidth; + + if (direction === Direction.LEFT) { + const newX = Math.max( + scrollPosition - scrollOffset - visibleItems * cardWidth, + 0 + ); + setX({ + from: { x: scrollPosition }, + to: { + x: newX, + }, + onFrame: (props) => { + if (containerRef.current) { + containerRef.current.scrollLeft = props.x; + } + }, + reset: true, + config: { friction: 60, tension: 500, velocity: 20 }, + }); + + if (newX === 0) { + setScrollPos({ isStart: true, isEnd: false }); + } else { + setScrollPos({ isStart: false, isEnd: false }); + } + } else if (direction === Direction.RIGHT) { + const newX = Math.min( + scrollPosition - scrollOffset + visibleItems * cardWidth, + containerRef.current?.scrollWidth ?? 0 - clientWidth + ); + setX({ + from: { x: scrollPosition }, + to: { + x: newX, + }, + onFrame: (props) => { + if (containerRef.current) { + containerRef.current.scrollLeft = props.x; + } + }, + reset: true, + config: { friction: 60, tension: 500, velocity: 20 }, + }); + + if (newX >= (containerRef.current?.scrollWidth ?? 0) - clientWidth) { + setScrollPos({ isStart: false, isEnd: true }); + } else { + setScrollPos({ isStart: false, isEnd: false }); + } + } + }; + return ( -
- {items?.map((item, index) => ( -
+
+ +
- ))} - {isLoading && - [...Array(10)].map((_item, i) => ( -
- + + + +
+
+ {items?.map((item, index) => ( +
+ {item}
))} - {isEmpty && ( -
No Results
- )} + {isLoading && + [...Array(10)].map((_item, i) => ( +
+ +
+ ))} + {isEmpty && ( +
No Results
+ )} +
); };