Open90

Interactivity API を使ったタブブロックの作成

ちあきちあき

とりあえず create-block をしようってコマンド叩いたけどイマイチファイル構成が気に食わなかった

npx @wordpress/create-block@latest --wp-env --wp-scripts
ちあきちあき

wp-content/plugins ディレクトリ作って、その中で create-block をする

ちあきちあき

node_modules 散らかるの好きじゃないからルートの package.json でワークスペースつくっておく

"workspaces": [
    "wp-content/plugins/chiilog-iapi-tabs"
],
ちあきちあき

chiilog-iapi-tabs 内にできた node_modules は一旦けしけしして改めて npm install

ちあきちあき

view.js をビルドしたいから、 build コマンドと start コマンドに--experimental-modulesを入れる

ちあきちあき

block.json のsupports"interactivity": true を追加する

{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 3,
	"name": "create-block/chiilog-iapi-tabs",
	"version": "0.1.0",
	"title": "IAPI Tabs",
	"category": "widgets",
	"icon": "smiley",
	"description": "Example block scaffolded with Create Block tool.",
	"example": {},
	"supports": {
		"html": false,
		"interactivity": true
	},
	"textdomain": "chiilog-iapi-tabs",
	"editorScript": "file:./index.js",
	"editorStyle": "file:./index.css",
	"style": "file:./style-index.css",
	"viewScript": "file:./view.js"
}
ちあきちあき

prettierいれたいから以下実行

npm i -w wp-content/plugins/chiilog-iapi-tabs -D @wordpress/prettier-config prettier@npm:wp-prettier@latest
ちあきちあき

render.php の get_block_wrapper_attributes について

https://developer.wordpress.org/reference/functions/get_block_wrapper_attributes/

ちあきちあき

クラス追加したかったらこんな感じ

<?php
/**
 * @param array    $attributes The block attributes.
 * @param string   $content    The block default content.
 * @param WP_Block $block      The block instance.
 *
 * @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render
 */

$wrapper_attributes = get_block_wrapper_attributes(
	[
		'class' => 'custom-class',
	]
);
?>
<div <?php echo $wrapper_attributes; ?>>
	Block!
</div>
ちあきちあき

タブのliに対してパネルを紐づけるのってどうやればいいんだろ

ちあきちあき

案だし

クリックしたところをcontextで currentTab: 1 とかで持たせる

ちあきちあき

とりあえず下地になるタブのHTML+CSSは完成

<?php
/**
 * @param array    $attributes The block attributes.
 * @param string   $content    The block default content.
 * @param WP_Block $block      The block instance.
 *
 * @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render
 */
?>
<div
	<?php echo get_block_wrapper_attributes(); ?>
	data-wp-interactive="iapiTabs"
>
	<div class="tab-nav" role="tablist">
		<button
			role="tab"
			class="tab-nav__button"
			data-wp-on--click="actions.selectTab"
			aria-selected="true"
			id="tab-1"
			aria-controls="panel-1"
			tabindex="0"
		>
			ナビ1
		</button>
		<button
			role="tab"
			class="tab-nav__button"
			data-wp-on--click="actions.selectTab"
			aria-selected="false"
			id="tab-2"
			aria-controls="panel-2"
			tabindex="-1"
		>
			ナビ2
		</button>
		<button
			role="tab"
			class="tab-nav__button"
			data-wp-on--click="actions.selectTab"
			aria-selected="false"
			id="tab-3"
			aria-controls="panel-3"
			tabindex="-1"
		>
			ナビ3
		</button>
	</div>
	<div
		id="panel-1"
		role="tabpanel"
		tabindex="0"
		aria-labelledby="tab-1"
		class="tab-panel"
		aria-expanded="true"
		aria-hidden="false"
	>
		パネル1
	</div>
	<div
		id="panel-2"
		role="tabpanel"
		tabindex="0"
		aria-labelledby="tab-2"
		class="tab-panel"
		aria-expanded="false"
		aria-hidden="true"
	>
		パネル2
	</div>
	<div
		id="panel-3"
		role="tabpanel"
		tabindex="0"
		aria-labelledby="tab-3"
		class="tab-panel"
		aria-expanded="false"
		aria-hidden="true"
	>
		パネル3
	</div>
</div>
ちあきちあき

ここから

  • コンテナ
  • タブのボタン
  • タブのパネル

をそれぞれ別のブロックに分解していく

ちあきちあき

いやでも分解たらだめじゃね?ボタンとパネル連動してるからこのまま1つのブロックとして扱う方がよさそう

