WordPressでエクスパンダーを自作する方法

今回は WordPress のブロックエディタ で使用できる、簡単な折りたたみ機能を持つプラグインを作成してみます。WordPress 6.x 以上をお使いであれば、テキスト セクションに「詳細(Details)」というブロックがあり、簡単な折りたたみ機能なら標準で利用できます。

標準ブロックは便利ですが

  • 開閉時のアニメーションがない
  • デザインをカスタマイズしづらい
  • コードブロックとの相性が悪い場合がある
  • (ブロックの) HTML 構造が変えられない
  • 複数階層の入れ子が不安定になる

といった制限があるようです。

そこで今回は、公式の「ブロックエディタハンドブック」というページにある「チュートリアル」を参考にして、自作のエクスパンダ―・プラグインを作成します。複数階層の入れ子でも安定して動作します。

今回作成するプラグインの仕様は

  • 入れ子対応
  • エディタ側でも入れ子を個別に開閉可能
  • 入れ子の親子関係が干渉しない高さ処理
  • 枠線編集 (枠線の有無、色、太さの編集機能)
  • 開閉時のアニメーション

となっています。

プラグインのひな型を作成します

今回は Windows 版の MAMP 上で VSCode を使用して作成していきます。(WSL2 上でも作成可能ですが、WordPress のプラグインフォルダ (wp-content\plugins) 内に直接作成するので、管理者権限で VSCode を実行する必要があります。)

また、ブロックエディタは内部的に React を使用していて、プラグインのひな型を作成するために「npx」コマンドを使用しますので、こちらのサイト様などを参考にして Node.js もインストールしておきます。

今回の実装環境は以下のようになっています。

  • WordPress・・・バージョン 6.8.3
  • MAMP・・・Windows 版 5.0.6
  • VSCode・・・バージョン 1.106.2
  • npm (npx)・・・バージョン 11.5.1
・プラグインのひな型を作成します

エクスプローラーを起動して、WordPress のプラグインフォルダ (C:\MAMP\htdocs\blog\wp-content\plugins 等) まで移動します。上部のパス入力欄に「cmd」と入力してenterキーを押して、Windows のコマンドプロンプトを表示させます。

表示されたコマンドプロンプト画面に以下のように入力して、プラグインのひな型を作成します。プラグイン名 (my-expander-block 等) はお好みで任意の名称に変更してください。作成途中で確認メッセージが表示されますので「y」を入力してください。インストールが完了するまで少し時間がかかります。

npx @wordpress/create-block@latest my-expander-block

コマンドプロンプト画面で以下のように入力するか、VSCode を起動してエクスプローラーから「my-expander-block」フォルダをドラッグ&ドロップしてプラグインフォルダを開いておきます。

cd my-expander-block
code .

ひな型を作成すると、すでにプラグインとして起動可能になっていますので、WordPress のプラグインメニューから有効化しておきます。

ブロックエディタを起動して、検索欄に「my」と入力すると作成したブロックが表示されます。

以上でひな型の作成は完了です。次のセクションで機能を実装します。

プラグインを実装します

作成したプラグインのフォルダやファイル構成は以下のようになっています。

my-expander-block
├─ build/
├─ node_modules/
├─ src/
│   └─ my-expander-block/
│       ├─ block.json
│       ├─ edit.js
│       ├─ editor.scss
│       ├─ index.js
│       ├─ save.js
│       ├─ style.scss
│       └─ view.js
├─ .editorconfig
├─ .gitignore
├─ my-expander-block.php
├─ package-lock.json
├─ package.json
└─ readme.txt

この中で修正する必要があるファイルは以下の 6 ファイルです。

ファイル役割
block.json属性 (タイトル・中身) などを定義
edit.js編集画面の見た目 & 編集ロジックを定義
editor.scss編集画面でのスタイルを定義
save.jsブラウザーで表示する HTML を定義
style.scssブラウザーでのスタイルを定義
view.jsブラウザーで実行するスクリプトを定義

まず「block.json」ファイルを開いて、14 ~ 20 行目の項目を追加します。この項目でエキスパンダーのタイトル文字列を保持します。

