1. ホーム
  2. WordPress

【WordPress】BlockControls と ToolbarButton の使い方|カスタムブロックのツールバーにボタンを追加する

Share

ブロックエディターでブロックを選ぶと、そのブロックの上に小さなツールバーが出てきます。段落ブロックなら太字や配置、画像ブロックならトリミングなどが並ぶ、あの帯の部分です。自作ブロックにもここへボタンを足すことができ、その入口になるのが BlockControlsToolbarButton です。この記事では、ツールバーとサイドバーの使い分けから、基本の書き方、属性をトグルするボタンの完成コード、ボタンが出てこないときの原因までを順に解説します。

ツールバーに置くか、サイドバーに置くか

ブロックの設定 UI を差し込める場所は、大きく2か所あります。ブロックのすぐ上に出るツールバーと、画面右側に開く設定サイドバーです。前者を担当するのが BlockControls、後者を担当するのが InspectorControls で、どちらも @wordpress/block-editor から読み込みます。

この2つは機能が重なっているわけではなく、置くべきものが違います。ツールバーは常に目に入る場所にあり、クリック1回で結果が変わるので、執筆中に何度も切り替える操作――配置の変更、強調のオン・オフ、表示モードの切り替えなど――に向いています。一方サイドバーは、開かないと見えない代わりに広い面積を使えるので、一度決めたらあまり触らない細かい設定――色、余白、リンクの開き方、表示件数といった数値入力――を置く場所です。

ツールバーは横一列しかなく、ボタンを増やすほど窮屈になります。目安として、ツールバーに置くのはそのブロックで「よく使う操作」を2〜3個までにとどめ、残りはサイドバーへ回すと使いやすい UI になります。

BlockControls は edit の返り値に入れるだけでよい

BlockControls の使い方で最初に戸惑いやすいのが、書く場所と表示される場所が一致しない点です。BlockControls はブロックの edit が返す JSX の中に書きますが、実際に描画されるのはブロックの中ではなく、エディター上部のツールバー領域です。

これは Slot/Fill と呼ばれる仕組みによるものです。エディター側があらかじめ「ここに差し込める」という受け口(Slot)をツールバーに用意していて、BlockControls はそこへ中身を送り込む差込口(Fill)として働きます。そのため、開発者は座標や DOM の位置を気にする必要がなく、edit の返り値に BlockControls を含めるだけで、選択中のブロックのツールバーに内容が現れます。

差し込まれるのは、そのブロックが選択されている間だけです。選択を外せば自動的に消えるので、表示・非表示を自分で制御する必要はありません。

ToolbarGroup と ToolbarButton で1つボタンを置く

最小の構成は「BlockControls の中に ToolbarGroup、その中に ToolbarButton」の3階層です。ToolbarGroup はボタンをまとめる箱で、これで包むとボタンの間に区切り線が入り、コアのブロックと同じ見た目に揃います。ToolbarGroupToolbarButton@wordpress/components のコンポーネントなので、BlockControls とは import 元が違う点に注意してください。

edit.js
import { __ } from '@wordpress/i18n';
import { useBlockProps, BlockControls } from '@wordpress/block-editor';
import { ToolbarGroup, ToolbarButton } from '@wordpress/components';
import { starFilled } from '@wordpress/icons';

export default function Edit() {
    const blockProps = useBlockProps();

    return (
        <>
            <BlockControls>
                <ToolbarGroup>
                    <ToolbarButton
                        icon={ starFilled }
                        label={ __( '強調する', 'webool' ) }
                        isPressed={ false }
                        onClick={ () => console.log( 'clicked' ) }
                    />
                </ToolbarGroup>
            </BlockControls>
            <p { ...blockProps }>ブロックの中身</p>
        </>
    );
}

ポイントは、edit が返す JSX の一番外側をフラグメント(<>...</>)にしていることです。BlockControls とブロック本体という2つの要素を並べて返す必要があるため、どちらか一方だけを返す形になっていると、ツールバー側かブロック側のどちらかが消えてしまいます。

アイコンは @wordpress/icons パッケージから読み込むのが手軽です。starFilledchevronDown のように名前付きでエクスポートされているほか、icon="star-filled" のように Dashicons の名前を文字列で渡すこともできます。label は目には見えませんが、スクリーンリーダー向けの aria-label とマウスオーバー時のツールチップに使われるので、必ず付けておきましょう。

