Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RichText: set correct selection on split #19655

Draft
wants to merge 1 commit into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 9 additions & 47 deletions packages/block-editor/src/components/block-list/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,12 @@
* External dependencies
*/
import classnames from 'classnames';
import { first, last } from 'lodash';
import { animated } from 'react-spring/web.cjs';

/**
* WordPress dependencies
*/
import { useRef, useEffect, useLayoutEffect, useState, useContext } from '@wordpress/element';
import {
focus,
isTextField,
placeCaretAtHorizontalEdge,
} from '@wordpress/dom';
import { BACKSPACE, DELETE, ENTER } from '@wordpress/keycodes';
import {
getBlockType,
Expand Down Expand Up @@ -41,7 +35,6 @@ import BlockInvalidWarning from './block-invalid-warning';
import BlockCrashWarning from './block-crash-warning';
import BlockCrashBoundary from './block-crash-boundary';
import BlockHtml from './block-html';
import { isInsideRootBlock } from '../../utils/dom';
import useMovingAnimation from './moving-animation';
import { Context } from './root-container';

Expand Down Expand Up @@ -90,7 +83,6 @@ function BlockListBlock( {
index,
isValid,
attributes,
initialPosition,
wrapperProps,
setAttributes,
onReplace,
Expand Down Expand Up @@ -136,49 +128,19 @@ function BlockListBlock( {
const blockType = getBlockType( name );
const blockAriaLabel = useDebouncedAccessibleBlockLabel( blockType, attributes, index, moverDirection, 400 );

// Handing the focus of the block on creation and update

/**
* When a block becomes selected, transition focus to an inner tabbable.
*
* @param {boolean} ignoreInnerBlocks Should not focus inner blocks.
*/
const focusTabbable = ( ignoreInnerBlocks ) => {
// Focus is captured by the wrapper node, so while focus transition
// should only consider tabbables within editable display, since it
// may be the wrapper itself or a side control which triggered the
// focus event, don't unnecessary transition to an inner tabbable.
if ( wrapper.current.contains( document.activeElement ) ) {
return;
}

// Find all tabbables within node.
const textInputs = focus.tabbable
.find( wrapper.current )
.filter( isTextField )
// Exclude inner blocks
.filter( ( node ) => ! ignoreInnerBlocks || isInsideRootBlock( wrapper.current, node ) );

// If reversed (e.g. merge via backspace), use the last in the set of
// tabbables.
const isReverse = -1 === initialPosition;
const target = ( isReverse ? last : first )( textInputs );

if ( ! target ) {
wrapper.current.focus();
return;
}

placeCaretAtHorizontalEdge( target, isReverse );
};

// Focus the selected block's wrapper or inner input on mount and update
const isMounting = useRef( true );

useEffect( () => {
if ( ! isMultiSelecting && ! isNavigationMode ) {
if ( isSelected ) {
focusTabbable( ! isMounting.current );
// Focus is captured by the wrapper node, so while focus transition
// should only consider tabbables within editable display, since it
// may be the wrapper itself or a side control which triggered the
// focus event, don't unnecessary transition to an inner tabbable.
if ( ! wrapper.current.contains( document.activeElement ) ) {
wrapper.current.focus();
}
} else if ( isFirstMultiSelected ) {
wrapper.current.focus();
}
Expand Down Expand Up @@ -477,14 +439,14 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, { select } ) => {
}
}
},
onReplace( blocks, indexToSelect ) {
onReplace( blocks, ...props ) {
if (
blocks.length &&
! isUnmodifiedDefaultBlock( blocks[ blocks.length - 1 ] )
) {
__unstableMarkLastChangeAsPersistent();
}
replaceBlocks( [ ownProps.clientId ], blocks, indexToSelect );
replaceBlocks( [ ownProps.clientId ], blocks, ...props );
},
toggleSelection( selectionEnabled ) {
toggleSelection( selectionEnabled );
Expand Down
22 changes: 17 additions & 5 deletions packages/block-editor/src/components/rich-text/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* External dependencies
*/
import classnames from 'classnames';
import { omit } from 'lodash';
import { omit, findKey } from 'lodash';

/**
* WordPress dependencies
Expand Down Expand Up @@ -244,22 +244,34 @@ class RichTextWrapper extends Component {
blocks.push( onSplitMiddle() );
}

let newAttributeKey;

// If there's pasted blocks, append a block with the content after the
// caret. Otherwise, do append and empty block if there is no
// `onSplitMiddle` prop, but if there is and the content is empty, the
// middle block is enough to set focus in.
if ( hasPastedBlocks || ! onSplitMiddle || ! isEmpty( after ) ) {
blocks.push( onSplit( toHTMLString( {
value: after,
const START_OF_SELECTED_AREA = '\u0086';
const afterBlock = onSplit( toHTMLString( {
value: insert( after, START_OF_SELECTED_AREA, 0, 0 ),
multilineTag,
} ) ) );
} ) );

newAttributeKey = findKey( afterBlock.attributes, ( v ) =>
typeof v === 'string' && v.indexOf( START_OF_SELECTED_AREA ) !== -1
);

afterBlock.attributes[ newAttributeKey ] =
afterBlock.attributes[ newAttributeKey ].replace( START_OF_SELECTED_AREA, '' );

blocks.push( afterBlock );
}

// If there are pasted blocks, set the selection to the last one.
// Otherwise, set the selection to the second block.
const indexToSelect = hasPastedBlocks ? blocks.length - 1 : 1;

onReplace( blocks, indexToSelect );
onReplace( blocks, indexToSelect, newAttributeKey, 0 );
}

inputRule( value, valueToFormat ) {
Expand Down
4 changes: 3 additions & 1 deletion packages/block-editor/src/store/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ function getBlocksWithDefaultStylesApplied( blocks, blockEditorSettings ) {
*
* @yield {Object} Action object.
*/
export function* replaceBlocks( clientIds, blocks, indexToSelect ) {
export function* replaceBlocks( clientIds, blocks, indexToSelect, attributeKey, offset ) {
clientIds = castArray( clientIds );
blocks = getBlocksWithDefaultStylesApplied(
castArray( blocks ),
Expand Down Expand Up @@ -319,6 +319,8 @@ export function* replaceBlocks( clientIds, blocks, indexToSelect ) {
blocks,
time: Date.now(),
indexToSelect,
attributeKey,
offset,
};
yield* ensureDefaultBlock();
}
Expand Down
6 changes: 5 additions & 1 deletion packages/block-editor/src/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1000,7 +1000,11 @@ function selection( state = {}, action ) {
return state;
}

return { clientId: blockToSelect.clientId };
return {
clientId: blockToSelect.clientId,
attributeKey: action.attributeKey,
offset: action.offset,
};
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/rich-text/src/component/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ class RichText extends Component {
}

componentDidMount() {
this.applyRecord( this.record, { domOnly: true } );
this.applyRecord( this.record );
}

createRecord() {
Expand Down