{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 3,
	"name": "create-block/my-expander-block",
	"version": "0.1.0",
	"title": "My Expander Block",
	"category": "widgets",
	"icon": "smiley",
	"description": "Example block scaffolded with Create Block tool.",
	"example": {},
	"supports": {
		"html": false
	},
	"attributes": {
		"title": {
			"type": "string",
			"source": "text",
			"selector": ".my-expander-title"
		}
	},
	"textdomain": "my-expander-block",
	"editorScript": "file:./index.js",
	"editorStyle": "file:./index.css",
	"style": "file:./style-index.css",
	"viewScript": "file:./view.js"
}

次に「edit.js」ファイルを開いて、14 行目に「RichText」と「InnerBlocks」という React コンポーネントのインポート宣言を追加します。さらに、32 行目から始まる React 関数 (Edit) を以下のように修正します。「RichText」コンポーネントでタイトルを表示します。「InnerBlocks」コンポーネントでエクスパンダ―内部に配置された他のブロックを管理します。

/**
 * Retrieves the translation of text.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-i18n/
 */
import { __ } from '@wordpress/i18n';

/**
 * React hook that is used to mark the block wrapper element.
 * It provides all the necessary props like the class name.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops
 */
import { useBlockProps, RichText, InnerBlocks } from '@wordpress/block-editor';

/**
 * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
 * Those files can contain any CSS code that gets applied to the editor.
 *
 * @see https://www.npmjs.com/package/@wordpress/scripts#using-css
 */
import './editor.scss';

/**
 * The edit function describes the structure of your block in the context of the
 * editor. This represents what the editor will render when the block is used.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#edit
 *
 * @return {Element} Element to render.
 */
export default function Edit({ attributes, setAttributes }) {
	const { title } = attributes;
	const blockProps = useBlockProps({
		className: 'my-expander',
	});

	return (
		<div {...blockProps}>
			<div className="my-expander-title">
				<RichText
					tagName="span"
					value={title}
					onChange={(value) => setAttributes({ title: value })}
					placeholder={__('タイトルを入力…', 'my-expander-block')}
				/>
			</div>
			<div className="my-expander-content">
				<InnerBlocks
					placeholder={__('ここにブロックを追加…', 'my-expander-block')}
				// allowedBlocks で許可するブロックを絞ることも可能
				// allowedBlocks={ [ 'core/paragraph', 'core/image' ] }
				/>
			</div>
		</div>
	);
}

「save.js」ファイルを開いて、先ほどと同様に 7 行目にインポート宣言を追加します。さらに 18 行目から始まる React 関数 (save) を以下のように修正します。

/**
 * React hook that is used to mark the block wrapper element.
 * It provides all the necessary props like the class name.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops
 */
import { useBlockProps, RichText, InnerBlocks } from '@wordpress/block-editor';

/**
 * The save function defines the way in which the different attributes should
 * be combined into the final markup, which is then serialized by the block
 * editor into `post_content`.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#save
 *
 * @return {Element} Element to render.
 */
export default function save({ attributes }) {
	const { title } = attributes;
	const blockProps = useBlockProps.save({
		className: 'my-expander',
	});

	return (
		<div {...blockProps}>
			<div className="my-expander-title">
				<RichText.Content tagName="span" value={title} />
			</div>

			<div className="my-expander-content">
				<InnerBlocks.Content />
			</div>
		</div>
	);
}

「view.js」ファイルを開いて、23 行目以降を以下のように修正します。エキスパンダーのタイトル部分がクリックされたら「is-open」というクラス名をトグル動作で追加・削除する動作になっています。さらに、入れ子状態のエキスパンダーが展開された場合は、67 行目の「updateAncestors」関数ですべての親エクスパンダ―の高さを再計算します。また、「querySelector」関数の引数を「':scope > .my-expander-content'」とすることで、クリックされたエクスパンダ―のすぐ下にあるコンテンツ要素を取得するようになっています。

/**
 * Use this file for JavaScript code that you want to run in the front-end
 * on posts/pages that contain this block.
 *
 * When this file is defined as the value of the `viewScript` property
 * in `block.json` it will be enqueued on the front end of the site.
 *
 * Example:
 *
 * ```js
 * {
 *   "viewScript": "file:./view.js"
 * }
 * ```
 *
 * If you're not making any changes to this file because your project doesn't need any
 * JavaScript running in the front-end, then you should delete this file and remove
 * the `viewScript` property from `block.json`.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#view-script
 */

