diff --git a/editor/block-mover/index.js b/editor/block-mover/index.js
index f040d2394a67f2..5a6ae7c9148431 100644
--- a/editor/block-mover/index.js
+++ b/editor/block-mover/index.js
@@ -8,31 +8,48 @@ import { first, last } from 'lodash';
* WordPress dependencies
*/
import { IconButton } from 'components';
+import { getBlockType } from 'blocks';
/**
* Internal dependencies
*/
import './style.scss';
-import { isFirstBlock, isLastBlock } from '../selectors';
+import { isFirstBlock, isLastBlock, getBlockOrder, getBlock } from '../selectors';
+import { getBlockMoverLabel } from './mover-label';
-function BlockMover( { onMoveUp, onMoveDown, isFirst, isLast } ) {
+function BlockMover( { onMoveUp, onMoveDown, isFirst, isLast, uids, blockType, firstIndex } ) {
// We emulate a disabled state because forcefully applying the `disabled`
// attribute on the button while it has focus causes the screen to change
// to an unfocused state (body as active element) without firing blur on,
// the rendering parent, leaving it unable to react to focus out.
-
return (
@@ -43,6 +60,8 @@ export default connect(
( state, ownProps ) => ( {
isFirst: isFirstBlock( state, first( ownProps.uids ) ),
isLast: isLastBlock( state, last( ownProps.uids ) ),
+ firstIndex: getBlockOrder( state, first( ownProps.uids ) ),
+ blockType: getBlockType( getBlock( state, first( ownProps.uids ) ).name ),
} ),
( dispatch, ownProps ) => ( {
onMoveDown() {
diff --git a/editor/block-mover/mover-label.js b/editor/block-mover/mover-label.js
new file mode 100644
index 00000000000000..65db9b512213a3
--- /dev/null
+++ b/editor/block-mover/mover-label.js
@@ -0,0 +1,109 @@
+/**
+ * Wordpress dependencies
+ */
+import { __, sprintf } from 'i18n';
+
+/**
+ * Return a label for the block movement controls depending on block position.
+ *
+ * @param {number} selectedCount Number of blocks selected.
+ * @param {string} type Block type - in the case of a single block, should
+ * define its 'type'. I.e. 'Text', 'Heading', 'Image' etc.
+ * @param {number} firstIndex The index (position - 1) of the first block selected.
+ * @param {boolean} isFirst This is the first block.
+ * @param {boolean} isLast This is the last block.
+ * @param {number} dir Direction of movement (> 0 is considered to be going
+ * down, < 0 is up).
+ * @return {string} Label for the block movement controls.
+ */
+export function getBlockMoverLabel( selectedCount, type, firstIndex, isFirst, isLast, dir ) {
+ const position = ( firstIndex + 1 );
+
+ if ( selectedCount > 1 ) {
+ return getMultiBlockMoverLabel( selectedCount, firstIndex, isFirst, isLast, dir );
+ }
+
+ if ( isFirst && isLast ) {
+ // translators: %s: Type of block (i.e. Text, Image etc)
+ return sprintf( __( 'Block "%s" is the only block, and cannot be moved' ), type );
+ }
+
+ if ( dir > 0 && ! isLast ) {
+ // moving down
+ return sprintf(
+ __( 'Move "%(type)s" block from position %(position)d down to position %(newPosition)d' ),
+ {
+ type,
+ position,
+ newPosition: ( position + 1 ),
+ }
+ );
+ }
+
+ if ( dir > 0 && isLast ) {
+ // moving down, and is the last item
+ // translators: %s: Type of block (i.e. Text, Image etc)
+ return sprintf( __( 'Block "%s" is at the end of the content and can’t be moved down' ), type );
+ }
+
+ if ( dir < 0 && ! isFirst ) {
+ // moving up
+ return sprintf(
+ __( 'Move "%(type)s" block from position %(position)d up to position %(newPosition)d' ),
+ {
+ type,
+ position,
+ newPosition: ( position - 1 ),
+ }
+ );
+ }
+
+ if ( dir < 0 && isFirst ) {
+ // moving up, and is the first item
+ // translators: %s: Type of block (i.e. Text, Image etc)
+ return sprintf( __( 'Block "%s" is at the beginning of the content and can’t be moved up' ), type );
+ }
+}
+
+/**
+ * Return a label for the block movement controls depending on block position.
+ *
+ * @param {number} selectedCount Number of blocks selected.
+ * @param {number} firstIndex The index (position - 1) of the first block selected.
+ * @param {boolean} isFirst This is the first block.
+ * @param {boolean} isLast This is the last block.
+ * @param {number} dir Direction of movement (> 0 is considered to be going
+ * down, < 0 is up).
+ * @return {string} Label for the block movement controls.
+ */
+export function getMultiBlockMoverLabel( selectedCount, firstIndex, isFirst, isLast, dir ) {
+ const position = ( firstIndex + 1 );
+
+ if ( dir < 0 && isFirst ) {
+ return __( 'Blocks cannot be moved up as they are already at the top' );
+ }
+
+ if ( dir > 0 && isLast ) {
+ return __( 'Blocks cannot be moved down as they are already at the bottom' );
+ }
+
+ if ( dir < 0 && ! isFirst ) {
+ return sprintf(
+ __( 'Move %(selectedCount)d blocks from position %(position)d up by one place' ),
+ {
+ selectedCount,
+ position,
+ }
+ );
+ }
+
+ if ( dir > 0 && ! isLast ) {
+ return sprintf(
+ __( 'Move %(selectedCount)d blocks from position %(position)s down by one place' ),
+ {
+ selectedCount,
+ position,
+ }
+ );
+ }
+}
diff --git a/editor/block-mover/test/mover-label.js b/editor/block-mover/test/mover-label.js
new file mode 100644
index 00000000000000..354ab70dfb3413
--- /dev/null
+++ b/editor/block-mover/test/mover-label.js
@@ -0,0 +1,133 @@
+/**
+ * External dependencies
+ */
+import { expect } from 'chai';
+
+/**
+ * Internal dependencies
+ */
+import { getBlockMoverLabel, getMultiBlockMoverLabel } from '../mover-label';
+
+describe( 'block mover', () => {
+ const dirUp = -1,
+ dirDown = 1;
+
+ describe( 'getBlockMoverLabel', () => {
+ const type = 'TestType';
+
+ it( 'Should generate a title for the first item moving up', () => {
+ expect( getBlockMoverLabel(
+ 1,
+ type,
+ 0,
+ true,
+ false,
+ dirUp,
+ ) ).to.equal(
+ `Block "${ type }" is at the beginning of the content and can’t be moved up`
+ );
+ } );
+
+ it( 'Should generate a title for the last item moving down', () => {
+ expect( getBlockMoverLabel(
+ 1,
+ type,
+ 3,
+ false,
+ true,
+ dirDown,
+ ) ).to.equal(
+ `Block "${ type }" is at the end of the content and can’t be moved down`
+ );
+ } );
+
+ it( 'Should generate a title for the second item moving up', () => {
+ expect( getBlockMoverLabel(
+ 1,
+ type,
+ 1,
+ false,
+ false,
+ dirUp,
+ ) ).to.equal(
+ `Move "${ type }" block from position 2 up to position 1`
+ );
+ } );
+
+ it( 'Should generate a title for the second item moving down', () => {
+ expect( getBlockMoverLabel(
+ 1,
+ type,
+ 1,
+ false,
+ false,
+ dirDown,
+ ) ).to.equal(
+ `Move "${ type }" block from position 2 down to position 3`
+ );
+ } );
+
+ it( 'Should generate a title for the only item in the list', () => {
+ expect( getBlockMoverLabel(
+ 1,
+ type,
+ 0,
+ true,
+ true,
+ dirDown,
+ ) ).to.equal(
+ `Block "${ type }" is the only block, and cannot be moved`
+ );
+ } );
+ } );
+
+ describe( 'getMultiBlockMoverLabel', () => {
+ it( 'Should generate a title moving multiple blocks up', () => {
+ expect( getMultiBlockMoverLabel(
+ 4,
+ 1,
+ false,
+ true,
+ dirUp,
+ ) ).to.equal(
+ 'Move 4 blocks from position 2 up by one place'
+ );
+ } );
+
+ it( 'Should generate a title moving multiple blocks down', () => {
+ expect( getMultiBlockMoverLabel(
+ 4,
+ 0,
+ true,
+ false,
+ dirDown,
+ ) ).to.equal(
+ 'Move 4 blocks from position 1 down by one place'
+ );
+ } );
+
+ it( 'Should generate a title for a selection of blocks at the top', () => {
+ expect( getMultiBlockMoverLabel(
+ 4,
+ 1,
+ true,
+ true,
+ dirUp,
+ ) ).to.equal(
+ 'Blocks cannot be moved up as they are already at the top'
+ );
+ } );
+
+ it( 'Should generate a title for a selection of blocks at the bottom', () => {
+ expect( getMultiBlockMoverLabel(
+ 4,
+ 2,
+ false,
+ true,
+ dirDown,
+ ) ).to.equal(
+ 'Blocks cannot be moved down as they are already at the bottom'
+ );
+ } );
+ } );
+} );