diff --git a/docs/designers-developers/developers/block-api/block-registration.md b/docs/designers-developers/developers/block-api/block-registration.md index 1dc03779d4511..4656097189065 100644 --- a/docs/designers-developers/developers/block-api/block-registration.md +++ b/docs/designers-developers/developers/block-api/block-registration.md @@ -264,7 +264,10 @@ An object describing a variation defined for the block type can contain the foll - `attributes` (optional, type `Object`) – Values that override block attributes. - `innerBlocks` (optional, type `Array[]`) – Initial configuration of nested blocks. - `example` (optional, type `Object`) – Example provides structured data for the block preview. You can set to `undefined` to disable the preview shown for the block type. -- `scope` (optional, type `string[]`) - the list of scopes where the variation is applicable. When not provided, it assumes all available scopes. Available options: `block`, `inserter`. +- `scope` (optional, type `WPBlockVariationScope[]`) - the list of scopes where the variation is applicable. When not provided, it defaults to `block` and `inserter`. Available options: + - `inserter` - Block Variation is shown on the inserter. + - `block` - Used by blocks to filter specific block variations. Mostly used in Placeholder patterns like `Columns` block. + - `transform` - Block Variation will be shown in the component for Block Variations transformations. - `keywords` (optional, type `string[]`) - An array of terms (which can be translated) that help users discover the variation while searching. It's also possible to override the default block style variation using the `className` attribute when defining block variations. diff --git a/packages/block-editor/src/components/block-card/index.js b/packages/block-editor/src/components/block-card/index.js index aaff1af27e16d..c209ea39b5c73 100644 --- a/packages/block-editor/src/components/block-card/index.js +++ b/packages/block-editor/src/components/block-card/index.js @@ -3,16 +3,14 @@ */ import BlockIcon from '../block-icon'; -function BlockCard( { blockType } ) { +function BlockCard( { blockType: { icon, title, description } } ) { return (
- +
-

- { blockType.title } -

+

{ title }