document.addEventListener('DOMContentLoaded', function () {
	// ページ上のすべての my-expander を対象とする
	var items = document.querySelectorAll('.my-expander');

	function updateAncestors(expander, extraHeight) {
		var parent = expander.parentElement;

		while (parent) {
			if (parent.classList && parent.classList.contains('my-expander')) {
				var parentContent = parent.querySelector(':scope > .my-expander-content');
				if (parentContent && parent.classList.contains('is-open')) {
					var maxHeight = parentContent.scrollHeight + extraHeight;
					parentContent.style.maxHeight = maxHeight + 'px';
				}
			}
			parent = parent.parentElement;
		}
	}

	items.forEach(function (item) {
		var title = item.querySelector('.my-expander-title');
		var content = item.querySelector(':scope > .my-expander-content');

		if (!title || !content) return;

		// 初期状態は閉じる
		item.classList.remove('is-open');
		content.style.maxHeight = '0px';

		// タイトルがクリックされたときの処理
		title.addEventListener('click', function () {
			var isOpen = item.classList.contains('is-open');

			if (isOpen) {
				// ---- 自分を閉じる ----
				item.classList.remove('is-open');
				content.style.maxHeight = '0px';
			} else {
				// ---- 自分を開く ----
				item.classList.add('is-open');

				// 中身の高さを取得して、その分だけmax-heightを広げる
				var fullHeight = content.scrollHeight; // px 単位の数値
				content.style.maxHeight = fullHeight + 'px';
				updateAncestors(item, fullHeight);
			}
		});
	});
});

「style.scss」ファイルを開いて、14 行目以降に以下のスタイルを追加します。

/**
 * The following styles get applied both on the front of your site
 * and in the editor.
 *
 * Replace them with your own styles or remove the file completely.
 */

.wp-block-create-block-my-expander-block {
	background-color: #21759b;
	color: #fff;
	padding: 2px;
}

/* 中身のエリア(アコーディオン部分) */
.my-expander-content {
	max-height: 0;
	overflow: hidden;
}

「editor.scss」ファイルを開いて、11 行目あたりに以下のスタイルを追加して、ブロックエディタ内では常に展開されるようにします。

/**
 * The following styles get applied inside the editor only.
 *
 * Replace them with your own styles or remove the file completely.
 */

.wp-block-create-block-my-expander-block {
	border: 1px dotted #f00;
}

.wp-block-create-block-my-expander-block .my-expander-content {
	/* 編集画面では常に見えていてほしいので max-height 制限を外す */
	max-height: none;
	overflow: visible;
	padding-bottom: 0.8em;
}

修正が完了したら、VSCode で Ctrl + @ キーでターミナル画面を開いて、以下のように入力するとプラグインがコンパイルされます。

npm run build

以上で基本的な機能の実装は終了です。ブロックエディタを使用して、「+」ボタンなどで任意のブロックを視覚的に追加できます。

次のセクションでは、スタイルや動作を整えていきます。

スタイルや動作を変更します
・表示スタイルを変更します。

まずは、実際の表示スタイルを変更します。「style.scss」ファイルを開いて以下のように修正します。(面倒くさい場合は丸ごとコピーしてください。)

/**
 * The following styles get applied both on the front of your site
 * and in the editor.
 *
 * Replace them with your own styles or remove the file completely.
 */

.wp-block-create-block-my-expander-block {
	background-color: #fff;
	color: black;
	padding: 2px;
	border: 1px solid #ddd;
	border-radius: 4px;
}

.my-expander-title {
	position: relative;
	cursor: pointer;
	padding-left: 1.6em; /* ← 左側に三角形のスペースを作る */
}

/* ▼ 閉じているときの三角形(▶:右向き) */
.my-expander-title::after {
	content: "";
	position: absolute;
	left: 0.4em; /* 左側に配置 */
	top: 50%;
	transform: translateY(-50%);
	width: 0;
	height: 0;
	border-style: solid;
	border-width: 6px 0 6px 10px; /* ▶ を作る border */
	border-color: transparent transparent transparent #333;
}

