Browse Source

Merge pull request #1637 from Gamaranto/add-fade-to-carousel

Add Fade To Carousel Edges
Bedeho Mender 4 years ago
parent
commit
ca45fc2d45

+ 2 - 1
package.json

@@ -55,6 +55,7 @@
     "@storybook/theming": "^5.3.19",
     "@types/enzyme": "^3.10.5",
     "@types/faker": "^5.1.0",
+    "@types/glider-js": "^1.7.3",
     "@types/jest": "^24.0.0",
     "@types/lodash": "^4.14.157",
     "@types/node": "^12.0.0",
@@ -80,6 +81,7 @@
     "eslint-plugin-react-hooks": "^4.0.4",
     "faker": "^5.1.0",
     "fluent-ffmpeg": "^2.1.2",
+    "glider-js": "^1.7.3",
     "graphql": "^15.3.0",
     "graphql-tag": "^2.11.0",
     "graphql-tools": "^6.2.4",
@@ -93,7 +95,6 @@
     "react-app-rewired": "^2.1.6",
     "react-docgen-typescript-loader": "^3.7.1",
     "react-dom": "^16.13.1",
-    "react-glider": "^2.0.2",
     "react-error-boundary": "^3.0.2",
     "react-player": "^2.2.0",
     "react-scripts": "3.4.1",

+ 1 - 2
src/components/VideoGallery.tsx

@@ -1,5 +1,4 @@
 import React from 'react'
-import { BreakPoint } from 'react-glider'
 
 import styled from '@emotion/styled'
 
