1. ホーム
  2. WordPress

【WordPress】ToolsPanel・ToolsPanelItem の使い方|コア風のリセット付き設定サイドバーを作る方法

Share

段落ブロックや画像ブロックを選択すると、右サイドバーの見出しの横に「⋮」(オプション)ボタンがあり、そこから設定項目を追加したり外したりできます。外した項目は値が初期化され、「すべてリセット」を押せばそのパネルの設定が一度に元へ戻ります。この UI を作っているのが ToolsPanel と ToolsPanelItem です。

この記事では @wordpress/components の ToolsPanel / ToolsPanelItem を使って、カスタムブロックの設定サイドバーにコアブロックと同じ「出し入れできる・リセットできる」パネルを作る方法を解説します。PanelBody との違い、各 props の意味、そして項目が出てこないときの原因までを順番に見ていきます。

PanelBody との違いは「使っている項目だけを見せる」こと

PanelBody は、開閉できる枠の中に入れたコントロールを全部そのまま並べるコンポーネントです。設定項目が3つ4つのうちは問題ありませんが、10個近くになるとサイドバーが縦に長くなり、編集者は実際に使っている設定がどれなのか分からなくなります。

ToolsPanel は、そこに「表示する項目を選ぶ」という層を足したものです。初期状態では isShownByDefault を付けた項目だけが表示され、それ以外はパネル見出し横のメニューから選んで初めて現れます。値が入っている項目にはチェックが付き、メニューから外す(×を押す)とその設定が初期値に戻ります。さらに「すべてリセット」で、パネル内の設定をまとめて初期化できます。

観点PanelBodyToolsPanel
表示のしかた子要素をすべて表示する既定表示の項目と、設定済みの項目だけを表示する
項目の出し入れできない見出し横のメニューから追加・削除できる
リセット自分で用意する必要がある項目ごとのリセットと「すべてリセット」が標準で付く
設定済みかどうかの表示なしメニュー上でチェック(●)が付く
向いている場面項目が少なく、常に全部見せたいとき項目が多く、任意設定が混ざるとき

言い換えると、必ず設定する項目は PanelBody、設定しても・しなくてもよい任意の項目は ToolsPanel という住み分けになります。コアブロックでも、表示スタイルのような必須に近い設定は通常のパネルに、余白や枠線などの装飾系は ToolsPanel に置かれています。両方を1つのブロックで併用しても構いません。

WordPress 6.x では __experimental 付きでエクスポートされている

注意が必要なのがインポート名です。ToolsPanel と ToolsPanelItem は、WordPress 6.x の時点では @wordpress/components から __experimentalToolsPanel / __experimentalToolsPanelItem という名前でエクスポートされています。import { ToolsPanel } from '@wordpress/components' と書いても undefined になるだけで、パネルは描画されません。

src/edit.js(インポート)
import {
	__experimentalToolsPanel as ToolsPanel,
	__experimentalToolsPanelItem as ToolsPanelItem,
	RangeControl,
	SelectControl,
} from '@wordpress/components';

as で別名を付けてから使うのが慣例です。コアのブロックも同じ書き方をしているので、あとで正式名称に変わったときはインポート文の1行だけ直せば済みます。

__experimental というプレフィックスは「API がまだ固まっておらず、予告なく変更される可能性がある」という WordPress の印です。実際に props が増えたり挙動が調整されたりしているので、WordPress のバージョンを上げたときは挙動を確認するという前提で使ってください。とはいえコアブロックの設定サイドバーがすでにこのコンポーネントで作られているため、いきなり削除される類のものではありません。

ToolsPanel の props

外側の ToolsPanel には、パネル名・リセット処理・パネルの識別子の3つを渡すのが基本です。

prop型説明
labelstringパネルの見出しに表示する名前。項目を出し入れするメニューのアクセシブルな名前にも使われる
resetAllfunction「すべてリセット」を押したときに呼ばれる。パネル内の属性をまとめて初期値に戻す処理を書く
panelIdstringこのパネルの識別子。ブロックでは clientId を渡す。子の ToolsPanelItem にも同じ値を渡す
headingLevelnumber / string見出しのレベル。既定は 2
dropdownMenuPropsobject項目を出し入れするドロップダウンに渡す追加の props
classNamestringパネルに追加するクラス名