ちあきちあき
  • storeを設定するコンテナ
  • ボタン&パネル

でいいのか

ちあきちあき

innerBlockみたいに追加するボタンがないから別途ボタンをつける

ちあきちあき

む…。値が保存されないな…。そもそもひとつのdivの中でやるものじゃないからstringでjsonとして扱う方がいいのかなあ

ちあきちあき
{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 3,
	"name": "chiilog-blocks/iapi-tabs",
	"version": "0.1.0",
	"title": "IAPI Tabs",
	"category": "widgets",
	"icon": "smiley",
	"description": "Example block scaffolded with Create Block tool.",
	"attributes": {
		"contents": {
			"type": "array",
			"default": []
		},
		"tabNavText": {
			"type": "string",
			"source": "text",
			"selector": ".wp-block-chiilog-blocks-iapi-tabs__button span"
		}
	},
	"supports": {
		"interactivity": true
	},
	"textdomain": "chiilog-iapi-tabs",
	"editorScript": "file:./index.js",
	"editorStyle": "file:./index.css",
	"style": "file:./style-index.css",
	"viewScriptModule": "file:./view.js",
	"render": "file:./render.php"
}

できた。array のところに入れてた selector は不要

ちあきちあき

ナビボタンまわりの追加

import type { BlockEditProps } from '@wordpress/blocks';
import { RichText, useBlockProps } from '@wordpress/block-editor';
import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';

type BlockAttributes = {
	contents: TabItem[];
};

type TabItem = {
	tabNavText: string;
};

const defaultContents: TabItem = {
	tabNavText: '',
};

export default function Edit( {
	attributes: { contents },
	setAttributes,
}: BlockEditProps< BlockAttributes > ) {
	return (
		<div { ...useBlockProps() }>
			<div className="mb-2">
				<Button
					variant="secondary"
					onClick={ () => {
						setAttributes( {
							contents: [ ...contents, defaultContents ],
						} );
					} }
				>
					{ __( 'Add Tab', 'chiilog-iapi-tabs' ) }
				</Button>
			</div>
			<div
				className="wp-block-chiilog-blocks-iapi-tabs__nav"
				role="tablist"
			>
				{ contents.map( ( tabItem, index ) => {
					const tabId = index + 1;
					const isLastTab = index === contents.length - 1;

					return (
						<button
							role="tab"
							className="wp-block-chiilog-blocks-iapi-tabs__button"
							aria-selected={ isLastTab ? 'true' : 'false' }
							id={ `tab-${ tabId }` }
							aria-controls={ `panel-${ tabId }` }
							tabIndex={ isLastTab ? 0 : -1 }
							key={ index }
						>
							<RichText
								value={ tabItem.tabNavText }
								tagName="span"
								onChange={ ( value ) => {
									const newContents = [ ...contents ];
									newContents[ index ] = {
										...contents[ index ],
										tabNavText: value,
									};
									setAttributes( {
										contents: newContents,
									} );
								} }
								placeholder={ __( 'Tab', 'chiilog-iapi-tabs' ) }
							/>
						</button>
					);
				} ) }
			</div>
			...
	);
}
ちあきちあき

次はナビをクリックしたらカレントが変わるようにしたいから buttononClick をやっていく

ちあきちあき

useState 使ってカレントの位置をかえるようにする。これならAdd Tab ボタン押したら追加したタブに移動してくれる。

import { useState } from '@wordpress/element';
import type { BlockEditProps } from '@wordpress/blocks';
import { RichText, useBlockProps } from '@wordpress/block-editor';
import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';

type BlockAttributes = {
	contents: TabItem[];
};

type TabItem = {
	tabNavText: string;
};

const defaultContents: TabItem = {
	tabNavText: '',
};

