From 6cf451b7b0d9125a26d3745b5cb8d2639b899b22 Mon Sep 17 00:00:00 2001 From: Jake Laderman Date: Thu, 13 Jun 2024 20:06:34 -0400 Subject: [PATCH] feat: added arrow scroll component (#608) --- src/components/ArrowScroll.tsx | 126 +++++++++++++++++++++++++++++++++ src/components/TabList.tsx | 39 +++++----- src/index.ts | 1 + 3 files changed, 150 insertions(+), 16 deletions(-) create mode 100644 src/components/ArrowScroll.tsx diff --git a/src/components/ArrowScroll.tsx b/src/components/ArrowScroll.tsx new file mode 100644 index 00000000..ff309be3 --- /dev/null +++ b/src/components/ArrowScroll.tsx @@ -0,0 +1,126 @@ +import { + Children, + type ComponentProps, + type ReactElement, + cloneElement, + useEffect, + useRef, + useState, +} from 'react' +import styled from 'styled-components' + +import { ArrowLeftIcon, ArrowRightIcon } from '../icons' + +const ComponentWrapperSC = styled.div({ + position: 'relative', + overflowX: 'auto', + '& > *': { + scrollbarWidth: 'none', // Firefox + msOverflowStyle: 'none', // Edge + '&::-webkit-scrollbar': { + display: 'none', // Chrome, Safari, Opera + }, + }, +}) +const ArrowWrapperSC = styled.div<{ + $direction: 'left' | 'right' +}>(({ theme, $direction }) => ({ + color: theme.colors['icon-light'], + zIndex: theme.zIndexes.modal, + position: 'absolute', + top: 0, + bottom: 0, + width: '36px', + display: 'flex', + justifyContent: $direction === 'left' ? 'flex-start' : 'flex-end', + padding: `0 ${theme.spacing.xxsmall}px`, + alignItems: 'center', + transition: 'opacity .2s ease', + background: `linear-gradient(${ + $direction === 'left' ? 'to left' : 'to right' + }, transparent 0%, #303540 70%, #303540 100%)`, + ...($direction === 'left' ? { left: 0 } : { right: 0 }), + '&.visible': { + cursor: 'pointer', + opacity: 1, + }, + '&.hidden': { + opacity: 0, + }, +})) + +function Arrow({ + direction, + show, + ...props +}: { + direction: 'left' | 'right' + show: boolean +} & ComponentProps) { + const Icon = direction === 'left' ? ArrowLeftIcon : ArrowRightIcon + + return ( + + + + ) +} + +const scroll = ( + element: HTMLElement | null | undefined, + direction: 'left' | 'right' +) => { + if (element) { + element.scrollBy({ + left: direction === 'left' ? -100 : 100, + behavior: 'smooth', + }) + } +} + +function ArrowScroll({ children, ...props }: { children?: ReactElement }) { + const containerRef = useRef(undefined) + const [showLeftArrow, setShowLeftArrow] = useState(false) + const [showRightArrow, setShowRightArrow] = useState(false) + + const checkScroll = () => { + if (containerRef.current) { + const { scrollLeft, scrollWidth, clientWidth } = containerRef.current + + setShowLeftArrow(scrollLeft > 0) + setShowRightArrow(Math.ceil(scrollLeft + clientWidth) < scrollWidth) + } + } + + useEffect(() => { + checkScroll() + window.addEventListener('resize', checkScroll) + + return () => window.removeEventListener('resize', checkScroll) + }, []) + + return ( + + scroll(containerRef.current, 'left')} + direction="left" + show={showLeftArrow} + /> + scroll(containerRef.current, 'right')} + direction="right" + show={showRightArrow} + /> + {cloneElement(Children.only(children), { + onScroll: checkScroll, + ref: containerRef, + })} + + ) +} + +export default ArrowScroll diff --git a/src/components/TabList.tsx b/src/components/TabList.tsx index 0c104564..bb302ef8 100644 --- a/src/components/TabList.tsx +++ b/src/components/TabList.tsx @@ -23,6 +23,8 @@ import { import styled, { useTheme } from 'styled-components' import { useItemWrappedChildren } from './ListBox' +import ArrowScroll from './ArrowScroll' +import WrapWithIf from './WrapWithIf' export type MakeOptional = Omit & Partial> @@ -135,23 +137,28 @@ function TabListRef( } return ( - } > - {tabChildren} - + + {tabChildren} + + ) } diff --git a/src/index.ts b/src/index.ts index da560081..7ee7b823 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ export * from './plural-logos' // Components export { default as Accordion } from './components/Accordion' +export { default as ArrowScroll } from './components/ArrowScroll' export { default as Banner } from './components/Banner' export { default as Button } from './components/Button' export type { CardProps } from './components/Card'