Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/block-library/src/editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
@use "./social-link/editor.scss" as *;
@use "./social-links/editor.scss" as *;
@use "./spacer/editor.scss" as *;
@use "./tab-list/editor.scss" as *;
@use "./table/editor.scss" as *;
@use "./template-part/editor.scss" as *;
@use "./term-template/editor.scss" as *;
Expand Down
3 changes: 2 additions & 1 deletion packages/block-library/src/tab-list/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,6 @@
"padding": ".wp-block-tab-list button"
}
},
"style": "wp-block-tab-list"
"style": "wp-block-tab-list",
"editorStyle": "wp-block-tab-list-editor"
}
2 changes: 2 additions & 0 deletions packages/block-library/src/tab-list/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { useEffect, useMemo, useRef } from '@wordpress/element';
* Internal dependencies
*/
import TabToolbarControls from '../tabs/tab-toolbar-controls';
import TabMovers from './tab-movers';
import useTabActions from '../tabs/use-tab-actions';
import { useToolsPanelDropdownMenuProps } from '../utils/hooks';

Expand Down Expand Up @@ -199,6 +200,7 @@ function Edit( {
</ToolsPanelItem>
</ToolsPanel>
</InspectorControls>
<TabMovers tabsClientId={ tabsClientId } />
<TabToolbarControls tabsClientId={ tabsClientId } />
<div { ...blockProps }>
{ tabsList.map( ( tab, index ) => {
Expand Down
9 changes: 9 additions & 0 deletions packages/block-library/src/tab-list/editor.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@use "@wordpress/base-styles/mixins" as *;
@use "@wordpress/base-styles/variables" as *;

.wp-block-tab-list__mover-button {
@include break-small() {
width: $button-size-small;
min-width: 0 !important;
}
}
Comment on lines +4 to +9

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The button needs to resize according to the viewport, similar to the block mover button.

Image Image

72 changes: 72 additions & 0 deletions packages/block-library/src/tab-list/tab-movers.js

@ciampo ciampo Jul 14, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One way to avoid having to say "right" and "left" is to use words such as "previous" and "next", or "before" and "after", etc

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds like a good simplification to me!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice idea! Let's adopt that word.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6ba3ae2

LTR

image image

RTL

image image

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A clever solution.

Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* WordPress dependencies
*/
import { __, isRTL } from '@wordpress/i18n';
import {
BlockControls,
store as blockEditorStore,
} from '@wordpress/block-editor';
import { ToolbarButton } from '@wordpress/components';
import { chevronLeft, chevronRight } from '@wordpress/icons';
import { useSelect } from '@wordpress/data';

/**
* Internal dependencies
*/
import useTabActions from '../tabs/use-tab-actions';

/**
* Block toolbar buttons that reorder the active tab one position left or right.
* Reordering acts on the underlying core/tab-panel blocks; the tab-list labels
* follow their panel automatically via useTabListItemsSync. The buttons are
* disabled at the first/last position.
*
* @param {Object} props
* @param {string} props.tabsClientId The client ID of the parent tabs block.
* @return {React.JSX.Element} The toolbar control element.
*/
export default function TabMovers( { tabsClientId } ) {
const { moveTab } = useTabActions( tabsClientId );
const { tabCount, activeIndex } = useSelect(
( select ) => {
if ( ! tabsClientId ) {
return { tabCount: 0, activeIndex: 0 };
}
const { getBlocks, getBlockAttributes } =
select( blockEditorStore );
const tabsAttributes = getBlockAttributes( tabsClientId );
const tabPanels = getBlocks( tabsClientId ).find(
( block ) => block.name === 'core/tab-panels'
);
return {
tabCount: tabPanels?.innerBlocks.length ?? 0,
activeIndex:
tabsAttributes?.editorActiveTabIndex ??
tabsAttributes?.activeTabIndex ??
0,
};
},
[ tabsClientId ]
);

return (
<BlockControls group="parent">
<ToolbarButton
className="wp-block-tab-list__mover-button"
icon={ isRTL() ? chevronRight : chevronLeft }
label={ __( 'Move tab left' ) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these labels also get the isRTL check?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

You're right, this label needs to be flipped as well.

Fixed in 09637ca

onClick={ () => moveTab( -1 ) }
disabled={ activeIndex <= 0 }
accessibleWhenDisabled
/>
<ToolbarButton
className="wp-block-tab-list__mover-button"
icon={ isRTL() ? chevronLeft : chevronRight }
label={ __( 'Move tab right' ) }
onClick={ () => moveTab( 1 ) }
disabled={ activeIndex >= tabCount - 1 }
accessibleWhenDisabled
/>
</BlockControls>
);
}
35 changes: 32 additions & 3 deletions packages/block-library/src/tabs/use-tab-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,21 @@ import { store as blockEditorStore } from '@wordpress/block-editor';
import { useDispatch, useRegistry } from '@wordpress/data';

/**
* Provides callbacks to insert and remove tabs for a tabs block.
* Provides callbacks to insert, remove and move tabs for a tabs block.
*
* The hook intentionally avoids subscribing to the store: all data is derived
* lazily inside the callbacks via `registry.select`, so consumers don't
* re-render when the tab structure changes.
*
* @param {string|null} tabsClientId The client ID of the parent tabs block.
* @return {{ insertTab: Function, removeTab: Function }} Tab action callbacks.
* @return {{ insertTab: Function, removeTab: Function, moveTab: Function }} Tab action callbacks.
*/
export default function useTabActions( tabsClientId ) {
const registry = useRegistry();
const {
insertBlock,
removeBlock,
moveBlocksToPosition,
updateBlockAttributes,
__unstableMarkNextChangeAsNotPersistent,
} = useDispatch( blockEditorStore );
Expand Down Expand Up @@ -98,5 +99,33 @@ export default function useTabActions( tabsClientId ) {
removeBlock( target.clientId, false );
};

return { insertTab, removeTab };
// Move the active tab one position left or right by reordering the
// underlying tab-panel. The tab-list labels follow their panel
// automatically via useTabListItemsSync.
const moveTab = ( direction ) => {
const { tabPanelsClientId, tabPanelBlocks, activeIndex } =
getTabsState();
if ( ! tabPanelsClientId ) {
return;
}

const toIndex = activeIndex + direction;
const target = tabPanelBlocks[ activeIndex ];
if ( ! target || toIndex < 0 || toIndex >= tabPanelBlocks.length ) {
return;
}

__unstableMarkNextChangeAsNotPersistent();
updateBlockAttributes( tabsClientId, {
editorActiveTabIndex: toIndex,
} );
moveBlocksToPosition(
[ target.clientId ],
tabPanelsClientId,
tabPanelsClientId,
toIndex
);
};

return { insertTab, removeTab, moveTab };
}
69 changes: 69 additions & 0 deletions test/e2e/specs/editor/blocks/tabs.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,75 @@ test.describe( 'Tabs', () => {
await pageUtils.pressKeys( 'primary+z' );
await expect( tabs ).toHaveCount( 2 );
} );

test( 'moves the active tab to the previous position with the toolbar mover', async ( {
editor,
pageUtils,
} ) => {
const tab2 = editor.canvas.getByRole( 'tab', { name: 'Tab 2' } );
await tab2.click();

await editor.clickBlockToolbarButton( 'Move tab left' );

// The tab and its panel are reordered together.
await expect
.poll( async () =>
(
await editor.getBlocks()
)[ 0 ].innerBlocks[ 1 ].innerBlocks.map(
( panel ) => panel.attributes.label
)
)
.toEqual( [ 'Tab 2', 'Tab 1' ] );
await expect( editor.canvas.getByRole( 'tab' ) ).toHaveText( [
'Tab 2',
'Tab 1',
] );

// The moved tab stays active.
await expect( tab2 ).toHaveAttribute( 'aria-selected', 'true' );

// Undo restores the original order.
await pageUtils.pressKeys( 'primary+z' );
await expect( editor.canvas.getByRole( 'tab' ) ).toHaveText( [
'Tab 1',
'Tab 2',
] );
} );

test( 'moves the active tab to the next position with the toolbar mover', async ( {
editor,
pageUtils,
} ) => {
const tab1 = editor.canvas.getByRole( 'tab', { name: 'Tab 1' } );
await tab1.click();

await editor.clickBlockToolbarButton( 'Move tab right' );

await expect
.poll( async () =>
(
await editor.getBlocks()
)[ 0 ].innerBlocks[ 1 ].innerBlocks.map(
( panel ) => panel.attributes.label
)
)
.toEqual( [ 'Tab 2', 'Tab 1' ] );
await expect( editor.canvas.getByRole( 'tab' ) ).toHaveText( [
'Tab 2',
'Tab 1',
] );

// The moved tab stays active.
await expect( tab1 ).toHaveAttribute( 'aria-selected', 'true' );

// Undo restores the original order.
await pageUtils.pressKeys( 'primary+z' );
await expect( editor.canvas.getByRole( 'tab' ) ).toHaveText( [
'Tab 1',
'Tab 2',
] );
} );
} );

// TODO: Add a `Frontend functionality` describe block for front-end
Expand Down
Loading