Skip to content

Commit

Permalink
Replace JS initTabs in favor of interativity directives and state get…
Browse files Browse the repository at this point in the history
…ters
  • Loading branch information
creativecoder committed Sep 3, 2024
1 parent 2db172b commit cc304b5
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 96 deletions.
8 changes: 8 additions & 0 deletions packages/block-library/src/tab/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,16 @@ function render_block_core_tab( $attributes, $content ) {
$p = new WP_HTML_Tag_Processor( $content );

while ( $p->next_tag( array( 'class_name' => 'wp-block-tab' ) ) ) {
// Add role="tabpanel" to each tab panel.
$p->set_attribute( 'data-wp-bind--role', 'state.roleAttribute' );

// Hide all tab panels that are not currently selected.
$p->set_attribute( 'data-wp-bind--hidden', '!state.isActiveTab' );

// Add tabindex="0" to the selected tab panel, so it can be focused.
$p->set_attribute( 'data-wp-bind--tabindex', 'state.tabindexPanelAttribute' );

// Store the index of each tab panel for tracking the selected tab.
$p->set_attribute( 'data-tab-index', $attributes['tabIndex'] );
}

Expand Down
66 changes: 57 additions & 9 deletions packages/block-library/src/tabs/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,29 +35,77 @@ function render_block_core_tabs( $attributes, $content ) {
wp_enqueue_script_module( '@wordpress/block-library/tabs' );

// Modify markup to include interactivity API attributes.
$p = new WP_HTML_Tag_Processor( $content );
$title_id = wp_unique_id( 'tablist-label-' );
$p = new WP_HTML_Tag_Processor( $content );

// Generate a dictionary of tab panel and tab label ids used to populate aria attributes.
$tab_panel_to_label_id = array();
while ( $p->next_tag( array( 'class_name' => 'wp-block-tabs__tab-label' ) ) ) {
$tab_label_href = $p->get_attribute( 'href' );
$tab_panel_id = $tab_label_href ? substr( $tab_label_href, 1 ) : '';
$tab_label_id = $p->get_attribute( 'id' );

$tab_panel_to_label_id[ $tab_panel_id ] = $tab_label_id;
}

// Reset the processor to the beginning of the content.
$p = new WP_HTML_Tag_Processor( $content );

$title_id = wp_unique_id( 'tablist-label-' );
$tab_label_index = 0;
while ( $p->next_tag() ) {
if ( $p->has_class( 'wp-block-tabs' ) ) {
// Add class interactive to the block wrapper to indicate JavaScript has been loaded.
$p->set_attribute( 'data-wp-class--interactive', 'state' );

// Set up interactivity API and context.
$p->set_attribute( 'data-wp-interactive', 'core/tabs' );
$p->set_attribute( 'data-wp-context', '{ "activeTabIndex": 0 }' );
$p->set_attribute( 'data-wp-init', 'callbacks.init' );
}

// Set a unique ID for the title, so it can be used by aria-labelledby.
if ( $p->has_class( 'wp-block-tabs__title' ) ) {
} elseif ( $p->has_class( 'wp-block-tabs__title' ) ) {
// Set a unique ID for the title, so it can be used by aria-labelledby.
$p->set_attribute( 'id', $title_id );
}
} elseif ( $p->has_class( 'wp-block-tabs__list' ) ) {
// Add role="tablist" to the <ul> element.
$p->set_attribute( 'data-wp-bind--role', 'state.roleAttribute' );

// Add aria-labelledby attribute with the title ID to the <ul> element.
$p->set_attribute( 'data-wp-bind--aria-labelledby', $title_id );
} elseif ( $p->has_class( 'wp-block-tabs__list-item' ) ) {
// Add role="presentation" to each <li> element, because the inner <a> elements are the actual tabs.
$p->set_attribute( 'data-wp-bind--role', 'state.roleAttribute' );
} elseif ( $p->has_class( 'wp-block-tabs__tab-label' ) ) {
$tab_label_href = $p->get_attribute( 'href' );
$tab_panel_id = $tab_label_href ? substr( $tab_label_href, 1 ) : '';
if ( $tab_panel_id ) {
// Add aria-controls attribute with the corresponding tab panel ID to each tab label.
$p->set_attribute( 'data-wp-bind--aria-controls', $tab_panel_id );
}

if ( $p->has_class( 'wp-block-tabs__tab-label' ) ) {
// Add role="tab" to the <a> element that labels each tab panel.
$p->set_attribute( 'data-wp-bind--role', 'state.roleAttribute' );

// Add aria-selected attribute indicating if the tab is currently selected.
$p->set_attribute( 'data-wp-bind--aria-selected', 'state.isActiveTab' );

// Add tabindex="-1" to all non-selected tab labels, since they can be selected with arrow keys.
$p->set_attribute( 'data-wp-bind--tabindex', 'state.tabindexLabelAttribute' );

// Add click and keydown event handlers to each tab label.
$p->set_attribute( 'data-wp-on--click', 'actions.handleTabClick' );
$p->set_attribute( 'data-wp-on--keydown', 'actions.handleTabKeyDown' );

// Store the index of each tab label for tracking the selected tab.
$p->set_attribute( 'data-tab-index', $tab_label_index );
++$tab_label_index;
} elseif ( $p->has_class( 'wp-block-tab' ) ) {
$tab_panel_id = $p->get_attribute( 'id' );
$tab_label_id = $tab_panel_id && isset( $tab_panel_to_label_id[ $tab_panel_id ] )
? $tab_panel_to_label_id[ $tab_panel_id ]
: '';

if ( $tab_label_id ) {
// Add aria-labelledby attribute with the corresponding tab label ID to each tab panel.
$p->set_attribute( 'data-wp-bind--aria-labelledby', $tab_label_id );
}
}
}

Expand Down
108 changes: 21 additions & 87 deletions packages/block-library/src/tabs/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,87 +13,30 @@ function isNumeric( value ) {
return ! isNaN( parseFloat( value ) );
}

/**
* Generates a map of tab panel IDs to tab label IDs.
*
* @param {Element[]} tabLabels List of tab label elements.
* @return {Map} A map of tab panel IDs to tab label IDs.
*/
function getTabPanelToLabelIdMap( tabLabels ) {
if ( ! tabLabels ) {
return new Map();
}

return new Map(
tabLabels.map( ( tabLabel ) => {
const tabPanelId = tabLabel.getAttribute( 'href' )?.substring( 1 );
const tabLabelId = tabLabel.getAttribute( 'id' );
return [ tabPanelId, tabLabelId ];
} )
);
}

/**
* Initializes the tabs block with the necessary ARIA attributes.
*
* @param {HTMLElement} ref The block wrapper element.
*/
function initTabs( ref ) {
// Add class interactive to the block wrapper to indicate JavaScript has been loaded.
ref.classList.add( 'interactive' );

const tabList = ref.querySelector( '.wp-block-tabs__list' );
if ( tabList ) {
// Add role="tablist" to the <ul> element.
tabList.setAttribute( 'role', 'tablist' );

// Use the hidden <h3>Contents</h3> element as the label for the tab list, if found.
const tabListLabel = ref.querySelector( '.wp-block-tabs__title' );
if ( tabListLabel ) {
const tabListLabelId = tabListLabel.id || 'tablist-label';
tabListLabel.id = tabListLabelId;
tabList.setAttribute( 'aria-labelledby', tabListLabelId );
} else {
tabList.setAttribute( 'aria-label', 'Tabs' );
}
}

// Add role="presentation" to each <li> element, because the inner <a> elements are the actual tabs.
const tabItems = Array.from(
ref.querySelectorAll( '.wp-block-tabs__list-item' )
);
tabItems.forEach( ( item ) => {
item.setAttribute( 'role', 'presentation' );
} );

// Add role="tab" and aria-controls with the corresponding tab panel ID to each <a> element.
const tabLabels = Array.from(
ref.querySelectorAll( '.wp-block-tabs__tab-label' )
);
const tabPanelToLabelIdMap = getTabPanelToLabelIdMap( tabLabels );
tabLabels.forEach( ( label ) => {
label.setAttribute( 'role', 'tab' );
const tabPanelId = label.getAttribute( 'href' )?.substring( 1 );

if ( tabPanelId ) {
label.setAttribute( 'aria-controls', tabPanelId );
}
} );

// Add role="tabpanel" and aria-labelledby with the corresponding tab label ID to each tab panel.
const tabPanels = Array.from( ref.querySelectorAll( '.wp-block-tab' ) );
tabPanels.forEach( ( panel ) => {
panel.setAttribute( 'role', 'tabpanel' );
const tabLabelledById = tabPanelToLabelIdMap.get( panel.id );
if ( tabLabelledById ) {
panel.setAttribute( 'aria-labelledby', tabLabelledById );
}
} );
}

// Interactivy store for the tabs block.
const { state, actions } = store( 'core/tabs', {
state: {
get roleAttribute() {
const el = getElement();
const classList = el?.attributes?.class ?? '';
const classArray = classList.split( ' ' );

const classToRoleMap = new Map( [
[ 'wp-block-tabs__list', 'tablist' ],
[ 'wp-block-tabs__list-item', 'presentation' ],
[ 'wp-block-tabs__tab-label', 'tab' ],
[ 'wp-block-tab', 'tabpanel' ],
] );

for ( const className of classArray ) {
const role = classToRoleMap.get( className );
if ( role ) {
return role;
}
}

return false;
},
/**
* Whether the tab is the active tab.
*
Expand Down Expand Up @@ -200,13 +143,4 @@ const { state, actions } = store( 'core/tabs', {
context.activeTabIndex = tabIndex;
},
},
callbacks: {
/**
* Initializes the tabs block.
*/
init: () => {
const { ref } = getElement();
initTabs( ref );
},
},
} );

0 comments on commit cc304b5

Please sign in to comment.