Skip to content

Commit

Permalink
Adds proper ids to tabs, integrated with anchor attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
creativecoder committed Jul 28, 2024
1 parent 04fc6c5 commit 3a27615
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 22 deletions.
2 changes: 1 addition & 1 deletion docs/reference-guides/core-blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -896,7 +896,7 @@ Single tab within a tabs block. ([Source](https://github.com/WordPress/gutenberg
- **Category:** design
- **Parent:** core/tabs
- **Supports:** anchor, ~~html~~, ~~reusable~~
- **Attributes:** isActive, label, tabIndex
- **Attributes:** isActive, label, slug, tabIndex

## Table

Expand Down
7 changes: 6 additions & 1 deletion packages/block-library/src/tab/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
"default": false
},
"label": {
"type": "string"
"type": "string",
"default": ""
},
"slug": {
"type": "string",
"default": ""
},
"tabIndex": {
"type": "number"
Expand Down
38 changes: 35 additions & 3 deletions packages/block-library/src/tab/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,39 @@ import {
store as blockEditorStore,
} from '@wordpress/block-editor';
import { useSelect } from '@wordpress/data';
import { useEffect } from '@wordpress/element';
import { cleanForSlug } from '@wordpress/url';

export default function Edit( { attributes, clientId } ) {
function slugFromLabel( label, tabIndex ) {
// Get just the text content, filtering out any HTML tags from the RichText value.
const htmlDocument = new window.DOMParser().parseFromString(
label,
'text/html'
);
if ( htmlDocument.body?.textContent ) {
return cleanForSlug( htmlDocument.body.textContent );
}

// Fall back to using the tab index if the label is empty.
return 'tab-panel-' + tabIndex;
}

export default function Edit( { attributes, clientId, setAttributes } ) {
const { anchor, isActive, label, slug, tabIndex } = attributes;
// Use a custom anchor, if possible. Otherwise fall back to the slug generated from the label.
const tabPanelId = anchor || slug;
const tabLabelId = tabPanelId + '--tab';
const hasChildBlocks = useSelect(
( select ) =>
select( blockEditorStore ).getBlockOrder( clientId ).length > 0,
[ clientId ]
);
const { isActive } = attributes;

useEffect( () => {
if ( label ) {
setAttributes( { slug: slugFromLabel( label, tabIndex ) } );
}
}, [ label, setAttributes, tabIndex ] );

const blockProps = useBlockProps( {
className: isActive ? 'is-active' : '',
Expand All @@ -25,5 +50,12 @@ export default function Edit( { attributes, clientId } ) {
? undefined
: InnerBlocks.ButtonBlockAppender,
} );
return <div { ...innerBlocksProps } role="tabpanel"></div>;
return (
<div
{ ...innerBlocksProps }
aria-labelledby={ tabLabelId }
id={ tabPanelId }
role="tabpanel"
/>
);
}
13 changes: 11 additions & 2 deletions packages/block-library/src/tab/save.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,22 @@
import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';

export default function save( { attributes } ) {
const { tabIndex } = attributes;
const { anchor, slug, tabIndex } = attributes;
const tabPanelId = anchor || slug;
const tabLabelId = tabPanelId + '--tab';

// The first tab in the set is always active on initial load.
const blockProps = useBlockProps.save( {
className: tabIndex === 0 ? 'is-active' : '',
} );
const innerBlocksProps = useInnerBlocksProps.save( blockProps );

return <div { ...innerBlocksProps } role="tabpanel"></div>;
return (
<div
{ ...innerBlocksProps }
aria-labelledby={ tabLabelId }
id={ tabPanelId }
role="tabpanel"
/>
);
}
8 changes: 5 additions & 3 deletions packages/block-library/src/tabs/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
"attributes": {
"innerTabs": {
"type": "array",
"items": {
"type": "string"
},
"default": [],
"source": "query",
"selector": ".wp-block-tabs__tab-label",
"query": {
"id": {
"type": "string",
"source": "attribute",
"attribute": "aria-controls"
},
"label": {
"type": "string",
"source": "html"
Expand Down
20 changes: 12 additions & 8 deletions packages/block-library/src/tabs/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,11 @@ export default function Edit( { clientId, setAttributes } ) {
* - tabIndex to save the first tab as active, independent of the editor view
*/
useEffect( () => {
__unstableMarkNextChangeAsNotPersistent();
setAttributes( {
innerTabs: innerTabBlocks.map( ( block ) => ( {
label: block.attributes.label,
id: block.attributes.anchor || block.attributes.slug,
} ) ),
} );

Expand Down Expand Up @@ -149,26 +151,28 @@ export default function Edit( { clientId, setAttributes } ) {
<div { ...blockProps }>
<ul className="wp-block-tabs__list" role="tablist">
{ innerTabBlocks.map( ( block ) => {
const isActive = block.attributes.isActive;
const tabIndex = isActive ? 0 : -1;
const { anchor, isActive, label, slug } = block.attributes;
const tabIndexAttr = isActive ? 0 : -1;
const tabPanelId = anchor || slug;
const tabLabelId = tabPanelId + '--tab';

// TODO: Add unique ids and aria attributes for accessibility.
// (Try the anchor generation functionality from the heading block?)
return (
<li
key={ block.clientId }
className="wp-block-tabs__list-item"
role="presentation"
>
<a // eslint-disable-line jsx-a11y/anchor-is-valid -- TODO: add tab ids to href
<a
aria-controls={ tabPanelId }
aria-selected={ isActive }
className={ clsx( 'wp-block-tabs__tab-label', {
'is-active': isActive,
} ) }
href="#"
href={ '#' + tabPanelId }
id={ tabLabelId }
onClick={ () => setActiveTab( block.clientId ) }
role="tab"
tabIndex={ tabIndex }
tab-index={ tabIndexAttr }
>
<RichText
allowedFormats={ ALLOWED_FORMATS }
Expand All @@ -179,7 +183,7 @@ export default function Edit( { clientId, setAttributes } ) {
} )
}
placeholder={ __( 'Add label…' ) }
value={ block.attributes.label }
value={ label }
/>
</a>
</li>
Expand Down
10 changes: 6 additions & 4 deletions packages/block-library/src/tabs/save.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,24 @@ export default function save( { attributes } ) {
<ul className="wp-block-tabs__list" role="tablist">
{ innerTabs.map( ( tab, index ) => {
const isActive = index === 0;
const tabIndex = isActive ? 0 : -1;

const tabIndexAttr = isActive ? 0 : -1;
const tabLabelId = tab.id + '--tab';
return (
<li
className="wp-block-tabs__list-item"
key={ index }
role="presentation"
>
<RichText.Content
aria-controls={ tab.id }
aria-selected={ isActive }
className={ clsx( 'wp-block-tabs__tab-label', {
'is-active': isActive,
} ) }
href="#"
href={ '#' + tab.id }
id={ tabLabelId }
role="tab"
tabIndex={ tabIndex }
tab-index={ tabIndexAttr }
tagName="a"
value={ tab.label }
/>
Expand Down

0 comments on commit 3a27615

Please sign in to comment.