UNPKG

@wordpress/block-library

Version:
234 lines (227 loc) 6.08 kB
/** * 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 ); }, }, } );