Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]
Expand Down Expand Up @@ -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 }
/>
);
}
Expand Down
9 changes: 7 additions & 2 deletions packages/block-editor/src/components/link-control/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -651,7 +653,10 @@ function LinkControl( {
return (
<div
tabIndex={ -1 }
ref={ wrapperNode }
// The dialog holds local state that is only committed when the
// link is applied, so undo and redo within it must remain the
// browser's own.
ref={ wrapperRef }
className="block-editor-link-control"
>
{ isCreatingPage && (
Expand Down
3 changes: 3 additions & 0 deletions packages/block-editor/src/private-apis.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import useRemoteUrlData from './components/link-control/use-rich-url-data';
import { PrivateBlockContext } from './components/block-list/private-block-context';
import useListViewPanelState from './components/use-list-view-panel-state';
import InnerContent from './components/inner-content';
import { useNativeUndo, usesNativeUndo } from './utils/native-undo';
import {
isHashLink,
isRelativePath,
Expand Down Expand Up @@ -163,4 +164,6 @@ lock( privateApis, {
isHashLink,
isRelativePath,
InnerContent,
useNativeUndo,
usesNativeUndo,
} );
56 changes: 56 additions & 0 deletions packages/block-editor/src/utils/native-undo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* WordPress dependencies
*/
import { useRefEffect } from '@wordpress/compose';

/**
* Elements whose content the browser's own undo acts on (a dialog input, an
* HTML editing field, a search field), registered through `useNativeUndo`.
* The editor history must not act on events originating within them.
*/
const nativeUndoNodes = new WeakSet();

/**
* Returns a ref that marks an element (and its descendants) as relying on
* the browser's own undo, so the editor history leaves the undo and redo
* shortcuts alone within it.
*
* @return {Function} Ref callback.
*/
export function useNativeUndo() {
return useRefEffect( ( node ) => {
nativeUndoNodes.add( node );
return () => {
nativeUndoNodes.delete( node );
};
}, [] );
}

/**
* Returns true when the event originates from an element registered through
* `useNativeUndo`.
*
* Events coming from the editor canvas are re-dispatched on the iframe
* element, so the real target is resolved through the frame's focused
* element.
*
* @param {KeyboardEvent} event Keyboard event.
*
* @return {boolean} Whether the element relies on the browser's own undo.
*/
export function usesNativeUndo( event ) {
let { target } = event;

if ( target?.nodeName === 'IFRAME' ) {
target = target.contentDocument?.activeElement;
}

while ( target ) {
if ( nativeUndoNodes.has( target ) ) {
return true;
}
target = target.parentElement;
}

return false;
}
15 changes: 13 additions & 2 deletions packages/block-library/src/html/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ import {
__experimentalVStack as VStack,
__experimentalHStack as HStack,
} from '@wordpress/components';
import { PlainText, store as blockEditorStore } from '@wordpress/block-editor';
import {
PlainText,
store as blockEditorStore,
privateApis as blockEditorPrivateApis,
} from '@wordpress/block-editor';
import { fullscreen, square } from '@wordpress/icons';
import { useViewportMatch } from '@wordpress/compose';

Expand All @@ -25,6 +29,7 @@ import Preview from './preview';
import { parseContent, serializeContent } from './utils';

const { Tabs } = unlock( componentsPrivateApis );
const { useNativeUndo } = unlock( blockEditorPrivateApis );

export default function HTMLEditModal( { onRequestClose, content, onUpdate } ) {
// Parse content into separate sections and use as initial state
Expand All @@ -33,6 +38,9 @@ export default function HTMLEditModal( { onRequestClose, content, onUpdate } ) {
const [ editedCss, setEditedCss ] = useState( css );
const [ editedJs, setEditedJs ] = useState( js );
const [ isFullscreen, setIsFullscreen ] = useState( false );
// The fields hold local state that is only committed when the dialog is
// saved, so undo and redo within them must remain the browser's own.
const nativeUndoRef = useNativeUndo();

const isMobileViewport = useViewportMatch( 'small', '<' );

Expand Down Expand Up @@ -132,7 +140,10 @@ export default function HTMLEditModal( { onRequestClose, content, onUpdate } ) {
align="stretch"
gap={ 8 }
>
<div className="block-library-html__modal-content">
<div
ref={ nativeUndoRef }
className="block-library-html__modal-content"
>
<Tabs.TabPanel
tabId="html"
focusable={ false }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,28 @@ import {
import { isAppleOS } from '@wordpress/keycodes';
import { useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';

/**
* Internal dependencies
*/
import { unlock } from '../../lock-unlock';

const { usesNativeUndo } = unlock( blockEditorPrivateApis );

function KeyboardShortcuts( { undo, redo, save } ) {
useShortcut( 'core/customize-widgets/undo', ( event ) => {
if ( usesNativeUndo( event ) ) {
return;
}
undo();
event.preventDefault();
} );

useShortcut( 'core/customize-widgets/redo', ( event ) => {
if ( usesNativeUndo( event ) ) {
return;
}
redo();
event.preventDefault();
} );
Expand Down
2 changes: 2 additions & 0 deletions packages/e2e-tests/plugins/meta-box.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
*/
function gutenberg_test_meta_box_render_meta_box() {
echo 'Hello World';
echo '<label for="gutenberg_test_meta_box_input">Test meta box field</label>';
echo '<input type="text" id="gutenberg_test_meta_box_input" name="gutenberg_test_meta_box_input" />';
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ import clsx from 'clsx';
import { useRef, useEffect } from '@wordpress/element';
import { Spinner } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';

/**
* Internal dependencies
*/
import { store as editPostStore } from '../../../store';
import { unlock } from '../../../lock-unlock';

const { useNativeUndo } = unlock( blockEditorPrivateApis );

/**
* Render metabox area.
Expand Down Expand Up @@ -52,8 +56,12 @@ function MetaBoxesArea( { location } ) {
'is-loading': isSaving,
} );

// Meta box fields are not part of the editor history, so undo and redo
// within them must remain the browser's own.
const nativeUndoRef = useNativeUndo();

return (
<div className={ classes }>
<div className={ classes } ref={ nativeUndoRef }>
{ isSaving && (
<Spinner className="edit-post-meta-boxes-area__spinner" />
) }
Expand Down
10 changes: 10 additions & 0 deletions packages/edit-widgets/src/components/keyboard-shortcuts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,32 @@ import { isAppleOS } from '@wordpress/keycodes';
import { useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { store as coreStore } from '@wordpress/core-data';
import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';

/**
* Internal dependencies
*/
import { store as editWidgetsStore } from '../../store';
import { unlock } from '../../lock-unlock';

const { usesNativeUndo } = unlock( blockEditorPrivateApis );

function KeyboardShortcuts() {
const { redo, undo } = useDispatch( coreStore );
const { saveEditedWidgetAreas } = useDispatch( editWidgetsStore );

useShortcut( 'core/edit-widgets/undo', ( event ) => {
if ( usesNativeUndo( event ) ) {
return;
}
undo();
event.preventDefault();
} );

useShortcut( 'core/edit-widgets/redo', ( event ) => {
if ( usesNativeUndo( event ) ) {
return;
}
redo();
event.preventDefault();
} );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@
import { useShortcut } from '@wordpress/keyboard-shortcuts';
import { useDispatch, useSelect } from '@wordpress/data';
import { store as interfaceStore } from '@wordpress/interface';
import { store as blockEditorStore } from '@wordpress/block-editor';
import {
store as blockEditorStore,
privateApis as blockEditorPrivateApis,
} from '@wordpress/block-editor';

/**
* Internal dependencies
*/
import { store as editorStore } from '../../store';
import { unlock } from '../../lock-unlock';

const { usesNativeUndo } = unlock( blockEditorPrivateApis );

/**
* Handles the keyboard shortcuts for the editor.
Expand Down Expand Up @@ -61,11 +67,17 @@ export default function EditorKeyboardShortcuts() {
} );

useShortcut( 'core/editor/undo', ( event ) => {
if ( usesNativeUndo( event ) ) {
return;
}
undo();
event.preventDefault();
} );

useShortcut( 'core/editor/redo', ( event ) => {
if ( usesNativeUndo( event ) ) {
return;
}
redo();
event.preventDefault();
} );
Expand Down
38 changes: 38 additions & 0 deletions test/e2e/specs/editor/plugins/meta-boxes.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,44 @@ test.describe( 'Meta boxes', () => {
await expect( saveDraft ).toBeEnabled();
} );

test( 'should leave undo to the browser inside meta box fields', async ( {
editor,
page,
pageUtils,
} ) => {
await editor.canvas
.locator( 'role=button[name="Add default block"i]' )
.click();
await page.keyboard.type( 'canvas text' );

// Click near the label, away from the resize handle overlaying the
// center of the toggle.
await page
.getByRole( 'button', { name: 'Meta Boxes', exact: true } )
.click( { position: { x: 40, y: 10 } } );
const field = page.getByRole( 'textbox', {
name: 'Test meta box field',
} );
await field.click();
await page.keyboard.type( 'META' );
await expect( field ).toHaveValue( 'META' );

await pageUtils.pressKeys( 'primary+z', { times: 4 } );

// The browser undoes the typing within the field. The canvas is
// untouched, and redo restores the typing.
await expect( field ).toHaveValue( '' );
await expect.poll( editor.getBlocks ).toMatchObject( [
{
name: 'core/paragraph',
attributes: { content: 'canvas text' },
},
] );

await pageUtils.pressKeys( 'primaryShift+z', { times: 4 } );
await expect( field ).toHaveValue( 'META' );
} );

test( 'Should render dynamic blocks when the meta box uses the excerpt for front end rendering', async ( {
admin,
editor,
Expand Down
Loading
Loading