0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

日本語入力に対応したスプレッドシート風Reactデータグリッド「react-masume-grid」を作りました

0
Last updated at Posted at 2026-07-07

Pasted image 20260707085857.png

はじめに

業務系のWebアプリを作っていると、「Excelのように一覧をサクサク編集したい」という要望によく出会います。既存のデータグリッドライブラリはどれも高機能ですが、

  • 日本語IMEでの入力がうまく動かない(変換確定のEnterでセルが移動してしまう等)
  • 多機能すぎて導入・学習コストが高い

という点でしっくり来なかったため、スプレッドシート風のシンプルなデータグリッドライブラリ「MasumeGrid(マス目)」 を自作してnpmに公開しました。

・npm
https://www.npmjs.com/package/react-masume-grid

・github
https://github.com/t92345era/react-masume-grid

MasumeGridとは

軽量・汎用のReact用スプレッドシートコンポーネントです。依存はReactのみ(gzipで約5KB)で、日本語IMEに完全対応していること、基本的なセル型(文字列・数値・選択肢・日付・チェックボックス) に加えて任意のコンポーネントを描画できるテンプレート型を備えつつシンプルであることが特徴です。

主な機能は次のとおりです。

  • グリッド表示 — 行番号・ヘッダーの表示切り替え、列幅指定、ドラッグでの列幅リサイズ、行の仮想化描画(数万行でも軽快)
  • セル型 — 文字列 / 数値(全角・カンマ正規化)/ 選択肢(プルダウン、コード保存・ラベル表示)/ 日付(カレンダー入力、和式表記の貼り付け正規化)/ チェックボックス(クリック・Spaceでトグル)/ テンプレート(任意のコンポーネントを描画)
  • セル編集 — ダブルクリック / F2 / キー入力で編集開始。IMEオンの状態で「A」を打つと、セルが編集状態になり「あ」がそのまま入力されます
  • 範囲選択 — マウスドラッグ、Shift+クリック / 矢印キーで拡張、Ctrl(⌘)+クリックで複数範囲選択。行・列ヘッダークリックで行/列選択も可能
  • コピー&ペースト — Ctrl(⌘)+C / X / V。ExcelやGoogleスプレッドシートと相互運用できるTSV形式(改行・タブ・引用符を含むセルにも対応)。コピーした範囲はExcelと同じく動く点線枠(マーチングアンツ) で表示されます

デモ

インストール

npm install react-masume-grid

React 18以上(peerDependency)で動作します。ライセンスはMITです。

使い方

最小限の使い方はこれだけです。データは string[][] の2次元配列で渡す制御コンポーネント方式で、onChange で新しい配列を受け取ります。

import { useState } from 'react';
import { MasumeGrid } from 'react-masume-grid';
// ライブラリを import すると CSS も自動で読み込まれます。
// バンドラー設定によっては明示的に: import 'react-masume-grid/styles.css';

function App() {
  const [data, setData] = useState<string[][]>([
    ['りんご', '100', '果物'],
    ['にんじん', '80', '野菜'],
  ]);

  return (
    <MasumeGrid
      data={data}
      onChange={setData}
      columns={[
        { title: '品名', width: 160 },
        { title: '単価', width: 80 },
        { title: 'カテゴリ', width: 120, readOnly: true },
      ]}
      showRowNumbers
      style={{ height: 400 }}
    />
  );
}

columns を省略すると列数は data から自動で導出され、ヘッダーはスプレッドシートらしく A, B, C… 表記になります。

セル型

ColumnDef.type で列ごとのセル型を指定できます。ポイントは、内部データはすべて文字列のままという設計です。型は編集UI・入力の正規化・表示だけを制御するので、クリップボード経由のExcel連携がシンプルに保てます。