resetAll は引数なしで書いても動きます。後述する resetAllFilter を子で使う場合のみ、フィルター関数の配列が渡ってくる形になります。ブロックの属性をリセットするだけなら、次のように setAttributes() で既定値へ戻す関数を書けば十分です。

src/edit.js(resetAll の例)
const resetAll = () => {
	setAttributes( {
		spacing: undefined,
		alignment: undefined,
	} );
};

値を undefined に戻している点がポイントです。block.json で default を指定している属性は、undefined を代入すると保存された値が消え、その属性の既定値が使われる状態に戻ります。0 や空文字を入れると「0 という値が設定されている」状態になってしまい、後述する hasValue の判定とも噛み合わなくなります。

ToolsPanelItem の props

個々の設定コントロールを包むのが ToolsPanelItem です。「この項目は設定済みか」「外されたら何をするか」をこのコンポーネントに教える形になります。

prop型説明
hasValuefunctionこの項目が設定済みかどうかを返す関数。true のときメニューにチェックが付き、リセットの対象になる
labelstringメニューに並ぶ項目名。中のコントロールの label と揃えるのが基本
onDeselectfunction項目をメニューから外したときに呼ばれる。対応する属性を初期化する処理を書く
onSelectfunction項目をメニューから追加したときに呼ばれる。初期値を入れたいときなどに使う(省略可)
isShownByDefaultbooleantrue で、値が未設定でも最初から表示する
panelIdstring親の ToolsPanel と同じ値を渡す。一致しないと項目が登録されない
resetAllFilterfunction「すべてリセット」時に、親の resetAll へ渡すフィルター関数(省略可)

hasValue は「設定済みか」を返す関数

hasValue には真偽値ではなく関数を渡します。ToolsPanel はこの関数を呼んで、項目にチェックを付けるか、リセットボタンを出すかを判断します。中身は「属性に値が入っているか」を返すだけです。

src/edit.js(hasValue の書き方)
// 数値の属性:未設定なら undefined になっている想定
hasValue={ () => spacing !== undefined }

// 文字列の属性:空文字も「未設定」として扱う
hasValue={ () => !! alignment }

数値の属性で () => !! spacing と書くと、0 が設定されているときに false になってしまいます。「余白 0」は立派な設定値なので、数値のときは !== undefined で判定するほうが正確です。

onDeselect は「値を初期化する処理」

onDeselect は、編集者がメニューからその項目のチェックを外したとき(表示されている項目の×を押したとき)に呼ばれます。ToolsPanel がやってくれるのは項目を画面から消すところまでで、属性の値を消す処理は自分で書く必要があります。ここを空にしておくと、見た目だけ消えて設定は残ったまま、という分かりにくい状態になります。

src/edit.js(onDeselect の書き方)
onDeselect={ () => setAttributes( { spacing: undefined } ) }

つまり hasValue が「今どうなっているか」を読む係、onDeselect が「1項目だけ元に戻す」係、親の resetAll が「全部まとめて元に戻す」係、という役割分担です。この3つが同じ属性について矛盾なく書かれていれば、あとは ToolsPanel が UI を組み立ててくれます。

isShownByDefault で最初から出す項目を決める

isShownByDefault を付けた項目は、値が未設定でもパネルを開いた瞬間から表示されます。付けなかった項目は、値が入っているか、メニューから選ばれたときだけ現れます。よく使う設定を1〜2個だけ isShownByDefault にして、残りはメニューに隠しておく、という配分にするとコアブロックに近い使い心地になります。

すべての項目に isShownByDefault を付けてしまうと、結果的に PanelBody と同じ見た目になり、ToolsPanel を使う意味が薄れます。

ブロックに組み込む

ここまでを踏まえて、余白(数値)と配置(選択式)の2つを ToolsPanel で設定できるブロックを作ります。まず属性を定義します。