export default function Edit( {
	attributes: { contents },
	setAttributes,
}: BlockEditProps< BlockAttributes > ) {
	// 選択中のタブが何番目かを保持するステート
	const [ currentTab, setCurrentTab ] = useState( 0 );

	return (
		<div { ...useBlockProps() }>
			<div className="mb-2">
				<Button
					variant="secondary"
					onClick={ () => {
						setAttributes( {
							contents: [ ...contents, defaultContents ],
						} );
						setCurrentTab( contents.length );
					} }
				>
					{ __( 'Add Tab', 'chiilog-iapi-tabs' ) }
				</Button>
			</div>
			<div
				className="wp-block-chiilog-blocks-iapi-tabs__nav"
				role="tablist"
			>
				{ contents.map( ( tabItem, index ) => {
					const tabId = index + 1;

					return (
						<button
							role="tab"
							className="wp-block-chiilog-blocks-iapi-tabs__button"
							aria-selected={
								currentTab === index ? 'true' : 'false'
							}
							id={ `tab-${ tabId }` }
							aria-controls={ `panel-${ tabId }` }
							tabIndex={ currentTab === index ? 0 : -1 }
							key={ index }
							onClick={ () => {
								setCurrentTab( index );
							} }
						>
							<RichText
								value={ tabItem.tabNavText }
								tagName="span"
								onChange={ ( value ) => {
									const newContents = [ ...contents ];
									newContents[ index ] = {
										...contents[ index ],
										tabNavText: value,
									};
									setAttributes( {
										contents: newContents,
									} );
								} }
								placeholder={ __( 'Tab', 'chiilog-iapi-tabs' ) }
							/>
						</button>
					);
				} ) }
			</div>
			...
	);
}
ちあきちあき

増やすボタンがあれば消すボタンも必要

ちあきちあき

消すボタンは配置するならタブナビのボタンの横かなあ

ちあきちあき

タブがカレントのときだけ表示させるようにする

import { useState } from '@wordpress/element';
import type { BlockEditProps } from '@wordpress/blocks';
import { RichText, useBlockProps } from '@wordpress/block-editor';
import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { close } from '@wordpress/icons';

type BlockAttributes = {
	contents: TabItem[];
};

type TabItem = {
	tabNavText: string;
};

const defaultContents: TabItem = {
	tabNavText: '',
};

export default function Edit( {
	attributes: { contents },
	setAttributes,
}: BlockEditProps< BlockAttributes > ) {
	// 選択中のタブが何番目かを保持するステート
	const [ currentTab, setCurrentTab ] = useState( 0 );

	return (
		<div { ...useBlockProps() }>
			<div className="mb-2">
				<Button
					variant="secondary"
					onClick={ () => {
						setAttributes( {
							contents: [ ...contents, defaultContents ],
						} );
						setCurrentTab( contents.length );
					} }
				>
					{ __( 'Add Tab', 'chiilog-iapi-tabs' ) }
				</Button>
			</div>
			<div
				className="wp-block-chiilog-blocks-iapi-tabs__nav"
				role="tablist"
			>
				{ contents.map( ( tabItem, index ) => {
					const tabId = index + 1;

					return (
						<div className="flex flex-col gap-1 flex-auto">
							{ currentTab === index ? (
								<div>
									<Button
										size="small"
										icon={ close }
										label="削除"
										onClick={ () => {
											const newContents = [ ...contents ];
											newContents.splice( index, 1 );
											setAttributes( {
												contents: newContents,
											} );
										} }
									/>
								</div>
							) : null }

							<button
								role="tab"
								className="wp-block-chiilog-blocks-iapi-tabs__button mt-auto"
								aria-selected={
									currentTab === index ? 'true' : 'false'
								}
								id={ `tab-${ tabId }` }
								aria-controls={ `panel-${ tabId }` }
								tabIndex={ currentTab === index ? 0 : -1 }
								key={ index }
								onClick={ () => {
									setCurrentTab( index );
								} }
							>
								<RichText
									value={ tabItem.tabNavText }
									tagName="span"
									onChange={ ( value ) => {
										const newContents = [ ...contents ];
										newContents[ index ] = {
											...contents[ index ],
											tabNavText: value,
										};
										setAttributes( {
											contents: newContents,
										} );
									} }
									placeholder={ __(
										'Tab',
										'chiilog-iapi-tabs'
									) }
								/>
							</button>
						</div>
					);
				} ) }
			</div>
			...
		</div>
	);
}
ちあきちあき

左右に移動させたりも実装してみたいけどとりあえずそれは後回し

ちあきちあき

ナビ部分はできたので次はパネル。インナーブロックを複数置くことはできないので、やはりここはパネル用のブロックが必要そう。

ちあきちあき

わけた

/chiilog-iapi-tabs
├── container
│   ├── block.json
│   ├── edit.tsx
│   ├── index.js
│   ├── render.php
│   └── view.js
├── panel
│   ├── block.json
│   ├── edit.tsx
│   ├── index.js
│   └── save.tsx
└── style.css
ちあきちあき

パネル単体で増やせないように renderAppender: false をいれておく

ちあきちあき

Add Tab押したときにパネルも一緒に追加する

