diff --git a/.travis.yml b/.travis.yml index 7b87e251a2525..43b1058c0e1fa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ cache: branches: only: - - master + - wp/5.2 before_install: - nvm install diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 3942b64d18006..1d1089561ed2a 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -483,5 +483,4 @@ Undocumented declaration. -

Code is Poetry.

diff --git a/packages/block-editor/src/components/rich-text/editable.js b/packages/block-editor/src/components/rich-text/editable.js index 58c7217e1944c..c7c9a854fc0b8 100644 --- a/packages/block-editor/src/components/rich-text/editable.js +++ b/packages/block-editor/src/components/rich-text/editable.js @@ -83,7 +83,10 @@ function applyInternetExplorerInputFix( editorNode ) { } const IS_PLACEHOLDER_VISIBLE_ATTR_NAME = 'data-is-placeholder-visible'; -const CLASS_NAME = 'editor-rich-text__editable block-editor-rich-text__editable'; + +const oldClassName = 'editor-rich-text__editable'; + +export const className = 'block-editor-rich-text__editable'; /** * Whether or not the user agent is Internet Explorer. @@ -116,7 +119,11 @@ export default class Editable extends Component { } if ( ! isEqual( this.props.className, nextProps.className ) ) { - this.editorNode.className = classnames( nextProps.className, CLASS_NAME ); + this.editorNode.className = classnames( + className, + oldClassName, + nextProps.className + ); } const { removedKeys, updatedKeys } = diffAriaProps( this.props, nextProps ); @@ -156,7 +163,7 @@ export default class Editable extends Component { style, record, valueToEditableHTML, - className, + className: additionalClassName, isPlaceholderVisible, ...remainingProps } = this.props; @@ -166,7 +173,7 @@ export default class Editable extends Component { return createElement( tagName, { role: 'textbox', 'aria-multiline': true, - className: classnames( className, CLASS_NAME ), + className: classnames( className, oldClassName, additionalClassName ), contentEditable: true, [ IS_PLACEHOLDER_VISIBLE_ATTR_NAME ]: isPlaceholderVisible, ref: this.bindEditorNode, diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index c6cfad310c89c..403134acdc808 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -56,7 +56,7 @@ import Autocomplete from '../autocomplete'; import BlockFormatControls from '../block-format-controls'; import FormatEdit from './format-edit'; import FormatToolbar from './format-toolbar'; -import Editable from './editable'; +import Editable, { className as editableClassName } from './editable'; import { pickAriaProps } from './aria'; import { getPatterns } from './patterns'; import { withBlockEditContext } from '../block-edit/context'; @@ -202,7 +202,6 @@ export class RichText extends Component { range, multilineTag: this.multilineTag, multilineWrapperTags: this.multilineWrapperTags, - prepareEditableTree: this.props.prepareEditableTree, __unstableIsEditableTree: true, } ); } @@ -473,15 +472,18 @@ export class RichText extends Component { const boundarySelector = '*[data-rich-text-format-boundary]'; const element = this.editableRef.querySelector( boundarySelector ); - if ( element ) { - const computedStyle = getComputedStyle( element ); - const newColor = computedStyle.color - .replace( ')', ', 0.2)' ) - .replace( 'rgb', 'rgba' ); - - globalStyle.innerHTML = - `*:focus ${ boundarySelector }{background-color: ${ newColor }}`; + if ( ! element ) { + return; } + + const computedStyle = getComputedStyle( element ); + const newColor = computedStyle.color + .replace( ')', ', 0.2)' ) + .replace( 'rgb', 'rgba' ); + const selector = `.${ editableClassName }:focus ${ boundarySelector }`; + const rule = `background-color: ${ newColor }`; + + globalStyle.innerHTML = `${ selector } {${ rule }}`; } /** @@ -731,7 +733,10 @@ export class RichText extends Component { const { formats, text, start, end } = value; const { activeFormats = [] } = this.state; const collapsed = isCollapsed( value ); - const isReverse = event.keyCode === LEFT; + // To do: ideally, we should look at visual position instead. + const { direction } = getComputedStyle( this.editableRef ); + const reverseKey = direction === 'rtl' ? RIGHT : LEFT; + const isReverse = event.keyCode === reverseKey; // If the selection is collapsed and at the very start, do nothing if // navigating backward. diff --git a/packages/block-editor/src/components/writing-flow/index.js b/packages/block-editor/src/components/writing-flow/index.js index d03b11d0a84f7..784b21fa9fe4a 100644 --- a/packages/block-editor/src/components/writing-flow/index.js +++ b/packages/block-editor/src/components/writing-flow/index.js @@ -34,7 +34,7 @@ import { * Browser constants */ -const { getSelection } = window; +const { getSelection, getComputedStyle } = window; /** * Given an element, returns true if the element is a tabbable text field, or @@ -241,6 +241,18 @@ class WritingFlow extends Component { const hasModifier = isShift || event.ctrlKey || event.altKey || event.metaKey; const isNavEdge = isVertical ? isVerticalEdge : isHorizontalEdge; + // When presing any key other than up or down, the initial vertical + // position must ALWAYS be reset. The vertical position is saved so it + // can be restored as well as possible on sebsequent vertical arrow key + // presses. It may not always be possible to restore the exact same + // position (such as at an empty line), so it wouldn't be good to + // compute the position right before any vertical arrow key press. + if ( ! isVertical ) { + this.verticalRect = null; + } else if ( ! this.verticalRect ) { + this.verticalRect = computeCaretRect( target ); + } + // This logic inside this condition needs to be checked before // the check for event.nativeEvent.defaultPrevented. // The logic handles meta+a keypress and this event is default prevented @@ -281,11 +293,10 @@ class WritingFlow extends Component { return; } - if ( ! isVertical ) { - this.verticalRect = null; - } else if ( ! this.verticalRect ) { - this.verticalRect = computeCaretRect( target ); - } + // In the case of RTL scripts, right means previous and left means next, + // which is the exact reverse of LTR. + const { direction } = getComputedStyle( target ); + const isReverseDir = direction === 'rtl' ? ( ! isReverse ) : isReverse; if ( isShift ) { if ( @@ -316,9 +327,9 @@ class WritingFlow extends Component { placeCaretAtVerticalEdge( closestTabbable, isReverse, this.verticalRect ); event.preventDefault(); } - } else if ( isHorizontal && getSelection().isCollapsed && isHorizontalEdge( target, isReverse ) ) { - const closestTabbable = this.getClosestTabbable( target, isReverse ); - placeCaretAtHorizontalEdge( closestTabbable, isReverse ); + } else if ( isHorizontal && getSelection().isCollapsed && isHorizontalEdge( target, isReverseDir ) ) { + const closestTabbable = this.getClosestTabbable( target, isReverseDir ); + placeCaretAtHorizontalEdge( closestTabbable, isReverseDir ); event.preventDefault(); } } diff --git a/packages/block-library/README.md b/packages/block-library/README.md index 4c7d9cd0ae139..78573bbf753e7 100644 --- a/packages/block-library/README.md +++ b/packages/block-library/README.md @@ -33,5 +33,4 @@ registerCoreBlocks(); -

Code is Poetry.

diff --git a/packages/blocks/README.md b/packages/blocks/README.md index 0f5805c5244d0..960edae9da773 100644 --- a/packages/blocks/README.md +++ b/packages/blocks/README.md @@ -845,5 +845,4 @@ wrapped component. -

Code is Poetry.

diff --git a/packages/blocks/src/api/raw-handling/comment-remover.js b/packages/blocks/src/api/raw-handling/comment-remover.js new file mode 100644 index 0000000000000..fbfa319ccb831 --- /dev/null +++ b/packages/blocks/src/api/raw-handling/comment-remover.js @@ -0,0 +1,21 @@ +/** + * WordPress dependencies + */ +import { remove } from '@wordpress/dom'; + +/** + * Browser dependencies + */ +const { COMMENT_NODE } = window.Node; + +/** + * Looks for comments, and removes them. + * + * @param {Node} node The node to be processed. + * @return {void} + */ +export default function( node ) { + if ( node.nodeType === COMMENT_NODE ) { + remove( node ); + } +} diff --git a/packages/blocks/src/api/raw-handling/paste-handler.js b/packages/blocks/src/api/raw-handling/paste-handler.js index 1d67540c0b922..c99b4ea5c5d0a 100644 --- a/packages/blocks/src/api/raw-handling/paste-handler.js +++ b/packages/blocks/src/api/raw-handling/paste-handler.js @@ -11,6 +11,7 @@ import { getBlockContent } from '../serializer'; import { getBlockAttributes, parseWithGrammar } from '../parser'; import normaliseBlocks from './normalise-blocks'; import specialCommentConverter from './special-comment-converter'; +import commentRemover from './comment-remover'; import isInlineContent from './is-inline-content'; import phrasingContentReducer from './phrasing-content-reducer'; import headRemover from './head-remover'; @@ -44,7 +45,7 @@ const { console } = window; * @return {string} HTML only containing phrasing content. */ function filterInlineHTML( HTML ) { - HTML = deepFilterHTML( HTML, [ googleDocsUIDRemover, phrasingContentReducer ] ); + HTML = deepFilterHTML( HTML, [ googleDocsUIDRemover, phrasingContentReducer, commentRemover ] ); HTML = removeInvalidHTML( HTML, getPhrasingContentSchema(), { inline: true } ); // Allows us to ask for this information when we get a report. @@ -204,6 +205,7 @@ export function pasteHandler( { HTML = '', plainText = '', mode = 'AUTO', tagNam imageCorrector, phrasingContentReducer, specialCommentConverter, + commentRemover, figureContentReducer, blockquoteNormaliser, ]; diff --git a/packages/blocks/src/api/raw-handling/test/comment-remover.js b/packages/blocks/src/api/raw-handling/test/comment-remover.js new file mode 100644 index 0000000000000..6721e20bec23b --- /dev/null +++ b/packages/blocks/src/api/raw-handling/test/comment-remover.js @@ -0,0 +1,43 @@ +/** + * Internal dependencies + */ +import commentRemover from '../comment-remover'; +import { deepFilterHTML } from '../utils'; + +describe( 'commentRemover', () => { + it( 'should remove a single comment', () => { + expect( deepFilterHTML( + '', + [ commentRemover ] + ) ).toEqual( + '' + ); + } ); + it( 'should remove multiple comments', () => { + expect( deepFilterHTML( + '

First paragraph.

Second paragraph.

', + [ commentRemover ] + ) ).toEqual( + '

First paragraph.

Second paragraph.

' + ); + } ); + it( 'should remove nested comments', () => { + expect( deepFilterHTML( + '

Paragraph.

', + [ commentRemover ] + ) ).toEqual( + '

Paragraph.

' + ); + } ); + it( 'should remove multi-line comments', () => { + expect( deepFilterHTML( + `

First paragraph.

Second paragraph.

`, + [ commentRemover ] + ) ).toEqual( + '

First paragraph.

Second paragraph.

' + ); + } ); +} ); diff --git a/packages/dom/README.md b/packages/dom/README.md index e003966ab6775..c6b9945545602 100644 --- a/packages/dom/README.md +++ b/packages/dom/README.md @@ -268,5 +268,4 @@ Wraps the given node with a new node with the given tag name. -

Code is Poetry.

diff --git a/packages/dom/src/dom.js b/packages/dom/src/dom.js index aba561f992d59..c2491a0c07be1 100644 --- a/packages/dom/src/dom.js +++ b/packages/dom/src/dom.js @@ -143,12 +143,16 @@ function isEdge( container, isReverse, onlyVertical ) { return true; } + // In the case of RTL scripts, the horizontal edge is at the opposite side. + const { direction } = computedStyle; + const isReverseDir = direction === 'rtl' ? ( ! isReverse ) : isReverse; + // To calculate the horizontal position, we insert a test range and see if // this test range has the same horizontal position. This method proves to // be better than a DOM-based calculation, because it ignores empty text // nodes and a trailing line break element. In other words, we need to check // visual positioning, not DOM positioning. - const x = isReverse ? containerRect.left + 1 : containerRect.right - 1; + const x = isReverseDir ? containerRect.left + 1 : containerRect.right - 1; const y = isReverse ? containerRect.top + buffer : containerRect.bottom - buffer; const testRange = hiddenCaretRangeFromPoint( document, x, y, container ); @@ -156,7 +160,7 @@ function isEdge( container, isReverse, onlyVertical ) { return false; } - const side = isReverse ? 'left' : 'right'; + const side = isReverseDir ? 'left' : 'right'; const testRect = getRectangleFromRange( testRange ); return Math.round( testRect[ side ] ) === Math.round( rangeRect[ side ] ); diff --git a/packages/e2e-tests/config/setup-test-framework.js b/packages/e2e-tests/config/setup-test-framework.js index 4800da23a860f..dac807458e291 100644 --- a/packages/e2e-tests/config/setup-test-framework.js +++ b/packages/e2e-tests/config/setup-test-framework.js @@ -123,6 +123,19 @@ function observeConsoleLogging() { return; } + // A bug present in WordPress 5.2 will produce console warnings when + // loading the Dashicons font. These can be safely ignored, as they do + // not otherwise regress on application behavior. This logic should be + // removed once the associated ticket has been closed. + // + // See: https://core.trac.wordpress.org/ticket/47183 + if ( + text.startsWith( 'Failed to decode downloaded font:' ) || + text.startsWith( 'OTS parsing error:' ) + ) { + return; + } + const logFunction = OBSERVED_CONSOLE_MESSAGE_TYPES[ type ]; // As of Puppeteer 1.6.1, `message.text()` wrongly returns an object of diff --git a/packages/e2e-tests/specs/__snapshots__/links.test.js.snap b/packages/e2e-tests/specs/__snapshots__/links.test.js.snap index d63515e5d2ee3..292ac9e5781a9 100644 --- a/packages/e2e-tests/specs/__snapshots__/links.test.js.snap +++ b/packages/e2e-tests/specs/__snapshots__/links.test.js.snap @@ -47,3 +47,9 @@ exports[`Links should contain a label when it should open in a new tab 1`] = `

This is WordPress

" `; + +exports[`Links should contain a label when it should open in a new tab 2`] = ` +" +

This is WordPress

+" +`; diff --git a/packages/e2e-tests/specs/__snapshots__/rich-text.test.js.snap b/packages/e2e-tests/specs/__snapshots__/rich-text.test.js.snap index bf5fefd31a7e3..8aab0dcd5b734 100644 --- a/packages/e2e-tests/specs/__snapshots__/rich-text.test.js.snap +++ b/packages/e2e-tests/specs/__snapshots__/rich-text.test.js.snap @@ -24,6 +24,12 @@ exports[`RichText should apply multiple formats when selection is collapsed 1`] " `; +exports[`RichText should handle Home and End keys 1`] = ` +" +

-12+

+" +`; + exports[`RichText should handle change in tag name gracefully 1`] = ` "

diff --git a/packages/e2e-tests/specs/__snapshots__/rtl.test.js.snap b/packages/e2e-tests/specs/__snapshots__/rtl.test.js.snap new file mode 100644 index 0000000000000..19b4e8c305acc --- /dev/null +++ b/packages/e2e-tests/specs/__snapshots__/rtl.test.js.snap @@ -0,0 +1,63 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RTL should arrow navigate 1`] = ` +" +

٠١٢

+" +`; + +exports[`RTL should arrow navigate between blocks 1`] = ` +" +

٠
١

+ + + +

٠
١
٢

+" +`; + +exports[`RTL should merge backward 1`] = ` +" +

٠١

+" +`; + +exports[`RTL should merge forward 1`] = ` +" +

٠١

+" +`; + +exports[`RTL should navigate inline boundaries 1`] = ` +" +

١٠٢

+" +`; + +exports[`RTL should navigate inline boundaries 2`] = ` +" +

١٠٢

+" +`; + +exports[`RTL should navigate inline boundaries 3`] = ` +" +

٠١٢

+" +`; + +exports[`RTL should navigate inline boundaries 4`] = ` +" +

٠١٢

+" +`; + +exports[`RTL should split 1`] = ` +" +

٠

+ + + +

١

+" +`; diff --git a/packages/e2e-tests/specs/__snapshots__/writing-flow.test.js.snap b/packages/e2e-tests/specs/__snapshots__/writing-flow.test.js.snap index 0e41868f3cffa..e4c846f0631d0 100644 --- a/packages/e2e-tests/specs/__snapshots__/writing-flow.test.js.snap +++ b/packages/e2e-tests/specs/__snapshots__/writing-flow.test.js.snap @@ -197,3 +197,23 @@ exports[`adding blocks should not prematurely multi-select 1`] = `

>

" `; + +exports[`adding blocks should preserve horizontal position when navigating vertically between blocks 1`] = ` +" +

abc

+ + + +

123

+" +`; + +exports[`adding blocks should remember initial vertical position 1`] = ` +" +

1x

+ + + +


2

+" +`; diff --git a/packages/e2e-tests/specs/blocks/classic.test.js b/packages/e2e-tests/specs/blocks/classic.test.js index 2df2d1f2c8982..29d1605f0874b 100644 --- a/packages/e2e-tests/specs/blocks/classic.test.js +++ b/packages/e2e-tests/specs/blocks/classic.test.js @@ -43,8 +43,8 @@ describe( 'Classic', () => { await page.keyboard.type( 'test' ); // Click the image button. - await page.waitForSelector( 'div[aria-label="Add Media"]' ); - await page.click( 'div[aria-label="Add Media"]' ); + await page.waitForSelector( 'div[aria-label^="Add Media"]' ); + await page.click( 'div[aria-label^="Add Media"]' ); // Wait for media modal to appear and upload image. await page.waitForSelector( '.media-modal input[type=file]' ); diff --git a/packages/e2e-tests/specs/links.test.js b/packages/e2e-tests/specs/links.test.js index a41aa953bd8e7..b97a9cde0d08b 100644 --- a/packages/e2e-tests/specs/links.test.js +++ b/packages/e2e-tests/specs/links.test.js @@ -308,7 +308,9 @@ describe( 'Links', () => { } ); // Test for regressions of https://github.com/WordPress/gutenberg/issues/10496. - it( 'allows autocomplete suggestions to be navigated with the keyboard', async () => { + // This test isn't reliable on Travis and fails from time to time. + // See: https://github.com/WordPress/gutenberg/pull/15211. + it.skip( 'allows autocomplete suggestions to be navigated with the keyboard', async () => { const titleText = 'Test post keyboard'; const postURL = await createPostWithTitle( titleText ); @@ -501,5 +503,48 @@ describe( 'Links', () => { await page.keyboard.press( 'Enter' ); expect( await getEditedPostContent() ).toMatchSnapshot(); + + // Regression Test: This verifies that the UI is updated according to + // the expected changed values, where previously the value could have + // fallen out of sync with how the UI is displayed (specifically for + // collapsed selections). + // + // See: https://github.com/WordPress/gutenberg/pull/15573 + + // Collapse selection. + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'ArrowRight' ); + // Edit link. + await pressKeyWithModifier( 'primary', 'k' ); + await waitForAutoFocus(); + await pressKeyWithModifier( 'primary', 'a' ); + await page.keyboard.type( 'wordpress.org' ); + // Navigate to the settings toggle. + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Tab' ); + // Open settings. + await page.keyboard.press( 'Space' ); + // Navigate to the "Open in New Tab" checkbox. + await page.keyboard.press( 'Tab' ); + // Uncheck the checkbox. + await page.keyboard.press( 'Space' ); + // Navigate back to the input field. + await page.keyboard.press( 'Tab' ); + // Submit the form. + await page.keyboard.press( 'Enter' ); + + // Navigate back to inputs to verify appears as changed. + await pressKeyWithModifier( 'primary', 'k' ); + await waitForAutoFocus(); + const link = await page.evaluate( () => document.activeElement.value ); + expect( link ).toBe( 'http://wordpress.org' ); + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Space' ); + await page.keyboard.press( 'Tab' ); + const isChecked = await page.evaluate( () => document.activeElement.checked ); + expect( isChecked ).toBe( false ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); } ); } ); diff --git a/packages/e2e-tests/specs/plugins/wp-editor-meta-box.test.js b/packages/e2e-tests/specs/plugins/wp-editor-meta-box.test.js index c99da39b7cfe0..74dd45374e946 100644 --- a/packages/e2e-tests/specs/plugins/wp-editor-meta-box.test.js +++ b/packages/e2e-tests/specs/plugins/wp-editor-meta-box.test.js @@ -8,7 +8,9 @@ import { publishPost, } from '@wordpress/e2e-test-utils'; -describe( 'WP Editor Meta Boxes', () => { +// This test isn't reliable on Travis and fails from time to time. +// See: https://github.com/WordPress/gutenberg/pull/15211. +describe.skip( 'WP Editor Meta Boxes', () => { beforeAll( async () => { await activatePlugin( 'gutenberg-test-plugin-wp-editor-meta-box' ); await createNewPost(); diff --git a/packages/e2e-tests/specs/rich-text.test.js b/packages/e2e-tests/specs/rich-text.test.js index dcb1434abd89b..1bd6d79c1b10f 100644 --- a/packages/e2e-tests/specs/rich-text.test.js +++ b/packages/e2e-tests/specs/rich-text.test.js @@ -208,4 +208,19 @@ describe( 'RichText', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + + it( 'should handle Home and End keys', async () => { + await page.keyboard.press( 'Enter' ); + + await pressKeyWithModifier( 'primary', 'b' ); + await page.keyboard.type( '12' ); + await pressKeyWithModifier( 'primary', 'b' ); + + await page.keyboard.press( 'Home' ); + await page.keyboard.type( '-' ); + await page.keyboard.press( 'End' ); + await page.keyboard.type( '+' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); } ); diff --git a/packages/e2e-tests/specs/rtl.test.js b/packages/e2e-tests/specs/rtl.test.js new file mode 100644 index 0000000000000..76e6d15ff9aa3 --- /dev/null +++ b/packages/e2e-tests/specs/rtl.test.js @@ -0,0 +1,122 @@ +/** + * WordPress dependencies + */ +import { + createNewPost, + getEditedPostContent, + pressKeyWithModifier, +} from '@wordpress/e2e-test-utils'; + +// Avoid using three, as it looks too much like two with some fonts. +const ARABIC_ZERO = '٠'; +const ARABIC_ONE = '١'; +const ARABIC_TWO = '٢'; + +describe( 'RTL', () => { + beforeEach( async () => { + await createNewPost(); + } ); + + it( 'should arrow navigate', async () => { + await page.evaluate( () => document.dir = 'rtl' ); + await page.keyboard.press( 'Enter' ); + + // We need at least three characters as arrow navigation *from* the + // edges might be handled differently. + await page.keyboard.type( ARABIC_ONE ); + await page.keyboard.type( ARABIC_TWO ); + await page.keyboard.press( 'ArrowRight' ); + // This is the important key press: arrow nav *from* the middle. + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.type( ARABIC_ZERO ); + + // Expect: ARABIC_ZERO + ARABIC_ONE + ARABIC_TWO (

٠١٢

). + // N.b.: HTML is LTR, so direction will be reversed! + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'should split', async () => { + await page.evaluate( () => document.dir = 'rtl' ); + await page.keyboard.press( 'Enter' ); + + await page.keyboard.type( ARABIC_ZERO ); + await page.keyboard.type( ARABIC_ONE ); + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.press( 'Enter' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'should merge backward', async () => { + await page.evaluate( () => document.dir = 'rtl' ); + await page.keyboard.press( 'Enter' ); + + await page.keyboard.type( ARABIC_ZERO ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( ARABIC_ONE ); + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.press( 'Backspace' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'should merge forward', async () => { + await page.evaluate( () => document.dir = 'rtl' ); + await page.keyboard.press( 'Enter' ); + + await page.keyboard.type( ARABIC_ZERO ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( ARABIC_ONE ); + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.press( 'Delete' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'should arrow navigate between blocks', async () => { + await page.evaluate( () => document.dir = 'rtl' ); + await page.keyboard.press( 'Enter' ); + + await page.keyboard.type( ARABIC_ZERO ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( ARABIC_ONE ); + await pressKeyWithModifier( 'shift', 'Enter' ); + await page.keyboard.type( ARABIC_TWO ); + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.press( 'ArrowRight' ); + + // Move to the previous block with two lines in the current block. + await page.keyboard.press( 'ArrowRight' ); + await pressKeyWithModifier( 'shift', 'Enter' ); + await page.keyboard.type( ARABIC_ONE ); + + // Move to the next block with two lines in the current block. + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.type( ARABIC_ZERO ); + await pressKeyWithModifier( 'shift', 'Enter' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'should navigate inline boundaries', async () => { + await page.evaluate( () => document.dir = 'rtl' ); + await page.keyboard.press( 'Enter' ); + + await pressKeyWithModifier( 'primary', 'b' ); + await page.keyboard.type( ARABIC_ONE ); + await pressKeyWithModifier( 'primary', 'b' ); + await page.keyboard.type( ARABIC_TWO ); + + // Insert a character at each boundary position. + for ( let i = 4; i > 0; i-- ) { + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.type( ARABIC_ZERO ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + + await page.keyboard.press( 'Backspace' ); + } + } ); +} ); diff --git a/packages/e2e-tests/specs/taxonomies.test.js b/packages/e2e-tests/specs/taxonomies.test.js index 2cd8e7665eda4..1c88fa690be96 100644 --- a/packages/e2e-tests/specs/taxonomies.test.js +++ b/packages/e2e-tests/specs/taxonomies.test.js @@ -110,7 +110,10 @@ describe( 'Taxonomies', () => { expect( selectedCategories[ 0 ] ).toEqual( 'z rand category 1' ); } ); - it( 'should be able to open the tags panel and create a new tag if the user has the right capabilities', async () => { + // This test isn't reliable locally because repeated execution of the test triggers 400 network + // because of the tag's duplication. Also, it randomly doesn't add a new tag after pressing enter. + // See: https://github.com/WordPress/gutenberg/pull/15211. + it.skip( 'should be able to open the tags panel and create a new tag if the user has the right capabilities', async () => { await createNewPost(); await openDocumentSettingsSidebar(); diff --git a/packages/e2e-tests/specs/writing-flow.test.js b/packages/e2e-tests/specs/writing-flow.test.js index c9ca7bf2021df..5e870534bf010 100644 --- a/packages/e2e-tests/specs/writing-flow.test.js +++ b/packages/e2e-tests/specs/writing-flow.test.js @@ -342,4 +342,31 @@ describe( 'adding blocks', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + + it( 'should preserve horizontal position when navigating vertically between blocks', async () => { + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'abc' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( '23' ); + await page.keyboard.press( 'ArrowUp' ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'ArrowDown' ); + await page.keyboard.type( '1' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'should remember initial vertical position', async () => { + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( '1' ); + await page.keyboard.press( 'Enter' ); + await pressKeyWithModifier( 'shift', 'Enter' ); + await page.keyboard.type( '2' ); + await page.keyboard.press( 'ArrowUp' ); + await page.keyboard.press( 'ArrowUp' ); + await page.keyboard.type( 'x' ); // Should be right after "1". + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); } ); diff --git a/packages/edit-post/README.md b/packages/edit-post/README.md index 7831aeb6b4115..0c8eb1326e884 100644 --- a/packages/edit-post/README.md +++ b/packages/edit-post/README.md @@ -466,5 +466,4 @@ an initial state from prior to the crash. -

