From 0442276729b0fbd1f0ab7b8174354cc7887cdfcf Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 7 May 2019 16:25:40 -0400 Subject: [PATCH 01/12] Block Library: Render Column block with ButtonBlockAppender --- packages/block-library/src/column/edit.js | 26 ++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/block-library/src/column/edit.js b/packages/block-library/src/column/edit.js index 147dbc2b4c8b67..01bb95d163e69e 100644 --- a/packages/block-library/src/column/edit.js +++ b/packages/block-library/src/column/edit.js @@ -10,7 +10,11 @@ import { InnerBlocks, BlockControls, BlockVerticalAlignmentToolbar } from '@word import { withDispatch, withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; -const ColumnEdit = ( { attributes, updateAlignment } ) => { +function ColumnEdit( { + attributes, + updateAlignment, + hasChildBlocks, +} ) { const { verticalAlignment } = attributes; const classes = classnames( 'block-core-columns', { @@ -27,17 +31,29 @@ const ColumnEdit = ( { attributes, updateAlignment } ) => { value={ verticalAlignment } /> - + + ) } + /> ); -}; +} export default compose( - withSelect( ( select, { clientId } ) => { - const { getBlockRootClientId } = select( 'core/editor' ); + withSelect( ( select, ownProps ) => { + const { clientId } = ownProps; + const { + getBlockRootClientId, + getBlockOrder, + } = select( 'core/block-editor' ); return { parentColumnsBlockClientId: getBlockRootClientId( clientId ), + hasChildBlocks: getBlockOrder( clientId ).length > 0, }; } ), withDispatch( ( dispatch, { clientId, parentColumnsBlockClientId } ) => { From 67d2b75f282148cbd0229c94960a0343956746b6 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 8 May 2019 08:25:20 -0400 Subject: [PATCH 02/12] Block Library: Add width attribute for resizable Column blocks --- packages/block-library/src/column/block.json | 5 + packages/block-library/src/column/edit.js | 84 +++++++--- packages/block-library/src/column/index.js | 10 ++ packages/block-library/src/column/save.js | 10 +- packages/block-library/src/columns/edit.js | 148 ++++++++++++------ .../block-library/src/columns/editor.scss | 11 -- packages/block-library/src/columns/style.scss | 11 +- 7 files changed, 190 insertions(+), 89 deletions(-) diff --git a/packages/block-library/src/column/block.json b/packages/block-library/src/column/block.json index 9fc52d8e47f909..1d9b3917310344 100644 --- a/packages/block-library/src/column/block.json +++ b/packages/block-library/src/column/block.json @@ -4,6 +4,11 @@ "attributes": { "verticalAlignment": { "type": "string" + }, + "width": { + "type": "number", + "min": 0, + "max": 100 } } } diff --git a/packages/block-library/src/column/edit.js b/packages/block-library/src/column/edit.js index 01bb95d163e69e..76c982ea4db559 100644 --- a/packages/block-library/src/column/edit.js +++ b/packages/block-library/src/column/edit.js @@ -6,31 +6,49 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { InnerBlocks, BlockControls, BlockVerticalAlignmentToolbar } from '@wordpress/block-editor'; +import { + InnerBlocks, + BlockControls, + BlockVerticalAlignmentToolbar, + InspectorControls, +} from '@wordpress/block-editor'; +import { PanelBody, RangeControl } from '@wordpress/components'; import { withDispatch, withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; +import { __ } from '@wordpress/i18n'; function ColumnEdit( { attributes, updateAlignment, + updateWidth, hasChildBlocks, } ) { - const { verticalAlignment } = attributes; + const { verticalAlignment, width } = attributes; const classes = classnames( 'block-core-columns', { [ `is-vertically-aligned-${ verticalAlignment }` ]: verticalAlignment, } ); - const onChange = ( alignment ) => updateAlignment( alignment ); - return (
+ + + + + { const { clientId } = ownProps; - const { - getBlockRootClientId, - getBlockOrder, - } = select( 'core/block-editor' ); + const { getBlockOrder } = select( 'core/block-editor' ); return { - parentColumnsBlockClientId: getBlockRootClientId( clientId ), hasChildBlocks: getBlockOrder( clientId ).length > 0, }; } ), - withDispatch( ( dispatch, { clientId, parentColumnsBlockClientId } ) => { + withDispatch( ( dispatch, ownProps, registry ) => { return { - updateAlignment( alignment ) { - // Update self... - dispatch( 'core/editor' ).updateBlockAttributes( clientId, { - verticalAlignment: alignment, - } ); + updateAlignment( verticalAlignment ) { + const { clientId, setAttributes } = ownProps; + const { updateBlockAttributes } = dispatch( 'core/block-editor' ); + const { getBlockRootClientId } = registry.select( 'core/block-editor' ); + + // Update own alignment. + setAttributes( { verticalAlignment } ); // Reset Parent Columns Block - dispatch( 'core/editor' ).updateBlockAttributes( parentColumnsBlockClientId, { - verticalAlignment: null, - } ); + const rootClientId = getBlockRootClientId( clientId ); + updateBlockAttributes( rootClientId, { verticalAlignment: null } ); + }, + updateWidth( width ) { + const { clientId, attributes, setAttributes } = ownProps; + const { updateBlockAttributes } = dispatch( 'core/block-editor' ); + const { + getBlockRootClientId, + getBlockOrder, + getBlockAttributes, + } = registry.select( 'core/block-editor' ); + + // Update own width. + setAttributes( { width } ); + + // Constrain or expand siblings to account for gain or loss of + // total columns area. + const rootClientId = getBlockRootClientId( clientId ); + const columnClientIds = getBlockOrder( rootClientId ); + const { width: previousWidth = 100 / columnClientIds.length } = attributes; + const index = columnClientIds.indexOf( clientId ); + const isLastColumn = index === columnClientIds.length - 1; + const increment = isLastColumn ? -1 : 1; + const endIndex = isLastColumn ? 0 : columnClientIds.length - 1; + const adjustment = ( previousWidth - width ) / Math.abs( index - endIndex ); + + for ( let i = index + increment; i - increment !== endIndex; i += increment ) { + const columnClientId = columnClientIds[ i ]; + const { width: columnWidth = 100 / columnClientIds.length } = getBlockAttributes( columnClientId ); + updateBlockAttributes( columnClientId, { + width: columnWidth + adjustment, + } ); + } }, }; } ) diff --git a/packages/block-library/src/column/index.js b/packages/block-library/src/column/index.js index 869093459a83bb..250ce0bad65a42 100644 --- a/packages/block-library/src/column/index.js +++ b/packages/block-library/src/column/index.js @@ -25,6 +25,16 @@ export const settings = { reusable: false, html: false, }, + getEditWrapperProps( attributes ) { + const { width } = attributes; + if ( Number.isFinite( width ) ) { + return { + style: { + flexBasis: width + '%', + }, + }; + } + }, edit, save, }; diff --git a/packages/block-library/src/column/save.js b/packages/block-library/src/column/save.js index 9e8ea1ac4a3b32..d0dda9de3174b3 100644 --- a/packages/block-library/src/column/save.js +++ b/packages/block-library/src/column/save.js @@ -9,13 +9,19 @@ import classnames from 'classnames'; import { InnerBlocks } from '@wordpress/block-editor'; export default function save( { attributes } ) { - const { verticalAlignment } = attributes; + const { verticalAlignment, width } = attributes; + const wrapperClasses = classnames( { [ `is-vertically-aligned-${ verticalAlignment }` ]: verticalAlignment, } ); + let style; + if ( Number.isFinite( width ) ) { + style = { flexBasis: width + '%' }; + } + return ( -
+
); diff --git a/packages/block-library/src/columns/edit.js b/packages/block-library/src/columns/edit.js index 15adfff384dfff..21916716b1815f 100644 --- a/packages/block-library/src/columns/edit.js +++ b/packages/block-library/src/columns/edit.js @@ -2,12 +2,12 @@ * External dependencies */ import classnames from 'classnames'; +import { last } from 'lodash'; /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { compose } from '@wordpress/compose'; import { PanelBody, RangeControl, @@ -18,7 +18,8 @@ import { BlockControls, BlockVerticalAlignmentToolbar, } from '@wordpress/block-editor'; -import { withSelect, withDispatch } from '@wordpress/data'; +import { withDispatch } from '@wordpress/data'; +import { createBlock } from '@wordpress/blocks'; /** * Internal dependencies @@ -36,18 +37,18 @@ import { getColumnsTemplate } from './utils'; */ const ALLOWED_BLOCKS = [ 'core/column' ]; -export const ColumnsEdit = function( { attributes, setAttributes, className, updateAlignment } ) { +export function ColumnsEdit( { + attributes, + className, + updateAlignment, + updateColumns, +} ) { const { columns, verticalAlignment } = attributes; const classes = classnames( className, `has-${ columns }-columns`, { [ `are-vertically-aligned-${ verticalAlignment }` ]: verticalAlignment, } ); - const onChange = ( alignment ) => { - // Update all the (immediate) child Column Blocks - updateAlignment( alignment ); - }; - return ( <> @@ -55,11 +56,7 @@ export const ColumnsEdit = function( { attributes, setAttributes, className, upd { - setAttributes( { - columns: nextColumns, - } ); - } } + onChange={ updateColumns } min={ 2 } max={ 6 } /> @@ -67,7 +64,7 @@ export const ColumnsEdit = function( { attributes, setAttributes, className, upd @@ -79,45 +76,92 @@ export const ColumnsEdit = function( { attributes, setAttributes, className, upd
); -}; +} + +export default withDispatch( ( dispatch, ownProps, registry ) => ( { + /** + * Update all child Column blocks with a new vertical alignment setting + * based on whatever alignment is passed in. This allows change to parent + * to overide anything set on a individual column basis. + * + * @param {string} verticalAlignment the vertical alignment setting + */ + updateAlignment( verticalAlignment ) { + const { clientId, setAttributes } = ownProps; + const { updateBlockAttributes } = dispatch( 'core/block-editor' ); + const { getBlockOrder } = registry.select( 'core/block-editor' ); -const DEFAULT_EMPTY_ARRAY = []; + // Update own alignment. + setAttributes( { verticalAlignment } ); + + // Update all child Column Blocks to match + const innerBlockClientIds = getBlockOrder( clientId ); + innerBlockClientIds.forEach( ( innerBlockClientId ) => { + updateBlockAttributes( innerBlockClientId, { + verticalAlignment, + } ); + } ); + }, -export default compose( /** - * Selects the child column Blocks for this parent Column + * Updates the column count, including necessary revisions to child Column + * blocks to grant required or redistribute available space. + * + * @param {number} columns New column count. */ - withSelect( ( select, { clientId } ) => { - const { getBlocksByClientId } = select( 'core/editor' ); - const block = getBlocksByClientId( clientId )[ 0 ]; - - return { - childColumns: block ? block.innerBlocks : DEFAULT_EMPTY_ARRAY, - }; - } ), - withDispatch( ( dispatch, { clientId, childColumns } ) => { - return { - /** - * Update all child column Blocks with a new - * vertical alignment setting based on whatever - * alignment is passed in. This allows change to parent - * to overide anything set on a individual column basis - * - * @param {string} alignment the vertical alignment setting - */ - updateAlignment( alignment ) { - // Update self... - dispatch( 'core/editor' ).updateBlockAttributes( clientId, { - verticalAlignment: alignment, - } ); - - // Update all child Column Blocks to match - childColumns.forEach( ( childColumn ) => { - dispatch( 'core/editor' ).updateBlockAttributes( childColumn.clientId, { - verticalAlignment: alignment, - } ); - } ); - }, - }; - } ), -)( ColumnsEdit ); + updateColumns( columns ) { + const { clientId, setAttributes, attributes } = ownProps; + const { replaceInnerBlocks } = dispatch( 'core/block-editor' ); + const { getBlocks } = registry.select( 'core/block-editor' ); + + // Update columns count. + setAttributes( { columns } ); + + let innerBlocks = getBlocks( clientId ); + const hasExplicitColumnWidths = innerBlocks.some( ( innerBlock ) => ( + innerBlock.attributes.width !== undefined + ) ); + + let newOrRemovedColumnWidth; + if ( ! hasExplicitColumnWidths ) { + return; + } + + // Redistribute available width for existing inner blocks. + const { columns: previousColumns } = attributes; + const isAddingColumn = columns > previousColumns; + + if ( isAddingColumn ) { + // If adding a new column, assign width to the new column equal to + // as if it were `1 / columns` of the total available space. + newOrRemovedColumnWidth = ( 100 / columns ); + } else { + // The removed column will be the last of the inner blocks. + newOrRemovedColumnWidth = last( innerBlocks ).attributes.width || ( 100 / previousColumns ); + } + + const adjustment = newOrRemovedColumnWidth / ( isAddingColumn ? -1 * previousColumns : columns ); + innerBlocks = innerBlocks.map( ( innerBlock ) => { + const { width: columnWidth = ( 100 / previousColumns ) } = innerBlock.attributes; + return { + ...innerBlock, + attributes: { + ...innerBlocks.attributes, + width: parseFloat( ( columnWidth + adjustment ).toFixed( 2 ) ), + }, + }; + } ); + + // Explicitly manage the new column block, since template would not + // account for the explicitly assigned width. + if ( isAddingColumn ) { + const block = createBlock( 'core/column', { + width: parseFloat( newOrRemovedColumnWidth.toFixed( 2 ) ), + } ); + + innerBlocks = [ ...innerBlocks, block ]; + } + + replaceInnerBlocks( clientId, innerBlocks, false ); + }, +} ) )( ColumnsEdit ); diff --git a/packages/block-library/src/columns/editor.scss b/packages/block-library/src/columns/editor.scss index 7830f7780880a9..c771c79f5f9d3c 100644 --- a/packages/block-library/src/columns/editor.scss +++ b/packages/block-library/src/columns/editor.scss @@ -178,14 +178,3 @@ div.block-core-columns.is-vertically-aligned-bottom { display: none; } } - -// In absence of making the individual columns resizable, we prevent them from being clickable. -// This makes them less fiddly. @todo: This should be revisited as the interface is refined. -.wp-block-columns [data-type="core/column"] { - pointer-events: none; - - // This selector re-enables clicking on any child of a column block. - .block-core-columns .block-editor-block-list__layout { - pointer-events: all; - } -} diff --git a/packages/block-library/src/columns/style.scss b/packages/block-library/src/columns/style.scss index ab2ec375ca1e8e..893619d65808c3 100644 --- a/packages/block-library/src/columns/style.scss +++ b/packages/block-library/src/columns/style.scss @@ -14,8 +14,11 @@ margin-bottom: 1em; flex-grow: 1; - // Responsiveness: Show at most one columns on mobile. - flex-basis: 100%; + @media (max-width: #{ ($break-medium) }) { + // Responsiveness: Show at most one columns on mobile. This must be + // important since the Column assigns its own width as an inline style. + flex-basis: 100% !important; + } // Prevent the columns from growing wider than their distributed sizes. min-width: 0; @@ -26,11 +29,9 @@ @include break-small() { - // Beyond mobile, allow 2 columns. - flex-basis: calc(50% - #{$grid-size-large}); flex-grow: 0; - // Add space between the 2 columns. Themes can customize this if they wish to work differently. + // Add space between the multiple columns. Themes can customize this if they wish to work differently. // Only apply this beyond the mobile breakpoint, as there's only a single column on mobile. &:nth-child(even) { margin-left: $grid-size-large * 2; From 609e7872d4e95c8af4552cd821f9b968f231078f Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 9 May 2019 14:04:29 -0400 Subject: [PATCH 03/12] Block Library: Update Column width input label to clarify percentage --- packages/block-library/src/column/edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/column/edit.js b/packages/block-library/src/column/edit.js index 76c982ea4db559..ab38ff506aed47 100644 --- a/packages/block-library/src/column/edit.js +++ b/packages/block-library/src/column/edit.js @@ -40,7 +40,7 @@ function ColumnEdit( { Date: Thu, 9 May 2019 17:29:22 -0400 Subject: [PATCH 04/12] Block Library: Refactor Columns width redistribution to reusable utilities --- packages/block-library/src/column/edit.js | 62 +++-- packages/block-library/src/columns/edit.js | 55 ++--- .../block-library/src/columns/test/utils.js | 224 ++++++++++++++++++ packages/block-library/src/columns/utils.js | 124 +++++++++- 4 files changed, 410 insertions(+), 55 deletions(-) create mode 100644 packages/block-library/src/columns/test/utils.js diff --git a/packages/block-library/src/column/edit.js b/packages/block-library/src/column/edit.js index ab38ff506aed47..b1d17fbddac3ca 100644 --- a/packages/block-library/src/column/edit.js +++ b/packages/block-library/src/column/edit.js @@ -2,6 +2,7 @@ * External dependencies */ import classnames from 'classnames'; +import { forEach, find, difference } from 'lodash'; /** * WordPress dependencies @@ -17,6 +18,17 @@ import { withDispatch, withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import { + toWidthPrecision, + getTotalColumnsWidth, + getColumnWidths, + getAdjacentBlocks, + getRedistributedColumnWidths, +} from '../columns/utils'; + function ColumnEdit( { attributes, updateAlignment, @@ -85,35 +97,37 @@ export default compose( updateBlockAttributes( rootClientId, { verticalAlignment: null } ); }, updateWidth( width ) { - const { clientId, attributes, setAttributes } = ownProps; + const { clientId } = ownProps; const { updateBlockAttributes } = dispatch( 'core/block-editor' ); - const { - getBlockRootClientId, - getBlockOrder, - getBlockAttributes, - } = registry.select( 'core/block-editor' ); - - // Update own width. - setAttributes( { width } ); + const { getBlockRootClientId, getBlocks } = registry.select( 'core/block-editor' ); // Constrain or expand siblings to account for gain or loss of // total columns area. - const rootClientId = getBlockRootClientId( clientId ); - const columnClientIds = getBlockOrder( rootClientId ); - const { width: previousWidth = 100 / columnClientIds.length } = attributes; - const index = columnClientIds.indexOf( clientId ); - const isLastColumn = index === columnClientIds.length - 1; - const increment = isLastColumn ? -1 : 1; - const endIndex = isLastColumn ? 0 : columnClientIds.length - 1; - const adjustment = ( previousWidth - width ) / Math.abs( index - endIndex ); + const columns = getBlocks( getBlockRootClientId( clientId ) ); + const adjacentColumns = getAdjacentBlocks( columns, clientId ); + + // The occupied width is calculated as the sum of the new width + // and the total width of blocks _not_ in the adjacent set. + const occupiedWidth = width + getTotalColumnsWidth( + difference( columns, [ + find( columns, { clientId } ), + ...adjacentColumns, + ] ) + ); + + // Compute _all_ next column widths, in case the updated column + // is in the middle of a set of columns which don't yet have + // any explicit widths assigned (include updates to those not + // part of the adjacent blocks). + const nextColumnWidths = { + ...getColumnWidths( columns, columns.length ), + [ clientId ]: toWidthPrecision( width ), + ...getRedistributedColumnWidths( adjacentColumns, 100 - occupiedWidth, columns.length ), + }; - for ( let i = index + increment; i - increment !== endIndex; i += increment ) { - const columnClientId = columnClientIds[ i ]; - const { width: columnWidth = 100 / columnClientIds.length } = getBlockAttributes( columnClientId ); - updateBlockAttributes( columnClientId, { - width: columnWidth + adjustment, - } ); - } + forEach( nextColumnWidths, ( nextColumnWidth, columnClientId ) => { + updateBlockAttributes( columnClientId, { width: nextColumnWidth } ); + } ); }, }; } ) diff --git a/packages/block-library/src/columns/edit.js b/packages/block-library/src/columns/edit.js index 21916716b1815f..71625625233815 100644 --- a/packages/block-library/src/columns/edit.js +++ b/packages/block-library/src/columns/edit.js @@ -2,7 +2,7 @@ * External dependencies */ import classnames from 'classnames'; -import { last } from 'lodash'; +import { dropRight } from 'lodash'; /** * WordPress dependencies @@ -24,7 +24,13 @@ import { createBlock } from '@wordpress/blocks'; /** * Internal dependencies */ -import { getColumnsTemplate } from './utils'; +import { + getColumnsTemplate, + hasExplicitColumnWidths, + getMappedColumnWidths, + getRedistributedColumnWidths, + toWidthPrecision, +} from './utils'; /** * Allowed blocks constant is passed to InnerBlocks precisely as specified here. @@ -118,12 +124,7 @@ export default withDispatch( ( dispatch, ownProps, registry ) => ( { setAttributes( { columns } ); let innerBlocks = getBlocks( clientId ); - const hasExplicitColumnWidths = innerBlocks.some( ( innerBlock ) => ( - innerBlock.attributes.width !== undefined - ) ); - - let newOrRemovedColumnWidth; - if ( ! hasExplicitColumnWidths ) { + if ( ! hasExplicitColumnWidths( innerBlocks ) ) { return; } @@ -134,32 +135,26 @@ export default withDispatch( ( dispatch, ownProps, registry ) => ( { if ( isAddingColumn ) { // If adding a new column, assign width to the new column equal to // as if it were `1 / columns` of the total available space. - newOrRemovedColumnWidth = ( 100 / columns ); + const newColumnWidth = toWidthPrecision( 100 / columns ); + + // Redistribute in consideration of pending block insertion as + // constraining the available working width. + const widths = getRedistributedColumnWidths( innerBlocks, 100 - newColumnWidth ); + + innerBlocks = [ + ...getMappedColumnWidths( innerBlocks, widths ), + createBlock( 'core/column', { + width: newColumnWidth, + } ), + ]; } else { // The removed column will be the last of the inner blocks. - newOrRemovedColumnWidth = last( innerBlocks ).attributes.width || ( 100 / previousColumns ); - } + innerBlocks = dropRight( innerBlocks ); - const adjustment = newOrRemovedColumnWidth / ( isAddingColumn ? -1 * previousColumns : columns ); - innerBlocks = innerBlocks.map( ( innerBlock ) => { - const { width: columnWidth = ( 100 / previousColumns ) } = innerBlock.attributes; - return { - ...innerBlock, - attributes: { - ...innerBlocks.attributes, - width: parseFloat( ( columnWidth + adjustment ).toFixed( 2 ) ), - }, - }; - } ); - - // Explicitly manage the new column block, since template would not - // account for the explicitly assigned width. - if ( isAddingColumn ) { - const block = createBlock( 'core/column', { - width: parseFloat( newOrRemovedColumnWidth.toFixed( 2 ) ), - } ); + // Redistribute as if block is already removed. + const widths = getRedistributedColumnWidths( innerBlocks, 100 ); - innerBlocks = [ ...innerBlocks, block ]; + innerBlocks = getMappedColumnWidths( innerBlocks, widths ); } replaceInnerBlocks( clientId, innerBlocks, false ); diff --git a/packages/block-library/src/columns/test/utils.js b/packages/block-library/src/columns/test/utils.js new file mode 100644 index 00000000000000..b264829e3987db --- /dev/null +++ b/packages/block-library/src/columns/test/utils.js @@ -0,0 +1,224 @@ +/** + * Internal dependencies + */ +import { + getColumnsTemplate, + toWidthPrecision, + getAdjacentBlocks, + getEffectiveColumnWidth, + getTotalColumnsWidth, + getColumnWidths, + getRedistributedColumnWidths, + hasExplicitColumnWidths, + getMappedColumnWidths, +} from '../utils'; + +describe( 'getColumnsTemplate', () => { + it( 'should return a template corresponding to columns count', () => { + const template = getColumnsTemplate( 4 ); + + expect( template ).toEqual( [ + [ 'core/column' ], + [ 'core/column' ], + [ 'core/column' ], + [ 'core/column' ], + ] ); + } ); +} ); + +describe( 'toWidthPrecision', () => { + it( 'should round value to standard precision', () => { + const value = toWidthPrecision( 50.108 ); + + expect( value ).toBe( 50.11 ); + } ); +} ); + +describe( 'getAdjacentBlocks', () => { + const blockA = { clientId: 'a' }; + const blockB = { clientId: 'b' }; + const blockC = { clientId: 'c' }; + const blocks = [ blockA, blockB, blockC ]; + + it( 'should return blocks after clientId', () => { + const result = getAdjacentBlocks( blocks, 'b' ); + + expect( result ).toEqual( [ blockC ] ); + } ); + + it( 'should return blocks before clientId if clientId is last', () => { + const result = getAdjacentBlocks( blocks, 'c' ); + + expect( result ).toEqual( [ blockA, blockB ] ); + } ); +} ); + +describe( 'getEffectiveColumnWidth', () => { + it( 'should return attribute value if set, rounded to precision', () => { + const block = { attributes: { width: 50.108 } }; + + const width = getEffectiveColumnWidth( block, 3 ); + + expect( width ).toBe( 50.11 ); + } ); + + it( 'should return assumed width if attribute value not set, rounded to precision', () => { + const block = { attributes: {} }; + + const width = getEffectiveColumnWidth( block, 3 ); + + expect( width ).toBe( 33.33 ); + } ); +} ); + +describe( 'getTotalColumnsWidth', () => { + describe( 'explicit width', () => { + const blocks = [ + { clientId: 'a', attributes: { width: 30 } }, + { clientId: 'b', attributes: { width: 40 } }, + ]; + + it( 'returns the sum total of columns width', () => { + const width = getTotalColumnsWidth( blocks ); + + expect( width ).toBe( 70 ); + } ); + } ); + + describe( 'implicit width', () => { + const blocks = [ + { clientId: 'a', attributes: {} }, + { clientId: 'b', attributes: {} }, + ]; + + it( 'returns the sum total of columns width', () => { + const widths = getTotalColumnsWidth( blocks ); + + expect( widths ).toBe( 100 ); + } ); + } ); +} ); + +describe( 'getColumnWidths', () => { + describe( 'explicit width', () => { + const blocks = [ + { clientId: 'a', attributes: { width: 30.459 } }, + { clientId: 'b', attributes: { width: 29.543 } }, + ]; + + it( 'returns the column widths', () => { + const widths = getColumnWidths( blocks ); + + expect( widths ).toEqual( { + a: 30.46, + b: 29.54, + } ); + } ); + } ); + + describe( 'implicit width', () => { + const blocks = [ + { clientId: 'a', attributes: {} }, + { clientId: 'b', attributes: {} }, + ]; + + it( 'returns the column widths', () => { + const widths = getColumnWidths( blocks ); + + expect( widths ).toEqual( { + a: 50, + b: 50, + } ); + } ); + } ); +} ); + +describe( 'getRedistributedColumnWidths', () => { + describe( 'explicit width', () => { + const blocks = [ + { clientId: 'a', attributes: { width: 30 } }, + { clientId: 'b', attributes: { width: 40 } }, + ]; + + it( 'should constrain to fit available width', () => { + const widths = getRedistributedColumnWidths( blocks, 60 ); + + expect( widths ).toEqual( { + a: 25, + b: 35, + } ); + } ); + + it( 'should expand to fit available width', () => { + const widths = getRedistributedColumnWidths( blocks, 80 ); + + expect( widths ).toEqual( { + a: 35, + b: 45, + } ); + } ); + } ); + + describe( 'implicit width', () => { + const blocks = [ + { clientId: 'a', attributes: {} }, + { clientId: 'b', attributes: {} }, + ]; + + it( 'should equally distribute to available width', () => { + const widths = getRedistributedColumnWidths( blocks, 60 ); + + expect( widths ).toEqual( { + a: 30, + b: 30, + } ); + } ); + + it( 'should constrain to fit available width', () => { + const widths = getRedistributedColumnWidths( blocks, 66.66, 3 ); + + expect( widths ).toEqual( { + a: 33.33, + b: 33.33, + } ); + } ); + } ); +} ); + +describe( 'hasExplicitColumnWidths', () => { + it( 'returns false if no blocks have explicit width', () => { + const blocks = [ { attributes: {} } ]; + + const result = hasExplicitColumnWidths( blocks ); + + expect( result ).toBe( false ); + } ); + + it( 'returns true if a block has explicit width', () => { + const blocks = [ { attributes: { width: 10 } } ]; + + const result = hasExplicitColumnWidths( blocks ); + + expect( result ).toBe( true ); + } ); +} ); + +describe( 'getMappedColumnWidths', () => { + it( 'merges to block attributes using provided widths', () => { + const blocks = [ + { clientId: 'a', attributes: { width: 30 } }, + { clientId: 'b', attributes: { width: 40 } }, + ]; + const widths = { + a: 25, + b: 35, + }; + + const result = getMappedColumnWidths( blocks, widths ); + + expect( result ).toEqual( [ + { clientId: 'a', attributes: { width: 25 } }, + { clientId: 'b', attributes: { width: 35 } }, + ] ); + } ); +} ); diff --git a/packages/block-library/src/columns/utils.js b/packages/block-library/src/columns/utils.js index e7e3f90df70fd2..5da0b3e282c33e 100644 --- a/packages/block-library/src/columns/utils.js +++ b/packages/block-library/src/columns/utils.js @@ -2,7 +2,7 @@ * External dependencies */ import memoize from 'memize'; -import { times } from 'lodash'; +import { times, findIndex, sumBy, merge, mapValues } from 'lodash'; /** * Returns the layouts configuration for a given number of columns. @@ -14,3 +14,125 @@ import { times } from 'lodash'; export const getColumnsTemplate = memoize( ( columns ) => { return times( columns, () => [ 'core/column' ] ); } ); + +/** + * Returns a column width attribute value rounded to standard precision. + * + * @param {number} value Raw value. + * + * @return {number} Value rounded to standard precision. + */ +export const toWidthPrecision = ( value ) => parseFloat( value.toFixed( 2 ) ); + +/** + * Returns the considered adjacent to that of the specified `clientId` for + * resizing consideration. Adjacent blocks are those occurring after, except + * when the given block is the last block in the set. For the last block, the + * behavior is reversed. + * + * @param {WPBlock[]} blocks Block objects. + * @param {string} clientId Client ID to consider for adjacent blocks. + * + * @return {WPBlock[]} Adjacent block objects. + */ +export function getAdjacentBlocks( blocks, clientId ) { + const index = findIndex( blocks, { clientId } ); + const isLastBlock = index === blocks.length - 1; + + return isLastBlock ? blocks.slice( 0, index ) : blocks.slice( index + 1 ); +} + +/** + * Returns an effective width for a given block. An effective width is equal to + * its attribute value if set, or a computed value assuming equal distribution. + * + * @param {WPBlock} block Block object. + * @param {number} totalBlockCount Total number of blocks in Columns. + * + * @return {number} Effective column width. + */ +export function getEffectiveColumnWidth( block, totalBlockCount ) { + const { width = 100 / totalBlockCount } = block.attributes; + return toWidthPrecision( width ); +} + +/** + * Returns the total width occupied by the given set of column blocks. + * + * @param {WPBlock[]} blocks Block objects. + * @param {?number} totalBlockCount Total number of blocks in Columns. + * Defaults to number of blocks passed. + * + * @return {number} Total width occupied by blocks. + */ +export function getTotalColumnsWidth( blocks, totalBlockCount = blocks.length ) { + return sumBy( blocks, ( block ) => getEffectiveColumnWidth( block, totalBlockCount ) ); +} + +/** + * Returns an object of `clientId` → `width` of effective column widths. + * + * @param {WPBlock[]} blocks Block objects. + * @param {?number} totalBlockCount Total number of blocks in Columns. + * Defaults to number of blocks passed. + * + * @return {Object} Column widths. + */ +export function getColumnWidths( blocks, totalBlockCount = blocks.length ) { + return blocks.reduce( ( result, block ) => { + const width = getEffectiveColumnWidth( block, totalBlockCount ); + return Object.assign( result, { [ block.clientId ]: width } ); + }, {} ); +} + +/** + * Returns an object of `clientId` → `width` of column widths as redistributed + * proportional to their current widths, constrained or expanded to fit within + * the given available width. + * + * @param {WPBlock[]} blocks Block objects. + * @param {number} availableWidth Maximum width to fit within. + * @param {?number} totalBlockCount Total number of blocks in Columns. + * Defaults to number of blocks passed. + * + * @return {Object} Redistributed column widths. + */ +export function getRedistributedColumnWidths( blocks, availableWidth, totalBlockCount = blocks.length ) { + const totalWidth = getTotalColumnsWidth( blocks, totalBlockCount ); + const difference = availableWidth - totalWidth; + const adjustment = difference / blocks.length; + + return mapValues( + getColumnWidths( blocks, totalBlockCount ), + ( width ) => toWidthPrecision( width + adjustment ), + ); +} + +/** + * Returns true if column blocks within the provided set are assigned with + * explicit widths, or false otherwise. + * + * @param {WPBlock[]} blocks Block objects. + * + * @return {boolean} Whether columns have explicit widths. + */ +export function hasExplicitColumnWidths( blocks ) { + return blocks.some( ( block ) => Number.isFinite( block.attributes.width ) ); +} + +/** + * Returns a copy of the given set of blocks with new widths assigned from the + * provided object of redistributed column widths. + * + * @param {WPBlock[]} blocks Block objects. + * @param {Object} widths Redistributed column widths. + * + * @return {WPBlock[]} blocks Mapped block objects. + */ +export function getMappedColumnWidths( blocks, widths ) { + return blocks.map( ( block ) => merge( {}, block, { + attributes: { + width: widths[ block.clientId ], + }, + } ) ); +} From 5bb641cb1881f3d7bcea6513714166c3d570a727 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 9 May 2019 17:51:22 -0400 Subject: [PATCH 05/12] Plugin: Filter safe CSS to allow column flex-basis CSS attribute --- lib/compat.php | 26 ++++++++++++++++++++++++++ lib/load.php | 1 + 2 files changed, 27 insertions(+) create mode 100644 lib/compat.php diff --git a/lib/compat.php b/lib/compat.php new file mode 100644 index 00000000000000..fb09a6e49ef97f --- /dev/null +++ b/lib/compat.php @@ -0,0 +1,26 @@ + Date: Thu, 9 May 2019 19:16:27 -0400 Subject: [PATCH 06/12] Block Library: Return undefined for non-finite column width precision --- packages/block-library/src/columns/test/utils.js | 5 +++++ packages/block-library/src/columns/utils.js | 8 ++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/columns/test/utils.js b/packages/block-library/src/columns/test/utils.js index b264829e3987db..cb69e1740e1f32 100644 --- a/packages/block-library/src/columns/test/utils.js +++ b/packages/block-library/src/columns/test/utils.js @@ -32,6 +32,11 @@ describe( 'toWidthPrecision', () => { expect( value ).toBe( 50.11 ); } ); + + it( 'should return undefined for invalid number', () => { + expect( toWidthPrecision( null ) ).toBe( undefined ); + expect( toWidthPrecision( undefined ) ).toBe( undefined ); + } ); } ); describe( 'getAdjacentBlocks', () => { diff --git a/packages/block-library/src/columns/utils.js b/packages/block-library/src/columns/utils.js index 5da0b3e282c33e..0c4e4c59e9ccd3 100644 --- a/packages/block-library/src/columns/utils.js +++ b/packages/block-library/src/columns/utils.js @@ -17,12 +17,16 @@ export const getColumnsTemplate = memoize( ( columns ) => { /** * Returns a column width attribute value rounded to standard precision. + * Returns `undefined` if the value is not a valid finite number. * - * @param {number} value Raw value. + * @param {?number} value Raw value. * * @return {number} Value rounded to standard precision. */ -export const toWidthPrecision = ( value ) => parseFloat( value.toFixed( 2 ) ); +export const toWidthPrecision = ( value ) => + Number.isFinite( value ) ? + parseFloat( value.toFixed( 2 ) ) : + undefined; /** * Returns the considered adjacent to that of the specified `clientId` for From 5836ad0b8eadc43b1ac92339dc0f53373138bc97 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 9 May 2019 19:17:14 -0400 Subject: [PATCH 07/12] Components: Fix display of RangeControl reset button --- packages/components/CHANGELOG.md | 8 +++++++- packages/components/src/range-control/index.js | 12 +++++++++--- packages/components/src/range-control/style.scss | 4 ++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 97028d2638a19d..abde63ef28198d 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,7 +1,13 @@ -## 7.4.0 (Unreleased) +## Master + +### New Features - Added a new `HorizontalRule` component. +### Bug Fixes + +- Fixed display of reset button when using RangeControl `allowReset` prop. + ## 7.3.0 (2019-04-16) ### New Features diff --git a/packages/components/src/range-control/index.js b/packages/components/src/range-control/index.js index 2fd28d31bcae13..3b33f2b499c34b 100644 --- a/packages/components/src/range-control/index.js +++ b/packages/components/src/range-control/index.js @@ -98,11 +98,17 @@ function RangeControl( { onBlur={ resetCurrentInput } { ...props } /> - { allowReset && - - } + ) } ); } diff --git a/packages/components/src/range-control/style.scss b/packages/components/src/range-control/style.scss index e3dfca023ca837..b67e503592650e 100644 --- a/packages/components/src/range-control/style.scss +++ b/packages/components/src/range-control/style.scss @@ -22,6 +22,10 @@ } } +.components-range-control__reset { + margin-left: $grid-size; +} + // creating mixin because we can't do multiline variables, and we can't comma-group the selectors for styling the range slider @mixin range-thumb() { height: 18px; From 9f28c18c28b31f5a7aea2b61eb71c580bcc5fa75 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 9 May 2019 19:17:43 -0400 Subject: [PATCH 08/12] Block Library: Allow column block width resetting --- packages/block-library/src/column/edit.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/block-library/src/column/edit.js b/packages/block-library/src/column/edit.js index b1d17fbddac3ca..65fb0ab6436086 100644 --- a/packages/block-library/src/column/edit.js +++ b/packages/block-library/src/column/edit.js @@ -58,6 +58,7 @@ function ColumnEdit( { min={ 0 } max={ 100 } required + allowReset /> From 9334a4a2c0345cf7d3043b03cf8be979ebf0263e Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 9 May 2019 19:18:58 -0400 Subject: [PATCH 09/12] Block Library: Ensure Column width RangeControl treated as controlled input --- packages/block-library/src/column/edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/column/edit.js b/packages/block-library/src/column/edit.js index 65fb0ab6436086..2587229664bfe5 100644 --- a/packages/block-library/src/column/edit.js +++ b/packages/block-library/src/column/edit.js @@ -53,7 +53,7 @@ function ColumnEdit( { Date: Fri, 10 May 2019 08:15:55 -0400 Subject: [PATCH 10/12] Testing: Update E2E tests for button inserter columns block --- .../specs/block-hierarchy-navigation.test.js | 22 +++++++++++++++++-- packages/e2e-tests/specs/writing-flow.test.js | 11 +++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/packages/e2e-tests/specs/block-hierarchy-navigation.test.js b/packages/e2e-tests/specs/block-hierarchy-navigation.test.js index 9153526a262a8f..77d424d7e6381c 100644 --- a/packages/e2e-tests/specs/block-hierarchy-navigation.test.js +++ b/packages/e2e-tests/specs/block-hierarchy-navigation.test.js @@ -22,6 +22,11 @@ describe( 'Navigating the block hierarchy', () => { await insertBlock( 'Columns' ); // Add a paragraph in the first column. + await pressKeyTimes( 'Tab', 5 ); // Tab to inserter. + await page.keyboard.press( 'Enter' ); // Activate inserter. + await page.keyboard.type( 'Paragraph' ); + await pressKeyTimes( 'Tab', 3 ); // Tab to paragraph result. + await page.keyboard.press( 'Enter' ); // Insert paragraph. await page.keyboard.type( 'First column' ); // Navigate to the columns blocks. @@ -44,7 +49,11 @@ describe( 'Navigating the block hierarchy', () => { await lastColumnsBlockMenuItem.click(); // Insert text in the last column block. - await pressKeyTimes( 'Tab', 5 ); // Navigate to the appender. + await pressKeyTimes( 'Tab', 5 ); // Tab to inserter. + await page.keyboard.press( 'Enter' ); // Activate inserter. + await page.keyboard.type( 'Paragraph' ); + await pressKeyTimes( 'Tab', 3 ); // Tab to paragraph result. + await page.keyboard.press( 'Enter' ); // Insert paragraph. await page.keyboard.type( 'Third column' ); expect( await getEditedPostContent() ).toMatchSnapshot(); @@ -54,6 +63,11 @@ describe( 'Navigating the block hierarchy', () => { await insertBlock( 'Columns' ); // Add a paragraph in the first column. + await pressKeyTimes( 'Tab', 5 ); // Tab to inserter. + await page.keyboard.press( 'Enter' ); // Activate inserter. + await page.keyboard.type( 'Paragraph' ); + await pressKeyTimes( 'Tab', 3 ); // Tab to paragraph result. + await page.keyboard.press( 'Enter' ); // Insert paragraph. await page.keyboard.type( 'First column' ); // Navigate to the columns blocks using the keyboard. @@ -76,7 +90,11 @@ describe( 'Navigating the block hierarchy', () => { await page.keyboard.press( 'Enter' ); // Insert text in the last column block - await pressKeyTimes( 'Tab', 5 ); // Navigate to the appender. + await pressKeyTimes( 'Tab', 5 ); // Tab to inserter. + await page.keyboard.press( 'Enter' ); // Activate inserter. + await page.keyboard.type( 'Paragraph' ); + await pressKeyTimes( 'Tab', 3 ); // Tab to paragraph result. + await page.keyboard.press( 'Enter' ); // Insert paragraph. await page.keyboard.type( 'Third column' ); expect( await getEditedPostContent() ).toMatchSnapshot(); diff --git a/packages/e2e-tests/specs/writing-flow.test.js b/packages/e2e-tests/specs/writing-flow.test.js index c9ca7bf2021df7..6555ef5a8f300b 100644 --- a/packages/e2e-tests/specs/writing-flow.test.js +++ b/packages/e2e-tests/specs/writing-flow.test.js @@ -24,13 +24,22 @@ describe( 'adding blocks', () => { await page.keyboard.press( 'Enter' ); await page.keyboard.type( '/columns' ); await page.keyboard.press( 'Enter' ); + await pressKeyTimes( 'Tab', 5 ); // Tab to inserter. + await page.keyboard.press( 'Enter' ); // Activate inserter. + await page.keyboard.type( 'Paragraph' ); + await pressKeyTimes( 'Tab', 3 ); // Tab to paragraph result. + await page.keyboard.press( 'Enter' ); // Insert paragraph. await page.keyboard.type( 'First col' ); // Arrow down should navigate through layouts in columns block (to // its default appender). Two key presses are required since the first // will land user on the Column wrapper block. await page.keyboard.press( 'ArrowDown' ); - await page.keyboard.press( 'ArrowDown' ); + await pressKeyTimes( 'Tab', 5 ); // Tab to inserter. + await page.keyboard.press( 'Enter' ); // Activate inserter. + await page.keyboard.type( 'Paragraph' ); + await pressKeyTimes( 'Tab', 3 ); // Tab to paragraph result. + await page.keyboard.press( 'Enter' ); // Insert paragraph. await page.keyboard.type( 'Second col' ); // Arrow down from last of layouts exits nested context to default From a840fd98239973c7dc665f3600f8b5259a5d9688 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 10 May 2019 15:35:33 -0400 Subject: [PATCH 11/12] Testing: Try to appease the E2E gods --- packages/e2e-tests/specs/writing-flow.test.js | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/e2e-tests/specs/writing-flow.test.js b/packages/e2e-tests/specs/writing-flow.test.js index 6555ef5a8f300b..fe801a3617284d 100644 --- a/packages/e2e-tests/specs/writing-flow.test.js +++ b/packages/e2e-tests/specs/writing-flow.test.js @@ -16,6 +16,10 @@ describe( 'adding blocks', () => { } ); it( 'Should navigate inner blocks with arrow keys', async () => { + // TODO: The `waitForSelector` calls in this function should ultimately + // not be necessary for interactions, and exist as a stop-gap solution + // where rendering delays in slower CPU can cause intermittent failure. + let activeElementText; // Add demo content @@ -24,19 +28,19 @@ describe( 'adding blocks', () => { await page.keyboard.press( 'Enter' ); await page.keyboard.type( '/columns' ); await page.keyboard.press( 'Enter' ); - await pressKeyTimes( 'Tab', 5 ); // Tab to inserter. - await page.keyboard.press( 'Enter' ); // Activate inserter. + await page.click( ':focus .block-editor-button-block-appender' ); + await page.waitForSelector( ':focus.block-editor-inserter__search' ); await page.keyboard.type( 'Paragraph' ); await pressKeyTimes( 'Tab', 3 ); // Tab to paragraph result. await page.keyboard.press( 'Enter' ); // Insert paragraph. await page.keyboard.type( 'First col' ); - // Arrow down should navigate through layouts in columns block (to - // its default appender). Two key presses are required since the first - // will land user on the Column wrapper block. - await page.keyboard.press( 'ArrowDown' ); - await pressKeyTimes( 'Tab', 5 ); // Tab to inserter. - await page.keyboard.press( 'Enter' ); // Activate inserter. + // TODO: ArrowDown should traverse into the second column. In slower + // CPUs, it can sometimes remain in the first column paragraph. This + // is a temporary solution. + await page.focus( '.wp-block[data-type="core/column"]:nth-child(2)' ); + await page.click( ':focus .block-editor-button-block-appender' ); + await page.waitForSelector( ':focus.block-editor-inserter__search' ); await page.keyboard.type( 'Paragraph' ); await pressKeyTimes( 'Tab', 3 ); // Tab to paragraph result. await page.keyboard.press( 'Enter' ); // Insert paragraph. From 1a9fa24c6fd50d8164ed2ea7929508576087361c Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 10 May 2019 17:11:09 -0400 Subject: [PATCH 12/12] Block Library: Restore flex-basis for mid-range viewports --- packages/block-library/src/columns/style.scss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/columns/style.scss b/packages/block-library/src/columns/style.scss index 893619d65808c3..7a2c0877889332 100644 --- a/packages/block-library/src/columns/style.scss +++ b/packages/block-library/src/columns/style.scss @@ -14,7 +14,7 @@ margin-bottom: 1em; flex-grow: 1; - @media (max-width: #{ ($break-medium) }) { + @media (max-width: #{ ($break-small - 1) }) { // Responsiveness: Show at most one columns on mobile. This must be // important since the Column assigns its own width as an inline style. flex-basis: 100% !important; @@ -29,6 +29,8 @@ @include break-small() { + // Beyond mobile, allow 2 columns. + flex-basis: calc(50% - #{$grid-size-large}); flex-grow: 0; // Add space between the multiple columns. Themes can customize this if they wish to work differently.