- { blockType.description } + { description }
diff --git a/packages/block-editor/src/components/block-inspector/index.js b/packages/block-editor/src/components/block-inspector/index.js index 259f4742febcd..3d68af7c08391 100644 --- a/packages/block-editor/src/components/block-inspector/index.js +++ b/packages/block-editor/src/components/block-inspector/index.js @@ -23,6 +23,7 @@ import InspectorAdvancedControls from '../inspector-advanced-controls'; import BlockStyles from '../block-styles'; import MultiSelectionInspector from '../multi-selection-inspector'; import DefaultStylePicker from '../default-style-picker'; +import BlockVariationTransforms from '../block-variation-transforms'; const BlockInspector = ( { blockType, count, @@ -66,6 +67,7 @@ const BlockInspector = ( { return (
+ { hasBlockStyles && (
diff --git a/packages/block-editor/src/components/block-variation-transforms/README.md b/packages/block-editor/src/components/block-variation-transforms/README.md new file mode 100644 index 0000000000000..494c734502979 --- /dev/null +++ b/packages/block-editor/src/components/block-variation-transforms/README.md @@ -0,0 +1,54 @@ +# Block Variation Transforms + +This component allows to display the selected block's variations which have the `transform` option set in `scope` property and to choose one of them. + +By selecting such a variation an update to the selected block's attributes happen, based on the variation's attributes. + +## Table of contents + +1. [Development guidelines](#development-guidelines) +2. [Related components](#related-components) + +## Development guidelines + +### Usage + +Renders the block's variations which have the `transform` option set in `scope` property. + +```jsx +import { useSelect } from '@wordpress/data'; +import { + __experimentalBlockVariationTransforms as BlockVariationTransforms, +} from '@wordpress/block-editor'; + +const MyBlockVariationTransforms = () => { + const { selectedBlockClientId } = useSelect( + ( select ) => { + const { getSelectedBlockClientId } = select( + 'core/block-editor' + ); + return { + selectedBlockClientId: getSelectedBlockClientId(), + }; + } + ); + + return ( + + ); +}; +``` + +### Props + +#### blockClientId + +The block's client id. + +- Type: `string` + +## Related components + +Block Editor components are components that can be used to compose the UI of your block editor. Thus, they can only be used under a [BlockEditorProvider](https://github.com/WordPress/gutenberg/blob/master/packages/block-editor/src/components/provider/README.md) in the components tree. diff --git a/packages/block-editor/src/components/block-variation-transforms/index.js b/packages/block-editor/src/components/block-variation-transforms/index.js new file mode 100644 index 0000000000000..5d07bbe8d9a82 --- /dev/null +++ b/packages/block-editor/src/components/block-variation-transforms/index.js @@ -0,0 +1,95 @@ +/** + * External dependencies + */ +import { isMatch } from 'lodash'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + DropdownMenu, + MenuGroup, + MenuItemsChoice, +} from '@wordpress/components'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { useState, useEffect } from '@wordpress/element'; +import { chevronDown } from '@wordpress/icons'; + +export const getMatchingVariationName = ( blockAttributes, variations ) => { + if ( ! variations || ! blockAttributes ) return; + const matches = variations.filter( ( { attributes } ) => { + if ( ! attributes || ! Object.keys( attributes ).length ) return false; + return isMatch( blockAttributes, attributes ); + } ); + if ( matches.length !== 1 ) return; + return matches[ 0 ].name; +}; + +function __experimentalBlockVariationTransforms( { blockClientId } ) { + const [ selectedValue, setSelectedValue ] = useState(); + const { updateBlockAttributes } = useDispatch( 'core/block-editor' ); + const { variations, blockAttributes } = useSelect( + ( select ) => { + const { getBlockVariations } = select( 'core/blocks' ); + const { getBlockName, getBlockAttributes } = select( + 'core/block-editor' + ); + const blockName = blockClientId && getBlockName( blockClientId ); + return { + variations: + blockName && getBlockVariations( blockName, 'transform' ), + blockAttributes: getBlockAttributes( blockClientId ), + }; + }, + [ blockClientId ] + ); + useEffect( () => { + setSelectedValue( + getMatchingVariationName( blockAttributes, variations ) + ); + }, [ blockAttributes, variations ] ); + if ( ! variations?.length ) return null; + + const selectOptions = variations.map( + ( { name, title, description } ) => ( { + value: name, + label: title, + info: description, + } ) + ); + const onSelectVariation = ( variationName ) => { + updateBlockAttributes( blockClientId, { + ...variations.find( ( { name } ) => name === variationName ) + .attributes, + } ); + }; + const baseClass = 'block-editor-block-variation-transforms'; + return ( + + { () => ( +
+ + + +
+ ) } +
+ ); +} + +export default __experimentalBlockVariationTransforms; diff --git a/packages/block-editor/src/components/block-variation-transforms/style.scss b/packages/block-editor/src/components/block-variation-transforms/style.scss new file mode 100644 index 0000000000000..fd929854018f1 --- /dev/null +++ b/packages/block-editor/src/components/block-variation-transforms/style.scss @@ -0,0 +1,38 @@ +.block-editor-block-variation-transforms { + padding: 0 $grid-unit-20 $grid-unit-20 56px; + width: 100%; + + .components-dropdown-menu__toggle { + border: 1px solid $gray-700; + border-radius: $radius-block-ui; + min-height: 30px; + width: 100%; + position: relative; + text-align: left; + justify-content: left; + padding: 6px 12px; + + // For all button sizes allow sufficient space for the + // dropdown "arrow" icon to display. + &.components-dropdown-menu__toggle { + padding-right: $icon-size; + } + + &:focus:not(:disabled) { + border-color: var(--wp-admin-theme-color); + box-shadow: 0 0 0 ($border-width-focus - $border-width) var(--wp-admin-theme-color); + } + + svg { + height: 100%; + padding: 0; + position: absolute; + right: 0; + top: 0; + } + } +} + +.block-editor-block-variation-transforms__popover .components-popover__content { + min-width: 230px; +} diff --git a/packages/block-editor/src/components/block-variation-transforms/test/index.js b/packages/block-editor/src/components/block-variation-transforms/test/index.js new file mode 100644 index 0000000000000..31b75099919ca --- /dev/null +++ b/packages/block-editor/src/components/block-variation-transforms/test/index.js @@ -0,0 +1,75 @@ +/** + * Internal dependencies + */ +import { getMatchingVariationName } from '../index'; + +describe( 'BlockVariationTransforms', () => { + describe( 'getMatchingVariationName', () => { + describe( 'should not find a match', () => { + it( 'when no variations or attributes passed', () => { + expect( + getMatchingVariationName( null, { content: 'hi' } ) + ).toBeUndefined(); + expect( getMatchingVariationName( {} ) ).toBeUndefined(); + } ); + it( 'when no variation matched', () => { + const variations = [ + { name: 'one', attributes: { level: 1 } }, + { name: 'two', attributes: { level: 2 } }, + ]; + expect( + getMatchingVariationName( { level: 4 }, variations ) + ).toBeUndefined(); + } ); + it( 'when more than one match found', () => { + const variations = [ + { name: 'one', attributes: { level: 1 } }, + { name: 'two', attributes: { level: 1, content: 'hi' } }, + ]; + expect( + getMatchingVariationName( + { level: 1, content: 'hi', other: 'prop' }, + variations + ) + ).toBeUndefined(); + } ); + it( 'when variation is a superset of attributes', () => { + const variations = [ + { name: 'one', attributes: { level: 1, content: 'hi' } }, + ]; + expect( + getMatchingVariationName( + { level: 1, other: 'prop' }, + variations + ) + ).toBeUndefined(); + } ); + } ); + describe( 'should find a match', () => { + it( 'when variation has one attribute', () => { + const variations = [ + { name: 'one', attributes: { level: 1 } }, + { name: 'two', attributes: { level: 2 } }, + ]; + expect( + getMatchingVariationName( + { level: 2, content: 'hi', other: 'prop' }, + variations + ) + ).toEqual( 'two' ); + } ); + it( 'when variation has many attributes', () => { + const variations = [ + { name: 'one', attributes: { level: 1, content: 'hi' } }, + { name: 'two', attributes: { level: 2 } }, + ]; + expect( + getMatchingVariationName( + { level: 1, content: 'hi', other: 'prop' }, + variations + ) + ).toEqual( 'one' ); + } ); + } ); + } ); +} ); diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index c6506c5c58f7a..939cf068e0cc7 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -21,6 +21,7 @@ export { BlockNavigationBlockFill as __experimentalBlockNavigationBlockFill } fr export { default as __experimentalBlockNavigationEditor } from './block-navigation/editor'; export { default as __experimentalBlockNavigationTree } from './block-navigation/tree'; export { default as __experimentalBlockVariationPicker } from './block-variation-picker'; +export { default as __experimentalBlockVariationTransforms } from './block-variation-transforms'; export { default as BlockVerticalAlignmentToolbar } from './block-vertical-alignment-toolbar'; export { default as ButtonBlockerAppender } from './button-block-appender'; export { default as ColorPalette } from './color-palette'; diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index bc117ec4307bb..5421186cbdff5 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -26,6 +26,7 @@ @import "./components/block-switcher/style.scss"; @import "./components/block-types-list/style.scss"; @import "./components/block-variation-picker/style.scss"; +@import "./components/block-variation-transforms/style.scss"; @import "./components/button-block-appender/style.scss"; @import "./components/colors-gradients/style.scss"; @import "./components/contrast-checker/style.scss"; diff --git a/packages/block-library/src/navigation/index.js b/packages/block-library/src/navigation/index.js index 3139a88c908ff..322d7f5cd017e 100644 --- a/packages/block-library/src/navigation/index.js +++ b/packages/block-library/src/navigation/index.js @@ -11,6 +11,7 @@ import metadata from './block.json'; import edit from './edit'; import save from './save'; import deprecated from './deprecated'; +import variations from './variations'; const { name } = metadata; @@ -18,31 +19,12 @@ export { metadata, name }; export const settings = { title: __( 'Navigation' ), - icon, - description: __( 'A collection of blocks that allow visitors to get around your site.' ), - keywords: [ __( 'menu' ), __( 'navigation' ), __( 'links' ) ], - - variations: [ - { - name: 'horizontal', - isDefault: true, - title: __( 'Navigation (horizontal)' ), - description: __( 'Links shown in a row.' ), - attributes: { orientation: 'horizontal' }, - }, - { - name: 'vertical', - title: __( 'Navigation (vertical)' ), - description: __( 'Links shown in a column.' ), - attributes: { orientation: 'vertical' }, - }, - ], - + variations, example: { innerBlocks: [ { @@ -71,10 +53,7 @@ export const settings = { }, ], }, - edit, - save, - deprecated, }; diff --git a/packages/block-library/src/navigation/variations.js b/packages/block-library/src/navigation/variations.js new file mode 100644 index 0000000000000..307fa8c0c1204 --- /dev/null +++ b/packages/block-library/src/navigation/variations.js @@ -0,0 +1,24 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +const variations = [ + { + name: 'horizontal', + isDefault: true, + title: __( 'Navigation (horizontal)' ), + description: __( 'Links shown in a row.' ), + attributes: { orientation: 'horizontal' }, + scope: [ 'inserter', 'transform' ], + }, + { + name: 'vertical', + title: __( 'Navigation (vertical)' ), + description: __( 'Links shown in a column.' ), + attributes: { orientation: 'vertical' }, + scope: [ 'inserter', 'transform' ], + }, +]; + +export default variations; diff --git a/packages/block-library/src/post-hierarchical-terms/variations.js b/packages/block-library/src/post-hierarchical-terms/variations.js index 8ff6dcc07f910..48ef92afc1097 100644 --- a/packages/block-library/src/post-hierarchical-terms/variations.js +++ b/packages/block-library/src/post-hierarchical-terms/variations.js @@ -8,7 +8,7 @@ const variations = [ name: 'category', title: __( 'Post Categories' ), icon: 'category', - is_default: true, + isDefault: true, attributes: { term: 'category' }, }, ]; diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 9052c76faf694..db9f1a3b7fd61 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -70,7 +70,7 @@ import { DEPRECATED_ENTRY_KEYS } from './constants'; /** * Named block variation scopes. * - * @typedef {'block'|'inserter'} WPBlockVariationScope + * @typedef {'block'|'inserter'|'transform'} WPBlockVariationScope */ /** diff --git a/packages/blocks/src/store/selectors.js b/packages/blocks/src/store/selectors.js index 21e88311b522f..5513ff4e4c488 100644 --- a/packages/blocks/src/store/selectors.js +++ b/packages/blocks/src/store/selectors.js @@ -90,7 +90,8 @@ export function getBlockVariations( state, blockName, scope ) { return variations; } return variations.filter( ( variation ) => { - return ! variation.scope || variation.scope.includes( scope ); + // For backward compatibility reasons, variation's scope defaults to `block` and `inserter` when not set. + return ( variation.scope || [ 'block', 'inserter' ] ).includes( scope ); } ); } diff --git a/packages/blocks/src/store/test/selectors.js b/packages/blocks/src/store/test/selectors.js index fde5d92253622..328293a898fcd 100644 --- a/packages/blocks/src/store/test/selectors.js +++ b/packages/blocks/src/store/test/selectors.js @@ -11,6 +11,7 @@ import deepFreeze from 'deep-freeze'; import { getBlockSupport, getChildBlockNames, + getBlockVariations, getDefaultBlockVariation, getGroupingBlockName, isMatchingSearchTerm, @@ -220,7 +221,7 @@ describe( 'selectors', () => { } ); } ); - describe( 'getDefaultBlockVariation', () => { + describe( 'Testing block variations selectors', () => { const blockName = 'block/name'; const createBlockVariationsState = ( variations ) => { return deepFreeze( { @@ -238,55 +239,94 @@ describe( 'selectors', () => { const thirdBlockVariation = { name: 'third-block-variation', }; + describe( 'getBlockVariations', () => { + it( 'should return undefined if no variations exists', () => { + expect( + getBlockVariations( { blockVariations: {} }, blockName ) + ).toBeUndefined(); + } ); + it( 'should return all variations if scope is not provided', () => { + const variations = [ + firstBlockVariation, + secondBlockVariation, + ]; + const state = createBlockVariationsState( variations ); + expect( getBlockVariations( state, blockName ) ).toEqual( + variations + ); + } ); + it( 'should return variations with scope not set at all or explicitly set', () => { + const variations = [ + { ...firstBlockVariation, scope: [ 'inserter' ] }, + { name: 'only-block', scope: [ 'block' ] }, + { + name: 'multiple-scopes-with-block', + scope: [ 'transform', 'block' ], + }, + { name: 'no-scope' }, + ]; + const state = createBlockVariationsState( variations ); + const result = getBlockVariations( state, blockName, 'block' ); + expect( result ).toHaveLength( 3 ); + expect( result.map( ( { name } ) => name ) ).toEqual( + expect.arrayContaining( [ + 'only-block', + 'multiple-scopes-with-block', + 'no-scope', + ] ) + ); + } ); + } ); + describe( 'getDefaultBlockVariation', () => { + it( 'should return the default variation when set', () => { + const defaultBlockVariation = { + ...secondBlockVariation, + isDefault: true, + }; + const state = createBlockVariationsState( [ + firstBlockVariation, + defaultBlockVariation, + thirdBlockVariation, + ] ); - it( 'should return the default variation when set', () => { - const defaultBlockVariation = { - ...secondBlockVariation, - isDefault: true, - }; - const state = createBlockVariationsState( [ - firstBlockVariation, - defaultBlockVariation, - thirdBlockVariation, - ] ); - - const result = getDefaultBlockVariation( state, blockName ); + const result = getDefaultBlockVariation( state, blockName ); - expect( result ).toEqual( defaultBlockVariation ); - } ); + expect( result ).toEqual( defaultBlockVariation ); + } ); - it( 'should return the last variation when multiple default variations added', () => { - const defaultBlockVariation = { - ...thirdBlockVariation, - isDefault: true, - }; - const state = createBlockVariationsState( [ - { - ...firstBlockVariation, - isDefault: true, - }, - { - ...secondBlockVariation, + it( 'should return the last variation when multiple default variations added', () => { + const defaultBlockVariation = { + ...thirdBlockVariation, isDefault: true, - }, - defaultBlockVariation, - ] ); + }; + const state = createBlockVariationsState( [ + { + ...firstBlockVariation, + isDefault: true, + }, + { + ...secondBlockVariation, + isDefault: true, + }, + defaultBlockVariation, + ] ); - const result = getDefaultBlockVariation( state, blockName ); + const result = getDefaultBlockVariation( state, blockName ); - expect( result ).toEqual( defaultBlockVariation ); - } ); + expect( result ).toEqual( defaultBlockVariation ); + } ); - it( 'should return the first variation when no default variation set', () => { - const state = createBlockVariationsState( [ - firstBlockVariation, - secondBlockVariation, - thirdBlockVariation, - ] ); + it( 'should return the first variation when no default variation set', () => { + const state = createBlockVariationsState( [ + firstBlockVariation, + secondBlockVariation, + thirdBlockVariation, + ] ); - const result = getDefaultBlockVariation( state, blockName ); + const result = getDefaultBlockVariation( state, blockName ); - expect( result ).toEqual( firstBlockVariation ); + expect( result ).toEqual( firstBlockVariation ); + } ); } ); } ); diff --git a/packages/components/src/button/README.md b/packages/components/src/button/README.md index 899f3ebade6e0..e28a5aca774f2 100644 --- a/packages/components/src/button/README.md +++ b/packages/components/src/button/README.md @@ -239,6 +239,14 @@ If provided with `icon`, sets the icon size. - Required: No - Default: `20 when a Dashicon is rendered, 24 for all other icons.` +#### iconPosition + +If provided with `icon`, sets the position of icon relative to the `text`. Available options are `left|right`. + +- Type: `string` +- Required: No +- Default: `left` + #### showTooltip If provided, renders a [Tooltip](/packages/components/src/tooltip/README.md) component for the button. diff --git a/packages/components/src/button/index.js b/packages/components/src/button/index.js index 32d1b9853a6c2..698c52e453b8a 100644 --- a/packages/components/src/button/index.js +++ b/packages/components/src/button/index.js @@ -34,6 +34,7 @@ export function Button( props, ref ) { className, disabled, icon, + iconPosition = 'left', iconSize, showTooltip, tooltipPosition, @@ -111,8 +112,13 @@ export function Button( props, ref ) { aria-label={ additionalProps[ 'aria-label' ] || label } ref={ ref } > - { icon && } + { icon && iconPosition === 'left' && ( + + ) } { text && <>{ text } } + { icon && iconPosition === 'right' && ( + + ) } { children } ); diff --git a/packages/components/src/menu-items-choice/index.js b/packages/components/src/menu-items-choice/index.js index 299eb2b743f19..617e44161526f 100644 --- a/packages/components/src/menu-items-choice/index.js +++ b/packages/components/src/menu-items-choice/index.js @@ -26,6 +26,7 @@ export default function MenuItemsChoice( { key={ item.value } role="menuitemradio" icon={ isSelected && check } + info={ item.info } isSelected={ isSelected } shortcut={ item.shortcut } className="components-menu-items-choice"