/* ▼ 開いているときの三角形(▼:下向きに回転) */
.my-expander.is-open > .my-expander-title::after {
	transform: translateY(-50%) rotate(90deg); /* ▶ を 90° 回転して ▼ */
}

/* 中身のエリア(アコーディオン部分) */
.my-expander-content {
	max-height: 0;
	overflow: hidden;
}

この修正で

  • 背景色が白に
  • 角丸が付いたグレーの枠線
  • タイトルの左側に「▶」マークの表示 (展開時は回転)

などの機能が追加できました。

・編集時も折りたためるように修正します

現在の機能では、ブロックエディタ画面では常に展開されるようになっているので、この画面でも折りたためるように修正します。

まずは「edit.js」ファイルを開いて以下のように修正します。修正内容は useState という React の「フック関数」と呼ばれる関数を使用して「isOpen」ステート変数で現在の状態を保持します。(35 行目) タイトルがクリックされる度にこのフラグを反転して、フラグが true の場合は、エディタ内のエクスパンダ―エレメントのクラスに「is-open」を追加します。(37 行目) また、それぞれのエクスパンダ―のコンテンツの表示スタイルも動的に変更します。15 行目のインポートも忘れないでください。

/**
 * Retrieves the translation of text.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-i18n/
 */
import { __ } from '@wordpress/i18n';

/**
 * React hook that is used to mark the block wrapper element.
 * It provides all the necessary props like the class name.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops
 */
import { useBlockProps, RichText, InnerBlocks } from '@wordpress/block-editor';
import { useState } from '@wordpress/element';

/**
 * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
 * Those files can contain any CSS code that gets applied to the editor.
 *
 * @see https://www.npmjs.com/package/@wordpress/scripts#using-css
 */
import './editor.scss';

/**
 * The edit function describes the structure of your block in the context of the
 * editor. This represents what the editor will render when the block is used.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#edit
 *
 * @return {Element} Element to render.
 */
export default function Edit({ attributes, setAttributes }) {
	const { title } = attributes;
	const [isOpen, setIsOpen] = useState(true);
	const blockProps = useBlockProps({
		className: `my-expander${isOpen ? ' is-open' : ''}`,
	});

	const onToggle = () => {
		setIsOpen((prev) => !prev);
	};

	// エディタ用:ここで表示/非表示を制御
	const contentStyle = isOpen
		? { maxHeight: 'none', overflow: 'visible', display: 'block' }
		: { maxHeight: 0, overflow: 'hidden', display: 'none' };

	return (
		<div {...blockProps}>
			<div className="my-expander-title" onClick={onToggle}>
				<RichText
					tagName="span"
					value={title}
					onChange={(value) => setAttributes({ title: value })}
					placeholder={__('タイトルを入力…', 'my-expander-block')}
				/>
			</div>
			<div className="my-expander-content" style={contentStyle}>
				<InnerBlocks
					placeholder={__('ここにブロックを追加…', 'my-expander-block')}
				// allowedBlocks で許可するブロックを絞ることも可能
				// allowedBlocks={ [ 'core/paragraph', 'core/image' ] }
				/>
			</div>
		</div>
	);
}

「editor.scss」ファイルを開いて、11 行目以降を削除またはコメントアウトします。修正が完了したらVSCode のコンソール画面で「npm run build」と入力してプラグインをコンパイルしておきます。

/**
 * The following styles get applied inside the editor only.
 *
 * Replace them with your own styles or remove the file completely.
 */

.wp-block-create-block-my-expander-block {
	border: 1px dotted #f00;
}

// .wp-block-create-block-my-expander-block .my-expander-content {
// 	/* 編集画面では常に見えていてほしいので max-height 制限を外す */
// 	max-height: none;
// 	overflow: visible;
// 	padding-bottom: 0.8em;
// }

以上の設定で、ブロックエディタでも折りたためるようになりました。

・アニメーションを追加します

「style.scss」ファイルを開いて、34 行目と 46 行目に以下のスタイルを追加します。

/**
 * The following styles get applied both on the front of your site
 * and in the editor.
 *
 * Replace them with your own styles or remove the file completely.
 */

.wp-block-create-block-my-expander-block {
	background-color: #fff;
	color: black;
	padding: 2px;
	border: 1px solid #ddd;
	border-radius: 4px;
}