src/block.json
{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 3,
	"name": "webool/notice-box",
	"title": "お知らせボックス",
	"category": "design",
	"icon": "megaphone",
	"attributes": {
		"spacing": {
			"type": "number"
		},
		"alignment": {
			"type": "string"
		}
	},
	"editorScript": "file:./index.js",
	"style": "file:./style-index.css"
}

ここではあえて default を書いていません。ToolsPanel は「未設定」と「設定済み」を区別する UI なので、属性の初期値を undefined のままにしておくと hasValue の判定が素直になります。既定値を持たせたい場合は、block.json の default ではなく、edit.js や save.js の側で「値がなければこの見た目」と分岐させるほうが扱いやすくなります。

src/edit.js
import { __ } from '@wordpress/i18n';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import {
	__experimentalToolsPanel as ToolsPanel,
	__experimentalToolsPanelItem as ToolsPanelItem,
	RangeControl,
	SelectControl,
} from '@wordpress/components';

export default function Edit( { attributes, setAttributes, clientId } ) {
	const { spacing, alignment } = attributes;
	const blockProps = useBlockProps( { className: 'notice-box' } );

	// 「すべてリセット」で呼ばれる。パネル内の属性をまとめて初期化する
	const resetAll = () => {
		setAttributes( {
			spacing: undefined,
			alignment: undefined,
		} );
	};

	return (
		<>
			<InspectorControls>
				<ToolsPanel
					label={ __( '表示設定', 'my-blocks' ) }
					resetAll={ resetAll }
					panelId={ clientId }
				>
					<ToolsPanelItem
						hasValue={ () => spacing !== undefined }
						label={ __( '内側の余白', 'my-blocks' ) }
						onDeselect={ () =>
							setAttributes( { spacing: undefined } )
						}
						isShownByDefault
						panelId={ clientId }
					>
						<RangeControl
							__next40pxDefaultSize
							__nextHasNoMarginBottom
							label={ __( '内側の余白', 'my-blocks' ) }
							value={ spacing }
							min={ 0 }
							max={ 80 }
							step={ 4 }
							onChange={ ( value ) =>
								setAttributes( { spacing: value } )
							}
						/>
					</ToolsPanelItem>

					<ToolsPanelItem
						hasValue={ () => !! alignment }
						label={ __( '文字の配置', 'my-blocks' ) }
						onDeselect={ () =>
							setAttributes( { alignment: undefined } )
						}
						panelId={ clientId }
					>
						<SelectControl
							__next40pxDefaultSize
							__nextHasNoMarginBottom
							label={ __( '文字の配置', 'my-blocks' ) }
							value={ alignment }
							options={ [
								{ label: __( '左', 'my-blocks' ), value: 'left' },
								{ label: __( '中央', 'my-blocks' ), value: 'center' },
								{ label: __( '右', 'my-blocks' ), value: 'right' },
							] }
							onChange={ ( value ) =>
								setAttributes( { alignment: value } )
							}
						/>
					</ToolsPanelItem>
				</ToolsPanel>
			</InspectorControls>

			<div
				{ ...blockProps }
				style={ {
					padding: spacing !== undefined ? `${ spacing }px` : undefined,
					textAlign: alignment,
				} }
			>
				<p>ここにお知らせの本文が入ります。</p>
			</div>
		</>
	);
}

panelId に渡している clientId は、編集中のブロック1つ1つに割り当てられる一意の ID です。Edit コンポーネントの引数として受け取れるので、追加の import は要りません。

ブロック本体側では、余白が未設定のときに padding を undefined にしています。`${ spacing }px` をそのまま書くと、未設定時に "undefinedpx" という不正な値になってしまうためです。textAlign のほうは、alignment が undefined ならプロパティごと出力されないので、そのまま渡して問題ありません。

panelId に clientId を渡す理由

ToolsPanel は、どの項目が表示中かという状態を内部に持っています。ブロック A で「文字の配置」を追加した状態のままブロック B を選択したとき、その表示状態が B に引き継がれてしまうと、B では設定していない項目が開いたままになります。

panelId は、その状態をどのパネルのものとして扱うかを示す値です。ブロックごとに変わる clientId を渡しておけば、選択するブロックが変わったときにパネルが別物として扱われ、表示状態がそのブロックの値に応じて組み直されます。ToolsPanelItem 側の panelId も、自分が今どのパネルに属しているかを判断するために使われます。

