diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 3c8ee800d31aa4..3a0a88048e982f 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -51,7 +51,7 @@ Prompt visitors to take action with a button-style link. ([Source](https://githu - **Name:** core/button - **Category:** design - **Parent:** core/buttons -- **Supports:** anchor, color (background, gradients, text), interactivity (clientNavigation), shadow (), spacing (padding), typography (fontSize, lineHeight), ~~alignWide~~, ~~align~~, ~~reusable~~ +- **Supports:** anchor, color (background, gradients, text), interactivity (clientNavigation), shadow (), spacing (padding), splitting, typography (fontSize, lineHeight), ~~alignWide~~, ~~align~~, ~~reusable~~ - **Attributes:** backgroundColor, gradient, linkTarget, placeholder, rel, tagName, text, textAlign, textColor, title, type, url, width ## Buttons @@ -359,7 +359,7 @@ Introduce new sections and organize content to help visitors (and search engines - **Name:** core/heading - **Category:** text -- **Supports:** __unstablePasteTextInline, align (full, wide), anchor, className, color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight) +- **Supports:** __unstablePasteTextInline, align (full, wide), anchor, className, color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), splitting, typography (fontSize, lineHeight) - **Attributes:** content, level, placeholder, textAlign ## Home Link @@ -426,7 +426,7 @@ Create a list item. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/ - **Category:** text - **Parent:** core/list - **Allowed Blocks:** core/list -- **Supports:** interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~className~~ +- **Supports:** interactivity (clientNavigation), spacing (margin, padding), splitting, typography (fontSize, lineHeight), ~~className~~ - **Attributes:** content, placeholder ## Login/out @@ -531,7 +531,7 @@ Start with the basic building block of all narrative. ([Source](https://github.c - **Name:** core/paragraph - **Category:** text -- **Supports:** __unstablePasteTextInline, anchor, color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~className~~ +- **Supports:** __unstablePasteTextInline, anchor, color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), splitting, typography (fontSize, lineHeight), ~~className~~ - **Attributes:** align, content, direction, dropCap, placeholder ## Pattern placeholder diff --git a/packages/block-editor/src/components/editable-text/README.md b/packages/block-editor/src/components/editable-text/README.md index aa5a2f4b1962b8..fda19ba52b1144 100644 --- a/packages/block-editor/src/components/editable-text/README.md +++ b/packages/block-editor/src/components/editable-text/README.md @@ -25,10 +25,6 @@ _Optional._ `Text` won't insert line breaks on `Enter` if set to `true`. _Optional._ Placeholder text to show when the field is empty, similar to the [`input` and `textarea` attribute of the same name](https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/HTML5_updates#The_placeholder_attribute). -### `onSplit( value: String ): Function` - -_Optional._ Called when the content can be split, where `value` is a piece of content being split off. Here you should create a new block with that content and return it. Note that you also need to provide `onReplace` in order for this to take any effect. - ### `onReplace( blocks: Array ): Function` _Optional._ Called when the `Text` instance can be replaced with the given blocks. diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index e90e8a094ba5ac..991830ed868c3e 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -352,7 +352,15 @@ function Iframe( { event.currentTarget.ownerDocument !== event.target.ownerDocument ) { + // We should only stop propagation of the React event, + // the native event should further bubble inside the + // iframe to the document and window. + // Alternatively, we could consider redispatching the + // native event in the iframe. + const { stopPropagation } = event.nativeEvent; + event.nativeEvent.stopPropagation = () => {}; event.stopPropagation(); + event.nativeEvent.stopPropagation = stopPropagation; bubbleEvent( event, window.KeyboardEvent, diff --git a/packages/block-editor/src/components/rich-text/README.md b/packages/block-editor/src/components/rich-text/README.md index a4c9b932e87905..bea07cdc5c065c 100644 --- a/packages/block-editor/src/components/rich-text/README.md +++ b/packages/block-editor/src/components/rich-text/README.md @@ -33,10 +33,6 @@ _Optional._ Disables inserting line breaks on `Enter` when it is set to `true` _Optional._ By default, a line break will be inserted on Enter. If the editable field can contain multiple paragraphs, this property can be set to create new paragraphs on Enter. -### `onSplit( value: String ): Function` - -_Optional._ Called when the content can be split, where `value` is a piece of content being split off. Here you should create a new block with that content and return it. Note that you also need to provide `onReplace` in order for this to take any effect. - ### `onReplace( blocks: Array ): Function` _Optional._ Called when the `RichText` instance can be replaced with the given blocks. diff --git a/packages/block-editor/src/components/rich-text/event-listeners/enter.js b/packages/block-editor/src/components/rich-text/event-listeners/enter.js index 8bff9b8f170cc1..63c2145fe7acbc 100644 --- a/packages/block-editor/src/components/rich-text/event-listeners/enter.js +++ b/packages/block-editor/src/components/rich-text/event-listeners/enter.js @@ -3,33 +3,37 @@ */ import { ENTER } from '@wordpress/keycodes'; import { insert, remove } from '@wordpress/rich-text'; -import { getBlockTransforms, findTransform } from '@wordpress/blocks'; - -/** - * Internal dependencies - */ -import { store as blockEditorStore } from '../../../store'; -import { splitValue } from '../split-value'; export default ( props ) => ( element ) => { - function onKeyDown( event ) { - if ( event.target.contentEditable !== 'true' ) { + function onKeyDownDeprecated( event ) { + if ( event.keyCode !== ENTER ) { return; } + const { onReplace, onSplit } = props.current; + + if ( onReplace && onSplit ) { + event.__deprecatedOnSplit = true; + } + } + + function onKeyDown( event ) { if ( event.defaultPrevented ) { return; } + // The event listener is attached to the window, so we need to check if + // the target is the element. + if ( event.target !== element ) { + return; + } + if ( event.keyCode !== ENTER ) { return; } const { - removeEditorOnlyFormats, value, - onReplace, - onSplit, onChange, disableLineBreaks, onSplitAtEnd, @@ -39,43 +43,12 @@ export default ( props ) => ( element ) => { event.preventDefault(); - const _value = { ...value }; - _value.formats = removeEditorOnlyFormats( value ); - const canSplit = onReplace && onSplit; - - if ( onReplace ) { - const transforms = getBlockTransforms( 'from' ).filter( - ( { type } ) => type === 'enter' - ); - const transformation = findTransform( transforms, ( item ) => { - return item.regExp.test( _value.text ); - } ); - - if ( transformation ) { - onReplace( [ - transformation.transform( { - content: _value.text, - } ), - ] ); - registry - .dispatch( blockEditorStore ) - .__unstableMarkAutomaticChange(); - return; - } - } - - const { text, start, end } = _value; + const { text, start, end } = value; if ( event.shiftKey ) { if ( ! disableLineBreaks ) { - onChange( insert( _value, '\n' ) ); + onChange( insert( value, '\n' ) ); } - } else if ( canSplit ) { - splitValue( { - value: _value, - onReplace, - onSplit, - } ); } else if ( onSplitAtEnd && start === end && end === text.length ) { onSplitAtEnd(); } else if ( @@ -88,17 +61,24 @@ export default ( props ) => ( element ) => { text.slice( -2 ) === '\n\n' ) { registry.batch( () => { + const _value = { ...value }; _value.start = _value.end - 2; onChange( remove( _value ) ); onSplitAtDoubleLineEnd(); } ); } else if ( ! disableLineBreaks ) { - onChange( insert( _value, '\n' ) ); + onChange( insert( value, '\n' ) ); } } - element.addEventListener( 'keydown', onKeyDown ); + const { defaultView } = element.ownerDocument; + + // Attach the listener to the window so parent elements have the chance to + // prevent the default behavior. + defaultView.addEventListener( 'keydown', onKeyDown ); + element.addEventListener( 'keydown', onKeyDownDeprecated ); return () => { - element.removeEventListener( 'keydown', onKeyDown ); + defaultView.removeEventListener( 'keydown', onKeyDown ); + element.removeEventListener( 'keydown', onKeyDownDeprecated ); }; }; diff --git a/packages/block-editor/src/components/rich-text/event-listeners/paste-handler.js b/packages/block-editor/src/components/rich-text/event-listeners/paste-handler.js index 04b7309441f1b3..59633f4750ff92 100644 --- a/packages/block-editor/src/components/rich-text/event-listeners/paste-handler.js +++ b/packages/block-editor/src/components/rich-text/event-listeners/paste-handler.js @@ -1,11 +1,7 @@ /** * WordPress dependencies */ -import { - pasteHandler, - findTransform, - getBlockTransforms, -} from '@wordpress/blocks'; +import { pasteHandler } from '@wordpress/blocks'; import { isEmpty, insert, create } from '@wordpress/rich-text'; import { isURL } from '@wordpress/url'; @@ -13,7 +9,6 @@ import { isURL } from '@wordpress/url'; * Internal dependencies */ import { addActiveFormats } from '../utils'; -import { splitValue } from '../split-value'; import { getPasteEventData } from '../../../utils/pasting'; /** @typedef {import('@wordpress/rich-text').RichTextValue} RichTextValue */ @@ -27,12 +22,22 @@ export default ( props ) => ( element ) => { formatTypes, tagName, onReplace, - onSplit, __unstableEmbedURLOnPaste, + preserveWhiteSpace, pastePlainText, } = props.current; - const { plainText, html, files } = getPasteEventData( event ); + // The event listener is attached to the window, so we need to check if + // the target is the element. + if ( event.target !== element ) { + return; + } + + if ( event.defaultPrevented ) { + return; + } + + const { plainText, html } = getPasteEventData( event ); event.preventDefault(); @@ -85,47 +90,7 @@ export default ( props ) => ( element ) => { return; } - if ( files?.length ) { - // Allows us to ask for this information when we get a report. - // eslint-disable-next-line no-console - window.console.log( 'Received items:\n\n', files ); - - const fromTransforms = getBlockTransforms( 'from' ); - const blocks = files - .reduce( ( accumulator, file ) => { - const transformation = findTransform( - fromTransforms, - ( transform ) => - transform.type === 'files' && - transform.isMatch( [ file ] ) - ); - if ( transformation ) { - accumulator.push( - transformation.transform( [ file ] ) - ); - } - return accumulator; - }, [] ) - .flat(); - if ( ! blocks.length ) { - return; - } - - if ( onReplace && isEmpty( value ) ) { - onReplace( blocks ); - } else { - splitValue( { - value, - pastedBlocks: blocks, - onReplace, - onSplit, - } ); - } - - return; - } - - let mode = onReplace && onSplit ? 'AUTO' : 'INLINE'; + let mode = 'INLINE'; const trimmedPlainText = plainText.trim(); @@ -144,6 +109,7 @@ export default ( props ) => ( element ) => { plainText, mode, tagName, + preserveWhiteSpace, } ); if ( typeof content === 'string' ) { @@ -151,19 +117,16 @@ export default ( props ) => ( element ) => { } else if ( content.length > 0 ) { if ( onReplace && isEmpty( value ) ) { onReplace( content, content.length - 1, -1 ); - } else { - splitValue( { - value, - pastedBlocks: content, - onReplace, - onSplit, - } ); } } } - element.addEventListener( 'paste', _onPaste ); + const { defaultView } = element.ownerDocument; + + // Attach the listener to the window so parent elements have the chance to + // prevent the default behavior. + defaultView.addEventListener( 'paste', _onPaste ); return () => { - element.removeEventListener( 'paste', _onPaste ); + defaultView.removeEventListener( 'paste', _onPaste ); }; }; diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 8871f5eeafef85..670c32975dc43b 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -20,6 +20,7 @@ import { } from '@wordpress/rich-text'; import { Popover } from '@wordpress/components'; import { getBlockType, store as blocksStore } from '@wordpress/blocks'; +import deprecated from '@wordpress/deprecated'; /** * Internal dependencies @@ -108,8 +109,14 @@ export function RichTextWrapper( ) { props = removeNativeProps( props ); - const instanceId = useInstanceId( RichTextWrapper ); + if ( onSplit ) { + deprecated( 'wp.blockEditor.RichText onSplit prop', { + since: '6.4', + alternative: 'block.json support key: "splitting"', + } ); + } + const instanceId = useInstanceId( RichTextWrapper ); const anchorRef = useRef(); const context = useBlockEditContext(); const { clientId, isSelected: isBlockSelected, name: blockName } = context; diff --git a/packages/block-editor/src/components/rich-text/multiline.js b/packages/block-editor/src/components/rich-text/multiline.js index 10d318b741600f..1fa77b25a4db9c 100644 --- a/packages/block-editor/src/components/rich-text/multiline.js +++ b/packages/block-editor/src/components/rich-text/multiline.js @@ -3,7 +3,9 @@ */ import { forwardRef } from '@wordpress/element'; import deprecated from '@wordpress/deprecated'; -import { useDispatch } from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { ENTER } from '@wordpress/keycodes'; +import { create, split, toHTMLString } from '@wordpress/rich-text'; /** * Internal dependencies @@ -33,6 +35,8 @@ function RichTextMultiline( } ); const { clientId } = useBlockEditContext(); + const { getSelectionStart, getSelectionEnd } = + useSelect( blockEditorStore ); const { selectionChange } = useDispatch( blockEditorStore ); const multilineTagName = getMultilineTag( multiline ); @@ -68,8 +72,32 @@ function RichTextMultiline( _onChange( newValues ); } } isSelected={ undefined } - onSplit={ ( v ) => v } - onReplace={ ( array ) => { + onKeyDown={ ( event ) => { + if ( event.keyCode !== ENTER ) { + return; + } + + event.preventDefault(); + + const { offset: start } = getSelectionStart(); + const { offset: end } = getSelectionEnd(); + + // Cannot split if there is no selection. + if ( + typeof start !== 'number' || + typeof end !== 'number' + ) { + return; + } + + const richTextValue = create( { html: _value } ); + richTextValue.start = start; + richTextValue.end = end; + + const array = split( richTextValue ).map( ( v ) => + toHTMLString( { value: v } ) + ); + const newValues = values.slice(); newValues.splice( index, 1, ...array ); _onChange( newValues ); diff --git a/packages/block-editor/src/components/rich-text/split-value.js b/packages/block-editor/src/components/rich-text/split-value.js deleted file mode 100644 index 17f54d9c9edd01..00000000000000 --- a/packages/block-editor/src/components/rich-text/split-value.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * WordPress dependencies - */ -import { isEmpty, split, toHTMLString } from '@wordpress/rich-text'; - -/* - * Signals to the RichText owner that the block can be replaced with two blocks - * as a result of splitting the block by pressing enter, or with blocks as a - * result of splitting the block by pasting block content in the instance. - */ -export function splitValue( { value, pastedBlocks = [], onReplace, onSplit } ) { - if ( ! onReplace || ! onSplit ) { - return; - } - - // Ensure the value has a selection. This might happen when trying to split - // an empty value before there was a `selectionchange` event. - const { start = 0, end = 0 } = value; - const valueWithEnsuredSelection = { ...value, start, end }; - const blocks = []; - const [ before, after ] = split( valueWithEnsuredSelection ); - const hasPastedBlocks = pastedBlocks.length > 0; - let lastPastedBlockIndex = -1; - - // Consider the after value to be the original it is not empty and the - // before value *is* empty. - const isAfterOriginal = isEmpty( before ) && ! isEmpty( after ); - - // Create a block with the content before the caret if there's no pasted - // blocks, or if there are pasted blocks and the value is not empty. We do - // not want a leading empty block on paste, but we do if we split with e.g. - // the enter key. - if ( ! hasPastedBlocks || ! isEmpty( before ) ) { - blocks.push( - onSplit( toHTMLString( { value: before } ), ! isAfterOriginal ) - ); - lastPastedBlockIndex += 1; - } - - if ( hasPastedBlocks ) { - blocks.push( ...pastedBlocks ); - lastPastedBlockIndex += pastedBlocks.length; - } - - // Create a block with the content after the caret if there's no pasted - // blocks, or if there are pasted blocks and the value is not empty. We do - // not want a trailing empty block on paste, but we do if we split with e.g. - // the enter key. - if ( ! hasPastedBlocks || ! isEmpty( after ) ) { - blocks.push( - onSplit( toHTMLString( { value: after } ), isAfterOriginal ) - ); - } - - // If there are pasted blocks, set the selection to the last one. Otherwise, - // set the selection to the second block. - const indexToSelect = hasPastedBlocks ? lastPastedBlockIndex : 1; - - // If there are pasted blocks, move the caret to the end of the selected - // block Otherwise, retain the default value. - const initialPosition = hasPastedBlocks ? -1 : 0; - - onReplace( blocks, indexToSelect, initialPosition ); -} diff --git a/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js b/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js index 43e887888dbd13..a6d5c61b8b5c8f 100644 --- a/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js +++ b/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js @@ -1,6 +1,12 @@ /** * WordPress dependencies */ +import { + pasteHandler, + findTransform, + getBlockTransforms, + hasBlockSupport, +} from '@wordpress/blocks'; import { documentHasSelection, documentHasUncollapsedSelection, @@ -13,7 +19,8 @@ import { useRefEffect } from '@wordpress/compose'; */ import { store as blockEditorStore } from '../../store'; import { useNotifyCopy } from '../../utils/use-notify-copy'; -import { getPasteBlocks, setClipboardBlocks } from './utils'; +import { setClipboardBlocks } from './utils'; +import { getPasteEventData } from '../../utils/pasting'; export default function useClipboardHandler() { const registry = useRegistry(); @@ -22,11 +29,13 @@ export default function useClipboardHandler() { getSelectedBlockClientIds, hasMultiSelection, getSettings, + getBlockName, __unstableIsFullySelected, __unstableIsSelectionCollapsed, __unstableIsSelectionMergeable, __unstableGetSelectedBlocksWithPartialSelection, canInsertBlockType, + getBlockRootClientId, } = useSelect( blockEditorStore ); const { flashBlock, @@ -34,7 +43,7 @@ export default function useClipboardHandler() { replaceBlocks, __unstableDeleteSelection, __unstableExpandSelection, - insertBlocks, + __unstableSplitSelection, } = useDispatch( blockEditorStore ); const notifyCopy = useNotifyCopy(); @@ -51,35 +60,36 @@ export default function useClipboardHandler() { return; } - // Always handle multiple selected blocks. - if ( ! hasMultiSelection() ) { - const { target } = event; - const { ownerDocument } = target; - // If copying, only consider actual text selection as selection. - // Otherwise, any focus on an input field is considered. - const hasSelection = - event.type === 'copy' || event.type === 'cut' - ? documentHasUncollapsedSelection( ownerDocument ) - : documentHasSelection( ownerDocument ); - - // Let native copy behaviour take over in input fields. - if ( hasSelection ) { - return; - } - } + const { activeElement } = event.target.ownerDocument; - if ( ! node.contains( event.target.ownerDocument.activeElement ) ) { + if ( ! node.contains( activeElement ) ) { return; } - event.preventDefault(); - const isSelectionMergeable = __unstableIsSelectionMergeable(); const shouldHandleWholeBlocks = __unstableIsSelectionCollapsed() || __unstableIsFullySelected(); const expandSelectionIsNeeded = ! shouldHandleWholeBlocks && ! isSelectionMergeable; if ( event.type === 'copy' || event.type === 'cut' ) { + if ( ! hasMultiSelection() ) { + const { target } = event; + const { ownerDocument } = target; + // If copying, only consider actual text selection as selection. + // Otherwise, any focus on an input field is considered. + const hasSelection = + event.type === 'copy' || event.type === 'cut' + ? documentHasUncollapsedSelection( ownerDocument ) + : documentHasSelection( ownerDocument ); + + // Let native copy behaviour take over in input fields. + if ( hasSelection ) { + return; + } + } + + event.preventDefault(); + if ( selectedBlockClientIds.length === 1 ) { flashBlock( selectedBlockClientIds[ 0 ] ); } @@ -124,37 +134,87 @@ export default function useClipboardHandler() { __experimentalCanUserUseUnfilteredHTML: canUserUseUnfilteredHTML, } = getSettings(); - const blocks = getPasteBlocks( - event, - canUserUseUnfilteredHTML - ); + const isInternal = + event.clipboardData.getData( 'rich-text' ) === 'true'; + if ( isInternal ) { + return; + } + const { plainText, html, files } = getPasteEventData( event ); + const isFullySelected = __unstableIsFullySelected(); + let blocks = []; + + if ( files.length ) { + const fromTransforms = getBlockTransforms( 'from' ); + blocks = files + .reduce( ( accumulator, file ) => { + const transformation = findTransform( + fromTransforms, + ( transform ) => + transform.type === 'files' && + transform.isMatch( [ file ] ) + ); + if ( transformation ) { + accumulator.push( + transformation.transform( [ file ] ) + ); + } + return accumulator; + }, [] ) + .flat(); + } else { + blocks = pasteHandler( { + HTML: html, + plainText, + mode: isFullySelected ? 'BLOCKS' : 'AUTO', + canUserUseUnfilteredHTML, + } ); + } - if ( selectedBlockClientIds.length === 1 ) { - const [ selectedBlockClientId ] = selectedBlockClientIds; + // Inline paste: let rich text handle it. + if ( typeof blocks === 'string' ) { + return; + } - if ( - blocks.every( ( block ) => - canInsertBlockType( - block.name, - selectedBlockClientId - ) - ) - ) { - insertBlocks( - blocks, - undefined, - selectedBlockClientId - ); - return; - } + if ( isFullySelected ) { + replaceBlocks( + selectedBlockClientIds, + blocks, + blocks.length - 1, + -1 + ); + event.preventDefault(); + return; } - replaceBlocks( - selectedBlockClientIds, - blocks, - blocks.length - 1, - -1 + // If a block doesn't support splitting, let rich text paste + // inline. + if ( + ! hasMultiSelection() && + ! hasBlockSupport( + getBlockName( selectedBlockClientIds[ 0 ] ), + 'splitting', + false + ) && + ! event.__deprecatedOnSplit + ) { + return; + } + + const [ firstSelectedClientId ] = selectedBlockClientIds; + const rootClientId = getBlockRootClientId( + firstSelectedClientId ); + + if ( + ! blocks.every( ( block ) => + canInsertBlockType( block.name, rootClientId ) + ) + ) { + return; + } + + __unstableSplitSelection( blocks ); + event.preventDefault(); } } diff --git a/packages/block-editor/src/components/writing-flow/use-input.js b/packages/block-editor/src/components/writing-flow/use-input.js index f9baf216885ecf..0f10cc9c2d1c75 100644 --- a/packages/block-editor/src/components/writing-flow/use-input.js +++ b/packages/block-editor/src/components/writing-flow/use-input.js @@ -4,7 +4,13 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { useRefEffect } from '@wordpress/compose'; import { ENTER, BACKSPACE, DELETE } from '@wordpress/keycodes'; -import { createBlock, getDefaultBlockName } from '@wordpress/blocks'; +import { + createBlock, + getDefaultBlockName, + hasBlockSupport, + getBlockTransforms, + findTransform, +} from '@wordpress/blocks'; /** * Internal dependencies @@ -18,8 +24,15 @@ export default function useInput() { const { __unstableIsFullySelected, getSelectedBlockClientIds, + getSelectedBlockClientId, __unstableIsSelectionMergeable, hasMultiSelection, + getBlockName, + canInsertBlockType, + getBlockRootClientId, + getSelectionStart, + getSelectionEnd, + getBlockAttributes, } = useSelect( blockEditorStore ); const { replaceBlocks, @@ -27,6 +40,7 @@ export default function useInput() { removeBlocks, __unstableDeleteSelection, __unstableExpandSelection, + __unstableMarkAutomaticChange, } = useDispatch( blockEditorStore ); return useRefEffect( ( node ) => { @@ -45,6 +59,66 @@ export default function useInput() { } if ( ! hasMultiSelection() ) { + if ( event.keyCode === ENTER ) { + if ( event.shiftKey || __unstableIsFullySelected() ) { + return; + } + + const clientId = getSelectedBlockClientId(); + const blockName = getBlockName( clientId ); + const selectionStart = getSelectionStart(); + const selectionEnd = getSelectionEnd(); + + if ( + selectionStart.attributeKey === + selectionEnd.attributeKey + ) { + const selectedAttributeValue = + getBlockAttributes( clientId )[ + selectionStart.attributeKey + ]; + const transforms = getBlockTransforms( 'from' ).filter( + ( { type } ) => type === 'enter' + ); + const transformation = findTransform( + transforms, + ( item ) => { + return item.regExp.test( + selectedAttributeValue + ); + } + ); + + if ( transformation ) { + replaceBlocks( + clientId, + transformation.transform( { + content: selectedAttributeValue, + } ) + ); + __unstableMarkAutomaticChange(); + return; + } + } + + if ( + ! hasBlockSupport( blockName, 'splitting', false ) && + ! event.__deprecatedOnSplit + ) { + return; + } + + // Ensure template is not locked. + if ( + canInsertBlockType( + blockName, + getBlockRootClientId( clientId ) + ) + ) { + __unstableSplitSelection(); + event.preventDefault(); + } + } return; } diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index bcc753253e8e7e..61acae16fcca2d 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -14,6 +14,7 @@ import { synchronizeBlocksWithTemplate, getBlockSupport, isUnmodifiedDefaultBlock, + isUnmodifiedBlock, } from '@wordpress/blocks'; import { speak } from '@wordpress/a11y'; import { __, _n, sprintf } from '@wordpress/i18n'; @@ -25,6 +26,7 @@ import deprecated from '@wordpress/deprecated'; */ import { retrieveSelectedAttribute, + findRichTextAttributeKey, START_OF_SELECTED_AREA, } from '../utils/selection'; import { @@ -821,27 +823,13 @@ export const __unstableDeleteSelection = /** * Split the current selection. + * @param {?Array} blocks */ export const __unstableSplitSelection = - () => - ( { select, dispatch } ) => { + ( blocks = [] ) => + ( { registry, select, dispatch } ) => { const selectionAnchor = select.getSelectionStart(); const selectionFocus = select.getSelectionEnd(); - - if ( selectionAnchor.clientId === selectionFocus.clientId ) { - return; - } - - // Can't split if the selection is not set. - if ( - ! selectionAnchor.attributeKey || - ! selectionFocus.attributeKey || - typeof selectionAnchor.offset === 'undefined' || - typeof selectionFocus.offset === 'undefined' - ) { - return; - } - const anchorRootClientId = select.getBlockRootClientId( selectionAnchor.clientId ); @@ -872,12 +860,91 @@ export const __unstableSplitSelection = const selectionA = selectionStart; const selectionB = selectionEnd; - const blockA = select.getBlock( selectionA.clientId ); const blockB = select.getBlock( selectionB.clientId ); + const blockAType = getBlockType( blockA.name ); + const blockBType = getBlockType( blockB.name ); + const attributeKeyA = + typeof selectionA.attributeKey === 'string' + ? selectionA.attributeKey + : findRichTextAttributeKey( blockAType ); + const attributeKeyB = + typeof selectionB.attributeKey === 'string' + ? selectionB.attributeKey + : findRichTextAttributeKey( blockBType ); - const htmlA = blockA.attributes[ selectionA.attributeKey ]; - const htmlB = blockB.attributes[ selectionB.attributeKey ]; + // Can't split if the selection is not set. + if ( + ! attributeKeyA || + ! attributeKeyB || + typeof selectionAnchor.offset === 'undefined' || + typeof selectionFocus.offset === 'undefined' + ) { + return; + } + + // We can do some short-circuiting if the selection is collapsed. + if ( + selectionA.clientId === selectionB.clientId && + attributeKeyA === attributeKeyB && + selectionA.offset === selectionB.offset + ) { + // If an unmodified default block is selected, replace it. We don't + // want to be converting into a default block. + if ( blocks.length ) { + if ( isUnmodifiedDefaultBlock( blockA ) ) { + dispatch.replaceBlocks( + [ selectionA.clientId ], + blocks, + blocks.length - 1, + -1 + ); + return; + } + } + + // If selection is at the start or end, we can simply insert an + // empty block, provided this block has no inner blocks. + else if ( ! select.getBlockOrder( selectionA.clientId ).length ) { + function createEmpty() { + const defaultBlockName = getDefaultBlockName(); + return select.canInsertBlockType( + defaultBlockName, + anchorRootClientId + ) + ? createBlock( defaultBlockName ) + : createBlock( + select.getBlockName( selectionA.clientId ) + ); + } + + const length = select.getBlockAttributes( selectionA.clientId )[ + attributeKeyA + ].length; + + if ( selectionA.offset === 0 && length ) { + dispatch.insertBlocks( + [ createEmpty() ], + select.getBlockIndex( selectionA.clientId ), + anchorRootClientId, + false + ); + return; + } + + if ( selectionA.offset === length ) { + dispatch.insertBlocks( + [ createEmpty() ], + select.getBlockIndex( selectionA.clientId ) + 1, + anchorRootClientId + ); + return; + } + } + } + + const htmlA = blockA.attributes[ attributeKeyA ]; + const htmlB = blockB.attributes[ attributeKeyB ]; let valueA = create( { html: htmlA } ); let valueB = create( { html: htmlB } ); @@ -885,28 +952,127 @@ export const __unstableSplitSelection = valueA = remove( valueA, selectionA.offset, valueA.text.length ); valueB = remove( valueB, 0, selectionB.offset ); - dispatch.replaceBlocks( select.getSelectedBlockClientIds(), [ - { - // Preserve the original client ID. - ...blockA, - attributes: { - ...blockA.attributes, - [ selectionA.attributeKey ]: toHTMLString( { - value: valueA, - } ), - }, + let head = { + // Preserve the original client ID. + ...blockA, + // If both start and end are the same, should only copy innerBlocks + // once. + innerBlocks: + blockA.clientId === blockB.clientId ? [] : blockA.innerBlocks, + attributes: { + ...blockA.attributes, + [ attributeKeyA ]: toHTMLString( { value: valueA } ), }, - { - // Preserve the original client ID. - ...blockB, - attributes: { - ...blockB.attributes, - [ selectionB.attributeKey ]: toHTMLString( { - value: valueB, - } ), - }, + }; + + const tail = { + ...blockB, + // Only preserve the original client ID if the end is different. + clientId: + blockA.clientId === blockB.clientId + ? createBlock( blockB.name ).clientId + : blockB.clientId, + attributes: { + ...blockB.attributes, + [ attributeKeyB ]: toHTMLString( { value: valueB } ), }, - ] ); + }; + + if ( ! blocks.length ) { + dispatch.replaceBlocks( select.getSelectedBlockClientIds(), [ + head, + tail, + ] ); + return; + } + + let selection; + const output = []; + const clonedBlocks = [ ...blocks ]; + const firstBlock = clonedBlocks.shift(); + const headType = getBlockType( head.name ); + const firstBlocks = + headType.merge && firstBlock.name === headType.name + ? [ firstBlock ] + : switchToBlockType( firstBlock, headType.name ); + + if ( firstBlocks?.length ) { + const first = firstBlocks.shift(); + head = { + ...head, + attributes: headType.merge( head.attributes, first.attributes ), + }; + output.push( head ); + selection = { + clientId: head.clientId, + attributeKey: attributeKeyA, + offset: create( { html: head.attributes[ attributeKeyA ] } ) + .text.length, + }; + clonedBlocks.unshift( ...firstBlocks ); + } else { + if ( ! isUnmodifiedBlock( head ) ) { + output.push( head ); + } + output.push( firstBlock ); + } + + const lastBlock = clonedBlocks.pop(); + const tailType = getBlockType( tail.name ); + + if ( clonedBlocks.length ) { + output.push( ...clonedBlocks ); + } + + if ( lastBlock ) { + const lastBlocks = + tailType.merge && tailType.name === lastBlock.name + ? [ lastBlock ] + : switchToBlockType( lastBlock, tailType.name ); + + if ( lastBlocks?.length ) { + const last = lastBlocks.pop(); + output.push( { + ...tail, + attributes: tailType.merge( + last.attributes, + tail.attributes + ), + } ); + output.push( ...lastBlocks ); + selection = { + clientId: tail.clientId, + attributeKey: attributeKeyB, + offset: create( { + html: last.attributes[ attributeKeyB ], + } ).text.length, + }; + } else { + output.push( lastBlock ); + if ( ! isUnmodifiedBlock( tail ) ) { + output.push( tail ); + } + } + } else if ( ! isUnmodifiedBlock( tail ) ) { + output.push( tail ); + } + + registry.batch( () => { + dispatch.replaceBlocks( + select.getSelectedBlockClientIds(), + output, + output.length - 1, + 0 + ); + if ( selection ) { + dispatch.selectionChange( + selection.clientId, + selection.attributeKey, + selection.offset, + selection.offset + ); + } + } ); }; /** diff --git a/packages/block-editor/src/utils/selection.js b/packages/block-editor/src/utils/selection.js index 4e971485838791..57aab8cc0f55dd 100644 --- a/packages/block-editor/src/utils/selection.js +++ b/packages/block-editor/src/utils/selection.js @@ -31,3 +31,11 @@ export function retrieveSelectedAttribute( blockAttributes ) { ); } ); } + +export function findRichTextAttributeKey( blockType ) { + for ( const [ key, value ] of Object.entries( blockType.attributes ) ) { + if ( value.source === 'rich-text' || value.source === 'html' ) { + return key; + } + } +} diff --git a/packages/block-library/src/button/block.json b/packages/block-library/src/button/block.json index ec9f042cf5bcf7..740f3e50f84eee 100644 --- a/packages/block-library/src/button/block.json +++ b/packages/block-library/src/button/block.json @@ -73,6 +73,7 @@ }, "supports": { "anchor": true, + "splitting": true, "align": false, "alignWide": false, "color": { diff --git a/packages/block-library/src/button/edit.js b/packages/block-library/src/button/edit.js index 24f82f1ba2f4f0..e9ae124c6a4390 100644 --- a/packages/block-library/src/button/edit.js +++ b/packages/block-library/src/button/edit.js @@ -293,12 +293,6 @@ function ButtonEdit( props ) { ...spacingProps.style, ...shadowProps.style, } } - onSplit={ ( value ) => - createBlock( 'core/button', { - ...attributes, - text: value, - } ) - } onReplace={ onReplace } onMerge={ mergeBlocks } identifier="text" diff --git a/packages/block-library/src/heading/block.json b/packages/block-library/src/heading/block.json index 9990ef582e2f43..39e985038c323a 100644 --- a/packages/block-library/src/heading/block.json +++ b/packages/block-library/src/heading/block.json @@ -29,6 +29,7 @@ "align": [ "wide", "full" ], "anchor": true, "className": true, + "splitting": true, "color": { "gradients": true, "link": true, diff --git a/packages/block-library/src/heading/edit.js b/packages/block-library/src/heading/edit.js index e0e36a831429bb..c9743c1325a51a 100644 --- a/packages/block-library/src/heading/edit.js +++ b/packages/block-library/src/heading/edit.js @@ -9,7 +9,6 @@ import classnames from 'classnames'; import { __ } from '@wordpress/i18n'; import { useEffect, Platform } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; -import { createBlock, getDefaultBlockName } from '@wordpress/blocks'; import { AlignmentControl, BlockControls, @@ -114,26 +113,6 @@ function HeadingEdit( { value={ content } onChange={ onContentChange } onMerge={ mergeBlocks } - onSplit={ ( value, isOriginal ) => { - let block; - - if ( isOriginal || value ) { - block = createBlock( 'core/heading', { - ...attributes, - content: value, - } ); - } else { - block = createBlock( - getDefaultBlockName() ?? 'core/heading' - ); - } - - if ( isOriginal ) { - block.clientId = clientId; - } - - return block; - } } onReplace={ onReplace } onRemove={ () => onReplace( [] ) } placeholder={ placeholder || __( 'Heading' ) } diff --git a/packages/block-library/src/list-item/block.json b/packages/block-library/src/list-item/block.json index 61c6eec4bb26f4..3acaf2a3bb29b2 100644 --- a/packages/block-library/src/list-item/block.json +++ b/packages/block-library/src/list-item/block.json @@ -21,6 +21,7 @@ }, "supports": { "className": false, + "splitting": true, "__experimentalSelector": "li", "spacing": { "margin": true, diff --git a/packages/block-library/src/list-item/edit.js b/packages/block-library/src/list-item/edit.js index 467154f76992e1..42a8bb5551319c 100644 --- a/packages/block-library/src/list-item/edit.js +++ b/packages/block-library/src/list-item/edit.js @@ -27,10 +27,8 @@ import { useSpace, useIndentListItem, useOutdentListItem, - useSplit, useMerge, } from './hooks'; -import { convertToListItems } from './utils'; export function IndentUI( { clientId } ) { const indentListItem = useIndentListItem( clientId ); @@ -73,7 +71,6 @@ export function IndentUI( { clientId } ) { export default function ListItemEdit( { attributes, setAttributes, - onReplace, clientId, mergeBlocks, } ) { @@ -85,7 +82,6 @@ export default function ListItemEdit( { } ); const useEnterRef = useEnter( { content, clientId } ); const useSpaceRef = useSpace( clientId ); - const onSplit = useSplit( clientId ); const onMerge = useMerge( clientId, mergeBlocks ); return ( <> @@ -100,18 +96,7 @@ export default function ListItemEdit( { value={ content } aria-label={ __( 'List text' ) } placeholder={ placeholder || __( 'List' ) } - onSplit={ onSplit } onMerge={ onMerge } - onReplace={ - onReplace - ? ( blocks, ...args ) => { - onReplace( - convertToListItems( blocks ), - ...args - ); - } - : undefined - } /> { innerBlocksProps.children } diff --git a/packages/block-library/src/paragraph/block.json b/packages/block-library/src/paragraph/block.json index fd325da4de0b0b..6f11baf838a944 100644 --- a/packages/block-library/src/paragraph/block.json +++ b/packages/block-library/src/paragraph/block.json @@ -30,6 +30,7 @@ } }, "supports": { + "splitting": true, "anchor": true, "className": false, "color": { diff --git a/packages/block-library/src/paragraph/edit.js b/packages/block-library/src/paragraph/edit.js index c3e434e93b714c..061cb672ae2593 100644 --- a/packages/block-library/src/paragraph/edit.js +++ b/packages/block-library/src/paragraph/edit.js @@ -21,7 +21,6 @@ import { useSettings, useBlockEditingMode, } from '@wordpress/block-editor'; -import { createBlock } from '@wordpress/blocks'; import { formatLtr } from '@wordpress/icons'; /** @@ -29,8 +28,6 @@ import { formatLtr } from '@wordpress/icons'; */ import { useOnEnter } from './use-enter'; -const name = 'core/paragraph'; - function ParagraphRTLControl( { direction, setDirection } ) { return ( isRTL() && ( @@ -149,24 +146,6 @@ function ParagraphBlock( { onChange={ ( newContent ) => setAttributes( { content: newContent } ) } - onSplit={ ( value, isOriginal ) => { - let newAttributes; - - if ( isOriginal || value ) { - newAttributes = { - ...attributes, - content: value, - }; - } - - const block = createBlock( name, newAttributes ); - - if ( isOriginal ) { - block.clientId = clientId; - } - - return block; - } } onMerge={ mergeBlocks } onReplace={ onReplace } onRemove={ onRemove } diff --git a/packages/rich-text/src/component/event-listeners/copy-handler.js b/packages/rich-text/src/component/event-listeners/copy-handler.js index d756095a94026b..0cc1594c3ab914 100644 --- a/packages/rich-text/src/component/event-listeners/copy-handler.js +++ b/packages/rich-text/src/component/event-listeners/copy-handler.js @@ -30,10 +30,12 @@ export default ( props ) => ( element ) => { } } - element.addEventListener( 'copy', onCopy ); - element.addEventListener( 'cut', onCopy ); + const { defaultView } = element.ownerDocument; + + defaultView.addEventListener( 'copy', onCopy ); + defaultView.addEventListener( 'cut', onCopy ); return () => { - element.removeEventListener( 'copy', onCopy ); - element.removeEventListener( 'cut', onCopy ); + defaultView.removeEventListener( 'copy', onCopy ); + defaultView.removeEventListener( 'cut', onCopy ); }; }; diff --git a/packages/rich-text/src/component/event-listeners/input-and-selection.js b/packages/rich-text/src/component/event-listeners/input-and-selection.js index 41dac7fa7ff980..621f1c59fab04e 100644 --- a/packages/rich-text/src/component/event-listeners/input-and-selection.js +++ b/packages/rich-text/src/component/event-listeners/input-and-selection.js @@ -234,6 +234,11 @@ export default ( props ) => ( element ) => { onSelectionChange( record.current.start, record.current.end ); + // There is no selection change event when the element is focused, so + // we need to manually trigger it. The selection is also not available + // yet in this call stack. + window.queueMicrotask( handleSelectionChange ); + ownerDocument.addEventListener( 'selectionchange', handleSelectionChange diff --git a/test/e2e/specs/editor/various/__snapshots__/Copy-cut-paste-should-copy-and-paste-individual-blocks-with-collapsed-selection-2-chromium.txt b/test/e2e/specs/editor/various/__snapshots__/Copy-cut-paste-should-copy-and-paste-individual-blocks-with-collapsed-selection-2-chromium.txt index 3dac62748e8e02..4d6ec0a8f5f5b5 100644 --- a/test/e2e/specs/editor/various/__snapshots__/Copy-cut-paste-should-copy-and-paste-individual-blocks-with-collapsed-selection-2-chromium.txt +++ b/test/e2e/specs/editor/various/__snapshots__/Copy-cut-paste-should-copy-and-paste-individual-blocks-with-collapsed-selection-2-chromium.txt @@ -3,9 +3,5 @@ -

2

- - - -

Copy - collapsed selection

+

2Copy - collapsed selection

\ No newline at end of file diff --git a/test/e2e/specs/editor/various/__snapshots__/Copy-cut-paste-should-cut-and-paste-individual-blocks-with-collapsed-selection-2-chromium.txt b/test/e2e/specs/editor/various/__snapshots__/Copy-cut-paste-should-cut-and-paste-individual-blocks-with-collapsed-selection-2-chromium.txt index ad1a1ef6e5c723..4a58677b62c4c3 100644 --- a/test/e2e/specs/editor/various/__snapshots__/Copy-cut-paste-should-cut-and-paste-individual-blocks-with-collapsed-selection-2-chromium.txt +++ b/test/e2e/specs/editor/various/__snapshots__/Copy-cut-paste-should-cut-and-paste-individual-blocks-with-collapsed-selection-2-chromium.txt @@ -1,7 +1,3 @@ -

2

- - - -

Cut - collapsed selection

+

2Cut - collapsed selection

\ No newline at end of file diff --git a/test/e2e/specs/editor/various/copy-cut-paste.spec.js b/test/e2e/specs/editor/various/copy-cut-paste.spec.js index a4055af4813949..539791d39e45f0 100644 --- a/test/e2e/specs/editor/various/copy-cut-paste.spec.js +++ b/test/e2e/specs/editor/various/copy-cut-paste.spec.js @@ -485,6 +485,7 @@ test.describe( 'Copy/cut/paste', () => { } ) => { pageUtils.setClipboardData( { html: '
x
', + plainText: 'x', } ); await editor.insertBlock( { name: 'core/list' } ); await pageUtils.pressKeys( 'primary+v' ); @@ -596,4 +597,47 @@ test.describe( 'Copy/cut/paste', () => { }, ] ); } ); + + test( 'should inherit existing block type on paste', async ( { + pageUtils, + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/heading', + attributes: { + content: 'A', + }, + } ); + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'b', + }, + } ); + await pageUtils.pressKeys( 'primary+a' ); + await pageUtils.pressKeys( 'primary+a' ); + await pageUtils.pressKeys( 'primary+c' ); + + await page.keyboard.press( 'Backspace' ); + await page.keyboard.type( '[]' ); + await page.keyboard.press( 'ArrowLeft' ); + + await pageUtils.pressKeys( 'primary+v' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: '[A', + }, + }, + { + name: 'core/paragraph', + attributes: { + content: 'b]', + }, + }, + ] ); + } ); } ); diff --git a/test/e2e/specs/editor/various/multi-block-selection.spec.js b/test/e2e/specs/editor/various/multi-block-selection.spec.js index 225b21234182ca..9148c316078a4a 100644 --- a/test/e2e/specs/editor/various/multi-block-selection.spec.js +++ b/test/e2e/specs/editor/various/multi-block-selection.spec.js @@ -538,10 +538,8 @@ test.describe( 'Multi-block selection (@firefox, @webkit)', () => { .poll( editor.getBlocks, 'should paste mid-block' ) .toMatchObject( [ { attributes: { content: 'first paragraph' } }, - { attributes: { content: 'second paragr' } }, - { attributes: { content: 'first paragraph' } }, - { attributes: { content: 'second paragraph|' } }, - { attributes: { content: 'aph' } }, + { attributes: { content: 'second paragrfirst paragraph' } }, + { attributes: { content: 'second paragraph|aph' } }, ] ); } ); diff --git a/test/e2e/specs/editor/various/rich-text-deprecated-on-split.spec.js b/test/e2e/specs/editor/various/rich-text-deprecated-on-split.spec.js new file mode 100644 index 00000000000000..30531ae2d0aed2 --- /dev/null +++ b/test/e2e/specs/editor/various/rich-text-deprecated-on-split.spec.js @@ -0,0 +1,75 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'RichText deprecated onSplit', () => { + test.beforeEach( async ( { admin, page, editor } ) => { + await admin.createNewPost(); + await page.evaluate( () => { + const registerBlockType = window.wp.blocks.registerBlockType; + const { useBlockProps, RichText } = window.wp.blockEditor; + const { createBlock, setDefaultBlockName } = window.wp.blocks; + const el = window.wp.element.createElement; + registerBlockType( 'core/rich-text-deprecated-on-split', { + apiVersion: 3, + title: 'Deprecated RichText onSplit', + attributes: { + value: { + type: 'string', + source: 'html', + selector: 'div', + }, + }, + edit: function Edit( { + attributes, + setAttributes, + onReplace, + } ) { + return el( RichText, { + ...useBlockProps(), + value: attributes.value, + onChange( value ) { + setAttributes( { value } ); + }, + onReplace, + onSplit( value ) { + return createBlock( + 'core/rich-text-deprecated-on-split', + { value } + ); + }, + } ); + }, + save( { attributes } ) { + return el( RichText.Content, { value: attributes.value } ); + }, + } ); + setDefaultBlockName( 'core/rich-text-deprecated-on-split' ); + } ); + await editor.insertBlock( { + name: 'core/rich-text-deprecated-on-split', + } ); + } ); + + test( 'should split', async ( { page, editor } ) => { + await page.keyboard.type( '1' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( '2' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/rich-text-deprecated-on-split', + attributes: { + value: '1', + }, + }, + { + name: 'core/rich-text-deprecated-on-split', + attributes: { + value: '2', + }, + }, + ] ); + } ); +} ); diff --git a/test/e2e/specs/editor/various/rich-text.spec.js b/test/e2e/specs/editor/various/rich-text.spec.js index cbf3dc41998871..aa56639281b491 100644 --- a/test/e2e/specs/editor/various/rich-text.spec.js +++ b/test/e2e/specs/editor/various/rich-text.spec.js @@ -492,19 +492,11 @@ test.describe( 'RichText (@firefox, @webkit)', () => { expect( await editor.getBlocks() ).toMatchObject( [ { name: 'core/paragraph', - attributes: { content: 'a' }, + attributes: { content: 'a1' }, }, { name: 'core/paragraph', - attributes: { content: '1' }, - }, - { - name: 'core/paragraph', - attributes: { content: '2' }, - }, - { - name: 'core/paragraph', - attributes: { content: 'b' }, + attributes: { content: '2b' }, }, ] ); } );