🐭

複数要素を表示させつつ、縦スクロールで1要素ずつスライドするページを作る

2023/08/31に公開

初めに...

こちらの記事だとトラックパッドを使った時の挙動がかなり違和感があります。新たに記事を書いたのでこちらをご参照ください。
https://zenn.dev/nanahiryu/articles/71086ad6aaaccd

やりたいこと

  • 複数要素を画面に表示していたい
  • マウスの縦スクロールで1スライドずつ送りたい

完成品

見づらいので「open in new window」をクリックするなどして見てください

実装について

コードの全体像

scrollableScreen.tsx
scrollableScreen.tsx
export const ScrollableScreen = (props: ScrollableScreenProps) => {
  const { index, visibleScreenItemNum = 4, screenItemNum = 8 } = props;
  const screenWindowRef = useRef<HTMLDivElement>(null);
  const [scrollLeftIndex, setScrollLeftIndex] = useState(0);
  const screenSize = 250;
  const screenGap = 4;
  const screenWindowWidth =
    screenSize * visibleScreenItemNum + screenGap * (visibleScreenItemNum - 1);
  const screenWrapperWidth =
    screenSize * screenItemNum + screenGap * (screenItemNum - 1);
  const scrollWidth = screenSize + screenGap;

  useEffect(() => {
    if (!screenWindowRef.current) return;

    screenWindowRef.current.onwheel = (e) => {
      if (!screenWindowRef.current) return;
      e.preventDefault();

      let delta = e.deltaY / Math.abs(e.deltaY);
      if (delta > 0) {
        setScrollLeftIndex((prev) => {
          console.log(prev);
          if (prev === screenItemNum - visibleScreenItemNum) return prev;
          return prev + 1;
        });
      } else {
        setScrollLeftIndex((prev) => {
          console.log(prev);
          if (prev === 0) return prev;
          return prev - 1;
        });
      }

      screenWindowRef.current!.scrollLeft = scrollLeftIndex * scrollWidth;
    };
  }, [scrollLeftIndex, scrollWidth]);

  const indexArray: number[] = Array.from(
    { length: screenItemNum },
    (_, index) => index + 1
  );

  return (
    <div className={styles.scrollable_screen_field}>
      <p className={styles.scrollable_screen_index}>{index}</p>
      <div
        className={styles.scrollable_window}
        style={{ width: screenWindowWidth }}
        ref={screenWindowRef}
      >
        <div
          className={styles.scrollable_screen_wrapper}
          style={{ width: screenWrapperWidth, gap: screenGap }}
        >
          {indexArray.map((i) => (
            <div
              key={i}
              className={styles.scrollable_screen}
              style={{
                width: screenSize,
                height: screenSize
              }}
            >
              <p className={styles.display_index}>{i}</p>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
};
scrollableScreen.module.scss
scrollableScreen.module.scss
.scrollable_screen_field {
  display: flex;
  flex-direction: column;
  gap: 40px;
  align-items: center;
  justify-content: center;
  width: 100%;
  margin: 80px 0;
  .scrollable_screen_index {
    color: black;
    font-size: 24px;
    font-style: normal;
    font-weight: 400;
    line-height: normal;
  }
  .scrollable_window {
    overflow: auto;
    scroll-behavior: smooth;
    &::-webkit-scrollbar {
      display: none;
    }
    .scrollable_screen_wrapper {
      display: flex;
      .scrollable_screen {
        display: flex;
        align-items: center;
        justify-content: center;
        background-color: gray;
        .display_index {
          font-size: xx-large;
          color: white;
        }
      }
    }
  }
}

スクロール部分について(tsx側)

以降それぞれのdivタグは次のように呼ぶ

  • window: className={styles.scrollable_window}のdivタグ
  • wrapper: className={styles.scrollable_screen_wrapper}のdivタグ
  • screen: className={styles.scrollable_screen}のdivタグ
scrollableScreen.tsx
const screenWindowRef = useRef<HTMLDivElement>(null);
const [scrollLeftIndex, setScrollLeftIndex] = useState(0);
const screenSize = 250;
const screenGap = 4;
const screenWindowWidth =
screenSize * visibleScreenItemNum + screenGap * (visibleScreenItemNum - 1);
const screenWrapperWidth =
screenSize * screenItemNum + screenGap * (screenItemNum - 1);
const scrollWidth = screenSize + screenGap;

定数やstateの定義
scrollLeftIndexのstateは[今一番左側に表示されているボックスに書かれた数字 - 1]を表現している
screenWindowWidth, screenWrapperWidthはそれぞれwindowとwrapperの幅を表している
scrollWidthscrollLeftIndexが1増加した時に要素がどれだけ動くかを表している

scrollableScreen.tsx
  useEffect(() => {
    if (!screenWindowRef.current) return;

    screenWindowRef.current.onwheel = (e) => {
      if (!screenWindowRef.current) return;
      e.preventDefault();

      let delta = e.deltaY / Math.abs(e.deltaY);
      if (delta > 0) {
        setScrollLeftIndex((prev) => {
          console.log(prev);
          if (prev === screenItemNum - visibleScreenItemNum) return prev;
          return prev + 1;
        });
      } else {
        setScrollLeftIndex((prev) => {
          console.log(prev);
          if (prev === 0) return prev;
          return prev - 1;
        });
      }

      screenWindowRef.current!.scrollLeft = scrollLeftIndex * scrollWidth;
    };
  }, [scrollLeftIndex, scrollWidth]);

全体としてはマウスホイールのスクロールが検知された時にその方向に応じてscrollLeftIndexを+1 or -1させ、scrollLeftIndexを参照してscreenWindowRef.current.scrollLeftに現在のスクロール位置(scrollLeftIndex * scrollWidth)を代入している。

わかりづらそうな(自分がつまづいた)ポイントに分けてリンクを貼っておく

refがわからない/ref.currentってなんだ
https://zenn.dev/luvmini511/articles/744bd38b14cfa1

e.preventDefault()ってなぜ必要なんだ
https://developer.mozilla.org/ja/docs/Web/API/Event/preventDefault
https://qiita.com/yokoto/items/27c56ebc4b818167ef9e

scrollLeftって具体的に何をしてるんだ
https://developer.mozilla.org/ja/docs/Web/API/Element/scrollLeft

スクロール部分について(scss側)

scrollable.scss
  .scrollable_window {
    overflow: auto;
    scroll-behavior: smooth;
    &::-webkit-scrollbar {
      display: none;
    }
    .scrollable_screen_wrapper {
      display: flex;
             // 略
    }
  }

window

overflow: autoを指定することでスクロールを可能かつ要素の外側に出た子要素を見えないようにしている
scroll-behavior: smoothを指定することでスクロールした時に次のスライドが見えるまで、スライドが滑らかに動くようにしている

&::-webkit-scrollbar {
  display: none;
}

スクロールバーがダサいので隠している

wrapper

子要素を等間隔横並びにするためにdisplay: flexを使用

課題

現状トラックパッドで非常に操作しづらくなっているため, 何かしらの対策を考える必要がある

参考リンク

https://qiita.com/nemutas/items/88e3f513bafe6149e84a

Discussion