Skip to content

Commit

Permalink
Initial commit of tabs
Browse files Browse the repository at this point in the history
  • Loading branch information
sethrubenstein committed Dec 19, 2024
1 parent b226cee commit 4154e4c
Show file tree
Hide file tree
Showing 27 changed files with 1,933 additions and 1 deletion.
2 changes: 2 additions & 0 deletions lib/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,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',
Expand Down
3 changes: 2 additions & 1 deletion packages/block-library/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
"./image/view": "./build-module/image/view.js",
"./navigation/view": "./build-module/navigation/view.js",
"./query/view": "./build-module/query/view.js",
"./search/view": "./build-module/search/view.js"
"./search/view": "./build-module/search/view.js",
"./tabs/view": "./build-module/tabs/view.js"
},
"sideEffects": [
"build-style/**",
Expand Down
4 changes: 4 additions & 0 deletions packages/block-library/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,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';
Expand Down Expand Up @@ -177,6 +179,8 @@ const getAllBlocks = () => {
socialLinks,
spacer,
table,
tab,
tabs,
tagCloud,
textColumns,
verse,
Expand Down
43 changes: 43 additions & 0 deletions packages/block-library/src/tab/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Tab

Contributors: Pew Research Center, creativecoder
Tags: block
Tested up to: 6.7
Stable tag: 1.0.0
License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

## Description

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.

## Instructions

This section describes how to use the block.

## Frequently Asked Questions

= A question that someone might have =

An answer to that question.

### What about foo bar?

Answer to foo bar dilemma.

## Screenshots

1. This screen shot description corresponds to screenshot-1.(png|jpg|jpeg|gif).
2. This is the second screen shot
3. You can store screenshots in a .docs folder in this block directory...

## Changelog

= 0.1.0 =
* Release

## Developer Notes

You may provide arbitrary sections, in the same format as the ones above. This may be of use for extremely complicated
blocks where more information needs to be conveyed that doesn't fit into the categories of "description" or
"installation." Arbitrary sections will be shown below the built-in sections outlined above.
63 changes: 63 additions & 0 deletions packages/block-library/src/tab/binding.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* WordPress dependencies
*/
import {
registerBlockVariation,
registerBlockBindingsSource,
} from '@wordpress/blocks';

export default function registerTabLabelBinding() {
const allowedAttributes = [ 'content' ];
registerBlockBindingsSource( {
name: 'tab/label',
usesContext: [ 'tab/label' ],
getValues( { bindings } ) {
const values = {};
for ( const [ attributeName, source ] of Object.entries(
bindings
) ) {
if ( allowedAttributes.includes( source.args.key ) ) {
values[ attributeName ] = 'Tab Title !';
}
}
return values;
},
} );
registerBlockVariation( 'core/paragraph', {
name: 'core-tab-label',
title: 'Tab: Label',
category: 'design',
attributes: {
content: 'Tab Label',
metadata: {
bindings: {
content: {
source: 'core/tab',
args: {
contextKey: 'tab/label',
},
},
},
},
},
} );
registerBlockVariation( 'core/heading', {
name: 'core-tab-label',
title: 'Tab: Label',
category: 'design',
attributes: {
content: 'Tab Label',
level: 3,
metadata: {
bindings: {
content: {
source: 'core/tab',
args: {
contextKey: 'tab/label',
},
},
},
},
},
} );
};
54 changes: 54 additions & 0 deletions packages/block-library/src/tab/block.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "core/tab",
"title": "Tab",
"version": "1.0.0",
"category": "design",
"attributes": {
"label": {
"type": "string",
"default": ""
},
"slug": {
"type": "string",
"default": ""
},
"tabIndex": {
"type": "number"
}
},
"parent": [ "core/tabs" ],
"supports": {
"anchor": true,
"html": false,
"reusable": false,
"color": {
"background": true,
"heading": true,
"text": true
},
"spacing": {
"padding": true,
"blockGap": true,
"__experimentalDefaultControls": {
"padding": true
}
},
"typography": {
"fontSize": true,
"__experimentalFontFamily": true,
"__experimentalDefaultControls": {
"fontSize": true,
"__experimentalFontFamily": true
}
}
},
"providesContext": {
"tab/label": "label",
"tab/slug": "slug",
"tab/index": "tabIndex"
},
"textdomain": "tab",
"style": "wp-block-tab"
}
157 changes: 157 additions & 0 deletions packages/block-library/src/tab/edit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* External Dependencies
*/

/**
* WordPress dependencies
*/
import {
InnerBlocks,
useBlockProps,
useInnerBlocksProps,
store as blockEditorStore,
InspectorControls,
} 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';

/**
* 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 }`;
}

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 { __unstableMarkNextChangeAsNotPersistent } =
useDispatch( blockEditorStore );

const { hasInnerBlockSelected, blockIndex } = useSelect(
( select ) => {
return {
hasInnerBlockSelected:
select( blockEditorStore ).hasSelectedInnerBlock(
clientId
),
blockIndex:
select( blockEditorStore ).getBlockIndex( clientId ),
};
},
[ clientId ]
);

/**
* These two hooks ensure the tab block's slug and tabIndex attributes are kept in sync with the parent tabs block.
*/
// 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();
setAttributes( { tabIndex: blockIndex } );
}
}, [
__unstableMarkNextChangeAsNotPersistent,
blockIndex,
setAttributes,
tabIndex,
] );

const displayEditComponent = useMemo( () => {
return isActive || isSelected || hasInnerBlockSelected;
}, [ isActive, hasInnerBlockSelected, isSelected ] );

// 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 }
/>
);
}

return <div hidden />;
}
19 changes: 19 additions & 0 deletions packages/block-library/src/tab/icon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* WordPress dependencies
*/
import { SVG, Path } from '@wordpress/components';

export default (
<SVG
width="24"
height="24"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<Path
fillRule="evenodd"
clipRule="evenodd"
d="M5.5498 10.3501V6.3501H9.8498V10.3501H11.3498V6.1001C11.3498 5.40974 10.7902 4.8501 10.0998 4.8501H5.2998C4.60945 4.8501 4.0498 5.40974 4.0498 6.1001V10.3501H5.5498ZM20 12.6001H4V14.1001L20 14.1001V12.6001ZM14 17.1001H4V18.6001H14V17.1001Z"
/>
</SVG>
);
Loading

0 comments on commit 4154e4c

Please sign in to comment.