ToolbarButton に渡せる主なプロパティ

ToolbarButton は内部で Button コンポーネントを使っており、Button と同じプロパティをそのまま受け取れます。よく使うものは次のとおりです。

プロパティ説明
iconボタンに表示するアイコン。@wordpress/icons のアイコンや Dashicons の名前(文字列)を渡す
labelボタンの説明文。aria-label とツールチップの文言になる
titlelabel と同じ役割の別名。既存のブロックのコードではこちらもよく使われている
isPressedtrue のとき「押されている」見た目になる。isActive を渡しても同じ扱いになる
disabledtrue でボタンを押せなくする。古い isDisabled は内部で disabled に読み替えられる
onClickクリックされたときに実行する関数
shortcutツールチップに併記するキーボードショートカットの表示文字列
children子要素にテキストを渡すと、アイコンではなく文字ラベルのボタンになる
containerClassNameボタンを包む要素に追加するクラス名

文字だけのボタンにしたいときは icon を省略し、子要素にテキストを書きます。ただしツールバーは横幅が限られるので、文字ラベルは短い語に絞るか、アイコン+label の組み合わせにしておくのが無難です。

edit.js(文字ラベルのボタン)
<ToolbarButton onClick={ handleReset }>リセット</ToolbarButton>

group プロパティで表示される位置が変わる

BlockControls には group というプロパティがあり、ツールバーのどのエリアに差し込むかを指定できます。省略した場合は default です。

group の値差し込まれる場所
default既定値。そのブロック固有のコントロールが並ぶエリア
blockブロックの種類を切り替えるブロックスイッチャーの隣。配置など、ブロック共通の操作が並ぶエリア
inline太字やリンクなど、選択した文字列に対する書式ボタンが並ぶエリア
other上のどれにも当てはまらないコントロール用のエリア

見た目の違いだけでなく、扱いにも差があります。group を省略した default のときは中身が自動的に ToolbarGroup で包まれますが、block などを指定したときは包まれません。group="block" を使うのは配置コントロールのような既製のコンポーネントを置くときが多く、それらは自前でグループを持っているため、この挙動で困ることはほとんどありません。自作のボタンを default 以外のグループに置く場合だけ、自分で ToolbarGroup を書く必要があると覚えておけば十分です。

配置コントロールなど既製のコンポーネントを載せる

ツールバーに置くものは、自作のボタンだけとは限りません。コアが用意しているコントロールをそのまま差し込めば、コアのブロックとまったく同じ操作感を数行で用意できます。代表的なのがテキストの配置を切り替える AlignmentControl です(以前からある AlignmentToolbar も同じ用途のコンポーネントで、どちらも @wordpress/block-editor から読み込めます)。

edit.js(配置コントロール)
import {
    useBlockProps,
    BlockControls,
    AlignmentControl,
} from '@wordpress/block-editor';

export default function Edit( { attributes, setAttributes } ) {
    const { textAlign } = attributes;

    // 配置に応じてコア互換のクラスを付ける
    const blockProps = useBlockProps( {
        className: textAlign ? `has-text-align-${ textAlign }` : undefined,
    } );

    return (
        <>
            <BlockControls group="block">
                <AlignmentControl
                    value={ textAlign }
                    onChange={ ( nextAlign ) =>
                        setAttributes( { textAlign: nextAlign } )
                    }
                />
            </BlockControls>
            <p { ...blockProps }>ブロックの中身</p>
        </>
    );
}

value に現在の値、onChange に更新用の関数を渡すだけで、左・中央・右の切り替え UI が手に入ります。textAlign は文字列型の属性として block.json に定義しておき、未選択のときは値なし(undefined)になる点に注意してください。なお、ブロック全体を「幅広」「全幅」にする配置は別のコントロールで、そちらは BlockAlignmentControl が担当します。

実践例:ハイライトを切り替えるボタンを作る

ここまでの内容を使って、テキストを強調表示するかどうかをツールバーから切り替えるブロックを作ってみます。真偽値の属性 isHighlighted を用意し、ボタンで反転させ、その状態を isPressed で見せて、保存する HTML のクラスにも反映させる、という流れです。

属性を block.json に定義する

まず、切り替えの状態をどこに保存するかを決めます。ブロックが持つ値は block.jsonattributes に書き、真偽値なので "type": "boolean"、初期値は false にしておきます。