@@ -31,7 +30,7 @@ const breakpoints = breakpointsOfGrid({
   settings: {
     slidesToShow: idx + 1,
   },
-})) as BreakPoint[]
+}))
 
 const VideoGallery: React.FC<VideoGalleryProps> = ({ title, videos, loading }) => {
   const displayPlaceholders = loading || !videos

+ 25 - 16
src/shared/components/Button/Button.tsx

@@ -4,24 +4,28 @@ import { ButtonStyleProps, StyledButton, StyledIcon } from './Button.style'
 import type { IconType } from '../Icon'
 
 export type ButtonProps = {
-  icon: IconType
-  disabled: boolean
-  containerCss: SerializedStyles
-  className: string
-  onClick: (e: React.MouseEvent<HTMLButtonElement>) => void
+  children?: React.ReactNode
+  icon?: IconType
+  disabled?: boolean
+  containerCss?: SerializedStyles
+  className?: string
+  onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
 } & Omit<ButtonStyleProps, 'clickable' | 'hasText'>
 
-const Button: React.FC<Partial<ButtonProps>> = ({
-  children,
-  icon,
-  variant = 'primary',
-  disabled = false,
-  full = false,
-  size = 'regular',
-  containerCss,
-  className,
-  onClick,
-}) => {
+const ButtonComponent: React.ForwardRefRenderFunction<HTMLButtonElement, ButtonProps> = (
+  {
+    children,
+    icon,
+    variant = 'primary',
+    disabled = false,
+    full = false,
+    size = 'regular',
+    containerCss,
+    className,
+    onClick,
+  },
+  ref
+) => {
   const clickable = !!onClick
   const hasText = !!children
   const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
@@ -40,6 +44,7 @@ const Button: React.FC<Partial<ButtonProps>> = ({
       hasText={hasText}
       full={full}
       size={size}
+      ref={ref}
     >
       {icon && <StyledIcon name={icon} />}
       {children && <span>{children}</span>}
@@ -47,4 +52,8 @@ const Button: React.FC<Partial<ButtonProps>> = ({
   )
 }
 
+const Button = React.forwardRef(ButtonComponent)
+
+Button.displayName = 'Button'
+
 export default Button

+ 40 - 34
src/shared/components/Carousel/Carousel.style.ts

@@ -1,48 +1,54 @@
 import styled from '@emotion/styled'
-import Glider from 'react-glider'
+import Button from '../Button'
 
-import Icon from '../Icon'
-import { colors } from '../../theme'
+export const Container = styled.div`
+  position: relative;
+`
+
+export const BackgroundGradient = styled.div<{ direction: 'prev' | 'next' }>`
+  position: absolute;
+  top: 0;
+  left: ${(props) => (props.direction === 'prev' ? 0 : 'auto')};
+  right: ${(props) => (props.direction === 'next' ? 0 : 'auto')};
+  bottom: 0;
+  width: 10%;
+  z-index: 1;
+  background-image: linear-gradient(
+    ${(props) => (props.direction === 'prev' ? 270 : 90)}deg,
+    transparent,
+    var(--gradientColor, transparent)
+  );
+  pointer-events: none;
+`
 
-export const Container = styled.div<{ trackPadding: string }>`
-  .glider-prev,
-  .glider-next {
-    position: absolute;
+export const Arrow = styled(Button)`
+  position: absolute;
+  width: 48px;
+  height: 48px;
+  transition: none;
 
-    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;
+  &.disabled {
+    display: none;
   }
-  .glider-prev {
+
+  &.glider-prev {
     left: 0;
   }
-  .glider-next {
+  &.glider-next {
     right: 0;
   }
-
-  .glider-track {
-    padding: ${(props) => props.trackPadding};
-    align-items: start;
+  + ${BackgroundGradient} {
+    --gradientColor: black;
+  }
+  &.disabled + ${BackgroundGradient} {
+    --gradientColor: transparent;
   }
 `
 
-export const StyledGlider = styled(Glider)`
+export const GliderContainer = styled.div`
   scrollbar-width: none;
 `
-export const Arrow = styled(Icon)``
+export const Track = styled.div<{ trackPadding: string }>`
+  align-items: flex-start;
+  padding: ${(props) => props.trackPadding};
+`

+ 23 - 58
src/shared/components/Carousel/Carousel.tsx

@@ -1,74 +1,39 @@
-import React, { useLayoutEffect, useRef, useState } from 'react'
-import { GliderMethods, GliderProps } from 'react-glider'
+import React, { useRef } from 'react'
 
-import { Arrow, Container, StyledGlider } from './Carousel.style'
+import { useGlider, GliderProps } from '../Glider'
 
-import 'glider-js/glider.min.css'
+import { Container, GliderContainer, Arrow, Track, BackgroundGradient } from './Carousel.style'
 
 type CarouselProps = {
   trackPadding?: string
-} & GliderProps
-
-type TrackProps = {
   className?: string
-}
-const Track: React.FC<TrackProps> = ({ className = '', children }) => (
-  <div className={`glider-track ${className}`}>{children}</div>
-)
-
-const RightArrow = <Arrow name="chevron-right" />
-const LeftArrow = <Arrow name="chevron-left" />
+} & GliderProps
 
 const Carousel: React.FC<CarouselProps> = ({
   children,
   trackPadding = '0',
-  className,
+  className = '',
   slidesToShow = 'auto',
-  ...gliderProps
+  ...gliderOptions
 }) => {
-  //  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)
-
-  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 })
-    }
-  }, [])
-
-  // 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)
-      }
-    }
-  }, [arrows])
-
+  const nextArrowRef = useRef<HTMLButtonElement>(null)
+  const prevArrowRef = useRef<HTMLButtonElement>(null)
+  const { ref, getContainerProps, getGliderProps, getTrackProps, getPrevArrowProps, getNextArrowProps } = useGlider<
+    HTMLDivElement
+  >({
+    slidesToShow,
+    arrows: { prev: prevArrowRef.current, next: nextArrowRef.current },
+    ...gliderOptions,
+  })
   return (
-    <Container trackPadding={trackPadding} className={className}>
-      <StyledGlider
-        addTrack
-        skipTrack
-        hasArrows
-        draggable
-        ref={gliderRef as React.RefObject<GliderMethods>}
-        iconLeft={LeftArrow}
-        iconRight={RightArrow}
-        slidesToShow={slidesToShow}
-        // 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>{children}</Track>
-      </StyledGlider>
+    <Container {...getContainerProps({ className })}>
+      <Arrow {...getPrevArrowProps()} icon="chevron-left" ref={prevArrowRef} />
+      <BackgroundGradient direction="prev" />
+      <GliderContainer {...getGliderProps()} ref={ref}>
+        <Track {...getTrackProps({ trackPadding })}>{children}</Track>
+      </GliderContainer>
+      <Arrow {...getNextArrowProps()} icon="chevron-right" ref={nextArrowRef} />
+      <BackgroundGradient direction="next" />
     </Container>
   )
 }

+ 120 - 0
src/shared/components/Glider/Glider.tsx