ちあきちあき
import { useDispatch } from '@wordpress/data';
import { createBlock } from '@wordpress/blocks';
import { Button } from '@wordpress/components';

function InsertBlockButton() {
    const { insertBlocks } = useDispatch('core/block-editor');

    const insertParagraphBlock = () => {
        // パラグラフブロックのインスタンスを作成
        const block = createBlock('core/paragraph', {
            content: 'Hello, world!',
        });

        // ブロックをエディターに挿入
        insertBlocks(block);
    };

    return <Button onClick={insertParagraphBlock}>Insert Paragraph</Button>;
}
ちあきちあき

insertBlocks がうまく動かんなと思ったけど、 clientId を足したら動いた。

ちあきちあき

ChatGPTにコード提示してここうまく動かないんですけど…どうしたらいいっすかね……って聞いたらしっかり答えてくれるから助かる〜〜〜〜〜〜〜なに調べたらいいかとかの目安になる

ちあきちあき

(いろいろ省略)

ご質問のコードについて

ご質問のコードでは、insertBlocksを使用してインナーブロックにブロックを挿入しようとしていますが、insertBlocksはそのままでは親ブロックの内部にブロックを挿入するために直接使用することはできません。代わりに、親ブロックのclientIdを使用してreplaceInnerBlocksinsertBlock、またはinsertBlocks(適切な引数とともに)を使用する必要があります。

さらに、useInnerBlocksPropsフックとともにインナーブロックを操作する際には、これらのブロック操作関数を適切に利用して目的のインナーブロック操作を行うようにしなければなりません。


っておしえてくれた

ちあきちあき

パネルを追加するまではできたけど、aria-xxx 系統をクリックのたびに変えたい 🤔

ちあきちあき
const updatePanelsVisibility = ( selectedIndex: number ) => {
	blocks.forEach( ( block: BlockInstance, index: number ) => {
		if ( block.name === panelBlockName ) {
			const isVisible = index === selectedIndex;
			updateBlockAttributes( block.clientId, {
				ariaExpanded: isVisible,
				ariaHidden: ! isVisible,
			} );
		}
	} );
};

2回目以降のAdd Tabクリックでblocksの数が合わない。

ちあきちあき
useEffect( () => {
	if ( blocks.length > 0 ) {
		updatePanelsVisibility( currentTab );
	}
}, [ blocks ] );

これ追加でうまく切り替えれた!

ちあきちあき
import { useDispatch, useSelect } from '@wordpress/data';
import { useEffect, useState } from '@wordpress/element';
import {
	type BlockEditProps,
	type BlockInstance,
	createBlock,
} from '@wordpress/blocks';
import {
	RichText,
	useBlockProps,
	useInnerBlocksProps,
	store as blockEditorStore,
} from '@wordpress/block-editor';
import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { close } from '@wordpress/icons';

type BlockAttributes = {
	contents: TabItem[];
};

type TabItem = {
	tabNavText: string;
};

const defaultContents: TabItem = {
	tabNavText: '',
};

const panelBlockName: string = 'chiilog-blocks/iapi-tabs-panel';

