diff --git a/packages/block-editor/src/components/block-list/block-html.js b/packages/block-editor/src/components/block-list/block-html.js index c670133de17c41..fd724258ada1d8 100644 --- a/packages/block-editor/src/components/block-list/block-html.js +++ b/packages/block-editor/src/components/block-list/block-html.js @@ -20,9 +20,11 @@ import { * Internal dependencies */ import { store as blockEditorStore } from '../../store'; +import { useNativeUndo } from '../../utils/native-undo'; function BlockHTML( { clientId } ) { const [ html, setHtml ] = useState( '' ); + const nativeUndoRef = useNativeUndo(); const block = useSelect( ( select ) => select( blockEditorStore ).getBlock( clientId ), [ clientId ] @@ -83,6 +85,9 @@ function BlockHTML( { clientId } ) { value={ html } onBlur={ onChange } onChange={ ( event ) => setHtml( event.target.value ) } + // The edits are local state until committed on blur, so undo + // and redo must remain the browser's own within the field. + ref={ nativeUndoRef } /> ); } diff --git a/packages/block-editor/src/components/link-control/index.js b/packages/block-editor/src/components/link-control/index.js index 075036a47dec82..cf6c501d74286b 100644 --- a/packages/block-editor/src/components/link-control/index.js +++ b/packages/block-editor/src/components/link-control/index.js @@ -16,7 +16,7 @@ import { } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { useRef, useState, useEffect, useMemo } from '@wordpress/element'; -import { useInstanceId } from '@wordpress/compose'; +import { useInstanceId, useMergeRefs } from '@wordpress/compose'; import { focus } from '@wordpress/dom'; import { ENTER } from '@wordpress/keycodes'; import { isShallowEqualObjects } from '@wordpress/is-shallow-equal'; @@ -29,6 +29,7 @@ import { isURL, prependHTTPS } from '@wordpress/url'; /** * Internal dependencies */ +import { useNativeUndo } from '../../utils/native-undo'; import LinkControlSettingsDrawer from './settings-drawer'; import LinkControlSearchInput from './search-input'; import LinkPreview from './link-preview'; @@ -227,6 +228,7 @@ function LinkControl( { const isMountingRef = useRef( true ); const wrapperNode = useRef(); + const wrapperRef = useMergeRefs( [ wrapperNode, useNativeUndo() ] ); const textInputRef = useRef(); const searchInputRef = useRef(); // TODO: Remove entityUrlFallbackRef and previewValue in favor of value prop after taxonomy entity binding @@ -651,7 +653,10 @@ function LinkControl( { return (
second
EDIT' ); + + // Undo granularity within the field is the browser's own; press + // once per typed character. Excess presses are harmless no-ops in + // browsers that group the typing into a single undo unit, since the + // editor history must not act while the field is focused. + await pageUtils.pressKeys( 'primary+z', { times: 4 } ); + + // The browser undoes the typing within the field. The other blocks + // are untouched, and redo restores the typing. + await expect( textarea ).toHaveValue( 'second
' ); + await expect.poll( editor.getBlocks ).toMatchObject( [ + { name: 'core/paragraph', attributes: { content: 'first' } }, + { name: 'core/paragraph', attributes: { content: 'second' } }, + { name: 'core/paragraph', attributes: { content: 'third' } }, + ] ); + + await pageUtils.pressKeys( 'primaryShift+z', { times: 4 } ); + await expect( textarea ).toHaveValue( 'second
EDIT' ); + } ); + + test( 'should leave undo to the browser inside the Custom HTML dialog', async ( { + page, + pageUtils, + editor, + } ) => { + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); + await page.keyboard.type( 'a paragraph' ); + await editor.insertBlock( { + name: 'core/html', + attributes: { content: 'markup
' }, + } ); + await editor.clickBlockToolbarButton( 'Edit code' ); + + const field = page.getByRole( 'dialog' ).getByRole( 'textbox' ); + await field.click(); + await page.keyboard.press( 'End' ); + await page.keyboard.type( 'EDIT' ); + await expect( field ).toHaveValue( 'markup
EDIT' ); + + await pageUtils.pressKeys( 'primary+z', { times: 4 } ); + + // The browser undoes the typing within the field. The dialog stays + // open and the content behind it is untouched. + await expect( field ).toHaveValue( 'markup
' ); + await expect.poll( editor.getEditedPostContent ) + .toBe( ` +a paragraph
+ + + +markup
+` ); + + // Redo restores the typing. This distinguishes the browser's own + // undo from the editor history remounting the dialog: a remount + // resets the field to the same committed value, but destroys the + // browser's redo stack with it. + await pageUtils.pressKeys( 'primaryShift+z', { times: 4 } ); + await expect( field ).toHaveValue( 'markup
EDIT' ); + } ); } ); class UndoUtils {