diff --git a/docs/designers-developers/developers/data/data-core-block-editor.md b/docs/designers-developers/developers/data/data-core-block-editor.md
index 6e496446d6be20..f765454e948878 100644
--- a/docs/designers-developers/developers/data/data-core-block-editor.md
+++ b/docs/designers-developers/developers/data/data-core-block-editor.md
@@ -824,7 +824,9 @@ _Returns_
# **clearSelectedBlock**
-Returns an action object used in signalling that the block selection is cleared.
+Returns an action object used in signaling that the block selection is cleared.
+This will save the current selection in a state called `previousSelection` and
+`restoreSelectedBlock` will be able to restore the selection.
_Returns_
@@ -1039,6 +1041,14 @@ _Returns_
- `Object`: Action object.
+# **restoreSelectedBlock**
+
+Returns an action object used in restoring the previously cleared selected blocks.
+
+_Returns_
+
+- `Object`: Action object.
+
# **selectBlock**
Returns an action object used in signalling that the block with the
@@ -1225,4 +1235,13 @@ _Returns_
Undocumented declaration.
+# **wipeSelectedBlock**
+
+Returns an action object used in signaling that the block selection is wiped.
+This will remove block selection so that `restoreSelectedBlock` will have no effect.
+
+_Returns_
+
+- `Object`: Action object.
+
diff --git a/packages/block-editor/src/components/block-selection-clearer/index.js b/packages/block-editor/src/components/block-selection-clearer/index.js
index be39e5df9e7de5..cc2e27944e06bd 100644
--- a/packages/block-editor/src/components/block-selection-clearer/index.js
+++ b/packages/block-editor/src/components/block-selection-clearer/index.js
@@ -33,12 +33,12 @@ class BlockSelectionClearer extends Component {
const {
hasSelectedBlock,
hasMultiSelection,
- clearSelectedBlock,
+ wipeSelectedBlock,
} = this.props;
const hasSelection = ( hasSelectedBlock || hasMultiSelection );
if ( event.target === this.container && hasSelection ) {
- clearSelectedBlock();
+ wipeSelectedBlock();
}
}
@@ -68,7 +68,7 @@ export default compose( [
};
} ),
withDispatch( ( dispatch ) => {
- const { clearSelectedBlock } = dispatch( 'core/block-editor' );
- return { clearSelectedBlock };
+ const { wipeSelectedBlock } = dispatch( 'core/block-editor' );
+ return { wipeSelectedBlock };
} ),
] )( BlockSelectionClearer );
diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js
index 4dafaa291fcf31..e1e4c1f69ed040 100644
--- a/packages/block-editor/src/store/actions.js
+++ b/packages/block-editor/src/store/actions.js
@@ -193,7 +193,21 @@ export function multiSelect( start, end ) {
}
/**
- * Returns an action object used in signalling that the block selection is cleared.
+ * Returns an action object used in signaling that the block selection is wiped.
+ * This will remove block selection so that `restoreSelectedBlock` will have no effect.
+ *
+ * @return {Object} Action object.
+ */
+export function wipeSelectedBlock() {
+ return {
+ type: 'WIPE_SELECTED_BLOCK',
+ };
+}
+
+/**
+ * Returns an action object used in signaling that the block selection is cleared.
+ * This will save the current selection in a state called `previousSelection` and
+ * `restoreSelectedBlock` will be able to restore the selection.
*
* @return {Object} Action object.
*/
@@ -203,6 +217,17 @@ export function clearSelectedBlock() {
};
}
+/**
+ * Returns an action object used in restoring the previously cleared selected blocks.
+ *
+ * @return {Object} Action object.
+ */
+export function restoreSelectedBlock() {
+ return {
+ type: 'RESTORE_SELECTED_BLOCK',
+ };
+}
+
/**
* Returns an action object that enables or disables block selection.
*
diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js
index d9d0ecbdd28b64..e62734d0412e2c 100644
--- a/packages/block-editor/src/store/reducer.js
+++ b/packages/block-editor/src/store/reducer.js
@@ -717,8 +717,21 @@ const BLOCK_SELECTION_INITIAL_STATE = {
*/
export function blockSelection( state = BLOCK_SELECTION_INITIAL_STATE, action ) {
switch ( action.type ) {
- case 'CLEAR_SELECTED_BLOCK':
+ case 'WIPE_SELECTED_BLOCK':
return BLOCK_SELECTION_INITIAL_STATE;
+ case 'CLEAR_SELECTED_BLOCK':
+ if ( isEqual( state, BLOCK_SELECTION_INITIAL_STATE ) ) {
+ return BLOCK_SELECTION_INITIAL_STATE;
+ }
+ return {
+ ...BLOCK_SELECTION_INITIAL_STATE,
+ previousSelection: omit( state, [ 'previousSelection' ] ),
+ };
+ case 'RESTORE_SELECTED_BLOCK':
+ return {
+ ...BLOCK_SELECTION_INITIAL_STATE,
+ ...state.previousSelection,
+ };
case 'START_MULTI_SELECT':
if ( state.isMultiSelecting ) {
return state;
diff --git a/packages/block-editor/src/store/test/actions.js b/packages/block-editor/src/store/test/actions.js
index 1fb97cd38d9bde..93123c17bf987b 100644
--- a/packages/block-editor/src/store/test/actions.js
+++ b/packages/block-editor/src/store/test/actions.js
@@ -3,6 +3,8 @@
*/
import {
clearSelectedBlock,
+ wipeSelectedBlock,
+ restoreSelectedBlock,
enterFormattedText,
exitFormattedText,
hideInsertionPoint,
@@ -116,6 +118,22 @@ describe( 'actions', () => {
} );
} );
+ describe( 'wipeSelectedBlock', () => {
+ it( 'should return WIPE_SELECTED_BLOCK action', () => {
+ expect( wipeSelectedBlock() ).toEqual( {
+ type: 'WIPE_SELECTED_BLOCK',
+ } );
+ } );
+ } );
+
+ describe( 'restoreSelectedBlock', () => {
+ it( 'should return RESTORE_SELECTED_BLOCK action', () => {
+ expect( restoreSelectedBlock() ).toEqual( {
+ type: 'RESTORE_SELECTED_BLOCK',
+ } );
+ } );
+ } );
+
describe( 'replaceBlock', () => {
it( 'should yield the REPLACE_BLOCKS action if the new block can be inserted in the destination root block', () => {
const block = {
diff --git a/packages/block-editor/src/store/test/reducer.js b/packages/block-editor/src/store/test/reducer.js
index 7543fdad57dd8b..b0d79d8911292e 100644
--- a/packages/block-editor/src/store/test/reducer.js
+++ b/packages/block-editor/src/store/test/reducer.js
@@ -1740,6 +1740,7 @@ describe( 'state', () => {
initialPosition: null,
isMultiSelecting: false,
isEnabled: true,
+ previousSelection: original,
} );
} );
diff --git a/packages/e2e-tests/specs/a11y.test.js b/packages/e2e-tests/specs/a11y.test.js
index 6f6cc960f204aa..784ee4dc556743 100644
--- a/packages/e2e-tests/specs/a11y.test.js
+++ b/packages/e2e-tests/specs/a11y.test.js
@@ -2,6 +2,7 @@
* WordPress dependencies
*/
import {
+ clickBlockAppender,
createNewPost,
pressKeyWithModifier,
} from '@wordpress/e2e-test-utils';
@@ -29,6 +30,56 @@ describe( 'a11y', () => {
expect( isFocusedToggle ).toBe( true );
} );
+ it( 'checks persistent selection', async () => {
+ await clickBlockAppender();
+
+ // adding one Paragraph block which contains a focusable RichText
+ await page.keyboard.type( 'Testing editor selection persistence' );
+
+ let isFocusedRichText = await page.$eval( ':focus', ( focusedElement ) => {
+ return focusedElement.classList.contains( 'block-editor-rich-text__editable' );
+ } );
+
+ expect( isFocusedRichText ).toBe( true );
+
+ // moving focus backwards using keyboard shortcuts
+ // twice to get to the inspector tabs
+ await pressKeyWithModifier( 'ctrlShift', '`' );
+ await pressKeyWithModifier( 'ctrlShift', '`' );
+
+ await page.keyboard.press( 'Tab' );
+
+ const isFocusedInspectorDocumentTab = await page.$eval( ':focus', ( focusedElement ) => {
+ return focusedElement.getAttribute( 'data-label' );
+ } );
+
+ expect( isFocusedInspectorDocumentTab ).toEqual( 'Document' );
+
+ await page.keyboard.press( 'Space' );
+
+ isFocusedRichText = await page.$eval( ':focus', ( focusedElement ) => {
+ return focusedElement.classList.contains( 'block-editor-rich-text__editable' );
+ } );
+
+ expect( isFocusedRichText ).toBe( false );
+
+ await page.keyboard.press( 'Tab' );
+
+ const isFocusedInspectorBlockTab = await page.$eval( ':focus', ( focusedElement ) => {
+ return focusedElement.getAttribute( 'data-label' );
+ } );
+
+ expect( isFocusedInspectorBlockTab ).toEqual( 'Block' );
+
+ await page.keyboard.press( 'Space' );
+
+ isFocusedRichText = await page.$eval( ':focus', ( focusedElement ) => {
+ return focusedElement.classList.contains( 'block-editor-rich-text__editable' );
+ } );
+
+ expect( isFocusedRichText ).toBe( true );
+ } );
+
it( 'constrains focus to a modal when tabbing', async () => {
// Open keyboard help modal.
await pressKeyWithModifier( 'access', 'h' );
diff --git a/packages/edit-post/src/components/sidebar/settings-header/index.js b/packages/edit-post/src/components/sidebar/settings-header/index.js
index eeb95a872166f5..47d00bce72048a 100644
--- a/packages/edit-post/src/components/sidebar/settings-header/index.js
+++ b/packages/edit-post/src/components/sidebar/settings-header/index.js
@@ -57,7 +57,8 @@ const SettingsHeader = ( { openDocumentSettings, openBlockSettings, sidebarName
export default withDispatch( ( dispatch ) => {
const { openGeneralSidebar } = dispatch( 'core/edit-post' );
- const { clearSelectedBlock } = dispatch( 'core/block-editor' );
+ const { clearSelectedBlock, restoreSelectedBlock } = dispatch( 'core/block-editor' );
+
return {
openDocumentSettings() {
openGeneralSidebar( 'edit-post/document' );
@@ -65,6 +66,7 @@ export default withDispatch( ( dispatch ) => {
},
openBlockSettings() {
openGeneralSidebar( 'edit-post/block' );
+ restoreSelectedBlock();
},
};
} )( SettingsHeader );
diff --git a/packages/edit-post/src/store/effects.js b/packages/edit-post/src/store/effects.js
index 54bfe07fbd67b5..36d3c2b501369c 100644
--- a/packages/edit-post/src/store/effects.js
+++ b/packages/edit-post/src/store/effects.js
@@ -109,7 +109,7 @@ const effects = {
SWITCH_MODE( action ) {
// Unselect blocks when we switch to the code editor.
if ( action.mode !== 'visual' ) {
- dispatch( 'core/block-editor' ).clearSelectedBlock();
+ dispatch( 'core/block-editor' ).wipeSelectedBlock();
}
const message = action.mode === 'visual' ? __( 'Visual editor selected' ) : __( 'Code editor selected' );