.my-expander-title {
	position: relative;
	cursor: pointer;
	padding-left: 1.6em; /* ← 左側に三角形のスペースを作る */
}

/* ▼ 閉じているときの三角形(▶:右向き) */
.my-expander-title::after {
	content: "";
	position: absolute;
	left: 0.4em; /* 左側に配置 */
	top: 50%;
	transform: translateY(-50%);
	width: 0;
	height: 0;
	border-style: solid;
	border-width: 6px 0 6px 10px; /* ▶ を作る border */
	border-color: transparent transparent transparent #333;
	transition: transform 0.4s ease; /* ▶マーク回転のアニメーション */
}

/* ▼ 開いているときの三角形(▼:下向きに回転) */
.my-expander.is-open > .my-expander-title::after {
	transform: translateY(-50%) rotate(90deg); /* ▶ を 90° 回転して ▼ */
}

/* 中身のエリア(アコーディオン部分) */
.my-expander-content {
	max-height: 0;
	overflow: hidden;
	transition: max-height 0.4s ease-in-out; /* max-height の変化をアニメーションさせる */
}

アニメーション速度は以下のような目安になっています。

速度
速め0.2s
標準0.4s ~ 0.6s
ゆっくり0.8s ~ 1.0s

入れ子になったエクスパンダ―のコンテンツサイズが大きい場合、親のエクスパンダ―を閉じるときにアニメーションが効かない場合があるようなので、60 ~ 67 行目に子エクスパンダ―が閉じたらすべての親エクスパンダ―のサイズを再計算する処理を追加します。またその際にテーマなどのレイアウトが崩れる場合があるので、42 ~ 48 行目の関数で「resize」イベントを発生させて、ブラウザーやテーマのレイアウトを更新しています。

/**
 * Use this file for JavaScript code that you want to run in the front-end
 * on posts/pages that contain this block.
 *
 * When this file is defined as the value of the `viewScript` property
 * in `block.json` it will be enqueued on the front end of the site.
 *
 * Example:
 *
 * ```js
 * {
 *   "viewScript": "file:./view.js"
 * }
 * ```
 *
 * If you're not making any changes to this file because your project doesn't need any
 * JavaScript running in the front-end, then you should delete this file and remove
 * the `viewScript` property from `block.json`.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#view-script
 */

document.addEventListener('DOMContentLoaded', function () {
	// ページ上のすべての my-expander を対象とする
	var items = document.querySelectorAll('.my-expander');

	function updateAncestors(expander, extraHeight) {
		var parent = expander.parentElement;

		while (parent) {
			if (parent.classList && parent.classList.contains('my-expander')) {
				var parentContent = parent.querySelector(':scope > .my-expander-content');
				if (parentContent && parent.classList.contains('is-open')) {
					var maxHeight = parentContent.scrollHeight + extraHeight;
					parentContent.style.maxHeight = maxHeight + 'px';
				}
			}
			parent = parent.parentElement;
		}
	}

	function triggerRelayout() {
		// レイアウト変更が反映された直後に発火させる
		requestAnimationFrame(function () {
			// resize イベントでブラウザとテーマのレイアウトを更新
			window.dispatchEvent(new Event('resize'));
		});
	}

	items.forEach(function (item) {
		var title = item.querySelector('.my-expander-title');
		var content = item.querySelector(':scope > .my-expander-content');

		if (!title || !content) return;

		// 初期状態は閉じる
		item.classList.remove('is-open');
		content.style.maxHeight = '0px';

		// max-height アニメーションが終わったら親の高さを更新
		content.addEventListener('transitionend', function (e) {
			if (e.propertyName !== 'max-height') return;
			if (!item.classList.contains('is-open')) {
				updateAncestors(item, 0);
				triggerRelayout();
			}
		});

		// タイトルがクリックされたときの処理
		title.addEventListener('click', function () {
			var isOpen = item.classList.contains('is-open');

			if (isOpen) {
				// ---- 自分を閉じる ----
				item.classList.remove('is-open');
				content.style.maxHeight = '0px';
			} else {
				// ---- 自分を開く ----
				item.classList.add('is-open');

				// 中身の高さを取得して、その分だけmax-heightを広げる
				var fullHeight = content.scrollHeight; // px 単位の数値
				content.style.maxHeight = fullHeight + 'px';
				updateAncestors(item, fullHeight);
			}
		});
	});
});

