[theme.json] settings.border で枠線の設定を行う

テーマディレクトリのルートに配置する theme.json により全体としてブロックの枠線をサポートをどのように設定するか、またはブロックごとにどのように設定するかを定義することができる。

【環境】
WordPress 5.8.1
Gutenberg 11.7.1

枠線のサポートの有効/無効

テーマディレクトリのルートの theme.json で border セクションの customColor, customRadius, customStyle, customWidth の値を true とする事で枠線の色、角丸、スタイル、幅がサポートされる。

{
	"version": 1,
	"settings": {
		"border": {
			"customColor": true,
			"customRadius": true,
			"customStyle": true,
			"customWidth": true
		}
	}
}
グループブロックに枠線(色、角丸、スタイル、幅)がサポートされる

値を false としたものは枠線がサポートされない。
以下では customStyle 以外を false としたためブロックでは枠線(スタイル)のみのサポートとなる。

{
	"version": 1,
	"settings": {
		"border": {
			"customColor": false,
			"customRadius": false,
			"customStyle": true,
			"customWidth": false
		}
	}
}
customStyle 以外を false としたため枠線(スタイル)のみサポートされる

ブロックセレクタによる上書き

settings セクションのサブセクションとしてブロックセレクタ(例:core/group )を用いる事ができ、先の設定を上書きし無効もしくは有効にできる。

{
	"version": 1,
	"settings": {
		"blocks": {
			"core/group": {
				"border": {…},
				"color": {…},
				︙
			}
		}
	}
}

例えば全体としてはブロックに枠線(色、角丸、スタイル、幅)をサポートさせるがグループブロックでは色、角丸は無効とする場合このような記述となる。

{
	"version": 1,
	"settings": {
		"border": {
			"customColor": true,
			"customRadius": true,
			"customStyle": true,
			"customWidth": true
		}
		"blocks": {
			"core/group": {
				"border": {
					"customColor": false,
					"customRadius": false
				}
			}
		}
	}
}

コアとテーマの theme.json

theme.json はコア(wp-includes/theme.json)とテーマディレクトリのルート(wp-content/themes/my-theme/theme.json)が存在し後者で前者を上書きできる。
ただし、以下の記述だけではボタンブロックで枠線(角丸)がサポートされてしまう。

{
	"version": 1,
	"settings": {
		"border": {
			"customColor": false,
			"customRadius": false,
			"customStyle": false,
			"customWidth": false
		}
	}
}
customRadius を false にしてもボタンブロックで枠線(角丸)がサポートされる

これはコア側にブロックセレクタ core/button が記述されておりブロックセレクタが settings/block の設定を上書きしたためとなる。
ボタンブロックで枠線(角丸)のサポートを無効にするにはテーマ側の theme.json のブロックセレクタ core/button に無効にする旨を記述する必要がある。

{
	"version": 1,
	"settings": {
		"blocks": {
			"core/button": {
				"border": {
					"customRadius": false
				}
			}
		}
	}
}

ブロッグ毎の色、角丸、スタイル、幅のサポートの調べ方

色、角丸、スタイル、幅のサポートはブロック毎に異なる。
執筆段階で枠線はグループ、テーブル、ボタン、画像、検索、プルクォートブロックで用いられており、これらのサポート状況は以下の通りとなる。

グループ: 色、角丸、スタイル、幅
テーブル:色、スタイル、幅
ボタン:角丸
画像:角丸
検索:色、角丸
プルクォート: 色、角丸、スタイル、幅

であるのでブロックセレクタ core/buttoncustomColortrue としてもボタンブロックで色はサポートされない。

{
	"version": 1,
	"settings": {
		"blocks": {
			"core/button": {
				"border": {
					"customColor": true
				}
			}
		}
	}
}
customColor を true としても枠線(色)はサポートされない

個々のブロックが色、角丸、スタイル、幅のいずれをサポートしているかは WordPress 内 wp-includes/blocks/block-name/block.json の supports > __experimentalBorder セクションで定義されている。
ボタンブロックの場合 radius のみ定義されているため色、スタイル、幅はサポートされない。

wp-includes/blocks/button/block.json

{
	"apiVersion": 2,
	"name": "core/button",
	"title": "Button",
	"category": "design",
	︙
	"supports": {
		"anchor": true,
		"align": true,
		"alignWide": false,
		"color": {
			"__experimentalSkipSerialization": true,
			"gradients": true
		},
		"typography": {
			"fontSize": true,
			"__experimentalFontFamily": true
		},
		"reusable": false,
		"__experimentalBorder": {
			"radius": true,
			"__experimentalSkipSerialization": true
		},
		"__experimentalSelector": ".wp-block-button__link"
	},
	︙
}