項目が表示されない・リセットが効かないとき

パネルは出るのに中身が空っぽ

見出しとメニューボタンだけが表示され、項目がひとつも出てこない場合、まず疑うのは panelId の不一致です。ToolsPanelItem は自分の panelId と親の panelId を照らし合わせ、食い違っていると親に登録されません。登録されない項目はメニューにも本体にも現れないので、結果として空のパネルになります。

よくあるのは、親には panelId={ clientId } を渡したのに子には渡し忘れているパターンと、子だけ別の文字列(ブロック名など)を渡しているパターンです。親と子で完全に同じ値になっているかを確認してください。

また、isShownByDefault をどの項目にも付けていない場合、値が未設定のブロックではすべての項目が隠れた状態になります。これは仕様どおりの動きなので、メニューから項目を選べば表示されます。「最初から見せたい項目」には忘れずに isShownByDefault を付けましょう。

リセットしても値が戻らない・リセットが出ない

hasValue が常に true を返していると、その項目は「いつでも設定済み」と見なされます。逆に常に false を返していると、値を入れてもチェックが付かず、リセットの対象にもなりません。

ありがちなのが hasValue={ spacing !== undefined } のように、関数ではなく評価結果を渡してしまうミスです。この場合、渡されるのは真偽値なので ToolsPanel が関数として呼び出せず、意図した判定になりません。必ず hasValue={ () => spacing !== undefined } と、アロー関数で包んだ形にします。

「×を押すと項目は消えるのに、ブロックの見た目が変わらない」という場合は onDeselect の中身を見てください。setAttributes() を呼んでいない、あるいは undefined ではなく 0 や空文字を入れていることが原因です。block.json で default を指定している属性の場合、その default と同じ値を入れても「設定済み」と判定されることがあるので、undefined に戻すのが確実です。

コンソールに「is not a function」と出る

「Element type is invalid」や、コンポーネントが undefined である旨のエラーが出るときは、インポート名を見直します。__experimental プレフィックスを省いて import { ToolsPanel } from '@wordpress/components' と書いていると、ToolsPanel は undefined になります。JSX で undefined をコンポーネントとして使おうとした結果のエラーです。

合わせて package.json や block.json の依存関係も確認してください。@wordpress/scripts でビルドしている場合、@wordpress/components は wp.components としてグローバルから解決されるため、動作環境の WordPress のバージョンが古いと、その時点で存在しないコンポーネントは取得できません。ToolsPanel 自体は長く使われている API ですが、dropdownMenuProps のような後から追加された props は、古いバージョンでは無視されます。

メニューの項目名とコントロールのラベルがずれる

ToolsPanelItem の label はメニューに並ぶ名前、中に入れた RangeControl などの label はコントロールの見出しで、別々に指定します。片方だけ直すと、メニューでは「余白」なのに開くと「スペース」と表示される、という食い違いが起きます。両方を同じ文言にしておくのが原則です。

なお ToolsPanelItem の label は、同じパネル内で重複しないようにします。項目はラベルで識別されるため、同名の項目が2つあるとメニューでどちらを指しているのか分からなくなります。

まとめ

ToolsPanel と ToolsPanelItem は、設定項目を出し入れでき、個別にもまとめてもリセットできるパネルを作るためのコンポーネントです。任意設定が多いブロックほど効果があり、コアブロックと同じ操作感を編集者に提供できます。WordPress 6.x では __experimentalToolsPanel / __experimentalToolsPanelItem という名前でエクスポートされている点を忘れないでください。

書くことは3つだけです。各項目に「設定済みかを返す関数」(hasValue)、「1項目だけ初期化する処理」(onDeselect)、そしてパネル全体に「すべて初期化する処理」(resetAll)。加えて親子の panelId に同じ clientId を渡せば、あとは ToolsPanel が表示の出し入れとリセットボタンの制御を引き受けてくれます。属性の初期化は undefined で行う、という点だけ意識しておけば、判定まわりで迷うことはほとんどなくなります。

参考ページ