Browse Source

Merge pull request #1554 from Gamaranto/carousel-improvements

Integrate Gliderjs
Bedeho Mender 4 years ago
parent
commit
2c2d42c17a

+ 1 - 0
package.json

@@ -93,6 +93,7 @@
     "react-app-rewired": "^2.1.6",
     "react-docgen-typescript-loader": "^3.7.1",
     "react-dom": "^16.13.1",
+    "react-glider": "^2.0.2",
     "react-player": "^2.2.0",
     "react-scripts": "3.4.1",
     "react-spring": "^8.0.27",

+ 4 - 1
src/components/ChannelGallery.tsx

@@ -3,6 +3,7 @@ import styled from '@emotion/styled'
 
 import { ChannelPreviewBase, Gallery } from '@/shared/components'
 import ChannelPreview from './ChannelPreviewWithNavigation'
+import { spacing } from '@/shared/theme'
 import { ChannelFields } from '@/api/queries/__generated__/ChannelFields'
 
 type ChannelGalleryProps = {
@@ -14,11 +15,13 @@ type ChannelGalleryProps = {
 
 const PLACEHOLDERS_COUNT = 12
 
+const trackPadding = `${spacing.xs} 0 0 ${spacing.xs}`
+
 const ChannelGallery: React.FC<ChannelGalleryProps> = ({ title, action, channels, loading }) => {
   const displayPlaceholders = loading || !channels
 
   return (
-    <Gallery title={title} action={action} disableControls={displayPlaceholders}>
+    <Gallery title={title} action={action} trackPadding={trackPadding}>
       {displayPlaceholders
         ? Array.from({ length: PLACEHOLDERS_COUNT }).map((_, idx) => (
             <StyledChannelPreviewBase key={`channel-placeholder-${idx}`} />

+ 8 - 32
src/components/VideoGallery.tsx

@@ -1,11 +1,12 @@
-import React, { useCallback, useMemo, useState } from 'react'
-import { css, SerializedStyles } from '@emotion/core'
+import React from 'react'
+
 import styled from '@emotion/styled'
 
 import { Gallery, MAX_VIDEO_PREVIEW_WIDTH, VideoPreviewBase } from '@/shared/components'
 import VideoPreview from './VideoPreviewWithNavigation'
 import { VideoFields } from '@/api/queries/__generated__/VideoFields'
-import { CAROUSEL_CONTROL_SIZE } from '@/shared/components/Carousel'
+
+import { spacing } from '@/shared/theme'
 
 type VideoGalleryProps = {
   title: string
@@ -16,42 +17,18 @@ type VideoGalleryProps = {
 
 const PLACEHOLDERS_COUNT = 12
 
-const VideoGallery: React.FC<VideoGalleryProps> = ({ title, action, videos, loading }) => {
-  const [posterSize, setPosterSize] = useState(0)
-  const [galleryControlCss, setGalleryControlCss] = useState<SerializedStyles>(css``)
-
-  useMemo(() => {
-    if (!posterSize) {
-      return
-    }
-
-    const topPx = posterSize / 2 - CAROUSEL_CONTROL_SIZE / 2
-    setGalleryControlCss(css`
-      top: ${topPx}px;
-    `)
-  }, [posterSize])
+const trackPadding = `${spacing.xs} 0 0 ${spacing.xs}`
 
+const VideoGallery: React.FC<VideoGalleryProps> = ({ title, action, videos, loading }) => {
   const displayPlaceholders = loading || !videos
 
-  const imgRef = useCallback((node: HTMLImageElement) => {
-    if (node != null) {
-      setPosterSize(node.clientHeight)
-    }
-  }, [])
-
   return (
-    <Gallery
-      title={title}
-      action={action}
-      leftControlCss={galleryControlCss}
-      rightControlCss={galleryControlCss}
-      disableControls={displayPlaceholders}
-    >
+    <Gallery title={title} action={action} trackPadding={trackPadding}>
       {displayPlaceholders
         ? Array.from({ length: PLACEHOLDERS_COUNT }).map((_, idx) => (
             <StyledVideoPreviewBase key={`video-placeholder-${idx}`} />
           ))
-        : videos!.map((video, idx) => (
+        : videos!.map((video) => (
             <StyledVideoPreview
               id={video.id}
               channelId={video.channel.id}
@@ -62,7 +39,6 @@ const VideoGallery: React.FC<VideoGalleryProps> = ({ title, action, videos, load
               createdAt={video.publishedOnJoystreamAt}
               duration={video.duration}
               posterURL={video.thumbnailURL}
-              imgRef={idx === 0 ? imgRef : null}
               key={video.id}
             />
           ))}

+ 47 - 49
src/shared/components/Carousel/Carousel.style.ts

@@ -1,49 +1,47 @@
-import { makeStyles, StyleFn } from '../../utils'
-import { spacing } from '../../theme'
-import theme from '@/shared/theme'
-
-export type CarouselStyleProps = Record<string, unknown>
-
-export const CAROUSEL_CONTROL_SIZE = theme.sizes.b12
-
-const container: StyleFn = () => ({
-  position: 'relative',
-  display: 'flex',
-})
-const outerItemsContainer: StyleFn = () => ({
-  overflow: 'hidden',
-  padding: `${spacing.xs} 0 0 ${spacing.xs}`,
-  margin: `-${spacing.xs} 0 0 -${spacing.xs}`,
-})
-
-const innerItemsContainer: StyleFn = () => ({
-  display: 'flex',
-})
-
-const navBase: StyleFn = () => ({
-  minWidth: `${CAROUSEL_CONTROL_SIZE}px`,
-  minHeight: `${CAROUSEL_CONTROL_SIZE}px`,
-  width: `${CAROUSEL_CONTROL_SIZE}px`,
-  height: `${CAROUSEL_CONTROL_SIZE}px`,
-  position: 'absolute',
-})
-
-const navLeft: StyleFn = (styles) => ({
-  ...styles,
-  left: 0,
-  top: `calc(50% - ${Math.round((CAROUSEL_CONTROL_SIZE + 1) / 2)}px)`,
-})
-
-const navRight: StyleFn = (styles) => ({
-  ...styles,
-  right: 0,
-  top: `calc(50% - ${Math.round((CAROUSEL_CONTROL_SIZE + 1) / 2)}px)`,
-})
-
-export const useCSS = (props: CarouselStyleProps) => ({
-  container: makeStyles([container])(props),
-  outerItemsContainer: makeStyles([outerItemsContainer])(props),
-  innerItemsContainer: makeStyles([innerItemsContainer])(props),
-  navLeft: makeStyles([navBase, navLeft])(props),
-  navRight: makeStyles([navBase, navRight])(props),
-})
+import styled from '@emotion/styled'
+import Glider from 'react-glider'
+
+import Icon from '../Icon'
+import { colors } from '../../theme'
+
+export const Container = styled.div<{ trackPadding: string }>`
+  .glider-prev,
+  .glider-next {
+    position: absolute;
+
+    display: grid;
+    place-items: center;
+    color: ${colors.white};
+    background-color: ${colors.blue[500]};
+    border: unset;
+    width: 48px;
+    height: 48px;
+    transition: none;
+    :hover {
+      color: ${colors.white};
+      background-color: ${colors.blue[700]};
+    }
+    :active {
+      background-color: ${colors.blue[900]};
+    }
+  }
+  .glider-prev.disabled,
+  .glider-next.disabled {
+    opacity: 0;
+  }
+  .glider-prev {
+    left: 0;
+  }
+  .glider-next {
+    right: 0;
+  }
+
+  .glider-track {
+    padding: ${(props) => props.trackPadding};
+  }
+`
+
+export const StyledGlider = styled(Glider)`
+  scrollbar-width: none;
+`
+export const Arrow = styled(Icon)``

+ 55 - 80
src/shared/components/Carousel/Carousel.tsx

@@ -1,94 +1,69 @@
-import React, { useState } from 'react'
-import { SerializedStyles } from '@emotion/core'
-import { animated, useSpring } from 'react-spring'
-import useResizeObserver from 'use-resize-observer'
-import { CarouselStyleProps, useCSS } from './Carousel.style'
-import NavButton from '../NavButton'
+import React, { useState, useLayoutEffect, useRef } from 'react'
+import { GliderProps, GliderMethods } from 'react-glider'
 
-export type CarouselProps = {
-  containerCss?: SerializedStyles
-  leftControlCss?: SerializedStyles
-  rightControlCss?: SerializedStyles
-  disableControls?: boolean
-  onScroll?: (direction: 'left' | 'right') => void
-} & CarouselStyleProps
+import { Container, StyledGlider, Arrow } from './Carousel.style'
 
-const Carousel: React.FC<CarouselProps> = ({
-  children,
-  containerCss,
-  leftControlCss,
-  rightControlCss,
-  disableControls = false,
-  onScroll = () => {},
-}) => {
-  const [scroll, setScroll] = useSpring(() => ({
-    transform: `translateX(0px)`,
-  }))
-  const [carouselOffset, setCarouselOffset] = useState(0)
-  const { width: containerWidth = 0, ref: containerRef } = useResizeObserver<HTMLDivElement>()
-  const { width: childrenWidth = 0, ref: childrenContainerRef } = useResizeObserver<HTMLDivElement>()
+import 'glider-js/glider.min.css'
 
-  const styles = useCSS({})
+type CarouselProps = {
+  trackPadding?: string
+} & GliderProps
 
-  const maxScrollOffset = childrenWidth - containerWidth
+type TrackProps = {
+  className?: string
+  padding?: string
+}
+const Track: React.FC<TrackProps> = ({ className = '', ...props }) => (
+  <div className={`glider-track ${className}`} {...props} />
+)
+
+const RightArrow = <Arrow name="chevron-right" />
+const LeftArrow = <Arrow name="chevron-left" />
 
-  const showLeftControl = !disableControls && carouselOffset > 0
-  const showRightControl = !disableControls && carouselOffset < maxScrollOffset
+const Carousel: React.FC<CarouselProps> = ({ children, trackPadding = '0', className, ...gliderProps }) => {
+  //  The GliderMethods type only has methods and I need the full instance
+  const gliderRef = useRef<GliderMethods & { ele: HTMLDivElement }>()
+  const [arrows, setArrows] = useState<{ prev: HTMLButtonElement; next: HTMLButtonElement } | undefined>(undefined)
 
-  const handleScroll = (direction: 'left' | 'right') => {
-    if (containerWidth == null) {
-      return
+  useLayoutEffect(() => {
+    if (gliderRef.current) {
+      const glider = gliderRef.current.ele
+      const prevArrow = glider.previousSibling as HTMLButtonElement
+      const nextArrow = glider.nextSibling as HTMLButtonElement
+
+      setArrows({ prev: prevArrow, next: nextArrow })
     }
-    let scrollAmount
-    switch (direction) {
-      case 'left': {
-        // Prevent overscroll on the left
-        const newOffset = carouselOffset - containerWidth
-        scrollAmount = newOffset < 0 ? 0 : newOffset
-        onScroll('left')
-        break
-      }
-      case 'right': {
-        // Prevent overscroll on the right
-        const newOffset = carouselOffset + containerWidth
-        scrollAmount = newOffset > maxScrollOffset ? maxScrollOffset : newOffset
-        onScroll('right')
-        break
+  }, [])
+
+  // This is needed because react-glider will render arrows only if the arrows option is undefined, so arrows won't display if you pass an object to StyledGlider
+  React.useLayoutEffect(() => {
+    if (gliderRef.current && arrows) {
+      const { prev: prevArrow, next: nextArrow } = arrows
+      const container = gliderRef.current.ele.parentElement
+      if (container) {
+        container.insertBefore(prevArrow, gliderRef.current.ele)
+        container.appendChild(nextArrow)
       }
     }
-    setCarouselOffset(scrollAmount)
-    setScroll({
-      transform: `translateX(-${scrollAmount}px)`,
-    })
-  }
-
-  if (!Array.isArray(children)) {
-    return <>{children}</>
-  }
+  }, [arrows])
 
   return (
-    <div css={[styles.container, containerCss]}>
-      <div css={styles.outerItemsContainer} ref={containerRef}>
-        <animated.div css={styles.innerItemsContainer} style={scroll}>
-          <div css={styles.innerItemsContainer} ref={childrenContainerRef}>
-            {children.map((element, idx) => (
-              <React.Fragment key={`Carousel-${idx}`}>{element}</React.Fragment>
-            ))}
-          </div>
-        </animated.div>
-      </div>
-      {showLeftControl && (
-        <NavButton outerCss={[styles.navLeft, leftControlCss]} direction="left" onClick={() => handleScroll('left')} />
-      )}
-      {showRightControl && (
-        <NavButton
-          outerCss={[styles.navRight, rightControlCss]}
-          direction="right"
-          onClick={() => handleScroll('right')}
-        />
-      )}
-    </div>
+    <Container trackPadding={trackPadding} className={className}>
+      <StyledGlider
+        addTrack
+        skipTrack
+        hasArrows
+        draggable
+        ref={gliderRef as React.RefObject<GliderMethods>}
+        iconLeft={LeftArrow}
+        iconRight={RightArrow}
+        // Akward conversion needed until this is resolved: https://github.com/hipstersmoothie/react-glider/issues/36
+        arrows={(arrows as unknown) as { prev: string; next: string }}
+        {...gliderProps}
+      >
+        <Track padding={trackPadding}>{children}</Track>
+      </StyledGlider>
+    </Container>
   )
 }
-
 export default Carousel

+ 2 - 4
src/shared/components/Carousel/index.ts

@@ -1,5 +1,3 @@
-import Carousel, { CarouselProps } from './Carousel'
-import { CAROUSEL_CONTROL_SIZE } from './Carousel.style'
+import Carousel from './Carousel'
 
-export { Carousel as default, CAROUSEL_CONTROL_SIZE }
-export type { CarouselProps }
+export default Carousel

+ 1 - 0
src/shared/components/ChannelPreview/ChannelPreviewBase.style.tsx

@@ -5,6 +5,7 @@ import { colors, sizes, spacing } from '../../theme'
 const imageTopOverflow = '2rem'
 
 export const OuterContainer = styled.article`
+  max-width: 200px;
   width: 200px;
   height: ${`calc(166px + ${imageTopOverflow})`};
   padding-top: ${imageTopOverflow};

+ 22 - 1
src/shared/components/Gallery/Gallery.style.ts

@@ -1,4 +1,6 @@
-import { spacing } from '../../theme'
+import styled from '@emotion/styled'
+import { spacing, typography } from '../../theme'
+
 import { makeStyles, StyleFn } from '../../utils'
 
 const container: StyleFn = () => ({
@@ -6,6 +8,25 @@ const container: StyleFn = () => ({
   flexDirection: 'column',
 })
 
+export const Container = styled.section`
+  display: flex;
+  flex-direction: column;
+`
+export const HeadingContainer = styled.div`
+  display: flex;
+  justify-content: space-between;
+  align-items: baseline;
+  margin-bottom: ${spacing.m};
+
+  > h4 {
+    font-size: ${typography.sizes.h5};
+    margin: 0;
+  }
+  > button {
+    font-size: ${typography.sizes.subtitle2};
+    padding: 0;
+  }
+`
 const headingContainer: StyleFn = () => ({
   display: 'flex',
   justifyContent: 'space-between',

+ 10 - 12
src/shared/components/Gallery/Gallery.tsx

@@ -1,30 +1,28 @@
 import React from 'react'
-import { SerializedStyles } from '@emotion/core'
-import { useCSS } from './Gallery.style'
+import { Container, HeadingContainer } from './Gallery.style'
 import Button from '../Button'
-import Carousel, { CarouselProps } from '../Carousel'
+import Carousel from '../Carousel'
 
 type GalleryProps = {
   title?: string
   action?: string
   onClick?: () => void
-  containerCss?: SerializedStyles
-} & CarouselProps
+  className?: string
+} & React.ComponentProps<typeof Carousel>
 
-const Gallery: React.FC<GalleryProps> = ({ title, action = '', containerCss, onClick, ...props }) => {
-  const styles = useCSS()
+const Gallery: React.FC<GalleryProps> = ({ title, action = '', className, onClick, ...carouselProps }) => {
   return (
-    <section css={[styles.container, containerCss]}>
-      <div css={styles.headingContainer}>
+    <Container className={className}>
+      <HeadingContainer>
         {title && <h4>{title}</h4>}
         {action && (
           <Button variant="tertiary" onClick={onClick}>
             {action}
           </Button>
         )}
-      </div>
-      <Carousel {...props} />
-    </section>
+      </HeadingContainer>
+      <Carousel {...carouselProps} />
+    </Container>
   )
 }
 

+ 5 - 8
src/shared/stories/12-Carousel.stories.tsx

@@ -21,13 +21,10 @@ export const Default = () => (
   </Carousel>
 )
 
-export const LongerScroll = () => (
-  <Carousel scrollAmount={500}>
-    <CarouselItem>CarouselItem 1</CarouselItem>
-    <CarouselItem>CarouselItem 2</CarouselItem>
-    <CarouselItem>CarouselItem 3</CarouselItem>
-    <CarouselItem>CarouselItem 4</CarouselItem>
-    <CarouselItem>CarouselItem 5</CarouselItem>
-    <CarouselItem>CarouselItem 6</CarouselItem>
+export const Draggable = () => (
+  <Carousel draggable>
+    {Array.from({ length: 10 }, (_, i) => (
+      <CarouselItem> Carousel Item {i}</CarouselItem>
+    ))}
   </Carousel>
 )

+ 12 - 0
yarn.lock

@@ -8393,6 +8393,11 @@ github-slugger@^1.0.0:
   dependencies:
     emoji-regex ">=6.0.0 <=6.1.1"
 
+glider-js@1.7.1:
+  version "1.7.1"
+  resolved "https://registry.yarnpkg.com/glider-js/-/glider-js-1.7.1.tgz#ffa4ae6775b0c005e2eeca72e586acd1f64774e9"
+  integrity sha512-kry/GVCmo/MFUtMTgZONTjXorcvzKskt4VklSQD8kVDJbVfT3arV4Xf7o/ZwMZkDhLKlWX9bpV30fsp7vLHuUA==
+
 glob-base@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
@@ -14063,6 +14068,13 @@ react-focus-lock@^2.1.0:
     use-callback-ref "^1.2.1"
     use-sidecar "^1.0.1"
 
+react-glider@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/react-glider/-/react-glider-2.0.2.tgz#d8d098f341b4776c6cafda160dfd275ba23b3194"
+  integrity sha512-ONDO+e5gFTp+R0bVGvEh+NWGSHVaTdPrQxNYQWz3u8Oybkr48DWmqOxLpdEH48kd09Q12TI3kd13EgOO8Xen/A==
+  dependencies:
+    glider-js "1.7.1"
+
 react-helmet-async@^1.0.2:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-1.0.7.tgz#b988fbc3abdc4b704982bb74b9cb4a08fcf062c1"