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' );