const columns: ColumnDef[] = [
  { title: '商品名' },                                        // text(既定)
  { title: '単価', type: 'number' },
  { title: 'カテゴリ', type: 'select', options: [
    { value: 'C01', label: '果物' },                          // コードを保存、ラベルを表示
    { value: 'C02', label: '青果' },
  ]},
  { title: '状態', type: 'select', options: ['在庫あり', '取り寄せ'], filterable: false },
  { title: '入荷日', type: 'date' },
  { title: '検品済', type: 'checkbox' },
  { title: '操作', type: 'template', readOnly: true,
    template: ({ row }) => <button onClick={() => openDetail(row)}>詳細</button> },
];
編集UI 動作
text テキスト(IME対応) 既定。自由入力
number テキスト(IME対応) 右寄せ表示。確定時に全角数字→半角、カンマ除去。数値でない入力は拒否(元の値を保持)
select 絞り込み付きプルダウン ↑↓で候補移動、Enter/クリックで確定、文字入力で絞り込み。{value, label} 形式ならコード保存・ラベル表示。strict: false で選択肢外の自由入力も許可
date ネイティブの日付ピッカー YYYY-MM-DD で保存。2026/7/62026年7月6日20260706・全角数字の貼り付けも正規化
checkbox トグル(テキスト編集なし) チェック時 'true'・未チェック時 '' を保存。クリックまたはSpaceでトグル
template なし(カスタム描画) 列定義の template 関数が任意のコンポーネントを描画。ボタン等はそのままクリック可能

正規化・検証は編集確定と貼り付けの両方に適用され、無効な値のセルは変更されずスキップされます。

チェックボックス型(v0.2.0で追加)

Excelの「レ点」列をそのまま再現したセル型です。

  • チェックボックスのクリック、またはセル選択中のSpaceキーでトグル。範囲選択して Space を押すと、選択中のチェックボックスセルをまとめてトグルできます(一括チェックに便利です)
  • データ上はチェック時 'true'・未チェック時 '' の文字列として保存されるので、「データはすべて文字列」という設計はそのままです
  • Excelからの貼り付けにも対応しており、TRUE/FALSE1/0yes/no などの表記を自動で正規化します(解釈できない値は拒否され、セルは元の値を保持)
  • 正規化ロジックは normalizeCheckboxInput / isCheckboxChecked としてエクスポートしているので、アプリ側の集計処理などにも再利用できます

あわせてv0.2.0では、select型に filterable オプションを追加しました。既定(true)では文字入力で候補が絞り込まれますが、filterable: false にするとネイティブの <select> に近い挙動になります(常に全候補を表示し、文字入力は先頭一致の候補へハイライトを移動)。候補数が少ないステータス列などに向いています。

テンプレート型セル(v0.3.0で追加)

type: 'template' を指定すると、セルの描画を任意のReactコンポーネントに委ねられます。行アクションのボタン、他セルから計算した派生値の表示、バッジやプログレスバーなど、組み込みのセル型では表現できないUIを自由に置けます。

列定義の template 関数は描画対象のセルごとに呼ばれ(行は仮想化されるため画面内のセルのみ)、引数として次のコンテキストを受け取ります。

フィールド 意味
row data 配列上の行インデックス(データソース上のインデックス)
col 列インデックス
value セルに保存されている文字列値

row にはスクロール位置に関係なく常にデータソース上のインデックスが渡されるので、そのまま data[row] で同じ行の他のセルを参照できます。

const [data, setData] = useState<string[][]>(initialData); // [商品名, 単価, 数量]

const columns = useMemo<ColumnDef[]>(
  () => [
    { title: '商品名' },
    { title: '単価', type: 'number' },
    { title: '数量', type: 'number' },

    // 派生表示: row でデータ行を参照し 単価×数量 を表示。
    // columns が data に依存するので、data の変更時に再計算する
    {
      title: '金額', width: 100, type: 'template', readOnly: true,
      template: ({ row }) => {
        const total = Number(data[row]?.[1] || 0) * Number(data[row]?.[2] || 0);
        return <span style={{ marginLeft: 'auto', padding: '0 6px' }}>¥{total.toLocaleString()}</span>;
      },
    },

    // 行アクション: テンプレート内のボタンはネイティブにクリックできる
    {
      title: '操作', width: 90, type: 'template', readOnly: true,
      template: ({ row }) => (
        <button type="button" onClick={() => openDetail(row)}>詳細</button>
      ),
    },
  ],
  [data],
);

いくつか補足です。

  • テンプレート型セルにテキストエディタはありません。文字キー・F2・ダブルクリックで編集は始まりませんが、キーボード移動・範囲選択・コピーは通常どおり動作します
  • グリッドは通常、フォーカスを内部の不可視textarea(後述)に固定しますが、テンプレートセル内のインタラクティブ要素(buttonainput など)は例外扱いになっており、フォーカスを奪われずネイティブに操作できます
  • コピーされるのは描画結果ではなく保存値data[row][col])です。貼り付けやDeleteも保存値に作用するため、上の例のような表示専用列には readOnly: true の指定をおすすめします