block.json
{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "webool/notice",
  "title": "お知らせ",
  "category": "text",
  "icon": "megaphone",
  "textdomain": "webool",
  "attributes": {
    "content": {
      "type": "string",
      "source": "html",
      "selector": "p",
      "default": ""
    },
    "isHighlighted": {
      "type": "boolean",
      "default": false
    }
  },
  "editorScript": "file:./index.js",
  "style": "file:./style-index.css"
}

edit.js でボタンと状態をつなぐ

編集画面側では、attributes から現在の値を取り出してボタンの isPressed に渡し、onClicksetAttributes を呼んで反転させます。この「読む」と「書く」が両方そろって、はじめて押下状態が正しく動きます。

edit.js
import { __ } from '@wordpress/i18n';
import {
    useBlockProps,
    BlockControls,
    RichText,
} from '@wordpress/block-editor';
import { ToolbarGroup, ToolbarButton } from '@wordpress/components';
import { starFilled } from '@wordpress/icons';

export default function Edit( { attributes, setAttributes } ) {
    const { content, isHighlighted } = attributes;

    // 編集画面でも保存後と同じクラスを付けて見た目を合わせる
    const blockProps = useBlockProps( {
        className: isHighlighted ? 'is-highlighted' : undefined,
    } );

    return (
        <>
            <BlockControls>
                <ToolbarGroup>
                    <ToolbarButton
                        icon={ starFilled }
                        label={ __( 'ハイライト', 'webool' ) }
                        isPressed={ isHighlighted }
                        onClick={ () =>
                            setAttributes( { isHighlighted: ! isHighlighted } )
                        }
                    />
                </ToolbarGroup>
            </BlockControls>

            <div { ...blockProps }>
                <RichText
                    tagName="p"
                    value={ content }
                    onChange={ ( value ) => setAttributes( { content: value } ) }
                    placeholder={ __( 'テキストを入力', 'webool' ) }
                />
            </div>
        </>
    );
}

isPressed={ isHighlighted } と書いているので、属性が true の間はボタンが押し込まれた表示になり、いま強調が有効かどうかが一目で分かります。ここを固定値のままにしたり、コンポーネント内の useState で持ったりすると、見た目は変わっても保存内容とずれてしまうので、必ず属性と結び付けてください。

save.js でクラスを出し分ける

保存側では useBlockProps.save() に同じ条件でクラス名を渡します。編集画面と保存後で同じクラスが付くので、CSS も1つで済みます。

save.js
import { useBlockProps, RichText } from '@wordpress/block-editor';

export default function save( { attributes } ) {
    const { content, isHighlighted } = attributes;

    const blockProps = useBlockProps.save( {
        className: isHighlighted ? 'is-highlighted' : undefined,
    } );

    return (
        <div { ...blockProps }>
            <RichText.Content tagName="p" value={ content } />
        </div>
    );
}

あとは .is-highlighted に CSS を当てれば完成です。block.jsonstyle で指定したスタイルシートは編集画面とフロントの両方で読み込まれるため、切り替えた結果が編集中にもそのまま見えます。

style.scss
.wp-block-webool-notice.is-highlighted {
  background-color: #fff7d6;
  border-left: 4px solid #f0b429;
  padding: 16px;
}

1つだけ注意点があります。すでに記事内で使われているブロックの save を書き換えると、保存済みの HTML と新しい save の出力が食い違い、「このブロックには、予期しないか無効なコンテンツが含まれています」という検証エラーになります。公開済みのブロックに後からこの仕組みを足すときは、deprecated に旧バージョンの save を登録してから変更してください。

選択肢が多いときは ToolbarDropdownMenu にまとめる

切り替えたい候補が3つ4つとある場合、ボタンを横に並べるとツールバーがすぐ一杯になります。そんなときは ToolbarDropdownMenu を使い、1つのアイコンを押すと候補が縦に開く形にまとめます。controls に候補を配列で渡すだけで、メニューが組み立てられます。

edit.js(ドロップダウン)
import { __ } from '@wordpress/i18n';
import { BlockControls } from '@wordpress/block-editor';
import { ToolbarGroup, ToolbarDropdownMenu } from '@wordpress/components';
import { info, warning, chevronDown } from '@wordpress/icons';