Code is Poetry.

diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index c0c88d1072fe9..fd1458f032043 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -732,6 +732,7 @@ export const multiSelect = getBlockEditorAction( 'multiSelect' ); export const clearSelectedBlock = getBlockEditorAction( 'clearSelectedBlock' ); export const toggleSelection = getBlockEditorAction( 'toggleSelection' ); export const replaceBlocks = getBlockEditorAction( 'replaceBlocks' ); +export const replaceBlock = getBlockEditorAction( 'replaceBlock' ); export const moveBlocksDown = getBlockEditorAction( 'moveBlocksDown' ); export const moveBlocksUp = getBlockEditorAction( 'moveBlocksUp' ); export const moveBlockToPosition = getBlockEditorAction( 'moveBlockToPosition' ); diff --git a/packages/format-library/src/link/inline.js b/packages/format-library/src/link/inline.js index e94ce68514fb3..c4b64e9e3c45c 100644 --- a/packages/format-library/src/link/inline.js +++ b/packages/format-library/src/link/inline.js @@ -79,7 +79,8 @@ const LinkViewerUrl = ( { url } ) => { const URLPopoverAtLink = ( { isActive, addingLink, value, ...props } ) => { const anchorRect = useMemo( () => { - const range = window.getSelection().getRangeAt( 0 ); + const selection = window.getSelection(); + const range = selection.rangeCount > 0 ? selection.getRangeAt( 0 ) : null; if ( ! range ) { return; } diff --git a/packages/rich-text/README.md b/packages/rich-text/README.md index 288b836083ea7..90fe67367ddc8 100644 --- a/packages/rich-text/README.md +++ b/packages/rich-text/README.md @@ -388,5 +388,4 @@ Unregisters a format. -

Code is Poetry.

diff --git a/packages/rich-text/src/apply-format.js b/packages/rich-text/src/apply-format.js index 14402735b1e60..3f9fea6a49990 100644 --- a/packages/rich-text/src/apply-format.js +++ b/packages/rich-text/src/apply-format.js @@ -2,7 +2,7 @@ * External dependencies */ -import { find } from 'lodash'; +import { find, reject } from 'lodash'; /** * Internal dependencies @@ -28,7 +28,7 @@ export function applyFormat( startIndex = value.start, endIndex = value.end ) { - const { formats, activeFormats = [] } = value; + const { formats, activeFormats } = value; const newFormats = formats.slice(); // The selection is collapsed. @@ -49,13 +49,6 @@ export function applyFormat( applyFormats( newFormats, endIndex, format ); endIndex++; } - // Otherwise, insert a placeholder with the format so new input appears - // with the format applied. - } else { - return { - ...value, - activeFormats: [ ...activeFormats, format ], - }; } } else { for ( let index = startIndex; index < endIndex; index++ ) { @@ -63,7 +56,17 @@ export function applyFormat( } } - return normaliseFormats( { ...value, formats: newFormats } ); + return normaliseFormats( { + ...value, + formats: newFormats, + // Always revise active formats. This serves as a placeholder for new + // inputs with the format so new input appears with the format applied, + // and ensures a format of the same type uses the latest values. + activeFormats: [ + ...reject( activeFormats, { type: format.type } ), + format, + ], + } ); } function applyFormats( formats, index, format ) { diff --git a/packages/rich-text/src/remove-format.js b/packages/rich-text/src/remove-format.js index 4a4c9c820f900..5ce2fcd7ce833 100644 --- a/packages/rich-text/src/remove-format.js +++ b/packages/rich-text/src/remove-format.js @@ -48,11 +48,6 @@ export function removeFormat( filterFormats( newFormats, endIndex, formatType ); endIndex++; } - } else { - return { - ...value, - activeFormats: reject( activeFormats, { type: formatType } ), - }; } } else { for ( let i = startIndex; i < endIndex; i++ ) { @@ -62,7 +57,11 @@ export function removeFormat( } } - return normaliseFormats( { ...value, formats: newFormats } ); + return normaliseFormats( { + ...value, + formats: newFormats, + activeFormats: reject( activeFormats, { type: formatType } ), + } ); } function filterFormats( formats, index, formatType ) { diff --git a/packages/rich-text/src/test/apply-format.js b/packages/rich-text/src/test/apply-format.js index b75dc7ee5de00..a46a8378eac3c 100644 --- a/packages/rich-text/src/test/apply-format.js +++ b/packages/rich-text/src/test/apply-format.js @@ -22,6 +22,7 @@ describe( 'applyFormat', () => { text: 'one two three', }; const expected = { + activeFormats: [ strong ], formats: [ , , , [ strong ], [ em, strong ], [ em, strong ], [ em ], , , , , , , ], text: 'one two three', }; @@ -40,6 +41,7 @@ describe( 'applyFormat', () => { end: 6, }; const expected = { + activeFormats: [ strong ], formats: [ , , , [ strong ], [ em, strong ], [ em, strong ], [ em ], , , , , , , ], text: 'one two three', start: 3, @@ -72,12 +74,14 @@ describe( 'applyFormat', () => { it( 'should apply format on existing format if selection is collapsed', () => { const record = { + activeFormats: [ a ], formats: [ , , , , [ a ], [ a ], [ a ], , , , , , , ], text: 'one two three', start: 4, end: 4, }; const expected = { + activeFormats: [ a2 ], formats: [ , , , , [ a2 ], [ a2 ], [ a2 ], , , , , , , ], text: 'one two three', start: 4, diff --git a/packages/rich-text/src/test/remove-format.js b/packages/rich-text/src/test/remove-format.js index 4d6b10f0d27fb..343eb8c47e011 100644 --- a/packages/rich-text/src/test/remove-format.js +++ b/packages/rich-text/src/test/remove-format.js @@ -21,6 +21,7 @@ describe( 'removeFormat', () => { }; const expected = { formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], + activeFormats: [], text: 'one two three', }; const result = removeFormat( deepFreeze( record ), 'strong', 3, 6 ); @@ -37,6 +38,7 @@ describe( 'removeFormat', () => { }; const expected = { formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], + activeFormats: [], text: 'one two three', }; const result = removeFormat( deepFreeze( record ), 'strong', 4, 4 ); diff --git a/packages/rich-text/src/test/toggle-format.js b/packages/rich-text/src/test/toggle-format.js index 4a890a43bfae5..b86d18a436f3a 100644 --- a/packages/rich-text/src/test/toggle-format.js +++ b/packages/rich-text/src/test/toggle-format.js @@ -23,6 +23,7 @@ describe( 'toggleFormat', () => { }; const expected = { formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], + activeFormats: [], text: 'one two three', start: 3, end: 6, @@ -43,6 +44,7 @@ describe( 'toggleFormat', () => { }; const expected = { formats: [ , , , [ strong ], [ em, strong ], [ em, strong ], [ em ], , , , , , , ], + activeFormats: [ strong ], text: 'one two three', start: 3, end: 6, diff --git a/test/integration/blocks-raw-handling.spec.js b/test/integration/blocks-raw-handling.spec.js index 83315ba0d38a6..fda576a042cbb 100644 --- a/test/integration/blocks-raw-handling.spec.js +++ b/test/integration/blocks-raw-handling.spec.js @@ -236,6 +236,8 @@ describe( 'Blocks raw handling', () => { 'apple', 'google-docs', 'google-docs-table', + 'google-docs-table-with-comments', + 'google-docs-with-comments', 'ms-word', 'ms-word-styled', 'ms-word-online', diff --git a/test/integration/fixtures/google-docs-in.html b/test/integration/fixtures/google-docs-in.html index c667fddceab44..e70f79825bf30 100644 --- a/test/integration/fixtures/google-docs-in.html +++ b/test/integration/fixtures/google-docs-in.html @@ -1 +1 @@ -

This is a title


This is a heading


Formatting test: bold, italic, link, strikethrough, superscript, subscript, nested.



  1. One

  2. Two

  3. Three


One

Two

Three

1

2

3

I

II

III




An image:



\ No newline at end of file +

This is a title


This is a heading


Formatting test: bold, italic, link, strikethrough, superscript, subscript, nested.



  1. One

  2. Two

  3. Three


One

Two

Three

1

2

3

I

II

III




An image:



\ No newline at end of file diff --git a/test/integration/fixtures/google-docs-table-in.html b/test/integration/fixtures/google-docs-table-in.html index 8a6b117fa6ed5..ad03a2dff0517 100644 --- a/test/integration/fixtures/google-docs-table-in.html +++ b/test/integration/fixtures/google-docs-table-in.html @@ -1 +1 @@ -

One

Two

Three

1

2

3

I

II

III

+

One

Two

Three

1

2

3

I

II

III

\ No newline at end of file diff --git a/test/integration/fixtures/google-docs-table-with-comments-in.html b/test/integration/fixtures/google-docs-table-with-comments-in.html new file mode 100644 index 0000000000000..a23b54c17a5e3 --- /dev/null +++ b/test/integration/fixtures/google-docs-table-with-comments-in.html @@ -0,0 +1,4 @@ + +

One

Two

Three

1

2

3

I

II

III

+ + \ No newline at end of file diff --git a/test/integration/fixtures/google-docs-table-with-comments-out.html b/test/integration/fixtures/google-docs-table-with-comments-out.html new file mode 100644 index 0000000000000..697c2d41ea5cd --- /dev/null +++ b/test/integration/fixtures/google-docs-table-with-comments-out.html @@ -0,0 +1,3 @@ + +
OneTwoThree
123
IIIIII
+ diff --git a/test/integration/fixtures/google-docs-with-comments-in.html b/test/integration/fixtures/google-docs-with-comments-in.html new file mode 100644 index 0000000000000..e838e0198fe8f --- /dev/null +++ b/test/integration/fixtures/google-docs-with-comments-in.html @@ -0,0 +1,4 @@ + +

This is a title


This is a heading


Formatting test: bold, italic, link, strikethrough, superscript, subscript, nested.



  1. One

  2. Two

  3. Three


One

Two

Three

1

2

3

I

II

III




An image:



+ + \ No newline at end of file diff --git a/test/integration/fixtures/google-docs-with-comments-out.html b/test/integration/fixtures/google-docs-with-comments-out.html new file mode 100644 index 0000000000000..7733ca660bdd0 --- /dev/null +++ b/test/integration/fixtures/google-docs-with-comments-out.html @@ -0,0 +1,35 @@ + +

This is a title

+ + + +

This is a heading

+ + + +

Formatting test: bold, italic, link, strikethrough, superscript, subscript, nested.

+ + + + + + + +
  1. One
  2. Two
  3. Three
+ + + +
OneTwoThree
123
IIIIII
+ + + +
+ + + +

An image:

+ + + +
+