参考

Global Styles & theme.json – Full Site Editing https://fullsiteediting.com/lessons/global-styles/

グローバル設定とスタイル (theme.json) – Japanese Team — WordPress.org https://ja.wordpress.org/team/handbook/block-editor/how-to-guides/themes/theme-json/

theme.json のセクション一覧はこちら

https://show-web.jp/2021/10/20/theme-json-settings/

theme.json セクションメモ

WordPress 5.8 から搭載された theme.json で設定できるフィールド、自分用メモ。(21/10/20時点)

version

settings

settings.border

settings.border.customColor

settings.border.customRadius

settings.border.customStyle

settings.border.customWidth

settings.color

settings.color.background

settings.color.custom

settings.color.customDuotone

settings.color.customGradient

settings.color.duotone

settings.color.gradients

settings.color.link

settings.color.pallette

settings.color.text

settings.custom

settings.layout

settings.layout.contentSize

settings.layout.wideSize

settings.spacing

settings.spacing.blockGap

settings.spacing.customMargin

settings.spacing.customPadding

settings.spacing.units

settings.typography

settings.typography.customFontSize

settings.typography.customFontStyle

settings.typography.customFontWeight

settings.typography.customLetterSpacing

settings.typography.customLineHeight

settings.typography.customTextDecorations

settings.typography.customTextTransforms

settings.typography.dropCap

settings.typography.fontFamilies

settings.typography.fontSizes

settings.blocks

styles

styles.border

styles.border.color

styles.border.radius

styles.border.style

styles.border.width

styles.color

styles.color.background

styles.color.gradient

styles.color.text

styles.filter

styles.filter.duotone

styles.spacing

styles.spacing.margin

styles.spacing.padding

styles.spacing.blockGap

styles.typography

styles.typography.fontFamily

styles.typography.fontSize

styles.typography.fontStyle

styles.typography.fontWeight

styles.typography.letterSpacing

styles.typography.lineHeight

styles.typography.textDecoration

styles.typography.fontStyletextTrasform

styles.block(ブロックスタイル)

styles.elements(要素スタイル)

customTemplates

templateParts

Block Editor モジュールの InnerBlocks コンポーネントで複数のブロックをひとまとまりにしたカスタムブロックを作る

WordPress のブロックエディタで投稿する際、案件によっては「これらのブロックがひとまとまりになったブロックがあれば良いのに」というケースがある。

デフォルトで用意されているブロックでは「メディアと文章」ブロックのように画像と段落がセットになっているものがそれにあたる。

「メディアと文章」ブロック、画像ブロックと段落ブロックで構成されている

案件で想定されるものとしては、画像・見出し・段落・テーブルをひとまとまりとした「商品情報ブロック」といったもの。
(WordPress 5.5 から導入されたブロックパターンでそれができるかなと思っていたが、ブロックパターンで作成したパターンは投稿のルートに挿入されグループブロックやカラムブロックの中に挿入できない)

https://ja.wordpress.org/team/handbook/block-editor/developers/block-api/block-patterns/

「商品情報ブロック」はデフォルトで備わっているブロックで構成されているため、1カラムのカラムブロックを作成しその中に画像ブロック、見出しブロック、段落ブロック、テーブルブロックを積み重ねていけば目的のものはできる。
しかし商品が幾つもあり且つユーザ側で商品情報を投稿するケースを考えた場合上記の方法は現実的ではない。

そこで画像・見出し等をひとまとまりとしたブロックを用意しユーザ側ではそのブロックを挿入し必要な情報を入力するだけとする。

@wordpress/create-block パッケージでカスタムブロックのひな形となるプラグインを生成

Create Blockによるプラグイン生成は以下参照。

プラグイン名はgutenitemとする。

$ npx @wordpress/create-block gutenitem
// gutenitem ディレクトリが生成される
$ cd gutenitem
$ npm start // 開発用ビルド開始

InnerBlocks コンポーネントで商品ブロックを作成する

InnerBlocks コンポーネントで他のブロックを内包することができるブロックを作成できる。
詳しくは公式ドキュメント参照。

https://ja.wordpress.org/team/handbook/block-editor/tutorials/block-tutorial/nested-blocks-inner-blocks/

ほぼ公式ドキュメントのままとなるが edit.js 及び save.js を編集していく。

edit.js

import {
	InnerBlocks,
	useBlockProps
} from '@wordpress/block-editor';

import './editor.scss';

