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"
- />
+ >
+
+
@@ -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 perwithin 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' ); } );