@wordpress/block-library
Version:
Block library for the WordPress editor.
234 lines (227 loc) • 6.08 kB
JavaScript
/**
* WordPress dependencies
*/
import {
store,
getContext,
getElement,
withSyncEvent,
} from '@wordpress/interactivity';
function createReadOnlyProxy( obj ) {
const arrayMutationMethods = new Set( [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse',
'copyWithin',
'fill',
] );
return new Proxy( obj, {
get( target, prop ) {
// If accessing an array mutation method, return a no-op function.
if ( Array.isArray( target ) && arrayMutationMethods.has( prop ) ) {
return () => {};
}
const value = target[ prop ];
if ( typeof value === 'object' && value !== null ) {
return createReadOnlyProxy( value );
}
return value;
},
set() {
return false;
},
deleteProperty() {
return false;
},
} );
}
// Private store for internal tabs functionality and security.
const { actions: privateActions, state: privateState } = store(
'core/tabs/private',
{
state: {
/**
* Gets a contextually aware list of tabs for the current tabs block.
*
* @type {Array}
*/
get tabsList() {
const context = getContext();
const tabsId = context?.tabsId;
const tabsList = privateState[ tabsId ];
return tabsList;
},
/**
* Gets the index of the active tab element whether it
* is a tab label or tab panel.
*
* @type {number|null}
*/
get tabIndex() {
const { attributes } = getElement();
const tabId = attributes?.id?.replace( 'tab__', '' ) || null;
if ( ! tabId ) {
return null;
}
const { tabsList } = privateState;
const tabIndex = tabsList.findIndex( ( t ) => t.id === tabId );
return tabIndex;
},
/**
* Whether the tab panel or tab label is the active tab.
*
* @type {boolean}
*/
get isActiveTab() {
const { activeTabIndex } = getContext();
const { tabIndex } = privateState;
return activeTabIndex === tabIndex;
},
/**
* The value of the tabindex attribute.
*
* @type {false|string}
*/
get tabIndexAttribute() {
return privateState.isActiveTab ? -1 : 0;
},
},
actions: {
/**
* Handles the keydown events for the tab label and tabs controller.
*
* @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 );
}
} else if ( event.key === 'ArrowLeft' && ! isVertical ) {
const { tabIndex } = privateState;
if ( tabIndex !== null ) {
privateActions.setActiveTab( tabIndex - 1 );
}
} else if ( event.key === 'ArrowDown' && isVertical ) {
const { tabIndex } = privateState;
if ( tabIndex !== null ) {
privateActions.setActiveTab( tabIndex + 1 );
}
} else if ( event.key === 'ArrowUp' && isVertical ) {
const { tabIndex } = privateState;
if ( tabIndex !== null ) {
privateActions.setActiveTab( tabIndex - 1 );
}
}
} ),
/**
* Handles the click event for the tab label.
*
* @param {MouseEvent} event The click event.
*/
handleTabClick: withSyncEvent( ( event ) => {
event.preventDefault();
const { tabIndex } = privateState;
if ( tabIndex !== null ) {
privateActions.setActiveTab( tabIndex );
}
} ),
/**
* 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 context = getContext();
context.activeTabIndex = tabIndex;
if ( scrollToTab ) {
const tabId = privateState.tabsList[ tabIndex ].id;
const tabElement = document.getElementById( tabId );
if ( tabElement ) {
setTimeout( () => {
tabElement.scrollIntoView( { behavior: 'smooth' } );
}, 100 );
}
}
},
},
callbacks: {
/**
* When the tabs are initialized, we need to check if there is a hash in the url and if so if it exists in the current tabsList, set the active tab to that index.
*
*/
onTabsInit: () => {
const { tabsList } = privateState;
if ( tabsList.length === 0 ) {
return;
}
const { hash } = window.location;
const tabId = hash.replace( '#', '' );
const tabIndex = tabsList.findIndex( ( t ) => t.id === tabId );
// Check if tabIndex is a positive number and if so we'll auto activate that tab.
if ( tabIndex >= 0 ) {
privateActions.setActiveTab( tabIndex, true );
}
},
},
},
{
lock: true,
}
);
// Public store for third-party extensibility.
store( 'core/tabs', {
state: {
/**
* Gets a contextually aware list of tabs for the current tabs block.
* Public API for third-party access.
*
* @type {Array}
*/
get tabsList() {
return createReadOnlyProxy( privateState.tabsList );
},
/**
* Gets the index of the active tab element whether it
* is a tab label or tab panel.
*
* @type {number|null}
*/
get tabIndex() {
return privateState.tabIndex;
},
/**
* Whether the tab panel or tab label is the active tab.
*
* @type {boolean}
*/
get isActiveTab() {
return privateState.isActiveTab;
},
},
actions: {
/**
* Sets the active tab index.
* Public API for third-party programmatic tab activation.
*
* @param {number} tabIndex The index of the active tab.
* @param {boolean} scrollToTab Whether to scroll to the tab element.
*/
setActiveTab: ( tabIndex, scrollToTab = false ) => {
privateActions.setActiveTab( tabIndex, scrollToTab );
},
},
} );