export default function Edit( {
	attributes: { contents },
	setAttributes,
	clientId,
}: BlockEditProps< BlockAttributes > ) {
	const { insertBlocks, updateBlockAttributes } =
		useDispatch( 'core/block-editor' );

	// 選択中のタブが何番目かを保持するステート
	const [ currentTab, setCurrentTab ] = useState( 0 );

	// インナーブロックの設定まわり
	const ALLOWED_BLOCKS = [ panelBlockName ];
	const innerBlocksProps = useInnerBlocksProps(
		{},
		{
			allowedBlocks: ALLOWED_BLOCKS,
			// @ts-ignore
			renderAppender: false,
		}
	);
	const blocks: BlockInstance[] = useSelect(
		// @ts-ignore
		( select ) => select( blockEditorStore ).getBlocks( clientId ),
		[ clientId ]
	);

	useEffect( () => {
		if ( blocks.length > 0 ) {
			updatePanelsVisibility( currentTab );
		}
	}, [ blocks ] );

	/**
	 * タブのナビアイテムを追加する
	 */
	const addNavItem = () => {
		setAttributes( {
			contents: [ ...contents, defaultContents ],
		} );
	};

	/**
	 * タブのナビアイテムを削除する
	 */
	const removeNavItem = ( index: number ) => {
		const newContents = [ ...contents ];
		newContents.splice( index, 1 );
		setAttributes( {
			contents: newContents,
		} );
	};

	/**
	 * タブのパネルを追加する
	 */
	const addNavPanel = ( index: number ) => {
		const createPanel = createBlock(
			panelBlockName,
			{
				panelId: `panel-${ index + 1 }`,
				ariaLabelledby: `tab-${ index + 1 }`,
				ariaExpanded: index === currentTab,
				ariaHidden: index !== currentTab,
			},
			[
				createBlock( 'core/paragraph', {
					content: __(
						`Tab Content : ${ index }`,
						'chiilog-iapi-tabs'
					),
				} ),
			]
		);
		insertBlocks( createPanel, index, clientId );
	};

	/**
	 * タブのパネルのカレントを更新する
	 */
	const updatePanelsVisibility = ( selectedIndex: number ) => {
		blocks.forEach( ( block: BlockInstance, index: number ) => {
			if ( block.name === panelBlockName ) {
				const isVisible = index === selectedIndex;
				updateBlockAttributes( block.clientId, {
					ariaExpanded: isVisible,
					ariaHidden: ! isVisible,
				} );
			}
			console.log( blocks );
			console.log( `selectedIndex: ${ selectedIndex }` );
			console.log( `index: ${ index }` );
		} );
	};

	return (
		<div { ...useBlockProps() }>
			<div className="mb-2">
				<Button
					variant="secondary"
					onClick={ () => {
						addNavItem();
						setCurrentTab( contents.length );
						addNavPanel( contents.length );
						updatePanelsVisibility( contents.length );
					} }
				>
					{ __( 'Add Tab', 'chiilog-iapi-tabs' ) }
				</Button>
			</div>
			<div
				className="wp-block-chiilog-blocks-iapi-tabs__nav"
				role="tablist"
			>
				{ contents.map( ( tabItem, index ) => {
					const tabId = index + 1;

					return (
						<div
							key={ `nav-${ index }` }
							className="flex flex-col gap-1 flex-auto"
						>
							{ currentTab === index ? (
								<>
									<Button
										size="small"
										icon={ close }
										label="削除"
										onClick={ () => {
											removeNavItem( index );
											setCurrentTab( 0 );
										} }
									/>
								</>
							) : null }

							<button
								role="tab"
								className="wp-block-chiilog-blocks-iapi-tabs__button mt-auto"
								aria-selected={
									currentTab === index ? 'true' : 'false'
								}
								id={ `tab-${ tabId }` }
								aria-controls={ `panel-${ tabId }` }
								tabIndex={ currentTab === index ? 0 : -1 }
								onClick={ () => {
									setCurrentTab( index );
									updatePanelsVisibility( index );
								} }
							>
								<RichText
									value={ tabItem.tabNavText }
									tagName="span"
									onChange={ ( value ) => {
										const newContents = [ ...contents ];
										newContents[ index ] = {
											...contents[ index ],
											tabNavText: value,
										};
										setAttributes( {
											contents: newContents,
										} );
									} }
									placeholder={ __(
										'Tab',
										'chiilog-iapi-tabs'
									) }
								/>
							</button>
						</div>
					);
				} ) }
			</div>
			<div { ...innerBlocksProps } />
		</div>
	);
}
ちあきちあき

edit.tsx完成〜〜〜〜〜〜〜〜〜〜!!
実質タブのJSまわりにかかったの1日かな…?

import { useDispatch, useSelect } from '@wordpress/data';
import { useEffect, useState } from '@wordpress/element';
import {
	type BlockEditProps,
	type BlockInstance,
	createBlock,
} from '@wordpress/blocks';
import {
	RichText,
	useBlockProps,
	useInnerBlocksProps,
	store as blockEditorStore,
} from '@wordpress/block-editor';
import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { close } from '@wordpress/icons';

type BlockAttributes = {
	contents: TabItem[];
};

type TabItem = {
	tabNavText: string;
};

const defaultContents: TabItem = {
	tabNavText: '',
};

const panelBlockName: string = 'chiilog-blocks/iapi-tabs-panel';

