From 3599a0e019910eaae89166266031bb08f272acf4 Mon Sep 17 00:00:00 2001 From: mricoul Date: Fri, 7 Nov 2025 17:16:17 +0100 Subject: [PATCH 1/6] docs (README): adds initial README for the plugin Creates a comprehensive README file to document the Blockparty Tabs plugin, including features, installation instructions, usage guidelines, accessibility details, and development setup. This aims to provide users with clear instructions and information to effectively use and contribute to the plugin. --- README.md | 140 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 139 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ef6d260..a9a5963 100644 --- a/README.md +++ b/README.md @@ -1 +1,139 @@ -# Blockparty Tabs \ No newline at end of file +# Blockparty Tabs + +An accessible tabs block for WordPress Gutenberg editor that follows ARIA best practices. + +## Features + +- ✅ **Accessible**: Built with proper ARIA attributes and keyboard navigation support +- 🎨 **Customizable**: Add custom colors and icons to your tabs +- 🧩 **Flexible**: Nest any WordPress block inside tab panels +- ⚡ **Easy to use**: Simple interface to add, remove, and reorder tabs + +## Requirements + +- WordPress 6.2 or higher +- PHP 8.1 or higher +- Gutenberg editor enabled + +## Installation + +### Manual Installation + +1. Download the plugin files +2. Upload the `blockparty-tabs` folder to the `/wp-content/plugins/` directory +3. Activate the plugin through the 'Plugins' menu in WordPress + +### Composer Installation + +```bash +composer require beapi/blockparty-tabs +``` + +## Usage + +### Adding a Tabs Block + +1. In the WordPress editor, click the **+** button to add a new block +2. Search for "Tabs" or "Blockparty Tabs" +3. Click on the block to insert it into your content + +By default, the block comes with 3 tabs. Each tab contains a panel where you can add any content. + +### Adding/Removing Tabs + +- **Add a tab**: Click the "Add Item After" button in the block toolbar +- **Remove a tab**: Select the tab you want to remove and click the trash icon in the toolbar + +### Customizing Tabs + +#### Adding Icons + +1. Select a tab item +2. In the block toolbar, click the "Icon" button +3. Choose an icon from the available options + +#### Changing Colors + +1. Select the tabs block +2. Use the color settings in the right sidebar to customize: + - Tab background colors + - Text colors + - Active tab colors + +### Adding Content to Tabs + +1. Click inside a tab panel +2. Add any WordPress block (paragraphs, images, buttons, etc.) +3. You can nest multiple blocks within each tab panel + +## Accessibility + +The plugin generates semantic HTML with proper ARIA attributes: + +- `role="tablist"` for the tabs container +- `role="tab"` for each tab button +- `role="tabpanel"` for each content panel +- Proper `aria-controls`, `aria-labelledby`, and `aria-selected` attributes +- Keyboard navigation support (Arrow keys, Tab, Enter) + +## Generated Markup Example + +```html +
+ + +
+
+
+ +
+
+ +
+
+``` + +## Development + +### Building the Plugin + +```bash +npm install +npm run build +``` + +### Development Mode + +```bash +npm start +``` + +### Linting + +```bash +npm run lint:js +npm run lint:css +``` + +## Support + +For bug reports and feature requests, please use the [GitHub issues](https://github.com/BeAPI/blockparty-tabs/issues) page. + +## Credits + +Developed by [Be API Technical Team](https://beapi.fr) + +## License + +GPL-2.0-or-later From 865716d20560cf68e7b3dba022a79ddd7a3a2995 Mon Sep 17 00:00:00 2001 From: Marie Comet Date: Wed, 11 Feb 2026 11:46:17 +0100 Subject: [PATCH 2/6] relase 1.0.6 : allow to use nested tabs block --- .plugin-data | 2 +- CHANGELOG.md | 4 + blockparty-tabs.php | 4 +- package.json | 2 +- readme.txt | 4 + src/blockparty-tabs-nav-item/block.json | 2 +- src/blockparty-tabs-nav-item/edit.js | 2 + src/blockparty-tabs-nav/block.json | 2 +- src/blockparty-tabs-panel-item/block.json | 2 +- src/blockparty-tabs-panel-item/edit.js | 2 + src/blockparty-tabs-panels/block.json | 2 +- src/blockparty-tabs/SyncTabsActive.js | 104 ++++++++++++++++++++++ src/blockparty-tabs/TabsFocus.js | 44 --------- src/blockparty-tabs/block.json | 2 +- src/blockparty-tabs/edit.js | 2 - src/blockparty-tabs/script.js | 13 ++- 16 files changed, 131 insertions(+), 62 deletions(-) create mode 100644 src/blockparty-tabs/SyncTabsActive.js delete mode 100644 src/blockparty-tabs/TabsFocus.js diff --git a/.plugin-data b/.plugin-data index dca7deb..c4ee859 100644 --- a/.plugin-data +++ b/.plugin-data @@ -1,4 +1,4 @@ { - "version": "1.0.5", + "version": "1.0.6", "slug": "blockparty-tabs" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 0eeec90..fe7011a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 1.0.6 - 2026-02-11 + +- Update view and edit script to allow to use nested tabs block (tabs inside tab panel). + ## 1.0.5 - 2025-11-10 - Update block icons diff --git a/blockparty-tabs.php b/blockparty-tabs.php index 0b48737..fbcefff 100644 --- a/blockparty-tabs.php +++ b/blockparty-tabs.php @@ -4,7 +4,7 @@ * Description: Accessible Tabs block for WordPress gutenberg. * Requires at least: 6.2 * Requires PHP: 8.1 - * Version: 1.0.5 + * Version: 1.0.6 * Author: Be API Technical team * Author URI: https://beapi.fr * License: GPL-2.0-or-later @@ -14,7 +14,7 @@ namespace Blockparty\Tabs; -define( 'BLOCKPARTY_TABS_VERSION', '1.0.5' ); +define( 'BLOCKPARTY_TABS_VERSION', '1.0.6' ); define( 'BLOCKPARTY_TABS_URL', plugin_dir_url( __FILE__ ) ); define( 'BLOCKPARTY_TABS_DIR', plugin_dir_path( __FILE__ ) ); define( 'BLOCKPARTY_TABS_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); diff --git a/package.json b/package.json index e1dfa1f..82bdcf0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blockparty-tabs", - "version": "1.0.5", + "version": "1.0.6", "description": "Accessible tabs block for WordPress", "author": "Be API Technical team", "license": "GPL-2.0-or-later", diff --git a/readme.txt b/readme.txt index 843065a..c6148a1 100644 --- a/readme.txt +++ b/readme.txt @@ -31,6 +31,10 @@ directory take precedence. For example, `/assets/screenshot-1.png` would win ove == Changelog == += 1.0.6 = + +* Update view and edit script to allow to use nested tabs block (tabs inside tab panel). + = 1.0.5 = * Update block icons diff --git a/src/blockparty-tabs-nav-item/block.json b/src/blockparty-tabs-nav-item/block.json index 4e32968..dc0bbb8 100644 --- a/src/blockparty-tabs-nav-item/block.json +++ b/src/blockparty-tabs-nav-item/block.json @@ -2,7 +2,7 @@ "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "blockparty/tabs-nav-item", - "version": "1.0.5", + "version": "1.0.6", "title": "Tab", "category": "widgets", "description": "Accessible tabs block item", diff --git a/src/blockparty-tabs-nav-item/edit.js b/src/blockparty-tabs-nav-item/edit.js index 2dc11e9..e044d51 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'; +import SyncTabsActive from '../blockparty-tabs/SyncTabsActive'; export default function Edit( { attributes, @@ -47,6 +48,7 @@ export default function Edit( { return ( <> + + { + const { + getBlockName, + getBlockIndex, + getSelectedBlockClientId, + getBlockParents, + getBlockRootClientId, + } = select( 'core/block-editor' ); + const selected = getSelectedBlockClientId(); + if ( ! selected ) { + return { + selectedBlockClientId: null, + closestTabItemId: null, + tabsBlockId: null, + myIndex: 0, + }; + } + // Find the tab item closest to the selection: the selected block if it + // is a tab item, otherwise the first tab item in the ancestor chain. + let closest = null; + if ( + TAB_ITEM_NAMES.indexOf( getBlockName( selected ) ) !== -1 + ) { + closest = selected; + } else { + const parents = getBlockParents( selected ); + for ( let i = 0; i < parents.length; i += 1 ) { + if ( + TAB_ITEM_NAMES.indexOf( + getBlockName( parents[ i ] ) + ) !== -1 + ) { + closest = parents[ i ]; + break; + } + } + } + // Parent chain: this block → tabs-nav or tabs-panels → blockparty/tabs. + const directParent = getBlockRootClientId( clientId ); + const tabsId = directParent + ? getBlockRootClientId( directParent ) + : null; + return { + selectedBlockClientId: selected, + closestTabItemId: closest, + tabsBlockId: tabsId, + myIndex: getBlockIndex( clientId ), + }; + }, + [ clientId ] + ); + + const { updateBlockAttributes } = useDispatch( 'core/block-editor' ); + + // When this block is the active tab (closest to selection or selected), sync tabsActive. + useEffect( () => { + if ( ! selectedBlockClientId || ! tabsBlockId ) { + return; + } + const isActiveTab = + closestTabItemId === clientId || selectedBlockClientId === clientId; + if ( ! isActiveTab ) { + return; + } + updateBlockAttributes( tabsBlockId, { tabsActive: myIndex } ); + }, [ + selectedBlockClientId, + closestTabItemId, + clientId, + tabsBlockId, + myIndex, + ] ); + + return null; +} diff --git a/src/blockparty-tabs/TabsFocus.js b/src/blockparty-tabs/TabsFocus.js deleted file mode 100644 index ddd229d..0000000 --- a/src/blockparty-tabs/TabsFocus.js +++ /dev/null @@ -1,44 +0,0 @@ -import { withSelect } from '@wordpress/data'; -import { compose } from '@wordpress/compose'; - -const TabsFocus = ( { activeTab, setAttributes } ) => { - if ( activeTab >= 0 ) { - setAttributes( { tabsActive: activeTab } ); - } - return null; -}; - -export default compose( [ - withSelect( ( select, ownProps ) => { - const { - getBlockName, - getBlockIndex, - getSelectedBlockClientId, - getBlockParentsByBlockName, - hasSelectedInnerBlock, - } = select( 'core/block-editor' ); - if ( ! hasSelectedInnerBlock( ownProps.clientId, true ) ) { - return { - activeTab: -1, - setAttributes: ownProps.setAttributes, - }; - } - const selected = getSelectedBlockClientId(); - const name = getBlockName( selected ); - let currentIndex = getBlockIndex( selected ); - if ( - 'blockparty/tabs-panel-item' !== name && - 'blockparty/tabs-nav-item' !== name - ) { - const parents = getBlockParentsByBlockName( selected, [ - 'blockparty/tabs-nav-item', - 'blockparty/tabs-panel-item', - ] ); - currentIndex = getBlockIndex( parents[ 0 ] ); - } - return { - activeTab: currentIndex, - setAttributes: ownProps.setAttributes, - }; - } ), -] )( TabsFocus ); diff --git a/src/blockparty-tabs/block.json b/src/blockparty-tabs/block.json index b6e6f31..c5830d4 100644 --- a/src/blockparty-tabs/block.json +++ b/src/blockparty-tabs/block.json @@ -2,7 +2,7 @@ "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "blockparty/tabs", - "version": "1.0.5", + "version": "1.0.6", "title": "Tabs", "category": "widgets", "description": "Accessible tabs block", diff --git a/src/blockparty-tabs/edit.js b/src/blockparty-tabs/edit.js index fd56104..edea034 100644 --- a/src/blockparty-tabs/edit.js +++ b/src/blockparty-tabs/edit.js @@ -9,7 +9,6 @@ import { } from '@wordpress/block-editor'; import { PanelBody, PanelRow, TextControl } from '@wordpress/components'; import { select } from '@wordpress/data'; -import TabsFocus from './TabsFocus'; import { heading, justifyRight, @@ -119,7 +118,6 @@ export default function Edit( { attributes, setAttributes, clientId } ) { -
); diff --git a/src/blockparty-tabs/script.js b/src/blockparty-tabs/script.js index 38f8623..8d02185 100644 --- a/src/blockparty-tabs/script.js +++ b/src/blockparty-tabs/script.js @@ -18,18 +18,17 @@ class TabsAutomatic { this.firstTab = null; this.lastTab = null; - this.tabs = Array.from( - this.tablistNode.querySelectorAll( '[role=tab]' ) + this.tabnav = this.tablistNode.querySelector( + '.wp-block-blockparty-tabs-nav' ); + this.tabs = Array.from( this.tabnav.querySelectorAll( 'a[role=tab]' ) ); + this.tabpanelsNode = this.tabnav.nextElementSibling; + this.tabpanels = []; for ( let i = 0; i < this.tabs.length; i += 1 ) { const tab = this.tabs[ i ]; - const tabpanel = this.tablistNode - .closest( '.wp-block-blockparty-tabs' ) - .querySelectorAll( '.wp-block-blockparty-tabs-panels > *' )[ - i - ]; + const tabpanel = this.tabpanelsNode.children[ i ]; tab.tabIndex = -1; tab.setAttribute( 'aria-selected', 'false' ); From d8b4f3546206442a9d79736f99c4550b00807cf4 Mon Sep 17 00:00:00 2001 From: Marie Comet Date: Wed, 11 Feb 2026 12:01:47 +0100 Subject: [PATCH 3/6] fix jsdoc block and add missing useEffect depedencie --- src/blockparty-tabs/SyncTabsActive.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/blockparty-tabs/SyncTabsActive.js b/src/blockparty-tabs/SyncTabsActive.js index d60101b..a0cbb5f 100644 --- a/src/blockparty-tabs/SyncTabsActive.js +++ b/src/blockparty-tabs/SyncTabsActive.js @@ -98,6 +98,7 @@ export default function SyncTabsActive( { clientId } ) { clientId, tabsBlockId, myIndex, + updateBlockAttributes, ] ); return null; From 129ed7e60e659f174f2c0928ec7fcdbb7dc30d01 Mon Sep 17 00:00:00 2001 From: Marie Comet Date: Wed, 11 Feb 2026 12:07:33 +0100 Subject: [PATCH 4/6] fix jsdoc block --- src/blockparty-tabs/SyncTabsActive.js | 32 +++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/blockparty-tabs/SyncTabsActive.js b/src/blockparty-tabs/SyncTabsActive.js index a0cbb5f..bf4f7a8 100644 --- a/src/blockparty-tabs/SyncTabsActive.js +++ b/src/blockparty-tabs/SyncTabsActive.js @@ -1,20 +1,20 @@ /** - * Syncs the active tab state with the block selection in the editor. - * - * When this tab item (nav-item or panel-item) is the one selected or is the - * closest tab item to the current block selection, updates the parent - * blockparty/tabs block's tabsActive attribute to this item's index. - * - * This direct link (the clicked block updates its own tabs block) ensures - * correct behavior with nested tabs: only the tabs block that owns the - * selected tab is updated. - * - * @since 1.0.6 - * - * @param {Object} props Component props. - * @param {string} props.clientId This block's clientId (tabs-nav-item or tabs-panel-item). - * @return {null} Renders nothing. - */ +* Syncs the active tab state with the block selection in the editor. +* +* When this tab item (nav-item or panel-item) is the one selected or is the +* closest tab item to the current block selection, updates the parent +* blockparty/tabs block's tabsActive attribute to this item's index. +* +* This direct link (the clicked block updates its own tabs block) ensures +* correct behavior with nested tabs: only the tabs block that owns the +* selected tab is updated. +* +* @since 1.0.6 +* +* @param {Object} props Component props. +* @param {string} props.clientId This block's clientId (tabs-nav-item or tabs-panel-item). +* @return {null} Renders nothing. +*/ import { useEffect } from '@wordpress/element'; import { useSelect, useDispatch } from '@wordpress/data'; From 88e9534ac8e2d72fd9be964dccf268a109226700 Mon Sep 17 00:00:00 2001 From: Marie Comet Date: Wed, 11 Feb 2026 13:40:22 +0100 Subject: [PATCH 5/6] fix jsdoc block --- src/blockparty-tabs/SyncTabsActive.js | 32 +++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/blockparty-tabs/SyncTabsActive.js b/src/blockparty-tabs/SyncTabsActive.js index bf4f7a8..a0cbb5f 100644 --- a/src/blockparty-tabs/SyncTabsActive.js +++ b/src/blockparty-tabs/SyncTabsActive.js @@ -1,20 +1,20 @@ /** -* Syncs the active tab state with the block selection in the editor. -* -* When this tab item (nav-item or panel-item) is the one selected or is the -* closest tab item to the current block selection, updates the parent -* blockparty/tabs block's tabsActive attribute to this item's index. -* -* This direct link (the clicked block updates its own tabs block) ensures -* correct behavior with nested tabs: only the tabs block that owns the -* selected tab is updated. -* -* @since 1.0.6 -* -* @param {Object} props Component props. -* @param {string} props.clientId This block's clientId (tabs-nav-item or tabs-panel-item). -* @return {null} Renders nothing. -*/ + * Syncs the active tab state with the block selection in the editor. + * + * When this tab item (nav-item or panel-item) is the one selected or is the + * closest tab item to the current block selection, updates the parent + * blockparty/tabs block's tabsActive attribute to this item's index. + * + * This direct link (the clicked block updates its own tabs block) ensures + * correct behavior with nested tabs: only the tabs block that owns the + * selected tab is updated. + * + * @since 1.0.6 + * + * @param {Object} props Component props. + * @param {string} props.clientId This block's clientId (tabs-nav-item or tabs-panel-item). + * @return {null} Renders nothing. + */ import { useEffect } from '@wordpress/element'; import { useSelect, useDispatch } from '@wordpress/data'; From d50113f64b42b1b170c6d6c132eecb121eae8f35 Mon Sep 17 00:00:00 2001 From: Marie Comet Date: Wed, 11 Feb 2026 13:43:24 +0100 Subject: [PATCH 6/6] fix jsdoc block again' --- src/blockparty-tabs/SyncTabsActive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blockparty-tabs/SyncTabsActive.js b/src/blockparty-tabs/SyncTabsActive.js index a0cbb5f..9785752 100644 --- a/src/blockparty-tabs/SyncTabsActive.js +++ b/src/blockparty-tabs/SyncTabsActive.js @@ -1,4 +1,4 @@ -/** + /** * Syncs the active tab state with the block selection in the editor. * * When this tab item (nav-item or panel-item) is the one selected or is the