diff --git a/packages/block-library/src/button/test/__snapshots__/index.js.snap b/packages/block-library/src/button/test/__snapshots__/index.js.snap index fa8009e4cbc03..9d96ec60114b2 100644 --- a/packages/block-library/src/button/test/__snapshots__/index.js.snap +++ b/packages/block-library/src/button/test/__snapshots__/index.js.snap @@ -23,7 +23,11 @@ exports[`core/button block edit matches snapshot 1`] = ` contenteditable="true" data-is-placeholder-visible="true" role="textbox" - /> + > +
+
diff --git a/packages/block-library/src/quote/index.js b/packages/block-library/src/quote/index.js index a87b0dba27453..49eef577f907a 100644 --- a/packages/block-library/src/quote/index.js +++ b/packages/block-library/src/quote/index.js @@ -115,7 +115,7 @@ export const settings = { blocks: [ 'core/paragraph' ], transform: ( { value, citation } ) => { const paragraphs = []; - if ( value ) { + if ( value && value !== '

' ) { paragraphs.push( ...split( create( { html: value, multilineTag: 'p' } ), '\u2028' ) .map( ( piece ) => @@ -125,7 +125,7 @@ export const settings = { ) ); } - if ( citation ) { + if ( citation && citation !== '

' ) { paragraphs.push( createBlock( 'core/paragraph', { content: citation, @@ -189,7 +189,6 @@ export const settings = { edit( { attributes, setAttributes, isSelected, mergeBlocks, onReplace, className } ) { const { align, value, citation } = attributes; - return ( diff --git a/packages/block-library/src/quote/test/__snapshots__/index.js.snap b/packages/block-library/src/quote/test/__snapshots__/index.js.snap index 813b6d8417cb6..54f0293d643a5 100644 --- a/packages/block-library/src/quote/test/__snapshots__/index.js.snap +++ b/packages/block-library/src/quote/test/__snapshots__/index.js.snap @@ -21,7 +21,11 @@ exports[`core/quote block edit matches snapshot 1`] = ` contenteditable="true" data-is-placeholder-visible="true" role="textbox" - /> + > +
+
diff --git a/packages/block-library/src/text-columns/test/__snapshots__/index.js.snap b/packages/block-library/src/text-columns/test/__snapshots__/index.js.snap index b618bdb565e07..19e26bedd4e4f 100644 --- a/packages/block-library/src/text-columns/test/__snapshots__/index.js.snap +++ b/packages/block-library/src/text-columns/test/__snapshots__/index.js.snap @@ -24,7 +24,11 @@ exports[`core/text-columns block edit matches snapshot 1`] = ` contenteditable="true" data-is-placeholder-visible="true" role="textbox" - /> + > +
+

@@ -55,7 +59,11 @@ exports[`core/text-columns block edit matches snapshot 1`] = ` contenteditable="true" data-is-placeholder-visible="true" role="textbox" - /> + > +
+