// 「商品情報ブロック」定義
const MY_TEMPLATE = [
	[ 'core/image', {} ],
	[ 'core/heading', { placeholder: '商品名入力', level: 3 } ],
	[ 'core/paragraph', { placeholder: '商品説明入力' } ],
	[ 'core/table', {} ]
]

export default function Edit() {
	const blockProps = useBlockProps();
	return (
		<div { ...blockProps }>
			<InnerBlocks
				template={ MY_TEMPLATE }
				templateLock="all"
			/>
		</div>
	);
}

#2
InnerBlocks コンポーネントを追加
#20-23
InnerBlocks をレンダリング。
template プロパティを使用するとブロックが挿入された際の初期状態のブロックの構成を定義できる。定義内容は #9-14 で指定。template プロパティについては後述。
templateLock プロパティで作成したブロック内でのブロックの入れ替えや新規ブロックの挿入をロックできる。all を指定すると入れ替え・挿入を行えなく、insert を指定すると新規ブロックの挿入は不可となるがブロックの入れ替えは可。

save.js

import {
	InnerBlocks,
	useBlockProps
} from '@wordpress/block-editor';

export default function save() {
	const blockProps = useBlockProps.save();
	return (
		<div { ...blockProps }>
			<InnerBlocks.Content />
		</div>
	);
}

#2
InnerBlocks コンポーネントを追加
#10
InnerBlocks.Content をレンダリング。入れ子になったブロックのコンテンツを自動的に置き換える。

保存しビルドを行い WordPress 投稿画面で gutenitem ブロックを選択すると想定していた「画像、見出し、段落、テーブル」がひとまとまりになったブロックが挿入された。

画像、見出し、段落、テーブルからなるブロックが挿入される

あとは通常のブロック同様に情報を入力。

情報を入力した段階
カラムブロック(50/50)内に「商品情報ブロック」を入れることも可能

以上で InnerBlocks コンポーネントを使用してのカスタムブロック作成完了。

以下駄文。

InnerBlocks コンポーネントの template プロパティについて

edit.jstemplate プロパティにセットする値は配列 [ ブロック名, 属性(オプション) ] で定義する。

(参考)
gutenberg/block-templates.md at master · WordPress/gutenberg

edit.js

const MY_TEMPLATE = [
	[ 'core/image', {} ],
	[ 'core/heading', { placeholder: '商品名入力', level: 3 } ],
	[ 'core/paragraph', { placeholder: '商品説明入力' } ],
	[ 'core/table', {} ]
]

