Skip to content

Commit

Permalink
Add 'Widget Group' block to widgets screens (#34484)
Browse files Browse the repository at this point in the history
* Take all of the work on Widget Box that @getdave did

* Rename 'core/widget-box' to 'core/widget-group'

* Set up flow inspired by Legacy Widget block

* Default title to the first block's label

* Add styling

* Add space to comment

Co-authored-by: Kai Hao <[email protected]>

* Fix custom title not appearing in frontend

* Use solid line to match reusable block styling

* Prevent Widget Group from appearing in its own list of valid transforms

* Change title of Widget Group form to 'Widget Group'

* Use useSelect properly

* Remove unnecessary eslint-ignore

* Fix Widget Group in Customizer

* Update Widget Group block description

* Fix misspelling of 'argument'

* Remove unused file

* Default TextControl value to ''

* Force appender to always have light styling

Co-authored-by: Dave Smith <[email protected]>
Co-authored-by: Kai Hao <[email protected]>
  • Loading branch information
3 people authored Sep 7, 2021

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent d1427be commit f2de298
Showing 15 changed files with 357 additions and 9 deletions.
2 changes: 1 addition & 1 deletion docs/reference-guides/block-api/block-transforms.md
Original file line number Diff line number Diff line change
@@ -42,7 +42,7 @@ A transformation of type `block` is an object that takes the following parameter
- **type** _(string)_: the value `block`.
- **blocks** _(array)_: a list of known block types. It also accepts the wildcard value (`"*"`), meaning that the transform is available to _all_ block types (eg: all blocks can transform into `core/group`).
- **transform** _(function)_: a callback that receives the attributes and inner blocks of the block being processed. It should return a block object or an array of block objects.
- **isMatch** _(function, optional)_: a callback that receives the block attributes and should return a boolean. Returning `false` from this function will prevent the transform from being available and displayed as an option to the user.
- **isMatch** _(function, optional)_: a callback that receives the block attributes as the first argument and the block object as the second argument and should return a boolean. Returning `false` from this function will prevent the transform from being available and displayed as an option to the user.
- **isMultiBlock** _(boolean, optional)_: whether the transformation can be applied when multiple blocks are selected. If true, the `transform` function's first parameter will be an array containing each selected block's attributes, and the second an array of each selected block's inner blocks. False by default.
- **priority** _(number, optional)_: controls the priority with which a transformation is applied, where a lower value will take precedence over higher values. This behaves much like a [WordPress hook](https://codex.wordpress.org/Plugin_API#Hook_to_WordPress). Like hooks, the default priority is `10` when not otherwise set.

2 changes: 2 additions & 0 deletions lib/blocks.php
Original file line number Diff line number Diff line change
@@ -106,9 +106,11 @@ function gutenberg_reregister_core_block_types() {
__DIR__ . '/../build/widgets/blocks/' => array(
'block_folders' => array(
'legacy-widget',
'widget-group',
),
'block_names' => array(
'legacy-widget.php' => 'core/legacy-widget',
'widget-group.php' => 'core/widget-group',
),
),
);
4 changes: 4 additions & 0 deletions packages/blocks/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -6,6 +6,10 @@

- Register a block even when an invalid value provided for the icon setting ([#34350](https://github.com/WordPress/gutenberg/pull/34350)).

### New API

- The `isMatch` callback on block transforms now receives the block object (or block objects if `isMulti` is `true`) as its second argument.

## 11.0.0 (2021-07-29)

### Breaking Change
3 changes: 2 additions & 1 deletion packages/blocks/src/api/factory.js
Original file line number Diff line number Diff line change
@@ -224,7 +224,8 @@ const isPossibleTransformForSource = ( transform, direction, blocks ) => {
const attributes = transform.isMultiBlock
? blocks.map( ( block ) => block.attributes )
: sourceBlock.attributes;
if ( ! transform.isMatch( attributes ) ) {
const block = transform.isMultiBlock ? blocks : sourceBlock;
if ( ! transform.isMatch( attributes, block ) ) {
return false;
}
}
14 changes: 7 additions & 7 deletions packages/blocks/src/api/test/factory.js
Original file line number Diff line number Diff line change
@@ -979,7 +979,7 @@ describe( 'block factory', () => {
expect( availableBlocks ).toEqual( [] );
} );

it( 'for a non multiblock transform, the isMatch function receives the source block’s attributes object as its first argument', () => {
it( 'for a non multiblock transform, the isMatch function receives the source block’s attributes object and the block object as its arguments', () => {
const isMatch = jest.fn();

registerBlockType( 'core/updated-text-block', {
@@ -1010,10 +1010,10 @@ describe( 'block factory', () => {

getPossibleBlockTransformations( [ block ] );

expect( isMatch ).toHaveBeenCalledWith( { value: 'ribs' } );
expect( isMatch ).toHaveBeenCalledWith( { value: 'ribs' }, block );
} );

it( 'for a multiblock transform, the isMatch function receives an array containing every source block’s attributes as its first argument', () => {
it( 'for a multiblock transform, the isMatch function receives an array containing every source block’s attributes and an array of source blocks as its arguments', () => {
const isMatch = jest.fn();

registerBlockType( 'core/updated-text-block', {
@@ -1049,10 +1049,10 @@ describe( 'block factory', () => {

getPossibleBlockTransformations( [ meatBlock, cheeseBlock ] );

expect( isMatch ).toHaveBeenCalledWith( [
{ value: 'ribs' },
{ value: 'halloumi' },
] );
expect( isMatch ).toHaveBeenCalledWith(
[ { value: 'ribs' }, { value: 'halloumi' } ],
[ meatBlock, cheeseBlock ]
);
} );

describe( 'wildcard block transforms', () => {
2 changes: 2 additions & 0 deletions packages/customize-widgets/src/index.js
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ import {
import {
registerLegacyWidgetBlock,
registerLegacyWidgetVariations,
registerWidgetGroupBlock,
} from '@wordpress/widgets';
import { setFreeformContentHandlerName } from '@wordpress/blocks';
import { dispatch } from '@wordpress/data';
@@ -56,6 +57,7 @@ export function initialize( editorName, blockEditorSettings ) {
} );
}
registerLegacyWidgetVariations( blockEditorSettings );
registerWidgetGroupBlock();

// As we are unregistering `core/freeform` to avoid the Classic block, we must
// replace it with something as the default freeform content handler. Failure to
4 changes: 4 additions & 0 deletions packages/edit-widgets/src/index.js
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ import { __experimentalFetchLinkSuggestions as fetchLinkSuggestions } from '@wor
import {
registerLegacyWidgetBlock,
registerLegacyWidgetVariations,
registerWidgetGroupBlock,
} from '@wordpress/widgets';
import { dispatch } from '@wordpress/data';
import { store as interfaceStore } from '@wordpress/interface';
@@ -26,6 +27,7 @@ import { store as interfaceStore } from '@wordpress/interface';
import './store';
import './filters';
import * as widgetArea from './blocks/widget-area';

import Layout from './components/layout';
import {
ALLOW_REUSABLE_BLOCKS,
@@ -89,6 +91,8 @@ export function initialize( id, settings ) {
}
registerLegacyWidgetVariations( settings );
registerBlock( widgetArea );
registerWidgetGroupBlock();

settings.__experimentalFetchLinkSuggestions = ( search, searchOptions ) =>
fetchLinkSuggestions( search, searchOptions, settings );

18 changes: 18 additions & 0 deletions packages/widgets/src/blocks/widget-group/block.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"apiVersion": 2,
"name": "core/widget-group",
"category": "widgets",
"attributes": {
"title": {
"type": "string"
}
},
"supports": {
"html": false,
"inserter": true,
"customClassName": true,
"reusable": false
},
"editorStyle": "wp-block-widget-group-editor",
"style": "wp-block-widget-group"
}
84 changes: 84 additions & 0 deletions packages/widgets/src/blocks/widget-group/edit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* WordPress dependencies
*/
import {
useBlockProps,
BlockIcon,
ButtonBlockAppender,
InnerBlocks,
store as blockEditorStore,
} from '@wordpress/block-editor';
import { Placeholder, TextControl } from '@wordpress/components';
import { group as groupIcon } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import { useSelect } from '@wordpress/data';
import { getBlockType } from '@wordpress/blocks';

export default function Edit( props ) {
const { clientId, isSelected } = props;
const { innerBlocks } = useSelect( ( select ) =>
select( blockEditorStore ).getBlock( clientId )
);

let content;
if ( innerBlocks.length === 0 ) {
content = <PlaceholderContent { ...props } />;
} else if ( isSelected ) {
content = <EditFormContent { ...props } innerBlocks={ innerBlocks } />;
} else {
content = <PreviewContent { ...props } innerBlocks={ innerBlocks } />;
}

return (
<div { ...useBlockProps( { className: 'widget' } ) }>{ content }</div>
);
}

function PlaceholderContent( { clientId } ) {
return (
<>
<Placeholder
className="wp-block-widget-group__placeholder"
icon={ <BlockIcon icon={ groupIcon } /> }
label={ __( 'Widget Group' ) }
>
<ButtonBlockAppender rootClientId={ clientId } />
</Placeholder>
<InnerBlocks renderAppender={ false } />
</>
);
}

function EditFormContent( { attributes, setAttributes, innerBlocks } ) {
return (
<div className="wp-block-widget-group__edit-form">
<h2 className="wp-block-widget-group__edit-form-title">
{ __( 'Widget Group' ) }
</h2>
<TextControl
label={ __( 'Title' ) }
placeholder={ getDefaultTitle( innerBlocks ) }
value={ attributes.title ?? '' }
onChange={ ( title ) => setAttributes( { title } ) }
/>
</div>
);
}

function PreviewContent( { attributes, innerBlocks } ) {
return (
<>
<h2 className="widget-title">
{ attributes.title || getDefaultTitle( innerBlocks ) }
</h2>
<InnerBlocks />
</>
);
}

function getDefaultTitle( innerBlocks ) {
if ( innerBlocks.length === 0 ) {
return null;
}
return getBlockType( innerBlocks[ 0 ].name ).title;
}
46 changes: 46 additions & 0 deletions packages/widgets/src/blocks/widget-group/editor.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
.wp-block-widget-group {
&.has-child-selected::after {
border-radius: $radius-block-ui;
border: 1px solid var(--wp-admin-theme-color);
bottom: 0;
content: "";
left: 0;
position: absolute;
right: 0;
top: 0;
}

.widget-title {
font-family: $default-font;
font-size: 18px;
font-weight: 600;
}
}

.wp-block-widget-group__placeholder {
.block-editor-inserter {
width: 100%;
}
}

// Force the appender to always have "light mode" styling as it appears in a
// light colored placeholder.
.is-dark-theme .wp-block-widget-group__placeholder .block-editor-button-block-appender {
box-shadow: inset 0 0 0 $border-width $gray-900;
color: $gray-900;
}

.wp-block-widget-group__edit-form {
background: $white;
border-radius: $radius-block-ui;
border: 1px solid $gray-900;
padding: $grid-unit-15 - 1px; // Subtract the border width.

.wp-block-widget-group__edit-form-title {
color: $black;
font-family: $default-font;
font-size: 14px;
font-weight: 600;
margin: 0 0 $grid-unit-15 0;
}
}
76 changes: 76 additions & 0 deletions packages/widgets/src/blocks/widget-group/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { createBlock } from '@wordpress/blocks';
import { group as icon } from '@wordpress/icons';

/**
* Internal dependencies
*/
import metadata from './block.json';
import edit from './edit';
import save from './save';
const { name } = metadata;
export { metadata, name };

export const settings = {
title: __( 'Widget Group' ),
description: __(
'Create a classic widget layout with a title that’s styled by your theme for your widget areas.'
),
icon,
__experimentalLabel: ( { name: label } ) => label,
edit,
save,
transforms: {
from: [
{
type: 'block',
isMultiBlock: true,
blocks: [ '*' ],
isMatch( attributes, blocks ) {
// Avoid transforming existing `widget-group` blocks.
return ! blocks.some(
( block ) => block.name === 'core/widget-group'
);
},
__experimentalConvert( blocks ) {
// Put the selected blocks inside the new Widget Group's innerBlocks.
let innerBlocks = [
...blocks.map( ( block ) => {
return createBlock(
block.name,
block.attributes,
block.innerBlocks
);
} ),
];

// If the first block is a heading then assume this is intended
// to be the Widget's "title".
const firstHeadingBlock =
innerBlocks[ 0 ].name === 'core/heading'
? innerBlocks[ 0 ]
: null;

// Remove the first heading block as we're copying
// it's content into the Widget Group's title attribute.
innerBlocks = innerBlocks.filter(
( block ) => block !== firstHeadingBlock
);

return createBlock(
'core/widget-group',
{
...( firstHeadingBlock && {
title: firstHeadingBlock.attributes.content,
} ),
},
innerBlocks
);
},
},
],
},
};
84 changes: 84 additions & 0 deletions packages/widgets/src/blocks/widget-group/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php
/**
* Server-side rendering of the `core/widget-group` block.
*
* @package WordPress
*/

/**
* Renders the 'core/widget-group' block.
*
* @param array $attributes The block attributes.
* @param string $content The block content.
* @param WP_Block $block The block.
*
* @return string Rendered block.
*/
function render_block_core_widget_group( $attributes, $content, $block ) {
global $wp_registered_sidebars, $_sidebar_being_rendered;

if ( isset( $wp_registered_sidebars[ $_sidebar_being_rendered ] ) ) {
$before_title = $wp_registered_sidebars[ $_sidebar_being_rendered ]['before_title'];
$after_title = $wp_registered_sidebars[ $_sidebar_being_rendered ]['after_title'];
} else {
$before_title = '<h2 class="widget-title">';
$after_title = '</h2>';
}

$html = '';

if ( ! empty( $attributes['title'] ) ) {
$title = $attributes['title'];
} elseif ( ! empty( $block->inner_blocks ) ) {
$title = $block->inner_blocks[0]->block_type->title;
}

if ( isset( $title ) ) {
$html .= $before_title . $title . $after_title;
}

$html .= '<div class="wp-widget-group__inner-blocks">';
foreach ( $block->inner_blocks as $inner_block ) {
$html .= $inner_block->render();
}
$html .= '</div>';

return $html;
}

/**
* Registers the 'core/widget-group' block.
*/
function register_block_core_widget_group() {
register_block_type_from_metadata(
__DIR__ . '/widget-group',
array(
'render_callback' => 'render_block_core_widget_group',
)
);
}

add_action( 'init', 'register_block_core_widget_group' );

/**
* Make a note of the sidebar being rendered before WordPress starts rendering
* it. This lets us get to the current sidebar in
* render_block_core_widget_group().
*
* @param int|string $index Index, name, or ID of the dynamic sidebar.
*/
function note_sidebar_being_rendered( $index ) {
global $_sidebar_being_rendered;
$_sidebar_being_rendered = $index;
}
add_action( 'dynamic_sidebar_before', 'note_sidebar_being_rendered' );

/**
* Clear whatever we set in note_sidebar_being_rendered() after WordPress
* finishes rendering a sidebar.
*/
function discard_sidebar_being_rendered() {
global $_sidebar_being_rendered;
unset( $_sidebar_being_rendered );
}
add_action( 'dynamic_sidebar_after', 'discard_sidebar_being_rendered' );
17 changes: 17 additions & 0 deletions packages/widgets/src/blocks/widget-group/save.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* WordPress dependencies
*/
import { InnerBlocks, RichText } from '@wordpress/block-editor';

export default function save( { attributes } ) {
return (
<>
<RichText.Content
tagName="h2"
className="widget-title"
value={ attributes.title }
/>
<InnerBlocks.Content />
</>
);
}
9 changes: 9 additions & 0 deletions packages/widgets/src/index.js
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ import { registerBlockType } from '@wordpress/blocks';
* Internal dependencies
*/
import * as legacyWidget from './blocks/legacy-widget';
import * as widgetGroup from './blocks/widget-group';

export * from './components';
export * from './utils';
@@ -24,4 +25,12 @@ export function registerLegacyWidgetBlock() {
registerBlockType( { name, ...metadata }, settings );
}

/**
* Registers the Widget Group block.
*/
export function registerWidgetGroupBlock() {
const { metadata, settings, name } = widgetGroup;
registerBlockType( { name, ...metadata }, settings );
}

export { default as registerLegacyWidgetVariations } from './register-legacy-widget-variations';
1 change: 1 addition & 0 deletions packages/widgets/src/style.scss
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
@import "./blocks/legacy-widget/editor.scss";
@import "./blocks/widget-group/editor.scss";

0 comments on commit f2de298

Please sign in to comment.