@wordpress/block-library
Version:
Block library for the WordPress editor.
235 lines (214 loc) • 6.59 kB
JavaScript
import clsx from 'clsx';
import { __ } from '@wordpress/i18n';
import {
InspectorControls,
useBlockProps,
store as blockEditorStore,
RichText,
__experimentalUseBorderProps as useBorderProps,
__experimentalUseColorProps as useColorProps,
__experimentalGetSpacingClassesAndStyles as getSpacingClassesAndStyles,
} from '@wordpress/block-editor';
import {
TextControl,
__experimentalToolsPanel as ToolsPanel,
__experimentalToolsPanelItem as ToolsPanelItem,
} from '@wordpress/components';
import { useSelect, useDispatch, useRegistry } from '@wordpress/data';
import { useEffect, useMemo, useRef } from '@wordpress/element';
import TabToolbarControls from '../tabs/tab-toolbar-controls';
import TabMovers from './tab-movers';
import useTabActions from '../tabs/use-tab-actions';
import { useToolsPanelDropdownMenuProps } from '../utils/hooks';
const EMPTY_ARRAY = [];
function Edit( {
attributes,
clientId,
setAttributes,
__unstableLayoutClassNames: layoutClassNames,
} ) {
const { ariaLabel } = attributes;
const colorProps = useColorProps( attributes );
const borderProps = useBorderProps( attributes );
const spacingProps = getSpacingClassesAndStyles( attributes );
const dropdownMenuProps = useToolsPanelDropdownMenuProps();
const { tabsClientId, tabPanels, editorActiveTabIndex, activeTabIndex } =
useSelect(
( select ) => {
const { getBlockRootClientId, getBlockAttributes, getBlocks } =
select( blockEditorStore );
const rootClientId = getBlockRootClientId( clientId );
const tabsAttributes = getBlockAttributes( rootClientId );
const tabPanelsBlock = getBlocks( rootClientId )?.find(
( block ) => block.name === 'core/tab-panels'
);
return {
tabsClientId: rootClientId,
tabPanels: tabPanelsBlock?.innerBlocks ?? EMPTY_ARRAY,
editorActiveTabIndex: tabsAttributes?.editorActiveTabIndex,
activeTabIndex: tabsAttributes?.activeTabIndex ?? 0,
};
},
[ clientId ]
);
const registry = useRegistry();
const { isBlockSelected, hasSelectedInnerBlock } =
useSelect( blockEditorStore );
const {
updateBlockAttributes,
selectBlock,
__unstableMarkNextChangeAsNotPersistent,
} = useDispatch( blockEditorStore );
const { insertTab, removeTab } = useTabActions( tabsClientId );
const effectiveActiveIndex = editorActiveTabIndex ?? activeTabIndex;
const tabsList = useMemo(
() =>
tabPanels.map( ( tab ) => ( {
label: tab.attributes.label || '',
clientId: tab.clientId,
} ) ),
[ tabPanels ]
);
function selectTabPanel( tabIndex ) {
if ( tabsClientId && tabIndex !== effectiveActiveIndex ) {
// Batch the selection and index update so the sync effect in
// the tab-panel block can't revert the switch from a stale
// inner-block selection in the previously active panel.
registry.batch( () => {
selectBlock( clientId );
__unstableMarkNextChangeAsNotPersistent();
updateBlockAttributes( tabsClientId, {
editorActiveTabIndex: tabIndex,
} );
} );
}
}
function handleLabelChange( tabIndex, newLabel ) {
const tab = tabsList[ tabIndex ];
if ( tab?.clientId ) {
updateBlockAttributes( tab.clientId, { label: newLabel } );
}
}
const menuRef = useRef();
const prevTabCountRef = useRef( tabsList.length );
// When tabs are added or removed, focus the appropriate button.
useEffect( () => {
const prevCount = prevTabCountRef.current;
prevTabCountRef.current = tabsList.length;
if ( ! menuRef.current || tabsList.length === prevCount ) {
return;
}
// Only move focus during active editing, not external data changes.
if (
! isBlockSelected( tabsClientId ) &&
! hasSelectedInnerBlock( tabsClientId, true )
) {
return;
}
const focusButtonAt = ( index ) => {
window.requestAnimationFrame( () => {
const button =
menuRef.current?.querySelectorAll( 'button' )?.[ index ];
(
button?.querySelector( '[contenteditable]' ) ?? button
)?.focus();
} );
};
focusButtonAt( effectiveActiveIndex );
}, [
effectiveActiveIndex,
hasSelectedInnerBlock,
isBlockSelected,
tabsClientId,
tabsList.length,
] );
const blockProps = useBlockProps( {
role: 'tablist',
ref: menuRef,
// Applied manually since this block has no inner blocks for the layout
// support to add its container classes to.
className: layoutClassNames,
} );
const buttonClassName = clsx( colorProps.className, borderProps.className );
const buttonStyle = {
...colorProps.style,
...borderProps.style,
...spacingProps.style,
};
return (
<>
<InspectorControls group="settings">
<ToolsPanel
label={ __( 'Settings' ) }
resetAll={ () =>
setAttributes( {
ariaLabel: undefined,
} )
}
dropdownMenuProps={ dropdownMenuProps }
>
<ToolsPanelItem
label={ __( 'Label' ) }
isShownByDefault
hasValue={ () => !! ariaLabel }
onDeselect={ () =>
setAttributes( { ariaLabel: undefined } )
}
>
<TextControl
label={ __( 'Label' ) }
help={ __(
'Briefly describe this tab section for screen reader users. Examples: Event information, Product details, and Account settings.'
) }
value={ ariaLabel || '' }
onChange={ ( value ) =>
setAttributes( {
ariaLabel: value || undefined,
} )
}
/>
</ToolsPanelItem>
</ToolsPanel>
</InspectorControls>
<TabMovers tabsClientId={ tabsClientId } />
<TabToolbarControls tabsClientId={ tabsClientId } />
<div { ...blockProps }>
{ tabsList.map( ( tab, index ) => {
const isActive = index === effectiveActiveIndex;
return (
<button
key={ tab.clientId || index }
type="button"
role="tab"
aria-selected={ isActive }
className={ buttonClassName || undefined }
style={ buttonStyle }
tabIndex={ -1 }
// Activate the matching panel whenever this tab
// receives focus — whether from a click or the caret
// moving into the label via the keyboard.
onFocus={ () => {
selectTabPanel( index );
} }
>
<RichText
tagName="span"
withoutInteractiveFormatting
placeholder={ __( 'Tab title' ) }
value={ tab.label }
onChange={ ( newLabel ) =>
handleLabelChange( index, newLabel )
}
__unstableOnSplitAtEnd={ () =>
insertTab( index + 1 )
}
onRemove={ () => removeTab( index ) }
/>
</button>
);
} ) }
</div>
</>
);
}
export default Edit;