diff --git a/packages/block-editor/src/components/block-list/index.js b/packages/block-editor/src/components/block-list/index.js
index 54511ac941ecc4..13ba64f4d1e880 100644
--- a/packages/block-editor/src/components/block-list/index.js
+++ b/packages/block-editor/src/components/block-list/index.js
@@ -1,19 +1,12 @@
/**
* External dependencies
*/
-import {
- findLast,
- invert,
- mapValues,
- sortBy,
- throttle,
-} from 'lodash';
import classnames from 'classnames';
/**
* WordPress dependencies
*/
-import { Component } from '@wordpress/element';
+import { Component, createRef } from '@wordpress/element';
import {
withSelect,
withDispatch,
@@ -27,7 +20,6 @@ import { compose } from '@wordpress/compose';
import BlockAsyncModeProvider from './block-async-mode-provider';
import BlockListBlock from './block';
import BlockListAppender from '../block-list-appender';
-import { getBlockDOMNode } from '../../utils/dom';
/**
* If the block count exceeds the threshold, we disable the reordering animation
@@ -43,71 +35,94 @@ const forceSyncUpdates = ( WrappedComponent ) => ( props ) => {
);
};
+/**
+ * Returns for the deepest node at the start or end of a container node. Ignores
+ * any text nodes that only contain HTML formatting whitespace.
+ *
+ * @param {Element} node Container to search.
+ * @param {string} type 'start' or 'end'.
+ */
+function getDeepestNode( node, type ) {
+ const child = type === 'start' ? 'firstChild' : 'lastChild';
+ const sibling = type === 'start' ? 'nextSibling' : 'previousSibling';
+
+ while ( node[ child ] ) {
+ node = node[ child ];
+
+ while (
+ node.nodeType === node.TEXT_NODE &&
+ /^[ \t\n]*$/.test( node.data ) &&
+ node[ sibling ]
+ ) {
+ node = node[ sibling ];
+ }
+ }
+
+ return node;
+}
+
class BlockList extends Component {
constructor( props ) {
super( props );
this.onSelectionStart = this.onSelectionStart.bind( this );
this.onSelectionEnd = this.onSelectionEnd.bind( this );
- this.setBlockRef = this.setBlockRef.bind( this );
- this.setLastClientY = this.setLastClientY.bind( this );
- this.onPointerMove = throttle( this.onPointerMove.bind( this ), 100 );
- // Browser does not fire `*move` event when the pointer position changes
- // relative to the document, so fire it with the last known position.
- this.onScroll = () => this.onPointerMove( { clientY: this.lastClientY } );
-
- this.lastClientY = 0;
- this.nodes = {};
- }
-
- componentDidMount() {
- window.addEventListener( 'mousemove', this.setLastClientY );
- }
+ this.setSelection = this.setSelection.bind( this );
- componentWillUnmount() {
- window.removeEventListener( 'mousemove', this.setLastClientY );
- }
-
- setLastClientY( { clientY } ) {
- this.lastClientY = clientY;
- }
-
- setBlockRef( node, clientId ) {
- if ( node === null ) {
- delete this.nodes[ clientId ];
- } else {
- this.nodes = {
- ...this.nodes,
- [ clientId ]: node,
- };
- }
+ this.ref = createRef();
}
/**
- * Handles a pointer move event to update the extent of the current cursor
- * multi-selection.
- *
- * @param {MouseEvent} event A mousemove event object.
+ * When the component updates, and there is multi selection, we need to
+ * select the entire block contents.
*/
- onPointerMove( { clientY } ) {
- // We don't start multi-selection until the mouse starts moving, so as
- // to avoid dispatching multi-selection actions on an in-place click.
- if ( ! this.props.isMultiSelecting ) {
- this.props.onStartMultiSelect();
+ componentDidUpdate() {
+ const {
+ hasMultiSelection,
+ blockClientIds,
+ // These must be in the right DOM order.
+ multiSelectedBlockClientIds,
+ } = this.props;
+
+ if ( ! hasMultiSelection ) {
+ return;
}
- const blockContentBoundaries = getBlockDOMNode( this.selectionAtStart ).getBoundingClientRect();
+ const { length } = multiSelectedBlockClientIds;
+ const start = multiSelectedBlockClientIds[ 0 ];
+ const end = multiSelectedBlockClientIds[ length - 1 ];
+ const startIndex = blockClientIds.indexOf( start );
- // prevent multi-selection from triggering when the selected block is a float
- // and the cursor is still between the top and the bottom of the block.
- if ( clientY >= blockContentBoundaries.top && clientY <= blockContentBoundaries.bottom ) {
+ // The selected block is not in this block list.
+ if ( startIndex === -1 ) {
return;
}
- const y = clientY - blockContentBoundaries.top;
- const key = findLast( this.coordMapKeys, ( coordY ) => coordY < y );
+ let startNode = this.ref.current.querySelector(
+ `[data-block="${ start }"]`
+ );
+ let endNode = this.ref.current.querySelector(
+ `[data-block="${ end }"]`
+ );
+
+ const selection = window.getSelection();
+ const range = document.createRange();
- this.onSelectionChange( this.coordMap[ key ] );
+ // The most stable way to select the whole block contents is to start
+ // and end at the deepest points.
+ startNode = getDeepestNode( startNode, 'start' );
+ endNode = getDeepestNode( endNode, 'end' );
+
+ range.setStartBefore( startNode );
+ range.setEndAfter( endNode );
+
+ selection.removeAllRanges();
+ selection.addRange( range );
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener( 'mouseup', this.onSelectionEnd );
+ window.cancelAnimationFrame( this.rafId );
}
/**
@@ -121,73 +136,72 @@ class BlockList extends Component {
return;
}
- const boundaries = this.nodes[ clientId ].getBoundingClientRect();
-
- // Create a clientId to Y coördinate map.
- const clientIdToCoordMap = mapValues( this.nodes, ( node ) =>
- node.getBoundingClientRect().top - boundaries.top );
-
- // Cache a Y coördinate to clientId map for use in `onPointerMove`.
- this.coordMap = invert( clientIdToCoordMap );
- // Cache an array of the Y coördinates for use in `onPointerMove`.
- // Sort the coördinates, as `this.nodes` will not necessarily reflect
- // the current block sequence.
- this.coordMapKeys = sortBy( Object.values( clientIdToCoordMap ) );
- this.selectionAtStart = clientId;
+ this.startClientId = clientId;
+ this.props.onStartMultiSelect();
- window.addEventListener( 'mousemove', this.onPointerMove );
- // Capture scroll on all elements.
- window.addEventListener( 'scroll', this.onScroll, true );
+ // `onSelectionStart` is called after `mousedown` and `mouseleave`
+ // (from a block). The selection ends when `mouseup` happens anywhere
+ // in the window.
window.addEventListener( 'mouseup', this.onSelectionEnd );
+
+ // Removing the contenteditable attributes within the block editor is
+ // essential for selection to work across editable areas. The edible
+ // hosts are removed, allowing selection to be extended outside the
+ // DOM element. `onStartMultiSelect` sets a flag in the store so the
+ // rich text components are updated, but the rerender may happen very
+ // slowly, especially in Safari for the blocks that are asynchonously
+ // rendered. To ensure the browser instantly removes the selection
+ // boundaries, we remove the contenteditable attributes manually.
+ Array.from(
+ this.ref.current.querySelectorAll( '.rich-text' )
+ ).forEach( ( node ) => {
+ node.removeAttribute( 'contenteditable' );
+ } );
}
/**
- * Handles multi-selection changes in response to pointer move.
- *
- * @param {string} clientId Client ID of block under cursor in multi-select
- * drag.
+ * Handles a mouseup event to end the current mouse multi-selection.
*/
- onSelectionChange( clientId ) {
- const { onMultiSelect, selectionStart, selectionEnd } = this.props;
- const { selectionAtStart } = this;
- const isAtStart = selectionAtStart === clientId;
+ onSelectionEnd() {
+ // Equivalent to attaching the listener once.
+ window.removeEventListener( 'mouseup', this.onSelectionEnd );
- if ( ! selectionAtStart || ! this.props.isSelectionEnabled ) {
+ if ( ! this.props.isMultiSelecting ) {
return;
}
- // If multi-selecting and cursor extent returns to the start of
- // selection, cancel multi-select.
- if ( isAtStart && selectionStart ) {
- onMultiSelect( null, null );
- }
-
- // Expand multi-selection to block under cursor.
- if ( ! isAtStart && selectionEnd !== clientId ) {
- onMultiSelect( selectionAtStart, clientId );
- }
+ this.rafId = window.requestAnimationFrame( this.setSelection );
}
- /**
- * Handles a mouseup event to end the current cursor multi-selection.
- */
- onSelectionEnd() {
- // Cancel throttled calls.
- this.onPointerMove.cancel();
+ setSelection() {
+ const selection = window.getSelection();
+
+ // If no selection is found, end multi selection.
+ if ( ! selection.rangeCount || selection.isCollapsed ) {
+ this.props.onStopMultiSelect();
+ return;
+ }
- delete this.coordMap;
- delete this.coordMapKeys;
- delete this.selectionAtStart;
+ let { focusNode } = selection;
+ let clientId;
- window.removeEventListener( 'mousemove', this.onPointerMove );
- window.removeEventListener( 'scroll', this.onScroll, true );
- window.removeEventListener( 'mouseup', this.onSelectionEnd );
+ // Find the client ID of the block where the selection ends.
+ do {
+ focusNode = focusNode.parentElement;
+ } while (
+ focusNode &&
+ ! ( clientId = focusNode.getAttribute( 'data-block' ) )
+ );
- // We may or may not be in a multi-selection when mouseup occurs (e.g.
- // an in-place mouse click), so only trigger stop if multi-selecting.
- if ( this.props.isMultiSelecting ) {
+ // If the final selection doesn't leave the block, there is no multi
+ // selection.
+ if ( this.startClientId === clientId ) {
this.props.onStopMultiSelect();
+ return;
}
+
+ this.props.onMultiSelect( this.startClientId, clientId );
+ this.props.onStopMultiSelect();
}
render() {
@@ -202,15 +216,17 @@ class BlockList extends Component {
hasMultiSelection,
renderAppender,
enableAnimation,
+ isMultiSelecting,
} = this.props;
return (
-
+ ) }
+ >
{ blockClientIds.map( ( clientId, index ) => {
const isBlockInSelection = hasMultiSelection ?
multiSelectedBlockClientIds.includes( clientId ) :
@@ -225,11 +241,10 @@ class BlockList extends Component {
.block-editor-block-list__block-edit::before {
// Use opacity to work in various editor styles.
border-color: $dark-opacity-light-800;
@@ -158,6 +157,28 @@
}
}
+ // Selected style.
+ &.is-multi-selected {
+ > .block-editor-block-list__block-edit::before {
+ border-left-color: $dark-opacity-light-800;
+ box-shadow: inset $block-left-border-width 0 0 0 $dark-gray-500;
+
+ .is-dark-theme & {
+ border-left-color: $light-opacity-light-800;
+ box-shadow: inset $block-left-border-width 0 0 0 $light-gray-600;
+ }
+
+ // Switch to outset borders on larger screens.
+ @include break-small() {
+ box-shadow: -$block-left-border-width 0 0 0 $dark-gray-500;
+
+ .is-dark-theme & {
+ box-shadow: -$block-left-border-width 0 0 0 $light-gray-600;
+ }
+ }
+ }
+ }
+
// Hover style.
&.is-hovered:not(.is-navigate-mode) > .block-editor-block-list__block-edit::before {
box-shadow: -$block-left-border-width 0 0 0 $dark-opacity-light-500;
@@ -248,36 +269,6 @@
* Cross-block selection
*/
-.block-editor-block-list__layout .block-editor-block-list__block {
- ::-moz-selection {
- background-color: $blue-medium-highlight;
- }
-
- ::selection {
- background-color: $blue-medium-highlight;
- }
-
- // Selection style for multiple blocks.
- &.is-multi-selected *::selection {
- background-color: transparent;
- }
-
- &.is-multi-selected .block-editor-block-list__block-edit::before {
- background: $blue-medium-highlight;
-
- // Use opacity to work in various editor styles.
- mix-blend-mode: multiply;
-
- // Collapse extra vertical padding on selection.
- top: -$block-padding;
- bottom: -$block-padding;
-
- .is-dark-theme & {
- mix-blend-mode: soft-light;
- }
- }
-}
-
/**
* Block styles and alignments
diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js
index 1e7d6bfdb85cfc..c970f58f12b066 100644
--- a/packages/block-editor/src/components/rich-text/index.js
+++ b/packages/block-editor/src/components/rich-text/index.js
@@ -472,6 +472,8 @@ const RichTextContainer = compose( [
getSettings,
didAutomaticChange,
__unstableGetBlockWithoutInnerBlocks,
+ isMultiSelecting,
+ hasMultiSelection,
} = select( 'core/block-editor' );
const selectionStart = getSelectionStart();
@@ -505,6 +507,7 @@ const RichTextContainer = compose( [
selectionEnd: isSelected ? selectionEnd.offset : undefined,
isSelected,
didAutomaticChange: didAutomaticChange(),
+ disabled: isMultiSelecting() || hasMultiSelection(),
...extraProps,
};
} ),
diff --git a/packages/block-editor/src/components/rich-text/style.scss b/packages/block-editor/src/components/rich-text/style.scss
index 8d3eeb019834d2..bb4818a6e47d02 100644
--- a/packages/block-editor/src/components/rich-text/style.scss
+++ b/packages/block-editor/src/components/rich-text/style.scss
@@ -14,10 +14,6 @@
background: $light-gray-200;
font-family: $editor-html-font;
font-size: inherit; // This is necessary to override upstream CSS.
-
- .is-multi-selected & {
- background: darken($blue-medium-highlight, 15%);
- }
}
&:focus {
diff --git a/packages/block-editor/src/components/warning/style.scss b/packages/block-editor/src/components/warning/style.scss
index 3f6c78825c68b2..da6306932af853 100644
--- a/packages/block-editor/src/components/warning/style.scss
+++ b/packages/block-editor/src/components/warning/style.scss
@@ -8,11 +8,6 @@
text-align: left;
padding: 10px $block-padding $block-padding;
- // Avoid conflict with the multi-selection highlight color.
- .has-warning.is-multi-selected & {
- background-color: transparent;
- }
-
.is-selected & {
// Use opacity to work in various editor styles.
border-color: $dark-opacity-light-800;
diff --git a/packages/e2e-tests/specs/editor/various/multi-block-selection.test.js b/packages/e2e-tests/specs/editor/various/multi-block-selection.test.js
index 6bf7a62bf7789e..0b9bef3825b67e 100644
--- a/packages/e2e-tests/specs/editor/various/multi-block-selection.test.js
+++ b/packages/e2e-tests/specs/editor/various/multi-block-selection.test.js
@@ -3,134 +3,105 @@
*/
import {
clickBlockAppender,
- insertBlock,
createNewPost,
pressKeyWithModifier,
pressKeyTimes,
getEditedPostContent,
} from '@wordpress/e2e-test-utils';
-describe( 'Multi-block selection', () => {
- beforeEach( async () => {
- await createNewPost();
- } );
-
- it( 'Should select/unselect multiple blocks', async () => {
- const firstBlockSelector = '[data-type="core/paragraph"]';
- const secondBlockSelector = '[data-type="core/image"]';
- const thirdBlockSelector = '[data-type="core/quote"]';
- const multiSelectedCssClass = 'is-multi-selected';
+async function getSelectedFlatIndices() {
+ return await page.evaluate( () => {
+ const indices = [];
+ let single;
- // Creating test blocks
- await clickBlockAppender();
- await page.keyboard.type( 'First Paragraph' );
- await insertBlock( 'Image' );
- await insertBlock( 'Quote' );
- await page.keyboard.type( 'Quote Block' );
-
- const blocks = [ firstBlockSelector, secondBlockSelector, thirdBlockSelector ];
- const expectMultiSelected = async ( selectors, areMultiSelected ) => {
- for ( const selector of selectors ) {
- const className = await page.$eval( selector, ( element ) => element.className );
- if ( areMultiSelected ) {
- expect( className ).toEqual( expect.stringContaining( multiSelectedCssClass ) );
- } else {
- expect( className ).not.toEqual( expect.stringContaining( multiSelectedCssClass ) );
- }
+ Array.from(
+ document.querySelectorAll( '.wp-block' )
+ ).forEach( ( node, index ) => {
+ if ( node.classList.contains( 'is-selected' ) ) {
+ single = index;
}
- };
- // Default: No selection
- await expectMultiSelected( blocks, false );
-
- // Multiselect via Shift + click
- await page.mouse.move( 200, 300 );
- await page.click( firstBlockSelector );
- await page.keyboard.down( 'Shift' );
- await page.click( thirdBlockSelector );
- await page.keyboard.up( 'Shift' );
-
- // Verify selection
- await expectMultiSelected( blocks, true );
+ if ( node.classList.contains( 'is-multi-selected' ) ) {
+ indices.push( index );
+ }
+ } );
- // Unselect
- await page.click( secondBlockSelector );
+ return single !== undefined ? single : indices;
+ } );
+}
- // No selection
- await expectMultiSelected( blocks, false );
+/**
+ * Tests if the native selection matches the block selection.
+ */
+async function testNativeSelection() {
+ await page.evaluate( () => {
+ const selection = window.getSelection();
+ const elements = Array.from(
+ document.querySelectorAll( '.is-multi-selected' )
+ );
+
+ if ( ! elements.length ) {
+ const element = document.querySelector( '.is-selected' );
+
+ if ( ! element || ! selection.rangeCount ) {
+ return;
+ }
- // Multiselect via keyboard
- await page.click( 'body' );
- await pressKeyWithModifier( 'primary', 'a' );
+ const { startContainer, endContainer } = selection.getRangeAt( 0 );
- // Verify selection
- await expectMultiSelected( blocks, true );
+ if ( ! element.contains( startContainer ) ) {
+ throw 'expected selection to start in the selected block';
+ }
- // Unselect
- await page.keyboard.press( 'Escape' );
+ if ( ! element.contains( endContainer ) ) {
+ throw 'expected selection to start in the selected block';
+ }
- // No selection
- await expectMultiSelected( blocks, false );
+ return;
+ }
- // Select all via double shortcut.
- await page.click( firstBlockSelector );
- await pressKeyWithModifier( 'primary', 'a' );
- await pressKeyWithModifier( 'primary', 'a' );
- await expectMultiSelected( blocks, true );
- } );
+ if ( ! selection.rangeCount === 1 ) {
+ throw 'expected one range';
+ }
- it( 'Should select/unselect multiple blocks using Shift + Arrows', async () => {
- const firstBlockSelector = '[data-type="core/paragraph"]';
- const secondBlockSelector = '[data-type="core/image"]';
- const thirdBlockSelector = '[data-type="core/quote"]';
- const multiSelectedCssClass = 'is-multi-selected';
+ if ( selection.isCollapsed ) {
+ throw 'expected an uncollapsed selection';
+ }
- // Creating test blocks
- await clickBlockAppender();
- await page.keyboard.type( 'First Paragraph' );
- await insertBlock( 'Image' );
- await insertBlock( 'Quote' );
- await page.keyboard.type( 'Quote Block' );
-
- const blocks = [ firstBlockSelector, secondBlockSelector, thirdBlockSelector ];
- const expectMultiSelected = async ( selectors, areMultiSelected ) => {
- for ( const selector of selectors ) {
- const className = await page.$eval( selector, ( element ) => element.className );
- if ( areMultiSelected ) {
- expect( className ).toEqual( expect.stringContaining( multiSelectedCssClass ) );
- } else {
- expect( className ).not.toEqual( expect.stringContaining( multiSelectedCssClass ) );
- }
- }
- };
+ const firstElement = elements[ 0 ];
+ const lastElement = elements[ elements.length - 1 ];
+ const { startContainer, endContainer } = selection.getRangeAt( 0 );
- // Default: No selection
- await expectMultiSelected( blocks, false );
+ if ( ! firstElement.contains( startContainer ) ) {
+ throw 'expected selection to start in the first selected block';
+ }
- // Multiselect via Shift + click
- await page.mouse.move( 200, 300 );
- await page.click( firstBlockSelector );
- await page.keyboard.down( 'Shift' );
- await page.keyboard.press( 'ArrowDown' ); // Two blocks selected
- await page.keyboard.press( 'ArrowDown' ); // Three blocks selected
- await page.keyboard.up( 'Shift' );
+ if ( ! lastElement.contains( endContainer ) ) {
+ throw 'expected selection to end in the last selected block';
+ }
+ } );
+}
- // Verify selection
- await expectMultiSelected( blocks, true );
+describe( 'Multi-block selection', () => {
+ beforeEach( async () => {
+ await createNewPost();
} );
- it( 'should speak() number of blocks selected with multi-block selection', async () => {
+ it( 'should select with double ctrl+a and speak', async () => {
await clickBlockAppender();
- await page.keyboard.type( 'First Paragraph' );
- await insertBlock( 'Paragraph' );
- await page.keyboard.type( 'Second Paragraph' );
- await insertBlock( 'Paragraph' );
- await page.keyboard.type( 'Third Paragraph' );
+ await page.keyboard.type( '1' );
+ await page.keyboard.press( 'Enter' );
+ await page.keyboard.type( '2' );
+ await page.keyboard.press( 'Enter' );
+ await page.keyboard.type( '3' );
// Multiselect via keyboard.
await pressKeyWithModifier( 'primary', 'a' );
await pressKeyWithModifier( 'primary', 'a' );
+ await testNativeSelection();
+ expect( await getSelectedFlatIndices() ).toEqual( [ 1, 2, 3 ] );
+
// TODO: It would be great to do this test by spying on `wp.a11y.speak`,
// but it's very difficult to do that because `wp.a11y` has
// DOM-dependant side-effect setup code and doesn't seem straightforward
@@ -188,6 +159,7 @@ describe( 'Multi-block selection', () => {
await page.keyboard.press( 'ArrowLeft' );
await pressKeyWithModifier( 'shift', 'ArrowRight' );
await pressKeyWithModifier( 'shift', 'ArrowUp' );
+ await testNativeSelection();
// This delete all blocks.
await page.keyboard.press( 'Backspace' );
@@ -203,4 +175,121 @@ describe( 'Multi-block selection', () => {
expect( await getEditedPostContent() ).toMatchSnapshot();
} );
+
+ it( 'should select and deselect with shift and arrow keys', async () => {
+ await clickBlockAppender();
+ await page.keyboard.type( '1' );
+ await page.keyboard.press( 'Enter' );
+ await page.keyboard.type( '2' );
+ await page.keyboard.press( 'Enter' );
+ await page.keyboard.type( '3' );
+ await page.keyboard.press( 'Enter' );
+ await page.keyboard.type( '4' );
+ await page.keyboard.press( 'Enter' );
+ await page.keyboard.type( '5' );
+ await page.keyboard.press( 'ArrowUp' );
+ await page.keyboard.press( 'ArrowUp' );
+ await pressKeyWithModifier( 'shift', 'ArrowDown' );
+
+ await testNativeSelection();
+ expect( await getSelectedFlatIndices() ).toEqual( [ 3, 4 ] );
+
+ await pressKeyWithModifier( 'shift', 'ArrowDown' );
+
+ await testNativeSelection();
+ expect( await getSelectedFlatIndices() ).toEqual( [ 3, 4, 5 ] );
+
+ await pressKeyWithModifier( 'shift', 'ArrowUp' );
+
+ await testNativeSelection();
+ expect( await getSelectedFlatIndices() ).toEqual( [ 3, 4 ] );
+
+ await pressKeyWithModifier( 'shift', 'ArrowUp' );
+
+ await testNativeSelection();
+ expect( await getSelectedFlatIndices() ).toBe( 3 );
+
+ await pressKeyWithModifier( 'shift', 'ArrowUp' );
+
+ await testNativeSelection();
+ expect( await getSelectedFlatIndices() ).toEqual( [ 2, 3 ] );
+
+ await pressKeyWithModifier( 'shift', 'ArrowUp' );
+
+ await testNativeSelection();
+ expect( await getSelectedFlatIndices() ).toEqual( [ 1, 2, 3 ] );
+
+ await pressKeyWithModifier( 'shift', 'ArrowDown' );
+
+ await testNativeSelection();
+ expect( await getSelectedFlatIndices() ).toEqual( [ 2, 3 ] );
+
+ await pressKeyWithModifier( 'shift', 'ArrowDown' );
+
+ await testNativeSelection();
+ expect( await getSelectedFlatIndices() ).toBe( 3 );
+ } );
+
+ it( 'should deselect with Escape', async () => {
+ await clickBlockAppender();
+ await page.keyboard.type( '1' );
+ await page.keyboard.press( 'Enter' );
+ await page.keyboard.type( '2' );
+
+ await pressKeyWithModifier( 'primary', 'a' );
+ await pressKeyWithModifier( 'primary', 'a' );
+
+ await testNativeSelection();
+ expect( await getSelectedFlatIndices() ).toEqual( [ 1, 2 ] );
+
+ await page.keyboard.press( 'Escape' );
+
+ expect( await getSelectedFlatIndices() ).toEqual( [] );
+ } );
+
+ it( 'should select with shift + click', async () => {
+ await clickBlockAppender();
+ await page.keyboard.type( '1' );
+ await page.keyboard.press( 'Enter' );
+ await page.keyboard.type( '2' );
+ await page.keyboard.down( 'Shift' );
+ await page.click( '.wp-block-paragraph' );
+ await page.keyboard.up( 'Shift' );
+
+ await testNativeSelection();
+ expect( await getSelectedFlatIndices() ).toEqual( [ 1, 2 ] );
+ } );
+
+ it( 'should select by dragging', async () => {
+ await clickBlockAppender();
+ await page.keyboard.type( '1' );
+ await page.keyboard.press( 'Enter' );
+ await page.keyboard.type( '2' );
+ await page.keyboard.press( 'ArrowUp' );
+
+ const [ coord1, coord2 ] = await page.evaluate( () => {
+ const elements = Array.from( document.querySelectorAll( '.wp-block-paragraph' ) );
+ const rect1 = elements[ 0 ].getBoundingClientRect();
+ const rect2 = elements[ 1 ].getBoundingClientRect();
+ return [
+ {
+ x: rect1.x + ( rect1.width / 2 ),
+ y: rect1.y + ( rect1.height / 2 ),
+ },
+ {
+ x: rect2.x + ( rect2.width / 2 ),
+ y: rect2.y + ( rect2.height / 2 ),
+ },
+ ];
+ } );
+
+ await page.mouse.move( coord1.x, coord1.y );
+ await page.mouse.down();
+ await page.mouse.move( coord2.x, coord2.y, { steps: 10 } );
+ await page.mouse.up();
+ await page.evaluate( () => new Promise( window.requestAnimationFrame ) );
+
+ expect( await getSelectedFlatIndices() ).toEqual( [ 1, 2 ] );
+ await testNativeSelection();
+ } );
} );
diff --git a/packages/rich-text/src/component/index.js b/packages/rich-text/src/component/index.js
index f4e1af80ec42a4..a71904801b1fca 100644
--- a/packages/rich-text/src/component/index.js
+++ b/packages/rich-text/src/component/index.js
@@ -496,6 +496,10 @@ class RichText extends Component {
return;
}
+ if ( this.props.disabled ) {
+ return;
+ }
+
// In case of a keyboard event, ignore selection changes during
// composition.
if (