diff --git a/editor/components/writing-flow/index.js b/editor/components/writing-flow/index.js
index 6f274ec02ee1db..a875c410f3934c 100644
--- a/editor/components/writing-flow/index.js
+++ b/editor/components/writing-flow/index.js
@@ -53,6 +53,41 @@ const isTabbableTextField = overEvery( [
focus.tabbable.isTabbableIndex,
] );
+/**
+ * Returns true if the given node is a TinyMCE inline boundary node.
+ *
+ * @param {Node} node Node to test.
+ *
+ * @return {boolean} Whether node is a TinyMCE inline boundary node.
+ */
+function isInlineBoundary( node ) {
+ return node.getAttribute( 'data-mce-selected' ) === 'inline-boundary';
+}
+
+/**
+ * Returns true if it can be inferred that horizontal navigation has already
+ * been handled in the desired direction, or false otherwise. Specifically,
+ * this accounts for TinyMCE's inline boundary traversal, where its keydown
+ * event takes effect before the event propagates to WritingFlow. This avoids
+ * the caret from being moved outside the block when inline boundary occurs at
+ * the end of a RichText.
+ *
+ * @see tinymce/src/core/main/ts/keyboard/ArrowKeys.ts (executeKeydownOverride)
+ *
+ * @param {boolean} isReverse Whether to test in reverse.
+ *
+ * @return {boolean} Whether horizontal navigation has been handled.
+ */
+function isHorizontalNavigationHandled( isReverse ) {
+ const { isCollapsed, focusNode } = getSelection();
+ if ( ! isCollapsed ) {
+ return false;
+ }
+
+ const siblingNode = focusNode[ isReverse ? 'nextSibling' : 'previousSibling' ];
+ return !! siblingNode && isInlineBoundary( siblingNode );
+}
+
class WritingFlow extends Component {
constructor() {
super( ...arguments );
@@ -217,7 +252,8 @@ class WritingFlow extends Component {
placeCaretAtVerticalEdge( closestTabbable, isReverse, this.verticalRect );
event.preventDefault();
}
- } else if ( isHorizontal && getSelection().isCollapsed && isHorizontalEdge( target, isReverse ) ) {
+ } else if ( isHorizontal && getSelection().isCollapsed &&
+ ! isHorizontalNavigationHandled( isReverse ) && isHorizontalEdge( target, isReverse ) ) {
const closestTabbable = this.getClosestTabbable( target, isReverse );
placeCaretAtHorizontalEdge( closestTabbable, isReverse );
event.preventDefault();
diff --git a/test/e2e/specs/__snapshots__/writing-flow.test.js.snap b/test/e2e/specs/__snapshots__/writing-flow.test.js.snap
index 2cc2c7ca4421d2..f1afe1f03ab127 100644
--- a/test/e2e/specs/__snapshots__/writing-flow.test.js.snap
+++ b/test/e2e/specs/__snapshots__/writing-flow.test.js.snap
@@ -13,3 +13,25 @@ exports[`adding blocks Should navigate with arrow keys 5`] = `"The Greeting:
Hello to the World! (Suffix)"`;
+
+exports[`adding blocks Should navigate with arrow keys 8`] = `"Bolded
"`;
+
+exports[`adding blocks Should navigate with arrow keys 9`] = `"Bolded Words
"`;
+
+exports[`adding blocks Should navigate with arrow keys 10`] = `"Bolded WordsAfter
"`;
+
+exports[`adding blocks Should navigate with arrow keys 11`] = `"The Greeting:
Hello to the World! (Suffixed)"`;
+
+exports[`adding blocks Should navigate with arrow keys 12`] = `
+"
+
The Greeting:
Hello to the World! (Suffixed)
Bolded WordsAfter
+ + + +Prefix: Hello
+" +`; diff --git a/test/e2e/specs/writing-flow.test.js b/test/e2e/specs/writing-flow.test.js index 8b9fe6dc3883fe..03ac637bad2612 100644 --- a/test/e2e/specs/writing-flow.test.js +++ b/test/e2e/specs/writing-flow.test.js @@ -7,7 +7,12 @@ import { times } from 'lodash'; * Internal dependencies */ import '../support/bootstrap'; -import { newPost, newDesktopBrowserPage, pressWithModifier } from '../support/utils'; +import { + newPost, + newDesktopBrowserPage, + pressWithModifier, + getHTMLFromCodeEditor, +} from '../support/utils'; describe( 'adding blocks', () => { beforeAll( async () => { @@ -111,5 +116,36 @@ describe( 'adding blocks', () => { await page.keyboard.up( 'Shift' ); await page.keyboard.type( ' (Suffix)' ); await expectParagraphToMatchSnapshot( 1 ); + + // Should arrow once to escape out of inline boundary (bold, etc), and + // escaping out should nullify any block traversal. + await page.keyboard.press( 'Enter' ); + await pressWithModifier( 'Mod', 'B' ); // Bold + await page.keyboard.type( 'Bolded' ); + await expectParagraphToMatchSnapshot( 2 ); + + // Pressing space while having escaped on right edge of inline boundary + // should continue text as bolded. + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.type( ' Words' ); + await expectParagraphToMatchSnapshot( 2 ); + + // But typing immediately after escaping should not be within. + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.type( 'After' ); + await expectParagraphToMatchSnapshot( 2 ); + + // Navigate back to previous block. Change "(Suffix)" to "(Suffixed)" + // + // "Bolded WordsAfter" = 17 characters + // + 2 inline boundaries + // + 1 horizontal block traversal + // + 1 into parenthesis + // = 21 + await promiseSequence( times( 21, () => () => page.keyboard.press( 'ArrowLeft' ) ) ); + await page.keyboard.type( 'ed' ); + await expectParagraphToMatchSnapshot( 1 ); + + expect( await getHTMLFromCodeEditor() ).toMatchSnapshot(); } ); } );