コピー範囲のマーチングアンツ表示(v0.3.0で追加)

Ctrl(⌘)+C / X でコピー・切り取りすると、対象範囲がExcelやGoogleスプレッドシートでおなじみの動く点線枠(マーチングアンツ) で囲まれます。「いまどこをコピーしたか」がひと目でわかるので、貼り付け先を選ぶ操作が迷いません。

点線枠は次のタイミングで消えます(Excelの挙動に準拠)。

  • Escape を押したとき
  • 貼り付けしたとき
  • セルの編集を開始したとき

実装面の小ネタですが、CSSの破線ボーダーは破線のオフセットをアニメーションできないため、4辺をストライプ状の linear-gradient 背景で描いて background-position を動かすことで「点線が流れる」表現をしています。prefers-reduced-motion 設定時はアニメーションを止めるようにしています。

日本語IME対応のしくみ

このライブラリを作った一番の動機がここです。多くのグリッドライブラリでは、IMEの変換を確定するEnterがセル移動として解釈されてしまったり、変換候補ウィンドウが変な位置に出たりします。

MasumeGridでは、常にフォーカスされた不可視の <textarea> をアクティブセルの上に重ねる方式(Googleスプレッドシートと同じ手法)を採用しています。compositionstart で編集を開始するため、

  • IMEオンのままタイプすると、候補ウィンドウがセルの位置に表示される
  • 変換確定のEnterがセル移動と誤解釈されない(イベント順序が異なるSafariにも対応)

という、日本語ユーザーにとって自然な編集体験になっています。

主なProps

Prop 説明
data string[][] グリッドの内容(必須)。行の長さは不揃いでも可
columns ColumnDef[] 列定義。省略時はdataから列数を導出
onChange (next: string[][]) => void 編集・貼り付け・削除のたびに新しい2次元配列で呼ばれる
onCellChange (row, col, value) => void 変更セルごとに呼ばれる
onSelectionChange (ranges) => void 選択範囲の変更時
onColumnResize (col, width) => void 列幅ドラッグの確定時
showRowNumbers / showHeader boolean 行番号・ヘッダーの表示(既定: true
readOnly boolean 編集禁止(選択・コピーは可能)
rowHeight / headerHeight / defaultColumnWidth number サイズ調整

ColumnDef{ title?, width?, readOnly?, resizable?, type?, options?, strict?, filterable?, template? } を指定できます。

データは完全な制御コンポーネント方式で、onChange を実装しない限りグリッドは変化しません。Reactの流儀に沿った素直なAPIにしています。

スタイルのカスタマイズ

CSS変数を上書きするだけでテーマを変更できます。

.my-grid {
  --masume-grid-accent: #0f9d58;
  --masume-grid-sel-bg: rgba(15, 157, 88, 0.12);
  --masume-grid-header-bg: #f0f4f1;
}

アクセントカラー(--masume-grid-accent)は選択枠だけでなく、マーチングアンツの点線色にも使われます。

現時点の制限

シンプルさを優先しているため、次の機能は現バージョンでは対応していません。

  • 内部データは文字列のみ(桁区切り等の表示フォーマットは今後の課題。テンプレート型で代用は可能です)
  • 列の仮想化は未対応(数百列を超える場合は注意)
  • Undo / Redoなし(onChange ベースの設計なので、履歴管理はホストアプリ側で実装できます)
  • セル結合・数式・列幅の自動調整なし

更新履歴

  • v0.2.0 — チェックボックス型セルを追加。select型に filterable オプションを追加。normalizeCheckboxInput / isCheckboxChecked をエクスポート
  • v0.3.0 — テンプレート型セル(任意コンポーネントの描画)を追加。コピー範囲のマーチングアンツ表示を追加

おわりに

「日本語入力が自然に使えて、基本的なセル型があって、それでいてシンプル」なデータグリッドが欲しくて作ったライブラリです。テンプレート型の追加で、行アクションボタン付きの一覧編集画面など、業務アプリの定番UIがこれ1つで組めるようになりました。

使ってみた感想や不具合報告など、GitHubのIssueでフィードバックをいただけると嬉しいです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?