上記の
[ 'core/heading', { placeholder: '商品名入力, level: 3 } ]
であれば、
ブロック名: core/heading
属性: { placeholder: '商品名入力, level: 3 }
となり Gutenberg コアの見出しブロックが挿入され、プレースホルダーには「商品名入力」と表示、見出し要素は h3 となる。

これらブロックの情報は Gutenberg コアに含まれるブロックであれば GitHub の Gutenberg Block libraryページ(gutenberg/packages/block-library at master · WordPress/gutenberg)の src ディレクトリ以下に各ブロックのディレクトリがあるのでその中の block.json に記載されている。また、Gutenberg コアに含まれるブロック以外のブロックも指定できる。

core/heading ブロックの block.json は以下の通り。(一部省略)

(参考)
https://github.com/WordPress/gutenberg/blob/master/packages/block-library/src/heading/block.json

block.json

{
	"apiVersion": 2,
	"name": "core/heading",
	"category": "text",
	"attributes": {
		"textAlign": {
			"type": "string"
		},
		"content": {
			"type": "string",
			"source": "html",
			"selector": "h1,h2,h3,h4,h5,h6",
			"default": ""
		},
		"level": {
			"type": "number",
			"default": 2
		},
		"placeholder": {
			"type": "string"
		}
	},
	"supports": {

		(略)
	},
}

例えばブロック挿入初期状態でテキストセンタリング、「タイトル」というテキストが入力されている、見出しレベル h4 としたい場合は以下のようになる。

[ 'core/heading', { textAlign: 'center', content: 'タイトル', level: 4 } ]

ここで気になるのが textAlignlevel の値として何を指定するのが正しいかであるが、(textAlign であれば left, center, rightlevel であれば 1~6 というのは想像できるとは思うが)、仮に

[ 'core/heading', { textAlign: 'hoge', content: 'タイトル', level: 8 } ]

としてもビルド時にエラーを吐かず、エディタ側では

<h8 role="group" aria-multiline="true" aria-label="ブロック: 見出し" class="block-editor-rich-text__editable block-editor-block-list__block wp-block is-selected has-text-align-hoge rich-text" contenteditable="true" id="block-e8382e04-b569-4596-a2fd-5402aca1f3af" tabindex="0" data-block="e8382e04-b569-4596-a2fd-5402aca1f3af" data-type="core/heading" data-title="見出し" style="white-space: pre-wrap;">タイトル</h8>

フロント側では

<h8 class="has-text-align-hoge">タイトル</h8>

と出力されてしまうので適切な値を指定する必要がある。

見出しブロックの属性の適切な値を調べる

見出しブロック選択時ツールバー

見出しブロック選択時にはツールバーが表示され、見出しレベル、テキスト配置共にドロップダウンリストが用意されているのでこれらの選択肢の値が適切な値であると想像できる。
見出しブロックのソースは gutenberg/packages/block-library/src/heading at master · WordPress/gutenberg にあり edit.js が見出しブロックのレンダリングを行っているので見てみる。

edit.js

/**
 * External dependencies
 */
import classnames from 'classnames';

/**
 * WordPress dependencies
 */
import { __ } from '@wordpress/i18n';
import { createBlock } from '@wordpress/blocks';
import {
	AlignmentToolbar,
	BlockControls,
	RichText,
	useBlockProps,
} from '@wordpress/block-editor';
import { ToolbarGroup } from '@wordpress/components';

/**
 * Internal dependencies
 */
import HeadingLevelDropdown from './heading-level-dropdown';

function HeadingEdit( {
	attributes,
	setAttributes,
	mergeBlocks,
	onReplace,
	mergedStyle,
} ) {
	const { textAlign, content, level, placeholder } = attributes;
	const tagName = 'h' + level;
	const blockProps = useBlockProps( {
		className: classnames( {
			[ `has-text-align-${ textAlign }` ]: textAlign,
		} ),
		style: mergedStyle,
	} );

	return (
		<>
			<BlockControls>
				<ToolbarGroup>
					<HeadingLevelDropdown
						selectedLevel={ level }
						onChange={ ( newLevel ) =>
							setAttributes( { level: newLevel } )
						}
					/>
				</ToolbarGroup>
				<AlignmentToolbar
					value={ textAlign }
					onChange={ ( nextAlign ) => {
						setAttributes( { textAlign: nextAlign } );
					} }
				/>
			</BlockControls>
			<RichText
				identifier="content"
				tagName={ tagName }
				value={ content }
				onChange={ ( value ) => setAttributes( { content: value } ) }
				onMerge={ mergeBlocks }
				onSplit={ ( value ) => {
					if ( ! value ) {
						return createBlock( 'core/paragraph' );
					}

					return createBlock( 'core/heading', {
						...attributes,
						content: value,
					} );
				} }
				onReplace={ onReplace }
				onRemove={ () => onReplace( [] ) }
				aria-label={ __( 'Heading text' ) }
				placeholder={ placeholder || __( 'Write heading…' ) }
				textAlign={ textAlign }
				{ ...blockProps }
			/>
		</>
	);
}

export default HeadingEdit;

31行目の const { textAlign, content, level, placeholder } = attributes;template プロパティで指定した属性を受け取っていそう。
level は 44行目からの HeadingLevelDropdown コンポーネントで、textAlign は 51行目からの AlignmentToolbar コンポーネントで使用されており、これらのコンポーネントが見出しレベル、テキスト配置ドロップダウンリストを構成していそうなのでそれぞれのソースを見てみる。

gutenberg/packages/block-library/src/heading/heading-level-dropdown.js

heading-level-dropdown.js

/**
 * WordPress dependencies
 */
import {
	Dropdown,
	Toolbar,
	ToolbarButton,
	ToolbarGroup,
/**
 * WordPress dependencies
 */
import {
	Dropdown,
	Toolbar,
	ToolbarButton,
	ToolbarGroup,
} from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import { DOWN } from '@wordpress/keycodes';

/**
 * Internal dependencies
 */
import HeadingLevelIcon from './heading-level-icon';

const HEADING_LEVELS = [ 1, 2, 3, 4, 5, 6 ];

const POPOVER_PROPS = {
	className: 'block-library-heading-level-dropdown',
	isAlternate: true,
};

/** @typedef {import('@wordpress/element').WPComponent} WPComponent */

/**
 * HeadingLevelDropdown props.
 *
 * @typedef WPHeadingLevelDropdownProps
 *
 * @property {number}                 selectedLevel The chosen heading level.
 * @property {(newValue:number)=>any} onChange      Callback to run when
 *                                                  toolbar value is changed.
 */

/**
 * Dropdown for selecting a heading level (1 through 6).
 *
 * @param {WPHeadingLevelDropdownProps} props Component props.
 *
 * @return {WPComponent} The toolbar.
 */
export default function HeadingLevelDropdown( { selectedLevel, onChange } ) {
	return (
		<Dropdown
			popoverProps={ POPOVER_PROPS }
			renderToggle={ ( { onToggle, isOpen } ) => {
				const openOnArrowDown = ( event ) => {
					if ( ! isOpen && event.keyCode === DOWN ) {
						event.preventDefault();
						event.stopPropagation();
						onToggle();
					}
				};

				return (
					<ToolbarButton
						aria-expanded={ isOpen }
						aria-haspopup="true"
						icon={ <HeadingLevelIcon level={ selectedLevel } /> }
						label={ __( 'Change heading level' ) }
						onClick={ onToggle }
						onKeyDown={ openOnArrowDown }
						showTooltip
					/>
				);
			} }
			renderContent={ () => (
				<Toolbar
					className="block-library-heading-level-toolbar"
					label={ __( 'Change heading level' ) }
				>
					<ToolbarGroup
						isCollapsed={ false }
						controls={ HEADING_LEVELS.map( ( targetLevel ) => {
							const isActive = targetLevel === selectedLevel;
							return {
								icon: (
									<HeadingLevelIcon
										level={ targetLevel }
										isPressed={ isActive }
									/>
								),
								title: sprintf(
									// translators: %s: heading level e.g: "1", "2", "3"
									__( 'Heading %d' ),
									targetLevel
								),
								isActive,
								onClick() {
									onChange( targetLevel );
								},
							};
						} ) }
					/>
				</Toolbar>
			) }
		/>
	);
}

26行目の const HEADING_LEVELS = [ 1, 2, 3, 4, 5, 6 ]; が適切な値っぽい。

gutenberg/packages/block-editor/src/components/alignment-toolbar/index.js

index.js

/**
 * External dependencies
 */
import { find } from 'lodash';

/**
 * WordPress dependencies
 */
import { __, isRTL } from '@wordpress/i18n';
import { ToolbarGroup } from '@wordpress/components';
import { alignLeft, alignRight, alignCenter } from '@wordpress/icons';

const DEFAULT_ALIGNMENT_CONTROLS = [
	{
		icon: alignLeft,
		title: __( 'Align text left' ),
		align: 'left',
	},
	{
		icon: alignCenter,
		title: __( 'Align text center' ),
		align: 'center',
	},
	{
		icon: alignRight,
		title: __( 'Align text right' ),
		align: 'right',
	},
];

const POPOVER_PROPS = {
	position: 'bottom right',
	isAlternate: true,
};

export function AlignmentToolbar( props ) {
	const {
		value,
		onChange,
		alignmentControls = DEFAULT_ALIGNMENT_CONTROLS,
		label = __( 'Change text alignment' ),
		isCollapsed = true,
	} = props;

	function applyOrUnset( align ) {
		return () => onChange( value === align ? undefined : align );
	}

	const activeAlignment = find(
		alignmentControls,
		( control ) => control.align === value
	);

	function setIcon() {
		if ( activeAlignment ) return activeAlignment.icon;
		return isRTL() ? alignRight : alignLeft;
	}

	return (
		<ToolbarGroup
			isCollapsed={ isCollapsed }
			icon={ setIcon() }
			label={ label }
			popoverProps={ POPOVER_PROPS }
			controls={ alignmentControls.map( ( control ) => {
				const { align } = control;
				const isActive = value === align;

				return {
					...control,
					isActive,
					role: isCollapsed ? 'menuitemradio' : undefined,
					onClick: applyOrUnset( align ),
				};
			} ) }
		/>
	);
}

export default AlignmentToolbar;

13行目の DEFAULT_ALIGNMENT_CONTROLS で定義されてる align が適切な値っぽい。

・・・というか Alignment Toolbar コンポーネントについてはドキュメントが用意されてた…

gutenberg/packages/block-editor/src/components/alignment-toolbar

というわけで大雑把に属性に指定する適切と思われる値を調べたけど手間が掛かり過ぎるのできちんと調べるとブロックごとに属性に関するドキュメント用意されていたりするのだろうか。
見出しブロックは属性名から値が容易に想定できたのでブロックの初期状態を定義するの簡単だけど core/table ブロックの block.json みると複雑そうだ。

https://github.com/WordPress/gutenberg/blob/master/packages/block-library/src/table/block.json