@@ -0,0 +1,120 @@
+import { useState, useRef, useEffect, useLayoutEffect } from 'react'
+import Glider, { GliderEvent, GliderEventMap, Options } from 'glider-js'
+import 'glider-js/glider.min.css'
+
+type GliderEventListeners = {
+  onAdd?: (event: GliderEvent<GliderEventMap['glider-add']>) => void
+  onAnimated?: (event: GliderEvent<GliderEventMap['glider-animated']>) => void
+  onDestroy?: (event: GliderEvent<GliderEventMap['glider-destroy']>) => void
+  onLoaded?: (event: GliderEvent<GliderEventMap['glider-loaded']>) => void
+  onRefresh?: (event: GliderEvent<GliderEventMap['glider-refresh']>) => void
+  onRemove?: (event: GliderEvent<GliderEventMap['glider-remove']>) => void
+  onSlideHidden?: (event: GliderEvent<GliderEventMap['glider-slide-hidden']>) => void
+  onSlideVisible?: (event: GliderEvent<GliderEventMap['glider-slide-visible']>) => void
+}
+
+export type GliderProps = Options & GliderEventListeners
+
+type PropsWithClassName<T> = {
+  className?: string
+} & T
+function getPropsFor(name: string) {
+  return function <T>({ className, ...otherProps }: PropsWithClassName<T> = {} as PropsWithClassName<T>) {
+    return { className: `${className ? `${className} ` : ''}${name}`, ...otherProps }
+  }
+}
+const getGliderProps = getPropsFor('glider')
+const getTrackProps = getPropsFor('glider-track')
+const getNextArrowProps = getPropsFor('glider-next')
+const getPrevArrowProps = getPropsFor('glider-prev')
+const getContainerProps = getPropsFor('glider-contain')
+
+export function useGlider<T extends HTMLElement>({
+  onAdd,
+  onAnimated,
+  onDestroy,
+  onLoaded,
+  onRefresh,
+  onRemove,
+  onSlideHidden,
+  onSlideVisible,
+  ...gliderOptions
+}: GliderProps) {
+  const [glider, setGlider] = useState<Glider.Static<HTMLElement>>()
+  const element = useRef<T>(null)
+
+  useLayoutEffect(() => {
+    if (!element.current) {
+      return
+    }
+    const newGlider = new Glider(element.current, { skipTrack: true })
+    setGlider(newGlider)
+
+    return () => {
+      if (newGlider) {
+        newGlider.destroy()
+      }
+    }
+  }, [])
+  useLayoutEffect(() => {
+    if (!glider) {
+      return
+    }
+    glider.setOption({ skipTrack: true, ...gliderOptions }, true)
+    glider.refresh(true)
+  }, [gliderOptions, glider])
+
+  useEventListener(element.current, 'glider-add', onAdd)
+  useEventListener(element.current, 'glider-animated', onAnimated)
+  useEventListener(element.current, 'glider-destroy', onDestroy)
+  useEventListener(element.current, 'glider-loaded', onLoaded)
+  useEventListener(element.current, 'glider-refresh', onRefresh)
+  useEventListener(element.current, 'glider-remove', onRemove)
+  useEventListener(element.current, 'glider-slide-hidden', onSlideHidden)
+  useEventListener(element.current, 'glider-slide-visible', onSlideVisible)
+  return {
+    ref: element,
+    glider,
+    getGliderProps,
+    getTrackProps,
+    getNextArrowProps,
+    getPrevArrowProps,
+    getContainerProps,
+  }
+}
+
+function useEventListener<K extends keyof GliderEventMap>(
+  element: HTMLElement | undefined | null,
+  event: K,
+  listener: (event: GliderEvent<GliderEventMap[K]>) => void = () => {}
+) {
+  const savedListener = useRef(listener)
+
+  useEffect(() => {
+    savedListener.current = listener
+  }, [listener])
+
+  useLayoutEffect(() => {
+    if (!element) {
+      return
+    }
+    element.addEventListener(event, savedListener.current)
+    return () => {
+      element.removeEventListener(event, savedListener.current)
+    }
+  }, [event, element])
+}
+
+declare global {
+  interface HTMLElement {
+    addEventListener<K extends keyof GliderEventMap>(
+      type: K,
+      listener: (event: GliderEvent<GliderEventMap[K]>) => void,
+      options?: boolean | AddEventListenerOptions
+    ): void
+    removeEventListener<K extends keyof GliderEventMap>(
+      type: K,
+      listener: (event: GliderEvent<GliderEventMap[K]>) => void
+    ): void
+  }
+}

+ 1 - 0
src/shared/components/Glider/index.tsx

@@ -0,0 +1 @@
+export * from './Glider'

+ 9 - 11
yarn.lock

@@ -2772,6 +2772,11 @@
   resolved "https://registry.yarnpkg.com/@types/faker/-/faker-5.1.2.tgz#3d5a97d5648502d7fb1eeb2809ca105edcd0d59b"
   integrity sha512-a3FADSHjjinczCwr7tTejoMZzbSS5vi70VCyns4C1idxJrDSRGZCQG0s27YppXLcoWrBOkwBbBsZ9vDRDpQK7A==
 
+"@types/glider-js@^1.7.3":
+  version "1.7.3"
+  resolved "https://registry.yarnpkg.com/@types/glider-js/-/glider-js-1.7.3.tgz#2adb1220922e8bfd32fbb8909f256f77d84dfac6"
+  integrity sha512-V+NRCK3sIdhXiZ/hG0U/vLEbl4Atz6ySbUuYeCfVPKiyyx4mfN30rBDatCke2uVhkKOgyQ1gRBeCAzpLAJVe0w==
+
 "@types/glob@^7.1.1":
   version "7.1.3"
   resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183"
@@ -8393,10 +8398,10 @@ 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==
+glider-js@^1.7.3:
+  version "1.7.3"
+  resolved "https://registry.yarnpkg.com/glider-js/-/glider-js-1.7.3.tgz#4d397fb9de0870753953244d146a94248d4eb394"
+  integrity sha512-smiepWalDyBAUtOH6Ev06fHlcQ128OUzGLx97E/9NcR8/fR+uALUVSMytVN6z5pIqO4kDGTzINniqZV2yMf8bg==
 
 glob-base@^0.3.0:
   version "0.3.0"
@@ -14075,13 +14080,6 @@ 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"