diff --git a/packages/editor/src/hooks/align.js b/packages/editor/src/hooks/align.js index 7b80924c433e2..7b7062ba558f5 100644 --- a/packages/editor/src/hooks/align.js +++ b/packages/editor/src/hooks/align.js @@ -2,20 +2,72 @@ * External dependencies */ import classnames from 'classnames'; -import { assign, get, has, includes } from 'lodash'; +import { assign, get, has, includes, without } from 'lodash'; /** * WordPress dependencies */ -import { createHigherOrderComponent } from '@wordpress/compose'; +import { compose, createHigherOrderComponent } from '@wordpress/compose'; import { addFilter } from '@wordpress/hooks'; -import { hasBlockSupport, getBlockSupport, getBlockType } from '@wordpress/blocks'; +import { getBlockSupport, getBlockType, hasBlockSupport } from '@wordpress/blocks'; +import { withSelect } from '@wordpress/data'; /** * Internal dependencies */ import { BlockControls, BlockAlignmentToolbar } from '../components'; +/** + * An array which includes all possible valid alignments, + * used to validate if an alignment is valid or not. + * + * @constant + * @type {string[]} +*/ +const ALL_ALIGNMENTS = [ 'left', 'center', 'right', 'wide', 'full' ]; + +/** + * An array which includes all wide alignments. + * In order for this alignments to be valid they need to be supported by the block, + * and by the theme. + * + * @constant + * @type {string[]} +*/ +const WIDE_ALIGNMENTS = [ 'wide', 'full' ]; + +/** + * Returns the valid alignments. + * Takes into consideration the aligns supported by a block, if the block supports wide controls or not and if theme supports wide controls or not. + * Exported just for testing purposes, not exported outside the module. + * + * @param {?boolean|string[]} blockAlign Aligns supported by the block. + * @param {?boolean} hasWideBlockSupport True if block supports wide alignments. And False otherwise. + * @param {?boolean} hasWideEnabled True if theme supports wide alignments. And False otherwise. + * + * @return {string[]} Valid alignments. + */ +export function getValidAlignments( blockAlign, hasWideBlockSupport = true, hasWideEnabled = true ) { + let validAlignments; + if ( Array.isArray( blockAlign ) ) { + validAlignments = blockAlign; + } else if ( blockAlign === true ) { + // `true` includes all alignments... + validAlignments = ALL_ALIGNMENTS; + } else { + validAlignments = []; + } + + if ( + ! hasWideEnabled || + ( blockAlign === true && ! hasWideBlockSupport ) + ) { + return without( validAlignments, ...WIDE_ALIGNMENTS ); + } + + return validAlignments; +} + /** * Filters registered block settings, extending attributes to include `align`. * @@ -39,33 +91,6 @@ export function addAttribute( settings ) { return settings; } -/** - * Returns an array of valid alignments for a block type depending on its - * defined supports. Returns an empty array if block does not support align. - * - * @param {string} blockName Block name to check - * @return {string[]} Valid alignments for block - */ -export function getBlockValidAlignments( blockName ) { - // Explicitly defined array set of valid alignments - const blockAlign = getBlockSupport( blockName, 'align' ); - if ( Array.isArray( blockAlign ) ) { - return blockAlign; - } - - const validAlignments = []; - if ( true === blockAlign ) { - // `true` includes all alignments... - validAlignments.push( 'left', 'center', 'right' ); - - if ( hasBlockSupport( blockName, 'alignWide', true ) ) { - validAlignments.push( 'wide', 'full' ); - } - } - - return validAlignments; -} - /** * Override the default edit UI to include new toolbar controls for block * alignment, if block defines support. @@ -73,46 +98,57 @@ export function getBlockValidAlignments( blockName ) { * @param {Function} BlockEdit Original component * @return {Function} Wrapped component */ -export const withToolbarControls = createHigherOrderComponent( ( BlockEdit ) => { - return ( props ) => { - const validAlignments = getBlockValidAlignments( props.name ); - - const updateAlignment = ( nextAlign ) => { - if ( ! nextAlign ) { - const blockType = getBlockType( props.name ); - const blockDefaultAlign = get( blockType, [ 'attributes', 'align', 'default' ] ); - if ( blockDefaultAlign ) { - nextAlign = ''; +export const withToolbarControls = createHigherOrderComponent( + ( BlockEdit ) => ( + ( props ) => { + const { name: blockName } = props; + // Compute valid alignments without taking into account, + // if the theme supports wide alignments or not. + // BlockAlignmentToolbar takes into account the theme support. + const validAlignments = getValidAlignments( + getBlockSupport( blockName, 'align' ), + hasBlockSupport( blockName, 'alignWide', true ), + ); + + const updateAlignment = ( nextAlign ) => { + if ( ! nextAlign ) { + const blockType = getBlockType( props.name ); + const blockDefaultAlign = get( blockType, [ 'attributes', 'align', 'default' ] ); + if ( blockDefaultAlign ) { + nextAlign = ''; + } } - } - props.setAttributes( { align: nextAlign } ); - }; - - return [ - validAlignments.length > 0 && props.isSelected && ( - - - - ), - , - ]; - }; -}, 'withToolbarControls' ); - -/** - * Override the default block element to add alignment wrapper props. - * - * @param {Function} BlockListBlock Original component - * @return {Function} Wrapped component - */ -export const withDataAlign = createHigherOrderComponent( ( BlockListBlock ) => { - return ( props ) => { - const { align } = props.block.attributes; - const validAlignments = getBlockValidAlignments( props.block.name ); + props.setAttributes( { align: nextAlign } ); + }; + + return [ + validAlignments.length > 0 && props.isSelected && ( + + + + ), + , + ]; + } + ), + 'withToolbarControls' +); + +// Exported just for testing purposes, not exported outside the module. +export const insideSelectWithDataAlign = ( BlockListBlock ) => ( + ( props ) => { + const { block, hasWideEnabled } = props; + const { name: blockName } = block; + const { align } = block.attributes; + const validAlignments = getValidAlignments( + getBlockSupport( blockName, 'align' ), + hasBlockSupport( blockName, 'alignWide', true ), + hasWideEnabled + ); let wrapperProps = props.wrapperProps; if ( includes( validAlignments, align ) ) { @@ -120,8 +156,28 @@ export const withDataAlign = createHigherOrderComponent( ( BlockListBlock ) => { } return ; - }; -}, 'withDataAlign' ); + } +); + +/** + * Override the default block element to add alignment wrapper props. + * + * @param {Function} BlockListBlock Original component + * @return {Function} Wrapped component + */ +export const withDataAlign = createHigherOrderComponent( + compose( [ + withSelect( + ( select ) => { + const { getEditorSettings } = select( 'core/editor' ); + return { + hasWideEnabled: !! getEditorSettings().alignWide, + }; + } + ), + insideSelectWithDataAlign, + ] ) +); /** * Override props assigned to save component to inject alignment class name if @@ -134,8 +190,16 @@ export const withDataAlign = createHigherOrderComponent( ( BlockListBlock ) => { */ export function addAssignedAlign( props, blockType, attributes ) { const { align } = attributes; - - if ( includes( getBlockValidAlignments( blockType ), align ) ) { + const blockAlign = getBlockSupport( blockType, 'align' ); + const hasWideBlockSupport = hasBlockSupport( blockType, 'alignWide', true ); + const isAlignValid = includes( + // Compute valid alignments without taking into account, + // if the theme supports wide alignments or not. + // This way changing themes does not impacts the block save. + getValidAlignments( blockAlign, hasWideBlockSupport ), + align + ); + if ( isAlignValid ) { props.className = classnames( `align${ align }`, props.className ); } diff --git a/packages/editor/src/hooks/test/align.js b/packages/editor/src/hooks/test/align.js index ccc678eee8d77..5d3f3dba87c28 100644 --- a/packages/editor/src/hooks/test/align.js +++ b/packages/editor/src/hooks/test/align.js @@ -18,9 +18,9 @@ import { * Internal dependencies */ import { - getBlockValidAlignments, + getValidAlignments, withToolbarControls, - withDataAlign, + insideSelectWithDataAlign, addAssignedAlign, } from '../align'; @@ -58,49 +58,61 @@ describe( 'align', () => { } ); } ); - describe( 'getBlockValidAlignments()', () => { + describe( 'getValidAlignments()', () => { it( 'should return an empty array if block does not define align support', () => { - registerBlockType( 'core/foo', blockSettings ); - const validAlignments = getBlockValidAlignments( 'core/foo' ); + expect( getValidAlignments() ).toEqual( [] ); + } ); - expect( validAlignments ).toEqual( [] ); + it( 'should return all custom aligns set', () => { + expect( + getValidAlignments( [ 'left', 'right' ] ) + ).toEqual( + [ 'left', 'right' ] + ); } ); - it( 'should return all custom align set', () => { - registerBlockType( 'core/foo', { - ...blockSettings, - supports: { - align: [ 'left', 'right' ], - }, - } ); - const validAlignments = getBlockValidAlignments( 'core/foo' ); + it( 'should return all aligns if block defines align support as true', () => { + expect( + getValidAlignments( true ) + ).toEqual( + [ 'left', 'center', 'right', 'wide', 'full' ] + ); + } ); - expect( validAlignments ).toEqual( [ 'left', 'right' ] ); + it( 'should return all aligns except wide if wide align explicitly false on the block', () => { + expect( + getValidAlignments( true, false, true ) + ).toEqual( [ 'left', 'center', 'right' ] ); + + expect( + getValidAlignments( true, false, false ) + ).toEqual( [ 'left', 'center', 'right' ] ); } ); - it( 'should return all aligns if block defines align support', () => { - registerBlockType( 'core/foo', { - ...blockSettings, - supports: { - align: true, - }, - } ); - const validAlignments = getBlockValidAlignments( 'core/foo' ); + it( 'should return all aligns except wide if wide align is not supported by the theme', () => { + expect( + getValidAlignments( true, true, false ) + ).toEqual( [ 'left', 'center', 'right' ] ); - expect( validAlignments ).toEqual( [ 'left', 'center', 'right', 'wide', 'full' ] ); + expect( + getValidAlignments( true, false, false ) + ).toEqual( [ 'left', 'center', 'right' ] ); } ); - it( 'should return all aligns except wide if wide align explicitly false', () => { - registerBlockType( 'core/foo', { - ...blockSettings, - supports: { - align: true, - alignWide: false, - }, - } ); - const validAlignments = getBlockValidAlignments( 'core/foo' ); + it( 'should not remove wide aligns if they are not supported by the block and were set using an array in supports align', () => { + expect( + getValidAlignments( [ 'left', 'right', 'wide', 'full' ], false, true ) + ).toEqual( [ 'left', 'right', 'wide', 'full' ] ); + } ); - expect( validAlignments ).toEqual( [ 'left', 'center', 'right' ] ); + it( 'should remove wide aligns if they are not supported by the theme and were set using an array in supports align', () => { + expect( + getValidAlignments( [ 'left', 'right', 'wide', 'full' ], true, false ) + ).toEqual( [ 'left', 'right' ] ); + + expect( + getValidAlignments( [ 'left', 'right', 'wide', 'full' ], false, false ) + ).toEqual( [ 'left', 'right' ] ); } ); } ); @@ -153,11 +165,11 @@ describe( 'align', () => { ...blockSettings, supports: { align: true, - alignWide: false, + alignWide: true, }, } ); - const EnhancedComponent = withDataAlign( ( { wrapperProps } ) => ( + const EnhancedComponent = insideSelectWithDataAlign( ( { wrapperProps } ) => (
) ); @@ -166,16 +178,43 @@ describe( 'align', () => { block={ { name: 'core/foo', attributes: { - align: 'left', + align: 'wide', }, } } /> ); expect( wrapper.toTree().rendered.props.wrapperProps ).toEqual( { - 'data-align': 'left', + 'data-align': 'wide', } ); } ); + it( 'should not render wide/full wrapper props if wide controls are not enabled', () => { + registerBlockType( 'core/foo', { + ...blockSettings, + supports: { + align: true, + alignWide: true, + }, + } ); + + const EnhancedComponent = insideSelectWithDataAlign( ( { wrapperProps } ) => ( +
+ ) ); + + const wrapper = renderer.create( + + ); + expect( wrapper.toTree().rendered.props.wrapperProps ).toEqual( undefined ); + } ); + it( 'should not render invalid align', () => { registerBlockType( 'core/foo', { ...blockSettings, @@ -185,7 +224,7 @@ describe( 'align', () => { }, } ); - const EnhancedComponent = withDataAlign( ( { wrapperProps } ) => ( + const EnhancedComponent = insideSelectWithDataAlign( ( { wrapperProps } ) => (
) );