@wordpress/block-library
Version:
Block library for the WordPress editor.
268 lines (251 loc) • 6.65 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 for tab buttons.
* Only the active tab should be in the tab sequence.
*
* @type {number}
*/
get tabIndexAttribute() {
return privateState.isActiveTab ? 0 : -1;
},
},
actions: {
/**
* Handles the keydown events for the tab label and tabs controller.
*
* @param {KeyboardEvent} event The keydown event.
*/
handleTabKeyDown: withSyncEvent( ( event ) => {
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 ) {
event.preventDefault();
privateActions.moveFocus( tabIndex - 1 );
} else if ( event.key === 'ArrowDown' && isVertical ) {
event.preventDefault();
privateActions.moveFocus( tabIndex + 1 );
} else if ( event.key === 'ArrowUp' && isVertical ) {
event.preventDefault();
privateActions.moveFocus( 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 );
}
} ),
/**
* 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 = newIndex;
if ( scrollToTab ) {
const tabId = tabsList[ newIndex ].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 );
},
},
} );