Carousel.tsx 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react'
  2. import { css, SerializedStyles } from '@emotion/core'
  3. import { animated, useSpring } from 'react-spring'
  4. import useResizeObserver from 'use-resize-observer'
  5. import { useCSS, CarouselStyleProps } from './Carousel.style'
  6. import NavButton from '../NavButton'
  7. type CarouselProps = {
  8. children: React.ReactNode
  9. containerCss: SerializedStyles
  10. leftControlCss: SerializedStyles
  11. rightControlCss: SerializedStyles
  12. onScroll: (direction: 'left' | 'right') => void
  13. } & CarouselStyleProps
  14. const Carousel: React.FC<Partial<CarouselProps>> = ({
  15. children,
  16. containerCss,
  17. leftControlCss,
  18. rightControlCss,
  19. onScroll = () => {},
  20. }) => {
  21. const [scroll, setScroll] = useSpring(() => ({
  22. transform: `translateX(0px)`,
  23. }))
  24. const [x, setX] = useState(0)
  25. const { width: containerWidth = NaN, ref: containerRef } = useResizeObserver<HTMLDivElement>()
  26. const elementsRefs = useRef<(HTMLDivElement | null)[]>([])
  27. const [childrensWidth, setChildrensWidth] = useState(0)
  28. useEffect(() => {
  29. if (Array.isArray(children)) {
  30. elementsRefs.current = elementsRefs.current.slice(0, children.length)
  31. const childrensWidth = elementsRefs.current.reduce(
  32. (accWidth, el) => (el != null ? accWidth + el.clientWidth : accWidth),
  33. 0
  34. )
  35. setChildrensWidth(childrensWidth)
  36. }
  37. }, [children])
  38. const styles = useCSS({})
  39. function handleScroll(direction: 'left' | 'right') {
  40. if (containerWidth == null) {
  41. return
  42. }
  43. let scrollAmount
  44. switch (direction) {
  45. case 'left': {
  46. // Prevent overscroll on the left
  47. scrollAmount = x + containerWidth >= 0 ? 0 : x + containerWidth
  48. onScroll('left')
  49. break
  50. }
  51. case 'right': {
  52. // Prevent overscroll on the right
  53. scrollAmount =
  54. x - containerWidth <= -(childrensWidth - containerWidth)
  55. ? -(childrensWidth - containerWidth)
  56. : x - containerWidth
  57. onScroll('right')
  58. break
  59. }
  60. }
  61. setX(scrollAmount)
  62. setScroll({
  63. transform: `translateX(${scrollAmount}px)`,
  64. })
  65. }
  66. if (!Array.isArray(children)) {
  67. return <>{children}</>
  68. }
  69. return (
  70. <div css={[styles.container, containerCss]}>
  71. <div css={styles.outerItemsContainer} ref={containerRef}>
  72. <animated.div style={scroll} css={styles.innerItemsContainer}>
  73. {children.map((element, idx) => (
  74. <div
  75. key={`Carousel-${idx}`}
  76. ref={(el) => {
  77. elementsRefs.current[idx] = el
  78. return el
  79. }}
  80. >
  81. {element}
  82. </div>
  83. ))}
  84. </animated.div>
  85. </div>
  86. <NavButton
  87. outerCss={[
  88. styles.navLeft,
  89. css`
  90. opacity: ${x === 0 ? 0 : 1};
  91. `,
  92. leftControlCss,
  93. ]}
  94. direction="left"
  95. onClick={() => {
  96. handleScroll('left')
  97. }}
  98. />
  99. <NavButton
  100. outerCss={[
  101. styles.navRight,
  102. css`
  103. opacity: ${x === -(childrensWidth - containerWidth) ? 0 : 1};
  104. `,
  105. rightControlCss,
  106. ]}
  107. direction="right"
  108. onClick={() => {
  109. handleScroll('right')
  110. }}
  111. />
  112. </div>
  113. )
  114. }
  115. export default Carousel