export default function Edit( {
	attributes: { contents },
	setAttributes,
	clientId,
}: BlockEditProps< BlockAttributes > ) {
	const { insertBlocks, removeBlocks, updateBlockAttributes } =
		useDispatch( 'core/block-editor' );

	// 選択中のタブが何番目かを保持するステート
	const [ currentTab, setCurrentTab ] = useState( 0 );

	// インナーブロックの設定まわり
	const ALLOWED_BLOCKS = [ panelBlockName ];
	const innerBlocksProps = useInnerBlocksProps(
		{},
		{
			allowedBlocks: ALLOWED_BLOCKS,
			// @ts-ignore
			renderAppender: false,
		}
	);
	const blocks: BlockInstance[] = useSelect(
		// @ts-ignore
		( select ) => select( blockEditorStore ).getBlocks( clientId ),
		[ clientId ]
	);

	/**
	 * 2回目以降のblocks追加でうまくblocksが取得できないため、
	 * blocksが更新されたタイミングでパネルの表示を更新する
	 */
	useEffect( () => {
		if ( blocks.length > 0 ) {
			updatePanelsVisibility( currentTab );
		}
	}, [ blocks ] );

	/**
	 * タブのナビアイテムを追加する
	 */
	const addNavItem = () => {
		setAttributes( {
			contents: [ ...contents, defaultContents ],
		} );
	};

	/**
	 * タブのナビアイテムを削除する
	 */
	const removeNavItem = ( index: number ) => {
		const newContents = [ ...contents ];
		newContents.splice( index, 1 );
		setAttributes( {
			contents: newContents,
		} );
	};

	/**
	 * タブのパネルを追加する
	 */
	const addNavPanel = ( index: number ) => {
		const createPanel = createBlock(
			panelBlockName,
			{
				panelId: `panel-${ index + 1 }`,
				ariaLabelledby: `tab-${ index + 1 }`,
				ariaExpanded: index === currentTab,
				ariaHidden: index !== currentTab,
			},
			[
				createBlock( 'core/paragraph', {
					content: __(
						`Tab Content : ${ index }`,
						'chiilog-iapi-tabs'
					),
				} ),
			]
		);
		insertBlocks( createPanel, index, clientId );
	};

	/**
	 * タブのパネルを削除する
	 */
	const removeNavPanel = ( index: number ) => {
		removeBlocks( blocks[ index ].clientId, false );
	};

	/**
	 * タブのパネルのカレントを更新する
	 */
	const updatePanelsVisibility = ( selectedIndex: number ) => {
		blocks.forEach( ( block: BlockInstance, index: number ) => {
			if ( block.name === panelBlockName ) {
				const isVisible = index === selectedIndex;
				updateBlockAttributes( block.clientId, {
					ariaExpanded: isVisible,
					ariaHidden: ! isVisible,
				} );
			}
		} );
	};

	return (
		<div { ...useBlockProps() }>
			<div className="mb-2">
				<Button
					variant="secondary"
					onClick={ () => {
						addNavItem();
						setCurrentTab( contents.length );
						addNavPanel( contents.length );
						updatePanelsVisibility( contents.length );
					} }
				>
					{ __( 'Add Tab', 'chiilog-iapi-tabs' ) }
				</Button>
			</div>
			<div
				className="wp-block-chiilog-blocks-iapi-tabs__nav"
				role="tablist"
			>
				{ contents.map( ( tabItem, index ) => {
					const tabId = index + 1;

					return (
						<div
							key={ `nav-${ index }` }
							className="flex flex-col gap-1 flex-auto"
						>
							{ currentTab === index ? (
								<>
									<Button
										size="small"
										icon={ close }
										label={ __(
											'Remove Tab',
											'chiilog-iapi-tabs'
										) }
										onClick={ () => {
											removeNavItem( index );
											setCurrentTab( 0 );
											removeNavPanel( index );
											updatePanelsVisibility( 0 );
										} }
									/>
								</>
							) : null }

							<button
								role="tab"
								className="wp-block-chiilog-blocks-iapi-tabs__button mt-auto"
								aria-selected={
									currentTab === index ? 'true' : 'false'
								}
								id={ `tab-${ tabId }` }
								aria-controls={ `panel-${ tabId }` }
								tabIndex={ currentTab === index ? 0 : -1 }
								onClick={ () => {
									setCurrentTab( index );
									updatePanelsVisibility( index );
								} }
							>
								<RichText
									value={ tabItem.tabNavText }
									tagName="span"
									onChange={ ( value ) => {
										const newContents = [ ...contents ];
										newContents[ index ] = {
											...contents[ index ],
											tabNavText: value,
										};
										setAttributes( {
											contents: newContents,
										} );
									} }
									placeholder={ __(
										'Tab',
										'chiilog-iapi-tabs'
									) }
								/>
							</button>
						</div>
					);
				} ) }
			</div>
			<div { ...innerBlocksProps } />
		</div>
	);
}
ちあきちあき

