diff --git a/docs/data/data-core-editor.md b/docs/data/data-core-editor.md index 4ce76d80ea62a..78c2371a6e3e2 100644 --- a/docs/data/data-core-editor.md +++ b/docs/data/data-core-editor.md @@ -707,7 +707,7 @@ otherwise. *Returns* -Whether block is first in mult-selection. +Whether block is first in multi-selection. ### isBlockMultiSelected @@ -1534,7 +1534,7 @@ be inserted, optionally at a specific index respective a root block list. * blocks: Block objects to insert. * index: Index at which block should be inserted. - * rootClientId: Optional root cliente ID of block list on + * rootClientId: Optional root client ID of block list on which to insert. ### showInsertionPoint @@ -1542,6 +1542,12 @@ be inserted, optionally at a specific index respective a root block list. Returns an action object used in signalling that the insertion point should be shown. +*Parameters* + + * rootClientId: Optional root client ID of block list on + which to insert. + * index: Index at which block should be inserted. + ### hideInsertionPoint Returns an action object hiding the insertion point. diff --git a/packages/editor/src/components/block-list/block.js b/packages/editor/src/components/block-list/block.js index 1213d1c424453..5858f8731fb7a 100644 --- a/packages/editor/src/components/block-list/block.js +++ b/packages/editor/src/components/block-list/block.js @@ -412,10 +412,9 @@ export class BlockListBlock extends Component { const shouldShowMobileToolbar = shouldAppearSelected; const { error, dragging } = this.state; - // Insertion point can only be made visible when the side inserter is - // not present, and either the block is at the extent of a selection or - // is the first block in the top-level list rendering. - const shouldShowInsertionPoint = ( isPartOfMultiSelection && isFirst ) || ! isPartOfMultiSelection; + // Insertion point can only be made visible if the block is at the + // the extent of a multi-selection, or not in a multi-selection. + const shouldShowInsertionPoint = ( isPartOfMultiSelection && isFirstMultiSelected ) || ! isPartOfMultiSelection; const canShowInBetweenInserter = ! isEmptyDefaultBlock && ! isPreviousBlockADefaultEmptyBlock; // The wp-block className is important for editor styles. @@ -501,7 +500,6 @@ export class BlockListBlock extends Component { clientId={ clientId } rootClientId={ rootClientId } canShowInserter={ canShowInBetweenInserter } - onInsert={ this.hideHoverEffects } /> ) } - { showInsertionPoint &&
} - { showInserter && ( -
- + ) } + { canShowInserter && ( +
+
) } @@ -74,44 +77,23 @@ class BlockInsertionPoint extends Component { ); } } -export default compose( - withSelect( ( select, { clientId, rootClientId, canShowInserter } ) => { - const { - canInsertBlockType, - getBlockIndex, - getBlockInsertionPoint, - getBlock, - isBlockInsertionPointVisible, - isTyping, - } = select( 'core/editor' ); - const { - getDefaultBlockName, - } = select( 'core/blocks' ); - const blockIndex = clientId ? getBlockIndex( clientId, rootClientId ) : -1; - const insertIndex = blockIndex; - const insertionPoint = getBlockInsertionPoint(); - const block = clientId ? getBlock( clientId ) : null; - const showInsertionPoint = ( - isBlockInsertionPointVisible() && - insertionPoint.index === insertIndex && - insertionPoint.rootClientId === rootClientId && - ( ! block || ! isUnmodifiedDefaultBlock( block ) ) - ); +export default withSelect( ( select, { clientId, rootClientId } ) => { + const { + getBlockIndex, + getBlockInsertionPoint, + getBlock, + isBlockInsertionPointVisible, + } = select( 'core/editor' ); + const blockIndex = getBlockIndex( clientId, rootClientId ); + const insertIndex = blockIndex; + const insertionPoint = getBlockInsertionPoint(); + const block = getBlock( clientId ); + const showInsertionPoint = ( + isBlockInsertionPointVisible() && + insertionPoint.index === insertIndex && + insertionPoint.rootClientId === rootClientId && + ! isUnmodifiedDefaultBlock( block ) + ); - const defaultBlockName = getDefaultBlockName(); - return { - canInsertDefaultBlock: canInsertBlockType( defaultBlockName, rootClientId ), - showInserter: ! isTyping() && canShowInserter, - index: insertIndex, - showInsertionPoint, - }; - } ), - ifCondition( ( { canInsertDefaultBlock } ) => canInsertDefaultBlock ), - withDispatch( ( dispatch ) => { - const { insertDefaultBlock, startTyping } = dispatch( 'core/editor' ); - return { - insertDefaultBlock, - startTyping, - }; - } ) -)( BlockInsertionPoint ); + return { showInsertionPoint, insertIndex }; +} )( BlockInsertionPoint ); diff --git a/packages/editor/src/components/block-list/style.scss b/packages/editor/src/components/block-list/style.scss index 5ae32d9bdc7ed..67f1aaa00a61d 100644 --- a/packages/editor/src/components/block-list/style.scss +++ b/packages/editor/src/components/block-list/style.scss @@ -640,7 +640,7 @@ justify-content: center; // Show a clickable plus. - .editor-block-list__insertion-point-button { + .editor-inserter__toggle { margin-top: -4px; border-radius: 50%; color: $blue-medium-focus; @@ -666,7 +666,7 @@ // Don't show the sibling inserter before the selected block. .edit-post-layout:not(.has-fixed-toolbar) { // The child selector is necessary for this to work properly in nested contexts. - .is-selected > .editor-block-list__insertion-point > .editor-block-list__insertion-point-inserter { + .is-selected > .editor-block-list__insertion-point .editor-inserter__toggle { opacity: 0; pointer-events: none; diff --git a/packages/editor/src/components/inserter/index.js b/packages/editor/src/components/inserter/index.js index 1879683b3c072..8017fa4b65c1a 100644 --- a/packages/editor/src/components/inserter/index.js +++ b/packages/editor/src/components/inserter/index.js @@ -99,15 +99,14 @@ class Inserter extends Component { } export default compose( [ - withSelect( ( select, { rootClientId } ) => { + withSelect( ( select, { rootClientId, index } ) => { const { getEditedPostAttribute, getBlockInsertionPoint, getInserterItems, } = select( 'core/editor' ); - let index; - if ( rootClientId === undefined ) { + if ( rootClientId === undefined && index === undefined ) { // Unless explicitly provided, the default insertion point provided // by the store occurs immediately following the selected block. // Otherwise, the default behavior for an undefined index is to diff --git a/packages/editor/src/components/inserter/menu.js b/packages/editor/src/components/inserter/menu.js index 9780661f6442f..be7623a6a50aa 100644 --- a/packages/editor/src/components/inserter/menu.js +++ b/packages/editor/src/components/inserter/menu.js @@ -130,10 +130,12 @@ export class InserterMenu extends Component { hoveredItem: item, } ); + const { showInsertionPoint, hideInsertionPoint } = this.props; if ( item ) { - this.props.showInsertionPoint(); + const { rootClientId, index } = this.props; + showInsertionPoint( rootClientId, index ); } else { - this.props.hideInsertionPoint(); + hideInsertionPoint(); } } diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 2935b833270f0..d0946c4f0e3d0 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -312,7 +312,7 @@ export function insertBlock( block, index, rootClientId ) { * * @param {Object[]} blocks Block objects to insert. * @param {?number} index Index at which block should be inserted. - * @param {?string} rootClientId Optional root cliente ID of block list on + * @param {?string} rootClientId Optional root client ID of block list on * which to insert. * * @return {Object} Action object. @@ -331,11 +331,17 @@ export function insertBlocks( blocks, index, rootClientId ) { * Returns an action object used in signalling that the insertion point should * be shown. * + * @param {?string} rootClientId Optional root client ID of block list on + * which to insert. + * @param {?number} index Index at which block should be inserted. + * * @return {Object} Action object. */ -export function showInsertionPoint() { +export function showInsertionPoint( rootClientId, index ) { return { type: 'SHOW_INSERTION_POINT', + rootClientId, + index, }; } diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 306451865409e..fc6a1e387d931 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -710,21 +710,23 @@ export function blocksMode( state = {}, action ) { } /** - * Reducer returning the block insertion point visibility, a boolean value - * reflecting whether the insertion point should be shown. + * Reducer returning the block insertion point visibility, either null if there + * is not an explicit insertion point assigned, or an object of its `index` and + * `rootClientId`. * * @param {Object} state Current state. * @param {Object} action Dispatched action. * * @return {Object} Updated state. */ -export function isInsertionPointVisible( state = false, action ) { +export function insertionPoint( state = null, action ) { switch ( action.type ) { case 'SHOW_INSERTION_POINT': - return true; + const { rootClientId, index } = action; + return { rootClientId, index }; case 'HIDE_INSERTION_POINT': - return false; + return null; } return state; @@ -1100,7 +1102,7 @@ export default optimist( combineReducers( { blockSelection, blocksMode, blockListSettings, - isInsertionPointVisible, + insertionPoint, preferences, saving, postLock, diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index ed734f7e5dcbf..79cd4ca0fc8a0 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -1016,10 +1016,10 @@ export function getLastMultiSelectedBlockClientId( state ) { * specified client ID is the first block of the multi-selection set, or false * otherwise. * - * @param {Object} state Editor state. - * @param {string} clientId Block client ID. + * @param {Object} state Editor state. + * @param {string} clientId Block client ID. * - * @return {boolean} Whether block is first in mult-selection. + * @return {boolean} Whether block is first in multi-selection. */ export function isFirstMultiSelectedBlock( state, clientId ) { return getFirstMultiSelectedBlockClientId( state ) === clientId; @@ -1277,7 +1277,12 @@ export function isCaretWithinFormattedText( state ) { export function getBlockInsertionPoint( state ) { let rootClientId, index; - const { end } = state.blockSelection; + const { insertionPoint, blockSelection } = state; + if ( insertionPoint !== null ) { + return insertionPoint; + } + + const { end } = blockSelection; if ( end ) { rootClientId = getBlockRootClientId( state, end ) || undefined; index = getBlockIndex( state, end, rootClientId ) + 1; @@ -1296,7 +1301,7 @@ export function getBlockInsertionPoint( state ) { * @return {?boolean} Whether the insertion point is visible or not. */ export function isBlockInsertionPointVisible( state ) { - return state.isInsertionPointVisible; + return state.insertionPoint !== null; } /** diff --git a/packages/editor/src/store/test/reducer.js b/packages/editor/src/store/test/reducer.js index c0462df96f411..8ab789e987077 100644 --- a/packages/editor/src/store/test/reducer.js +++ b/packages/editor/src/store/test/reducer.js @@ -30,7 +30,7 @@ import { preferences, saving, blocksMode, - isInsertionPointVisible, + insertionPoint, reusableBlocks, template, blockListSettings, @@ -1274,27 +1274,36 @@ describe( 'state', () => { } ); } ); - describe( 'isInsertionPointVisible', () => { - it( 'should default to false', () => { - const state = isInsertionPointVisible( undefined, {} ); + describe( 'insertionPoint', () => { + it( 'should default to null', () => { + const state = insertionPoint( undefined, {} ); - expect( state ).toBe( false ); + expect( state ).toBe( null ); } ); - it( 'should set insertion point visible', () => { - const state = isInsertionPointVisible( false, { + it( 'should set insertion point', () => { + const state = insertionPoint( null, { type: 'SHOW_INSERTION_POINT', + rootClientId: 'clientId1', + index: 0, } ); - expect( state ).toBe( true ); + expect( state ).toEqual( { + rootClientId: 'clientId1', + index: 0, + } ); } ); it( 'should clear the insertion point', () => { - const state = isInsertionPointVisible( true, { + const original = deepFreeze( { + rootClientId: 'clientId1', + index: 0, + } ); + const state = insertionPoint( original, { type: 'HIDE_INSERTION_POINT', } ); - expect( state ).toBe( false ); + expect( state ).toBe( null ); } ); } ); diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index dcfb2016d369b..157bdb62a5926 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -2761,6 +2761,40 @@ describe( 'selectors', () => { } ); describe( 'getBlockInsertionPoint', () => { + it( 'should return the explicitly assigned insertion point', () => { + const state = { + currentPost: {}, + preferences: { mode: 'visual' }, + blockSelection: { + start: 'clientId2', + end: 'clientId2', + }, + editor: { + present: { + blocksByClientId: { + clientId1: { clientId: 'clientId1' }, + clientId2: { clientId: 'clientId2' }, + }, + blockOrder: { + '': [ 'clientId1' ], + clientId1: [ 'clientId2' ], + clientId2: [], + }, + edits: {}, + }, + }, + insertionPoint: { + rootClientId: undefined, + index: 0, + }, + }; + + expect( getBlockInsertionPoint( state ) ).toEqual( { + rootClientId: undefined, + index: 0, + } ); + } ); + it( 'should return an object for the selected block', () => { const state = { currentPost: {}, @@ -2781,7 +2815,7 @@ describe( 'selectors', () => { edits: {}, }, }, - isInsertionPointVisible: false, + insertionPoint: null, }; expect( getBlockInsertionPoint( state ) ).toEqual( { @@ -2812,7 +2846,7 @@ describe( 'selectors', () => { edits: {}, }, }, - isInsertionPointVisible: false, + insertionPoint: null, }; expect( getBlockInsertionPoint( state ) ).toEqual( { @@ -2843,7 +2877,7 @@ describe( 'selectors', () => { edits: {}, }, }, - isInsertionPointVisible: false, + insertionPoint: null, }; expect( getBlockInsertionPoint( state ) ).toEqual( { @@ -2874,7 +2908,7 @@ describe( 'selectors', () => { edits: {}, }, }, - isInsertionPointVisible: false, + insertionPoint: null, }; expect( getBlockInsertionPoint( state ) ).toEqual( { @@ -2885,9 +2919,20 @@ describe( 'selectors', () => { } ); describe( 'isBlockInsertionPointVisible', () => { - it( 'should return the value in state', () => { + it( 'should return false if no assigned insertion point', () => { + const state = { + insertionPoint: null, + }; + + expect( isBlockInsertionPointVisible( state ) ).toBe( false ); + } ); + + it( 'should return true if assigned insertion point', () => { const state = { - isInsertionPointVisible: true, + insertionPoint: { + rootClientId: undefined, + index: 5, + }, }; expect( isBlockInsertionPointVisible( state ) ).toBe( true ); diff --git a/test/e2e/specs/adding-blocks.test.js b/test/e2e/specs/adding-blocks.test.js index d4eaf27ad8e45..0c38f03fdbfa1 100644 --- a/test/e2e/specs/adding-blocks.test.js +++ b/test/e2e/specs/adding-blocks.test.js @@ -67,11 +67,20 @@ describe( 'adding blocks', () => { await page.click( '.editor-post-title__input' ); // Using the between inserter - const insertionPoint = await page.$( '[data-type="core/quote"] .editor-block-list__insertion-point-button' ); + const insertionPoint = await page.$( '[data-type="core/quote"] .editor-inserter__toggle' ); const rect = await insertionPoint.boundingBox(); await page.mouse.move( rect.x + ( rect.width / 2 ), rect.y + ( rect.height / 2 ), { steps: 10 } ); - await page.waitForSelector( '[data-type="core/quote"] .editor-block-list__insertion-point-button' ); - await page.click( '[data-type="core/quote"] .editor-block-list__insertion-point-button' ); + await page.waitForSelector( '[data-type="core/quote"] .editor-inserter__toggle' ); + await page.click( '[data-type="core/quote"] .editor-inserter__toggle' ); + // [TODO]: Search input should be focused immediately. It shouldn't be + // necessary to have `waitForFunction`. + await page.waitForFunction( () => ( + document.activeElement && + document.activeElement.classList.contains( 'editor-inserter__search' ) + ) ); + await page.keyboard.type( 'para' ); + await pressTimes( 'Tab', 3 ); + await page.keyboard.press( 'Enter' ); await page.keyboard.type( 'Second paragraph' ); // Switch to Text Mode to check HTML Output