diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 72281a53c3dd18..511d65f4149be2 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -887,6 +887,17 @@ Add white space between blocks and customize its height. ([Source](https://githu - **Supports:** anchor, interactivity (clientNavigation), spacing (margin) - **Attributes:** height, width +## Tab + +Single tab within a tabs block. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/tab)) + +- **Name:** core/tab +- **Experimental:** true +- **Category:** design +- **Parent:** core/tabs +- **Supports:** anchor, ~~html~~, ~~reusable~~ +- **Attributes:** isActive, label, slug, tabIndex + ## Table Create structured content in rows and columns to display information. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/table)) @@ -906,6 +917,17 @@ Summarize your post with a list of headings. Add HTML anchors to Heading blocks - **Supports:** color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** headings, onlyIncludeCurrentPage +## Tabs + +Organize content into tabs. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/tabs)) + +- **Name:** core/tabs +- **Experimental:** true +- **Category:** design +- **Allowed Blocks:** core/tab +- **Supports:** align (full, wide), color (text, ~~background~~), interactivity, spacing (margin, padding), ~~html~~ +- **Attributes:** innerTabs + ## Tag Cloud A cloud of popular keywords, each sized by how often it appears. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/tag-cloud)) diff --git a/lib/blocks.php b/lib/blocks.php index c3fdb26700c58c..3f53eb8c4372fe 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -116,6 +116,8 @@ function gutenberg_reregister_core_block_types() { 'site-logo.php' => 'core/site-logo', 'site-tagline.php' => 'core/site-tagline', 'site-title.php' => 'core/site-title', + 'tab.php' => 'core/tab', + 'tabs.php' => 'core/tabs', 'tag-cloud.php' => 'core/tag-cloud', 'template-part.php' => 'core/template-part', 'term-description.php' => 'core/term-description', diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss index 52f3aa64287fae..896f7fe4ab6720 100644 --- a/packages/block-library/src/editor.scss +++ b/packages/block-library/src/editor.scss @@ -44,6 +44,7 @@ @import "./social-links/editor.scss"; @import "./spacer/editor.scss"; @import "./table/editor.scss"; +@import "./tabs/editor.scss"; @import "./tag-cloud/editor.scss"; @import "./template-part/editor.scss"; @import "./text-columns/editor.scss"; diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index 56365c87a268fd..2ebd1b0cb0ecc3 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -114,6 +114,8 @@ import * as socialLinks from './social-links'; import * as spacer from './spacer'; import * as table from './table'; import * as tableOfContents from './table-of-contents'; +import * as tab from './tab'; +import * as tabs from './tabs'; import * as tagCloud from './tag-cloud'; import * as templatePart from './template-part'; import * as termDescription from './term-description'; @@ -232,6 +234,12 @@ const getAllBlocks = () => { queryTitle, postAuthorBiography, ]; + + if ( window.__experimentalEnableBlockExperiments ) { + blocks.push( tab ); + blocks.push( tabs ); + } + if ( window?.__experimentalEnableFormBlocks ) { blocks.push( form ); blocks.push( formInput ); diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss index 8f17cd7a50f55c..e4f029c084a003 100644 --- a/packages/block-library/src/style.scss +++ b/packages/block-library/src/style.scss @@ -53,9 +53,11 @@ @import "./site-title/style.scss"; @import "./social-links/style.scss"; @import "./spacer/style.scss"; +@import "./tab/style.scss"; @import "./tag-cloud/style.scss"; @import "./table/style.scss"; @import "./table-of-contents/style.scss"; +@import "./tabs/style.scss"; @import "./term-description/style.scss"; @import "./text-columns/style.scss"; @import "./verse/style.scss"; diff --git a/packages/block-library/src/tab/block.json b/packages/block-library/src/tab/block.json new file mode 100644 index 00000000000000..9234361b44c86d --- /dev/null +++ b/packages/block-library/src/tab/block.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "core/tab", + "title": "Tab", + "category": "design", + "description": "Single tab within a tabs block.", + "textdomain": "default", + "__experimental": true, + "attributes": { + "isActive": { + "type": "boolean", + "default": false + }, + "label": { + "type": "string", + "default": "" + }, + "slug": { + "type": "string", + "default": "" + }, + "tabIndex": { + "type": "number" + } + }, + "parent": [ "core/tabs" ], + "supports": { + "anchor": true, + "html": false, + "reusable": false + }, + "style": "wp-block-tab" +} diff --git a/packages/block-library/src/tab/edit.js b/packages/block-library/src/tab/edit.js new file mode 100644 index 00000000000000..0d86e5e9ae8015 --- /dev/null +++ b/packages/block-library/src/tab/edit.js @@ -0,0 +1,68 @@ +/** + * WordPress dependencies + */ +import { + InnerBlocks, + useBlockProps, + useInnerBlocksProps, + store as blockEditorStore, +} from '@wordpress/block-editor'; +import { useSelect } from '@wordpress/data'; +import { useEffect } from '@wordpress/element'; +import { cleanForSlug } from '@wordpress/url'; + +/** + * Generates a slug from a tab's text label. + * + * @param {string} label Tab label RichText value. + * @param {number} tabIndex Tab index value. + * + * @return {string} The generated slug with HTML stripped out. + */ +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 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 ] + ); + + useEffect( () => { + if ( label ) { + setAttributes( { slug: slugFromLabel( label, tabIndex ) } ); + } + }, [ label, setAttributes, tabIndex ] ); + + const blockProps = useBlockProps(); + const innerBlocksProps = useInnerBlocksProps( blockProps, { + renderAppender: hasChildBlocks + ? undefined + : InnerBlocks.ButtonBlockAppender, + } ); + return ( +