パネルのブロックインナーブロックだけどIAPIできるのかなあって試しに save.tsx で data-wp-on--click つけたら動いたからいけそう

import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
import type { BlockEditProps } from '@wordpress/blocks';
import type { BlockAttributes } from './type';

export default function Edit( {
	attributes: { panelId, ariaLabelledby, ariaExpanded, ariaHidden },
}: BlockEditProps< BlockAttributes > ) {
	return (
		<div
			{ ...useBlockProps.save( { anchor: panelId } ) }
			role="tabpanel"
			tabIndex={ 0 }
			aria-labelledby={ ariaLabelledby }
			aria-expanded={ ariaExpanded }
			aria-hidden={ ariaHidden }
			data-wp-on--click="actions.selectTab"
		>
			<div { ...useInnerBlocksProps.save() } />
		</div>
	);
}
ちあきちあき

まずはrender.php でナビ、パネルともに最初のやつをカレントにする

ちあきちあき

ナビは最初からそうなってるけど、パネルは最後に編集したやつがカレントになってる(editとの兼ね合いのため)

ちあきちあき

aria-expandedaria-hidden をそれぞれ data-wp-bind--aria-expanded``data-wp-bind--aria-hiddenに置き換え

ちあきちあき

うーん、 data-wp-bind--xxx いれたらエディターでWarning出るな…

Warning: Trying to access array offset on value of type bool in /var/www/html/wp-includes/interactivity-api/class-wp-interactivity-api.php on line 385

Warning: Cannot modify header information - headers already sent by (output started at /var/www/html/wp-includes/interactivity-api/class-wp-interactivity-api.php:385) in /var/www/html/wp-admin/admin-header.php on line 9
ちあきちあき

IAPI系の記述消したら消えたから、これは WP_HTML_Tag_Processor 使って後付したほうがいいやつかなあ

ちあきちあき

add_filterrender_block_ はじまり。今回私が作ってるやつで言うと

add_filter( 'render_block_chiilog-blocks/iapi-tabs-panel', 'add_directives_to_inner_blocks', 10, 2 );

こうなる

ちあきちあき
function add_directives_to_inner_blocks( $block_content, $block ) {
	$panels = new WP_HTML_Tag_Processor( $block_content );

	if ( $panels->next_tag( array( 'class_name' => 'wp-block-chiilog-blocks-iapi-tabs-panel' ) ) ) {
		$panels->set_attribute( 'data-wp-bind--aria-expanded', 'false' );
		$panels->set_attribute( 'data-wp-bind--aria-hidden', 'true' );
	}

	return $panels->get_updated_html();
}
add_filter( 'render_block_chiilog-blocks/iapi-tabs-panel', 'add_directives_to_inner_blocks', 10, 2 );

とりあえず一旦ベタ打ちでいれる

ちあきちあき

最初のパネルだけ表示させたいのでwhileつかう。あとadd_filterの対象を変える。

function add_directives_to_inner_blocks( $block_content, $block ) {
	$panels = new WP_HTML_Tag_Processor( $block_content );
	$panelCount = 0;

	while ( $panels->next_tag() ) {
		foreach ( $panels->class_list() as $class_name ) {
			if ( $class_name === 'wp-block-chiilog-blocks-iapi-tabs-panel' ) {
				$panels->set_attribute( 'data-wp-bind--aria-expanded', $panelCount === 0);
				$panels->set_attribute( 'data-wp-bind--aria-hidden', $panelCount !== 0 );
				$panelCount++;
			}
		}
	}

	return $panels->get_updated_html();
}
add_filter( 'render_block_chiilog-blocks/iapi-tabs', 'add_directives_to_inner_blocks', 10, 2 );
ちあきちあき

これで一旦は最初のナビアイテムとパネルを表示させるまではできたぞ!

ちあきちあき

クリックしたらカレントを変える。ついにきた

ちあきちあき

とりあえず wp_interactivity_state で初期値いれておけばいいのかな

ちあきちあき

state を console.log してみたら入ってたから、 wp_interactivity_statestate 追加したら getContext() はいらないっぽいな

ちあきちあき

The state defined on the server with wp_interactivity_state() gets merged with the stores defined in the view.js files.

マージ済みだからとくになんか作業いらないってことか? 🤔

ちあきちあき

とりあえずこんな感じに。

/**
 * WordPress dependencies
 */
import { store } from '@wordpress/interactivity';

