管理画面を作る際などに一覧表を用意する時があります。時折この一覧表の一行に必要な情報が肥大化して手なりで表示した場合、画面内に収まらない場合があります。そういった時はフォントサイズ、折り畳み、一マスに二データ、表示非表示制御などあの手この手で収まる様に苦慮します。この記事で紹介するのはそういった時の解決方法の一つで常に横スクロールバーを表示する方法です。
単純に横に長い表を表示した場合のデモが次です。端の方の情報を見るためにスクロールが必要です。Shift+マウスホイールなどの横スクロールジェスチャーが期待できる環境ならばこのままでも構わないのですが、そうもいかない場合があります。そういった時に横に長い表を維持したままユーザビリティを挙げる解決策の一つが、通常では表の最下部にある横スクロールバーを常に画面内に表示させる、です。
横スクロールバーが常に画面内に入る実装のデモが次です。
これは次のソースコードと react-scroll-sync というライブラリで実現しています。react-scroll-sync は複数要素のスクロールを同期させるためのライブラリで、これで表示されているスクロールバーと表のスクロールを同期させています。
okonet/react-scroll-sync: Synced scroll position across multiple scrollable elements
React SyncScroll Style Guide
ソースコードはざっくばらんに言えば、 position: fixed で画面内にいるスクロールバーを用意し、それと表のスクロールを連動させるコードです。
import "./styles.css"; import { data } from "./data"; import { useRef, useEffect, useState } from "react"; import { ScrollSync, ScrollSyncPane } from "react-scroll-sync"; export default function App() { // 素の JavaScript の要素に作用する機能を駆使するので、各コンポーネントの生要素を得られる様に ref を用意 const tableRef = useRef<HTMLTableElement>(null); const tableContainerRef = useRef<HTMLDivElement>(null); const scrollBarRef = useRef<HTMLDivElement>(null); const scrollBarContainerRef = useRef<HTMLDivElement>(null); const tableBodyTailObserveTargetRef = useRef<HTMLTableRowElement>(null); // テーブル下部スクロールバーの大きさ制御 /** 実行時の表の幅にスクロールバーの幅を一致させる関数 */ const setScrollBarSizeAndPosition = () => { if ( !tableContainerRef.current || !tableRef.current || !scrollBarRef.current || !scrollBarContainerRef.current ) { return; } const innerElementWidth = tableRef.current.clientWidth; const outerElementWidth = tableContainerRef.current.clientWidth; scrollBarRef.current.style.width = `${innerElementWidth}px`; scrollBarContainerRef.current.style.width = `${outerElementWidth}px`; scrollBarContainerRef.current.scrollTo({ top: tableContainerRef.current.scrollTop, left: tableContainerRef.current.scrollLeft }); }; useEffect(() => { if ( !tableContainerRef.current || !tableRef.current || !scrollBarRef.current || !scrollBarContainerRef.current ) { return; } // 初期化時とテーブル要素のサイズ変更時に ResizeObserver でスクロールバーの幅調整関数を実行 // @see https://developer.mozilla.org/en-US/docs/Web/API/Resize_Observer_API setScrollBarSizeAndPosition(); const observer = new ResizeObserver(() => { setScrollBarSizeAndPosition(); }); observer.observe(tableContainerRef.current); return () => { observer.disconnect(); }; }, [data]); // テーブル下部スクロールバーの表示・非表示コントロール const [displayFloatScrollBar, setDisplayFloatScrollBar] = useState(true); useEffect(() => { if (!tableBodyTailObserveTargetRef.current) { return; } // 画面内に表最下部の要素が入った or 出た場合にスクロールバーの表示・非表示を切り替え const observer = new IntersectionObserver((entries) => { setScrollBarSizeAndPosition(); for (const e of entries) { setDisplayFloatScrollBar(!e.isIntersecting); } }); observer.observe(tableBodyTailObserveTargetRef.current); return () => { observer.disconnect(); }; }, [tableBodyTailObserveTargetRef.current]); return ( <ScrollSync horizontal={true} vertical={false}> <div className="App"> <ScrollSyncPane> <div className="table-container" ref={tableContainerRef}> <table ref={tableRef}> <thead> <tr> <th>名前</th> <th>メールアドレス</th> <th>住所</th> <th>登録日時</th> </tr> </thead> <tbody> {data.map((row) => ( <tr key={`${row.address}`}> <td>{row.name}</td> <td>{row.email}</td> <td>{row.address}</td> <td>{row.createdAt}</td> </tr> ))} {/* この最下部行を目印に独自スクロールバーの表示・非表示を制御 */} <tr className={"table-body-tail-observe-target"} ref={tableBodyTailObserveTargetRef} /> </tbody> </table> </div> </ScrollSyncPane> <ScrollSyncPane> {/* 基本的に画面下部に現れる独自スクロールバーを用意。 */} {/* これのスクロールと表のスクロールを連動させる */} <div className="search-result-table-scroll-bar-container" ref={scrollBarContainerRef} style={{ visibility: displayFloatScrollBar ? "visible" : "collapse", position: "fixed", bottom: 0, overflow: "auto", zIndex: 1 }} > <div className="search-result-table-scroll-bar" ref={scrollBarRef} style={{ height: "1px" }} /> </div> </ScrollSyncPane> </div> </ScrollSync> ); }