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
1 parent d1427be commit f2de298
Show file tree
Hide file tree
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
Expand Up @@ -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.

Expand Down
2 changes: 2 additions & 0 deletions lib/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
),
),
);
Expand Down
4 changes: 4 additions & 0 deletions packages/blocks/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion packages/blocks/src/api/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
14 changes: 7 additions & 7 deletions packages/blocks/src/api/test/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', {
Expand Down Expand Up @@ -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', {
Expand Down Expand Up @@ -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', () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/customize-widgets/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import {
registerLegacyWidgetBlock,
registerLegacyWidgetVariations,
registerWidgetGroupBlock,
} from '@wordpress/widgets';
import { setFreeformContentHandlerName } from '@wordpress/blocks';
import { dispatch } from '@wordpress/data';
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions packages/edit-widgets/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -89,6 +91,8 @@ export function initialize( id, settings ) {
}
registerLegacyWidgetVariations( settings );
registerBlock( widgetArea );
registerWidgetGroupBlock();

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

Expand Down
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
);
},
},
],
},
};
Loading

0 comments on commit f2de298

Please sign in to comment.