Skip to content

Commit

Permalink
Moving forward with Fabian's slotfill method for handling tab activat…
Browse files Browse the repository at this point in the history
…ion/element-grouping. Will come back in later this week to add key handlers
  • Loading branch information
sethrubenstein committed Jan 6, 2025
1 parent d0e886a commit bf5d44f
Show file tree
Hide file tree
Showing 8 changed files with 437 additions and 399 deletions.
4 changes: 3 additions & 1 deletion packages/block-library/src/tab/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Tab

Contributors: Pew Research Center, creativecoder
Contributors: Seth Rubenstein on behalf of Pew Research Center, creativecoder
Tags: block
Tested up to: 6.7
Stable tag: 1.0.0
Expand All @@ -11,6 +11,8 @@ License URI: https://www.gnu.org/licenses/gpl-2.0.html

This is an exploration at a [`core/` level block](https://github.com/WordPress/gutenberg/pull/63689/) that allows for the creation of tabbed content. Bootstrapped from work that @creativecoder was close to finishing but had to abandon due to other commitments.

## TODO

## Instructions

This section describes how to use the block.
Expand Down
4 changes: 4 additions & 0 deletions packages/block-library/src/tab/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
},
"tabIndex": {
"type": "number"
},
"isActive": {
"type": "boolean",
"default": false
}
},
"parent": [ "core/tabs" ],
Expand Down
207 changes: 114 additions & 93 deletions packages/block-library/src/tab/edit.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
/**
* External Dependencies
*/

/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import {
InnerBlocks,
useBlockProps,
useInnerBlocksProps,
store as blockEditorStore,
InspectorControls,
RichText,
} from '@wordpress/block-editor';
import { useSelect, useDispatch } from '@wordpress/data';
import { Fragment, useEffect, useMemo } from '@wordpress/element';
import { PanelBody, TextControl } from '@wordpress/components';
import { cleanForSlug } from '@wordpress/url';

/**
* Internal dependencies
*/
import { TabFill, TabsListSlot } from './slotfill';

/**
* Generates a slug from a tab's text label.
*
Expand All @@ -39,91 +42,39 @@ function slugFromLabel( label, tabIndex ) {
return `tab-panel-${ tabIndex }`;
}

function EditComponent( { attributes, clientId, setAttributes } ) {
const { anchor, label, slug } = attributes;
// Use a custom anchor, if set. Otherwise fall back to the slug generated from the label text.
const tabPanelId = anchor || slug;
const tabLabelId = `${ tabPanelId }--tab`;
const hasChildBlocks = useSelect(
( select ) =>
select( blockEditorStore ).getBlockOrder( clientId ).length > 0,
[ clientId ]
);

const blockProps = useBlockProps();

const innerBlocksProps = useInnerBlocksProps( blockProps, {
renderAppender: hasChildBlocks
? undefined
: InnerBlocks.ButtonBlockAppender,
} );

return (
<Fragment>
<InspectorControls>
<PanelBody title="Tab Settings">
<TextControl
label="Label"
value={ label }
onChange={ ( value ) =>
setAttributes( { label: value } )
}
__next40pxDefaultSize
__nextHasNoMarginBottom
/>
</PanelBody>
</InspectorControls>
<section
{ ...innerBlocksProps }
aria-labelledby={ tabLabelId }
id={ tabPanelId }
role="tabpanel"
/>
</Fragment>
);
}

export default function Edit( {
attributes,
clientId,
isSelected,
setAttributes,
} ) {
const { isActive, label, tabIndex } = attributes;
const { anchor, label, slug, tabIndex } = attributes;
const { __unstableMarkNextChangeAsNotPersistent } =
useDispatch( blockEditorStore );

const { hasInnerBlockSelected, blockIndex } = useSelect(
( select ) => {
return {
hasInnerBlockSelected:
select( blockEditorStore ).hasSelectedInnerBlock(
clientId
),
blockIndex:
select( blockEditorStore ).getBlockIndex( clientId ),
};
},
[ clientId ]
);
const { blockIndex, hasChildBlocks, hasInnerBlocksSelected, tabsClientId } =
useSelect(
( select ) => {
const rootClientId =
select( blockEditorStore ).getBlockRootClientId( clientId );
return {
blockIndex:
select( blockEditorStore ).getBlockIndex( clientId ),
hasChildBlocks:
select( blockEditorStore ).getBlockOrder( clientId )
.length > 0,
hasInnerBlocksSelected: select(
blockEditorStore
).hasSelectedInnerBlock( clientId, true ),
tabsClientId: rootClientId,
};
},
[ clientId ]
);

/**
* These two hooks ensure the tab block's slug and tabIndex attributes are kept in sync with the parent tabs block.
* This hook ensures the tabIndex attribute is kept in sync.
*/
// Construct or update the slug when the label changes:
useEffect( () => {
if ( label ) {
__unstableMarkNextChangeAsNotPersistent();
setAttributes( { slug: slugFromLabel( label, tabIndex ) } );
}
}, [
__unstableMarkNextChangeAsNotPersistent,
label,
setAttributes,
tabIndex,
] );

