From c34d6b78e77cd4edbc2963003409151fb0939312 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Tue, 19 Mar 2019 11:06:51 +0000 Subject: [PATCH] Core Columns Block - adds ability to vertically align all the columns (#13899) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adds ability to valign all the columns via parent Proof of concept demonstrating ability to change the valignment of columns with a parent column Block * Adds ability to v-align individual Columns * Adds ability to select individual Column Blocks to access Block Toolbar Previously the ability to select an individual Column Block was disabled via CSS. This was because the Column itself was just a passthrough element with no UI or Tools of its own. Now we’ve introduced the v-align toolbar on the Column the Block needs to be selectable. * Adds Material UI icons @thomasguillot recommended using Material UI icons. Added dedicated icons file and implemented. * Updates to add i18n to text strings Ensure all text strings are translatable. Also reuse icon defs in parent Columns Block alignment settings * Extracts reusable VAlignToolbar to DRY up implementation * Fixes alignment attribute not updating * Updates Toolbar API to observe Single Responsibility Previously Toolbar was too aware of its context and assumed responsibility for updating parent Block’s attributes via setAttributes prop. Revised to utilise callback prop to propagate change events up to the parent. Now upto the consuming component to observe the change and handle it as required. This makes the VAlignToolbar easy to reuse in other locations. Refactor Blocks to utilise new pattern. * Extract deprecations for readability This prepares deprecations for adding a new deprecation to handle the addition of the `verticalAlignment` attribute. * Updates valignment default to no alignment specified Previously we enforced a “top” valign on all columns. This seemed heavy handed given that Themes might wish to control this. Now we default to no valignment and allow user to optionally choose to specifiy a valignment. * Adds ability to toggle alignment settings on and off Previously once alignment had been set there was no way to disable it. Added ability to toggle each setting on and off. * Adds editor preview of vertical alignment * Adds reusable valign toolbar to editor package components * Updates Columns Block to utilise new editor package toolbar component Previously we utilised a bespoke toolbar for the Block. As this toolbar is also required in other components determined best to extract to a resuable component within the @wordpress/editor package. Updates Block to utilise this new component. Removes old bespoke component. See Slack convo: https://wordpress.slack.com/archives/C02QB2JS7/p1550657882430900 * Adds documentation to the BlockVerticalAlignmentToolbar * Updates to use simplified arrow function syntax * Fixes deprecation save definition back to original version Previously the save definition of the deprecation was accidentally altered during testing. Restoring to the current state of `master` to ensure that we haven’t broken the existing deprecation when moving this block to a seperate file. * Updates known classes to use simplified classnames util style Resolves * https://github.com/WordPress/gutenberg/pull/13899#discussion_r258769783 * https://github.com/WordPress/gutenberg/pull/13899#discussion_r258770591 * https://github.com/WordPress/gutenberg/pull/13899#discussion_r258770657 * remove extraneous file with icons * Fixes icon vars to use standard coding style Resolves https://github.com/WordPress/gutenberg/pull/13899#discussion_r258772021 * Updates test to use `it` instead of `test` Resolves https://github.com/WordPress/gutenberg/pull/13899#discussion_r258773251 * Fix spelling and grammatical errors Resolves * https://github.com/WordPress/gutenberg/pull/13899#discussion_r258774530 * https://github.com/WordPress/gutenberg/pull/13899#discussion_r258774080 * Removes duplicate dependencies comment Resolves https://github.com/WordPress/gutenberg/pull/13899#discussion_r258775474 * Updates code readability and self documentation Previously the tenary to determine which icon to show when in “collapsed” mode wasn’t easy to comprehend. Update to add clear variable names to make it more explicit about what happens when there is/isn’t a active alignment value set. This address the concern raised in https://github.com/WordPress/gutenberg/pull/13899#discussion_r258777358 but looks to prefer code readability over absolutely succinct code. * Consolidates selectors for improve readability/comprehension Prevously nested of selectors was getting a little confusing. Pulled onto a single line to this unit of code in a single location. Resolves https://github.com/WordPress/gutenberg/pull/13899#discussion_r258551134 * Updates to consolidate similar CSS rules Resolves https://github.com/WordPress/gutenberg/pull/13899#discussion_r258552884 * Fixes spelling error Resolves https://github.com/WordPress/gutenberg/pull/13899#discussion_r258557977 * Removes unecessary type checking in favour of coercion rules Previously we were explicitly checking the type of the `verticalAlignment` attr with `isNil()`. However this only checks for `undefined` or `null`. Other falsey values would have passed here which is undesirable. As `undefined` and `null` are both coerced to falsey we can remove the dep on `isNil` and simplify. Resolves https://github.com/WordPress/gutenberg/pull/13899#discussion_r258487802 * update icon sizes to match others in Toolbar * Fixes bottom alignment on front end 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 Resolves https://github.com/WordPress/gutenberg/pull/13899#issuecomment-465958295 * Fix failing e2e testing due to additional of toolbar e2e tests were failing due to introduction of valign toolbar meaning that number of tab stops needed to be increased by 3 (the number of items in the new toolbar). Also uncovered a bug with Pupeteer not handling Cmd + A to “Select All” on a Mac. This is a known issue (https://github.com/GoogleChrome/puppeteer/issues/1313 ) and it is now worked around using arrows and backspace. * Fixes toolbar snapshot unit test * Adds force update of child columns when parent setting changes Previously it was possible to have a setting on the parent and individual settings on the individual columns. However the parent would still show an alignment setting even though not all of the child columns had that alignment. Now if the parent has an alignment setting it will be auto applied to the state of the child columns directly. If a child column changes it’s individual alignment to something which does not match the parent’s alignment setting then the parent’s alignment setting is removed. However the individual column alignments still stay “as is” because each individual column has the valign setting directly rather than pseudo inheried from the parent via CSS. Addresses: https://github.com/WordPress/gutenberg/pull/13899#issuecomment-466406846 * Updates changelog with `BlockVerticalAlignmentToolbar` feature * Adds valign feature to changelog for Block Library * Creates dedicated files for large code blocks Previously all code blocks were in a single large index file. This was becoming unwieldy and difficult to manage/edit. Extract each large block into it’s own file for readability and maintainability. * Updates to avoid tests depending on erroneous _id prop Previously the `_id` prop was included purely to make testing easier (more resilient). On reflection introducing code just for tests was a bad idea. Now depends on string comparison which is an acceptable compromise. Resolves https://github.com/WordPress/gutenberg/pull/13899#discussion_r263302166 * Removes usage of unused attribute introduced as experiment This attribute was introduced as part of experimenting in https://github.com/WordPress/gutenberg/pull/13899/commits/e7593150c3c22358fa6a26398dd4aa32e19069c9 and never removed. It’s unused and has been removed. * Updates example to run without erroring due to missing var Resolves https://github.com/WordPress/gutenberg/pull/13899#discussion_r263300008 * Fixes comment to match coding style guidelines Resolves https://github.com/WordPress/gutenberg/pull/13899#discussion_r263313204 * Fixes to avoid mutation in render method Previously in order to reset the Parent’s alignment when one of the child Columns set it’s own alignment, the render method of the Parent Columns mutated the attributes causing a re-render. To avoid this we now reset the Parent from the Child Column whose alignment changed. Addresses https://github.com/WordPress/gutenberg/pull/13899#discussion_r263341084 * Fixes single Column Block breadcrumb to RHS of boundary Resolves point raised in https://github.com/WordPress/gutenberg/pull/13899#issuecomment-471509812 * Restores pass through click behaviour on single Column Block As discussed [here](https://github.com/WordPress/gutenberg/pull/13899#issuecomment-471965971) selection of parent/child Blocks is currently being worked on elsewhere. Therefore to get the vertical-alignment shipped, this commit reverts the change that made Columns individually selectable. Now reverts to original behaviour of only being selectable via the Block Inspector (or keyboard). This assumes that future PRs will land better Parent/Child selection behaviour. Also removes unwanted space within the Columns themselves. * Updates to tidy CSS selectors for improved readability * Updates to move VAlign Toolbar into Block Editor package Previously the editor package container the toolbar. Now the new Block Editor should be the new home for the toolbar * Automated README update * Updates changlogs to reflect release status --- packages/block-editor/README.md | 6 + .../README.md | 84 +++++++++ .../block-vertical-alignment-toolbar/icons.js | 26 +++ .../block-vertical-alignment-toolbar/index.js | 76 ++++++++ .../test/__snapshots__/index.js.snap | 84 +++++++++ .../test/index.js | 46 +++++ packages/block-editor/src/components/index.js | 1 + packages/block-library/CHANGELOG.md | 4 + packages/block-library/src/columns/column.js | 80 ++++++++- .../block-library/src/columns/deprecated.js | 99 ++++++++++ packages/block-library/src/columns/edit.js | 121 +++++++++++++ .../block-library/src/columns/editor.scss | 108 +++++++---- packages/block-library/src/columns/index.js | 170 +++--------------- packages/block-library/src/columns/style.scss | 43 ++++- packages/block-library/src/columns/utils.js | 16 ++ .../specs/block-hierarchy-navigation.test.js | 15 +- 16 files changed, 783 insertions(+), 196 deletions(-) create mode 100644 packages/block-editor/src/components/block-vertical-alignment-toolbar/README.md create mode 100644 packages/block-editor/src/components/block-vertical-alignment-toolbar/icons.js create mode 100644 packages/block-editor/src/components/block-vertical-alignment-toolbar/index.js create mode 100644 packages/block-editor/src/components/block-vertical-alignment-toolbar/test/__snapshots__/index.js.snap create mode 100644 packages/block-editor/src/components/block-vertical-alignment-toolbar/test/index.js create mode 100644 packages/block-library/src/columns/deprecated.js create mode 100644 packages/block-library/src/columns/edit.js create mode 100644 packages/block-library/src/columns/utils.js 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();