diff --git a/packages/block-editor/src/components/alignment-toolbar/multi-block.js b/packages/block-editor/src/components/alignment-toolbar/multi-block.js
new file mode 100644
index 0000000000000..dee613fb0348f
--- /dev/null
+++ b/packages/block-editor/src/components/alignment-toolbar/multi-block.js
@@ -0,0 +1,9 @@
+/**
+ * Internal dependencies
+ */
+import { withMultiBlockSupport } from '../block-controls/multi-block-controls';
+import AlignmentToolbar from './';
+
+const MultiBlockAlignmentToolbar = withMultiBlockSupport( AlignmentToolbar, 'align' );
+
+export default MultiBlockAlignmentToolbar;
diff --git a/packages/block-editor/src/components/block-controls/multi-block-controls.js b/packages/block-editor/src/components/block-controls/multi-block-controls.js
new file mode 100644
index 0000000000000..24ce3534792ad
--- /dev/null
+++ b/packages/block-editor/src/components/block-controls/multi-block-controls.js
@@ -0,0 +1,102 @@
+/**
+ * WordPress dependencies
+ */
+import { createSlotFill, Toolbar } from '@wordpress/components';
+import { createHigherOrderComponent, compose } from '@wordpress/compose';
+import { withSelect, withDispatch } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import { withFirstOrOnlyBlockSelected } from '../block-edit/context';
+
+const { Fill, Slot } = createSlotFill( 'MultiBlockControls' );
+
+const MultiBlockControlsFill = ( { controls, children } ) => (
+
+
+ { children }
+
+);
+
+const MultiBlockControls = withFirstOrOnlyBlockSelected( MultiBlockControlsFill );
+
+MultiBlockControls.Slot = Slot;
+
+export default MultiBlockControls;
+
+/**
+ * Reduces blocks to a single attribute's value, taking the first in the list as
+ * a default, returning `undefined` if all blocks are not the same value.
+ *
+ * @param {Array} multiSelectedBlocks Array of selected blocks.
+ * @param {string} attributeName Attribute name.
+ *
+ * @return {*} Reduced value of attribute.
+ */
+function reduceAttribute( multiSelectedBlocks, attributeName ) {
+ let attribute;
+ // Reduce the selected block's attributes, so if they all have the
+ // same value for an attribute, we get it in the multi toolbar attributes.
+ for ( let i = 0; i < multiSelectedBlocks.length; i++ ) {
+ const block = multiSelectedBlocks[ i ];
+ if ( block.attributes[ attributeName ] === attribute || 0 === i ) {
+ attribute = block.attributes[ attributeName ];
+ } else {
+ attribute = undefined;
+ }
+ }
+ return attribute;
+}
+
+/**
+ * Adds multi block support to a block control. If the control is used when there is a
+ * multi block selection, the `onChange` and `value` props are intercepted, and uses
+ * `reduceAttribute` to get a single value for the control from all selected blocks,
+ * and changes all selected blocks with the new value.
+ *
+ * This requires that multi block controls have `value` and `onChange` props, and
+ * set attributes on blocks with no other side effects (other than those handled
+ * when the edit component receives new props)
+ *
+ * @param {Component} component Component to make multi block selection aware.
+ * @param {string} attributeName Attribute name the component controls.
+ *
+ * @return {Component} Component that can handle multple selected blocks.
+ */
+export const withMultiBlockSupport = ( component, attributeName ) => createHigherOrderComponent( ( OriginalComponent ) => {
+ const multSelectComponent = ( props ) => {
+ let newProps = props;
+ if ( props.multiSelectedBlocks.length > 1 ) {
+ newProps = { ...props };
+ newProps.value = reduceAttribute( props.multiSelectedBlocks, attributeName );
+ newProps.onChange = ( newValue ) => {
+ const newAttributes = {
+ [ attributeName ]: newValue,
+ };
+ for ( let i = 0; i < props.multiSelectedBlocks.length; i++ ) {
+ newProps.onMultiBlockChange( props.multiSelectedBlocks[ i ].clientId, newAttributes );
+ }
+ };
+ }
+ return (
+
+ );
+ };
+ return compose( [
+ withSelect( ( select ) => {
+ const { getMultiSelectedBlocks } = select( 'core/editor' );
+ return {
+ multiSelectedBlocks: getMultiSelectedBlocks(),
+ };
+ } ),
+ withDispatch( ( dispatch ) => {
+ const { updateBlockAttributes } = dispatch( 'core/editor' );
+ return {
+ onMultiBlockChange( uid, attributes ) {
+ updateBlockAttributes( uid, attributes );
+ },
+ };
+ } ),
+ ] )( multSelectComponent );
+}, 'withMultiBlockSupport' )( component );
diff --git a/packages/block-editor/src/components/block-edit/context.js b/packages/block-editor/src/components/block-edit/context.js
index 863cdc3e5d6fa..9ab62211dcf59 100644
--- a/packages/block-editor/src/components/block-edit/context.js
+++ b/packages/block-editor/src/components/block-edit/context.js
@@ -1,13 +1,14 @@
/**
* External dependencies
*/
-import { noop } from 'lodash';
+import { noop, uniq } from 'lodash';
/**
* WordPress dependencies
*/
import { createContext } from '@wordpress/element';
-import { createHigherOrderComponent } from '@wordpress/compose';
+import { createHigherOrderComponent, compose } from '@wordpress/compose';
+import { withSelect } from '@wordpress/data';
const { Consumer, Provider } = createContext( {
name: '',
@@ -59,3 +60,46 @@ export const ifBlockEditSelected = createHigherOrderComponent( ( OriginalCompone
);
}, 'ifBlockEditSelected' );
+
+/**
+ * A Higher Order Component used to render conditionally the wrapped
+ * component only when the BlockEdit has selected state set or it is
+ * the first block in a multi selection of all one type of block..
+ *
+ * @param {Component} OriginalComponent Component to wrap.
+ *
+ * @return {Component} Component which renders only when the BlockEdit is selected or it is the first block in a multi selection.
+ */
+const isFirstOrOnlyBlockSelected = createHigherOrderComponent( ( OriginalComponent ) => {
+ return ( props ) => {
+ return (
+
+ { ( { isSelected, clientId } ) => ( isSelected || ( clientId === props.getFirstMultiSelectedBlockClientId && props.allSelectedBlocksOfSameType ) ) && (
+
+ ) }
+
+ );
+ };
+}, 'isFirstOrOnlyBlockSelected' );
+
+export const withFirstOrOnlyBlockSelected = ( component ) => {
+ return compose( [
+ withSelect( ( select ) => {
+ const {
+ getMultiSelectedBlocks,
+ getFirstMultiSelectedBlockClientId,
+ isMultiSelecting,
+ } = select( 'core/editor' );
+ const allSelectedBlocksOfSameType = uniq(
+ getMultiSelectedBlocks().map( ( { name } ) => name )
+ ).length === 1;
+ return {
+ getFirstMultiSelectedBlockClientId: getFirstMultiSelectedBlockClientId(),
+ isSelecting: isMultiSelecting(),
+ selectedBlocks: getMultiSelectedBlocks(),
+ allSelectedBlocksOfSameType,
+ };
+ } ),
+ isFirstOrOnlyBlockSelected,
+ ] )( component );
+};
diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js
index c5bc11d920f95..d8b023be8485b 100644
--- a/packages/block-editor/src/components/block-toolbar/index.js
+++ b/packages/block-editor/src/components/block-toolbar/index.js
@@ -8,8 +8,9 @@ import { Fragment } from '@wordpress/element';
* Internal dependencies
*/
import BlockSwitcher from '../block-switcher';
-import MultiBlocksSwitcher from '../block-switcher/multi-blocks-switcher';
import BlockControls from '../block-controls';
+import MultiBlockControls from '../block-controls/multi-block-controls';
+import MultiBlocksSwitcher from '../block-switcher/multi-blocks-switcher';
import BlockFormatControls from '../block-format-controls';
import BlockSettingsMenu from '../block-settings-menu';
@@ -22,6 +23,7 @@ function BlockToolbar( { blockClientIds, isValid, mode } ) {
return (
+
);
@@ -32,6 +34,7 @@ function BlockToolbar( { blockClientIds, isValid, mode } ) {
{ mode === 'visual' && isValid && (
+
diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js
index c9a454c253d27..f64ef8d08881d 100644
--- a/packages/block-editor/src/components/index.js
+++ b/packages/block-editor/src/components/index.js
@@ -16,6 +16,8 @@ export * from './font-sizes';
export { default as InnerBlocks } from './inner-blocks';
export { default as InspectorAdvancedControls } from './inspector-advanced-controls';
export { default as InspectorControls } from './inspector-controls';
+export { default as MultiBlockAlignmentToolbar } from './alignment-toolbar/multi-block';
+export { default as MultiBlockControls } from './block-controls/multi-block-controls';
export { default as PanelColorSettings } from './panel-color-settings';
export { default as PlainText } from './plain-text';
export {
diff --git a/packages/block-library/src/paragraph/edit.js b/packages/block-library/src/paragraph/edit.js
index 0ed9c5c91b582..5c91c7e973522 100644
--- a/packages/block-library/src/paragraph/edit.js
+++ b/packages/block-library/src/paragraph/edit.js
@@ -19,8 +19,8 @@ import {
} from '@wordpress/components';
import {
withColors,
- AlignmentToolbar,
- BlockControls,
+ MultiBlockAlignmentToolbar,
+ MultiBlockControls,
ContrastChecker,
FontSizePicker,
InspectorControls,
@@ -152,8 +152,8 @@ class ParagraphBlock extends Component {
return (
-
-
+ {
setAttributes( { align: nextAlign } );
@@ -176,7 +176,7 @@ class ParagraphBlock extends Component {
] }
/>
) }
-
+