// noticeType は文字列型の属性
<BlockControls>
    <ToolbarGroup>
        <ToolbarDropdownMenu
            icon={ chevronDown }
            label={ __( '種類を選ぶ', 'webool' ) }
            controls={ [
                {
                    title: __( 'お知らせ', 'webool' ),
                    icon: info,
                    isActive: noticeType === 'info',
                    onClick: () => setAttributes( { noticeType: 'info' } ),
                },
                {
                    title: __( '注意', 'webool' ),
                    icon: warning,
                    isActive: noticeType === 'warning',
                    onClick: () => setAttributes( { noticeType: 'warning' } ),
                },
            ] }
        />
    </ToolbarGroup>
</BlockControls>

各項目の isActive に「いま選ばれているか」を渡しておくと、メニューを開いたときに現在の選択肢にチェックが付きます。ToolbarButtonisPressed と同じ考え方で、状態を属性から読んで渡すのがコツです。

ボタンがツールバーに表示されないとき

コードを書いたのにツールバーに何も増えない、という場合、原因はだいたい次の3つのどれかです。

edit の返り値に含まれていない

BlockControls は「返して初めて差し込まれる」コンポーネントです。edit の中で変数に代入しただけだったり、条件分岐の都合で返らない経路に入っていたりすると、当然ながらツールバーには何も出ません。return の中に BlockControls が入っているかをまず確認してください。

次に多いのが、フラグメントを使わずに要素を返そうとしているケースです。edit は1つの要素しか返せないため、BlockControls とブロック本体を並べるときは全体を <>...</> で包む必要があります。ここを忘れて BlockControls をブロック本体の外に書くと、JSX の構文エラーになるか、片方だけを返して見た目が壊れます。

import 元のパッケージを取り違えている

この記事で扱ったコンポーネントは、パッケージが2つに分かれています。BlockControlsAlignmentControl@wordpress/block-editorToolbarGroup / ToolbarButton / ToolbarDropdownMenu@wordpress/components です。逆に書くと undefined が読み込まれ、ブロック全体が描画されなくなります。ブラウザのコンソールに「Element type is invalid」のようなエラーが出ていたら、まず import 行を疑ってください。

あわせて、ビルド時の依存指定も確認します。@wordpress/scripts でビルドしていれば、これらの import は自動的に wp-block-editor / wp-components への依存として index.asset.php に書き出され、block.jsoneditorScript 経由で正しく読み込まれます。自前でビルド設定を組んでいる場合は、依存が抜けていないかを見直しましょう。

古いブロックの JS が残っている

ビルドを回し忘れている、あるいはブラウザやキャッシュ系プラグインが古い JavaScript を配信している、という単純な原因もよくあります。開発中は npm run start でウォッチしておき、それでも変わらないときはスーパーリロードでキャッシュを飛ばして確認してください。

押しても状態が変わって見えないとき

ボタンは出ているのに、クリックしても押されている感じがしない――この症状はほぼ isPressed の渡し方が原因です。

isPressed に属性ではない値を渡していると、クリックで属性は変わっても見た目が追従しません。isPressed={ isHighlighted } のように、表示に使う値と保存する値を同じものにするのが原則です。コンポーネント内の useState で状態を持つのも避けてください。見た目はそれらしく動きますが、その値は保存されないので、ページを再読み込みすると元に戻ってしまいます。

逆に、押下状態は変わるのに保存されない場合は、onClick の中で setAttributes を呼べているかを確認します。setAttributes は変更したいキーだけを含むオブジェクトを渡す関数なので、setAttributes( { isHighlighted: ! isHighlighted } ) のように書きます。属性名のつづりが block.json の定義と1文字でも違うと、未定義の属性として扱われ保存されません。

まとめ

BlockControls は、ブロック選択時に上へ出るツールバーへ独自のコントロールを差し込むためのコンポーネントです。edit の返り値に含めるだけで、Slot/Fill の仕組みによってツールバー上に描画されます。中身は ToolbarGroup でまとめ、ToolbarButtonicon / label / isPressed / onClick を渡すのが基本形です。isPressed には必ず属性の値を渡し、onClicksetAttributes を呼んで更新する、という往復を作れば、押下状態と保存内容がずれません。選択肢が増えたら ToolbarDropdownMenu に、細かい設定は InspectorControls のサイドバーに逃がす――この使い分けができれば、コアのブロックと違和感のない操作感の自作ブロックになります。

参考ページ