diff --git a/packages/block-library/src/verse/test/__snapshots__/index.js.snap b/packages/block-library/src/verse/test/__snapshots__/index.js.snap index dadd75177de95..4141acd82063f 100644 --- a/packages/block-library/src/verse/test/__snapshots__/index.js.snap +++ b/packages/block-library/src/verse/test/__snapshots__/index.js.snap @@ -18,7 +18,11 @@ exports[`core/verse block edit matches snapshot 1`] = ` contenteditable="true" data-is-placeholder-visible="true" role="textbox" - /> + > +
+

diff --git a/packages/editor/src/components/rich-text/tinymce.js b/packages/editor/src/components/rich-text/tinymce.js
index 25b3a2ac86032..c589dde327075 100644
--- a/packages/editor/src/components/rich-text/tinymce.js
+++ b/packages/editor/src/components/rich-text/tinymce.js
@@ -101,9 +101,10 @@ export default class TinyMCE extends Component {
 	constructor() {
 		super();
 		this.bindEditorNode = this.bindEditorNode.bind( this );
+		this.onFocus = this.onFocus.bind( this );
 	}
 
-	componentDidMount() {
+	onFocus() {
 		this.initialize();
 	}
 
@@ -158,6 +159,11 @@ export default class TinyMCE extends Component {
 			browser_spellcheck: true,
 			entity_encoding: 'raw',
 			convert_urls: false,
+			// Disables TinyMCE's parsing to verify HTML. It makes
+			// initialisation a bit faster. Since we're setting raw HTML
+			// already with dangerouslySetInnerHTML, we don't need this to be
+			// verified.
+			verify_html: false,
 			inline_boundaries_selector: 'a[href],code,b,i,strong,em,del,ins,sup,sub',
 			plugins: [],
 		} );
@@ -169,6 +175,18 @@ export default class TinyMCE extends Component {
 				this.editor = editor;
 				this.props.onSetup( editor );
 
+				// TinyMCE resets the element content on initialization, even
+				// when it's already identical to what exists currently. This
+				// behavior clobbers a selection which exists at the time of
+				// initialization, thus breaking writing flow navigation. The
+				// hack here neutralizes setHTML during initialization.
+				let setHTML;
+
+				editor.on( 'preinit', () => {
+					setHTML = editor.dom.setHTML;
+					editor.dom.setHTML = () => {};
+				} );
+
 				editor.on( 'init', () => {
 					// See https://github.com/tinymce/tinymce/blob/master/src/core/main/ts/keyboard/FormatShortcuts.ts
 					[ 'b', 'i', 'u' ].forEach( ( character ) => {
@@ -177,6 +195,8 @@ export default class TinyMCE extends Component {
 					[ 1, 2, 3, 4, 5, 6, 7, 8, 9 ].forEach( ( number ) => {
 						editor.shortcuts.remove( `access+${ number }` );
 					} );
+
+					editor.dom.setHTML = setHTML;
 				} );
 			},
 		} );
@@ -247,6 +267,11 @@ export default class TinyMCE extends Component {
 			} );
 		}
 
+		if ( initialHTML === '' ) {
+			// Ensure the field is ready to receive focus by TinyMCE.
+			initialHTML = '
'; + } + return createElement( tagName, { ...ariaProps, className: classnames( className, 'editor-rich-text__tinymce' ), @@ -258,6 +283,7 @@ export default class TinyMCE extends Component { dangerouslySetInnerHTML: { __html: initialHTML }, onPaste, onInput, + onFocus: this.onFocus, } ); } } diff --git a/test/e2e/specs/block-deletion.test.js b/test/e2e/specs/block-deletion.test.js index 7da0695f1ab43..fb6dd2b40abd5 100644 --- a/test/e2e/specs/block-deletion.test.js +++ b/test/e2e/specs/block-deletion.test.js @@ -7,6 +7,7 @@ import { newPost, pressWithModifier, ACCESS_MODIFIER_KEYS, + waitForRichTextInitialization, } from '../support/utils'; const addThreeParagraphsToNewPost = async () => { @@ -16,8 +17,10 @@ const addThreeParagraphsToNewPost = async () => { await clickBlockAppender(); await page.keyboard.type( 'First paragraph' ); await page.keyboard.press( 'Enter' ); + await waitForRichTextInitialization(); await page.keyboard.type( 'Second paragraph' ); await page.keyboard.press( 'Enter' ); + await waitForRichTextInitialization(); }; const clickOnBlockSettingsMenuItem = async ( buttonLabel ) => { @@ -96,6 +99,7 @@ describe( 'block deletion -', () => { // Add a third paragraph for this test. await page.keyboard.type( 'Third paragraph' ); await page.keyboard.press( 'Enter' ); + await waitForRichTextInitialization(); // Press the up arrow once to select the third and fourth blocks. await pressWithModifier( 'Shift', 'ArrowUp' ); diff --git a/test/e2e/specs/blocks/__snapshots__/quote.test.js.snap b/test/e2e/specs/blocks/__snapshots__/quote.test.js.snap index 6a46b767390ef..5257716d67d00 100644 --- a/test/e2e/specs/blocks/__snapshots__/quote.test.js.snap +++ b/test/e2e/specs/blocks/__snapshots__/quote.test.js.snap @@ -58,7 +58,11 @@ exports[`Quote can be converted to paragraphs and renders a paragraph for the ci " `; -exports[`Quote can be converted to paragraphs and renders a void paragraph if both the cite and quote are void 1`] = `""`; +exports[`Quote can be converted to paragraphs and renders a void paragraph if both the cite and quote are void 1`] = ` +" +

+" +`; exports[`Quote can be converted to paragraphs and renders one paragraph block per

within quote 1`] = ` " diff --git a/test/e2e/specs/splitting-merging.test.js b/test/e2e/specs/splitting-merging.test.js index d059fbf42ea63..fca2a8820a013 100644 --- a/test/e2e/specs/splitting-merging.test.js +++ b/test/e2e/specs/splitting-merging.test.js @@ -8,6 +8,7 @@ import { pressTimes, pressWithModifier, META_KEY, + waitForRichTextInitialization, } from '../support/utils'; describe( 'splitting and merging blocks', () => { @@ -132,7 +133,9 @@ describe( 'splitting and merging blocks', () => { await pressWithModifier( META_KEY, 'b' ); await page.keyboard.press( 'ArrowRight' ); await page.keyboard.press( 'Enter' ); + await waitForRichTextInitialization(); await page.keyboard.press( 'Enter' ); + await waitForRichTextInitialization(); await page.keyboard.press( 'Backspace' ); @@ -172,8 +175,11 @@ describe( 'splitting and merging blocks', () => { await insertBlock( 'Paragraph' ); await page.keyboard.type( 'First' ); await page.keyboard.press( 'Enter' ); + await waitForRichTextInitialization(); await page.keyboard.press( 'Enter' ); + await waitForRichTextInitialization(); await page.keyboard.press( 'Enter' ); + await waitForRichTextInitialization(); await page.keyboard.type( 'Second' ); await page.keyboard.press( 'ArrowUp' ); await page.keyboard.press( 'ArrowUp' ); diff --git a/test/e2e/support/utils.js b/test/e2e/support/utils.js index 4ac62bf737299..7275da4d1d2fc 100644 --- a/test/e2e/support/utils.js +++ b/test/e2e/support/utils.js @@ -100,7 +100,7 @@ async function login() { * @return {Promise} Promise resolving once RichText is initialized, or is * determined to not be a container of the active element. */ -async function waitForRichTextInitialization() { +export async function waitForRichTextInitialization() { const isInRichText = await page.evaluate( () => { return !! document.activeElement.closest( '.editor-rich-text__tinymce' ); } );