From f2de29830423d8c0890add9889ee07d6bf7163f1 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Tue, 7 Sep 2021 14:28:07 +1000 Subject: [PATCH] Add 'Widget Group' block to widgets screens (#34484) * 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 * 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 Co-authored-by: Kai Hao --- .../block-api/block-transforms.md | 2 +- lib/blocks.php | 2 + packages/blocks/CHANGELOG.md | 4 + packages/blocks/src/api/factory.js | 3 +- packages/blocks/src/api/test/factory.js | 14 ++-- packages/customize-widgets/src/index.js | 2 + packages/edit-widgets/src/index.js | 4 + .../src/blocks/widget-group/block.json | 18 ++++ .../widgets/src/blocks/widget-group/edit.js | 84 +++++++++++++++++++ .../src/blocks/widget-group/editor.scss | 46 ++++++++++ .../widgets/src/blocks/widget-group/index.js | 76 +++++++++++++++++ .../widgets/src/blocks/widget-group/index.php | 84 +++++++++++++++++++ .../widgets/src/blocks/widget-group/save.js | 17 ++++ packages/widgets/src/index.js | 9 ++ packages/widgets/src/style.scss | 1 + 15 files changed, 357 insertions(+), 9 deletions(-) create mode 100644 packages/widgets/src/blocks/widget-group/block.json create mode 100644 packages/widgets/src/blocks/widget-group/edit.js create mode 100644 packages/widgets/src/blocks/widget-group/editor.scss create mode 100644 packages/widgets/src/blocks/widget-group/index.js create mode 100644 packages/widgets/src/blocks/widget-group/index.php create mode 100644 packages/widgets/src/blocks/widget-group/save.js diff --git a/docs/reference-guides/block-api/block-transforms.md b/docs/reference-guides/block-api/block-transforms.md index 1078608345e526..d1de0e98fa12ac 100644 --- a/docs/reference-guides/block-api/block-transforms.md +++ b/docs/reference-guides/block-api/block-transforms.md @@ -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. diff --git a/lib/blocks.php b/lib/blocks.php index cc3f4c8662254e..77dcaae989b556 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -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', ), ), ); diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md index b51c240a069ea1..486282484503c1 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -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 diff --git a/packages/blocks/src/api/factory.js b/packages/blocks/src/api/factory.js index 5b1239c46fc7cf..3013a3d9f79e5c 100644 --- a/packages/blocks/src/api/factory.js +++ b/packages/blocks/src/api/factory.js @@ -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; } } diff --git a/packages/blocks/src/api/test/factory.js b/packages/blocks/src/api/test/factory.js index fc38d753e997b6..3b4d8671f6e1ba 100644 --- a/packages/blocks/src/api/test/factory.js +++ b/packages/blocks/src/api/test/factory.js @@ -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', () => { diff --git a/packages/customize-widgets/src/index.js b/packages/customize-widgets/src/index.js index 8751caab3a8115..ef93ba1264c8b7 100644 --- a/packages/customize-widgets/src/index.js +++ b/packages/customize-widgets/src/index.js @@ -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 diff --git a/packages/edit-widgets/src/index.js b/packages/edit-widgets/src/index.js index 15961a863047e6..c45bb294ab46fa 100644 --- a/packages/edit-widgets/src/index.js +++ b/packages/edit-widgets/src/index.js @@ -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 ); diff --git a/packages/widgets/src/blocks/widget-group/block.json b/packages/widgets/src/blocks/widget-group/block.json new file mode 100644 index 00000000000000..ec48d90eda5ca7 --- /dev/null +++ b/packages/widgets/src/blocks/widget-group/block.json @@ -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" +} diff --git a/packages/widgets/src/blocks/widget-group/edit.js b/packages/widgets/src/blocks/widget-group/edit.js new file mode 100644 index 00000000000000..663946364904de --- /dev/null +++ b/packages/widgets/src/blocks/widget-group/edit.js @@ -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 = ; + } else if ( isSelected ) { + content = ; + } else { + content = ; + } + + return ( +
{ content }
+ ); +} + +function PlaceholderContent( { clientId } ) { + return ( + <> + } + label={ __( 'Widget Group' ) } + > + + + + + ); +} + +function EditFormContent( { attributes, setAttributes, innerBlocks } ) { + return ( +
+

+ { __( 'Widget Group' ) } +

+ setAttributes( { title } ) } + /> +
+ ); +} + +function PreviewContent( { attributes, innerBlocks } ) { + return ( + <> +

+ { attributes.title || getDefaultTitle( innerBlocks ) } +

+ + + ); +} + +function getDefaultTitle( innerBlocks ) { + if ( innerBlocks.length === 0 ) { + return null; + } + return getBlockType( innerBlocks[ 0 ].name ).title; +} diff --git a/packages/widgets/src/blocks/widget-group/editor.scss b/packages/widgets/src/blocks/widget-group/editor.scss new file mode 100644 index 00000000000000..df7a459e383682 --- /dev/null +++ b/packages/widgets/src/blocks/widget-group/editor.scss @@ -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; + } +} diff --git a/packages/widgets/src/blocks/widget-group/index.js b/packages/widgets/src/blocks/widget-group/index.js new file mode 100644 index 00000000000000..012a4a0954bb24 --- /dev/null +++ b/packages/widgets/src/blocks/widget-group/index.js @@ -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 + ); + }, + }, + ], + }, +}; diff --git a/packages/widgets/src/blocks/widget-group/index.php b/packages/widgets/src/blocks/widget-group/index.php new file mode 100644 index 00000000000000..b225b08387385d --- /dev/null +++ b/packages/widgets/src/blocks/widget-group/index.php @@ -0,0 +1,84 @@ +'; + $after_title = ''; + } + + $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 .= '
'; + foreach ( $block->inner_blocks as $inner_block ) { + $html .= $inner_block->render(); + } + $html .= '
'; + + 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' ); diff --git a/packages/widgets/src/blocks/widget-group/save.js b/packages/widgets/src/blocks/widget-group/save.js new file mode 100644 index 00000000000000..203d063c27ca05 --- /dev/null +++ b/packages/widgets/src/blocks/widget-group/save.js @@ -0,0 +1,17 @@ +/** + * WordPress dependencies + */ +import { InnerBlocks, RichText } from '@wordpress/block-editor'; + +export default function save( { attributes } ) { + return ( + <> + + + + ); +} diff --git a/packages/widgets/src/index.js b/packages/widgets/src/index.js index c02afc7a3f57cf..6c88d65556d8b0 100644 --- a/packages/widgets/src/index.js +++ b/packages/widgets/src/index.js @@ -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'; diff --git a/packages/widgets/src/style.scss b/packages/widgets/src/style.scss index 4ba09348938568..72ee31dd3b859a 100644 --- a/packages/widgets/src/style.scss +++ b/packages/widgets/src/style.scss @@ -1 +1,2 @@ @import "./blocks/legacy-widget/editor.scss"; +@import "./blocks/widget-group/editor.scss";