ソートとフィルター
列フィルター(text / number / date / select / set / custom)とグローバルフィルター
ヘッダーの列メニューからソートとフィルターを操作できます。フィルター UI の種別は column.filterType で列ごとに指定します。
デモ
- 商品名 —
textSet:条件 AND 選択の複合フィルター(テキスト版)。「を含む / に等しい / で始まる / で終わる / 空白 / 空白でない」の条件と Set 一覧を組み合わせます - カテゴリ —
select:単一選択(候補は rows から自動収集) - ステータス —
set:チェックボックスの複数選択(Excel 風) - 数量 —
numberSet:条件 AND 選択の複合フィルター(数値版)。数値条件(以上 / 範囲 など)と Set 一覧を 1 つの popover で組み合わせます(例:「10 以上のうち、12 だけ除外」) - 単価 —
auto:種別を指定せず、列の値から自動判定します(値が全て数値なのでnumberSetとして開きます) - 登録日 —
dateSet:条件 AND 選択の複合フィルター(日付版)。日付条件(範囲 / 以降 / 以前 など)と相対プリセット(今日 / 今月 / 過去 30 日)、そして年 / 月 / 日の 3 階層ツリーの Set 一覧を組み合わせます
上部のグローバルフィルター入力は全列を横断して部分一致します。フィルター適用中はチップバー(showFilterChipBar)に条件が並び、ここから個別解除もできます。列メニューまたはチップバーの「フィルター管理」から、全フィルターを一覧で編集できるパネルも開けます。
列定義
const columns: GridColumn<Row>[] = [
{ key: 'name', title: '商品名', width: 180, filterType: 'textSet' },
{ key: 'category', title: 'カテゴリ', width: 130, filterType: 'select' },
{ key: 'status', title: 'ステータス', width: 130, filterType: 'set' },
{ key: 'qty', title: '数量', width: 110, filterType: 'numberSet' },
{ key: 'price', title: '単価', width: 130, filterType: 'auto' },
{ key: 'registered', title: '登録日', width: 140, filterType: 'dateSet' },
]select/set/numberSet/textSet/dateSetの候補は rows から自動収集されます。明示したい場合はfilterOptions: [{ label, value }]を指定します。number/numberSetの比較 / 範囲では空白セル(null / 空文字)は常に不一致です(0 とは区別されます)。空白行の抽出・除外は演算子「空白 / 空白でない」か、Set 一覧の「(空白)」項目を使います(dateSetの日付条件でも同じ規則です)。numberSet/textSet/dateSetでは条件を適用すると Set 候補が条件を満たす値だけに連動して絞られます。候補から外れた値のチェック状態は破棄されず、条件を戻せば復活します(条件の試行錯誤が可逆)。クリアは「条件のみ」「値のみ」「全部」の 3 粒度です。textSetの条件(Set の検索欄との違い): 検索欄は「候補を選ぶための絞り込み」で、述語としては残りません。「◯◯ を含む」を述語として保持したい(後から増えた該当データも自動で拾いたい)場合は条件欄を使います。dateSetの相対プリセット(今日 / 今月 / 過去 30 日)は相対のまま保存され、フィルター評価のたびに「今日」を基準へ解決されます(getState()で保存した状態を翌日復元すると範囲が追従します)。グリッドを開いたまま日付が変わったときも、深夜 0 時に自動で翌日の範囲へ切り替わります(スリープ明けなどでタイマーが遅れたときはタブが前面に戻った時点。範囲が変わる日だけ絞り込み直し、serverSide はブロックを取り直します。「今月」なら月が変わるときだけです)。日付を固定したい場合は「範囲」等の絶対条件を使います。チップの非表示・カスタマイズは下記「相対プリセットの構成(dateFilterPresets)」を参照してください。- 独自の絞り込みロジックは
filterType: 'custom'+filterFn: (row, filterValue) => booleanで書けます。 - ソートは値で順序が決まります。数値として解釈できる値 → 文字列 → 空白セル(null / undefined / 空文字)の順で、空白セルは昇順でも降順でも末尾に並びます(Excel と同じ)。
- ソート / フィルター / グローバルフィルターは
enableSorting/enableColumnFilter/enableGlobalFilter(いずれも既定true)で無効化できます。
相対プリセットの構成(dateFilterPresets)
dateSet popover の相対プリセットチップは、列オプション dateFilterPresets で列ごとに構成できます。未指定はビルトイン 3 種(今日 / 今月 / 過去 30 日)です。
// ① 非表示(オプトアウト): チップ行そのものを出さない
{ key: 'updatedAt', title: '更新日時', filterType: 'dateSet', dateFilterPresets: false }
// ② カスタム構成: ビルトイン ID の再利用とカスタム定義を混在できる(表示順のまま描画)
{
key: 'updatedAt',
title: '更新日時',
filterType: 'dateSet',
dateFilterPresets: [
'today', // ビルトイン ID(ラベルは既定の「今日」)
{
id: 'thisWeek', // フィルター値に保存されるのはこの id(相対のまま)
label: '今週',
resolve: (now) => { // フィルター評価のたびに呼ばれ、絶対範囲へ解決される
const day = now.getDay();
const monday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - ((day + 6) % 7));
return { from: monday, to: now }; // 'YYYY-MM-DD' 文字列 or Date(両端含む)
},
},
],
}- カスタムプリセットも**ビルトインと同じ「相対保存」**です。保存されるのは
idだけで、評価のたびにresolve(now)が絶対範囲へ解決します(開いたまま日付が変わったときの解決し直しもビルトインと同じです)。 resolveの返り値は{ from?, to? }(両端含む)。片側だけ返すと 以降 / 以前 として評価され、from > toは自動で入れ替わります。- 列定義からカスタム ID を外すと、保存済みのそのプリセット条件は「条件なし」として評価されます(全行が消える事故を避ける安全側の挙動)。
- serverSide(
dataSource)ではカスタム ID も{ mode: 'preset', preset: id }のまま送出されます。id の解釈はサーバ側の責務です(サーバーサイド行モデル)。
dateSet の日付入力(既定 UI)
dateSet 条件の日付入力は、v0.28 からネイティブ <input type="date"> に代わり内製の日付フィールドになりました(ブラウザ依存の見た目・操作性の問題を解消)。
- 自由入力 —
2026/7/1/2026-07-01など表記ゆれを受け付け、Enter / blur で正規化して確定します。解釈できない入力は赤枠のまま確定しません(適用済みの条件は保持)。 - カレンダー — フィールド右のボタンで開閉。タイトルクリックで 日 → 月一覧 → 年一覧 とドリルアップでき、遠い年代へも素早く移動できます。フッターに「今日」「クリア」(月・年ビューでは選択せずに戻れる「← 戻る」も表示)。
- ダークテーマ / 密度トークンへ追従します。追加の依存はありません。
- セル値の日付の解釈 —
Dateインスタンスはブラウザのローカル時刻の年月日、文字列は先頭のYYYY-MM-DD/YYYY/M/D部分をそのまま使います(後ろの時刻 / タイムゾーン表記は見ません)。new Date('2026-07-01')は UTC 0 時の値のため、UTC より遅れたタイムゾーン(米国など)ではローカルの年月日が前日(2026-06-30)になります。サーバーから日付だけを受け取る場合は、Dateに変換せず文字列のままrowsに入れるのが確実です。
日付入力の差し替え(renderFilterDateInput)
内製フィールドではなくアプリの UI ライブラリで統一したい場合は、グリッドの prop renderFilterDateInput でピッカーごと差し替えられます(対象はフィルター popover の条件欄のみ)。
import { DatePickerInput } from '@mantine/dates';
<SpreadsheetGrid
renderFilterDateInput={({ value, onChange, ariaLabel }) => (
<DatePickerInput
aria-label={ariaLabel}
value={value || null} // value は 'YYYY-MM-DD' か ''
onChange={onChange} // string | Date | null をそのまま渡せる(内部で正規化)
valueFormat="YYYY/MM/DD"
size="xs"
clearable
popoverProps={{ withinPortal: false }} // ★ 外側クリック対策(下記)
style={{ flex: 1, minWidth: 0 }}
/>
)}
/>- コンテキストは
{ value, onChange, ariaLabel, slot: 'single' | 'from' | 'to', columnKey }。onChangeはDate/null(クリア)/'2026/7/1'のような表記ゆれ文字列も受け付け、内部で'YYYY-MM-DD'へ正規化されます(解釈できない値はクリア扱い)。値を変更するとプリセットチップの選択は解除されます。 - 外側クリック対策: フィルター popover は外側 pointerdown で閉じるため、ピッカーのカレンダーが body 直下ポータルに出ると操作で閉じてしまいます。①ポータル無効化で popover 内に描画(Mantine の
popoverProps={{ withinPortal: false }}等。推奨)、または ②ポップアップ要素へdata-ssg-filter-keep-open属性を付与、のどちらかで回避してください。 - HeroUI の例や詳細な契約は API リファレンス(パッケージ同梱の
API_REFERENCE.md「日付入力の差し替え」節)を参照してください。
種別の自動判定(filterType: 'auto')
列ごとに種別を書き分けるのが面倒なときは filterType: 'auto' を指定すると、列の値から numberSet / textSet / dateSet を自動判定します。明示的な opt-in のみで、filterType 未指定の列は従来どおりフィルターなしのままです。
{ key: 'price', title: '単価', width: 130, filterType: 'auto' }- 判定は popover を初回に開いた時点で 1 回だけ行い、以後その列では固定されます(行を編集・追加するたびに種別が変わると、適用中のフィルターと UI が食い違うためです)。
- 優先順位: 適用済みフィルター → 前回の判定結果 →
editor種別のヒント({ type: 'number' }/{ type: 'date' })→ 値のサンプリング(空白を除く最大 1,000 件)。 - 厳格判定です。サンプルが全て日付なら
dateSet、全て数値ならnumberSet、1 件でも混ざればtextSet(安全側)になります。 - 判定は**値の型ではなく「数値 / 日付として解釈できるか」**です。DB の型が文字列でも
'1234'は数値とみなします。ただし'0001'のような先頭ゼロの値は数値とみなしません(品番コード・郵便番号を数値と誤認しないため。Excel と同じ扱いです)。 - serverSide(
dataSource)では値からの推定を行いません(クライアントが全行を持たないため)。editor種別があればそれで確定し、無ければtext(部分一致)になります。auto を使う列にはeditorの指定を推奨します。
非表示列のフィルター / ソート
列を非表示(visible: false)にしても、その列に載っている列フィルター / ソートは解除されず、行の絞り込み / 並べ替えに効き続けます。ツールバーの件数表示やフィルター管理パネルの「適用中」と結果は一致します。解除したい場合はフィルター管理パネルやソート管理パネルから外してください。グローバル検索(クイックフィルター)は可視列だけが対象です。
絞り込み / 並べ替えを外部に委ねる(manualFiltering / manualSorting)
「条件はサーバー(SQL の WHERE / ORDER BY)、行はクライアント(絞り込み済みの結果を rows で渡す)」という構成では、フィルター / ソートの UI はそのまま使いたい一方で、グリッド自身に行を絞り込み・並べ替えさせたくありません(二重に処理される / 上限で打ち切った結果を並べ替えると意味が変わる)。manualFiltering / manualSorting(いずれも既定 false)を true にすると、UI と状態(GridState / onStateChange)は従来どおり動き、行の処理だけを行わなくなります。
<SpreadsheetGrid
rows={rowsFromServer}
columns={columns}
manualFiltering
// 取得が上限に達したときだけサーバ側 ORDER BY へ切り替える(再マウント不要)
manualSorting={reachedLimit}
// フィルター / ソートが実際に変わったときだけ呼ばれる(列幅などでは呼ばれない)
onFiltersChange={(filters) => {
// filters.columnFilters(ColumnFilterValue の記述子)から WHERE を組み立てる
}}
onSortChange={(sort) => {
// sort から ORDER BY を組み立てる
}}
/>manualFiltering: 列フィルターもグローバルフィルターも評価しません(number 列の Float64 キー前計算も走りません)。rowsが 0 件でフィルターが載っているときの空表示はnoMatchingRowsTextになります。manualSorting: ソートを適用しません。falseへ戻すと即座にクライアントソートが適用されます。手動ソート中は「並べ替えていない」扱いなので、labelRow.sortModeの連動や行ドラッグの無効化は起きません。- serverSide(
dataSource)ではグリッドはもともと絞り込み / 並べ替えを行わないため、どちらも無視されます。 - 記述子の形(
ColumnFilterValueのkind別 shape)は serverSide の「列フィルターの wire format」を参照してください。clientSide でも同じ記述子がGridState.filters.columnFiltersに載ります。 - 以前の回避策(全列に
filterFn: () => trueを付けてenableGlobalFilter={false})は不要です。
候補を非同期に供給する(getFilterOptions)
set / select / 複合(numberSet / textSet / dateSet)の候補は既定では rows から集めますが、DB 側で絞り込んだ後の rows からは選んだ値しか集まりません。getFilterOptions(グリッド prop)を指定すると、popover を開くたびにコールバックで候補を取得します(DB の SELECT DISTINCT など)。
<SpreadsheetGrid
rows={rowsFromServer}
columns={columns}
getFilterOptions={async ({ columnKey, columnFilters, signal }) => {
// columnFilters は開いている列を除いた他列の有効フィルター(Excel と同じく候補を他列の条件で絞れる)
const values = await db.distinct(columnKey, buildWhere(columnFilters), { limit: LIMIT + 1, signal })
return {
options: values.slice(0, LIMIT).map((v) => ({ label: v ?? '(空白)', value: v ?? '' })),
truncated: values.length > LIMIT,
}
}}
/>- 引数は
{ columnKey, column, columnFilters, globalText, signal }。閉じる / 列切替でsignalが abort されます(abort による reject はエラー表示しません)。 - 戻り値は
{ options: { label, value }[], truncated?: boolean }。valueは記述子のvaluesにそのまま載る文字列です。空白(NULL)は''、dateSet 列は'YYYY-MM-DD'を渡します。 - popover は読み込み中 / 失敗(「再試行」ボタン付き)/ 打ち切り(
truncated: trueで「先頭のみ・打ち切り」の注記)を表示します。複合列では取得中も条件欄は使えます。 - 優先順位は
column.filterOptions(静的)>getFilterOptions> rows 自動収集。静的候補を持つ列ではコールバックは呼ばれません。 - 非同期候補の列は反転(exclude)できます。「これ以外」は SQL では
NOT INになるため、打ち切られた候補の外にある値も正しく扱えます。 - ライブラリはキャッシュを持ちません。同じ引数で再取得したくない場合は、アプリ側で
columnKey+ 他列フィルターをキーに Promise を保持してください。 - 検索欄は取得済みの候補の中だけを探します(サーバへ検索文字列を渡す再取得は行いません)。
- serverSide(
dataSource)でも同じ prop が使えます(「候補が未指定」表示の代わりになります)。
状態を外から扱う
ソート / フィルター状態はグリッド内部の reducer が持ちますが、getState() / applyState()(命令的ハンドル)で保存・復元、onStateChange で変更検知ができます。フィルター / ソートだけを追いたい場合は onFiltersChange / onSortChange(該当スライスが実際に変化したときだけ発火)が使えます。詳細は API リファレンス を参照してください。