From c1e55b14ab0027da6dce3b27f3e51485017cc457 Mon Sep 17 00:00:00 2001 From: Milan Ricoul Date: Thu, 24 Sep 2026 14:48:18 +0200 Subject: [PATCH 01/30] chore(scripts): rename env start/stop npm scripts (#93541) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 126c772..8a40421 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "packages-update": "wp-scripts packages-update", "plugin-zip": "wp-scripts plugin-zip", "start": "wp-scripts start", - "start:env": "wp-env start", - "stop:env": "wp-env stop", + "env:start": "wp-env start", + "env:stop": "wp-env stop", "env": "wp-env", "make-pot": "wp i18n make-pot . languages/blockparty-tabs.pot --slug=blockparty-tabs --domain=blockparty-tabs --exclude=node_modules,src", "make-json": "wp i18n make-json languages/ --no-purge", From 3f1da6cdd36027bfc17d97f5c52f977f579194c2 Mon Sep 17 00:00:00 2001 From: Milan Ricoul Date: Thu, 24 Sep 2026 14:48:22 +0200 Subject: [PATCH 02/30] feat(blocks): expand supports and refresh descriptions (#93541) --- src/blockparty-tabs-nav-item/block.json | 39 ++++++++++++++------ src/blockparty-tabs-nav/block.json | 11 ++---- src/blockparty-tabs-panel-item/block.json | 35 ++++++++++++------ src/blockparty-tabs-panels/block.json | 11 ++---- src/blockparty-tabs/block.json | 45 ++++++++++++++++++----- 5 files changed, 92 insertions(+), 49 deletions(-) diff --git a/src/blockparty-tabs-nav-item/block.json b/src/blockparty-tabs-nav-item/block.json index 9912859..43667a6 100644 --- a/src/blockparty-tabs-nav-item/block.json +++ b/src/blockparty-tabs-nav-item/block.json @@ -5,20 +5,38 @@ "version": "1.1.5", "title": "Tab", "category": "widgets", - "description": "Accessible tabs block item", - "parent": [ - "blockparty/tabs-nav" - ], + "description": "A single tab button used to switch to its matching content panel.", + "parent": [ "blockparty/tabs-nav" ], "supports": { "html": false, - "anchor": false, - "align": false, + "visibility": false, + "lock": false, "color": { "background": true, "text": true }, - "lock": false, - "reusable": false + "typography": { + "fontSize": true + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true + }, + "layout": { + "default": { + "type": "flex", + "flexWrap": "wrap" + }, + "allowVerticalAlignment": false, + "allowOrientation": false, + "allowWrap": false + }, + "spacing": { + "padding": true, + "blockGap": true + } }, "attributes": { "label": { @@ -42,10 +60,7 @@ "type": "integer" } }, - "usesContext": [ - "blockparty/Tabs", - "blockparty/TabsActive" - ], + "usesContext": [ "blockparty/Tabs", "blockparty/TabsActive" ], "textdomain": "blockparty-tabs", "editorScript": "file:./index.js" } diff --git a/src/blockparty-tabs-nav/block.json b/src/blockparty-tabs-nav/block.json index 80318b9..6929f8c 100644 --- a/src/blockparty-tabs-nav/block.json +++ b/src/blockparty-tabs-nav/block.json @@ -5,20 +5,17 @@ "version": "1.1.5", "title": "Tabs list", "category": "widgets", - "description": "Accessible tabs block item", - "parent": [ - "blockparty/tabs" - ], + "description": "Display the tab list for switching between content panels.", + "parent": [ "blockparty/tabs" ], "supports": { "html": false, "anchor": false, "align": false, + "ariaLabel": true, "lock": false, "reusable": false }, - "usesContext": [ - "blockparty/Tabs" - ], + "usesContext": [ "blockparty/Tabs" ], "textdomain": "blockparty-tabs", "editorScript": "file:./index.js" } diff --git a/src/blockparty-tabs-panel-item/block.json b/src/blockparty-tabs-panel-item/block.json index 7052126..920a129 100644 --- a/src/blockparty-tabs-panel-item/block.json +++ b/src/blockparty-tabs-panel-item/block.json @@ -6,20 +6,34 @@ "title": "Panel", "category": "widgets", "icon": "layout", - "description": "Accessible tabs block item", - "parent": [ - "blockparty/tabs-panels" - ], + "description": "The content panel shown when its matching tab is selected.", + "parent": [ "blockparty/tabs-panels" ], "supports": { + "anchor": true, + "background": { + "backgroundImage": true, + "backgroundSize": true, + "gradient": true + }, "html": false, - "anchor": false, - "align": false, "color": { "background": true, + "button": true, + "heading": true, + "link": true, "text": true }, - "lock": false, - "reusable": false + "layout": true, + "shadow": true, + "spacing": { + "blockGap": true, + "padding": true + }, + "typography": { + "fontSize": true, + "lineHeight": true + }, + "visibility": false }, "attributes": { "linkId": { @@ -32,10 +46,7 @@ "type": "integer" } }, - "usesContext": [ - "blockparty/Tabs", - "blockparty/TabsActive" - ], + "usesContext": [ "blockparty/Tabs", "blockparty/TabsActive" ], "textdomain": "blockparty-tabs", "editorScript": "file:./index.js" } diff --git a/src/blockparty-tabs-panels/block.json b/src/blockparty-tabs-panels/block.json index 8f2abaf..653bc56 100644 --- a/src/blockparty-tabs-panels/block.json +++ b/src/blockparty-tabs-panels/block.json @@ -5,10 +5,8 @@ "version": "1.1.5", "title": "Panels", "category": "widgets", - "description": "Accessible tabs block item", - "parent": [ - "blockparty/tabs" - ], + "description": "Group the content panels of a tabbed interface.", + "parent": [ "blockparty/tabs" ], "supports": { "html": false, "anchor": false, @@ -21,10 +19,7 @@ "type": "string" } }, - "usesContext": [ - "blockparty/Tabs", - "blockparty/TabsActive" - ], + "usesContext": [ "blockparty/Tabs", "blockparty/TabsActive" ], "textdomain": "blockparty-tabs", "editorScript": "file:./index.js" } diff --git a/src/blockparty-tabs/block.json b/src/blockparty-tabs/block.json index 4b8f37e..ecd7651 100644 --- a/src/blockparty-tabs/block.json +++ b/src/blockparty-tabs/block.json @@ -5,24 +5,49 @@ "version": "1.1.5", "title": "Tabs", "category": "widgets", - "description": "Accessible tabs block", + "description": "Organize content into tabs so visitors can browse sections without scrolling endlessly.", "supports": { - "html": false, + "align": [ "full", "wide" ], "anchor": true, - "align": [ - "wide", - "full" - ] + "background": { + "backgroundImage": true, + "backgroundSize": true, + "gradient": true + }, + "color": { + "text": true, + "background": true, + "heading": true, + "button": true, + "link": true + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true + }, + "layout": { + "allowEditing": false + }, + "html": false, + "interactivity": true, + "spacing": { + "blockGap": true, + "margin": true, + "padding": true + }, + "shadow": true, + "typography": { + "fontSize": true, + "lineHeight": true + } }, "attributes": { "tabsIndex": { "type": "integer", "default": 0 }, - "title": { - "type": "string", - "default": "" - }, "tabsActive": { "type": "integer", "default": 0 From c06a0a2d06fa04518c28c0048f8b7ec5d25fcfbb Mon Sep 17 00:00:00 2001 From: Milan Ricoul Date: Thu, 24 Sep 2026 14:48:27 +0200 Subject: [PATCH 03/30] feat(toolbar): share add/remove tab controls (#93541) --- .../ComposeBlockControls.js | 69 +-------- src/blockparty-tabs-panels/edit.js | 10 +- src/blockparty-tabs/TabsAddRemoveControls.js | 139 ++++++++++++++++++ src/blockparty-tabs/edit.js | 24 +-- 4 files changed, 157 insertions(+), 85 deletions(-) create mode 100644 src/blockparty-tabs/TabsAddRemoveControls.js diff --git a/src/blockparty-tabs-nav-item/ComposeBlockControls.js b/src/blockparty-tabs-nav-item/ComposeBlockControls.js index bea562b..b7bff6c 100644 --- a/src/blockparty-tabs-nav-item/ComposeBlockControls.js +++ b/src/blockparty-tabs-nav-item/ComposeBlockControls.js @@ -2,24 +2,18 @@ import { __ } from '@wordpress/i18n'; import { BlockControls } from '@wordpress/block-editor'; import { withDispatch, withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; -import { createBlock } from '@wordpress/blocks'; import { ToolbarGroup, ToolbarButton } from '@wordpress/components'; -import { - queryPaginationPrevious, - queryPaginationNext, - trash, -} from '@wordpress/icons'; import { shapes } from '@beapi/icons'; +import { TabsAddRemoveToolbar } from '../blockparty-tabs/TabsAddRemoveControls'; const ComposeBlockControls = ( { + clientId, hasIcon, hasIconBlock, index, count, onMoveDown, onMoveUp, - onRemoveBlocks, - onInsertBlock, setAttributes, } ) => ( @@ -27,7 +21,7 @@ const ComposeBlockControls = ( { controls={ [ { icon: 'arrow-left-alt2', - title: __( 'Move Item Before', 'blockparty-tabs' ), + title: __( 'Move tab before', 'blockparty-tabs' ), isDisabled: 0 === index, onClick: () => { onMoveUp( index ); @@ -35,7 +29,7 @@ const ComposeBlockControls = ( { }, { icon: 'arrow-right-alt2', - title: __( 'Move Item After', 'blockparty-tabs' ), + title: __( 'Move tab after', 'blockparty-tabs' ), isDisabled: count === index + 1, onClick: () => { onMoveDown( index ); @@ -54,31 +48,7 @@ const ComposeBlockControls = ( { } } /> - { - onInsertBlock( index ); - }, - }, - { - icon: queryPaginationNext, - title: __( 'Add Item After', 'blockparty-tabs' ), - onClick: () => { - onInsertBlock( index + 1 ); - }, - }, - { - icon: trash, - title: __( 'Delete Item', 'blockparty-tabs' ), - onClick: () => { - onRemoveBlocks( index ); - }, - }, - ] } - /> + ); @@ -105,14 +75,8 @@ export default compose( [ }; } ), withDispatch( ( dispatch, { nav, navId, panels, panelId } ) => { - const { - removeBlock, - moveBlocksDown, - moveBlocksUp, - insertBlock, - updateBlockAttributes, - } = dispatch( 'core/block-editor' ); - const LOCK_TEMPLATE = { lock: { move: true, remove: true } }; + const { moveBlocksDown, moveBlocksUp, updateBlockAttributes } = + dispatch( 'core/block-editor' ); return { onMoveDown( index ) { updateBlockAttributes( [ nav[ index ], panels[ index ] ], { @@ -134,25 +98,6 @@ export default compose( [ lock: { move: true, remove: true }, } ); }, - onRemoveBlocks( index ) { - updateBlockAttributes( [ nav[ index ], panels[ index ] ], { - lock: { move: true, remove: false }, - } ); - removeBlock( panels[ index ] ); - removeBlock( nav[ index ] ); - }, - onInsertBlock( index ) { - const newNavItem = createBlock( - 'blockparty/tabs-nav-item', - LOCK_TEMPLATE - ); - const newPanelItem = createBlock( - 'blockparty/tabs-panel-item', - LOCK_TEMPLATE - ); - insertBlock( newPanelItem, index, panelId ); - insertBlock( newNavItem, index, navId ); - }, }; } ), ] )( ComposeBlockControls ); diff --git a/src/blockparty-tabs-panels/edit.js b/src/blockparty-tabs-panels/edit.js index fcd8b60..dbbf98f 100644 --- a/src/blockparty-tabs-panels/edit.js +++ b/src/blockparty-tabs-panels/edit.js @@ -1,10 +1,11 @@ import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; +import { TabsAddRemoveBlockControls } from '../blockparty-tabs/TabsAddRemoveControls'; const BLOCKS_CHILD = 'blockparty/tabs-panel-item'; const ALLOWED_BLOCKS = [ BLOCKS_CHILD ]; const LOCK_TEMPLATE = { lock: { move: true, remove: true } }; -export default function Edit( {} ) { +export default function Edit( { clientId } ) { const blockProps = useBlockProps(); const innerBlocksProps = useInnerBlocksProps( blockProps, { allowedBlocks: ALLOWED_BLOCKS, @@ -19,5 +20,10 @@ export default function Edit( {} ) { renderAppender: false, } ); - return
; + return ( + <> + +
+ + ); } diff --git a/src/blockparty-tabs/TabsAddRemoveControls.js b/src/blockparty-tabs/TabsAddRemoveControls.js new file mode 100644 index 0000000..a331328 --- /dev/null +++ b/src/blockparty-tabs/TabsAddRemoveControls.js @@ -0,0 +1,139 @@ +/** + * Shared Add / Remove tab toolbar controls for the tabs block tree. + */ + +import { __ } from '@wordpress/i18n'; +import { BlockControls } from '@wordpress/block-editor'; +import { ToolbarGroup, ToolbarButton } from '@wordpress/components'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { createBlock } from '@wordpress/blocks'; + +const TABS_BLOCK_NAME = 'blockparty/tabs'; +const LOCK_TEMPLATE = { lock: { move: true, remove: true } }; + +/** + * Resolves the owning blockparty/tabs clientId from any block in the tree. + * Uses the closest ancestor so nested tabs stay scoped to their own root. + * + * @param {Function} selectStore Bound select from useSelect. + * @param {string} clientId Current block clientId. + * @return {string|null} Tabs root clientId, or null. + */ +function getTabsRootClientId( selectStore, clientId ) { + const { getBlockName, getBlockParents } = + selectStore( 'core/block-editor' ); + + if ( getBlockName( clientId ) === TABS_BLOCK_NAME ) { + return clientId; + } + + const parents = getBlockParents( clientId, true ); + for ( let i = 0; i < parents.length; i += 1 ) { + if ( getBlockName( parents[ i ] ) === TABS_BLOCK_NAME ) { + return parents[ i ]; + } + } + + return null; +} + +/** + * Toolbar group with Add tab / Remove tab buttons. + * + * @param {Object} props Component props. + * @param {string} props.clientId Block clientId (any block in the tabs tree). + * @param {number} [props.index] Explicit tab index; falls back to tabsActive. + * @return {JSX.Element|null} Toolbar group, or null when the tabs root is missing. + */ +export const TabsAddRemoveToolbar = ( { clientId, index } ) => { + const { navId, panelId, nav, panels, count, resolvedIndex } = useSelect( + ( selectStore ) => { + const { getBlockOrder, getBlockAttributes } = + selectStore( 'core/block-editor' ); + const tabsRootId = getTabsRootClientId( selectStore, clientId ); + + if ( ! tabsRootId ) { + return { + navId: null, + panelId: null, + nav: [], + panels: [], + count: 0, + resolvedIndex: 0, + }; + } + + const wrappers = getBlockOrder( tabsRootId ); + const navBlocks = getBlockOrder( wrappers[ 0 ] ); + const tabsActive = + getBlockAttributes( tabsRootId )?.tabsActive ?? 0; + + return { + navId: wrappers[ 0 ], + panelId: wrappers[ 1 ], + nav: navBlocks, + panels: getBlockOrder( wrappers[ 1 ] ), + count: navBlocks.length, + resolvedIndex: typeof index === 'number' ? index : tabsActive, + }; + }, + [ clientId, index ] + ); + + const { removeBlock, insertBlock, updateBlockAttributes } = + useDispatch( 'core/block-editor' ); + + if ( ! navId || ! panelId ) { + return null; + } + + const onInsertBlock = () => { + const insertAt = resolvedIndex + 1; + const newNavItem = createBlock( + 'blockparty/tabs-nav-item', + LOCK_TEMPLATE + ); + const newPanelItem = createBlock( + 'blockparty/tabs-panel-item', + LOCK_TEMPLATE + ); + insertBlock( newPanelItem, insertAt, panelId ); + insertBlock( newNavItem, insertAt, navId ); + }; + + const onRemoveBlocks = () => { + updateBlockAttributes( + [ nav[ resolvedIndex ], panels[ resolvedIndex ] ], + { + lock: { move: true, remove: false }, + } + ); + removeBlock( panels[ resolvedIndex ] ); + removeBlock( nav[ resolvedIndex ] ); + }; + + return ( + + + { __( 'Add tab', 'blockparty-tabs' ) } + + = count } onClick={ onRemoveBlocks }> + { __( 'Remove tab', 'blockparty-tabs' ) } + + + ); +}; + +/** + * BlockControls wrapper for Add / Remove tab actions. + * + * @param {Object} props Component props. + * @param {string} props.clientId Block clientId (any block in the tabs tree). + * @param {number} [props.index] Explicit tab index; falls back to tabsActive. + * @return {JSX.Element} Block controls with shared toolbar. + */ +export const TabsAddRemoveBlockControls = ( { clientId, index } ) => ( + + + +); diff --git a/src/blockparty-tabs/edit.js b/src/blockparty-tabs/edit.js index 0da9a0e..1e7e8b1 100644 --- a/src/blockparty-tabs/edit.js +++ b/src/blockparty-tabs/edit.js @@ -3,14 +3,11 @@ import classnames from 'classnames'; import { useBlockProps, useInnerBlocksProps, - InspectorControls, BlockControls, AlignmentControl, } from '@wordpress/block-editor'; -import { PanelBody, PanelRow, TextControl } from '@wordpress/components'; import { select } from '@wordpress/data'; import { - heading, justifyRight, justifyCenter, justifyLeft, @@ -20,6 +17,7 @@ import { } from '@wordpress/icons'; import './editor.scss'; import { useSyncTabsActiveForTabsBlock } from './SyncTabsActive'; +import { TabsAddRemoveToolbar } from './TabsAddRemoveControls'; const DEFAULT_TABS_POSITIONS = [ { @@ -63,7 +61,7 @@ const setTabsIndex = ( setAttributes, clientId ) => { export default function Edit( { attributes, setAttributes, clientId } ) { useSyncTabsActiveForTabsBlock( clientId ); setTabsIndex( setAttributes, clientId ); - const { title, mode } = attributes; + const { mode } = attributes; const blockProps = useBlockProps( { className: classnames( { [ `has-align-${ mode }` ]: mode, @@ -102,24 +100,8 @@ export default function Edit( { attributes, setAttributes, clientId } ) { setAttributes( { mode: newAlign } ) } /> + - - - - { - setAttributes( { title: content } ); - } } - /> - - -
); From 78fd99c4c94b9f1a9bd6362c5c708eac52a2c30b Mon Sep 17 00:00:00 2001 From: Milan Ricoul Date: Thu, 24 Sep 2026 14:48:33 +0200 Subject: [PATCH 04/30] feat(a11y): move tablist to nav and complete ARIA roles (#93541) --- src/blockparty-tabs-nav-item/deprecated.js | 58 ++++++++ src/blockparty-tabs-nav-item/edit.js | 72 ++++++---- src/blockparty-tabs-nav-item/index.js | 2 + src/blockparty-tabs-nav-item/save.js | 7 +- src/blockparty-tabs-nav/deprecated.js | 26 ++++ src/blockparty-tabs-nav/edit.js | 43 +++++- src/blockparty-tabs-nav/index.js | 10 +- src/blockparty-tabs-nav/save.js | 6 +- src/blockparty-tabs-panel-item/deprecated.js | 43 ++++++ src/blockparty-tabs-panel-item/edit.js | 32 +++-- src/blockparty-tabs-panel-item/index.js | 2 + src/blockparty-tabs-panel-item/save.js | 10 +- src/blockparty-tabs/GetSynchedID.js | 5 +- src/blockparty-tabs/deprecated.js | 137 +++++++++++++++++++ src/blockparty-tabs/index.js | 2 + src/blockparty-tabs/save.js | 5 +- 16 files changed, 400 insertions(+), 60 deletions(-) create mode 100644 src/blockparty-tabs-nav-item/deprecated.js create mode 100644 src/blockparty-tabs-nav/deprecated.js create mode 100644 src/blockparty-tabs-panel-item/deprecated.js create mode 100644 src/blockparty-tabs/deprecated.js diff --git a/src/blockparty-tabs-nav-item/deprecated.js b/src/blockparty-tabs-nav-item/deprecated.js new file mode 100644 index 0000000..2b927c5 --- /dev/null +++ b/src/blockparty-tabs-nav-item/deprecated.js @@ -0,0 +1,58 @@ +/** + * Deprecated versions of the tabs-nav-item block. + */ + +import { useBlockProps, InnerBlocks, RichText } from '@wordpress/block-editor'; + +/** + * Tab link without aria-selected / tabIndex in the saved markup. + */ +const v1 = { + attributes: { + label: { + type: 'string', + }, + hasIcon: { + type: 'boolean', + value: false, + }, + hasFocus: { + type: 'boolean', + value: false, + }, + linkId: { + type: 'string', + }, + panelId: { + type: 'string', + }, + index: { + type: 'integer', + }, + }, + supports: { + html: false, + visibility: false, + lock: false, + }, + save( { attributes } ) { + const { hasIcon, label, panelId, linkId } = attributes; + + return ( +
  • + + { hasIcon && } + + +
  • + ); + }, +}; + +export default [ v1 ]; diff --git a/src/blockparty-tabs-nav-item/edit.js b/src/blockparty-tabs-nav-item/edit.js index 762e5aa..cb8088a 100644 --- a/src/blockparty-tabs-nav-item/edit.js +++ b/src/blockparty-tabs-nav-item/edit.js @@ -5,6 +5,7 @@ import { getBlockType } from '@wordpress/blocks'; import { select } from '@wordpress/data'; import ComposeBlockControls from './ComposeBlockControls'; import getSynchedID from '../blockparty-tabs/GetSynchedID'; + export default function Edit( { attributes, setAttributes, @@ -41,11 +42,12 @@ export default function Edit( { ); const hasIconBlock = registeredIconBlocks.length > 0; const templateIconBlock = registeredIconBlocks[ 0 ]; - const { hasIcon, label, index } = attributes; - const TabsActive = context[ 'blockparty/TabsActive' ]; + const { hasIcon, label, index, panelId, linkId } = attributes; + const tabsActive = context?.[ 'blockparty/TabsActive' ]; + const isSelected = tabsActive === index; const blockProps = useBlockProps( { className: classnames( { - 'is-active': TabsActive === index, + 'is-active': isSelected, } ), } ); @@ -59,32 +61,48 @@ export default function Edit( { setAttributes={ setAttributes } />
  • - { hasIcon && hasIconBlock && ( - { + event.preventDefault(); + } } + > + { hasIcon && hasIconBlock && ( + + ) } + { + setAttributes( { label: content } ); + } } /> - ) } - { - setAttributes( { label: content } ); - } } - /> +
  • ); diff --git a/src/blockparty-tabs-nav-item/index.js b/src/blockparty-tabs-nav-item/index.js index 6cac13c..8d19a56 100644 --- a/src/blockparty-tabs-nav-item/index.js +++ b/src/blockparty-tabs-nav-item/index.js @@ -1,6 +1,7 @@ import { registerBlockType } from '@wordpress/blocks'; import Edit from './edit'; import save from './save'; +import deprecated from './deprecated'; import metadata from './block.json'; import { tabsMenuItem } from '@wordpress/icons'; @@ -8,4 +9,5 @@ registerBlockType( metadata.name, { icon: tabsMenuItem, edit: Edit, save, + deprecated, } ); diff --git a/src/blockparty-tabs-nav-item/save.js b/src/blockparty-tabs-nav-item/save.js index 6af393a..08ac1b9 100644 --- a/src/blockparty-tabs-nav-item/save.js +++ b/src/blockparty-tabs-nav-item/save.js @@ -1,7 +1,8 @@ import { useBlockProps, InnerBlocks, RichText } from '@wordpress/block-editor'; export default function save( { attributes } ) { - const { hasIcon, label, panelId, linkId } = attributes; + const { hasIcon, label, panelId, linkId, index } = attributes; + const isSelected = 0 === index; return (
  • @@ -9,7 +10,9 @@ export default function save( { attributes } ) { id={ linkId } role="tab" aria-controls={ panelId } - className={ 'wp-block-blockparty-tabs-nav-link' } + aria-selected={ isSelected } + tabIndex={ isSelected ? undefined : -1 } + className="wp-block-blockparty-tabs-nav-link" href={ '#' + linkId } > { hasIcon && } diff --git a/src/blockparty-tabs-nav/deprecated.js b/src/blockparty-tabs-nav/deprecated.js new file mode 100644 index 0000000..2338321 --- /dev/null +++ b/src/blockparty-tabs-nav/deprecated.js @@ -0,0 +1,26 @@ +/** + * Deprecated versions of the tabs-nav block. + */ + +import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; + +/** + * Nav list without role="tablist" (role lived on the parent tabs wrapper). + */ +const v1 = { + supports: { + html: false, + anchor: false, + align: false, + lock: false, + reusable: false, + }, + save() { + const innerBlocksProps = useInnerBlocksProps.save( + useBlockProps.save() + ); + return
      ; + }, +}; + +export default [ v1 ]; diff --git a/src/blockparty-tabs-nav/edit.js b/src/blockparty-tabs-nav/edit.js index 95f0a44..12d0285 100644 --- a/src/blockparty-tabs-nav/edit.js +++ b/src/blockparty-tabs-nav/edit.js @@ -1,11 +1,21 @@ -import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; +import { __ } from '@wordpress/i18n'; +import { + useBlockProps, + useInnerBlocksProps, + InspectorControls, +} from '@wordpress/block-editor'; +import { PanelBody, PanelRow, TextControl } from '@wordpress/components'; +import { TabsAddRemoveBlockControls } from '../blockparty-tabs/TabsAddRemoveControls'; const BLOCKS_CHILD = 'blockparty/tabs-nav-item'; const ALLOWED_BLOCKS = [ BLOCKS_CHILD ]; const LOCK_TEMPLATE = { lock: { move: true, remove: true } }; -export default function Edit() { - const blockProps = useBlockProps(); +export default function Edit( { attributes, setAttributes, clientId } ) { + const { ariaLabel } = attributes; + const blockProps = useBlockProps( { + role: 'tablist', + } ); const innerBlocksProps = useInnerBlocksProps( blockProps, { allowedBlocks: ALLOWED_BLOCKS, __experimentalDirectInsert: false, @@ -20,5 +30,30 @@ export default function Edit() { renderAppender: false, } ); - return
        ; + return ( + <> + + + + + { + setAttributes( { ariaLabel: content } ); + } } + /> + + + +
          + + ); } diff --git a/src/blockparty-tabs-nav/index.js b/src/blockparty-tabs-nav/index.js index 037e1c8..941027a 100644 --- a/src/blockparty-tabs-nav/index.js +++ b/src/blockparty-tabs-nav/index.js @@ -1,19 +1,13 @@ import { registerBlockType } from '@wordpress/blocks'; import Edit from './edit'; import save from './save'; +import deprecated from './deprecated'; import metadata from './block.json'; import { tabsMenu } from '@wordpress/icons'; registerBlockType( metadata.name, { icon: tabsMenu, - - /** - * @see ./edit.js - */ edit: Edit, - - /** - * @see ./save.js - */ save, + deprecated, } ); diff --git a/src/blockparty-tabs-nav/save.js b/src/blockparty-tabs-nav/save.js index fc980e8..e7121a4 100644 --- a/src/blockparty-tabs-nav/save.js +++ b/src/blockparty-tabs-nav/save.js @@ -1,6 +1,10 @@ import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; export default function save() { - const innerBlocksProps = useInnerBlocksProps.save( useBlockProps.save() ); + const innerBlocksProps = useInnerBlocksProps.save( + useBlockProps.save( { + role: 'tablist', + } ) + ); return
            ; } diff --git a/src/blockparty-tabs-panel-item/deprecated.js b/src/blockparty-tabs-panel-item/deprecated.js new file mode 100644 index 0000000..0a5fb46 --- /dev/null +++ b/src/blockparty-tabs-panel-item/deprecated.js @@ -0,0 +1,43 @@ +/** + * Deprecated versions of the tabs-panel-item block. + */ + +import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; + +/** + * Panel without active/hidden classes derived from tabsActive context. + */ +const v1 = { + attributes: { + linkId: { + type: 'string', + }, + panelId: { + type: 'string', + }, + index: { + type: 'integer', + }, + }, + supports: { + html: false, + visibility: false, + }, + save( { attributes } ) { + const { panelId, linkId } = attributes; + const blockProps = useBlockProps.save( { + role: 'tabpanel', + tabIndex: 0, + } ); + const innerBlocksProps = useInnerBlocksProps.save( { + className: 'wp-block-blockparty-tabs-panel-item__inner', + } ); + return ( +
            +
            +
            + ); + }, +}; + +export default [ v1 ]; diff --git a/src/blockparty-tabs-panel-item/edit.js b/src/blockparty-tabs-panel-item/edit.js index 4e155f0..043bc99 100644 --- a/src/blockparty-tabs-panel-item/edit.js +++ b/src/blockparty-tabs-panel-item/edit.js @@ -2,6 +2,8 @@ import classnames from 'classnames'; import { InnerBlocks, useBlockProps } from '@wordpress/block-editor'; import { select } from '@wordpress/data'; import getSynchedID from '../blockparty-tabs/GetSynchedID'; +import { TabsAddRemoveBlockControls } from '../blockparty-tabs/TabsAddRemoveControls'; + export default function Edit( { setAttributes, clientId, @@ -9,12 +11,17 @@ export default function Edit( { attributes, } ) { getSynchedID( clientId, context, setAttributes ); - const { index } = attributes; - const TabsActive = context[ 'blockparty/TabsActive' ]; + const { index, panelId, linkId } = attributes; + const tabsActive = context?.[ 'blockparty/TabsActive' ]; + const isSelected = tabsActive === index; const blockProps = useBlockProps( { className: classnames( { - 'is-active': TabsActive === index, + 'is-active': isSelected, } ), + role: 'tabpanel', + tabIndex: 0, + id: panelId, + 'aria-labelledby': linkId, } ); let allowedBlocks = []; const hasSupport = select( 'core/blocks' ).hasBlockSupport( @@ -44,13 +51,16 @@ export default function Edit( { } return ( -
            - -
            + <> + +
            + +
            + ); } diff --git a/src/blockparty-tabs-panel-item/index.js b/src/blockparty-tabs-panel-item/index.js index 2798640..5d975ab 100644 --- a/src/blockparty-tabs-panel-item/index.js +++ b/src/blockparty-tabs-panel-item/index.js @@ -1,6 +1,7 @@ import { registerBlockType } from '@wordpress/blocks'; import Edit from './edit'; import save from './save'; +import deprecated from './deprecated'; import metadata from './block.json'; import { homeButton } from '@wordpress/icons'; @@ -8,4 +9,5 @@ registerBlockType( metadata.name, { icon: homeButton, edit: Edit, save, + deprecated, } ); diff --git a/src/blockparty-tabs-panel-item/save.js b/src/blockparty-tabs-panel-item/save.js index 153b25b..ba593f2 100644 --- a/src/blockparty-tabs-panel-item/save.js +++ b/src/blockparty-tabs-panel-item/save.js @@ -1,13 +1,15 @@ import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; export default function save( { attributes } ) { - const { panelId, linkId } = attributes; + const { panelId, linkId, index } = attributes; + const isSelected = 0 === index; const blockProps = useBlockProps.save( { - role: `tabpanel`, - tabindex: 0, + role: 'tabpanel', + tabIndex: 0, + className: isSelected ? 'is-active' : 'is-hidden', } ); const innerBlocksProps = useInnerBlocksProps.save( { - className: `wp-block-blockparty-tabs-panel-item__inner`, + className: 'wp-block-blockparty-tabs-panel-item__inner', } ); return (
            diff --git a/src/blockparty-tabs/GetSynchedID.js b/src/blockparty-tabs/GetSynchedID.js index f0173c2..b0f4974 100644 --- a/src/blockparty-tabs/GetSynchedID.js +++ b/src/blockparty-tabs/GetSynchedID.js @@ -1,7 +1,10 @@ import { select } from '@wordpress/data'; const GetSynchedID = ( clientId, context, setAttributes ) => { - const InstanceId = context[ 'blockparty/Tabs' ]; + const InstanceId = context?.[ 'blockparty/Tabs' ]; + if ( typeof InstanceId === 'undefined' ) { + return; + } const currentIndex = select( 'core/block-editor' ).getBlockIndex( clientId ); const synkedId = InstanceId + '-' + currentIndex; diff --git a/src/blockparty-tabs/deprecated.js b/src/blockparty-tabs/deprecated.js new file mode 100644 index 0000000..a42ac76 --- /dev/null +++ b/src/blockparty-tabs/deprecated.js @@ -0,0 +1,137 @@ +/** + * Deprecated versions of the tabs block. + */ + +import classnames from 'classnames'; +import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; + +/** + * Moves a legacy parent label onto the tabs-nav inner block as ariaLabel. + * + * @param {Object} attributes Parent block attributes. + * @param {Array} innerBlocks Parent inner blocks. + * @param {string} label Label to apply on the nav block. + * @return {Array} Migrated [ attributes, innerBlocks ]. + */ +function migrateLabelToNav( attributes, innerBlocks, label ) { + const { title, ariaLabel, ...rest } = attributes; + const value = label || ariaLabel || title || ''; + + if ( ! value || ! Array.isArray( innerBlocks ) ) { + return [ rest, innerBlocks ]; + } + + const nextInnerBlocks = innerBlocks.map( ( block ) => { + if ( block.name !== 'blockparty/tabs-nav' ) { + return block; + } + + return { + ...block, + attributes: { + ...block.attributes, + ariaLabel: value, + }, + }; + } ); + + return [ rest, nextInnerBlocks ]; +} + +/** + * Parent wrapper with role="tablist" and supports.ariaLabel (pre-nav move). + */ +const v2 = { + attributes: { + tabsIndex: { + type: 'integer', + default: 0, + }, + tabsActive: { + type: 'integer', + default: 0, + }, + mode: { + type: 'string', + default: 'top-left', + }, + ariaLabel: { + type: 'string', + source: 'attribute', + attribute: 'aria-label', + selector: '*', + }, + }, + supports: { + html: false, + anchor: true, + align: [ 'wide', 'full' ], + ariaLabel: true, + }, + migrate( attributes, innerBlocks ) { + return migrateLabelToNav( attributes, innerBlocks ); + }, + save( { attributes } ) { + const { mode } = attributes; + + const innerBlocksProps = useInnerBlocksProps.save( + useBlockProps.save( { + className: classnames( { + [ `has-align-${ mode }` ]: mode, + } ), + role: 'tablist', + } ) + ); + + return
            ; + }, +}; + +/** + * Original parent wrapper using a custom title attribute as aria-label. + */ +const v1 = { + attributes: { + tabsIndex: { + type: 'integer', + default: 0, + }, + title: { + type: 'string', + default: '', + }, + tabsActive: { + type: 'integer', + default: 0, + }, + mode: { + type: 'string', + default: 'top-left', + }, + }, + supports: { + html: false, + anchor: true, + align: [ 'wide', 'full' ], + }, + migrate( attributes, innerBlocks ) { + return migrateLabelToNav( attributes, innerBlocks, attributes.title ); + }, + save( { attributes } ) { + const { title, mode } = attributes; + + const innerBlocksProps = useInnerBlocksProps.save( + useBlockProps.save( { + className: classnames( { + [ `has-align-${ mode }` ]: mode, + } ), + } ) + ); + + return ( +
            + ); + }, +}; + +export default [ v2, v1 ]; diff --git a/src/blockparty-tabs/index.js b/src/blockparty-tabs/index.js index 5fea10b..c307ad9 100644 --- a/src/blockparty-tabs/index.js +++ b/src/blockparty-tabs/index.js @@ -2,6 +2,7 @@ import { registerBlockType } from '@wordpress/blocks'; import './style.scss'; import Edit from './edit'; import save from './save'; +import deprecated from './deprecated'; import metadata from './block.json'; import { tabs } from '@wordpress/icons'; @@ -9,4 +10,5 @@ registerBlockType( metadata.name, { icon: tabs, edit: Edit, save, + deprecated, } ); diff --git a/src/blockparty-tabs/save.js b/src/blockparty-tabs/save.js index 20e348d..f24c6fd 100644 --- a/src/blockparty-tabs/save.js +++ b/src/blockparty-tabs/save.js @@ -2,7 +2,7 @@ import classnames from 'classnames'; import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; export default function save( { attributes } ) { - const { title, mode } = attributes; + const { mode } = attributes; const innerBlocksProps = useInnerBlocksProps.save( useBlockProps.save( { @@ -11,5 +11,6 @@ export default function save( { attributes } ) { } ), } ) ); - return
            ; + + return
            ; } From 99d1477eaf81c32ab4f3c4ff75ee95b33ac6e64a Mon Sep 17 00:00:00 2001 From: Milan Ricoul Date: Thu, 24 Sep 2026 15:01:45 +0200 Subject: [PATCH 05/30] feat(nav): add appender to insert synced tabs (#93541) --- src/blockparty-tabs-nav/edit.js | 7 +- src/blockparty-tabs/TabsAddRemoveControls.js | 89 ++++++++++++++++---- src/blockparty-tabs/editor.scss | 32 +++++++ 3 files changed, 109 insertions(+), 19 deletions(-) diff --git a/src/blockparty-tabs-nav/edit.js b/src/blockparty-tabs-nav/edit.js index 12d0285..b7903a6 100644 --- a/src/blockparty-tabs-nav/edit.js +++ b/src/blockparty-tabs-nav/edit.js @@ -5,7 +5,10 @@ import { InspectorControls, } from '@wordpress/block-editor'; import { PanelBody, PanelRow, TextControl } from '@wordpress/components'; -import { TabsAddRemoveBlockControls } from '../blockparty-tabs/TabsAddRemoveControls'; +import { + TabsAddRemoveBlockControls, + TabsNavAppender, +} from '../blockparty-tabs/TabsAddRemoveControls'; const BLOCKS_CHILD = 'blockparty/tabs-nav-item'; const ALLOWED_BLOCKS = [ BLOCKS_CHILD ]; @@ -27,7 +30,7 @@ export default function Edit( { attributes, setAttributes, clientId } ) { [ BLOCKS_CHILD, LOCK_TEMPLATE ], ], templateInsertUpdatesSelection: true, - renderAppender: false, + renderAppender: () => , } ); return ( diff --git a/src/blockparty-tabs/TabsAddRemoveControls.js b/src/blockparty-tabs/TabsAddRemoveControls.js index a331328..d3fc549 100644 --- a/src/blockparty-tabs/TabsAddRemoveControls.js +++ b/src/blockparty-tabs/TabsAddRemoveControls.js @@ -4,9 +4,10 @@ import { __ } from '@wordpress/i18n'; import { BlockControls } from '@wordpress/block-editor'; -import { ToolbarGroup, ToolbarButton } from '@wordpress/components'; +import { ToolbarGroup, ToolbarButton, Button } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; import { createBlock } from '@wordpress/blocks'; +import { plus } from '@wordpress/icons'; const TABS_BLOCK_NAME = 'blockparty/tabs'; const LOCK_TEMPLATE = { lock: { move: true, remove: true } }; @@ -38,14 +39,13 @@ function getTabsRootClientId( selectStore, clientId ) { } /** - * Toolbar group with Add tab / Remove tab buttons. + * Hook exposing insert / remove / append helpers for a tabs tree. * - * @param {Object} props Component props. - * @param {string} props.clientId Block clientId (any block in the tabs tree). - * @param {number} [props.index] Explicit tab index; falls back to tabsActive. - * @return {JSX.Element|null} Toolbar group, or null when the tabs root is missing. + * @param {string} clientId Block clientId (any block in the tabs tree). + * @param {number} [index] Explicit tab index; falls back to tabsActive. + * @return {Object} Tabs mutation helpers and resolved state. */ -export const TabsAddRemoveToolbar = ( { clientId, index } ) => { +export const useTabsMutations = ( clientId, index ) => { const { navId, panelId, nav, panels, count, resolvedIndex } = useSelect( ( selectStore ) => { const { getBlockOrder, getBlockAttributes } = @@ -83,12 +83,11 @@ export const TabsAddRemoveToolbar = ( { clientId, index } ) => { const { removeBlock, insertBlock, updateBlockAttributes } = useDispatch( 'core/block-editor' ); - if ( ! navId || ! panelId ) { - return null; - } + const insertTab = ( insertAt ) => { + if ( ! navId || ! panelId ) { + return; + } - const onInsertBlock = () => { - const insertAt = resolvedIndex + 1; const newNavItem = createBlock( 'blockparty/tabs-nav-item', LOCK_TEMPLATE @@ -97,11 +96,23 @@ export const TabsAddRemoveToolbar = ( { clientId, index } ) => { 'blockparty/tabs-panel-item', LOCK_TEMPLATE ); - insertBlock( newPanelItem, insertAt, panelId ); - insertBlock( newNavItem, insertAt, navId ); + insertBlock( newPanelItem, insertAt, panelId, false ); + insertBlock( newNavItem, insertAt, navId, true ); + }; + + const insertTabAfterActive = () => { + insertTab( resolvedIndex + 1 ); }; - const onRemoveBlocks = () => { + const appendTab = () => { + insertTab( count ); + }; + + const removeTab = () => { + if ( ! navId || ! panelId || 1 >= count ) { + return; + } + updateBlockAttributes( [ nav[ resolvedIndex ], panels[ resolvedIndex ] ], { @@ -112,18 +123,62 @@ export const TabsAddRemoveToolbar = ( { clientId, index } ) => { removeBlock( nav[ resolvedIndex ] ); }; + return { + count, + resolvedIndex, + canRemove: 1 < count, + insertTabAfterActive, + appendTab, + removeTab, + }; +}; + +/** + * Toolbar group with Add tab / Remove tab buttons. + * + * @param {Object} props Component props. + * @param {string} props.clientId Block clientId (any block in the tabs tree). + * @param {number} [props.index] Explicit tab index; falls back to tabsActive. + * @return {JSX.Element} Toolbar group. + */ +export const TabsAddRemoveToolbar = ( { clientId, index } ) => { + const { canRemove, insertTabAfterActive, removeTab } = useTabsMutations( + clientId, + index + ); + return ( - + { __( 'Add tab', 'blockparty-tabs' ) } - = count } onClick={ onRemoveBlocks }> + { __( 'Remove tab', 'blockparty-tabs' ) } ); }; +/** + * Button-style appender that inserts a synced nav item + panel item. + * + * @param {Object} props Component props. + * @param {string} props.clientId tabs-nav block clientId. + * @return {JSX.Element} Appender button. + */ +export const TabsNavAppender = ( { clientId } ) => { + const { appendTab } = useTabsMutations( clientId ); + + return ( +