Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 3 additions & 5 deletions packages/block-library/src/tabs-menu-item/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ function block_core_tabs_menu_item_render_callback( array $attributes, string $c

// Set tab-specific attributes
$tag_processor->set_attribute( 'id', 'tab__' . $tab_id );
$tag_processor->set_attribute( 'href', '#' . $tab_id );
$tag_processor->set_attribute( 'role', 'tab' );
$tag_processor->set_attribute( 'aria-controls', $tab_id );

// Add IAPI directives
Expand All @@ -55,10 +53,10 @@ function block_core_tabs_menu_item_render_callback( array $attributes, string $c
// Get updated HTML and inject the label
$output = $tag_processor->get_updated_html();

// The save.js outputs <a><span class="screen-reader-text">...</span></a>
// Replace the anchor content with the actual tab label
// The save.js outputs <button><span class="screen-reader-text">...</span></button>
// Replace the button content with the actual tab label
$output = preg_replace(
'/(<a[^>]*>).*?(<\/a>)/s',
'/(<button[^>]*>).*?(<\/button>)/s',
'$1' . '<span>' . esc_html( html_entity_decode( $tab_label ) ) . '</span>' . '$2',
$output
);
Expand Down
6 changes: 4 additions & 2 deletions packages/block-library/src/tabs-menu-item/save.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ export default function save( { attributes } ) {
className: 'wp-block-tabs-menu-item__template',
style: customColorStyles,
hidden: true,
type: 'button',
role: 'tab',
} );

return (
<a { ...blockProps }>
<button { ...blockProps }>
<span className="screen-reader-text">
{ __( 'Tab menu item' ) }
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a separate issue, but doing localization in the save function can break blocks, so it's something we'll need to address in a follow-up. I recently wrote an article about this.

https://aki-hamano.blog/en/2026/02/04/wordpress-i18n/#Don8217t_Use_Translation_Functions_in_Save_Function

</span>
</a>
</button>
);
}
6 changes: 6 additions & 0 deletions packages/block-library/src/tabs-menu-item/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
flex-basis: inherit !important;
flex-grow: inherit !important;

// Button reset
border: none;
background: none;
appearance: none;
-webkit-appearance: none;

margin: 0;
padding-block: var(--tab-padding-block, var(--wp--preset--spacing--20, 0.5em));
padding-inline: var(--tab-padding-inline, var(--wp--preset--spacing--30, 1em));
Expand Down
2 changes: 1 addition & 1 deletion packages/block-library/src/tabs-menu/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function block_core_tabs_menu_render_callback( array $attributes, string $conten

// Find the template block and replace it in $content with $tabs_markup
$content = preg_replace(
'/<a\b[^>]*\bwp-block-tabs-menu-item__template\b[^>]*>.*?<\/a>/si',
'/<button\b[^>]*\bwp-block-tabs-menu-item__template\b[^>]*>.*?<\/button>/si',
$tabs_markup,
$content
);
Expand Down
90 changes: 62 additions & 28 deletions packages/block-library/src/tabs/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,12 @@ const { actions: privateActions, state: privateState } = store(
},
/**
* The value of the tabindex attribute.
* Only the active tab should be in the tab sequence.
*
* @type {false|string}
* @type {number}
*/
get tabIndexAttribute() {
return privateState.isActiveTab ? -1 : 0;
return privateState.isActiveTab ? 0 : -1;
},
},
actions: {
Expand All @@ -101,33 +102,26 @@ const { actions: privateActions, state: privateState } = store(
* @param {KeyboardEvent} event The keydown event.
*/
handleTabKeyDown: withSyncEvent( ( event ) => {
// If this is the enter key then lets get the tab index from context and set the active tab to that index.
const { isVertical } = getContext();
if ( event.key === 'Enter' ) {
const { tabIndex } = privateState;
if ( tabIndex !== null ) {
privateActions.setActiveTab( tabIndex );
}
} else if ( event.key === 'ArrowRight' && ! isVertical ) {
const { tabIndex } = privateState;
if ( tabIndex !== null ) {
privateActions.setActiveTab( tabIndex + 1 );
}
const context = getContext();
const { isVertical } = context;
const { tabIndex } = privateState;

if ( tabIndex === null ) {
return;
}

if ( event.key === 'ArrowRight' && ! isVertical ) {
event.preventDefault();
privateActions.moveFocus( tabIndex + 1 );
} else if ( event.key === 'ArrowLeft' && ! isVertical ) {
const { tabIndex } = privateState;
if ( tabIndex !== null ) {
privateActions.setActiveTab( tabIndex - 1 );
}
event.preventDefault();
privateActions.moveFocus( tabIndex - 1 );
} else if ( event.key === 'ArrowDown' && isVertical ) {
const { tabIndex } = privateState;
if ( tabIndex !== null ) {
privateActions.setActiveTab( tabIndex + 1 );
}
event.preventDefault();
privateActions.moveFocus( tabIndex + 1 );
} else if ( event.key === 'ArrowUp' && isVertical ) {
const { tabIndex } = privateState;
if ( tabIndex !== null ) {
privateActions.setActiveTab( tabIndex - 1 );
}
event.preventDefault();
privateActions.moveFocus( tabIndex - 1 );
}
} ),
/**
Expand All @@ -143,17 +137,56 @@ const { actions: privateActions, state: privateState } = store(
privateActions.setActiveTab( tabIndex );
}
} ),
/**
* Moves focus to a specific tab without activating it.
*
* @param {number} tabIndex The index to move focus to.
*/
moveFocus: ( tabIndex ) => {
const { tabsList } = privateState;

if ( ! tabsList || tabsList.length === 0 ) {
return;
}

let newIndex = tabIndex;
if ( newIndex < 0 ) {
newIndex = tabsList.length - 1;
} else if ( newIndex >= tabsList.length ) {
newIndex = 0;
}

const tabId = tabsList[ newIndex ].id;
const tabElement = document.getElementById( 'tab__' + tabId );
if ( tabElement ) {
tabElement.focus();
}
},
/**
* Sets the active tab index (internal implementation).
*
* @param {number} tabIndex The index of the active tab.
* @param {boolean} scrollToTab Whether to scroll to the tab element.
*/
setActiveTab: ( tabIndex, scrollToTab = false ) => {
const { tabsList } = privateState;

if ( ! tabsList || tabsList.length === 0 ) {
return;
}

let newIndex = tabIndex;
if ( newIndex < 0 ) {
newIndex = 0;
} else if ( newIndex >= tabsList.length ) {
newIndex = tabsList.length - 1;
}

const context = getContext();
context.activeTabIndex = tabIndex;
context.activeTabIndex = newIndex;

if ( scrollToTab ) {
const tabId = privateState.tabsList[ tabIndex ].id;
const tabId = tabsList[ newIndex ].id;
const tabElement = document.getElementById( tabId );
if ( tabElement ) {
setTimeout( () => {
Expand All @@ -173,6 +206,7 @@ const { actions: privateActions, state: privateState } = store(
if ( tabsList.length === 0 ) {
return;
}

const { hash } = window.location;
const tabId = hash.replace( '#', '' );
const tabIndex = tabsList.findIndex( ( t ) => t.id === tabId );
Expand Down
Loading