永遠にWIP

スクロール可能な方向に影をつけるあれを実装する

Frontend
React

はじめに

こんにちはダメガネです。
皆さんは下の画像のような、スクロール可能な方向に影を表示するようなものを実装したことがあるでしょうか?

割と見たことがあるが、実際に実装するときに少し詰まったのでメモ程度代わりに記事を書こうと思います。

注意: この記事は結構雑です

実装

とりあえず実装を載せます!

import React, { useRef, useState, useEffect, useCallback } from "react";
import styled from "@emotion/styled";

const scrollShadowMixin = (canScroll: boolean) => `
  content: '';
  position: absolute;
  z-index: 1;
  width: 100px;
  height: 100%;
  opacity: ${canScroll ? 1 : 0};
  transition: opacity 0.1s;
  pointer-events: none;
`;

const StyledDiv = styled.div<{
  canScrollRight: boolean;
  canScrollLeft: boolean;
}>`
  position: relative;
  display: flex;
  margin: 0 -2px;
  padding: 0 2px;
  width: 200px;
  height: 200px;

  &::after {
    ${(props) => scrollShadowMixin(props.canScrollRight)}

    right: 0;
    left: auto;
    background: linear-gradient(
      270deg,
      #f6fbfc 0%,
      rgba(246, 251, 252, 0) 100%
    );
  }

  &::before {
    ${(props) => scrollShadowMixin(props.canScrollLeft)}

    right: auto;
    left: 0;
    background: linear-gradient(
      270deg,
      rgba(246, 251, 252, 0) 0%,
      #f6fbfc 100%
    );
  }
`;

const Wrapper = styled.div`
  display: flex;
  overflow-x: auto;
`;

const Rect = styled.div`
  width: 100px;
  min-width: 100px;
  height: 100px;
  background-color: red;
  border: 1px solid black;
`;

const useScrollShadow = () => {
  const ref = useRef<HTMLDivElement>(null);
  const [scrollShadow, setScrollShadow] = useState({
    left: false,
    right: false,
  });

  const handleAddScrollShadow = useCallback(() => {
    if (ref.current) {
      const { clientWidth, scrollWidth, scrollLeft } = ref.current;

      if (scrollWidth > clientWidth) {
        if (scrollLeft > 0) {
          setScrollShadow((pre) => ({ ...pre, left: true }));
        } else {
          setScrollShadow((pre) => ({ ...pre, left: false }));
        }

        if (scrollWidth - clientWidth - scrollLeft > 0) {
          setScrollShadow((pre) => ({ ...pre, right: true }));
        } else {
          setScrollShadow((pre) => ({ ...pre, right: false }));
        }
      } else {
        setScrollShadow(() => ({ left: false, right: false }));
      }
    }
  }, [ref.current]);

  useEffect(() => {
    ref.current?.addEventListener('scroll', handleAddScrollShadow);
    window.addEventListener('resize', handleAddScrollShadow);

    handleAddScrollShadow();

    return () => {
      ref.current?.removeEventListener('scroll', handleAddScrollShadow);
      window.removeEventListener('resize', handleAddScrollShadow);
    };
  }, [ref.current]);

  return { ref, scrollShadow };
};


export const ScrollShadow: React.VFC = () => {
  const { ref, scrollShadow } = useScrollShadow();

  return (
    <StyledDiv
      canScrollLeft={scrollShadow.left}
      canScrollRight={scrollShadow.right}
    >
      <Wrapper ref={ref}>
        {[...Array(5).keys()].map((i) => (
          <Rect />
        ))}
      </Wrapper>
    </StyledDiv>
  );
};

https://codesandbox.io/s/nice-shockley-csj9q?file=/src/ScrollShadow.tsx:0-2523

いや、長い...
実装としては、スクロール可能な方向に疑似要素を配置しているというものになります。
ただ、CSSを駆使すれば同じようなことができるらしいです...

https://stackoverflow.com/questions/44793453/how-do-i-add-a-top-and-bottom-shadow-while-scrolling-but-only-when-needed

今回はこのように実装してしまいましたが、上のようなCSSを使った実装を理解してそっちに置き換えてもいいかもしれないです(IEでもしっかり動作するかなども確認したい)。

最後に

いかがでしたでしょうか?(言いたいだけ)
なにか、より良い実装やアドバイスなどがありましたらTwitterなどで教えていただけると嬉しいです

© 2020 DuGlaser