【React】table 上の左複数列を固定する方法

  • 2021年10月20日
  • 2021年10月20日
  • React

position – CSS: カスケーディングスタイルシート | MDN#sticky
 端の一列は簡単です。td 要素にposition: stickyをつけ、固定したい方向にleft: 0の様にするのみでよしなにしてくれます。次のデモがこれです。

/* 1列目を左端に固定 */
td:first-child {
  position: sticky;// 固定
  left: 0; // 左端
}

 上下の複数行はちょっと工夫が要りますが、シンプルではあります。上部はtheadで固定したい範囲を囲ってtheadposition: stickyをかけます。下部はtfootとして同様です。次のデモがこれです。

thead {
  position: sticky;
  top: 0;
}
tfoot {
  position: sticky;
  bottom: 0;
}

 左右の複数列は上下複数行に比べてずいぶん手間です。画面を描画した時、画面が変わった時、それぞれで都度左から何pxに置くべきかを計算して、それを要素に与える必要があります。あんまりにもあんまりなのでその内 colgroup に position:stickey をかけたらそれだけで複数列固定になるやもしれません。

– HTML: HyperText Markup Language | MDN
 これを実装したデモとソースコードが次です。

ソースコードを展開
// App.tsx
import "./styles.css";
import { AppTableCell } from "./AppTableCell";

export default function App() {
  // 表のデータを作成
  const arr = Array(50)
    .fill(null)
    .map((v, i) => {
      return Array(50)
        .fill(null)
        .map((v, j) => `${i},${j}`);
    });

  return (
    <div className="App">
      <table>
        <caption>左複数列を固定</caption>
        <tbody>
          {arr.map((vp, i) => (
            <tr key={i}>
              {/* sticky にしたい列だけ特別なコンポーネントを呼出し */}
              {vp.map((vc, j) =>
                j === 0 || j === 1 ? (
                  <AppTableCell key={`${i},${j}`} stickyIndex={j}>
                    {vc}
                  </AppTableCell>
                ) : (
                  <td key={`${i},${j}`}>{vc}</td>
                )
              )}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

// AppTableCell.tsx
import React, { CSSProperties, useEffect, useRef } from "react";
import { atom, useAtom } from "jotai";

type AppTableCellProps = {
  stickyIndex?: number; // sticky として左からいくつ目か(いくつ目と書いたが 0 スタートのインデックス的採番)
};

// 表中の各列の幅を管理する atom を用意。Cellコンポーネントの外でグローバルに管理できればなんでも良し
const tableCellWidths = atom<number[]>([]);

export const AppTableCell: React.FC<AppTableCellProps> = (props) => {
  const { stickyIndex } = props;
  const [tcw, setTcw] = useAtom(tableCellWidths);
  // マスを直に触るので ref を用意
  // @see https://ja.reactjs.org/docs/refs-and-the-dom.html
  const cellRef = useRef<HTMLTableCellElement>(null);

  // style.left
  const [left, setLeft] = React.useState("");
  // マスが変わった時に管理されている幅を更新
  useEffect(() => {
    if (typeof stickyIndex !== "number" || !cellRef.current) {
      return;
    }
    // 更新ロジック
    const updateAction = () => {
      const tcwUpdater = (old: number[]) => {
        if (typeof stickyIndex !== "number" || !cellRef.current) {
          return old;
        }
        // 以前の幅と現在の幅が違うのであれば更新
        if (
          old[stickyIndex] !== cellRef.current.getBoundingClientRect().width
        ) {
          old[stickyIndex] = cellRef.current.getBoundingClientRect().width;
        }
        return old;
      };
      setTcw(tcwUpdater);
      // 左から何 px 目で固定されるべきなのかをグローバルに管理している各列の幅から計算する
      setLeft(
        `${tcwUpdater(tcw)
          .filter((v, i) => i < stickyIndex)
          .reduce((carry, cur) => carry + cur, 0)}px`
      );
    };
    updateAction();
    const observer = new ResizeObserver(updateAction);
    observer.observe(cellRef.current);
    return () => {
      observer.disconnect();
    };
  }, [cellRef, tcw, setTcw, stickyIndex]);

  const style: CSSProperties = {};
  if (typeof stickyIndex === "number") {
    // 左から何 px 目で固定されるべきなのかをグローバルに管理している各列の幅から計算する
    // 計算内容は自分の左のマスまでで使われている幅の合計の算出
    style.left = left;
    style.background = "rgb(200, 200, 200)";
  }

  // 計算結果を含んだ style を受け渡し
  return (
    <td ref={cellRef} style={style}>
      {props.children}
    </td>
  );
};

 余談ですがこのコードは left の値を決定するコードにすぎず、固定対象の列の幅が固定されているのであれば、デモの様に都度計算する必要はなく、単にtd:first-child {left:0px;},td:nth-child(2) {left:1列目の幅px;},td:nth-child(3) {left:(1列目の幅+2列目の幅)px;},…と固定幅に合わせてべた書きするだけで同じものを作れます。

>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG