Carousel.tsx 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. import React, { useState } from 'react'
  2. import { SerializedStyles } from '@emotion/core'
  3. import { animated, useSpring } from 'react-spring'
  4. import useResizeObserver from 'use-resize-observer'
  5. import { CarouselStyleProps, useCSS } from './Carousel.style'
  6. import NavButton from '../NavButton'
  7. export type CarouselProps = {
  8. containerCss?: SerializedStyles
  9. leftControlCss?: SerializedStyles
  10. rightControlCss?: SerializedStyles
  11. disableControls?: boolean
  12. onScroll?: (direction: 'left' | 'right') => void
  13. } & CarouselStyleProps
  14. const Carousel: React.FC<CarouselProps> = ({
  15. children,
  16. containerCss,
  17. leftControlCss,
  18. rightControlCss,
  19. disableControls = false,
  20. onScroll = () => {},
  21. }) => {
  22. const [scroll, setScroll] = useSpring(() => ({
  23. transform: `translateX(0px)`,
  24. }))
  25. const [carouselOffset, setCarouselOffset] = useState(0)
  26. const { width: containerWidth = 0, ref: containerRef } = useResizeObserver<HTMLDivElement>()
  27. const { width: childrenWidth = 0, ref: childrenContainerRef } = useResizeObserver<HTMLDivElement>()
  28. const styles = useCSS({})
  29. const maxScrollOffset = childrenWidth - containerWidth
  30. const showLeftControl = !disableControls && carouselOffset > 0
  31. const showRightControl = !disableControls && carouselOffset < maxScrollOffset
  32. const handleScroll = (direction: 'left' | 'right') => {
  33. if (containerWidth == null) {
  34. return
  35. }
  36. let scrollAmount
  37. switch (direction) {
  38. case 'left': {
  39. // Prevent overscroll on the left
  40. const newOffset = carouselOffset - containerWidth
  41. scrollAmount = newOffset < 0 ? 0 : newOffset
  42. onScroll('left')
  43. break
  44. }
  45. case 'right': {
  46. // Prevent overscroll on the right
  47. const newOffset = carouselOffset + containerWidth
  48. scrollAmount = newOffset > maxScrollOffset ? maxScrollOffset : newOffset
  49. onScroll('right')
  50. break
  51. }
  52. }
  53. setCarouselOffset(scrollAmount)
  54. setScroll({
  55. transform: `translateX(-${scrollAmount}px)`,
  56. })
  57. }
  58. if (!Array.isArray(children)) {
  59. return <>{children}</>
  60. }
  61. return (
  62. <div css={[styles.container, containerCss]}>
  63. <div css={styles.outerItemsContainer} ref={containerRef}>
  64. <animated.div css={styles.innerItemsContainer} style={scroll}>
  65. <div css={styles.innerItemsContainer} ref={childrenContainerRef}>
  66. {children.map((element, idx) => (
  67. <React.Fragment key={`Carousel-${idx}`}>{element}</React.Fragment>
  68. ))}
  69. </div>
  70. </animated.div>
  71. </div>
  72. {showLeftControl && (
  73. <NavButton outerCss={[styles.navLeft, leftControlCss]} direction="left" onClick={() => handleScroll('left')} />
  74. )}
  75. {showRightControl && (
  76. <NavButton
  77. outerCss={[styles.navRight, rightControlCss]}
  78. direction="right"
  79. onClick={() => handleScroll('right')}
  80. />
  81. )}
  82. </div>
  83. )
  84. }
  85. export default Carousel