以上で終了です。コンソール画面に「npm run build」と入力して、プラグインをコンパイルしておきます。次のセクションでは、ブロックエディタ画面でスタイルや動作を変更できるように修正していきます。

ブロックエディタで設定を変えられるように修正します

設定を変更するたびに毎回コンパイルする訳にはいかないので、ブロックエディタの右側にある設定欄でスタイルや動作を変更できるように修正していきます。とりあえずは枠線の有無と線の色と太さを変更できるようにします。

まず「block.json」ファイルを開いて、20 ~ 31 行目に以下の設定を追加します。この設定で「枠線の有無」、「線の色」、「線の太さ」を管理します。

{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 3,
	"name": "create-block/my-expander-block",
	"version": "0.1.0",
	"title": "My Expander Block",
	"category": "widgets",
	"icon": "smiley",
	"description": "Example block scaffolded with Create Block tool.",
	"example": {},
	"supports": {
		"html": false
	},
	"attributes": {
		"title": {
			"type": "string",
			"source": "text",
			"selector": ".my-expander-title"
		},
		"borderEnabled": {
			"type": "boolean",
			"default": true
		},
		"borderColor": {
			"type": "string",
			"default": "#dddddd"
		},
		"borderWidth": {
			"type": "number",
			"default": 1
		}
	},
	"textdomain": "my-expander-block",
	"editorScript": "file:./index.js",
	"editorStyle": "file:./index.css",
	"style": "file:./style-index.css",
	"viewScript": "file:./view.js"
}

次に「edit.js」ファイルを開いて以下のように修正します。(大変なので丸ごとコピーした方がいいかもしれません。) おおまかにご説明すると、「InspectorControls」という React コンポーネントを使用して右側にある編集用の画面を組み立てて、「block.json」で定義されているプロパティと結びつけています。詳しく知りたい方は、こちらの「公式サイト」またはこちらのサイト様をご参照ください。

/**
 * Retrieves the translation of text.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-i18n/
 */
import { __ } from '@wordpress/i18n';

/**
 * React hook that is used to mark the block wrapper element.
 * It provides all the necessary props like the class name.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops
 */
import { useBlockProps, RichText, InnerBlocks, InspectorControls } from '@wordpress/block-editor';
import { useState } from '@wordpress/element';
import { PanelBody, ToggleControl, RangeControl, ColorPalette } from '@wordpress/components';

/**
 * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
 * Those files can contain any CSS code that gets applied to the editor.
 *
 * @see https://www.npmjs.com/package/@wordpress/scripts#using-css
 */
import './editor.scss';

const COLORS = [
	{ name: '薄いグレー', color: '#dddddd' },
	{ name: '黒', color: '#000000' },
	{ name: '青', color: '#0073aa' },
	{ name: 'オレンジ', color: '#ff9900' },
];

/**
 * The edit function describes the structure of your block in the context of the
 * editor. This represents what the editor will render when the block is used.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#edit
 *
 * @return {Element} Element to render.
 */
