Filter Search Panelブロック
FilterSearchPanel は、送信型検索フィールド、フィルター設定ボタン、削除可能なフィルターチップを備えた、MUI 上に構築された検索・フィルターツールバーです。
インストール
- npm
- yarn
- pnpm
- bun
npm install @nodeblocks/frontend-filter-search-panel-block
yarn add @nodeblocks/frontend-filter-search-panel-block
pnpm add @nodeblocks/frontend-filter-search-panel-block
bun add @nodeblocks/frontend-filter-search-panel-block
必要なもの
| 項目 | 用途 |
|---|---|
filters | パネルに表示するアクティブなフィルターチップ({ label, key, groupName? }) |
defaultSearchValue | 検索フィールドの初期値 |
onSearch | フォーム送信時にパース済みフォームデータで呼び出される(デフォルトフィールド: search) |
onSearchChange | 制御型検索が必要なときに検索入力の変更イベントを受け取る |
onClickRemoveFilter | ユーザーがチップを削除したときにチップを除去する |
onClickFilterButton | フィルター UI(モーダル、ドロワー、ルートなど)を開く |
searchPlaceholder | SearchInput で使用するプレースホルダーテキスト |
noFilterText | フィルターが選択されていないときに表示するテキスト |
filterLabel | フィルター設定ボタンに表示するラベル |
親が所有するフィルター状態
FilterSearchPanel は filters 配列を保持しません。チップはアプリの状態で管理し、onClickRemoveFilter(および他の場所でフィルターを適用するとき)で更新してください。
コード例
- クイックスタート
- ラベル
- 複合コンポーネント
- ブロックのオーバーライド
ライブエディター
function Example() { const defaultFilters = [ {label: 'Active', key: 'status-active'}, {label: 'Inactive', key: 'status-inactive'}, ]; const filterCatalog = [ {label: 'Active', key: 'status-active'}, {label: 'Inactive', key: 'status-inactive'}, {label: 'Remote', key: 'work-remote', groupName: 'Work type'}, {label: 'Full-time', key: 'employment-full-time', groupName: 'Employment'}, {label: 'Contract', key: 'employment-contract', groupName: 'Employment'}, ]; const [filters, setFilters] = React.useState(defaultFilters); const handleRemoveFilter = filter => { setFilters(current => current.filter(f => f.key !== filter.key)); }; const handleFilterButton = () => { const nextFilter = filterCatalog.find(option => !filters.some(f => f.key === option.key)); if (nextFilter) { setFilters(current => [...current, nextFilter]); } }; const handleSearch = ({search}) => { if (!search?.trim()) { return; } setFilters(current => [...current, {label: search, key: `search-${Date.now()}`, groupName: 'Search'}]); }; return ( <FilterSearchPanel filters={filters} searchPlaceholder="サービスを検索..." noFilterText="フィルターが適用されていません" filterLabel="フィルター設定" onClickFilterButton={handleFilterButton} onClickRemoveFilter={handleRemoveFilter} onSearch={handleSearch} /> ); }
結果
Loading...
ライブエディター
function Example() { const filterCatalog = [ {label: 'Open', key: 'status-open', groupName: 'Status'}, {label: 'In progress', key: 'status-in-progress', groupName: 'Status'}, {label: 'Archived', key: 'status-archived', groupName: 'Status'}, ]; const [filters, setFilters] = React.useState([]); const handleRemoveFilter = filter => { setFilters(current => current.filter(f => f.key !== filter.key)); }; const handleFilterButton = () => { const nextFilter = filterCatalog.find(option => !filters.some(f => f.key === option.key)); if (nextFilter) { setFilters(current => [...current, nextFilter]); } }; const handleSearch = data => { if (!data.search?.trim()) { return; } setFilters(current => [...current, {label: data.search, key: `search-${Date.now()}`}]); }; return ( <FilterSearchPanel filters={filters} defaultSearchValue="React development" searchPlaceholder="プロジェクトを検索..." noFilterText="すべてのプロジェクトを表示" filterLabel="詳細オプション" onClickFilterButton={handleFilterButton} onClickRemoveFilter={handleRemoveFilter} onSearch={handleSearch} /> ); }
結果
Loading...
レイアウトとスタイルをカスタマイズするために子ブロックを使用します。
ライブエディター
function Example() { const handleSearch = ({search}) => { //your debounced search }; return ( <FilterSearchPanel searchPlaceholder="サービスを検索..." onSearch={handleSearch}> <FilterSearchPanel.SearchInput /> </FilterSearchPanel> ); }
結果
Loading...
関数の children を使ってデフォルトブロックをオーバーライドし、順序を変更したりカスタムブロックを注入します。
ライブエディター
function Example() { const defaultFilters = [ {label: 'Active', key: 'status-active'}, {label: 'Web Development', key: 'category-web'}, ]; const filterCatalog = [ {label: 'Active', key: 'status-active'}, {label: 'Web Development', key: 'category-web'}, {label: 'Premium', key: 'tier-premium', groupName: 'Tier'}, {label: 'Enterprise', key: 'tier-enterprise', groupName: 'Tier'}, ]; const [filters, setFilters] = React.useState(defaultFilters); const handleRemoveFilter = filter => { setFilters(current => current.filter(f => f.key !== filter.key)); }; const handleFilterButton = () => { const nextFilter = filterCatalog.find(option => !filters.some(f => f.key === option.key)); if (nextFilter) { setFilters(current => [...current, nextFilter]); } }; const handleSearch = ({search}) => { if (!search?.trim()) { return; } setFilters(current => [...current, {label: search, key: `search-${Date.now()}`}]); }; return ( <FilterSearchPanel filters={filters} filterLabel="オプション" searchPlaceholder="サービスを検索..." onClickFilterButton={handleFilterButton} onClickRemoveFilter={handleRemoveFilter} onSearch={handleSearch} > {({defaultBlocks, defaultBlockOrder}) => ({ blocks: { ...defaultBlocks, searchTip: ( <Box sx={{ px: 1.5, py: 1, bgcolor: 'info.light', borderRadius: 1, fontSize: 'body2.fontSize', color: 'info.dark', }} > Enter キーで検索するか、フィルターで結果を絞り込んでください。 </Box> ), }, blockOrder: [...defaultBlockOrder, 'searchTip'], })} </FilterSearchPanel> ); }
結果
Loading...
ブロックのオーバーライドを使うタイミング
ブロックの順序変更、デフォルトブロックの差し替え、ルートコンポーネントの検索とチップの動作を維持しながらカスタムブロック(上記の searchTip など)の追加が必要な場合にオーバーライドを使います。
主要プロパティ
コアプロパティ
| プロパティ | 型 | 必須 | デフォルト | 説明 |
|---|---|---|---|---|
filters | FilterChip[] | いいえ | [] | パネルに表示する選択済みフィルターチップ |
defaultSearchValue | string | いいえ | undefined | SearchInput で使用する初期値 |
onSearch | (data: T) => void | いいえ | undefined | フォーム送信時に呼び出される。T のデフォルトは { search: string } |
onSearchChange | (event: React.ChangeEvent<HTMLInputElement>) => void | いいえ | undefined | 検索フィールドの変更ハンドラー。制御型検索入力で一般的に使用 |
onClickRemoveFilter | (filter: FilterChip) => void | いいえ | undefined | チップの削除アイコンがクリックされたときに呼び出される |
onClickFilterButton | () => void | いいえ | undefined | フィルター設定ボタンがクリックされたときに呼び出される |
コンテンツプロパティ
| プロパティ | 型 | 必須 | デフォルト | 説明 |
|---|---|---|---|---|
searchPlaceholder | string | いいえ | フリーワードで検索 | 検索入力に表示するプレースホルダー |
noFilterText | string | いいえ | 条件未設定 | フィルターが選択されていないときに表示するテキスト |
filterLabel | string | いいえ | 絞込み設定 | フィルター設定ボタンに表示するラベル |
レイアウトと構成プロパティ
| プロパティ | 型 | 必須 | デフォルト | 説明 |
|---|---|---|---|---|
children | BlocksOverride | ReactNode | いいえ | undefined | デフォルトブロックをオーバーライドする、または複合コンポーネントの子をレンダリングする |
className | string | いいえ | undefined | ルートフォームのクラス名 |
sx | SxProps | いいえ | undefined | ルートフォーム用の MUI システムスタイル |
spacing | StackProps['spacing'] | いいえ | { xs: 1.5, sm: 2.5 } | ルートブロック間のスペース |
direction | StackProps['direction'] | いいえ | 'column' | ルートスタックの flex 方向 |
FilterSearchPanel は StackProps(children、component、onSubmit を除く)を継承し、form としてレンダリングされます。送信は内部で処理され、指定時は onSearch が呼び出されます。
サブコンポーネントのプロパティ
サブコンポーネントはルートとコンテキストを共有するため、デフォルトではコンテキストのプロパティ値を使用しますが、ローカルでオーバーライドできます。
| サブコンポーネント | 主要プロパティ | 継承元 |
|---|---|---|
FilterSearchPanel.SearchInput | searchPlaceholder, defaultSearchValue, onSearchChange, name, type | TextField |
FilterSearchPanel.FilterButton | filterLabel, onClickFilterButton, children | Button |
FilterSearchPanel.SelectedFilterList | filters, noFilterText, onClickRemoveFilter, children | Box |
FilterSearchPanel.FilterBadge | filter (必須), onClickRemoveFilter, children | Chip |
FilterSearchPanel.ActionGroup | children (デフォルト: フィルターボタン + チップリスト) | Stack |
デフォルト UI ブロック
| ブロック | ベース | 備考 |
|---|---|---|
FilterSearchPanel (ルート) | Stack を form としてレンダリング | 送信を内部で処理し、子ブロックに検索/フィルターコンテキストを提供する |
FilterSearchPanel.SearchInput | TextField + 送信 IconButton | name="search", size="small"。検索アイコンでフォームを送信する |
FilterSearchPanel.ActionGroup | Stack | 横並び: フィルターボタン + 選択済みチップ(スクロール可能) |
デフォルトのレンダリング順: searchInput → actionGroup。
追加フィールドプリミティブ
| プリミティブ | ベース | 備考 |
|---|---|---|
FilterSearchPanel.FilterButton | Button + Tune icon | onClickFilterButton 経由でフィルター UI を開く。デフォルトラベルは 絞込み設定 |
FilterSearchPanel.SelectedFilterList | Box + Typography | 空のときは noFilterText(条件未設定)を表示し、それ以外は groupName でチップをグループ化する |
FilterSearchPanel.FilterBadge | Chip | 削除可能なチップ。削除時に onClickRemoveFilter(filter) を呼び出す |
TypeScript
import * as React from 'react';
import {FilterSearchPanel} from '@nodeblocks/frontend-filter-search-panel-block';
type FilterChip = {
label: string;
key: string;
groupName?: string;
};
type SearchFormData = {
search: string;
};
const filterCatalog: FilterChip[] = [
{label: 'Remote', key: 'work-remote', groupName: 'Work type'},
{label: 'Full-time', key: 'employment-full-time', groupName: 'Employment'},
];
export function ServiceSearchToolbar() {
const [filters, setFilters] = React.useState<FilterChip[]>([
{label: 'Active', key: 'status-active', groupName: 'Status'},
]);
const handleRemoveFilter = (filter: FilterChip) => {
setFilters(current => current.filter(f => f.key !== filter.key));
};
const handleFilterButton = () => {
const nextFilter = filterCatalog.find(option => !filters.some(f => f.key === option.key));
if (nextFilter) {
setFilters(current => [...current, nextFilter]);
}
};
const handleSearch = (data: SearchFormData) => {
const search = data.search.trim();
if (!search) {
return;
}
setFilters(current => [...current, {label: search, key: `search-${Date.now()}`, groupName: 'Search'}]);
};
return (
<FilterSearchPanel<SearchFormData>
filters={filters}
searchPlaceholder="サービスを検索..."
noFilterText="フィルターが適用されていません"
filterLabel="フィルター設定"
onClickFilterButton={handleFilterButton}
onClickRemoveFilter={handleRemoveFilter}
onSearch={handleSearch}
/>
);
}