@lembryo/voxsheet
仮想スクロールで数百万行を滑らかに描画する React 製スプレッドシートコンポーネント。Excel 風の選択・編集・オートフィル・コピペ・列リサイズ、ホスト制御のソート/フィルタ/検索に対応します。
▶ ライブデモを開く ブラウザ内データで実際に動かせます
概要
VoxSheet は DOM ベースの仮想スクロールにより、行数が膨大でも表示中のセルだけを描画します。データ取得とドメイン状態(ソート・フィルタ・検索)はホストが所有し、ビューポート・選択・編集バッファ・キーボードはグリッドが所有します。
- Transport 非依存 — 取得手段は
fetchRowsに委ねるため、REST / GraphQL / ローカル配列どれでも可。 - TypeScript ファースト — 公開 API はすべて型付き。
- ピア依存モデル —
react/react-domは peer dependency。 - 外部 CSS フレームワーク不使用 —
--vox-*変数とvox-クラスで完結。
なぜ voxsheet?
voxsheet の特徴は、「無償(MIT)で Excel 風の編集体験」と「サーバ駆動を前提にした設計」を最初から両立している点です。
- 無償で Excel 風編集 — オートフィル(フィルハンドル)・複数レンジ選択・コピー&ペースト・列リサイズを標準装備。
- サーバ駆動が前提 — ソート・フィルタ・検索・ページングを
fetchRows経由でバックエンドに委譲。巨大データでも可視範囲だけを取得します。 - React ネイティブ / DOM ベース — hooks・controlled props・JSX セル。Canvas 実装と異なり、通常の DOM で拡張・検査・アクセシビリティを扱えます。
- TypeScript ファースト・外部 CSS 不使用・peer 依存 — 既存のビルドやテーマに溶け込みやすい。
競合との違い
voxsheet が標準提供する機能には、他の主要グリッドでは有償プランや別ライセンスが必要なものがあります(下表は代表例。各製品のライセンスは変わり得るため、採用前に最新の条件を確認してください)。
| 機能 | voxsheet | 他の代表例 |
|---|---|---|
| オートフィル(フィルハンドル) | 標準・無償 | AG Grid は Enterprise(有償)、MUI X は Premium(有償) |
| 複数レンジ選択 | 標準・無償 | AG Grid は Enterprise(有償) |
| サーバ駆動データ(取得をホストに委譲) | 標準 | AG Grid は Enterprise(Server-Side Row Model) |
| ライセンス | MIT | Handsontable は商用利用に有償ライセンスが必要 |
インストール
npm install @lembryo/voxsheet
react / react-dom(v18 以上)は peer dependency です。スタイルシートのインポートも忘れずに行ってください。
import { VoxSheet } from "@lembryo/voxsheet"
import "@lembryo/voxsheet/styles.css"
クイックスタート
最小構成の例です。columns・totalRows・fetchRows の 3 つが必須です。
import { useCallback, useState } from "react"
import { VoxSheet } from "@lembryo/voxsheet"
import type { Column, FetchResult, Query, SortSpec } from "@lembryo/voxsheet"
import "@lembryo/voxsheet/styles.css"
const columns: Column[] = [
{ name: "id", type: "number" },
{ name: "name", type: "string" },
{
name: "salary",
type: "number",
format: { kind: "number", options: { style: "currency", currency: "USD" } },
},
{ name: "joinedAt", type: "date" },
]
export function App() {
const [sort, setSort] = useState<SortSpec[]>([])
const [total, setTotal] = useState(0)
// グリッドは Query(offset/limit + controlled な sort/filters/search)と
// 陳腐化リクエストを中断するための AbortSignal を渡してくる。
const fetchRows = useCallback(
async (query: Query, signal: AbortSignal): Promise<FetchResult> => {
const res = await fetch("/api/rows", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(query),
signal,
})
const json: FetchResult = await res.json()
if (typeof json.total === "number") setTotal(json.total)
return json
},
[],
)
return (
<VoxSheet
columns={columns}
totalRows={total}
fetchRows={fetchRows}
sort={sort}
onSortChange={setSort}
/>
)
}
onSortChange を渡したときのみ現れます。
コアコンセプト
VoxSheet は controlled かつ transport 非依存です。責務は次のように分かれます。
| 所有者 | 担当 |
|---|---|
| ホスト | データソース(fetchRows)、ドメイン状態(sort /
filters / search)、永続化(編集コミット)、フィルタ条件 UI
|
| グリッド | ビューポート(仮想スクロール)、セル選択、編集バッファ、Undo/Redo、キーボード、クリップボード |
ソートやフィルタの「状態」はホストが useState 等で持ち、グリッドはユーザー操作をコールバックで通知するだけです。状態が変わると
fetchRows が新しい Query で再度呼ばれます。
データ契約
グリッドとホストの間で受け渡す型は次のとおりです。これらが voxsheet を使う上での中心になります。
CellValue
セルの値。JSON シリアライズ可能な型に限定されます。
type CellValue = string | number | boolean | null
- 日付は ISO 8601 文字列(推奨)または epoch ミリ秒で運び、列
type: "date"で解釈します。 nullは SQL の NULL を表し、空文字""と区別されます。
Query
グリッドが fetchRows に渡す取得条件。offset / limit はグリッドが決め、sort
/ filters / search はホストが渡した controlled 値がそのまま入ります。
type Query = {
offset: number
limit: number
sort: SortSpec[] // 多列。配列順 = 優先順位
filters: FilterSpec[] // AND 結合
search?: string
}
FetchResult
fetchRows が返す値。data[i][j] は i 行目・j 列目で、列順は
columns に揃えます。
| プロパティ | 型 | 説明 |
|---|---|---|
| data必須 | CellValue[][] | data[i][j] = i 行 j 列。列順は columns に整列。 |
| ids必須 | number[] | 各行の安定 ID。編集コミット時に行→ID 解決に使う。 |
| ordinals必須 | (number | null)[] | 行番号ガターに表示する連番。null
を明示するとその行のガターを空欄で描画
(固定ヘッダ行など。セル自体は維持)。undefined は offset + i + 1
にフォールバック。
|
| total任意 | number | フィルタ/検索適用後の総件数。スクロールバーと同期する(毎回返すのが望ましい)。 |
fetchRows
行データを範囲指定で取得する関数。第 2 引数の signal で陳腐化した取得を中断できます。
type FetchRowsFn = (query: Query, signal: AbortSignal) => Promise<FetchResult>
fetchRows は useCallback 等で安定した参照にしてください。毎レンダーで新しい関数を渡すと不要な再取得が発生します。
列定義
Column
列メタデータ。type が描画・編集・比較・オートフィルを駆動します。表示/非表示は配列から除外して表現します(controlled)。
| プロパティ | 型 | 説明 |
|---|---|---|
| name必須 | string | 識別子兼表示名。sort / filter の column 参照に使う。
|
| type任意 | "string" | "number" | "date" | "boolean" | "select" | 既定 "string"。整形・整列・パース・比較・エディタ種別の基準。 |
| align任意 | "left" | "right" | "center" | 既定は type から導出(number/date=right、boolean=center)。 |
| width任意 | number | 既定 defaultColumnWidth。ユーザーリサイズは列名キーで記憶。 |
| format任意 | ColumnFormat | 表示整形。下記参照。 |
| editable任意 | boolean | ((ctx) => boolean) | 既定は親 readOnly に従う。 |
| validate任意 | (value, ctx) => boolean | string | false または文字列(エラーメッセージ)で却下。 |
| sortModes任意 | { id: string; label: string }[] | 並べ替え方式の選択肢(例: テキスト/数値)。▾ ピッカーを表示し、選択 id を SortSpec.mode
に載せる。
|
| defaultSortMode任意 | string | 初期/既定の方式 id(未指定なら sortModes[0])。 |
| options任意 | { value: CellValue; label?: string }[] | type: "select" のときの選択肢。下記参照。 |
select(ドロップダウン)列
type: "select" と options を宣言すると、セルは一致する option の
label(無ければ実値)を表示し、編集はドロップダウンになります(選択で即コミット)。
ペースト/オートフィルの文字列は String(value) 一致で option の実値に解決され、一致しない場合は
raw 文字列のまま保持して validate に委ねます。ワイヤ上の値は通常の
CellValue のままなので、バックエンドのソート/フィルタ/保存は変わりません。
const columns: Column[] = [
{
name: "department",
type: "select",
options: [
{ value: "eng", label: "Engineering" },
{ value: "sales", label: "Sales" },
],
},
]
表示整形 format
Intl のオプション、または任意の関数で値を文字列化します。解決順は 関数 > kind
別既定 です。
type ColumnFormat =
| { kind: "number"; options: Intl.NumberFormatOptions }
| { kind: "date"; options: Intl.DateTimeFormatOptions }
| ((value: CellValue, ctx: { column: Column; row: number }) => string)
// 通貨表示
{ name: "salary", type: "number",
format: { kind: "number", options: { style: "currency", currency: "JPY" } } }
// 関数で完全カスタム
{ name: "status", format: (v) => (v === 1 ? "有効" : "無効") }
編集とバリデーション
editable で列・行単位の編集可否を、validate で入力検証を行います。
{
name: "email",
type: "string",
editable: ({ row }) => row > 0, // 行ごとに可否
validate: (value) =>
typeof value === "string" && value.includes("@")
? true
: "メールアドレスの形式が不正です", // 文字列を返すと却下+メッセージ
}
Props リファレンス
VoxSheetProps をカテゴリ別に示します。
データ
| プロパティ | 型 | 説明 |
|---|---|---|
| columns必須 | Column[] | 列定義(controlled)。 |
| totalRows必須 | number | 総行数。FetchResult.total で同期する。未確定なら 0
でよい(先頭チャンクを取得して総数を確定します)。
|
| fetchRows必須 | FetchRowsFn | 行取得関数 (query, signal) => Promise<FetchResult>。 |
| queryKey任意 | unknown | ホスト独自クエリ入力の無効化キー。変化でキャッシュを破棄して再取得。 |
レイアウト・表示
| プロパティ | 型 | 説明 |
|---|---|---|
| rowHeight任意 | number | 行の高さ(px)。既定 28。density より優先。 |
| density任意 | "compact" | "normal" | "comfortable" | フォントと行高をセットで切替。既定 "normal"。 |
| defaultColumnWidth任意 | number | 列幅の既定(px)。既定 120。 |
| rowHeaderWidth任意 | number | 行番号ガターの幅(px)。未指定なら自動フィット(下記)。1.2.0 で対応。 |
| autoRowHeaderWidth任意 | boolean | ガター幅を totalRows の桁数から自動フィット(52〜120px)。既定
true。false で従来互換の固定 52px。1.2.0 で対応。
|
| frozenRows任意 | number | 先頭から固定する行数。既定 0。先頭 N 行を本体スクロール外の固定バンドに描画します(横スクロールのみ同期)。1.1.0 で対応。 |
| frozenColumns任意 | number | 先頭から固定する列数。既定 0。先頭 N
列を横スクロール時も左に固定(sticky)。frozenRows と合成可。1.1.0 で対応。
|
| theme任意 | "light" | "dark" | "system" | data-vox-theme を設定。既定 "system"。 |
| className任意 | string | ルート要素のクラス。 |
| style任意 | CSSProperties | ルート要素のインラインスタイル。 |
振る舞い(controlled ドメイン状態)
| プロパティ | 型 | 説明 |
|---|---|---|
| readOnly任意 | boolean | 編集 UI を無効化(コピー・選択・ナビ、ヘッダの並べ替え/絞り込みは可)。 |
| sort任意 | SortSpec[] | 多列ソート(controlled)。 |
| filters任意 | FilterSpec[] | フィルタ(controlled・条件 UI はホスト所有)。 |
| search任意 | string | 検索キーワード。 |
| searchHighlights任意 | CellAddress[] | ハイライト対象セル。 |
| currentSearchHit任意 | CellAddress | null | 現在ヒット。変化でスクロールする。 |
状態 UI・拡張
| プロパティ | 型 | 説明 |
|---|---|---|
| renderLoading任意 | () => ReactNode | ローディング表示の差し替え。 |
| renderEmpty任意 | () => ReactNode | 空表示の差し替え。 |
| labels任意 | Partial<VoxLabels> | 内蔵 UI 文言の上書き(i18n)。 |
| icons任意 | Icons | アイコンの差し替え。 |
| platform任意 | PlatformAdapter | クリップボード/通知/確認/保存の実装注入。 |
イベント(コールバック)
ボタンや操作系の affordance は、対応するコールバックが未指定だと非表示・無効になります。
| プロパティ | 型 | 説明 |
|---|---|---|
| onSortChange | (sort: SortSpec[]) => void | none→asc→desc→none をトグルし通知。通常クリックはその列だけに置換、
Shift+クリックで多列ソートに追加。Column.sortModes 指定時は選択方式を
SortSpec.mode に載せる。未指定でソートボタン非表示。
|
| onFilterButtonClick | (col: number, anchor: DOMRect) => void | フィルタ操作要求(ホストがポップオーバー表示)。未指定でフィルタボタン非表示。 |
| onColumnResize | (col: number, width: number) => void | 列幅変更(ドラッグ/ダブルクリック自動フィット)。 |
| onColumnRename | (col: number, newName: string) => void | ヘッダ名編集。未指定でリネーム無効。 |
| onColumnReorder | (from: number, to: number) => void | ヘッダのドラッグで列を並べ替え(挿入インジケータ表示)。未指定でドラッグ無効。列順はホストが
columns で反映(controlled)。
|
| onAddColumn | (atCol: number) => void | 列追加要求。未指定で追加ボタン非表示。 |
| onCellChange | (edit: CellEdit) => void | ローカル編集の都度。 |
| onDirtyChange | (hasChanges: boolean) => void | 未コミット変更の有無。 |
| onAppendRow | (atRow: number, count?: number) => void | Promise<void> | 行追加の要求。最下行 Enter は count = 1、貼り付けが最終行を超えたときは
不足行数。Promise を返すと、行数が増えるのを待ってから残りを貼り付けます。
|
| onInsertRow | (atRow: number, pos: "above" | "below") => void | 行挿入要求。 |
| onDeleteRows | (rows: number[]) => void | 行削除要求。 |
| onAutoFill | (p: { sourceRange; direction; toEnd }) => void | フィルハンドルのドラッグ(上下左右・グリッドがパターン反復を適用して通知)/
ダブルクリック(toEnd: true・値はホストが確定)。
|
| onSelectionChange | (selection: Selection[]) => void | 選択変更。 |
| onSelectionStats | (stats: SelectionStats | null) => void | 選択集計(合計/平均/件数)。未読込セルを含むと partial: true。 |
| onCellKeyDown | (e, ctx: CellContext) => void | 既定キー処理前にフック(preventDefault で抑止)。 |
| onError | (err, ctx: { phase: "fetch" | "commit" | "clipboard" }) => void | 取得/コミット/クリップボード操作の失敗。取得失敗時は再試行ボタン付きの エラーオーバーレイも内蔵表示。すでに行が表示されている場合は、代わりに小さな再試行バーを出し 失敗したチャンクだけ取り直します。 |
ソート
ソートは controlled です。グリッドはヘッダのソートボタンで none → asc → desc → none
をトグルし onSortChange で通知、ホストが sort を更新します。
通常クリックはその列だけのソートに置換し、Shift+クリックで既存ソートに
追加します(多列ソート)。配列の順序が多列ソートの優先順位です。
const [sort, setSort] = useState<SortSpec[]>([])
<VoxSheet columns={columns} totalRows={total} fetchRows={fetchRows}
sort={sort} onSortChange={setSort} />
並べ替え方式(sortModes)
同じ列でも「テキストとして」か「数値として」かで並べ替え結果が変わる場合があります。Column.sortModes
を指定するとヘッダに小さな ▾ ピッカーが出て、選んだ方式 id が SortSpec.mode
に載ります。実際の比較はバックエンドが解釈します(グリッドは id を運ぶだけでトランスポート非依存)。sortModes
未指定の列は従来どおりで mode を送らないため、既定はバックエンドの素の(テキスト)並べ替えになります。
const columns: Column[] = [
{ name: "code", sortModes: [
{ id: "text", label: "テキスト" },
{ id: "numeric", label: "数値" },
], defaultSortMode: "text" },
]
// fetchRows には例えば { column: "code", direction: "asc", mode: "numeric" } が渡る
// → サーバ側で mode を ORDER BY に変換する。
フィルタ
フィルタ条件 UI はホストが所有します。グリッドは onFilterButtonClick(col,
anchorRect) で「この列のフィルタを編集したい」と通知するので、ホストはその
anchorRect 付近にポップオーバーを出し、確定後 filters を更新します。フィルタは
AND 結合です。
type FilterOperator =
| "=" | "!=" | ">" | ">=" | "<" | "<="
| "contains" | "startsWith" | "endsWith"
| "isNull" | "notNull"
type FilterSpec = { column: string; operator: FilterOperator; value?: CellValue }
ある列に2 件以上のフィルタが適用されているとき、ヘッダのフィルタボタンに件数バッジが表示され、複数条件を一目で把握できます。
検索
search にキーワードを渡すと fetchRows の Query.search
に反映されます。ヒット箇所のハイライトとスクロールはホストが searchHighlights / currentSearchHit
で制御します。
<VoxSheet ...
search={keyword}
searchHighlights={hits}
currentSearchHit={hits[cursor] ?? null}
/>
編集とコミット
編集はまずローカル層に溜まります(ダーティ表示、onCellChange / onDirtyChange
通知)。ホストは任意のタイミングで getLocalEdits() を読み、各 row を fetchRows
で受け取った ids で安定 ID に解決し、永続化後に clearLocalEdits() を呼びます。
const ref = useRef<VoxSheetHandle>(null)
async function save() {
const edits = ref.current!.getLocalEdits() // CellEdit[]
// row → 安定 ID へ解決して PATCH 等で永続化
await commitToServer(edits)
ref.current!.clearLocalEdits() // 成功後にクリア
}
<VoxSheet ref={ref} ... onDirtyChange={setDirty} />
type CellEdit = { row: number; col: number; oldValue: CellValue; newValue: CellValue }
エディタは列の type に応じて切り替わります: select / boolean
はドロップダウン(選択で即コミット。boolean は Space でトグルも可)、date
はネイティブ日付入力、number は数値入力モード、その他はテキスト入力。IME
の直接入力にも対応します(セル上で変換を始めると確定文字列で編集開始)。直接入力・IME
で開いたときはキャレットが入力済み文字の後ろに置かれます(全選択して開くのは F2
とダブルクリックのときだけ)。
Column.validate に却下された値は、入力内容を残したままエディタが開いたままになります。
Enter/Tab/他セルのクリックでもセルから出ないので、その場で直せます(Esc で取り消し)。
未取得(キャッシュ外)の行への編集は元の値が分かりません。Undo は null で塗り潰さずに
編集自体を取り消し(サーバの値が再表示されます)、getLocalEdits() の
oldValue はそれらのセルで null になります。
sort / filters / search /
columns / queryKey が変わると同じ index が別レコードを指します。誤コミットを防ぐため、
これらの変化時にグリッドは編集と Undo/Redo を破棄し、開いていたエディタも閉じたうえで
onDirtyChange(false) を通知します。未保存編集を守りたい場合は、クエリ変更の UI 側で確認してから反映してください。
同様に row → id の対応(ids)もクエリ毎に無効になるため、ホスト側の
id マップもクエリ変更時に破棄してください。
選択と集計
選択は Excel 風の矩形レンジの配列です。onSelectionChange で選択範囲、onSelectionStats
で件数・数値件数・合計・平均が得られます(数値が無い場合 sum / average は
null)。集計は読込済み(キャッシュ内)のセルのみで計算され、
未取得セルを含む場合や上限(10 万セル)超過時は partial: true が立ちます。
type CellAddress = { row: number; col: number }
type Selection = { start: CellAddress; end: CellAddress }
type SelectionStats = {
count: number
numericCount: number
sum: number | null
average: number | null
partial?: boolean // 未読込セルを含む「読込済みのみ」の集計であることを示す
}
コピー・ペースト・エクスポート
- コピーは整形前の raw 値を TSV で書き出します(再ペーストで型が崩れない)。
- 未取得行を含む選択はバルク取得してからコピーします。10 万セル超は
platform.confirmで確認、100 万セルで抑止。 - カットはクリップボードへ書き込めたときだけセルを消します (拒否・失敗時に消えてデータが失われることはありません)。
- ペーストはアクティブレンジ左上をアンカーにし、選択矩形が貼付データの整数倍なら タイル状に繰り返します(Excel 同様)。貼付後は貼付範囲を選択します。
- 貼り付けが最終行を超える場合は
onAppendRow(atRow, count)で行追加を要求し、総行数(totalRows/FetchResult.total)が増えるのを 待って(最大 5 秒)から残りを貼ります。行を作るのはデータを持つホスト側の役目です。 それでも入らなかったセルは貼らずに通知します(ラベルpasteClipped)。 ハンドラでは行を作り、queryKeyを変えずに行数を更新し(変えると貼り付けた編集が 破棄されます)、invalidate()でキャッシュを取り直してください。 - コンテキストメニューから選択範囲を CSV でエクスポートできます
(
platform.saveFile。readOnlyでもコピーと共に使用可)。 クリップボードを読めない環境ではメニューの「貼り付け」は無効になります (Ctrl+V は動作します)。
オートフィル
選択範囲右下のフィルハンドルを上下左右にドラッグすると、グリッドが選択パターンを
反復して埋め、onAutoFill で通知します。ハンドルをダブルクリックすると
toEnd: true で通知だけが行われ、末尾までの範囲と値の確定は(サーバ/ドメイン依存のため)
ホストが担います。
onAutoFill={(p) => {
// p.sourceRange: 元の選択, p.direction: "down"|"up"|"left"|"right"
// p.toEnd: true = ダブルクリック(列末尾までのフィルをホストが実施)
}}
キーボード操作
| キー | 動作 |
|---|---|
| ↑ ↓ ← → / Tab / Enter | セル移動 |
| Home End / PageUp PageDown | 行頭・行末 / ページ単位移動 |
| Ctrl+Home / Ctrl+End | 表の先頭・末尾へ |
| F2 / 直接入力 | 編集開始 / 上書き入力(IME 直接入力も可) |
| Delete / Backspace | 選択範囲全体をクリア / アクティブセルを空にして編集開始 |
| Space | boolean セルをトグル |
| Ctrl+A/C/X/V | 全選択 / コピー / カット / 貼り付け(ネイティブ paste イベント経由・Firefox 対応) |
| Ctrl+Z / Ctrl+Y(Ctrl+Shift+Z) | Undo / Redo |
| Shift+矢印 / Shift+クリック | 選択範囲を拡張 |
| Ctrl+クリック | 複数レンジ選択 |
移動は縦・横ともスクロール追従し、ドラッグ選択/フィルはビューポート端で自動スクロールします。IME
は compositionend で確定されます。readOnly
時はコピー・全選択・ナビゲーション(Home/End/Page 系含む)のみ有効です(ヘッダの並べ替え/絞り込みは
読み取り操作なので使えます)。
行の固定
frozenRows を指定すると、先頭 N
行を本体スクロールの外側に置いた固定バンドに描画します。バンドはその場に留まり、残りのグリッドだけが縦スクロールします。横スクロールのみバンドに同期するため、固定セルは対応する列と揃ったままになります。本体スクロール領域は
[frozenRows, total) の行を担当するので、固定行が二重に表示されることはありません。
固定行は通常の行と同じように選択・編集でき、それらを含む先頭側のチャンクはスクロール位置に依らず常に取得されます。既定値
0 で固定は無効です。列を固定する場合は列の固定を参照してください。
<VoxSheet columns={columns} totalRows={total} fetchRows={fetchRows} frozenRows={2} />
列の固定
frozenColumns を指定すると、先頭 N 列を左に固定します。残りのグリッドを横スクロールしても常に見えたままになり(sticky
で実装)、列幅や行高は本体とずれません。frozenRows と組み合わせると、左上の交差部分が縦横どちらのスクロールでも固定されます。
既定値 0 で固定は無効です。
<VoxSheet columns={columns} totalRows={total} fetchRows={fetchRows} frozenColumns={1} />
列の並べ替え
onColumnReorder を指定すると、列ヘッダをドラッグして並べ替えられます。ドラッグ中は挿入位置のインジケータが出て、ドロップで
onColumnReorder(from, to) が発火します。列順はホストが所有します(columns
配列を並べ替えて反映)。
データは位置ベース(data[i][j] は columns 対応)のため、並べ替え後は fetchRows
が各行のセルを現在の列順で返すようにしてください(バックエンドがその順で返す、またはクライアントで並べ替える)。
onColumnReorder={(from, to) => {
setColumns((cols) => {
const next = [...cols]
const [moved] = next.splice(from, 1)
next.splice(to, 0, moved)
return next
})
}}
命令的ハンドル(ref)
ref 経由で VoxSheetHandle を取得し、スクロール・選択・編集・Undo
などを命令的に操作できます。
| メソッド | 説明 |
|---|---|
| scrollToRow(row) | 指定行までスクロール。 |
| scrollToCell(row, col) | 指定セルまでスクロール。 |
| focusCell(row, col) | 指定セルにフォーカス。 |
| getSelection() | 現在の選択(Selection[])を取得。 |
| setSelection(sel) | 選択を設定。 |
| startEdit(row, col) | 指定セルの編集を開始。 |
| getLocalEdits() | 未コミット編集(CellEdit[])を取り出す。 |
| clearLocalEdits() | コミット後にローカル編集と Undo/Redo をクリア。 |
| undo() / redo() | 元に戻す / やり直す。 |
| invalidate() | キャッシュ破棄+再取得(明示リフレッシュ)。 |
スタイリング・テーマ
スタイルは vox- クラスと --vox-* CSS 変数で自己完結しています。@lembryo/voxsheet/styles.css
を読み込み、変数やクラスを上書きしてテーマ調整します。ダークモードは prefers-color-scheme
に追従し、theme prop で強制もできます。
.vox-sheet {
--vox-row-height: 32px;
--vox-color-accent: #06c755;
}
--vox-* 変数は :root ではなく .vox-sheet ルートに定義されています。:root
で上書きしても効きません。上書きは .vox-sheet(またはラッパークラス)にスコープしてください(上の例)。
densityで行高とフォントをまとめて切替(compact/normal/comfortable)。rowHeightはdensityより優先。themeはdata-vox-theme属性を設定(light/dark/system)。
国際化(labels)
内蔵 UI 文言は labels(Partial<VoxLabels>)で部分上書きできます。
<VoxSheet ...
labels={{
loading: "読み込み中…",
empty: "データがありません",
contextCopy: "コピー",
contextPaste: "貼り付け",
}}
/>
キー一覧は 型リファレンス の VoxLabels を参照してください。
アイコン
ソート/フィルタのアイコンを差し替えられます。指定しないキーは内蔵アイコンが使われます。
<VoxSheet ...
icons={{
filter: ({ size }) => <MyFilterIcon width={size} />,
}}
/>
差し替え可能なキー: sortAscending / sortDescending /
sortUnsorted / filter / filterActive。
プラットフォームアダプタ
クリップボード・トースト通知・確認ダイアログ・ファイル保存の実装を注入できます。未指定時はブラウザ標準の挙動にフォールバックします。
confirm は大量コピー(10 万セル超)の確認、saveFile
はコンテキストメニューの CSV エクスポートで使われます。内蔵トースト/モーダルは
theme prop(およびダークモード)に追従します。
<VoxSheet ...
platform={{
notify: (kind, message) => { showToast(kind, message); return id },
confirm: async ({ message }) => window.confirm(message),
clipboard: {
readText: () => navigator.clipboard.readText(),
writeText: (t) => navigator.clipboard.writeText(t),
},
}}
/>
型リファレンス
公開されている主要な型の一覧です。
type CellValue = string | number | boolean | null
type ColumnType = "string" | "number" | "date" | "boolean" | "select"
type ColumnAlign = "left" | "right" | "center"
type SelectOption = { value: CellValue; label?: string } // type "select" の選択肢
type SortDirection = "asc" | "desc"
type SortMode = { id: string; label: string }
type SortSpec = { column: string; direction: SortDirection; mode?: string }
type FilterOperator =
| "=" | "!=" | ">" | ">=" | "<" | "<="
| "contains" | "startsWith" | "endsWith"
| "isNull" | "notNull"
type FilterSpec = { column: string; operator: FilterOperator; value?: CellValue }
type CellAddress = { row: number; col: number }
type Selection = { start: CellAddress; end: CellAddress }
type SelectionStats = {
count: number; numericCount: number
sum: number | null; average: number | null
partial?: boolean // 未読込セルを含む集計(読込済みのみで計算)
}
type CellEdit = { row: number; col: number; oldValue: CellValue; newValue: CellValue }
type CellContext = { row: number; col: number; value: CellValue; column: Column }
// 内蔵 UI 文言
type VoxLabels = {
loading: string; empty: string
error: string; retry: string
contextCut: string; contextCopy: string; contextPaste: string
contextInsertRowAbove: string; contextInsertRowBelow: string
contextDeleteRows: string; contextExportCsv: string
contextUndo: string; contextRedo: string
sortOptions: string; sortClear: string
confirmLargeCopyTitle: string; confirmLargeCopyMessage: string
confirmOk: string; confirmCancel: string
preparingCopy: string; selectionTooLarge: string
pasteClipped: string
}
// アイコン
type IconName =
"sortAscending" | "sortDescending" | "sortUnsorted" | "filter" | "filterActive"
type IconProps = { size?: number; className?: string }
type Icons = Partial<Record<IconName, (props: IconProps) => ReactElement>>
// プラットフォーム
type ToastKind = "loading" | "success" | "error" | "info"
type PlatformAdapter = {
clipboard?: {
readText?: () => Promise<string>
writeText?: (text: string) => Promise<void>
}
notify?: (kind: ToastKind, message: string,
opts?: { id?: string; durationMs?: number }) => string
confirm?: (opts: { title?: string; message: string;
confirmLabel?: string; cancelLabel?: string }) => Promise<boolean>
saveFile?: (opts: { suggestedName: string; mimeType?: string;
data: string | Blob }) => Promise<void>
}
制限事項
- 列の仮想化 — 行はウィンドウ化しますが全列を描画します。列数の多い横長テーブルでは列数を穏当に保ってください。
- RTL レイアウト(
dirprop)は対応予定です。
コールバック未指定のヘッダボタンは非表示になります(仕様)。