const { state, actions } = store( 'iapiTabs', {
	state: {
		get currentItem() {
			if ( state.currentTab ) {
				return state.currentTab;
			}

			return 0;
		},
	},
	actions: {
		selectTab: () => {
			console.log( state.currentItem );
		},
	},
	callbacks: {},
} );
wp_interactivity_state( 'iapiTabs', array (
	'currentTab' => 1
));
ちあきちあき

さて………どうやってナビ動かすんだ? 🤔

ちあきちあき
/**
 * パネルの aria-expanded と aria-hidden を data-wp-bind--xxx に変換する
 *
 * @param $block_content
 * @param $block
 * @return string
 */
function add_directives_to_inner_blocks( $block_content, $block ) {
	$panels = new WP_HTML_Tag_Processor( $block_content );
	$panelCount = 0;

	while ( $panels->next_tag() ) {
		foreach ( $panels->class_list() as $class_name ) {
			if ( $class_name === 'wp-block-chiilog-blocks-iapi-tabs-panel' ) {
				$panels->set_attribute( 'data-wp-bind--aria-expanded', 'state.panelExpanded' );
				$panels->set_attribute( 'data-wp-bind--aria-hidden', 'state.panelHidden' );
				$panels->set_attribute( 'data-wp-context', '{ "panelPosition": ' . $panelCount . ' }' );
				$panelCount++;
			}
		}
	}

	return $panels->get_updated_html();
}
add_filter( 'render_block_chiilog-blocks/iapi-tabs', 'add_directives_to_inner_blocks', 10, 2 );

stateに差し替え

ちあきちあき

view.js でカレントつける

/**
 * WordPress dependencies
 */
import { getContext, store } from '@wordpress/interactivity';

const { state, actions } = store( 'iapiTabs', {
	state: {
		get panelExpanded() {
			const ctx = getContext();
			return ctx.panelPosition === state.currentItem;
		},
		get panelHidden() {
			const ctx = getContext();
			return ctx.panelPosition !== state.currentItem;
		},
	},
	actions: {
		selectTab: () => {
			console.log( 'click' );
		},
	},
	callbacks: {},
} );
ちあきちあき

data-wp-context 使って番号もたせて、番号とカレント位置が同じだったら表示的なロジックになるかなあ

ちあきちあき

ナビボタン

<button
	role="tab"
	class="wp-block-chiilog-blocks-iapi-tabs__button"
	data-wp-on--click="actions.changeCurrentTab"
	data-wp-bind--aria-selected="state.tabSelected"
	id="tab-<?php echo esc_attr( $tabNumber ); ?>"
	aria-controls="panel-<?php echo esc_attr( $tabNumber ); ?>"
	tabindex="<?php echo esc_attr( $index === 0 ? '0' : '-1' ); ?>"
	data-wp-context='{ "position": <?php echo esc_attr( $index ); ?> }'
>
	<?php echo esc_html( $navItem['tabNavText'] ); ?>
</button>

パネル部分

$panels->set_attribute( 'data-wp-bind--aria-expanded', 'state.panelExpanded' );
$panels->set_attribute( 'data-wp-bind--aria-hidden', 'state.panelHidden' );
$panels->set_attribute( 'data-wp-context', '{ "position": ' . $panelCount . ' }' );
$panelCount++;

view.js

/**
 * WordPress dependencies
 */
import { getContext, store } from '@wordpress/interactivity';

const { state, actions } = store( 'iapiTabs', {
	state: {
		get panelExpanded() {
			const ctx = getContext();
			return ctx.position === state.currentTab;
		},
		get panelHidden() {
			const ctx = getContext();
			return ctx.position !== state.currentTab;
		},
		get tabSelected() {
			const ctx = getContext();
			return ctx.position === state.currentTab;
		},
	},
	actions: {
		changeCurrentTab: () => {
			const ctx = getContext();
			state.currentTab = ctx.position;
		},
	},
	callbacks: {},
} );

できた!

ちあきちあき

あー…store名にユニークな値をもたせたら解決するのか?

ちあきちあき

wp_interactivity_state をやめて、wp_interactivity_data_wp_context で初期値をもたせたら解決した!

ちあきちあき

それはそれとして複数配置すると id が重複してくるので、ブロックの clientIdを使って重複しないようにする。

ちあきちあき

複数使わないときは wp_interactivity_state で、複数使うときは wp_interactivity_data_wp_context 使うって使い分けになるのかなあ。

ちあきちあき

ああ、save.js でもいいじゃんと思ったけど今はrender.phpの方しかサポートしてないのね