diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 1d1089561ed2a..da61b36fac7c8 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -118,6 +118,12 @@ Undocumented declaration. Undocumented declaration. +### BlockVerticalAlignmentToolbar + +[src/index.js#L15-L15](src/index.js#L15-L15) + +Undocumented declaration. + ### ColorPalette [src/index.js#L15-L15](src/index.js#L15-L15) diff --git a/packages/block-editor/src/components/block-vertical-alignment-toolbar/README.md b/packages/block-editor/src/components/block-vertical-alignment-toolbar/README.md new file mode 100644 index 0000000000000..8c2c58a03720d --- /dev/null +++ b/packages/block-editor/src/components/block-vertical-alignment-toolbar/README.md @@ -0,0 +1,84 @@ +BlockVerticalAlignmentToolbar +============================= + +`BlockVerticalAlignmentToolbar` is a simple `Toolbar` component designed to provide _vertical_ alignment UI controls for use within the editor `BlockControls` toolbar. + +This builds upon similar patterns to the [`BlockAlignmentToolbar`](https://github.com/WordPress/gutenberg/tree/master/packages/editor/src/components/block-alignment-toolbar) but is focused on vertical alignment only. + +## Usage + +In a block's `edit` implementation, render a `` component. Then inside of this add the `` where required. + + +```jsx +import { registerBlockType } from '@wordpress/blocks'; +import { Fragment } from '@wordpress/element'; +import { + BlockControls, + BlockVerticalAlignmentToolbar, +} from '@wordpress/block-editor'; + +registerBlockType( 'my-plugin/my-block', { + // ... + + attributes: { + // other attributes here + // ... + + verticalAlignment: { + type: 'string', + }, + }, + + edit( { attributes, setAttributes } ) { + + const { verticalAlignment } = attributes; + + // Change handler to set Block `attributes` + const onChange = ( alignment ) => setAttributes( { verticalAlignment: alignment } ); + + return ( + + + + +
+ // your Block here +
+
+ ); + } +} ); +``` + +_Note:_ by default if you do not provide an initial `value` prop for the current alignment value, then no value will be selected (ie: there is no default alignment set). + +_Note:_ the user can repeatedly click on the toolbar buttons to toggle the alignment values on/off. This is handled within the component. + +## Props + +### `value` +* **Type:** `String` +* **Default:** `undefined` +* **Options:**: `top`, `center`, `bottom` + +The current value of the alignment setting. You may only choose from the `Options` listed above. + + +### `onChange` +* **Type:** `Function` + +A callback function invoked when the toolbar's alignment value is changed via an interaction with any of the toolbar's buttons. Called with the new alignment value (ie: `top`, `center`, `bottom`, `undefined`) as the only argument. + +Note: the value may be `undefined` if the user has toggled the component "off". + +```js +const onChange = ( alignment ) => setAttributes( { verticalAlignment: alignment } ); +``` + +## Examples + +The [Core Columns](https://github.com/WordPress/gutenberg/tree/master/packages/block-library/src/columns) Block utilises the `BlockVerticalAlignmentToolbar`. \ No newline at end of file diff --git a/packages/block-editor/src/components/block-vertical-alignment-toolbar/icons.js b/packages/block-editor/src/components/block-vertical-alignment-toolbar/icons.js new file mode 100644 index 0000000000000..4642eddb367c7 --- /dev/null +++ b/packages/block-editor/src/components/block-vertical-alignment-toolbar/icons.js @@ -0,0 +1,26 @@ +/** + * WordPress dependencies + */ +import { Path, SVG } from '@wordpress/components'; + +export const alignBottom = ( + + + + +); + +export const alignCenter = ( + + + + +); + +export const alignTop = ( + + + + +); diff --git a/packages/block-editor/src/components/block-vertical-alignment-toolbar/index.js b/packages/block-editor/src/components/block-vertical-alignment-toolbar/index.js new file mode 100644 index 0000000000000..f75f324b52e5d --- /dev/null +++ b/packages/block-editor/src/components/block-vertical-alignment-toolbar/index.js @@ -0,0 +1,76 @@ +/** + * WordPress dependencies + */ +import { _x } from '@wordpress/i18n'; +import { Toolbar } from '@wordpress/components'; +import { withViewportMatch } from '@wordpress/viewport'; +import { withSelect } from '@wordpress/data'; +import { compose } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import { withBlockEditContext } from '../block-edit/context'; +import { alignTop, alignCenter, alignBottom } from './icons'; + +const BLOCK_ALIGNMENTS_CONTROLS = { + top: { + icon: alignTop, + title: _x( 'Vertically Align Top', 'Block vertical alignment setting' ), + }, + center: { + icon: alignCenter, + title: _x( 'Vertically Align Middle', 'Block vertical alignment setting' ), + }, + bottom: { + icon: alignBottom, + title: _x( 'Vertically Align Bottom', 'Block vertical alignment setting' ), + }, +}; + +const DEFAULT_CONTROLS = [ 'top', 'center', 'bottom' ]; +const DEFAULT_CONTROL = 'top'; + +export function BlockVerticalAlignmentToolbar( { isCollapsed, value, onChange, controls = DEFAULT_CONTROLS } ) { + function applyOrUnset( align ) { + return () => onChange( value === align ? undefined : align ); + } + + const activeAlignment = BLOCK_ALIGNMENTS_CONTROLS[ value ]; + const defaultAlignmentControl = BLOCK_ALIGNMENTS_CONTROLS[ DEFAULT_CONTROL ]; + + return ( + { + return { + ...BLOCK_ALIGNMENTS_CONTROLS[ control ], + isActive: value === control, + onClick: applyOrUnset( control ), + }; + } ) + } + /> + ); +} + +export default compose( + withBlockEditContext( ( { clientId } ) => { + return { + clientId, + }; + } ), + withViewportMatch( { isLargeViewport: 'medium' } ), + withSelect( ( select, { clientId, isLargeViewport, isCollapsed } ) => { + const { getBlockRootClientId, getEditorSettings } = select( 'core/editor' ); + return { + isCollapsed: isCollapsed || ! isLargeViewport || ( + ! getEditorSettings().hasFixedToolbar && + getBlockRootClientId( clientId ) + ), + }; + } ), +)( BlockVerticalAlignmentToolbar ); diff --git a/packages/block-editor/src/components/block-vertical-alignment-toolbar/test/__snapshots__/index.js.snap b/packages/block-editor/src/components/block-vertical-alignment-toolbar/test/__snapshots__/index.js.snap new file mode 100644 index 0000000000000..5af12d1e1a1c7 --- /dev/null +++ b/packages/block-editor/src/components/block-vertical-alignment-toolbar/test/__snapshots__/index.js.snap @@ -0,0 +1,84 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BlockVerticalAlignmentToolbar should match snapshot 1`] = ` + + + + , + "isActive": true, + "onClick": [Function], + "title": "Vertically Align Top", + }, + Object { + "icon": + + + , + "isActive": false, + "onClick": [Function], + "title": "Vertically Align Middle", + }, + Object { + "icon": + + + , + "isActive": false, + "onClick": [Function], + "title": "Vertically Align Bottom", + }, + ] + } + icon={ + + + + + } + label="Change Alignment" +/> +`; diff --git a/packages/block-editor/src/components/block-vertical-alignment-toolbar/test/index.js b/packages/block-editor/src/components/block-vertical-alignment-toolbar/test/index.js new file mode 100644 index 0000000000000..6840e35ba6331 --- /dev/null +++ b/packages/block-editor/src/components/block-vertical-alignment-toolbar/test/index.js @@ -0,0 +1,46 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; + +/** + * Internal dependencies + */ +import { BlockVerticalAlignmentToolbar } from '../'; + +describe( 'BlockVerticalAlignmentToolbar', () => { + const alignment = 'top'; + const onChange = jest.fn(); + + const wrapper = shallow( ); + + const controls = wrapper.props().controls; + + afterEach( () => { + onChange.mockClear(); + } ); + + it( 'should match snapshot', () => { + expect( wrapper ).toMatchSnapshot(); + } ); + + it( 'should call onChange with undefined, when the control is already active', () => { + const activeControl = controls.find( ( { title } ) => title.toLowerCase().includes( alignment ) ); + activeControl.onClick(); + + expect( activeControl.isActive ).toBe( true ); + expect( onChange ).toHaveBeenCalledTimes( 1 ); + expect( onChange ).toHaveBeenCalledWith( undefined ); + } ); + + it( 'should call onChange with alignment value when the control is inactive', () => { + // note "middle" alias for "center" + const inactiveCenterControl = controls.find( ( { title } ) => title.toLowerCase().includes( 'middle' ) ); + + inactiveCenterControl.onClick(); + + expect( inactiveCenterControl.isActive ).toBe( false ); + expect( onChange ).toHaveBeenCalledTimes( 1 ); + expect( onChange ).toHaveBeenCalledWith( 'center' ); + } ); +} ); diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index a43c53ea54f12..aade441e3dde6 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -7,6 +7,7 @@ export { default as BlockEdit } from './block-edit'; export { default as BlockFormatControls } from './block-format-controls'; export { default as BlockNavigationDropdown } from './block-navigation/dropdown'; export { default as BlockIcon } from './block-icon'; +export { default as BlockVerticalAlignmentToolbar } from './block-vertical-alignment-toolbar'; export { default as ColorPalette } from './color-palette'; export { default as withColorContext } from './color-palette/with-color-context'; export * from './colors'; diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index f46b47f579afe..34ac5b9d4807d 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.4.0 (Unreleased) + +- Add vertical alignment controls to `columns` Block ([#13899](https://github.com/WordPress/gutenberg/pull/13899/)). + ## 2.3.0 (2019-03-06) ### New Feature diff --git a/packages/block-library/src/columns/column.js b/packages/block-library/src/columns/column.js index b593f3ee06134..d2c725997d0bb 100644 --- a/packages/block-library/src/columns/column.js +++ b/packages/block-library/src/columns/column.js @@ -1,12 +1,66 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * WordPress dependencies */ import { Path, SVG } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { InnerBlocks } from '@wordpress/block-editor'; +import { InnerBlocks, BlockControls, BlockVerticalAlignmentToolbar } from '@wordpress/block-editor'; +import { withDispatch, withSelect } from '@wordpress/data'; +import { compose } from '@wordpress/compose'; export const name = 'core/column'; +const ColumnEdit = ( { attributes, updateAlignment } ) => { + const { verticalAlignment } = attributes; + + const classes = classnames( 'block-core-columns', { + [ `is-vertically-aligned-${ verticalAlignment }` ]: verticalAlignment, + } ); + + const onChange = ( alignment ) => updateAlignment( alignment ); + + return ( +
+ + + + +
+ ); +}; + +const edit = compose( + withSelect( ( select, { clientId } ) => { + const { getBlockRootClientId } = select( 'core/editor' ); + + return { + parentColumsBlockClientId: getBlockRootClientId( clientId ), + }; + } ), + withDispatch( ( dispatch, { clientId, parentColumsBlockClientId } ) => { + return { + updateAlignment( alignment ) { + // Update self... + dispatch( 'core/editor' ).updateBlockAttributes( clientId, { + verticalAlignment: alignment, + } ); + + // Reset Parent Columns Block + dispatch( 'core/editor' ).updateBlockAttributes( parentColumsBlockClientId, { + verticalAlignment: null, + } ); + }, + }; + } ) +)( ColumnEdit ); + export const settings = { title: __( 'Column' ), @@ -18,17 +72,31 @@ export const settings = { category: 'common', + attributes: { + verticalAlignment: { + type: 'string', + }, + }, + supports: { inserter: false, reusable: false, html: false, }, - edit() { - return ; - }, + edit, - save() { - return
; + save( { attributes } ) { + const { verticalAlignment } = attributes; + const wrapperClasses = classnames( { + [ `is-vertically-aligned-${ verticalAlignment }` ]: verticalAlignment, + } ); + + return ( +
+ +
+ ); }, }; + diff --git a/packages/block-library/src/columns/deprecated.js b/packages/block-library/src/columns/deprecated.js new file mode 100644 index 0000000000000..7d3d16c770adb --- /dev/null +++ b/packages/block-library/src/columns/deprecated.js @@ -0,0 +1,99 @@ +/** + * WordPress dependencies + */ +import { createBlock } from '@wordpress/blocks'; +import { + InnerBlocks, +} from '@wordpress/block-editor'; + +/** + * Given an HTML string for a deprecated columns inner block, returns the + * column index to which the migrated inner block should be assigned. Returns + * undefined if the inner block was not assigned to a column. + * + * @param {string} originalContent Deprecated Columns inner block HTML. + * + * @return {?number} Column to which inner block is to be assigned. + */ +function getDeprecatedLayoutColumn( originalContent ) { + let { doc } = getDeprecatedLayoutColumn; + if ( ! doc ) { + doc = document.implementation.createHTMLDocument( '' ); + getDeprecatedLayoutColumn.doc = doc; + } + + let columnMatch; + + doc.body.innerHTML = originalContent; + for ( const classListItem of doc.body.firstChild.classList ) { + if ( ( columnMatch = classListItem.match( /^layout-column-(\d+)$/ ) ) ) { + return Number( columnMatch[ 1 ] ) - 1; + } + } +} + +export default [ + { + attributes: { + columns: { + type: 'number', + default: 2, + }, + }, + isEligible( attributes, innerBlocks ) { + // Since isEligible is called on every valid instance of the + // Columns block and a deprecation is the unlikely case due to + // its subsequent migration, optimize for the `false` condition + // by performing a naive, inaccurate pass at inner blocks. + const isFastPassEligible = innerBlocks.some( ( innerBlock ) => ( + /layout-column-\d+/.test( innerBlock.originalContent ) + ) ); + + if ( ! isFastPassEligible ) { + return false; + } + + // Only if the fast pass is considered eligible is the more + // accurate, durable, slower condition performed. + return innerBlocks.some( ( innerBlock ) => ( + getDeprecatedLayoutColumn( innerBlock.originalContent ) !== undefined + ) ); + }, + migrate( attributes, innerBlocks ) { + const columns = innerBlocks.reduce( ( result, innerBlock ) => { + const { originalContent } = innerBlock; + + let columnIndex = getDeprecatedLayoutColumn( originalContent ); + if ( columnIndex === undefined ) { + columnIndex = 0; + } + + if ( ! result[ columnIndex ] ) { + result[ columnIndex ] = []; + } + + result[ columnIndex ].push( innerBlock ); + + return result; + }, [] ); + + const migratedInnerBlocks = columns.map( ( columnBlocks ) => ( + createBlock( 'core/column', {}, columnBlocks ) + ) ); + + return [ + attributes, + migratedInnerBlocks, + ]; + }, + save( { attributes } ) { + const { columns } = attributes; + + return ( +
+ +
+ ); + }, + }, +]; diff --git a/packages/block-library/src/columns/edit.js b/packages/block-library/src/columns/edit.js new file mode 100644 index 0000000000000..af5431807645d --- /dev/null +++ b/packages/block-library/src/columns/edit.js @@ -0,0 +1,121 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { compose } from '@wordpress/compose'; +import { + PanelBody, + RangeControl, +} from '@wordpress/components'; +import { Fragment } from '@wordpress/element'; +import { + InspectorControls, + InnerBlocks, + BlockControls, + BlockVerticalAlignmentToolbar, +} from '@wordpress/block-editor'; +import { withSelect, withDispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { getColumnsTemplate } from './utils'; + +/** + * Allowed blocks constant is passed to InnerBlocks precisely as specified here. + * The contents of the array should never change. + * The array should contain the name of each block that is allowed. + * In columns block, the only block we allow is 'core/column'. + * + * @constant + * @type {string[]} +*/ +const ALLOWED_BLOCKS = [ 'core/column' ]; + +export const ColumnsEdit = function( { attributes, setAttributes, className, updateAlignment } ) { + 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 ( + + + + { + setAttributes( { + columns: nextColumns, + } ); + } } + min={ 2 } + max={ 6 } + /> + + + + + +
+ +
+
+ ); +}; + +export default compose( + /** + * Selects the child column Blocks for this parent Column + */ + withSelect( ( select, { clientId } ) => { + const { getBlocksByClientId } = select( 'core/editor' ); + + return { + childColumns: getBlocksByClientId( clientId )[ 0 ].innerBlocks, + }; + } ), + 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 ); diff --git a/packages/block-library/src/columns/editor.scss b/packages/block-library/src/columns/editor.scss index 2f316ef347154..4e8905f9bbca1 100644 --- a/packages/block-library/src/columns/editor.scss +++ b/packages/block-library/src/columns/editor.scss @@ -1,13 +1,20 @@ +@mixin flex-full-height() { + display: flex; + flex-direction: column; + flex: 1; +} + + // These margins make sure that nested blocks stack/overlay with the parent block chrome // This is sort of an experiment at making sure the editor looks as much like the end result as possible // Potentially the rules here can apply to all nested blocks and enable stacking, in which case it should be moved elsewhere // When using CSS grid, margins do not collapse on the container. -.wp-block-columns .block-editor-block-list__layout { +.wp-block-columns .editor-block-list__layout { margin-left: 0; margin-right: 0; // This max-width is used to constrain the main editor column, it should not cascade into columns - .block-editor-block-list__block { + .editor-block-list__block { max-width: none; } } @@ -16,7 +23,7 @@ // This is not a 1:1 preview with the front-end where these margins would presumably be zero. // @todo This could be revisited, by for example showing this margin only when the parent block was selected first. // Then at least an unselected columns block would be an accurate preview. -.block-editor-block-list__block[data-align="full"] .wp-block-columns > .block-editor-inner-blocks { +.editor-block-list__block[data-align="full"] .wp-block-columns > .editor-inner-blocks { padding-left: $block-padding; padding-right: $block-padding; @@ -29,7 +36,7 @@ .wp-block-columns { display: block; - > .block-editor-inner-blocks > .block-editor-block-list__layout { + > .editor-inner-blocks > .editor-block-list__layout { display: flex; // Responsiveness: Allow wrapping on mobile. @@ -38,44 +45,21 @@ @include break-medium() { flex-wrap: nowrap; } - + // Set full heights on Columns to enable vertical alignment preview + > [data-type="core/column"], + > [data-type="core/column"] > .editor-block-list__block-edit, + > [data-type="core/column"] > .editor-block-list__block-edit > div[data-block], + > [data-type="core/column"] > .editor-block-list__block-edit .block-core-columns { + @include flex-full-height(); + } // Adjust the individual column block. > [data-type="core/column"] { - display: flex; - flex-direction: column; - flex: 1; - - // The Column block is a child of the Columns block and is mostly a passthrough container. - // Therefore it shouldn't add additional paddings and margins, so we reset these, and compensate for margins. - // @todo In the future, if a passthrough feature lands, it would be good to apply these rules to such an element in a more generic way. - > .block-editor-block-list__block-edit > div > .block-editor-inner-blocks { - margin-top: -$block-padding - $block-padding; - margin-bottom: -$block-padding - $block-padding; - } - - > .block-editor-block-list__block-edit { - margin-top: 0; - margin-bottom: 0; - } - - // Extend the passthrough concept to the block paddings, which we zero out. - > .block-editor-block-list__block-edit::before { - left: 0; - right: 0; - } - > .block-editor-block-list__block-edit > .block-editor-block-contextual-toolbar { - margin-left: -$border-width; - } // On mobile, only a single column is shown, so match adjacent block paddings. padding-left: 0; padding-right: 0; margin-left: -$block-padding; margin-right: -$block-padding; - @include break-small() { - margin-left: $block-padding; - margin-right: $block-padding; - } // Prevent the columns from growing wider than their distributed sizes. min-width: 0; @@ -91,6 +75,8 @@ @include break-small() { flex-basis: calc(50% - (#{$grid-size-large} + #{$block-padding * 2})); flex-grow: 0; + margin-left: $block-padding; + margin-right: $block-padding; } // Add space between columns. Themes can customize this if they wish to work differently. @@ -108,10 +94,62 @@ margin-left: calc(#{$grid-size-large * 2} + #{$block-padding}); } } + + > .editor-block-list__block-edit { + margin-top: 0; + margin-bottom: 0; + + // Remove Block "padding" so individual Column is flush with parent Columns + &::before { + left: 0; + right: 0; + } + + > .editor-block-contextual-toolbar { + margin-left: -$border-width; + } + + // The Column block is a child of the Columns block and is mostly a passthrough container. + // Therefore it shouldn't add additional paddings and margins, so we reset these, and compensate for margins. + // @todo In the future, if a passthrough feature lands, it would be good to apply these rules to such an element in a more generic way. + > div > .block-core-columns > .editor-inner-blocks { + margin-top: -$block-padding - $block-padding; + margin-bottom: -$block-padding - $block-padding; + } + } } } } +/** + * Vertical Alignment Preview + * note: specificity is important here to ensure individual + * * columns alignment is prioritised over parent column alignment + * + */ +.are-vertically-aligned-top .block-core-columns, +div.block-core-columns.is-vertically-aligned-top { + justify-content: flex-start; +} + +.are-vertically-aligned-center .block-core-columns, +div.block-core-columns.is-vertically-aligned-center { + justify-content: center; +} + +.are-vertically-aligned-bottom .block-core-columns, +div.block-core-columns.is-vertically-aligned-bottom { + justify-content: flex-end; +} + + +/** + * Fixes single Column breadcrumb to RHS of Block boundary + */ +[data-type="core/column"] > .editor-block-list__block-edit > .editor-block-list__breadcrumb { + right: 0; +} + // 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"] { @@ -132,6 +170,6 @@ } // This selector re-enables clicking on any child of a column block. -:not(.components-disabled) > .wp-block-columns > .block-editor-inner-blocks > .block-editor-block-list__layout > [data-type="core/column"] > .block-editor-block-list__block-edit > * { +:not(.components-disabled) > .wp-block-columns > .editor-inner-blocks > .editor-block-list__layout > [data-type="core/column"] > .editor-block-list__block-edit > * { pointer-events: all; } diff --git a/packages/block-library/src/columns/index.js b/packages/block-library/src/columns/index.js index eabd06e0e5ae2..b0c4e9b2c0a5f 100644 --- a/packages/block-library/src/columns/index.js +++ b/packages/block-library/src/columns/index.js @@ -1,69 +1,27 @@ /** * External dependencies */ -import { times } from 'lodash'; import classnames from 'classnames'; -import memoize from 'memize'; /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { PanelBody, RangeControl, G, SVG, Path } from '@wordpress/components'; -import { Fragment } from '@wordpress/element'; -import { createBlock } from '@wordpress/blocks'; import { - InspectorControls, + G, + SVG, + Path, +} from '@wordpress/components'; + +import { InnerBlocks, } from '@wordpress/block-editor'; /** - * Allowed blocks constant is passed to InnerBlocks precisely as specified here. - * The contents of the array should never change. - * The array should contain the name of each block that is allowed. - * In columns block, the only block we allow is 'core/column'. - * - * @constant - * @type {string[]} -*/ -const ALLOWED_BLOCKS = [ 'core/column' ]; - -/** - * Returns the layouts configuration for a given number of columns. - * - * @param {number} columns Number of columns. - * - * @return {Object[]} Columns layout configuration. + * Internal dependencies */ -const getColumnsTemplate = memoize( ( columns ) => { - return times( columns, () => [ 'core/column' ] ); -} ); - -/** - * Given an HTML string for a deprecated columns inner block, returns the - * column index to which the migrated inner block should be assigned. Returns - * undefined if the inner block was not assigned to a column. - * - * @param {string} originalContent Deprecated Columns inner block HTML. - * - * @return {?number} Column to which inner block is to be assigned. - */ -function getDeprecatedLayoutColumn( originalContent ) { - let { doc } = getDeprecatedLayoutColumn; - if ( ! doc ) { - doc = document.implementation.createHTMLDocument( '' ); - getDeprecatedLayoutColumn.doc = doc; - } - - let columnMatch; - - doc.body.innerHTML = originalContent; - for ( const classListItem of doc.body.firstChild.classList ) { - if ( ( columnMatch = classListItem.match( /^layout-column-(\d+)$/ ) ) ) { - return Number( columnMatch[ 1 ] ) - 1; - } - } -} +import deprecated from './deprecated'; +import edit from './edit'; export const name = 'core/columns'; @@ -79,6 +37,9 @@ export const settings = { type: 'number', default: 2, }, + verticalAlignment: { + type: 'string', + }, }, description: __( 'Add a block that displays content in multiple columns, then add whatever content blocks you’d like.' ), @@ -88,111 +49,22 @@ export const settings = { html: false, }, - deprecated: [ - { - attributes: { - columns: { - type: 'number', - default: 2, - }, - }, - isEligible( attributes, innerBlocks ) { - // Since isEligible is called on every valid instance of the - // Columns block and a deprecation is the unlikely case due to - // its subsequent migration, optimize for the `false` condition - // by performing a naive, inaccurate pass at inner blocks. - const isFastPassEligible = innerBlocks.some( ( innerBlock ) => ( - /layout-column-\d+/.test( innerBlock.originalContent ) - ) ); - - if ( ! isFastPassEligible ) { - return false; - } - - // Only if the fast pass is considered eligible is the more - // accurate, durable, slower condition performed. - return innerBlocks.some( ( innerBlock ) => ( - getDeprecatedLayoutColumn( innerBlock.originalContent ) !== undefined - ) ); - }, - migrate( attributes, innerBlocks ) { - const columns = innerBlocks.reduce( ( result, innerBlock ) => { - const { originalContent } = innerBlock; - - let columnIndex = getDeprecatedLayoutColumn( originalContent ); - if ( columnIndex === undefined ) { - columnIndex = 0; - } - - if ( ! result[ columnIndex ] ) { - result[ columnIndex ] = []; - } - - result[ columnIndex ].push( innerBlock ); - - return result; - }, [] ); - - const migratedInnerBlocks = columns.map( ( columnBlocks ) => ( - createBlock( 'core/column', {}, columnBlocks ) - ) ); - - return [ - attributes, - migratedInnerBlocks, - ]; - }, - save( { attributes } ) { - const { columns } = attributes; - - return ( -
- -
- ); - }, - }, - ], + deprecated, - edit( { attributes, setAttributes, className } ) { - const { columns } = attributes; - const classes = classnames( className, `has-${ columns }-columns` ); - - return ( - - - - { - setAttributes( { - columns: nextColumns, - } ); - } } - min={ 2 } - max={ 6 } - required - /> - - -
- -
-
- ); - }, + edit, save( { attributes } ) { - const { columns } = attributes; + const { columns, verticalAlignment } = attributes; + + const wrapperClasses = classnames( `has-${ columns }-columns`, { + [ `are-vertically-aligned-${ verticalAlignment }` ]: verticalAlignment, + } ); return ( -
+
); }, }; + diff --git a/packages/block-library/src/columns/style.scss b/packages/block-library/src/columns/style.scss index 00322b0afe799..e377480792af0 100644 --- a/packages/block-library/src/columns/style.scss +++ b/packages/block-library/src/columns/style.scss @@ -11,7 +11,6 @@ .wp-block-column { flex-grow: 1; - margin-bottom: 1em; // Responsiveness: Show at most one columns on mobile. flex-basis: 100%; @@ -44,3 +43,45 @@ } } } + +// Specificity overide to ensure margin is applied +// and preserved on last child to ensure that when columns +// are aligned to bottom they are are flush with each other +.wp-block-column, +.entry-content > .wp-block-columns .wp-block-column:last-child { + margin-bottom: 1em; +} + +/** + * All Columns Alignment + */ +.wp-block-columns { + &.are-vertically-aligned-top { + align-items: flex-start; + } + + &.are-vertically-aligned-center { + align-items: center; + } + + &.are-vertically-aligned-bottom { + align-items: flex-end; + } +} + +/** + * Individual Column Alignment + */ +.wp-block-column { + &.is-vertically-aligned-top { + align-self: flex-start; + } + + &.is-vertically-aligned-center { + align-self: center; + } + + &.is-vertically-aligned-bottom { + align-self: flex-end; + } +} diff --git a/packages/block-library/src/columns/utils.js b/packages/block-library/src/columns/utils.js new file mode 100644 index 0000000000000..e7e3f90df70fd --- /dev/null +++ b/packages/block-library/src/columns/utils.js @@ -0,0 +1,16 @@ +/** + * External dependencies + */ +import memoize from 'memize'; +import { times } from 'lodash'; + +/** + * Returns the layouts configuration for a given number of columns. + * + * @param {number} columns Number of columns. + * + * @return {Object[]} Columns layout configuration. + */ +export const getColumnsTemplate = memoize( ( columns ) => { + return times( columns, () => [ 'core/column' ] ); +} ); diff --git a/packages/e2e-tests/specs/block-hierarchy-navigation.test.js b/packages/e2e-tests/specs/block-hierarchy-navigation.test.js index 200e5a234f06b..eb23d8fba8c26 100644 --- a/packages/e2e-tests/specs/block-hierarchy-navigation.test.js +++ b/packages/e2e-tests/specs/block-hierarchy-navigation.test.js @@ -44,7 +44,7 @@ describe( 'Navigating the block hierarchy', () => { await lastColumnsBlockMenuItem.click(); // Insert text in the last column block. - await pressKeyTimes( 'Tab', 2 ); // Navigate to the appender. + await pressKeyTimes( 'Tab', 5 ); // Navigate to the appender. await page.keyboard.type( 'Third column' ); expect( await getEditedPostContent() ).toMatchSnapshot(); @@ -76,17 +76,19 @@ describe( 'Navigating the block hierarchy', () => { await page.keyboard.press( 'Enter' ); // Insert text in the last column block - await pressKeyTimes( 'Tab', 2 ); // Navigate to the appender. + await pressKeyTimes( 'Tab', 5 ); // Navigate to the appender. await page.keyboard.type( 'Third column' ); expect( await getEditedPostContent() ).toMatchSnapshot(); } ); it( 'should appear and function even without nested blocks', async () => { + const textString = 'You say goodbye'; + await insertBlock( 'Paragraph' ); // Add content so there is a block in the hierachy. - await page.keyboard.type( 'You say goodbye' ); + await page.keyboard.type( textString ); // Create an image block too. await page.keyboard.press( 'Enter' ); @@ -96,8 +98,11 @@ describe( 'Navigating the block hierarchy', () => { await openBlockNavigator(); await page.keyboard.press( 'Space' ); - // Replace its content. - await pressKeyWithModifier( 'primary', 'A' ); + // Replace its content + // note Cmd/Ctrl + A doesn't work on Mac with Pupetter right now + // https://github.com/GoogleChrome/puppeteer/issues/1313 + await pressKeyTimes( 'ArrowRight', textString.length ); + await pressKeyTimes( 'Backspace', textString.length ); await page.keyboard.type( 'and I say hello' ); expect( await getEditedPostContent() ).toMatchSnapshot();