diff --git a/packages/block-editor/src/components/rich-text/toolbar-button.js b/packages/block-editor/src/components/rich-text/toolbar-button.js index 2febd82e717c57..8c1adafca15502 100644 --- a/packages/block-editor/src/components/rich-text/toolbar-button.js +++ b/packages/block-editor/src/components/rich-text/toolbar-button.js @@ -3,8 +3,17 @@ */ import { Fill, ToolbarButton } from '@wordpress/components'; import { displayShortcut } from '@wordpress/keycodes'; +import { useRef } from '@wordpress/element'; + +export function RichTextToolbarButton( { + name, + shortcutType, + shortcutCharacter, + onClick, + ...props +} ) { + const activeElement = useRef( null ); -export function RichTextToolbarButton( { name, shortcutType, shortcutCharacter, ...props } ) { let shortcut; let fillName = 'RichText.ToolbarControls'; @@ -21,6 +30,19 @@ export function RichTextToolbarButton( { name, shortcutType, shortcutCharacter, { + activeElement.current = document.activeElement; + }, + } } + onClick={ () => { + onClick( ...arguments ); + + if ( activeElement.current ) { + activeElement.current.focus(); + activeElement.current = null; + } + } } /> ); 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 78b78101a59a5d..c237c2049ef678 100644 --- a/packages/e2e-tests/specs/__snapshots__/rich-text.test.js.snap +++ b/packages/e2e-tests/specs/__snapshots__/rich-text.test.js.snap @@ -48,6 +48,12 @@ exports[`RichText should not lose selection direction 1`] = ` " `; +exports[`RichText should not return focus when tabbing to formatting button 1`] = ` +" +

Some bold.

+" +`; + exports[`RichText should not undo backtick transform with backspace after selection change 1`] = `""`; exports[`RichText should not undo backtick transform with backspace after typing 1`] = `""`; @@ -76,14 +82,14 @@ exports[`RichText should transform backtick to code 2`] = ` " `; -exports[`RichText should update internal selection after fresh focus 1`] = ` +exports[`RichText should undo backtick transform with backspace 1`] = ` " -

12

+

\`a\`

" `; -exports[`RichText should undo backtick transform with backspace 1`] = ` +exports[`RichText should update internal selection after fresh focus 1`] = ` " -

\`a\`

+

12

" `; diff --git a/packages/e2e-tests/specs/blocks/list.test.js b/packages/e2e-tests/specs/blocks/list.test.js index 5e04dbbcfeba06..b3a9c302a038f3 100644 --- a/packages/e2e-tests/specs/blocks/list.test.js +++ b/packages/e2e-tests/specs/blocks/list.test.js @@ -182,6 +182,7 @@ describe( 'List', () => { await page.keyboard.type( 'one' ); await page.keyboard.press( 'Enter' ); await clickBlockToolbarButton( 'Indent list item' ); + await pressKeyTimes( 'Tab', 6 ); await page.keyboard.type( 'two' ); await transformBlockTo( 'Paragraph' ); @@ -264,6 +265,7 @@ describe( 'List', () => { await page.keyboard.type( 'one' ); await page.keyboard.press( 'Enter' ); await clickBlockToolbarButton( 'Indent list item' ); + await pressKeyTimes( 'Tab', 6 ); await page.keyboard.type( 'two' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( 'three' ); diff --git a/packages/e2e-tests/specs/plugins/annotations.test.js b/packages/e2e-tests/specs/plugins/annotations.test.js index 5bab673a41d806..4425e8c7e8f539 100644 --- a/packages/e2e-tests/specs/plugins/annotations.test.js +++ b/packages/e2e-tests/specs/plugins/annotations.test.js @@ -49,6 +49,7 @@ describe( 'Using Plugins API', () => { // Click add annotation button. const addAnnotationButton = ( await page.$x( "//button[contains(text(), 'Add annotation')]" ) )[ 0 ]; await addAnnotationButton.click(); + await page.evaluate( () => document.querySelector( '[contenteditable]' ).focus() ); } /** @@ -60,6 +61,7 @@ describe( 'Using Plugins API', () => { // Click remove annotations button. const addAnnotationButton = ( await page.$x( "//button[contains(text(), 'Remove annotations')]" ) )[ 0 ]; await addAnnotationButton.click(); + await page.evaluate( () => document.querySelector( '[contenteditable]' ).focus() ); } /** diff --git a/packages/e2e-tests/specs/rich-text.test.js b/packages/e2e-tests/specs/rich-text.test.js index 13acc998e39466..19db0e2a8e34a8 100644 --- a/packages/e2e-tests/specs/rich-text.test.js +++ b/packages/e2e-tests/specs/rich-text.test.js @@ -7,6 +7,7 @@ import { insertBlock, clickBlockAppender, pressKeyWithModifier, + pressKeyTimes, } from '@wordpress/e2e-test-utils'; describe( 'RichText', () => { @@ -92,6 +93,30 @@ describe( 'RichText', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + it( 'should not return focus when tabbing to formatting button', async () => { + await clickBlockAppender(); + await page.keyboard.type( 'Some ' ); + await pressKeyWithModifier( 'alt', 'F10' ); + // Wait for button to receive focus. + await page.waitForFunction( () => + document.activeElement.nodeName === 'BUTTON' + ); + await pressKeyTimes( 'Tab', 2 ); + await page.keyboard.press( 'Space' ); + await pressKeyTimes( 'Tab', 5 ); + await page.keyboard.type( 'bold' ); + await pressKeyWithModifier( 'alt', 'F10' ); + await page.waitForFunction( () => + document.activeElement.nodeName === 'BUTTON' + ); + await pressKeyTimes( 'Tab', 2 ); + await page.keyboard.press( 'Space' ); + await pressKeyTimes( 'Tab', 5 ); + await page.keyboard.type( '.' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + it( 'should transform backtick to code', async () => { await clickBlockAppender(); await page.keyboard.type( 'A `backtick`' ); diff --git a/packages/format-library/src/link/inline.js b/packages/format-library/src/link/inline.js index 815a8fcaae1d3f..37f3db42464c04 100644 --- a/packages/format-library/src/link/inline.js +++ b/packages/format-library/src/link/inline.js @@ -149,9 +149,9 @@ class InlineLinkUI extends Component { if ( isCollapsed( value ) && ! isActive ) { const toInsert = applyFormat( create( { text: url } ), format, 0, url.length ); - onChange( insert( value, toInsert ) ); + onChange( insert( value, toInsert ), { focus: true } ); } else { - onChange( applyFormat( value, format ) ); + onChange( applyFormat( value, format ), { focus: true } ); } this.resetState(); diff --git a/packages/rich-text/src/component/index.js b/packages/rich-text/src/component/index.js index a4f4fd8f220051..3a160df4674d14 100644 --- a/packages/rich-text/src/component/index.js +++ b/packages/rich-text/src/component/index.js @@ -170,6 +170,8 @@ class RichText extends Component { applyRecord( record, { domOnly } = {} ) { const { __unstableMultilineTag: multilineTag } = this.props; + this.ignoreFocus = true; + apply( { value: record, current: this.editableRef, @@ -179,6 +181,8 @@ class RichText extends Component { __unstableDomOnly: domOnly, placeholder: this.props.placeholder, } ); + + this.ignoreFocus = false; } /** @@ -276,25 +280,31 @@ class RichText extends Component { onFocus() { const { unstableOnFocus } = this.props; + if ( this.ignoreFocus ) { + return; + } + if ( unstableOnFocus ) { unstableOnFocus(); } this.recalculateBoundaryStyle(); - // We know for certain that on focus, the old selection is invalid. It - // will be recalculated on the next mouseup, keyup, or touchend event. - const index = undefined; - const activeFormats = undefined; + if ( ! this.props.__unstableIsSelected ) { + // We know for certain that on focus, the old selection is invalid. It + // will be recalculated on the next mouseup, keyup, or touchend event. + const index = undefined; + const activeFormats = undefined; - this.record = { - ...this.record, - start: index, - end: index, - activeFormats, - }; - this.props.onSelectionChange( index, index ); - this.setState( { activeFormats } ); + this.record = { + ...this.record, + start: index, + end: index, + activeFormats, + }; + this.props.onSelectionChange( index, index ); + this.setState( { activeFormats } ); + } // Update selection as soon as possible, which is at the next animation // frame. The event listener for selection changes may be added too late @@ -524,8 +534,14 @@ class RichText extends Component { * @param {Object} $2 Named options. * @param {boolean} $2.withoutHistory If true, no undo level will be * created. + * @param {boolean} $2.focus If true, the rich text element will + * receive focus. */ - onChange( record, { withoutHistory } = {} ) { + onChange( record, { withoutHistory, focus } = {} ) { + if ( focus ) { + this.editableRef.focus(); + } + this.applyRecord( record ); const { start, end, activeFormats = [] } = record; diff --git a/packages/rich-text/src/to-dom.js b/packages/rich-text/src/to-dom.js index 9d7aed3cfdfd00..3864bc802491de 100644 --- a/packages/rich-text/src/to-dom.js +++ b/packages/rich-text/src/to-dom.js @@ -278,10 +278,7 @@ export function applySelection( { startPath, endPath }, current ) { range.setStart( startContainer, startOffset ); range.setEnd( endContainer, endOffset ); - // Set back focus if focus is lost. - if ( ownerDocument.activeElement !== current ) { - current.focus(); - } + const { activeElement } = ownerDocument; if ( selection.rangeCount > 0 ) { // If the to be added range and the live range are the same, there's no @@ -294,4 +291,5 @@ export function applySelection( { startPath, endPath }, current ) { } selection.addRange( range ); + activeElement.focus(); }