export default function Edit({ attributes, setAttributes }) {
	const { title, borderEnabled, borderColor, borderWidth } = attributes;
	const [isOpen, setIsOpen] = useState(true);
	// 枠線スタイル(有効なときだけ付ける)
	const style =
		borderEnabled
			? {
				border: `${borderWidth || 1}px solid ${borderColor || '#dddddd'}`,
				borderRadius: '4px',
			}
			: { border: 'none' };
	const blockProps = useBlockProps({
		className: `my-expander${isOpen ? ' is-open' : ''}`,
		style,
	});

	const onToggle = () => {
		setIsOpen((prev) => !prev);
	};

	// エディタ用:ここで表示/非表示を制御
	const contentStyle = isOpen
		? { maxHeight: 'none', overflow: 'visible', display: 'block' }
		: { maxHeight: 0, overflow: 'hidden', display: 'none' };

	return (
		<>
			<InspectorControls>
				<PanelBody title="枠線の設定" initialOpen={true}>
					<ToggleControl
						label="枠線を表示する"
						checked={borderEnabled}
						onChange={(value) => setAttributes({ borderEnabled: value })}
					/>
					{borderEnabled && (
						<>
							<p>枠線の色</p>
							<ColorPalette
								colors={COLORS}
								value={borderColor}
								onChange={(color) => setAttributes({ borderColor: color })}
							/>
							<RangeControl
								label="枠線の太さ(px)"
								value={borderWidth}
								onChange={(value) => setAttributes({ borderWidth: value })}
								min={0}
								max={10}
							/>
						</>
					)}
				</PanelBody>
			</InspectorControls>

			<div {...blockProps}>
				<div className="my-expander-title" onClick={onToggle}>
					<RichText
						tagName="span"
						value={title}
						onChange={(value) => setAttributes({ title: value })}
						placeholder={__('タイトルを入力…', 'my-expander-block')}
					/>
				</div>
				<div className="my-expander-content" style={contentStyle}>
					<InnerBlocks
						placeholder={__('ここにブロックを追加…', 'my-expander-block')}
					// allowedBlocks で許可するブロックを絞ることも可能
					// allowedBlocks={ [ 'core/paragraph', 'core/image' ] }
					/>
				</div>
			</div>
		</>
	);
}

次に「save.js」ファイルを開いて以下のように修正します。

/**
 * React hook that is used to mark the block wrapper element.
 * It provides all the necessary props like the class name.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops
 */
import { useBlockProps, RichText, InnerBlocks } from '@wordpress/block-editor';

/**
 * The save function defines the way in which the different attributes should
 * be combined into the final markup, which is then serialized by the block
 * editor into `post_content`.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#save
 *
 * @return {Element} Element to render.
 */
export default function save({ attributes }) {
	const { title, borderEnabled, borderColor, borderWidth } = attributes;
	const style =
		borderEnabled
			? {
				border: `${borderWidth || 1}px solid ${borderColor || '#dddddd'}`,
				borderRadius: '4px',
			}
			: { border: 'none' };
	const blockProps = useBlockProps.save({
		className: 'my-expander',
		style,
	});

	return (
		<div {...blockProps}>
			<div className="my-expander-title">
				<RichText.Content tagName="span" value={title} />
			</div>

			<div className="my-expander-content">
				<InnerBlocks.Content />
			</div>
		</div>
	);
}

以上で修正は終了です。VSCode のコンソール画面で「npm run build」と入力してプラグインをコンパイルします。WordPress のブロックエディタ画面を再読み込みすると機能が有効になります。(「無効なコンテンツが含まれています。」などのエラーが発生する場合は、「復旧を試みる」ボタンを押すと直ると思われます。)

ブラウザー画面での表示も問題ないようです。

プラグインのインストール方法

今回作成したプラグインは、こちらからダウンロードできます。WordPress のプラグインフォルダ (C:\MAMP\htdocs\blog\wp-content\plugins 等) に展開してから、WordPress のプラグイン画面で有効化してください。

WSL2 で使用する場合は、プラグインフォルダに展開してからファイル等の所有者を変更してください。

sudo chown -R www-data:www-data ./my-expander-block

プラグインを自分で変更したい場合は、VSCode でプラグインフォルダを開いてコンソール画面に以下のように入力して npm パッケージを復元してください。(ローカル環境で行った方が安全です。)

npm install

インストールする場合は、最低限以下のフォルダとファイルがあれば動作します。(node_modules/ フォルダ等のバイナリを含むフォルダをアップロードしないでください。)

my-expander-block
├─ build/
└─ my-expander-block.php

プラグインのコードはご自由に改変してご利用ください。
なお、WordPress のバージョンアップ(現在は 6.9)などにより動作が変わる可能性があります。実際にご使用の際は、自己責任でお願いいたします。

クリックで展開します (自作プラグイン版)

見出し

  • test1
  • test2
  • test3
コードサンプル
npm run build

test1
  • item1
test2
  • item2
test3
  • item3
test4
  • item4

以上です。よろしかったらお試しください。

更新履歴

  • 2025/12/01:長いコンテンツが見切れる現象を修正しました。
  • 2025/12/03:エクスパンダ―を入れ子にできるように修正しました。
  • 2025/12/04:折りたたんだ時にテーマなどのレイアウトが崩れる現象を修正しました。