diff --git a/package-lock.json b/package-lock.json
index 530c3d48d08708..897d57ec68a79e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -54959,6 +54959,7 @@
"@wordpress/is-shallow-equal": "file:../is-shallow-equal",
"@wordpress/keycodes": "file:../keycodes",
"@wordpress/priority-queue": "file:../priority-queue",
+ "@wordpress/undo-manager": "file:../undo-manager",
"change-case": "^4.1.2",
"clipboard": "^2.0.8",
"mousetrap": "^1.6.5",
@@ -67872,6 +67873,7 @@
"@wordpress/is-shallow-equal": "file:../is-shallow-equal",
"@wordpress/keycodes": "file:../keycodes",
"@wordpress/priority-queue": "file:../priority-queue",
+ "@wordpress/undo-manager": "file:../undo-manager",
"change-case": "^4.1.2",
"clipboard": "^2.0.8",
"mousetrap": "^1.6.5",
diff --git a/packages/compose/CHANGELOG.md b/packages/compose/CHANGELOG.md
index ab2f98424481d5..1c9f30d2f7fbc4 100644
--- a/packages/compose/CHANGELOG.md
+++ b/packages/compose/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+### Features
+
+- `useStateWithHistory`: Add a new hook to manage state with undo/redo support.
+
## 6.18.0 (2023-08-31)
## 6.17.0 (2023-08-16)
diff --git a/packages/compose/README.md b/packages/compose/README.md
index 62ebdef6d798ed..b0cff6e7128db4 100644
--- a/packages/compose/README.md
+++ b/packages/compose/README.md
@@ -484,6 +484,18 @@ const App = () => {
};
```
+### useStateWithHistory
+
+useState with undo/redo history.
+
+_Parameters_
+
+- _initialValue_ `T`: Initial value.
+
+_Returns_
+
+- Value, setValue, hasUndo, hasRedo, undo, redo.
+
### useThrottle
Throttles a function similar to Lodash's `throttle`. A new throttled function will be returned and any scheduled calls cancelled if any of the arguments change, including the function to throttle, so please wrap functions created on render in components in `useCallback`.
diff --git a/packages/compose/package.json b/packages/compose/package.json
index e8d98651f768be..afe3eabfc4bb6c 100644
--- a/packages/compose/package.json
+++ b/packages/compose/package.json
@@ -37,6 +37,7 @@
"@wordpress/is-shallow-equal": "file:../is-shallow-equal",
"@wordpress/keycodes": "file:../keycodes",
"@wordpress/priority-queue": "file:../priority-queue",
+ "@wordpress/undo-manager": "file:../undo-manager",
"change-case": "^4.1.2",
"clipboard": "^2.0.8",
"mousetrap": "^1.6.5",
diff --git a/packages/compose/src/hooks/use-state-with-history/index.ts b/packages/compose/src/hooks/use-state-with-history/index.ts
new file mode 100644
index 00000000000000..39aeab6a944b8f
--- /dev/null
+++ b/packages/compose/src/hooks/use-state-with-history/index.ts
@@ -0,0 +1,101 @@
+/**
+ * WordPress dependencies
+ */
+import { createUndoManager } from '@wordpress/undo-manager';
+import { useCallback, useReducer } from '@wordpress/element';
+import type { UndoManager } from '@wordpress/undo-manager';
+
+type UndoRedoState< T > = {
+ manager: UndoManager;
+ value: T;
+};
+
+function undoRedoReducer< T >(
+ state: UndoRedoState< T >,
+ action:
+ | { type: 'UNDO' }
+ | { type: 'REDO' }
+ | { type: 'RECORD'; value: T; isStaged: boolean }
+): UndoRedoState< T > {
+ switch ( action.type ) {
+ case 'UNDO': {
+ const undoRecord = state.manager.undo();
+ if ( undoRecord ) {
+ return {
+ ...state,
+ value: undoRecord[ 0 ].changes.prop.from,
+ };
+ }
+ return state;
+ }
+ case 'REDO': {
+ const redoRecord = state.manager.redo();
+ if ( redoRecord ) {
+ return {
+ ...state,
+ value: redoRecord[ 0 ].changes.prop.to,
+ };
+ }
+ return state;
+ }
+ case 'RECORD': {
+ state.manager.addRecord(
+ [
+ {
+ id: 'object',
+ changes: {
+ prop: { from: state.value, to: action.value },
+ },
+ },
+ ],
+ action.isStaged
+ );
+ return {
+ ...state,
+ value: action.value,
+ };
+ }
+ }
+
+ return state;
+}
+
+function initReducer< T >( value: T ) {
+ return {
+ manager: createUndoManager(),
+ value,
+ };
+}
+
+/**
+ * useState with undo/redo history.
+ *
+ * @param initialValue Initial value.
+ * @return Value, setValue, hasUndo, hasRedo, undo, redo.
+ */
+export default function useStateWithHistory< T >( initialValue: T ) {
+ const [ state, dispatch ] = useReducer(
+ undoRedoReducer,
+ initialValue,
+ initReducer
+ );
+
+ return {
+ value: state.value,
+ setValue: useCallback( ( newValue: T, isStaged: boolean ) => {
+ dispatch( {
+ type: 'RECORD',
+ value: newValue,
+ isStaged,
+ } );
+ }, [] ),
+ hasUndo: state.manager.hasUndo(),
+ hasRedo: state.manager.hasRedo(),
+ undo: useCallback( () => {
+ dispatch( { type: 'UNDO' } );
+ }, [] ),
+ redo: useCallback( () => {
+ dispatch( { type: 'REDO' } );
+ }, [] ),
+ };
+}
diff --git a/packages/compose/src/hooks/use-state-with-history/test/index.js b/packages/compose/src/hooks/use-state-with-history/test/index.js
new file mode 100644
index 00000000000000..93db929f7e316f
--- /dev/null
+++ b/packages/compose/src/hooks/use-state-with-history/test/index.js
@@ -0,0 +1,59 @@
+/**
+ * External dependencies
+ */
+import { screen, render, fireEvent } from '@testing-library/react';
+
+/**
+ * Internal dependencies
+ */
+import useStateWithHistory from '../';
+
+const TestComponent = () => {
+ const { value, setValue, hasUndo, hasRedo, undo, redo } =
+ useStateWithHistory( 'foo' );
+
+ return (
+
+ setValue( event.target.value ) }
+ />
+
+
+
+ );
+};
+
+describe( 'useStateWithHistory', () => {
+ it( 'should allow undo/redo', async () => {
+ render( );
+ const input = screen.getByRole( 'textbox' );
+ expect( input ).toHaveValue( 'foo' );
+ const buttonUndo = screen.getByRole( 'button', { name: 'Undo' } );
+ const buttonRedo = screen.getByRole( 'button', { name: 'Redo' } );
+ expect( buttonUndo ).toBeDisabled();
+ expect( buttonRedo ).toBeDisabled();
+
+ // Make a change
+ fireEvent.change( input, { target: { value: 'bar' } } );
+ expect( input ).toHaveValue( 'bar' );
+ expect( buttonUndo ).toBeEnabled();
+ expect( buttonRedo ).toBeDisabled();
+
+ // Undo the change
+ fireEvent.click( buttonUndo );
+ expect( input ).toHaveValue( 'foo' );
+ expect( buttonUndo ).toBeDisabled();
+ expect( buttonRedo ).toBeEnabled();
+
+ // Redo the change
+ fireEvent.click( buttonRedo );
+ expect( input ).toHaveValue( 'bar' );
+ expect( buttonUndo ).toBeEnabled();
+ expect( buttonRedo ).toBeDisabled();
+ } );
+} );
diff --git a/packages/compose/src/index.js b/packages/compose/src/index.js
index 01b08000bee205..1a667c98cb6905 100644
--- a/packages/compose/src/index.js
+++ b/packages/compose/src/index.js
@@ -33,6 +33,7 @@ export { default as useKeyboardShortcut } from './hooks/use-keyboard-shortcut';
export { default as useMediaQuery } from './hooks/use-media-query';
export { default as usePrevious } from './hooks/use-previous';
export { default as useReducedMotion } from './hooks/use-reduced-motion';
+export { default as useStateWithHistory } from './hooks/use-state-with-history';
export { default as useViewportMatch } from './hooks/use-viewport-match';
export { default as useResizeObserver } from './hooks/use-resize-observer';
export { default as useAsyncList } from './hooks/use-async-list';
diff --git a/packages/compose/tsconfig.json b/packages/compose/tsconfig.json
index 56cf355929c5e0..a1ea50bcc54be1 100644
--- a/packages/compose/tsconfig.json
+++ b/packages/compose/tsconfig.json
@@ -11,7 +11,8 @@
{ "path": "../element" },
{ "path": "../is-shallow-equal" },
{ "path": "../keycodes" },
- { "path": "../priority-queue" }
+ { "path": "../priority-queue" },
+ { "path": "../undo-manager" }
],
"include": [ "src/**/*" ]
}
diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js
index a79d1236682582..bc24782f2f4edf 100644
--- a/packages/core-data/src/actions.js
+++ b/packages/core-data/src/actions.js
@@ -425,14 +425,13 @@ export const editEntityRecord =
export const undo =
() =>
( { select, dispatch } ) => {
- const undoEdit = select.getUndoManager().getUndoRecord();
- if ( ! undoEdit ) {
+ const undoRecord = select.getUndoManager().undo();
+ if ( ! undoRecord ) {
return;
}
- select.getUndoManager().undo();
dispatch( {
type: 'UNDO',
- record: undoEdit,
+ record: undoRecord,
} );
};
@@ -443,14 +442,13 @@ export const undo =
export const redo =
() =>
( { select, dispatch } ) => {
- const redoEdit = select.getUndoManager().getRedoRecord();
- if ( ! redoEdit ) {
+ const redoRecord = select.getUndoManager().redo();
+ if ( ! redoRecord ) {
return;
}
- select.getUndoManager().redo();
dispatch( {
type: 'REDO',
- record: redoEdit,
+ record: redoRecord,
} );
};
diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts
index e4fb2eada0cf87..8a5268773483da 100644
--- a/packages/core-data/src/selectors.ts
+++ b/packages/core-data/src/selectors.ts
@@ -904,7 +904,7 @@ export function getRedoEdit( state: State ): Optional< any > {
* @return Whether there is a previous edit or not.
*/
export function hasUndo( state: State ): boolean {
- return Boolean( state.undoManager.getUndoRecord() );
+ return state.undoManager.hasUndo();
}
/**
@@ -916,7 +916,7 @@ export function hasUndo( state: State ): boolean {
* @return Whether there is a next edit or not.
*/
export function hasRedo( state: State ): boolean {
- return Boolean( state.undoManager.getRedoRecord() );
+ return state.undoManager.hasRedo();
}
/**
diff --git a/packages/undo-manager/src/index.js b/packages/undo-manager/src/index.js
index 379172943dfb02..2873fb7fde8af2 100644
--- a/packages/undo-manager/src/index.js
+++ b/packages/undo-manager/src/index.js
@@ -153,19 +153,29 @@ export function createUndoManager() {
dropPendingRedos();
appendStagedRecordToLatestHistoryRecord();
}
+ const undoRecord = history[ history.length - 1 + offset ];
+ if ( ! undoRecord ) {
+ return;
+ }
offset -= 1;
+ return undoRecord;
},
redo() {
+ const redoRecord = history[ history.length + offset ];
+ if ( ! redoRecord ) {
+ return;
+ }
offset += 1;
+ return redoRecord;
},
- getUndoRecord() {
- return history[ history.length - 1 + offset ];
+ hasUndo() {
+ return !! history[ history.length - 1 + offset ];
},
- getRedoRecord() {
- return history[ history.length + offset ];
+ hasRedo() {
+ return !! history[ history.length + offset ];
},
};
}
diff --git a/packages/undo-manager/src/test/index.js b/packages/undo-manager/src/test/index.js
index 32ec2713f7bc28..97af523589c1c3 100644
--- a/packages/undo-manager/src/test/index.js
+++ b/packages/undo-manager/src/test/index.js
@@ -5,98 +5,101 @@ import { createUndoManager } from '../';
describe( 'Undo Manager', () => {
it( 'stacks undo levels', () => {
- const undo = createUndoManager();
+ const manager = createUndoManager();
- undo.addRecord( [
+ manager.addRecord( [
{ id: '1', changes: { value: { from: undefined, to: 1 } } },
] );
- expect( undo.getUndoRecord() ).toEqual( [
+ expect( manager.undo() ).toEqual( [
{ id: '1', changes: { value: { from: undefined, to: 1 } } },
] );
+ manager.redo();
- undo.addRecord( [
+ manager.addRecord( [
{ id: '1', changes: { value: { from: 1, to: 2 } } },
] );
- undo.addRecord( [
+ manager.addRecord( [
{ id: '1', changes: { value: { from: 2, to: 3 } } },
] );
- expect( undo.getUndoRecord() ).toEqual( [
+ expect( manager.undo() ).toEqual( [
{ id: '1', changes: { value: { from: 2, to: 3 } } },
] );
} );
it( 'handles undos/redos', () => {
- const undo = createUndoManager();
- undo.addRecord( [
+ const manager = createUndoManager();
+ manager.addRecord( [
{ id: '1', changes: { value: { from: undefined, to: 1 } } },
] );
- undo.addRecord( [
+ manager.addRecord( [
{ id: '1', changes: { value: { from: 1, to: 2 } } },
] );
- undo.addRecord( [
+ manager.addRecord( [
{ id: '1', changes: { value: { from: 2, to: 3 } } },
] );
- undo.undo();
- expect( undo.getUndoRecord() ).toEqual( [
+ manager.undo();
+ expect( manager.undo() ).toEqual( [
{ id: '1', changes: { value: { from: 1, to: 2 } } },
] );
- expect( undo.getRedoRecord() ).toEqual( [
+ manager.redo();
+ expect( manager.redo() ).toEqual( [
{ id: '1', changes: { value: { from: 2, to: 3 } } },
] );
- undo.undo();
- expect( undo.getUndoRecord() ).toEqual( [
+ manager.undo();
+ manager.undo();
+ expect( manager.undo() ).toEqual( [
{ id: '1', changes: { value: { from: undefined, to: 1 } } },
] );
- expect( undo.getRedoRecord() ).toEqual( [
+ manager.redo();
+ expect( manager.redo() ).toEqual( [
{ id: '1', changes: { value: { from: 1, to: 2 } } },
] );
- undo.redo();
- undo.redo();
- expect( undo.getUndoRecord() ).toEqual( [
+ manager.redo();
+ expect( manager.undo() ).toEqual( [
{ id: '1', changes: { value: { from: 2, to: 3 } } },
] );
+ manager.redo();
- undo.addRecord( [
+ manager.addRecord( [
{ id: '1', changes: { value: { from: 3, to: 4 } } },
] );
- expect( undo.getUndoRecord() ).toEqual( [
+ expect( manager.undo() ).toEqual( [
{ id: '1', changes: { value: { from: 3, to: 4 } } },
] );
// Check that undoing and editing will slice of
// all the levels after the current one.
- undo.undo();
- undo.undo();
- undo.addRecord( [
+ manager.undo();
+ manager.addRecord( [
{ id: '1', changes: { value: { from: 2, to: 5 } } },
] );
- undo.undo();
- expect( undo.getUndoRecord() ).toEqual( [
+ manager.undo();
+ expect( manager.undo() ).toEqual( [
{ id: '1', changes: { value: { from: 1, to: 2 } } },
] );
} );
it( 'handles staged edits', () => {
- const undo = createUndoManager();
- undo.addRecord( [
+ const manager = createUndoManager();
+ manager.addRecord( [
{ id: '1', changes: { value: { from: undefined, to: 1 } } },
] );
- undo.addRecord(
+ manager.addRecord(
[ { id: '1', changes: { value2: { from: undefined, to: 2 } } } ],
true
);
- undo.addRecord(
+ manager.addRecord(
[ { id: '1', changes: { value: { from: 1, to: 3 } } } ],
true
);
- undo.addRecord( [
+ manager.addRecord( [
{ id: '1', changes: { value: { from: 3, to: 4 } } },
] );
- undo.undo();
- expect( undo.getUndoRecord() ).toEqual( [
+ manager.undo();
+ expect( manager.undo() ).toEqual( [
{
id: '1',
changes: {
@@ -108,31 +111,31 @@ describe( 'Undo Manager', () => {
} );
it( 'handles explicit undo level creation', () => {
- const undo = createUndoManager();
- undo.addRecord( [
+ const manager = createUndoManager();
+ manager.addRecord( [
{ id: '1', changes: { value: { from: undefined, to: 1 } } },
] );
// These three calls do nothing because they're empty.
- undo.addRecord( [] );
- undo.addRecord();
- undo.addRecord( [
+ manager.addRecord( [] );
+ manager.addRecord();
+ manager.addRecord( [
{ id: '1', changes: { value: { from: 1, to: 1 } } },
] );
// Check that nothing happens if there are no pending
// transient edits.
- undo.undo();
- expect( undo.getUndoRecord() ).toBe( undefined );
- undo.redo();
+ manager.undo();
+ expect( manager.undo() ).toBe( undefined );
+ manager.redo();
// Check that transient edits are merged into the last
// edits.
- undo.addRecord(
+ manager.addRecord(
[ { id: '1', changes: { value2: { from: undefined, to: 2 } } } ],
true
);
- undo.addRecord( [] ); // Records the staged edits.
- undo.undo();
- expect( undo.getRedoRecord() ).toEqual( [
+ manager.addRecord( [] ); // Records the staged edits.
+ manager.undo();
+ expect( manager.redo() ).toEqual( [
{
id: '1',
changes: {
@@ -144,16 +147,16 @@ describe( 'Undo Manager', () => {
} );
it( 'explicitly creates an undo level when undoing while there are pending transient edits', () => {
- const undo = createUndoManager();
- undo.addRecord( [
+ const manager = createUndoManager();
+ manager.addRecord( [
{ id: '1', changes: { value: { from: undefined, to: 1 } } },
] );
- undo.addRecord(
+ manager.addRecord(
[ { id: '1', changes: { value2: { from: undefined, to: 2 } } } ],
true
);
- undo.undo();
- expect( undo.getRedoRecord() ).toEqual( [
+ manager.undo();
+ expect( manager.redo() ).toEqual( [
{
id: '1',
changes: {
@@ -165,9 +168,9 @@ describe( 'Undo Manager', () => {
} );
it( 'supports records as ids', () => {
- const undo = createUndoManager();
+ const manager = createUndoManager();
- undo.addRecord(
+ manager.addRecord(
[
{
id: { kind: 'postType', name: 'post', recordId: 1 },
@@ -176,7 +179,7 @@ describe( 'Undo Manager', () => {
],
true
);
- undo.addRecord(
+ manager.addRecord(
[
{
id: { kind: 'postType', name: 'post', recordId: 1 },
@@ -185,7 +188,7 @@ describe( 'Undo Manager', () => {
],
true
);
- undo.addRecord(
+ manager.addRecord(
[
{
id: { kind: 'postType', name: 'post', recordId: 2 },
@@ -194,8 +197,8 @@ describe( 'Undo Manager', () => {
],
true
);
- undo.addRecord();
- expect( undo.getUndoRecord() ).toEqual( [
+ manager.addRecord();
+ expect( manager.undo() ).toEqual( [
{
id: { kind: 'postType', name: 'post', recordId: 1 },
changes: {
@@ -213,15 +216,15 @@ describe( 'Undo Manager', () => {
} );
it( 'should ignore empty records', () => {
- const undo = createUndoManager();
+ const manager = createUndoManager();
// All the following changes are considered empty for different reasons.
- undo.addRecord();
- undo.addRecord( [] );
- undo.addRecord( [
+ manager.addRecord();
+ manager.addRecord( [] );
+ manager.addRecord( [
{ id: '1', changes: { a: { from: 'value', to: 'value' } } },
] );
- undo.addRecord( [
+ manager.addRecord( [
{
id: '1',
changes: {
@@ -231,12 +234,12 @@ describe( 'Undo Manager', () => {
},
] );
- expect( undo.getUndoRecord() ).toBeUndefined();
+ expect( manager.undo() ).toBeUndefined();
// The following changes is not empty
// and should also record the function changes in the history.
- undo.addRecord( [
+ manager.addRecord( [
{
id: '1',
changes: {
@@ -246,7 +249,7 @@ describe( 'Undo Manager', () => {
},
] );
- const undoRecord = undo.getUndoRecord();
+ const undoRecord = manager.undo();
expect( undoRecord ).not.toBeUndefined();
// b is included in the changes.
expect( Object.keys( undoRecord[ 0 ].changes ) ).toEqual( [
diff --git a/packages/undo-manager/src/types.ts b/packages/undo-manager/src/types.ts
index e2e1d995f8e5db..ead93df121403b 100644
--- a/packages/undo-manager/src/types.ts
+++ b/packages/undo-manager/src/types.ts
@@ -12,8 +12,8 @@ export type HistoryRecord = Array< HistoryChanges >;
export type UndoManager = {
addRecord: ( record: HistoryRecord, isStaged: boolean ) => void;
- undo: () => void;
- redo: () => void;
- getUndoRecord: () => HistoryRecord;
- getRedoRecord: () => HistoryRecord;
+ undo: () => HistoryRecord | undefined;
+ redo: () => HistoryRecord | undefined;
+ hasUndo: () => boolean;
+ hasRedo: () => boolean;
};
diff --git a/storybook/main.js b/storybook/main.js
index ad4756fa2d472b..add59003bbc8bf 100644
--- a/storybook/main.js
+++ b/storybook/main.js
@@ -73,7 +73,7 @@ module.exports = {
...config.module.rules,
{
// Adds a `sourceLink` parameter to the story metadata, based on the file path
- test: /\/stories\/.+\.(j|t)sx?$/,
+ test: /\/stories\/.+\.story\.(j|t)sx?$/,
loader: path.resolve(
__dirname,
'./webpack/source-link-loader.js'
diff --git a/storybook/stories/playground/box/index.js b/storybook/stories/playground/box/index.js
new file mode 100644
index 00000000000000..444b7810e5e89e
--- /dev/null
+++ b/storybook/stories/playground/box/index.js
@@ -0,0 +1,44 @@
+/**
+ * WordPress dependencies
+ */
+import { useEffect, useState } from '@wordpress/element';
+import { registerCoreBlocks } from '@wordpress/block-library';
+import {
+ BlockEditorProvider,
+ BlockCanvas,
+ BlockTools,
+} from '@wordpress/block-editor';
+
+/**
+ * Internal dependencies
+ */
+import editorStyles from '../editor-styles';
+import './style.css';
+
+export default function EditorBox() {
+ const [ blocks, updateBlocks ] = useState( [] );
+
+ useEffect( () => {
+ registerCoreBlocks();
+ }, [] );
+
+ return (
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions
+ event.stopPropagation() }
+ >
+
+
+
+
+
+ );
+}
diff --git a/storybook/stories/playground/box/style.css b/storybook/stories/playground/box/style.css
new file mode 100644
index 00000000000000..f0f0329e0e2bc4
--- /dev/null
+++ b/storybook/stories/playground/box/style.css
@@ -0,0 +1,3 @@
+.editor-box {
+ border: 1px solid #ddd;
+}
diff --git a/storybook/stories/playground/fullpage/index.js b/storybook/stories/playground/fullpage/index.js
new file mode 100644
index 00000000000000..961c15f71f31d0
--- /dev/null
+++ b/storybook/stories/playground/fullpage/index.js
@@ -0,0 +1,55 @@
+/**
+ * WordPress dependencies
+ */
+import { useEffect, useState } from '@wordpress/element';
+import {
+ BlockCanvas,
+ BlockEditorProvider,
+ BlockTools,
+ BlockInspector,
+} from '@wordpress/block-editor';
+import { registerCoreBlocks } from '@wordpress/block-library';
+import '@wordpress/format-library';
+
+/**
+ * Internal dependencies
+ */
+import styles from './style.lazy.scss';
+import { editorStyles } from '../editor-styles';
+
+export default function EditorFullPage() {
+ const [ blocks, updateBlocks ] = useState( [] );
+
+ useEffect( () => {
+ registerCoreBlocks();
+ }, [] );
+
+ // Ensures that the CSS intended for the playground (especially the style resets)
+ // are only loaded for the playground and don't leak into other stories.
+ useEffect( () => {
+ styles.use();
+
+ return styles.unuse;
+ } );
+
+ return (
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions
+ event.stopPropagation() }
+ >
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/storybook/stories/playground/reset.scss b/storybook/stories/playground/fullpage/reset.scss
similarity index 100%
rename from storybook/stories/playground/reset.scss
rename to storybook/stories/playground/fullpage/reset.scss
diff --git a/storybook/stories/playground/style.lazy.scss b/storybook/stories/playground/fullpage/style.lazy.scss
similarity index 100%
rename from storybook/stories/playground/style.lazy.scss
rename to storybook/stories/playground/fullpage/style.lazy.scss
diff --git a/storybook/stories/playground/index.story.js b/storybook/stories/playground/index.story.js
index d3d44e0dedf707..f49269f37a814d 100644
--- a/storybook/stories/playground/index.story.js
+++ b/storybook/stories/playground/index.story.js
@@ -1,58 +1,9 @@
-/**
- * WordPress dependencies
- */
-import { useEffect, useState } from '@wordpress/element';
-import {
- BlockCanvas,
- BlockEditorProvider,
- BlockTools,
- BlockInspector,
-} from '@wordpress/block-editor';
-import { registerCoreBlocks } from '@wordpress/block-library';
-import '@wordpress/format-library';
-
/**
* Internal dependencies
*/
-import styles from './style.lazy.scss';
-import { editorStyles } from './editor-styles';
-
-function App() {
- const [ blocks, updateBlocks ] = useState( [] );
-
- useEffect( () => {
- registerCoreBlocks();
- }, [] );
-
- // Ensures that the CSS intended for the playground (especially the style resets)
- // are only loaded for the playground and don't leak into other stories.
- useEffect( () => {
- styles.use();
-
- return styles.unuse;
- } );
-
- return (
- // eslint-disable-next-line jsx-a11y/no-static-element-interactions
- event.stopPropagation() }
- >
-
-
-
-
-
-
-
-
-
- );
-}
+import EditorFullPage from './fullpage';
+import EditorBox from './box';
+import EditorWithUndoRedo from './with-undo-redo';
export default {
title: 'Playground/Block Editor',
@@ -62,38 +13,13 @@ export default {
};
export const _default = () => {
- return ;
+ return ;
};
-function EditorBox() {
- const [ blocks, updateBlocks ] = useState( [] );
-
- useEffect( () => {
- registerCoreBlocks();
- }, [] );
-
- return (
- // eslint-disable-next-line jsx-a11y/no-static-element-interactions
- event.stopPropagation() }
- >
-
-
-
-
-
- );
-}
-
export const Box = () => {
return ;
};
+
+export const UndoRedo = () => {
+ return ;
+};
diff --git a/storybook/stories/playground/with-undo-redo/index.js b/storybook/stories/playground/with-undo-redo/index.js
new file mode 100644
index 00000000000000..a51d8624282a6d
--- /dev/null
+++ b/storybook/stories/playground/with-undo-redo/index.js
@@ -0,0 +1,67 @@
+/**
+ * WordPress dependencies
+ */
+import { useEffect } from '@wordpress/element';
+import { useStateWithHistory } from '@wordpress/compose';
+import { registerCoreBlocks } from '@wordpress/block-library';
+import {
+ BlockEditorProvider,
+ BlockCanvas,
+ BlockTools,
+} from '@wordpress/block-editor';
+import { Button } from '@wordpress/components';
+import { undo as undoIcon, redo as redoIcon } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import editorStyles from '../editor-styles';
+import './style.css';
+
+export default function EditorWithUndoRedo() {
+ const { value, setValue, hasUndo, hasRedo, undo, redo } =
+ useStateWithHistory( { blocks: [] } );
+
+ useEffect( () => {
+ registerCoreBlocks();
+ }, [] );
+
+ return (
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions
+ event.stopPropagation() }
+ >
+
+ setValue( { blocks, selection }, true )
+ }
+ onChange={ ( blocks, { selection } ) =>
+ setValue( { blocks, selection }, false )
+ }
+ settings={ {
+ hasFixedToolbar: true,
+ } }
+ >
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/storybook/stories/playground/with-undo-redo/style.css b/storybook/stories/playground/with-undo-redo/style.css
new file mode 100644
index 00000000000000..a3f0bd5d23debf
--- /dev/null
+++ b/storybook/stories/playground/with-undo-redo/style.css
@@ -0,0 +1,10 @@
+.editor-with-undo-redo {
+ border: 1px solid #ddd;
+}
+
+.editor-with-undo-redo__toolbar {
+ display: flex;
+ align-items: center;
+ border-bottom: 1px solid #ddd;
+ height: 48px;
+}