thiskevinwang/coffee-code-climb

View on GitHub
src/components/StickyNumbers/index.tsx

Summary

Maintainability
D
1 day
Test Coverage
import * as React from "react"
import "intersection-observer"
import ResizeObserver from "resize-observer-polyfill"
import {
  useSpring,
  animated,
  config,
  /** types */
  AnimatedValue,
  OpaqueInterpolation,
} from "react-spring"
import { useScroll, useHover, useGesture, addV } from "react-use-gesture"
import { ReactEventHandlers } from "react-use-gesture/dist/types"
import styled from "styled-components"
import _ from "lodash"

import useIO from "hooks/useIO"
import { rhythm } from "utils/typography"

// TODO: FIX THE OVERFLOW FOR MOBILE
const STICKY_STYLE = {
  transform: `scale(1.3)`,
  opacity: 0.9,
  background: `rgba(100,255,100)`,
}
const INTERSECTING_STYLE = {
  transform: `scale(1)`,
  opacity: 0,
  background: `rgba(255,100,100)`,
}
const MOUSE_OVER_STYLE = {
  transform: `scale(1.5)`,
  opacity: 0.9,
  background: `rgba(100,100,255)`,
}

type Arr = {
  bind: { ref: React.MutableRefObject<any> }
  props: AnimatedValue<any>
  bindHoverProps: (...args: any[]) => ReactEventHandlers
  bindDragProps: (...args: any[]) => ReactEventHandlers
  xy: OpaqueInterpolation<number[]>
}

// z-index & opacity
// https://stackoverflow.com/a/2849104/9823455

const Container = styled(animated.div)`
  position: absolute;
  right: 0%;
  padding-right: ${rhythm(0.5)};
  /* border: 1px dotted red; */
`
const Sentinel = styled(animated.div)`
  padding-top: 5px;
  padding-bottom: 0px;
  z-index: 10;
`
const StickyNumber = styled(animated.p)`
  border-radius: 5px;
  cursor: move;
  font-size: 20px;
  font-weight: 100;
  text-align: center;
  padding-right: 5px;
  padding-left: 5px;
  position: sticky;
  position: -webkit-sticky;
  top: 85px;
  z-index: 10;
`

/** divide the page up into these many divisions */
const DIVISIONS = 10
/** an empty array  */
const ARRAY_FROM_DIVISIONS = Array.from(Array(DIVISIONS))

/** !!! MAIN COMPONENT !!! */
const StickyNumbers = () => {
  const [isScrolling, setIsScrolling] = React.useState(false)
  const [isHovering, setIsHovering] = React.useState(false)

  /**
   * The shared height of each Sentinel
   * This is needed for the parent of a `sticky` element
   */
  const [sentinelProps, setSentinelProps] = useSpring(() => ({
    height: 0,
  }))
  const [ro] = React.useState(
    () =>
      new ResizeObserver(([entry]: [ResizeObserverEntry]) => {
        setSentinelProps({
          height: Math.floor(entry.contentRect.height / DIVISIONS),
        })
      })
  )
  React.useEffect(() => {
    ro.observe(document.documentElement)
    return () => {
      ro.disconnect()
    }
  }, [ro])

  /**
   * # arr
   * An array of objects with the following properties... + others
   * @property {{ref: MutableRefObject}} bind refs to be attached to targets to be observed by IntersectionObeservers (useIO)
   * @property {AnimatedValue} props
   */
  const arr: Arr[] = ARRAY_FROM_DIVISIONS.map(e => {
    const [isIntersecting, bind] = useIO({
      root: null,
      rootMargin: `-100px 0px 100%`,
      threshold: 0.99,
    })
    const [isMouseOver, setIsMouseOver] = React.useState(false)
    const [isDragging, setIsDragging] = React.useState(false)

    /** spring props for `useDrag` */
    const [{ xy }, setXY] = useSpring(() => ({
      xy: [0, 0],
      config: config.wobbly,
    }))

    /**
     * @usage <animated.div {...bindDragProps()} />
     */
    const bindDragProps = useGesture({
      onDrag: ({
        down,
        movement,
        event,
        memo = xy.getValue(),
        // I didn't know you could do this!
        // memo: [mX, my] = xy.getValue(),
      }) => {
        event.preventDefault()

        setXY({ xy: addV(movement, memo) })
        setIsDragging(down)
        return memo
      },
    })

    const bindHoverProps = useHover(({ hovering }) => {
      setIsMouseOver(hovering)
    })

    /**
     * # individual spring props
     * for each array element
     *
     * ## 3 different styles
     * @MOUSE_OVER_STYLE when hovering
     * @INTERSECTING_STYLE when intersecting with the IO
     * @STICKY_STYLE when no longer intersecting (AKA sticky at top)
     *
     * @TODO make this object property overwriting neater
     */
    const props = useSpring({
      to:
        isMouseOver || isDragging
          ? { ...MOUSE_OVER_STYLE }
          : isIntersecting
          ? {
              ...INTERSECTING_STYLE,
              opacity:
                isHovering || isScrolling || isDragging
                  ? 0.8
                  : INTERSECTING_STYLE.opacity,
              transform:
                isHovering || isScrolling || isDragging
                  ? `scale(1)`
                  : INTERSECTING_STYLE.transform,
            }
          : {
              ...STICKY_STYLE,
              opacity:
                isHovering || isScrolling || isDragging
                  ? 1
                  : STICKY_STYLE.opacity,
              transform:
                isHovering || isScrolling || isDragging
                  ? `scale(1.4)`
                  : STICKY_STYLE.transform,
            },
      config: config.wobbly,
    })

    return {
      isIntersecting,
      bind,
      props,
      bindHoverProps,
      bindDragProps,
      xy,
    }
  })

  /** debounced scroll-end handler */
  const reset = React.useCallback(
    _.debounce(() => {
      setIsScrolling(false)
    }, 700),
    []
  )
  /**
   * bindScrollGesture
   *
   * A scroll-gesture handler for the `window`
   */
  const bindScrollGesture = useScroll(
    state => {
      if (state.scrolling) setIsScrolling(state.scrolling)
      if (!state.scrolling) reset()
    },
    { domTarget: typeof window !== "undefined" && window }
  )
  React.useEffect(bindScrollGesture, [bindScrollGesture])

  const containerRef = React.useRef(null)
  const bindContainerProps = useHover(
    state => {
      // console.log(state)
      setIsHovering(state.hovering)
    },
    { domTarget: containerRef }
  )
  return (
    <Container ref={containerRef} {...bindContainerProps()}>
      {arr.map(
        ({ bind, props, bindHoverProps, bindDragProps, xy }, i: number) => {
          return (
            <Sentinel
              key={i}
              {...bind}
              {...bindDragProps()}
              style={{
                ...sentinelProps,
                transform: xy.interpolate(
                  (x, y) => `translate3D(${x}px, ${y}px, 0)`
                ),
              }}
            >
              <StickyNumber {...bindHoverProps()} style={props}>{`${(i * 100) /
                DIVISIONS}%`}</StickyNumber>
            </Sentinel>
          )
        }
      )}
    </Container>
  )
}

export { StickyNumbers }