// Ensure tabIndex attributes are in sync with the order relative to the root
useEffect( () => {
if ( blockIndex !== tabIndex ) {
__unstableMarkNextChangeAsNotPersistent();
Expand All @@ -136,22 +87,92 @@ export default function Edit( {
tabIndex,
] );

const displayEditComponent = useMemo( () => {
return isActive || isSelected || hasInnerBlockSelected;
}, [ isActive, hasInnerBlockSelected, isSelected ] );
/**
* This hook determines if the current tab is selected. This is true if it is the active tab, or if it is selected directly.
*/
const isSelectedTab = useMemo( () => {
return isSelected || hasInnerBlocksSelected;
}, [ isSelected, hasInnerBlocksSelected ] );

// If the block is not selected, and or not active then
// there is no reason to render the edit component. This saves on
// memory and performance.
if ( displayEditComponent ) {
return (
<EditComponent
attributes={ attributes }
clientId={ clientId }
setAttributes={ setAttributes }
/>
);
}
// Use a custom anchor, if set. Otherwise fall back to the slug generated from the label text.
const tabPanelId = useMemo( () => anchor || slug, [ anchor, slug ] );
const tabLabelId = useMemo( () => `${ tabPanelId }--tab`, [ tabPanelId ] );

return <div hidden />;
const blockProps = useBlockProps();

const innerBlocksProps = useInnerBlocksProps(
{
'aria-labelledby': tabLabelId,
id: tabPanelId,
role: 'tabpanel',
},
{
renderAppender: hasChildBlocks
? undefined
: InnerBlocks.ButtonBlockAppender,
}
);

return (
<Fragment>
<InspectorControls>
<PanelBody title="Tab Settings">
<TextControl
label="Label"
value={ label }
onChange={ ( value ) =>
setAttributes( { label: value } )
}
__next40pxDefaultSize
__nextHasNoMarginBottom
/>
</PanelBody>
</InspectorControls>

<div { ...blockProps }>
<TabFill tabsClientId={ tabsClientId }>
<li role="presentation" className="tabs__list-item">
<a // eslint-disable-line jsx-a11y/anchor-is-valid -- remove href attribute in editor so inner text can be selected for editing
aria-controls={ tabPanelId }
aria-selected={ isSelectedTab }
className="tabs__tab-label"
id={ tabLabelId }
// onClick={ () => console.log( 'onClick', clientId ) }
// onFocus={ () => console.log( 'onFocus', clientId ) }
// onKeyDown={ ( event ) => {
// if ( event.key === 'Enter' ) {
// console.log( 'onEnter', clientId );
// }
// } }
role="tab"
tabIndex={ isSelectedTab ? 0 : -1 }
>
<RichText
tagName="span"
withoutInteractiveFormatting
value={ label }
placeholder={ __( 'Add label…' ) }
onChange={ ( value ) =>
setAttributes( {
label: value,
slug: slugFromLabel(
label,
blockIndex
),
} )
}
/>
</a>
</li>
</TabFill>

{ isSelectedTab && (
<Fragment>
<TabsListSlot tabsClientId={ tabsClientId } />
<section { ...innerBlocksProps } />
</Fragment>
) }
</div>
</Fragment>
);
}
25 changes: 25 additions & 0 deletions packages/block-library/src/tab/slotfill.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* WordPress dependencies
*/
import { createSlotFill } from '@wordpress/components';

/**
* Props to @fabiankaegy for the SlotFill implementation https://github.com/WordPress/gutenberg/pull/63689#issuecomment-2268555989
*/
const { Fill, Slot } = createSlotFill( 'TabsList' );

export const TabFill = ( { children, tabsClientId } ) => {
return <Fill name={ `TabsList-${ tabsClientId }` }>{ children }</Fill>;
};

export const TabsListSlot = ( { tabsClientId } ) => {
return (
<Slot
name={ `TabsList-${ tabsClientId }` }
bubblesVirtually
as="ul"
role="tablist"
className="tabs__list"
/>
);
};
Loading

0 comments on commit bf5d44f

Please sign in to comment.