+
);
-}
+};
+export const TabPanel = forwardRef( UnforwardedTabPanel );
export default TabPanel;
diff --git a/packages/components/src/toolbar/toolbar/index.tsx b/packages/components/src/toolbar/toolbar/index.tsx
index db5ced5a382a21..9c837a72aa0e83 100644
--- a/packages/components/src/toolbar/toolbar/index.tsx
+++ b/packages/components/src/toolbar/toolbar/index.tsx
@@ -16,7 +16,22 @@ import deprecated from '@wordpress/deprecated';
import ToolbarGroup from '../toolbar-group';
import ToolbarContainer from './toolbar-container';
import type { ToolbarProps } from './types';
-import type { WordPressComponentProps } from '../../ui/context';
+import {
+ WordPressComponentProps,
+ ContextSystemProvider,
+} from '../../ui/context';
+
+// TODO:
+// - (optional) make the legacy `DropdownMenu` read the context variable
+// - swap the legacy `DropdownMenu` with the new version of the component
+// once it's stable
+const CONTEXT_SYSTEM_VALUE = {
+ DropdownMenu: {
+ // Note: the legacy `DropdownMenu` component is not yet reactive to this
+ // context variant. See https://github.com/WordPress/gutenberg/pull/51097.
+ variant: 'toolbar',
+ },
+};
function UnforwardedToolbar(
{
@@ -40,12 +55,14 @@ function UnforwardedToolbar(
className
);
return (
-
+
+
+
);
}
diff --git a/packages/components/src/ui/context/index.ts b/packages/components/src/ui/context/index.ts
index 54694e47304ff2..266e2c3459a547 100644
--- a/packages/components/src/ui/context/index.ts
+++ b/packages/components/src/ui/context/index.ts
@@ -4,6 +4,7 @@ export {
} from './context-system-provider';
export {
contextConnect,
+ contextConnectWithoutRef,
hasConnectNamespace,
getConnectNamespace,
} from './context-connect';
diff --git a/packages/components/src/utils/config-values.js b/packages/components/src/utils/config-values.js
index 40c15a22ec17e3..5587720573e95c 100644
--- a/packages/components/src/utils/config-values.js
+++ b/packages/components/src/utils/config-values.js
@@ -67,6 +67,7 @@ export default Object.assign( {}, CONTROL_PROPS, TOGGLE_GROUP_CONTROL_PROPS, {
cardPaddingSmall: `${ space( 4 ) }`,
cardPaddingMedium: `${ space( 4 ) } ${ space( 6 ) }`,
cardPaddingLarge: `${ space( 6 ) } ${ space( 8 ) }`,
+ popoverShadow: `0 0.7px 1px rgba(0, 0, 0, 0.1), 0 1.2px 1.7px -0.2px rgba(0, 0, 0, 0.1), 0 2.3px 3.3px -0.5px rgba(0, 0, 0, 0.1)`,
surfaceBackgroundColor: COLORS.white,
surfaceBackgroundSubtleColor: '#F3F3F3',
surfaceBackgroundTintColor: '#F5F5F5',
diff --git a/packages/components/src/view/README.md b/packages/components/src/view/README.md
index 794d7b49a0d386..c1e04ebd8cfff4 100644
--- a/packages/components/src/view/README.md
+++ b/packages/components/src/view/README.md
@@ -27,13 +27,13 @@ function Example() {
## Props
-##### as
+### as
**Type**: `string`,`E`
Render the component as another React Component or HTML Element.
-##### css
+### css
**Type**: `InterpolatedCSS`
diff --git a/packages/compose/src/hooks/use-drop-zone/README.md b/packages/compose/src/hooks/use-drop-zone/README.md
new file mode 100644
index 00000000000000..558a5f6866b54e
--- /dev/null
+++ b/packages/compose/src/hooks/use-drop-zone/README.md
@@ -0,0 +1,71 @@
+# useDropZone (experimental)
+
+A hook to facilitate drag and drop handling within a designated drop zone area. An optional `dropZoneElement` can be provided, however by default the drop zone is bound by the area where the returned `ref` is assigned.
+
+When using a `dropZoneElement`, it is expected that the `ref` will be attached to a node that is a descendent of the `dropZoneElement`. Additionally, the element passed to `dropZoneElement` should be stored in state rather than a plain ref to ensure reactive updating when it changes.
+
+## Usage
+
+```js
+import { useDropZone } from '@wordpress/compose';
+import { useState } from '@wordpress/element';
+
+const WithWrapperDropZoneElement = () => {
+ const [ dropZoneElement, setDropZoneElement ] = useState( null );
+
+ const dropZoneRef = useDropZone(
+ {
+ dropZoneElement,
+ onDrop() => {
+ console.log( 'Dropped within the drop zone.' );
+ },
+ onDragEnter() => {
+ console.log( 'Dragging within the drop zone' );
+ }
+ }
+ )
+
+ return (
+
+ );
+};
+
+const WithoutWrapperDropZoneElement = () => {
+ const dropZoneRef = useDropZone(
+ {
+ onDrop() => {
+ console.log( 'Dropped within the drop zone.' );
+ },
+ onDragEnter() => {
+ console.log( 'Dragging within the drop zone' );
+ }
+ }
+ )
+
+ return (
+
+ );
+};
+```
+
+## Parameters
+
+- _props_ `Object`: Named parameters.
+- _props.dropZoneElement_ `HTMLElement`: Optional element to be used as the drop zone.
+- _props.isDisabled_ `boolean`: Whether or not to disable the drop zone.
+- _props.onDragStart_ `( e: DragEvent ) => void`: Called when dragging has started.
+- _props.onDragEnter_ `( e: DragEvent ) => void`: Called when the zone is entered.
+- _props.onDragOver_ `( e: DragEvent ) => void`: Called when the zone is moved within.
+- _props.onDragLeave_ `( e: DragEvent ) => void`: Called when the zone is left.
+- _props.onDragEnd_ `( e: MouseEvent ) => void`: Called when dragging has ended.
+- _props.onDrop_ `( e: DragEvent ) => void`: Called when dropping in the zone.
+
+_Returns_
+
+- `RefCallback< HTMLElement >`: Ref callback to be passed to the drop zone element.
diff --git a/packages/compose/src/hooks/use-drop-zone/index.js b/packages/compose/src/hooks/use-drop-zone/index.js
index d6388e09f0bf08..b537f0a5ab6227 100644
--- a/packages/compose/src/hooks/use-drop-zone/index.js
+++ b/packages/compose/src/hooks/use-drop-zone/index.js
@@ -33,18 +33,20 @@ function useFreshRef( value ) {
/**
* A hook to facilitate drag and drop handling.
*
- * @param {Object} props Named parameters.
- * @param {boolean} [props.isDisabled] Whether or not to disable the drop zone.
- * @param {(e: DragEvent) => void} [props.onDragStart] Called when dragging has started.
- * @param {(e: DragEvent) => void} [props.onDragEnter] Called when the zone is entered.
- * @param {(e: DragEvent) => void} [props.onDragOver] Called when the zone is moved within.
- * @param {(e: DragEvent) => void} [props.onDragLeave] Called when the zone is left.
- * @param {(e: MouseEvent) => void} [props.onDragEnd] Called when dragging has ended.
- * @param {(e: DragEvent) => void} [props.onDrop] Called when dropping in the zone.
+ * @param {Object} props Named parameters.
+ * @param {?HTMLElement} [props.dropZoneElement] Optional element to be used as the drop zone.
+ * @param {boolean} [props.isDisabled] Whether or not to disable the drop zone.
+ * @param {(e: DragEvent) => void} [props.onDragStart] Called when dragging has started.
+ * @param {(e: DragEvent) => void} [props.onDragEnter] Called when the zone is entered.
+ * @param {(e: DragEvent) => void} [props.onDragOver] Called when the zone is moved within.
+ * @param {(e: DragEvent) => void} [props.onDragLeave] Called when the zone is left.
+ * @param {(e: MouseEvent) => void} [props.onDragEnd] Called when dragging has ended.
+ * @param {(e: DragEvent) => void} [props.onDrop] Called when dropping in the zone.
*
* @return {import('react').RefCallback} Ref callback to be passed to the drop zone element.
*/
export default function useDropZone( {
+ dropZoneElement,
isDisabled,
onDrop: _onDrop,
onDragStart: _onDragStart,
@@ -61,11 +63,16 @@ export default function useDropZone( {
const onDragOverRef = useFreshRef( _onDragOver );
return useRefEffect(
- ( element ) => {
+ ( elem ) => {
if ( isDisabled ) {
return;
}
+ // If a custom dropZoneRef is passed, use that instead of the element.
+ // This allows the dropzone to cover an expanded area, rather than
+ // be restricted to the area of the ref returned by this hook.
+ const element = dropZoneElement ?? elem;
+
let isDragging = false;
const { ownerDocument } = element;
@@ -228,6 +235,6 @@ export default function useDropZone( {
);
};
},
- [ isDisabled ]
+ [ isDisabled, dropZoneElement ] // Refresh when the passed in dropZoneElement changes.
);
}
diff --git a/packages/compose/src/hooks/use-drop-zone/test/index.js b/packages/compose/src/hooks/use-drop-zone/test/index.js
new file mode 100644
index 00000000000000..260d5cbbbdceb1
--- /dev/null
+++ b/packages/compose/src/hooks/use-drop-zone/test/index.js
@@ -0,0 +1,63 @@
+/**
+ * External dependencies
+ */
+import { render, screen } from '@testing-library/react';
+
+/**
+ * WordPress dependencies
+ */
+import { useState } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import useDropZone from '../';
+
+describe( 'useDropZone', () => {
+ const ComponentWithWrapperDropZone = () => {
+ const [ dropZoneElement, setDropZoneElement ] = useState( null );
+ const dropZoneRef = useDropZone( {
+ dropZoneElement,
+ } );
+
+ return (
+
+ );
+ };
+
+ const ComponentWithoutWrapperDropZone = () => {
+ const dropZoneRef = useDropZone( {} );
+
+ return (
+
+ );
+ };
+
+ it( 'will attach dropzone to outer wrapper', () => {
+ const { rerender } = render( );
+ // Ensure `useEffect` has run.
+ rerender( );
+
+ expect( screen.getByRole( 'main' ) ).toHaveAttribute(
+ 'data-is-drop-zone'
+ );
+ } );
+
+ it( 'will attach dropzone to element with dropZoneRef attached', () => {
+ const { rerender } = render( );
+ // Ensure `useEffect` has run.
+ rerender( );
+
+ expect( screen.getByRole( 'region' ) ).toHaveAttribute(
+ 'data-is-drop-zone'
+ );
+ } );
+} );
diff --git a/packages/core-data/README.md b/packages/core-data/README.md
index dddc3550e03b26..d2ac90a4a5165b 100644
--- a/packages/core-data/README.md
+++ b/packages/core-data/README.md
@@ -535,6 +535,8 @@ _Returns_
### getRedoEdit
+> **Deprecated** since 6.3
+
Returns the next edit from the current undo offset for the entity records edits history, if any.
_Parameters_
@@ -578,6 +580,8 @@ _Returns_
### getUndoEdit
+> **Deprecated** since 6.3
+
Returns the previous edit from the current undo offset for the entity records edits history, if any.
_Parameters_
diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js
index ffae417a83cd13..cfab95aae9f8fc 100644
--- a/packages/core-data/src/actions.js
+++ b/packages/core-data/src/actions.js
@@ -18,6 +18,7 @@ import { receiveItems, removeItems, receiveQueriedItems } from './queried-data';
import { getOrLoadEntitiesConfig, DEFAULT_ENTITY_KEY } from './entities';
import { createBatch } from './batch';
import { STORE_NAME } from './name';
+import { getUndoEdits, getRedoEdits } from './private-selectors';
/**
* Returns an action object used in signalling that authors have been received.
@@ -406,14 +407,14 @@ export const editEntityRecord =
export const undo =
() =>
( { select, dispatch } ) => {
- const undoEdit = select.getUndoEdit();
+ // Todo: we shouldn't have to pass "root" here.
+ const undoEdit = select( ( state ) => getUndoEdits( state.root ) );
if ( ! undoEdit ) {
return;
}
dispatch( {
- type: 'EDIT_ENTITY_RECORD',
- ...undoEdit,
- meta: { isUndo: true },
+ type: 'UNDO',
+ stackedEdits: undoEdit,
} );
};
@@ -424,14 +425,14 @@ export const undo =
export const redo =
() =>
( { select, dispatch } ) => {
- const redoEdit = select.getRedoEdit();
+ // Todo: we shouldn't have to pass "root" here.
+ const redoEdit = select( ( state ) => getRedoEdits( state.root ) );
if ( ! redoEdit ) {
return;
}
dispatch( {
- type: 'EDIT_ENTITY_RECORD',
- ...redoEdit,
- meta: { isRedo: true },
+ type: 'REDO',
+ stackedEdits: redoEdit,
} );
};
diff --git a/packages/core-data/src/index.js b/packages/core-data/src/index.js
index 43fa4a0b3cd074..c2b491fa8c1ea1 100644
--- a/packages/core-data/src/index.js
+++ b/packages/core-data/src/index.js
@@ -62,7 +62,6 @@ const storeConfig = () => ( {
* @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/data/README.md#createReduxStore
*/
export const store = createReduxStore( STORE_NAME, storeConfig() );
-
register( store );
export { default as EntityProvider } from './entity-provider';
diff --git a/packages/core-data/src/private-selectors.ts b/packages/core-data/src/private-selectors.ts
new file mode 100644
index 00000000000000..0ac2c750239692
--- /dev/null
+++ b/packages/core-data/src/private-selectors.ts
@@ -0,0 +1,30 @@
+/**
+ * Internal dependencies
+ */
+import type { State, UndoEdit } from './selectors';
+
+type Optional< T > = T | undefined;
+
+/**
+ * Returns the previous edit from the current undo offset
+ * for the entity records edits history, if any.
+ *
+ * @param state State tree.
+ *
+ * @return The edit.
+ */
+export function getUndoEdits( state: State ): Optional< UndoEdit[] > {
+ return state.undo.list[ state.undo.list.length - 1 + state.undo.offset ];
+}
+
+/**
+ * Returns the next edit from the current undo offset
+ * for the entity records edits history, if any.
+ *
+ * @param state State tree.
+ *
+ * @return The edit.
+ */
+export function getRedoEdits( state: State ): Optional< UndoEdit[] > {
+ return state.undo.list[ state.undo.list.length + state.undo.offset ];
+}
diff --git a/packages/core-data/src/reducer.js b/packages/core-data/src/reducer.js
index f04d543919b8c8..b7dd9d73df15a7 100644
--- a/packages/core-data/src/reducer.js
+++ b/packages/core-data/src/reducer.js
@@ -183,6 +183,30 @@ export function themeGlobalStyleVariations( state = {}, action ) {
return state;
}
+const withMultiEntityRecordEdits = ( reducer ) => ( state, action ) => {
+ if ( action.type === 'UNDO' || action.type === 'REDO' ) {
+ const { stackedEdits } = action;
+
+ let newState = state;
+ stackedEdits.forEach(
+ ( { kind, name, recordId, property, from, to } ) => {
+ newState = reducer( newState, {
+ type: 'EDIT_ENTITY_RECORD',
+ kind,
+ name,
+ recordId,
+ edits: {
+ [ property ]: action.type === 'UNDO' ? from : to,
+ },
+ } );
+ }
+ );
+ return newState;
+ }
+
+ return reducer( state, action );
+};
+
/**
* Higher Order Reducer for a given entity config. It supports:
*
@@ -196,6 +220,8 @@ export function themeGlobalStyleVariations( state = {}, action ) {
*/
function entity( entityConfig ) {
return compose( [
+ withMultiEntityRecordEdits,
+
// Limit to matching action type so we don't attempt to replace action on
// an unhandled action.
ifMatchingAction(
@@ -411,8 +437,9 @@ export const entities = ( state = {}, action ) => {
/**
* @typedef {Object} UndoStateMeta
*
- * @property {number} offset Where in the undo stack we are.
- * @property {Object} [flattenedUndo] Flattened form of undo stack.
+ * @property {number} list The undo stack.
+ * @property {number} offset Where in the undo stack we are.
+ * @property {Object} cache Cache of unpersisted transient edits.
*/
/** @typedef {Array & UndoStateMeta} UndoState */
@@ -422,10 +449,7 @@ export const entities = ( state = {}, action ) => {
*
* @todo Given how we use this we might want to make a custom class for it.
*/
-const UNDO_INITIAL_STATE = Object.assign( [], { offset: 0 } );
-
-/** @type {Object} */
-let lastEditAction;
+const UNDO_INITIAL_STATE = { list: [], offset: 0 };
/**
* Reducer keeping track of entity edit undo history.
@@ -436,107 +460,114 @@ let lastEditAction;
* @return {UndoState} Updated state.
*/
export function undo( state = UNDO_INITIAL_STATE, action ) {
+ const omitPendingRedos = ( currentState ) => {
+ return {
+ ...currentState,
+ list: currentState.list.slice(
+ 0,
+ currentState.offset || undefined
+ ),
+ offset: 0,
+ };
+ };
+
+ const appendCachedEditsToLastUndo = ( currentState ) => {
+ if ( ! currentState.cache ) {
+ return currentState;
+ }
+
+ let nextState = {
+ ...currentState,
+ list: [ ...currentState.list ],
+ };
+ nextState = omitPendingRedos( nextState );
+ const previousUndoState = nextState.list.pop();
+ const updatedUndoState = currentState.cache.reduce(
+ appendEditToStack,
+ previousUndoState
+ );
+ nextState.list.push( updatedUndoState );
+
+ return {
+ ...nextState,
+ cache: undefined,
+ };
+ };
+
+ const appendEditToStack = (
+ stack = [],
+ { kind, name, recordId, property, from, to }
+ ) => {
+ const existingEditIndex = stack?.findIndex(
+ ( { kind: k, name: n, recordId: r, property: p } ) => {
+ return (
+ k === kind && n === name && r === recordId && p === property
+ );
+ }
+ );
+ const nextStack = [ ...stack ];
+ if ( existingEditIndex !== -1 ) {
+ // If the edit is already in the stack leave the initial "from" value.
+ nextStack[ existingEditIndex ] = {
+ ...nextStack[ existingEditIndex ],
+ to,
+ };
+ } else {
+ nextStack.push( {
+ kind,
+ name,
+ recordId,
+ property,
+ from,
+ to,
+ } );
+ }
+ return nextStack;
+ };
+
switch ( action.type ) {
- case 'EDIT_ENTITY_RECORD':
case 'CREATE_UNDO_LEVEL':
- let isCreateUndoLevel = action.type === 'CREATE_UNDO_LEVEL';
- const isUndoOrRedo =
- ! isCreateUndoLevel &&
- ( action.meta.isUndo || action.meta.isRedo );
- if ( isCreateUndoLevel ) {
- action = lastEditAction;
- } else if ( ! isUndoOrRedo ) {
- // Don't lose the last edit cache if the new one only has transient edits.
- // Transient edits don't create new levels so updating the cache would make
- // us skip an edit later when creating levels explicitly.
- if (
- Object.keys( action.edits ).some(
- ( key ) => ! action.transientEdits[ key ]
- )
- ) {
- lastEditAction = action;
- } else {
- lastEditAction = {
- ...action,
- edits: {
- ...( lastEditAction && lastEditAction.edits ),
- ...action.edits,
- },
- };
- }
- }
+ return appendCachedEditsToLastUndo( state );
- /** @type {UndoState} */
- let nextState;
-
- if ( isUndoOrRedo ) {
- // @ts-ignore we might consider using Object.assign({}, state)
- nextState = [ ...state ];
- nextState.offset =
- state.offset + ( action.meta.isUndo ? -1 : 1 );
-
- if ( state.flattenedUndo ) {
- // The first undo in a sequence of undos might happen while we have
- // flattened undos in state. If this is the case, we want execution
- // to continue as if we were creating an explicit undo level. This
- // will result in an extra undo level being appended with the flattened
- // undo values.
- // We also have to take into account if the `lastEditAction` had opted out
- // of being tracked in undo history, like the action that persists the latest
- // content right before saving. In that case we have to update the `lastEditAction`
- // to avoid returning early before applying the existing flattened undos.
- isCreateUndoLevel = true;
- if ( ! lastEditAction.meta.undo ) {
- lastEditAction.meta.undo = {
- edits: {},
- };
- }
- action = lastEditAction;
- } else {
- return nextState;
- }
- }
+ case 'UNDO':
+ case 'REDO': {
+ const nextState = appendCachedEditsToLastUndo( state );
+ return {
+ ...nextState,
+ offset: state.offset + ( action.type === 'UNDO' ? -1 : 1 ),
+ };
+ }
+ case 'EDIT_ENTITY_RECORD': {
if ( ! action.meta.undo ) {
return state;
}
- // Transient edits don't create an undo level, but are
- // reachable in the next meaningful edit to which they
- // are merged. They are defined in the entity's config.
- if (
- ! isCreateUndoLevel &&
- ! Object.keys( action.edits ).some(
- ( key ) => ! action.transientEdits[ key ]
- )
- ) {
- // @ts-ignore we might consider using Object.assign({}, state)
- nextState = [ ...state ];
- nextState.flattenedUndo = {
- ...state.flattenedUndo,
- ...action.edits,
+ const isCachedChange = Object.keys( action.edits ).every(
+ ( key ) => action.transientEdits[ key ]
+ );
+
+ const edits = Object.keys( action.edits ).map( ( key ) => {
+ return {
+ kind: action.kind,
+ name: action.name,
+ recordId: action.recordId,
+ property: key,
+ from: action.meta.undo.edits[ key ],
+ to: action.edits[ key ],
};
- nextState.offset = state.offset;
- return nextState;
- }
+ } );
- // Clear potential redos, because this only supports linear history.
- nextState =
- // @ts-ignore this needs additional cleanup, probably involving code-level changes
- nextState || state.slice( 0, state.offset || undefined );
- nextState.offset = nextState.offset || 0;
- nextState.pop();
- if ( ! isCreateUndoLevel ) {
- nextState.push( {
- kind: action.meta.undo.kind,
- name: action.meta.undo.name,
- recordId: action.meta.undo.recordId,
- edits: {
- ...state.flattenedUndo,
- ...action.meta.undo.edits,
- },
- } );
+ if ( isCachedChange ) {
+ return {
+ ...state,
+ cache: edits.reduce( appendEditToStack, state.cache ),
+ };
}
+
+ let nextState = omitPendingRedos( state );
+ nextState = appendCachedEditsToLastUndo( nextState );
+ nextState = { ...nextState, list: [ ...nextState.list ] };
// When an edit is a function it's an optimization to avoid running some expensive operation.
// We can't rely on the function references being the same so we opt out of comparing them here.
const comparisonUndoEdits = Object.values(
@@ -546,16 +577,11 @@ export function undo( state = UNDO_INITIAL_STATE, action ) {
( edit ) => typeof edit !== 'function'
);
if ( ! isShallowEqual( comparisonUndoEdits, comparisonEdits ) ) {
- nextState.push( {
- kind: action.kind,
- name: action.name,
- recordId: action.recordId,
- edits: isCreateUndoLevel
- ? { ...state.flattenedUndo, ...action.edits }
- : action.edits,
- } );
+ nextState.list.push( edits );
}
+
return nextState;
+ }
}
return state;
diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts
index 7513d918109673..a6b7774d37094c 100644
--- a/packages/core-data/src/selectors.ts
+++ b/packages/core-data/src/selectors.ts
@@ -22,6 +22,7 @@ import {
setNestedValue,
} from './utils';
import type * as ET from './entity-types';
+import { getUndoEdits, getRedoEdits } from './private-selectors';
// This is an incomplete, high-level approximation of the State type.
// It makes the selectors slightly more safe, but is intended to evolve
@@ -73,9 +74,18 @@ interface EntityConfig {
kind: string;
}
-interface UndoState extends Array< Object > {
- flattenedUndo: unknown;
+export interface UndoEdit {
+ name: string;
+ kind: string;
+ recordId: string;
+ from: any;
+ to: any;
+}
+
+interface UndoState {
+ list: Array< UndoEdit[] >;
offset: number;
+ cache: UndoEdit[];
}
interface UserState {
@@ -884,24 +894,38 @@ function getCurrentUndoOffset( state: State ): number {
* Returns the previous edit from the current undo offset
* for the entity records edits history, if any.
*
- * @param state State tree.
+ * @deprecated since 6.3
+ *
+ * @param state State tree.
*
* @return The edit.
*/
export function getUndoEdit( state: State ): Optional< any > {
- return state.undo[ state.undo.length - 2 + getCurrentUndoOffset( state ) ];
+ deprecated( "select( 'core' ).getUndoEdit()", {
+ since: '6.3',
+ } );
+ return state.undo.list[
+ state.undo.list.length - 2 + getCurrentUndoOffset( state )
+ ]?.[ 0 ];
}
/**
* Returns the next edit from the current undo offset
* for the entity records edits history, if any.
*
- * @param state State tree.
+ * @deprecated since 6.3
+ *
+ * @param state State tree.
*
* @return The edit.
*/
export function getRedoEdit( state: State ): Optional< any > {
- return state.undo[ state.undo.length + getCurrentUndoOffset( state ) ];
+ deprecated( "select( 'core' ).getRedoEdit()", {
+ since: '6.3',
+ } );
+ return state.undo.list[
+ state.undo.list.length + getCurrentUndoOffset( state )
+ ]?.[ 0 ];
}
/**
@@ -913,7 +937,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( getUndoEdit( state ) );
+ return Boolean( getUndoEdits( state ) );
}
/**
@@ -925,7 +949,7 @@ export function hasUndo( state: State ): boolean {
* @return Whether there is a next edit or not.
*/
export function hasRedo( state: State ): boolean {
- return Boolean( getRedoEdit( state ) );
+ return Boolean( getRedoEdits( state ) );
}
/**
@@ -1142,11 +1166,7 @@ export const hasFetchedAutosaves = createRegistrySelector(
export const getReferenceByDistinctEdits = createSelector(
// This unused state argument is listed here for the documentation generating tool (docgen).
( state: State ) => [],
- ( state: State ) => [
- state.undo.length,
- state.undo.offset,
- state.undo.flattenedUndo,
- ]
+ ( state: State ) => [ state.undo.list.length, state.undo.offset ]
);
/**
diff --git a/packages/core-data/src/test/reducer.js b/packages/core-data/src/test/reducer.js
index 63caf5fb83b177..4f7d9b9c0d2aec 100644
--- a/packages/core-data/src/test/reducer.js
+++ b/packages/core-data/src/test/reducer.js
@@ -143,28 +143,34 @@ describe( 'entities', () => {
} );
describe( 'undo', () => {
- let lastEdits;
+ let lastValues;
let undoState;
let expectedUndoState;
- const createEditActionPart = ( edits ) => ( {
+
+ const createExpectedDiff = ( property, { from, to } ) => ( {
kind: 'someKind',
name: 'someName',
recordId: 'someRecordId',
- edits,
+ property,
+ from,
+ to,
} );
const createNextEditAction = ( edits, transientEdits = {} ) => {
let action = {
- ...createEditActionPart( edits ),
+ kind: 'someKind',
+ name: 'someName',
+ recordId: 'someRecordId',
+ edits,
transientEdits,
};
action = {
type: 'EDIT_ENTITY_RECORD',
...action,
meta: {
- undo: { ...action, edits: lastEdits },
+ undo: { edits: lastValues },
},
};
- lastEdits = { ...lastEdits, ...edits };
+ lastValues = { ...lastValues, ...edits };
return action;
};
const createNextUndoState = ( ...args ) => {
@@ -172,17 +178,17 @@ describe( 'undo', () => {
if ( args[ 0 ] === 'isUndo' || args[ 0 ] === 'isRedo' ) {
// We need to "apply" the undo level here and build
// the action to move the offset.
- lastEdits =
- undoState[
- undoState.length +
- undoState.offset -
- ( args[ 0 ] === 'isUndo' ? 2 : 0 )
- ].edits;
+ const lastEdits =
+ undoState.list[
+ undoState.list.length -
+ ( args[ 0 ] === 'isUndo' ? 1 : 0 ) +
+ undoState.offset
+ ];
+ lastEdits.forEach( ( { property, from, to } ) => {
+ lastValues[ property ] = args[ 0 ] === 'isUndo' ? from : to;
+ } );
action = {
- type: 'EDIT_ENTITY_RECORD',
- meta: {
- [ args[ 0 ] ]: true,
- },
+ type: args[ 0 ] === 'isUndo' ? 'UNDO' : 'REDO',
};
} else if ( args[ 0 ] === 'isCreate' ) {
action = { type: 'CREATE_UNDO_LEVEL' };
@@ -192,10 +198,9 @@ describe( 'undo', () => {
return deepFreeze( undo( undoState, action ) );
};
beforeEach( () => {
- lastEdits = {};
+ lastValues = {};
undoState = undefined;
- expectedUndoState = [];
- expectedUndoState.offset = 0;
+ expectedUndoState = { list: [], offset: 0 };
} );
it( 'initializes', () => {
@@ -208,19 +213,41 @@ describe( 'undo', () => {
// Check that the first edit creates an undo level for the current state and
// one for the new one.
undoState = createNextUndoState( { value: 1 } );
- expectedUndoState.push(
- createEditActionPart( {} ),
- createEditActionPart( { value: 1 } )
- );
+ expectedUndoState.list.push( [
+ createExpectedDiff( 'value', { from: undefined, to: 1 } ),
+ ] );
expect( undoState ).toEqual( expectedUndoState );
// Check that the second and third edits just create an undo level for
// themselves.
undoState = createNextUndoState( { value: 2 } );
- expectedUndoState.push( createEditActionPart( { value: 2 } ) );
+ expectedUndoState.list.push( [
+ createExpectedDiff( 'value', { from: 1, to: 2 } ),
+ ] );
expect( undoState ).toEqual( expectedUndoState );
undoState = createNextUndoState( { value: 3 } );
- expectedUndoState.push( createEditActionPart( { value: 3 } ) );
+ expectedUndoState.list.push( [
+ createExpectedDiff( 'value', { from: 2, to: 3 } ),
+ ] );
+ expect( undoState ).toEqual( expectedUndoState );
+ } );
+
+ it( 'stacks multi-property undo levels', () => {
+ undoState = createNextUndoState();
+
+ undoState = createNextUndoState( { value: 1 } );
+ undoState = createNextUndoState( { value2: 2 } );
+ expectedUndoState.list.push(
+ [ createExpectedDiff( 'value', { from: undefined, to: 1 } ) ],
+ [ createExpectedDiff( 'value2', { from: undefined, to: 2 } ) ]
+ );
+ expect( undoState ).toEqual( expectedUndoState );
+
+ // Check that that creating another undo level merges the "edits"
+ undoState = createNextUndoState( { value: 2 } );
+ expectedUndoState.list.push( [
+ createExpectedDiff( 'value', { from: 1, to: 2 } ),
+ ] );
expect( undoState ).toEqual( expectedUndoState );
} );
@@ -229,11 +256,10 @@ describe( 'undo', () => {
undoState = createNextUndoState( { value: 1 } );
undoState = createNextUndoState( { value: 2 } );
undoState = createNextUndoState( { value: 3 } );
- expectedUndoState.push(
- createEditActionPart( {} ),
- createEditActionPart( { value: 1 } ),
- createEditActionPart( { value: 2 } ),
- createEditActionPart( { value: 3 } )
+ expectedUndoState.list.push(
+ [ createExpectedDiff( 'value', { from: undefined, to: 1 } ) ],
+ [ createExpectedDiff( 'value', { from: 1, to: 2 } ) ],
+ [ createExpectedDiff( 'value', { from: 2, to: 3 } ) ]
);
expect( undoState ).toEqual( expectedUndoState );
@@ -255,17 +281,22 @@ describe( 'undo', () => {
// Check that another edit will go on top when there
// is no undo level offset.
undoState = createNextUndoState( { value: 4 } );
- expectedUndoState.push( createEditActionPart( { value: 4 } ) );
+ expectedUndoState.list.push( [
+ createExpectedDiff( 'value', { from: 3, to: 4 } ),
+ ] );
expect( undoState ).toEqual( expectedUndoState );
// Check that undoing and editing will slice of
// all the levels after the current one.
undoState = createNextUndoState( 'isUndo' );
undoState = createNextUndoState( 'isUndo' );
+
undoState = createNextUndoState( { value: 5 } );
- expectedUndoState.pop();
- expectedUndoState.pop();
- expectedUndoState.push( createEditActionPart( { value: 5 } ) );
+ expectedUndoState.list.pop();
+ expectedUndoState.list.pop();
+ expectedUndoState.list.push( [
+ createExpectedDiff( 'value', { from: 2, to: 5 } ),
+ ] );
expect( undoState ).toEqual( expectedUndoState );
} );
@@ -277,10 +308,15 @@ describe( 'undo', () => {
{ transientValue: true }
);
undoState = createNextUndoState( { value: 3 } );
- expectedUndoState.push(
- createEditActionPart( {} ),
- createEditActionPart( { value: 1, transientValue: 2 } ),
- createEditActionPart( { value: 3 } )
+ expectedUndoState.list.push(
+ [
+ createExpectedDiff( 'value', { from: undefined, to: 1 } ),
+ createExpectedDiff( 'transientValue', {
+ from: undefined,
+ to: 2,
+ } ),
+ ],
+ [ createExpectedDiff( 'value', { from: 1, to: 3 } ) ]
);
expect( undoState ).toEqual( expectedUndoState );
} );
@@ -292,10 +328,9 @@ describe( 'undo', () => {
// transient edits.
undoState = createNextUndoState( { value: 1 } );
undoState = createNextUndoState( 'isCreate' );
- expectedUndoState.push(
- createEditActionPart( {} ),
- createEditActionPart( { value: 1 } )
- );
+ expectedUndoState.list.push( [
+ createExpectedDiff( 'value', { from: undefined, to: 1 } ),
+ ] );
expect( undoState ).toEqual( expectedUndoState );
// Check that transient edits are merged into the last
@@ -305,18 +340,19 @@ describe( 'undo', () => {
{ transientValue: true }
);
undoState = createNextUndoState( 'isCreate' );
- expectedUndoState[
- expectedUndoState.length - 1
- ].edits.transientValue = 2;
+ expectedUndoState.list[ expectedUndoState.list.length - 1 ].push(
+ createExpectedDiff( 'transientValue', { from: undefined, to: 2 } )
+ );
expect( undoState ).toEqual( expectedUndoState );
- // Check that undo levels are created with the latest action,
- // even if undone.
+ // Check that create after undo does nothing.
undoState = createNextUndoState( { value: 3 } );
undoState = createNextUndoState( 'isUndo' );
undoState = createNextUndoState( 'isCreate' );
- expectedUndoState.pop();
- expectedUndoState.push( createEditActionPart( { value: 3 } ) );
+ expectedUndoState.list.push( [
+ createExpectedDiff( 'value', { from: 1, to: 3 } ),
+ ] );
+ expectedUndoState.offset = -1;
expect( undoState ).toEqual( expectedUndoState );
} );
@@ -328,10 +364,10 @@ describe( 'undo', () => {
{ transientValue: true }
);
undoState = createNextUndoState( 'isUndo' );
- expectedUndoState.push(
- createEditActionPart( {} ),
- createEditActionPart( { value: 1, transientValue: 2 } )
- );
+ expectedUndoState.list.push( [
+ createExpectedDiff( 'value', { from: undefined, to: 1 } ),
+ createExpectedDiff( 'transientValue', { from: undefined, to: 2 } ),
+ ] );
expectedUndoState.offset--;
expect( undoState ).toEqual( expectedUndoState );
} );
@@ -341,7 +377,6 @@ describe( 'undo', () => {
undoState = createNextUndoState();
undoState = createNextUndoState( { value } );
undoState = createNextUndoState( { value: () => {} } );
- expectedUndoState.push( createEditActionPart( { value } ) );
expect( undoState ).toEqual( expectedUndoState );
} );
} );
diff --git a/packages/core-data/src/test/selectors.js b/packages/core-data/src/test/selectors.js
index 0ea9e26e505437..84fecc7d07cda9 100644
--- a/packages/core-data/src/test/selectors.js
+++ b/packages/core-data/src/test/selectors.js
@@ -838,20 +838,20 @@ describe( 'getCurrentUser', () => {
describe( 'getReferenceByDistinctEdits', () => {
it( 'should return referentially equal values across empty states', () => {
- const state = { undo: [] };
+ const state = { undo: { list: [] } };
expect( getReferenceByDistinctEdits( state ) ).toBe(
getReferenceByDistinctEdits( state )
);
- const beforeState = { undo: [] };
- const afterState = { undo: [] };
+ const beforeState = { undo: { list: [] } };
+ const afterState = { undo: { list: [] } };
expect( getReferenceByDistinctEdits( beforeState ) ).toBe(
getReferenceByDistinctEdits( afterState )
);
} );
it( 'should return referentially equal values across unchanging non-empty state', () => {
- const undoStates = [ {} ];
+ const undoStates = { list: [ {} ] };
const state = { undo: undoStates };
expect( getReferenceByDistinctEdits( state ) ).toBe(
getReferenceByDistinctEdits( state )
@@ -866,9 +866,9 @@ describe( 'getReferenceByDistinctEdits', () => {
describe( 'when adding edits', () => {
it( 'should return referentially different values across changing states', () => {
- const beforeState = { undo: [ {} ] };
+ const beforeState = { undo: { list: [ {} ] } };
beforeState.undo.offset = 0;
- const afterState = { undo: [ {}, {} ] };
+ const afterState = { undo: { list: [ {}, {} ] } };
afterState.undo.offset = 1;
expect( getReferenceByDistinctEdits( beforeState ) ).not.toBe(
getReferenceByDistinctEdits( afterState )
@@ -878,9 +878,9 @@ describe( 'getReferenceByDistinctEdits', () => {
describe( 'when using undo', () => {
it( 'should return referentially different values across changing states', () => {
- const beforeState = { undo: [ {}, {} ] };
+ const beforeState = { undo: { list: [ {}, {} ] } };
beforeState.undo.offset = 1;
- const afterState = { undo: [ {}, {} ] };
+ const afterState = { undo: { list: [ {}, {} ] } };
afterState.undo.offset = 0;
expect( getReferenceByDistinctEdits( beforeState ) ).not.toBe(
getReferenceByDistinctEdits( afterState )
diff --git a/packages/create-block/lib/templates.js b/packages/create-block/lib/templates.js
index 48649527ecc842..018385c9fde82c 100644
--- a/packages/create-block/lib/templates.js
+++ b/packages/create-block/lib/templates.js
@@ -220,7 +220,7 @@ const getPluginTemplate = async ( templateName ) => {
const getDefaultValues = ( pluginTemplate, variant ) => {
return {
$schema: 'https://schemas.wp.org/trunk/block.json',
- apiVersion: 2,
+ apiVersion: 3,
namespace: 'create-block',
category: 'widgets',
author: 'The WordPress Contributors',
diff --git a/packages/data/src/redux-store/index.js b/packages/data/src/redux-store/index.js
index 50a0ade0d551c9..2618eb92c67454 100644
--- a/packages/data/src/redux-store/index.js
+++ b/packages/data/src/redux-store/index.js
@@ -54,12 +54,11 @@ const trimUndefinedValues = ( array ) => {
* @return {Array} Transformed object.
*/
const mapValues = ( obj, callback ) =>
- Object.entries( obj ?? {} ).reduce(
- ( acc, [ key, value ] ) => ( {
- ...acc,
- [ key ]: callback( value, key ),
- } ),
- {}
+ Object.fromEntries(
+ Object.entries( obj ?? {} ).map( ( [ key, value ] ) => [
+ key,
+ callback( value, key ),
+ ] )
);
// Convert Map objects to plain objects
@@ -176,77 +175,77 @@ export default function createReduxStore( key, options ) {
lock( store, privateRegistrationFunctions );
const resolversCache = createResolversCache();
- let resolvers;
- const actions = mapActions(
- {
- ...metadataActions,
- ...options.actions,
- },
- store
- );
+ function bindAction( action ) {
+ return ( ...args ) =>
+ Promise.resolve( store.dispatch( action( ...args ) ) );
+ }
+
+ const actions = {
+ ...mapValues( metadataActions, bindAction ),
+ ...mapValues( options.actions, bindAction ),
+ };
+
lock(
actions,
new Proxy( privateActions, {
get: ( target, prop ) => {
- return (
- mapActions( privateActions, store )[ prop ] ||
- actions[ prop ]
- );
+ const privateAction = privateActions[ prop ];
+ return privateAction
+ ? bindAction( privateAction )
+ : actions[ prop ];
},
} )
);
- let selectors = mapSelectors(
- {
- ...mapValues(
- metadataSelectors,
- ( selector ) =>
- ( state, ...args ) =>
- selector( state.metadata, ...args )
- ),
- ...mapValues( options.selectors, ( selector ) => {
- if ( selector.isRegistrySelector ) {
- selector.registry = registry;
- }
+ function bindSelector( selector ) {
+ if ( selector.isRegistrySelector ) {
+ selector.registry = registry;
+ }
+ const boundSelector = ( ...args ) => {
+ const state = store.__unstableOriginalGetState();
+ return selector( state.root, ...args );
+ };
+ boundSelector.hasResolver = false;
+ return boundSelector;
+ }
- return ( state, ...args ) =>
- selector( state.root, ...args );
- } ),
- },
- store
- );
- lock(
- selectors,
- new Proxy( privateSelectors, {
- get: ( target, prop ) => {
- return (
- mapSelectors(
- mapValues( privateSelectors, ( selector ) => {
- if ( selector.isRegistrySelector ) {
- selector.registry = registry;
- }
-
- return ( state, ...args ) =>
- selector( state.root, ...args );
- } ),
- store
- )[ prop ] || selectors[ prop ]
- );
- },
- } )
- );
+ function bindMetadataSelector( selector ) {
+ const boundSelector = ( ...args ) => {
+ const state = store.__unstableOriginalGetState();
+ return selector( state.metadata, ...args );
+ };
+ boundSelector.hasResolver = false;
+ return boundSelector;
+ }
+ let selectors = {
+ ...mapValues( metadataSelectors, bindMetadataSelector ),
+ ...mapValues( options.selectors, bindSelector ),
+ };
+
+ let resolvers;
if ( options.resolvers ) {
- const result = mapResolvers(
- options.resolvers,
+ resolvers = mapResolvers( options.resolvers );
+ selectors = mapSelectorsWithResolvers(
selectors,
+ resolvers,
store,
resolversCache
);
- resolvers = result.resolvers;
- selectors = result.selectors;
}
+ lock(
+ selectors,
+ new Proxy( privateSelectors, {
+ get: ( target, prop ) => {
+ const privateSelector = privateSelectors[ prop ];
+ return privateSelector
+ ? bindSelector( privateSelector )
+ : selectors[ prop ];
+ },
+ } )
+ );
+
const resolveSelectors = mapResolveSelectors( selectors, store );
const suspendSelectors = mapSuspendSelectors( selectors, store );
@@ -360,59 +359,6 @@ function instantiateReduxStore( key, options, registry, thunkArgs ) {
);
}
-/**
- * Maps selectors to a store.
- *
- * @param {Object} selectors Selectors to register. Keys will be used as the
- * public facing API. Selectors will get passed the
- * state as first argument.
- * @param {Object} store The store to which the selectors should be mapped.
- * @return {Object} Selectors mapped to the provided store.
- */
-function mapSelectors( selectors, store ) {
- const createStateSelector = ( registrySelector ) => {
- const selector = function runSelector() {
- // This function is an optimized implementation of:
- //
- // selector( store.getState(), ...arguments )
- //
- // Where the above would incur an `Array#concat` in its application,
- // the logic here instead efficiently constructs an arguments array via
- // direct assignment.
- const argsLength = arguments.length;
- const args = new Array( argsLength + 1 );
- args[ 0 ] = store.__unstableOriginalGetState();
- for ( let i = 0; i < argsLength; i++ ) {
- args[ i + 1 ] = arguments[ i ];
- }
-
- return registrySelector( ...args );
- };
- selector.hasResolver = false;
- return selector;
- };
-
- return mapValues( selectors, createStateSelector );
-}
-
-/**
- * Maps actions to dispatch from a given store.
- *
- * @param {Object} actions Actions to register.
- * @param {Object} store The redux store to which the actions should be mapped.
- *
- * @return {Object} Actions mapped to the redux store provided.
- */
-function mapActions( actions, store ) {
- const createBoundAction =
- ( action ) =>
- ( ...args ) => {
- return Promise.resolve( store.dispatch( action( ...args ) ) );
- };
-
- return mapValues( actions, createBoundAction );
-}
-
/**
* Maps selectors to functions that return a resolution promise for them
*
@@ -519,20 +465,13 @@ function mapSuspendSelectors( selectors, store ) {
}
/**
- * Returns resolvers with matched selectors for a given namespace.
- * Resolvers are side effects invoked once per argument set of a given selector call,
- * used in ensuring that the data needs for the selector are satisfied.
+ * Convert resolvers to a normalized form, an object with `fulfill` method and
+ * optional methods like `isFulfilled`.
*
- * @param {Object} resolvers Resolvers to register.
- * @param {Object} selectors The current selectors to be modified.
- * @param {Object} store The redux store to which the resolvers should be mapped.
- * @param {Object} resolversCache Resolvers Cache.
+ * @param {Object} resolvers Resolver to convert
*/
-function mapResolvers( resolvers, selectors, store, resolversCache ) {
- // The `resolver` can be either a function that does the resolution, or, in more advanced
- // cases, an object with a `fullfill` method and other optional methods like `isFulfilled`.
- // Here we normalize the `resolver` function to an object with `fulfill` method.
- const mappedResolvers = mapValues( resolvers, ( resolver ) => {
+function mapResolvers( resolvers ) {
+ return mapValues( resolvers, ( resolver ) => {
if ( resolver.fulfill ) {
return resolver;
}
@@ -542,99 +481,81 @@ function mapResolvers( resolvers, selectors, store, resolversCache ) {
fulfill: resolver, // Add the fulfill method.
};
} );
+}
- const mapSelector = ( selector, selectorName ) => {
- const resolver = resolvers[ selectorName ];
- if ( ! resolver ) {
- selector.hasResolver = false;
- return selector;
+/**
+ * Returns resolvers with matched selectors for a given namespace.
+ * Resolvers are side effects invoked once per argument set of a given selector call,
+ * used in ensuring that the data needs for the selector are satisfied.
+ *
+ * @param {Object} selectors The current selectors to be modified.
+ * @param {Object} resolvers Resolvers to register.
+ * @param {Object} store The redux store to which the resolvers should be mapped.
+ * @param {Object} resolversCache Resolvers Cache.
+ */
+function mapSelectorsWithResolvers(
+ selectors,
+ resolvers,
+ store,
+ resolversCache
+) {
+ function fulfillSelector( resolver, selectorName, args ) {
+ const state = store.getState();
+
+ if (
+ resolversCache.isRunning( selectorName, args ) ||
+ ( typeof resolver.isFulfilled === 'function' &&
+ resolver.isFulfilled( state, ...args ) )
+ ) {
+ return;
}
- const selectorResolver = ( ...args ) => {
- async function fulfillSelector() {
- const state = store.getState();
-
- if (
- resolversCache.isRunning( selectorName, args ) ||
- ( typeof resolver.isFulfilled === 'function' &&
- resolver.isFulfilled( state, ...args ) )
- ) {
- return;
- }
-
- const { metadata } = store.__unstableOriginalGetState();
+ const { metadata } = store.__unstableOriginalGetState();
- if (
- metadataSelectors.hasStartedResolution(
- metadata,
- selectorName,
- args
- )
- ) {
- return;
- }
+ if (
+ metadataSelectors.hasStartedResolution(
+ metadata,
+ selectorName,
+ args
+ )
+ ) {
+ return;
+ }
- resolversCache.markAsRunning( selectorName, args );
+ resolversCache.markAsRunning( selectorName, args );
- setTimeout( async () => {
- resolversCache.clear( selectorName, args );
- store.dispatch(
- metadataActions.startResolution( selectorName, args )
- );
- try {
- await fulfillResolver(
- store,
- mappedResolvers,
- selectorName,
- ...args
- );
- store.dispatch(
- metadataActions.finishResolution(
- selectorName,
- args
- )
- );
- } catch ( error ) {
- store.dispatch(
- metadataActions.failResolution(
- selectorName,
- args,
- error
- )
- );
- }
- } );
+ setTimeout( async () => {
+ resolversCache.clear( selectorName, args );
+ store.dispatch(
+ metadataActions.startResolution( selectorName, args )
+ );
+ try {
+ const action = resolver.fulfill( ...args );
+ if ( action ) {
+ await store.dispatch( action );
+ }
+ store.dispatch(
+ metadataActions.finishResolution( selectorName, args )
+ );
+ } catch ( error ) {
+ store.dispatch(
+ metadataActions.failResolution( selectorName, args, error )
+ );
}
+ }, 0 );
+ }
+
+ return mapValues( selectors, ( selector, selectorName ) => {
+ const resolver = resolvers[ selectorName ];
+ if ( ! resolver ) {
+ return selector;
+ }
- fulfillSelector( ...args );
+ const selectorResolver = ( ...args ) => {
+ fulfillSelector( resolver, selectorName, args );
return selector( ...args );
};
selectorResolver.hasResolver = true;
return selectorResolver;
- };
-
- return {
- resolvers: mappedResolvers,
- selectors: mapValues( selectors, mapSelector ),
- };
-}
-
-/**
- * Calls a resolver given arguments
- *
- * @param {Object} store Store reference, for fulfilling via resolvers
- * @param {Object} resolvers Store Resolvers
- * @param {string} selectorName Selector name to fulfill.
- * @param {Array} args Selector Arguments.
- */
-async function fulfillResolver( store, resolvers, selectorName, ...args ) {
- const resolver = resolvers[ selectorName ];
- if ( ! resolver ) {
- return;
- }
-
- const action = resolver.fulfill( ...args );
- if ( action ) {
- await store.dispatch( action );
- }
+ } );
}
diff --git a/packages/data/src/test/privateAPIs.js b/packages/data/src/test/privateAPIs.js
index 93df00d287d600..1ccf6adb0c5115 100644
--- a/packages/data/src/test/privateAPIs.js
+++ b/packages/data/src/test/privateAPIs.js
@@ -96,6 +96,37 @@ describe( 'Private data APIs', () => {
);
} );
+ it( 'should support combination of private selectors and resolvers', async () => {
+ const testStore = createReduxStore( 'test', {
+ reducer: ( state = {}, action ) => {
+ if ( action.type === 'RECEIVE' ) {
+ return { ...state, [ action.key ]: action.value };
+ }
+ return state;
+ },
+ selectors: {
+ get: ( state, key ) => state[ key ],
+ },
+ resolvers: {
+ get:
+ ( key ) =>
+ async ( { dispatch } ) => {
+ const value = await ( 'resolved-' + key );
+ dispatch( { type: 'RECEIVE', key, value } );
+ },
+ },
+ } );
+ unlock( testStore ).registerPrivateSelectors( {
+ peek: ( state, key ) => state[ key ],
+ } );
+ registry.register( testStore );
+
+ await registry.resolveSelect( testStore ).get( 'x' );
+ expect( unlock( registry.select( testStore ) ).peek( 'x' ) ).toBe(
+ 'resolved-x'
+ );
+ } );
+
it( 'should give private selectors access to the state', () => {
const groceryStore = createStore();
unlock( groceryStore ).registerPrivateSelectors( {
diff --git a/packages/date/src/test/index.js b/packages/date/src/test/index.js
index ff82748e02f23a..3c52aae1fbd0df 100644
--- a/packages/date/src/test/index.js
+++ b/packages/date/src/test/index.js
@@ -623,7 +623,7 @@ describe( 'Moment.js Localization', () => {
} );
describe( 'humanTimeDiff', () => {
- it( 'should return human readable time differences', () => {
+ it( 'should return human readable time differences in the past', () => {
expect(
humanTimeDiff(
'2023-04-28T11:00:00.000Z',
@@ -643,5 +643,20 @@ describe( 'Moment.js Localization', () => {
)
).toBe( '2 days ago' );
} );
+
+ it( 'should return human readable time differences in the future', () => {
+ // Future.
+ const now = new Date();
+ const twoHoursLater = new Date(
+ now.getTime() + 2 * 60 * 60 * 1000
+ );
+ expect( humanTimeDiff( twoHoursLater ) ).toBe( 'in 2 hours' );
+
+ const twoDaysLater = new Date(
+ now.getTime() + 2 * 24 * 60 * 60 * 1000
+ ); // Adding 2 days in milliseconds
+
+ expect( humanTimeDiff( twoDaysLater ) ).toBe( 'in 2 days' );
+ } );
} );
} );
diff --git a/packages/dependency-extraction-webpack-plugin/test/__snapshots__/build.js.snap b/packages/dependency-extraction-webpack-plugin/test/__snapshots__/build.js.snap
index 42102ca2cb1322..85683906793548 100644
--- a/packages/dependency-extraction-webpack-plugin/test/__snapshots__/build.js.snap
+++ b/packages/dependency-extraction-webpack-plugin/test/__snapshots__/build.js.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DependencyExtractionWebpackPlugin Webpack \`combine-assets\` should produce expected output: Asset file 'assets.php' should match snapshot 1`] = `
-" array('dependencies' => array('lodash', 'wp-blob'), 'version' => 'cf268e19006bef774112'), 'fileB.js' => array('dependencies' => array('wp-token-list'), 'version' => '7f3970305cf0aecb54ab'));
+" array('dependencies' => array('lodash', 'wp-blob'), 'version' => 'cbe985cf6e1a25d848e5'), 'fileB.js' => array('dependencies' => array('wp-token-list'), 'version' => '7f3970305cf0aecb54ab'));
"
`;
@@ -73,7 +73,7 @@ exports[`DependencyExtractionWebpackPlugin Webpack \`function-output-filename\`
`;
exports[`DependencyExtractionWebpackPlugin Webpack \`has-extension-suffix\` should produce expected output: Asset file 'index.min.asset.php' should match snapshot 1`] = `
-" array('lodash', 'wp-blob'), 'version' => '49dba68ef238f954b358');
+" array('lodash', 'wp-blob'), 'version' => 'c3f17b34fdd974d57d0f');
"
`;
diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/combine-assets/file-a.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/combine-assets/file-a.js
index aa683b111a5ab2..52384ea4de1a47 100644
--- a/packages/dependency-extraction-webpack-plugin/test/fixtures/combine-assets/file-a.js
+++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/combine-assets/file-a.js
@@ -1,6 +1,7 @@
/**
* External dependencies
*/
+// eslint-disable-next-line no-restricted-imports
import { isEmpty } from 'lodash';
/**
diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/has-extension-suffix/index.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/has-extension-suffix/index.js
index aa683b111a5ab2..52384ea4de1a47 100644
--- a/packages/dependency-extraction-webpack-plugin/test/fixtures/has-extension-suffix/index.js
+++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/has-extension-suffix/index.js
@@ -1,6 +1,7 @@
/**
* External dependencies
*/
+// eslint-disable-next-line no-restricted-imports
import { isEmpty } from 'lodash';
/**
diff --git a/packages/e2e-test-utils-playwright/src/page-utils/drag-files.ts b/packages/e2e-test-utils-playwright/src/page-utils/drag-files.ts
index ea59c4602b1f60..f6378b73e33ecc 100644
--- a/packages/e2e-test-utils-playwright/src/page-utils/drag-files.ts
+++ b/packages/e2e-test-utils-playwright/src/page-utils/drag-files.ts
@@ -9,7 +9,7 @@ import { getType } from 'mime';
* Internal dependencies
*/
import type { PageUtils } from './index';
-import type { Locator } from '@playwright/test';
+import type { ElementHandle, Locator } from '@playwright/test';
type FileObject = {
name: string;
@@ -141,21 +141,25 @@ async function dragFiles(
/**
* Drop the files at the current position.
+ *
+ * @param locator
*/
- drop: async () => {
- const topMostElement = await this.page.evaluateHandle(
- ( { x, y } ) => {
- return document.elementFromPoint( x, y );
- },
- position
- );
- const elementHandle = topMostElement.asElement();
+ drop: async ( locator: Locator | ElementHandle | null ) => {
+ if ( ! locator ) {
+ const topMostElement = await this.page.evaluateHandle(
+ ( { x, y } ) => {
+ return document.elementFromPoint( x, y );
+ },
+ position
+ );
+ locator = topMostElement.asElement();
+ }
- if ( ! elementHandle ) {
+ if ( ! locator ) {
throw new Error( 'Element not found.' );
}
- await elementHandle.dispatchEvent( 'drop', { dataTransfer } );
+ await locator.dispatchEvent( 'drop', { dataTransfer } );
await cdpSession.detach();
},
diff --git a/packages/e2e-test-utils-playwright/src/page-utils/press-keys.ts b/packages/e2e-test-utils-playwright/src/page-utils/press-keys.ts
index 2d93a184913bba..9929ebf19d01a1 100644
--- a/packages/e2e-test-utils-playwright/src/page-utils/press-keys.ts
+++ b/packages/e2e-test-utils-playwright/src/page-utils/press-keys.ts
@@ -49,6 +49,9 @@ export function setClipboardData(
async function emulateClipboard( page: Page, type: 'copy' | 'cut' | 'paste' ) {
clipboardDataHolder = await page.evaluate(
( [ _type, _clipboardData ] ) => {
+ const canvasDoc =
+ // @ts-ignore
+ document.activeElement?.contentDocument ?? document;
const clipboardDataTransfer = new DataTransfer();
if ( _type === 'paste' ) {
@@ -61,7 +64,7 @@ async function emulateClipboard( page: Page, type: 'copy' | 'cut' | 'paste' ) {
_clipboardData.html
);
} else {
- const selection = window.getSelection()!;
+ const selection = canvasDoc.defaultView.getSelection()!;
const plainText = selection.toString();
let html = plainText;
if ( selection.rangeCount ) {
@@ -70,7 +73,8 @@ async function emulateClipboard( page: Page, type: 'copy' | 'cut' | 'paste' ) {
html = Array.from( fragment.childNodes )
.map(
( node ) =>
- ( node as Element ).outerHTML ?? node.nodeValue
+ ( node as Element ).outerHTML ??
+ ( node as Element ).nodeValue
)
.join( '' );
}
@@ -78,7 +82,7 @@ async function emulateClipboard( page: Page, type: 'copy' | 'cut' | 'paste' ) {
clipboardDataTransfer.setData( 'text/html', html );
}
- document.activeElement?.dispatchEvent(
+ canvasDoc.activeElement?.dispatchEvent(
new ClipboardEvent( _type, {
bubbles: true,
cancelable: true,
diff --git a/packages/e2e-test-utils/src/click-block-appender.js b/packages/e2e-test-utils/src/click-block-appender.js
index caae3dbf11dc79..b1c69789544c78 100644
--- a/packages/e2e-test-utils/src/click-block-appender.js
+++ b/packages/e2e-test-utils/src/click-block-appender.js
@@ -1,3 +1,8 @@
+/**
+ * Internal dependencies
+ */
+import { canvas } from './canvas';
+
/**
* Clicks the default block appender.
*/
@@ -6,7 +11,7 @@ export async function clickBlockAppender() {
await page.evaluate( () =>
window.wp.data.dispatch( 'core/block-editor' ).clearSelectedBlock()
);
- const appender = await page.waitForSelector(
+ const appender = await canvas().waitForSelector(
'.block-editor-default-block-appender__content'
);
await appender.click();
diff --git a/packages/e2e-test-utils/src/inserter.js b/packages/e2e-test-utils/src/inserter.js
index 84521728160bee..58f6d01a66514d 100644
--- a/packages/e2e-test-utils/src/inserter.js
+++ b/packages/e2e-test-utils/src/inserter.js
@@ -261,7 +261,7 @@ export async function insertFromGlobalInserter( category, searchTerm ) {
// Extra wait for the reusable block to be ready.
if ( category === 'Reusable' ) {
- await page.waitForSelector(
+ await canvas().waitForSelector(
'.block-library-block__reusable-block-container'
);
}
diff --git a/packages/e2e-test-utils/src/press-key-with-modifier.js b/packages/e2e-test-utils/src/press-key-with-modifier.js
index 265c186752f8dd..71d8ed72e8de75 100644
--- a/packages/e2e-test-utils/src/press-key-with-modifier.js
+++ b/packages/e2e-test-utils/src/press-key-with-modifier.js
@@ -24,8 +24,9 @@ import { modifiers, SHIFT, ALT, CTRL } from '@wordpress/keycodes';
async function emulateSelectAll() {
await page.evaluate( () => {
const isMac = /Mac|iPod|iPhone|iPad/.test( window.navigator.platform );
+ const canvasDoc = document.activeElement.contentDocument ?? document;
- document.activeElement.dispatchEvent(
+ canvasDoc.activeElement.dispatchEvent(
new KeyboardEvent( 'keydown', {
bubbles: true,
cancelable: true,
@@ -58,14 +59,14 @@ async function emulateSelectAll() {
} );
const wasPrevented =
- ! document.activeElement.dispatchEvent( preventableEvent ) ||
+ ! canvasDoc.activeElement.dispatchEvent( preventableEvent ) ||
preventableEvent.defaultPrevented;
if ( ! wasPrevented ) {
- document.execCommand( 'selectall', false, null );
+ canvasDoc.execCommand( 'selectall', false, null );
}
- document.activeElement.dispatchEvent(
+ canvasDoc.activeElement.dispatchEvent(
new KeyboardEvent( 'keyup', {
bubbles: true,
cancelable: true,
@@ -103,10 +104,12 @@ export async function setClipboardData( { plainText = '', html = '' } ) {
async function emulateClipboard( type ) {
await page.evaluate( ( _type ) => {
+ const canvasDoc = document.activeElement.contentDocument ?? document;
+
if ( _type !== 'paste' ) {
window._clipboardData = new DataTransfer();
- const selection = window.getSelection();
+ const selection = canvasDoc.defaultView.getSelection();
const plainText = selection.toString();
let html = plainText;
@@ -123,7 +126,7 @@ async function emulateClipboard( type ) {
window._clipboardData.setData( 'text/html', html );
}
- document.activeElement.dispatchEvent(
+ canvasDoc.activeElement.dispatchEvent(
new ClipboardEvent( _type, {
bubbles: true,
cancelable: true,
diff --git a/packages/e2e-tests/config/setup-test-framework.js b/packages/e2e-tests/config/setup-test-framework.js
index 6f5a264ad4da63..631aa1f7f5b0e2 100644
--- a/packages/e2e-tests/config/setup-test-framework.js
+++ b/packages/e2e-tests/config/setup-test-framework.js
@@ -158,6 +158,15 @@ function observeConsoleLogging() {
return;
}
+ // Ignore framer-motion warnings about reduced motion.
+ if (
+ text.includes(
+ 'You have Reduced Motion enabled on your device. Animations may not appear as expected.'
+ )
+ ) {
+ 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/plugins/block-context.php b/packages/e2e-tests/plugins/block-context.php
index d1cc347640718b..dc92132152e1f0 100644
--- a/packages/e2e-tests/plugins/block-context.php
+++ b/packages/e2e-tests/plugins/block-context.php
@@ -52,7 +52,7 @@ function gutenberg_test_register_context_blocks() {
'postId',
'postType',
),
- 'render_callback' => function( $attributes, $content, $block ) {
+ 'render_callback' => static function( $attributes, $content, $block ) {
$ordered_context = array(
$block->context['gutenberg/recordId'],
$block->context['postId'],
diff --git a/packages/e2e-tests/plugins/iframed-block.php b/packages/e2e-tests/plugins/iframed-block.php
index 29e71fa75a2ec8..574a48400ea2fb 100644
--- a/packages/e2e-tests/plugins/iframed-block.php
+++ b/packages/e2e-tests/plugins/iframed-block.php
@@ -9,14 +9,14 @@
add_action(
'setup_theme',
- function() {
+ static function() {
add_theme_support( 'block-templates' );
}
);
add_action(
'init',
- function() {
+ static function() {
wp_register_script(
'iframed-block-jquery-test',
plugin_dir_url( __FILE__ ) . 'iframed-block/jquery.test.js',
diff --git a/packages/e2e-tests/plugins/iframed-block/block.json b/packages/e2e-tests/plugins/iframed-block/block.json
index f1110534e27dc1..85f86dea8131d9 100644
--- a/packages/e2e-tests/plugins/iframed-block/block.json
+++ b/packages/e2e-tests/plugins/iframed-block/block.json
@@ -1,5 +1,5 @@
{
- "apiVersion": 2,
+ "apiVersion": 3,
"name": "test/iframed-block",
"title": "Iframed Block",
"category": "text",
diff --git a/packages/e2e-tests/plugins/iframed-enqueue-block-assets.php b/packages/e2e-tests/plugins/iframed-enqueue-block-assets.php
index c85c77fe11d0e9..ad98354dd45dc7 100644
--- a/packages/e2e-tests/plugins/iframed-enqueue-block-assets.php
+++ b/packages/e2e-tests/plugins/iframed-enqueue-block-assets.php
@@ -9,7 +9,7 @@
add_action(
'enqueue_block_assets',
- function() {
+ static function() {
wp_enqueue_style(
'iframed-enqueue-block-assets',
plugin_dir_url( __FILE__ ) . 'iframed-enqueue-block-assets/style.css',
diff --git a/packages/e2e-tests/plugins/iframed-inline-styles.php b/packages/e2e-tests/plugins/iframed-inline-styles.php
index f54c8eb83e6233..ec44e371af6f08 100644
--- a/packages/e2e-tests/plugins/iframed-inline-styles.php
+++ b/packages/e2e-tests/plugins/iframed-inline-styles.php
@@ -9,14 +9,14 @@
add_action(
'setup_theme',
- function() {
+ static function() {
add_theme_support( 'block-templates' );
}
);
add_action(
'init',
- function() {
+ static function() {
wp_register_script(
'iframed-inline-styles-editor-script',
plugin_dir_url( __FILE__ ) . 'iframed-inline-styles/editor.js',
@@ -40,7 +40,7 @@ function() {
add_action(
'enqueue_block_editor_assets',
- function() {
+ static function() {
wp_enqueue_style(
'iframed-inline-styles-compat-style',
plugin_dir_url( __FILE__ ) . 'iframed-inline-styles/compat-style.css',
diff --git a/packages/e2e-tests/plugins/iframed-inline-styles/block.json b/packages/e2e-tests/plugins/iframed-inline-styles/block.json
index 9e30ecfd6ca544..293b5af5e971f4 100644
--- a/packages/e2e-tests/plugins/iframed-inline-styles/block.json
+++ b/packages/e2e-tests/plugins/iframed-inline-styles/block.json
@@ -1,5 +1,5 @@
{
- "apiVersion": 2,
+ "apiVersion": 3,
"name": "test/iframed-inline-styles",
"title": "Iframed Inline Styles",
"category": "text",
diff --git a/packages/e2e-tests/plugins/iframed-inline-styles/editor.js b/packages/e2e-tests/plugins/iframed-inline-styles/editor.js
index 1898e630758957..719c36a912b5d8 100644
--- a/packages/e2e-tests/plugins/iframed-inline-styles/editor.js
+++ b/packages/e2e-tests/plugins/iframed-inline-styles/editor.js
@@ -4,7 +4,7 @@
const { useBlockProps } = blockEditor;
registerBlockType( 'test/iframed-inline-styles', {
- apiVersion: 2,
+ apiVersion: 3,
edit: function Edit() {
return el( 'div', useBlockProps(), 'Edit' );
},
diff --git a/packages/e2e-tests/plugins/iframed-masonry-block.php b/packages/e2e-tests/plugins/iframed-masonry-block.php
index d8d6ea14120f85..3e7948ae2fa127 100644
--- a/packages/e2e-tests/plugins/iframed-masonry-block.php
+++ b/packages/e2e-tests/plugins/iframed-masonry-block.php
@@ -9,14 +9,14 @@
add_action(
'setup_theme',
- function() {
+ static function() {
add_theme_support( 'block-templates' );
}
);
add_action(
'init',
- function() {
+ static function() {
wp_register_script(
'iframed-masonry-block-editor',
plugin_dir_url( __FILE__ ) . 'iframed-masonry-block/editor.js',
diff --git a/packages/e2e-tests/plugins/iframed-masonry-block/block.json b/packages/e2e-tests/plugins/iframed-masonry-block/block.json
index 6b7c995df96494..b9b3f17234e30d 100644
--- a/packages/e2e-tests/plugins/iframed-masonry-block/block.json
+++ b/packages/e2e-tests/plugins/iframed-masonry-block/block.json
@@ -1,5 +1,5 @@
{
- "apiVersion": 2,
+ "apiVersion": 3,
"name": "test/iframed-masonry-block",
"title": "Iframed Masonry Block",
"category": "text",
diff --git a/packages/e2e-tests/plugins/iframed-masonry-block/editor.js b/packages/e2e-tests/plugins/iframed-masonry-block/editor.js
index 82b0c8d83a7da6..eebd7d370e085a 100644
--- a/packages/e2e-tests/plugins/iframed-masonry-block/editor.js
+++ b/packages/e2e-tests/plugins/iframed-masonry-block/editor.js
@@ -31,7 +31,7 @@
];
registerBlockType( 'test/iframed-masonry-block', {
- apiVersion: 2,
+ apiVersion: 3,
edit: function Edit() {
const ref = useRefEffect( ( node ) => {
const { ownerDocument } = node;
diff --git a/packages/e2e-tests/plugins/iframed-multiple-stylesheets.php b/packages/e2e-tests/plugins/iframed-multiple-stylesheets.php
index 40d7d5040083cf..e70ba31938d14b 100644
--- a/packages/e2e-tests/plugins/iframed-multiple-stylesheets.php
+++ b/packages/e2e-tests/plugins/iframed-multiple-stylesheets.php
@@ -9,14 +9,14 @@
add_action(
'setup_theme',
- function() {
+ static function() {
add_theme_support( 'block-templates' );
}
);
add_action(
'init',
- function() {
+ static function() {
wp_register_script(
'iframed-multiple-stylesheets-editor-script',
plugin_dir_url( __FILE__ ) . 'iframed-multiple-stylesheets/editor.js',
diff --git a/packages/e2e-tests/plugins/iframed-multiple-stylesheets/block.json b/packages/e2e-tests/plugins/iframed-multiple-stylesheets/block.json
index fbd2e65e133970..4db03f471177d7 100644
--- a/packages/e2e-tests/plugins/iframed-multiple-stylesheets/block.json
+++ b/packages/e2e-tests/plugins/iframed-multiple-stylesheets/block.json
@@ -1,5 +1,5 @@
{
- "apiVersion": 2,
+ "apiVersion": 3,
"name": "test/iframed-multiple-stylesheets",
"title": "Iframed Multiple Stylesheets",
"category": "text",
diff --git a/packages/e2e-tests/plugins/iframed-multiple-stylesheets/editor.js b/packages/e2e-tests/plugins/iframed-multiple-stylesheets/editor.js
index be3ded2d9e2b73..fce08fadff850a 100644
--- a/packages/e2e-tests/plugins/iframed-multiple-stylesheets/editor.js
+++ b/packages/e2e-tests/plugins/iframed-multiple-stylesheets/editor.js
@@ -4,7 +4,7 @@
const { useBlockProps } = blockEditor;
registerBlockType( 'test/iframed-multiple-stylesheets', {
- apiVersion: 2,
+ apiVersion: 3,
edit: function Edit() {
return el( 'div', useBlockProps(), 'Edit' );
},
diff --git a/packages/e2e-tests/plugins/marquee-function-widget.php b/packages/e2e-tests/plugins/marquee-function-widget.php
index dda0c3f9a6e273..85881bab14c495 100644
--- a/packages/e2e-tests/plugins/marquee-function-widget.php
+++ b/packages/e2e-tests/plugins/marquee-function-widget.php
@@ -14,7 +14,7 @@ function marquee_greeting_init() {
wp_register_sidebar_widget(
'marquee_greeting',
'Marquee Greeting',
- function() {
+ static function() {
$greeting = get_option( 'marquee_greeting', 'Hello!' );
printf( '%s ', esc_html( $greeting ) );
}
@@ -23,7 +23,7 @@ function() {
wp_register_widget_control(
'marquee_greeting',
'Marquee Greeting',
- function() {
+ static function() {
if ( isset( $_POST['marquee-greeting'] ) ) {
update_option(
'marquee_greeting',
diff --git a/packages/e2e-tests/specs/editor/blocks/post-title.test.js b/packages/e2e-tests/specs/editor/blocks/post-title.test.js
index 621e6815cfba84..0f9fc610be3ee5 100644
--- a/packages/e2e-tests/specs/editor/blocks/post-title.test.js
+++ b/packages/e2e-tests/specs/editor/blocks/post-title.test.js
@@ -5,6 +5,7 @@ import {
createNewPost,
insertBlock,
saveDraft,
+ canvas,
} from '@wordpress/e2e-test-utils';
describe( 'Post Title block', () => {
@@ -14,11 +15,11 @@ describe( 'Post Title block', () => {
it( 'Can edit the post title', async () => {
// Create a block with some text that will trigger a list creation.
- await insertBlock( 'Post Title' );
+ await insertBlock( 'Title' );
const editablePostTitleSelector =
'.wp-block-post-title[contenteditable="true"]';
- await page.waitForSelector( editablePostTitleSelector );
- await page.focus( editablePostTitleSelector );
+ await canvas().waitForSelector( editablePostTitleSelector );
+ await canvas().focus( editablePostTitleSelector );
// Create a second list item.
await page.keyboard.type( 'Just tweaking the post title' );
@@ -26,7 +27,7 @@ describe( 'Post Title block', () => {
await saveDraft();
await page.reload();
await page.waitForSelector( '.edit-post-layout' );
- const title = await page.$eval(
+ const title = await canvas().$eval(
'.editor-post-title__input',
( element ) => element.textContent
);
diff --git a/packages/e2e-tests/specs/editor/blocks/site-title.test.js b/packages/e2e-tests/specs/editor/blocks/site-title.test.js
index 32428a73c8e206..d24b79b6bb3720 100644
--- a/packages/e2e-tests/specs/editor/blocks/site-title.test.js
+++ b/packages/e2e-tests/specs/editor/blocks/site-title.test.js
@@ -9,6 +9,7 @@ import {
pressKeyWithModifier,
setOption,
openDocumentSettingsSidebar,
+ canvas,
} from '@wordpress/e2e-test-utils';
const saveEntities = async () => {
@@ -45,8 +46,8 @@ describe( 'Site Title block', () => {
await insertBlock( 'Site Title' );
const editableSiteTitleSelector =
'[aria-label="Block: Site Title"] a[contenteditable="true"]';
- await page.waitForSelector( editableSiteTitleSelector );
- await page.focus( editableSiteTitleSelector );
+ await canvas().waitForSelector( editableSiteTitleSelector );
+ await canvas().focus( editableSiteTitleSelector );
await pressKeyWithModifier( 'primary', 'a' );
await page.keyboard.type( 'New Site Title' );
diff --git a/packages/e2e-tests/specs/editor/plugins/annotations.test.js b/packages/e2e-tests/specs/editor/plugins/annotations.test.js
index 85265bf424abc5..f0134812d4a7eb 100644
--- a/packages/e2e-tests/specs/editor/plugins/annotations.test.js
+++ b/packages/e2e-tests/specs/editor/plugins/annotations.test.js
@@ -8,6 +8,7 @@ import {
clickOnMoreMenuItem,
createNewPost,
deactivatePlugin,
+ canvas,
} from '@wordpress/e2e-test-utils';
const clickOnBlockSettingsMenuItem = async ( buttonLabel ) => {
@@ -28,6 +29,13 @@ describe( 'Annotations', () => {
beforeEach( async () => {
await createNewPost();
+ // To do: run with iframe.
+ await page.evaluate( () => {
+ window.wp.blocks.registerBlockType( 'test/v2', {
+ apiVersion: '2',
+ title: 'test',
+ } );
+ } );
} );
/**
@@ -51,7 +59,7 @@ describe( 'Annotations', () => {
await page.$x( "//button[contains(text(), 'Add annotation')]" )
)[ 0 ];
await addAnnotationButton.click();
- await page.evaluate( () =>
+ await canvas().evaluate( () =>
document.querySelector( '.wp-block-paragraph' ).focus()
);
}
@@ -67,7 +75,7 @@ describe( 'Annotations', () => {
await page.$x( "//button[contains(text(), 'Remove annotations')]" )
)[ 0 ];
await addAnnotationButton.click();
- await page.evaluate( () =>
+ await canvas().evaluate( () =>
document.querySelector( '[contenteditable]' ).focus()
);
}
@@ -78,11 +86,11 @@ describe( 'Annotations', () => {
* @return {Promise} The annotated text.
*/
async function getAnnotatedText() {
- const annotations = await page.$$( ANNOTATIONS_SELECTOR );
+ const annotations = await canvas().$$( ANNOTATIONS_SELECTOR );
const annotation = annotations[ 0 ];
- return await page.evaluate( ( el ) => el.innerText, annotation );
+ return await canvas().evaluate( ( el ) => el.innerText, annotation );
}
/**
@@ -91,7 +99,7 @@ describe( 'Annotations', () => {
* @return {Promise} Inner HTML.
*/
async function getRichTextInnerHTML() {
- const htmlContent = await page.$$( '.wp-block-paragraph' );
+ const htmlContent = await canvas().$$( '.wp-block-paragraph' );
return await page.evaluate( ( el ) => {
return el.innerHTML;
}, htmlContent[ 0 ] );
@@ -102,12 +110,12 @@ describe( 'Annotations', () => {
await clickOnMoreMenuItem( 'Annotations' );
- let annotations = await page.$$( ANNOTATIONS_SELECTOR );
+ let annotations = await canvas().$$( ANNOTATIONS_SELECTOR );
expect( annotations ).toHaveLength( 0 );
await annotateFirstBlock( 9, 13 );
- annotations = await page.$$( ANNOTATIONS_SELECTOR );
+ annotations = await canvas().$$( ANNOTATIONS_SELECTOR );
expect( annotations ).toHaveLength( 1 );
const text = await getAnnotatedText();
@@ -115,7 +123,7 @@ describe( 'Annotations', () => {
await clickOnBlockSettingsMenuItem( 'Edit as HTML' );
- const htmlContent = await page.$$(
+ const htmlContent = await canvas().$$(
'.block-editor-block-list__block-html-textarea'
);
const html = await page.evaluate( ( el ) => {
@@ -136,7 +144,7 @@ describe( 'Annotations', () => {
await page.keyboard.type( 'D' );
await removeAnnotations();
- const htmlContent = await page.$$( '.wp-block-paragraph' );
+ const htmlContent = await canvas().$$( '.wp-block-paragraph' );
const html = await page.evaluate( ( el ) => {
return el.innerHTML;
}, htmlContent[ 0 ] );
diff --git a/packages/e2e-tests/specs/editor/plugins/block-variations.test.js b/packages/e2e-tests/specs/editor/plugins/block-variations.test.js
index 886382a4667b1e..4a24cd3f478d26 100644
--- a/packages/e2e-tests/specs/editor/plugins/block-variations.test.js
+++ b/packages/e2e-tests/specs/editor/plugins/block-variations.test.js
@@ -11,6 +11,7 @@ import {
openDocumentSettingsSidebar,
togglePreferencesOption,
toggleMoreMenu,
+ canvas,
} from '@wordpress/e2e-test-utils';
describe( 'Block variations', () => {
@@ -45,7 +46,7 @@ describe( 'Block variations', () => {
await insertBlock( 'Large Quote' );
expect(
- await page.$(
+ await canvas().$(
'.wp-block[data-type="core/quote"] blockquote.is-style-large'
)
).toBeDefined();
@@ -58,7 +59,7 @@ describe( 'Block variations', () => {
await page.keyboard.press( 'Enter' );
expect(
- await page.$(
+ await canvas().$(
'.wp-block[data-type="core/quote"] blockquote.is-style-large'
)
).toBeDefined();
@@ -75,7 +76,7 @@ describe( 'Block variations', () => {
test( 'Insert the Success Message block variation', async () => {
await insertBlock( 'Success Message' );
- const successMessageBlock = await page.$(
+ const successMessageBlock = await canvas().$(
'.wp-block[data-type="core/paragraph"]'
);
expect( successMessageBlock ).toBeDefined();
@@ -86,12 +87,12 @@ describe( 'Block variations', () => {
test( 'Pick the additional variation in the inserted Columns block', async () => {
await insertBlock( 'Columns' );
- const fourColumnsVariation = await page.waitForSelector(
+ const fourColumnsVariation = await canvas().waitForSelector(
'.wp-block[data-type="core/columns"] .block-editor-block-variation-picker__variation[aria-label="Four columns"]'
);
await fourColumnsVariation.click();
expect(
- await page.$$(
+ await canvas().$$(
'.wp-block[data-type="core/columns"] .wp-block[data-type="core/column"]'
)
).toHaveLength( 4 );
diff --git a/packages/e2e-tests/specs/editor/plugins/cpt-locking.test.js b/packages/e2e-tests/specs/editor/plugins/cpt-locking.test.js
index 1a4d8e2b6d4332..447be0793fafbb 100644
--- a/packages/e2e-tests/specs/editor/plugins/cpt-locking.test.js
+++ b/packages/e2e-tests/specs/editor/plugins/cpt-locking.test.js
@@ -12,6 +12,7 @@ import {
pressKeyTimes,
pressKeyWithModifier,
setPostContent,
+ canvas,
} from '@wordpress/e2e-test-utils';
describe( 'cpt locking', () => {
@@ -35,7 +36,7 @@ describe( 'cpt locking', () => {
};
const shouldNotAllowBlocksToBeRemoved = async () => {
- await page.type(
+ await canvas().type(
'.block-editor-rich-text__editable[data-type="core/paragraph"]',
'p1'
);
@@ -46,12 +47,12 @@ describe( 'cpt locking', () => {
};
const shouldAllowBlocksToBeMoved = async () => {
- await page.click(
+ await canvas().click(
'div > .block-editor-rich-text__editable[data-type="core/paragraph"]'
);
expect( await page.$( 'button[aria-label="Move up"]' ) ).not.toBeNull();
await page.click( 'button[aria-label="Move up"]' );
- await page.type(
+ await canvas().type(
'div > .block-editor-rich-text__editable[data-type="core/paragraph"]',
'p1'
);
@@ -71,14 +72,14 @@ describe( 'cpt locking', () => {
);
it( 'should not allow blocks to be moved', async () => {
- await page.click(
+ await canvas().click(
'.block-editor-rich-text__editable[data-type="core/paragraph"]'
);
expect( await page.$( 'button[aria-label="Move up"]' ) ).toBeNull();
} );
it( 'should not error when deleting the cotents of a paragraph', async () => {
- await page.click(
+ await canvas().click(
'.block-editor-block-list__block[data-type="core/paragraph"]'
);
const textToType = 'Paragraph';
@@ -88,7 +89,7 @@ describe( 'cpt locking', () => {
} );
it( 'should insert line breaks when using enter and shift-enter', async () => {
- await page.click(
+ await canvas().click(
'.block-editor-block-list__block[data-type="core/paragraph"]'
);
await page.keyboard.type( 'First line' );
@@ -118,12 +119,14 @@ describe( 'cpt locking', () => {
} );
it( 'should not allow blocks to be inserted in inner blocks', async () => {
- await page.click( 'button[aria-label="Two columns; equal split"]' );
+ await canvas().click(
+ 'button[aria-label="Two columns; equal split"]'
+ );
await page.evaluate(
() => new Promise( window.requestIdleCallback )
);
expect(
- await page.$(
+ await canvas().$(
'.wp-block-column .block-editor-button-block-appender'
)
).toBeNull();
@@ -173,7 +176,7 @@ describe( 'cpt locking', () => {
} );
it( 'should allow blocks to be removed', async () => {
- await page.type(
+ await canvas().type(
'.block-editor-rich-text__editable[data-type="core/paragraph"]',
'p1'
);
@@ -193,7 +196,7 @@ describe( 'cpt locking', () => {
} );
it( 'should allow blocks to be removed', async () => {
- await page.type(
+ await canvas().type(
'div > .block-editor-rich-text__editable[data-type="core/paragraph"]',
'p1'
);
@@ -219,7 +222,7 @@ describe( 'cpt locking', () => {
);
it( 'should not allow blocks to be moved', async () => {
- await page.click(
+ await canvas().click(
'.block-editor-rich-text__editable[data-type="core/paragraph"]'
);
expect( await page.$( 'button[aria-label="Move up"]' ) ).toBeNull();
@@ -239,7 +242,7 @@ describe( 'cpt locking', () => {
);
it( 'should not allow blocks to be moved', async () => {
- await page.click(
+ await canvas().click(
'.block-editor-rich-text__editable[data-type="core/paragraph"]'
);
expect( await page.$( 'button[aria-label="Move up"]' ) ).toBeNull();
diff --git a/packages/e2e-tests/specs/editor/plugins/iframed-inline-styles.test.js b/packages/e2e-tests/specs/editor/plugins/iframed-inline-styles.test.js
index 2a9af38c49209c..eafc0b1f48b614 100644
--- a/packages/e2e-tests/specs/editor/plugins/iframed-inline-styles.test.js
+++ b/packages/e2e-tests/specs/editor/plugins/iframed-inline-styles.test.js
@@ -35,8 +35,10 @@ describe( 'iframed inline styles', () => {
await insertBlock( 'Iframed Inline Styles' );
expect( await getEditedPostContent() ).toMatchSnapshot();
- expect( await getComputedStyle( page, 'padding' ) ).toBe( '20px' );
- expect( await getComputedStyle( page, 'border-width' ) ).toBe( '2px' );
+ expect( await getComputedStyle( canvas(), 'padding' ) ).toBe( '20px' );
+ expect( await getComputedStyle( canvas(), 'border-width' ) ).toBe(
+ '2px'
+ );
await createNewTemplate( 'Iframed Test' );
@@ -48,5 +50,7 @@ describe( 'iframed inline styles', () => {
expect( await getComputedStyle( canvas(), 'border-width' ) ).toBe(
'2px'
);
+
+ expect( console ).toHaveWarned();
} );
} );
diff --git a/packages/e2e-tests/specs/editor/plugins/iframed-masonry-block.test.js b/packages/e2e-tests/specs/editor/plugins/iframed-masonry-block.test.js
index 47767edc8e5d29..503beeb92e1642 100644
--- a/packages/e2e-tests/specs/editor/plugins/iframed-masonry-block.test.js
+++ b/packages/e2e-tests/specs/editor/plugins/iframed-masonry-block.test.js
@@ -39,7 +39,7 @@ describe( 'iframed masonry block', () => {
await insertBlock( 'Iframed Masonry Block' );
expect( await getEditedPostContent() ).toMatchSnapshot();
- expect( await didMasonryLoadCorrectly( page ) ).toBe( true );
+ expect( await didMasonryLoadCorrectly( canvas() ) ).toBe( true );
await createNewTemplate( 'Iframed Test' );
await canvas().waitForSelector( '.grid-item[style]' );
diff --git a/packages/e2e-tests/specs/editor/plugins/iframed-multiple-block-stylesheets.test.js b/packages/e2e-tests/specs/editor/plugins/iframed-multiple-block-stylesheets.test.js
index 9d8e5975d3f2ec..23058b48b8da43 100644
--- a/packages/e2e-tests/specs/editor/plugins/iframed-multiple-block-stylesheets.test.js
+++ b/packages/e2e-tests/specs/editor/plugins/iframed-multiple-block-stylesheets.test.js
@@ -35,7 +35,7 @@ describe( 'iframed multiple block stylesheets', () => {
it( 'should load multiple block stylesheets in iframe', async () => {
await insertBlock( 'Iframed Multiple Stylesheets' );
- await page.waitForSelector(
+ await canvas().waitForSelector(
'.wp-block-test-iframed-multiple-stylesheets'
);
await createNewTemplate( 'Iframed Test' );
diff --git a/packages/e2e-tests/specs/editor/plugins/inner-blocks-allowed-blocks.test.js b/packages/e2e-tests/specs/editor/plugins/inner-blocks-allowed-blocks.test.js
index 4431d3bd5802f0..aec9987f596a86 100644
--- a/packages/e2e-tests/specs/editor/plugins/inner-blocks-allowed-blocks.test.js
+++ b/packages/e2e-tests/specs/editor/plugins/inner-blocks-allowed-blocks.test.js
@@ -10,6 +10,7 @@ import {
openGlobalBlockInserter,
closeGlobalBlockInserter,
clickBlockToolbarButton,
+ canvas,
} from '@wordpress/e2e-test-utils';
describe( 'Allowed Blocks Setting on InnerBlocks', () => {
@@ -32,8 +33,8 @@ describe( 'Allowed Blocks Setting on InnerBlocks', () => {
const childParagraphSelector = `${ parentBlockSelector } ${ paragraphSelector }`;
await insertBlock( 'Allowed Blocks Unset' );
await closeGlobalBlockInserter();
- await page.waitForSelector( childParagraphSelector );
- await page.click( childParagraphSelector );
+ await canvas().waitForSelector( childParagraphSelector );
+ await canvas().click( childParagraphSelector );
await openGlobalBlockInserter();
await expect(
(
@@ -47,8 +48,8 @@ describe( 'Allowed Blocks Setting on InnerBlocks', () => {
const childParagraphSelector = `${ parentBlockSelector } ${ paragraphSelector }`;
await insertBlock( 'Allowed Blocks Set' );
await closeGlobalBlockInserter();
- await page.waitForSelector( childParagraphSelector );
- await page.click( childParagraphSelector );
+ await canvas().waitForSelector( childParagraphSelector );
+ await canvas().click( childParagraphSelector );
await openGlobalBlockInserter();
const allowedBlocks = await getAllBlockInserterItemTitles();
expect( allowedBlocks.sort() ).toEqual( [
@@ -66,8 +67,8 @@ describe( 'Allowed Blocks Setting on InnerBlocks', () => {
const parentBlockSelector = '[data-type="test/allowed-blocks-dynamic"]';
const blockAppender = '.block-list-appender button';
const appenderSelector = `${ parentBlockSelector } ${ blockAppender }`;
- await page.waitForSelector( appenderSelector );
- await page.click( appenderSelector );
+ await canvas().waitForSelector( appenderSelector );
+ await canvas().click( appenderSelector );
expect( await getAllBlockInserterItemTitles() ).toEqual( [
'Image',
'List',
diff --git a/packages/e2e-tests/specs/editor/plugins/inner-blocks-prioritized-inserter-blocks.test.js b/packages/e2e-tests/specs/editor/plugins/inner-blocks-prioritized-inserter-blocks.test.js
index 77ae8d38274325..7621fbea12140a 100644
--- a/packages/e2e-tests/specs/editor/plugins/inner-blocks-prioritized-inserter-blocks.test.js
+++ b/packages/e2e-tests/specs/editor/plugins/inner-blocks-prioritized-inserter-blocks.test.js
@@ -8,6 +8,7 @@ import {
getAllBlockInserterItemTitles,
insertBlock,
closeGlobalBlockInserter,
+ canvas,
} from '@wordpress/e2e-test-utils';
const QUICK_INSERTER_RESULTS_SELECTOR =
@@ -108,7 +109,7 @@ describe( 'Prioritized Inserter Blocks Setting on InnerBlocks', () => {
describe( 'Slash inserter', () => {
it( 'uses the priority ordering if prioritzed blocks setting is set', async () => {
await insertBlock( 'Prioritized Inserter Blocks Set' );
- await page.click( '[data-type="core/image"]' );
+ await canvas().click( '[data-type="core/image"]' );
await page.keyboard.press( 'Enter' );
await page.keyboard.type( '/' );
// Wait for the results to display.
diff --git a/packages/e2e-tests/specs/editor/various/__snapshots__/links.test.js.snap b/packages/e2e-tests/specs/editor/various/__snapshots__/links.test.js.snap
index 7b54b5bd2f598f..541a5456fd4d53 100644
--- a/packages/e2e-tests/specs/editor/various/__snapshots__/links.test.js.snap
+++ b/packages/e2e-tests/specs/editor/various/__snapshots__/links.test.js.snap
@@ -12,18 +12,6 @@ exports[`Links can be created by selecting text and clicking Link 1`] = `
"
`;
-exports[`Links can be created by selecting text and using keyboard shortcuts 1`] = `
-"
-This is Gutenberg
-"
-`;
-
-exports[`Links can be created by selecting text and using keyboard shortcuts 2`] = `
-"
-This is Gutenberg
-"
-`;
-
exports[`Links can be created instantly when a URL is selected 1`] = `
"
This is Gutenberg: https://wordpress.org/gutenberg
@@ -59,15 +47,3 @@ exports[`Links can be removed 1`] = `
This is Gutenberg
"
`;
-
-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/editor/various/adding-inline-tokens.test.js b/packages/e2e-tests/specs/editor/various/adding-inline-tokens.test.js
index 513a9fa7af27bc..7c04bae1f95ddb 100644
--- a/packages/e2e-tests/specs/editor/various/adding-inline-tokens.test.js
+++ b/packages/e2e-tests/specs/editor/various/adding-inline-tokens.test.js
@@ -68,7 +68,6 @@ describe( 'adding inline tokens', () => {
'.block-editor-format-toolbar__image-popover'
);
await page.keyboard.press( 'Tab' );
- await page.keyboard.press( 'Tab' );
await page.keyboard.type( '20' );
await page.keyboard.press( 'Enter' );
diff --git a/packages/e2e-tests/specs/editor/various/autosave.test.js b/packages/e2e-tests/specs/editor/various/autosave.test.js
index abf4ad8b83e68a..528efc2d463167 100644
--- a/packages/e2e-tests/specs/editor/various/autosave.test.js
+++ b/packages/e2e-tests/specs/editor/various/autosave.test.js
@@ -9,6 +9,7 @@ import {
publishPost,
saveDraft,
toggleOfflineMode,
+ canvas,
} from '@wordpress/e2e-test-utils';
// Constant to override editor preference
@@ -258,7 +259,7 @@ describe( 'autosave', () => {
await page.keyboard.type( 'before publish' );
await publishPost();
- await page.click( '[data-type="core/paragraph"]' );
+ await canvas().click( '[data-type="core/paragraph"]' );
await page.keyboard.type( ' after publish' );
// Trigger remote autosave.
diff --git a/packages/e2e-tests/specs/editor/various/block-editor-keyboard-shortcuts.test.js b/packages/e2e-tests/specs/editor/various/block-editor-keyboard-shortcuts.test.js
index e6e156ca8ee15d..f1e6be7b816abb 100644
--- a/packages/e2e-tests/specs/editor/various/block-editor-keyboard-shortcuts.test.js
+++ b/packages/e2e-tests/specs/editor/various/block-editor-keyboard-shortcuts.test.js
@@ -9,6 +9,7 @@ import {
clickBlockToolbarButton,
clickMenuItem,
clickOnCloseModalButton,
+ canvas,
} from '@wordpress/e2e-test-utils';
const createTestParagraphBlocks = async () => {
@@ -51,7 +52,7 @@ describe( 'block editor keyboard shortcuts', () => {
await createTestParagraphBlocks();
expect( await getEditedPostContent() ).toMatchSnapshot();
await pressKeyWithModifier( 'shift', 'ArrowUp' );
- await page.waitForSelector(
+ await canvas().waitForSelector(
'[aria-label="Multiple selected blocks"]'
);
await moveUp();
@@ -63,7 +64,7 @@ describe( 'block editor keyboard shortcuts', () => {
expect( await getEditedPostContent() ).toMatchSnapshot();
await page.keyboard.press( 'ArrowUp' );
await pressKeyWithModifier( 'shift', 'ArrowUp' );
- await page.waitForSelector(
+ await canvas().waitForSelector(
'[aria-label="Multiple selected blocks"]'
);
await moveDown();
diff --git a/packages/e2e-tests/specs/editor/various/block-grouping.test.js b/packages/e2e-tests/specs/editor/various/block-grouping.test.js
index 07dcabcbf0526d..f67273a550d1c2 100644
--- a/packages/e2e-tests/specs/editor/various/block-grouping.test.js
+++ b/packages/e2e-tests/specs/editor/various/block-grouping.test.js
@@ -14,6 +14,7 @@ import {
activatePlugin,
deactivatePlugin,
createReusableBlock,
+ canvas,
} from '@wordpress/e2e-test-utils';
async function insertBlocksOfSameType() {
@@ -115,8 +116,8 @@ describe( 'Block Grouping', () => {
const getParagraphText = async () => {
const paragraphInReusableSelector =
'.block-editor-block-list__block[data-type="core/block"] p';
- await page.waitForSelector( paragraphInReusableSelector );
- return page.$eval(
+ await canvas().waitForSelector( paragraphInReusableSelector );
+ return canvas().$eval(
paragraphInReusableSelector,
( element ) => element.innerText
);
@@ -128,14 +129,14 @@ describe( 'Block Grouping', () => {
await clickBlockToolbarButton( 'Options' );
await clickMenuItem( 'Group' );
- let group = await page.$$( '[data-type="core/group"]' );
+ let group = await canvas().$$( '[data-type="core/group"]' );
expect( group ).toHaveLength( 1 );
// Make sure the paragraph in reusable block exists.
expect( await getParagraphText() ).toMatch( paragraphText );
await clickBlockToolbarButton( 'Options' );
await clickMenuItem( 'Ungroup' );
- group = await page.$$( '[data-type="core/group"]' );
+ group = await canvas().$$( '[data-type="core/group"]' );
expect( group ).toHaveLength( 0 );
// Make sure the paragraph in reusable block exists.
expect( await getParagraphText() ).toEqual( paragraphText );
diff --git a/packages/e2e-tests/specs/editor/various/block-hierarchy-navigation.test.js b/packages/e2e-tests/specs/editor/various/block-hierarchy-navigation.test.js
index b1c5a7278efda8..9737007ce656e4 100644
--- a/packages/e2e-tests/specs/editor/various/block-hierarchy-navigation.test.js
+++ b/packages/e2e-tests/specs/editor/various/block-hierarchy-navigation.test.js
@@ -10,6 +10,7 @@ import {
openDocumentSettingsSidebar,
getListViewBlocks,
switchBlockInspectorTab,
+ canvas,
} from '@wordpress/e2e-test-utils';
async function openListViewSidebar() {
@@ -53,7 +54,7 @@ describe( 'Navigating the block hierarchy', () => {
it( 'should navigate using the list view sidebar', async () => {
await insertBlock( 'Columns' );
- await page.click( '[aria-label="Two columns; equal split"]' );
+ await canvas().click( '[aria-label="Two columns; equal split"]' );
// Add a paragraph in the first column.
await page.keyboard.press( 'ArrowDown' ); // Navigate to inserter.
@@ -114,7 +115,7 @@ describe( 'Navigating the block hierarchy', () => {
it( 'should navigate block hierarchy using only the keyboard', async () => {
await insertBlock( 'Columns' );
await openDocumentSettingsSidebar();
- await page.click( '[aria-label="Two columns; equal split"]' );
+ await canvas().click( '[aria-label="Two columns; equal split"]' );
// Add a paragraph in the first column.
await page.keyboard.press( 'ArrowDown' ); // Navigate to inserter.
@@ -141,13 +142,15 @@ describe( 'Navigating the block hierarchy', () => {
// Navigate to the third column in the columns block.
await pressKeyWithModifier( 'ctrlShift', '`' );
await pressKeyWithModifier( 'ctrlShift', '`' );
- await pressKeyTimes( 'Tab', 4 );
+ await pressKeyTimes( 'Tab', 3 );
await pressKeyTimes( 'ArrowDown', 4 );
- await page.waitForSelector(
+ await canvas().waitForSelector(
'.is-highlighted[aria-label="Block: Column (3 of 3)"]'
);
await page.keyboard.press( 'Enter' );
- await page.waitForSelector( '.is-selected[data-type="core/column"]' );
+ await canvas().waitForSelector(
+ '.is-selected[data-type="core/column"]'
+ );
// Insert text in the last column block.
await page.keyboard.press( 'ArrowDown' ); // Navigate to inserter.
@@ -190,12 +193,12 @@ describe( 'Navigating the block hierarchy', () => {
// Insert a group block.
await insertBlock( 'Group' );
// Select the default, selected Group layout from the variation picker.
- await page.click(
+ await canvas().click(
'button[aria-label="Group: Gather blocks in a container."]'
);
// Insert some random blocks.
// The last block shouldn't be a textual block.
- await page.click( '.block-list-appender .block-editor-inserter' );
+ await canvas().click( '.block-list-appender .block-editor-inserter' );
const paragraphMenuItem = (
await page.$x( `//button//span[contains(text(), 'Paragraph')]` )
)[ 0 ];
@@ -207,7 +210,7 @@ describe( 'Navigating the block hierarchy', () => {
expect( await getEditedPostContent() ).toMatchSnapshot();
// Unselect the blocks.
- await page.click( '.editor-post-title' );
+ await canvas().click( '.editor-post-title' );
// Try selecting the group block using the Outline.
await page.click(
@@ -217,7 +220,7 @@ describe( 'Navigating the block hierarchy', () => {
await groupMenuItem.click();
// The group block's wrapper should be selected.
- const isGroupBlockSelected = await page.evaluate(
+ const isGroupBlockSelected = await canvas().evaluate(
() =>
document.activeElement.getAttribute( 'data-type' ) ===
'core/group'
diff --git a/packages/e2e-tests/specs/editor/various/change-detection.test.js b/packages/e2e-tests/specs/editor/various/change-detection.test.js
index dab3765ef0d148..97157060d36245 100644
--- a/packages/e2e-tests/specs/editor/various/change-detection.test.js
+++ b/packages/e2e-tests/specs/editor/various/change-detection.test.js
@@ -11,6 +11,7 @@ import {
openDocumentSettingsSidebar,
isCurrentURL,
openTypographyToolsPanelMenu,
+ canvas,
} from '@wordpress/e2e-test-utils';
describe( 'Change detection', () => {
@@ -78,7 +79,7 @@ describe( 'Change detection', () => {
} );
it( 'Should autosave post', async () => {
- await page.type( '.editor-post-title__input', 'Hello World' );
+ await canvas().type( '.editor-post-title__input', 'Hello World' );
// Force autosave to occur immediately.
await Promise.all( [
@@ -94,7 +95,7 @@ describe( 'Change detection', () => {
} );
it( 'Should prompt to confirm unsaved changes for autosaved draft for non-content fields', async () => {
- await page.type( '.editor-post-title__input', 'Hello World' );
+ await canvas().type( '.editor-post-title__input', 'Hello World' );
// Toggle post as needing review (not persisted for autosave).
await ensureSidebarOpened();
@@ -117,7 +118,7 @@ describe( 'Change detection', () => {
} );
it( 'Should prompt to confirm unsaved changes for autosaved published post', async () => {
- await page.type( '.editor-post-title__input', 'Hello World' );
+ await canvas().type( '.editor-post-title__input', 'Hello World' );
await publishPost();
@@ -130,7 +131,7 @@ describe( 'Change detection', () => {
] );
// Should be dirty after autosave change of published post.
- await page.type( '.editor-post-title__input', '!' );
+ await canvas().type( '.editor-post-title__input', '!' );
await Promise.all( [
page.waitForSelector(
@@ -162,7 +163,7 @@ describe( 'Change detection', () => {
} );
it( 'Should prompt if property changed without save', async () => {
- await page.type( '.editor-post-title__input', 'Hello World' );
+ await canvas().type( '.editor-post-title__input', 'Hello World' );
await assertIsDirty( true );
} );
@@ -175,7 +176,7 @@ describe( 'Change detection', () => {
} );
it( 'Should not prompt if changes saved', async () => {
- await page.type( '.editor-post-title__input', 'Hello World' );
+ await canvas().type( '.editor-post-title__input', 'Hello World' );
await saveDraft();
@@ -192,7 +193,7 @@ describe( 'Change detection', () => {
} );
it( 'Should not save if all changes saved', async () => {
- await page.type( '.editor-post-title__input', 'Hello World' );
+ await canvas().type( '.editor-post-title__input', 'Hello World' );
await saveDraft();
@@ -205,7 +206,7 @@ describe( 'Change detection', () => {
} );
it( 'Should prompt if save failed', async () => {
- await page.type( '.editor-post-title__input', 'Hello World' );
+ await canvas().type( '.editor-post-title__input', 'Hello World' );
await page.setOfflineMode( true );
@@ -231,7 +232,7 @@ describe( 'Change detection', () => {
} );
it( 'Should prompt if changes and save is in-flight', async () => {
- await page.type( '.editor-post-title__input', 'Hello World' );
+ await canvas().type( '.editor-post-title__input', 'Hello World' );
// Hold the posts request so we don't deal with race conditions of the
// save completing early. Other requests should be allowed to continue,
@@ -247,7 +248,7 @@ describe( 'Change detection', () => {
} );
it( 'Should prompt if changes made while save is in-flight', async () => {
- await page.type( '.editor-post-title__input', 'Hello World' );
+ await canvas().type( '.editor-post-title__input', 'Hello World' );
// Hold the posts request so we don't deal with race conditions of the
// save completing early. Other requests should be allowed to continue,
@@ -257,7 +258,7 @@ describe( 'Change detection', () => {
// Keyboard shortcut Ctrl+S save.
await pressKeyWithModifier( 'primary', 'S' );
- await page.type( '.editor-post-title__input', '!' );
+ await canvas().type( '.editor-post-title__input', '!' );
await page.waitForSelector( '.editor-post-save-draft' );
await releaseSaveIntercept();
@@ -266,7 +267,7 @@ describe( 'Change detection', () => {
} );
it( 'Should prompt if property changes made while save is in-flight, and save completes', async () => {
- await page.type( '.editor-post-title__input', 'Hello World' );
+ await canvas().type( '.editor-post-title__input', 'Hello World' );
// Hold the posts request so we don't deal with race conditions of the
// save completing early.
@@ -282,7 +283,7 @@ describe( 'Change detection', () => {
);
// Dirty post while save is in-flight.
- await page.type( '.editor-post-title__input', '!' );
+ await canvas().type( '.editor-post-title__input', '!' );
// Allow save to complete. Disabling interception flushes pending.
await Promise.all( [ savedPromise, releaseSaveIntercept() ] );
@@ -291,7 +292,7 @@ describe( 'Change detection', () => {
} );
it( 'Should prompt if block revision is made while save is in-flight, and save completes', async () => {
- await page.type( '.editor-post-title__input', 'Hello World' );
+ await canvas().type( '.editor-post-title__input', 'Hello World' );
// Hold the posts request so we don't deal with race conditions of the
// save completing early.
@@ -324,7 +325,7 @@ describe( 'Change detection', () => {
await saveDraft();
// Verify that the title is empty.
- const title = await page.$eval(
+ const title = await canvas().$eval(
'.editor-post-title__input',
// Trim padding non-breaking space.
( element ) => element.textContent.trim()
@@ -337,7 +338,7 @@ describe( 'Change detection', () => {
it( 'should not prompt to confirm unsaved changes when trashing an existing post', async () => {
// Enter title.
- await page.type( '.editor-post-title__input', 'Hello World' );
+ await canvas().type( '.editor-post-title__input', 'Hello World' );
// Save.
await saveDraft();
@@ -381,7 +382,7 @@ describe( 'Change detection', () => {
] );
// Change the paragraph's `drop cap`.
- await page.click( '[data-type="core/paragraph"]' );
+ await canvas().click( '[data-type="core/paragraph"]' );
await openTypographyToolsPanelMenu();
await page.click( 'button[aria-label="Show Drop cap"]' );
@@ -390,7 +391,7 @@ describe( 'Change detection', () => {
"//label[contains(text(), 'Drop cap')]"
);
await dropCapToggle.click();
- await page.click( '[data-type="core/paragraph"]' );
+ await canvas().click( '[data-type="core/paragraph"]' );
// Check that the post is dirty.
await page.waitForSelector( '.editor-post-save-draft' );
@@ -402,7 +403,7 @@ describe( 'Change detection', () => {
] );
// Change the paragraph's `drop cap` again.
- await page.click( '[data-type="core/paragraph"]' );
+ await canvas().click( '[data-type="core/paragraph"]' );
await dropCapToggle.click();
// Check that the post is dirty.
diff --git a/packages/e2e-tests/specs/editor/various/editor-modes.test.js b/packages/e2e-tests/specs/editor/various/editor-modes.test.js
index de328b87f736d0..81878ebf7208e3 100644
--- a/packages/e2e-tests/specs/editor/various/editor-modes.test.js
+++ b/packages/e2e-tests/specs/editor/various/editor-modes.test.js
@@ -11,6 +11,7 @@ import {
pressKeyTimes,
pressKeyWithModifier,
openTypographyToolsPanelMenu,
+ canvas,
} from '@wordpress/e2e-test-utils';
describe( 'Editing modes (visual/HTML)', () => {
@@ -22,7 +23,7 @@ describe( 'Editing modes (visual/HTML)', () => {
it( 'should switch between visual and HTML modes', async () => {
// This block should be in "visual" mode by default.
- let visualBlock = await page.$$( '[data-block].rich-text' );
+ let visualBlock = await canvas().$$( '[data-block].rich-text' );
expect( visualBlock ).toHaveLength( 1 );
// Change editing mode from "Visual" to "HTML".
@@ -30,7 +31,7 @@ describe( 'Editing modes (visual/HTML)', () => {
await clickMenuItem( 'Edit as HTML' );
// Wait for the block to be converted to HTML editing mode.
- const htmlBlock = await page.$$(
+ const htmlBlock = await canvas().$$(
'[data-block] .block-editor-block-list__block-html-textarea'
);
expect( htmlBlock ).toHaveLength( 1 );
@@ -40,7 +41,7 @@ describe( 'Editing modes (visual/HTML)', () => {
await clickMenuItem( 'Edit visually' );
// This block should be in "visual" mode by default.
- visualBlock = await page.$$( '[data-block].rich-text' );
+ visualBlock = await canvas().$$( '[data-block].rich-text' );
expect( visualBlock ).toHaveLength( 1 );
} );
@@ -67,7 +68,7 @@ describe( 'Editing modes (visual/HTML)', () => {
await clickMenuItem( 'Edit as HTML' );
// Make sure the paragraph content is rendered as expected.
- let htmlBlockContent = await page.$eval(
+ let htmlBlockContent = await canvas().$eval(
'.block-editor-block-list__layout .block-editor-block-list__block .block-editor-block-list__block-html-textarea',
( node ) => node.textContent
);
@@ -83,7 +84,7 @@ describe( 'Editing modes (visual/HTML)', () => {
await dropCapToggle.click();
// Make sure the HTML content updated.
- htmlBlockContent = await page.$eval(
+ htmlBlockContent = await canvas().$eval(
'.block-editor-block-list__layout .block-editor-block-list__block .block-editor-block-list__block-html-textarea',
( node ) => node.textContent
);
@@ -138,7 +139,7 @@ describe( 'Editing modes (visual/HTML)', () => {
const editPosition = textContent.indexOf( 'Hello' );
// Replace the word 'Hello' with 'Hi'.
- await page.click( '.editor-post-title__input' );
+ await canvas().click( '.editor-post-title__input' );
await page.keyboard.press( 'Tab' );
await pressKeyTimes( 'ArrowRight', editPosition );
await pressKeyTimes( 'Delete', 5 );
diff --git a/packages/e2e-tests/specs/editor/various/embedding.test.js b/packages/e2e-tests/specs/editor/various/embedding.test.js
index a7522b88e7729f..4461fc62330530 100644
--- a/packages/e2e-tests/specs/editor/various/embedding.test.js
+++ b/packages/e2e-tests/specs/editor/various/embedding.test.js
@@ -3,7 +3,6 @@
*/
import {
clickBlockAppender,
- clickButton,
createEmbeddingMatcher,
createJSONResponse,
createNewPost,
@@ -12,6 +11,7 @@ import {
insertBlock,
publishPost,
setUpResponseMocking,
+ canvas,
} from '@wordpress/e2e-test-utils';
const MOCK_EMBED_WORDPRESS_SUCCESS_RESPONSE = {
@@ -178,24 +178,24 @@ describe( 'Embedding content', () => {
it( 'should render embeds in the correct state', async () => {
// Valid embed. Should render valid figure element.
await insertEmbed( 'https://twitter.com/notnownikki' );
- await page.waitForSelector( 'figure.wp-block-embed' );
+ await canvas().waitForSelector( 'figure.wp-block-embed' );
// Valid provider; invalid content. Should render failed, edit state.
await insertEmbed( 'https://twitter.com/wooyaygutenberg123454312' );
- await page.waitForSelector(
+ await canvas().waitForSelector(
'input[value="https://twitter.com/wooyaygutenberg123454312"]'
);
// WordPress invalid content. Should render failed, edit state.
await insertEmbed( 'https://wordpress.org/gutenberg/handbook/' );
- await page.waitForSelector(
+ await canvas().waitForSelector(
'input[value="https://wordpress.org/gutenberg/handbook/"]'
);
// Provider whose oembed API has gone wrong. Should render failed, edit
// state.
await insertEmbed( 'https://twitter.com/thatbunty' );
- await page.waitForSelector(
+ await canvas().waitForSelector(
'input[value="https://twitter.com/thatbunty"]'
);
@@ -204,18 +204,18 @@ describe( 'Embedding content', () => {
await insertEmbed(
'https://wordpress.org/gutenberg/handbook/block-api/attributes/'
);
- await page.waitForSelector( 'figure.wp-block-embed' );
+ await canvas().waitForSelector( 'figure.wp-block-embed' );
// Video content. Should render valid figure element, and include the
// aspect ratio class.
await insertEmbed( 'https://www.youtube.com/watch?v=lXMskKTw3Bc' );
- await page.waitForSelector(
+ await canvas().waitForSelector(
'figure.wp-block-embed.is-type-video.wp-embed-aspect-16-9'
);
// Photo content. Should render valid figure element.
await insertEmbed( 'https://cloudup.com/cQFlxqtY4ob' );
- await page.waitForSelector(
+ await canvas().waitForSelector(
'iframe[title="Embedded content from cloudup"'
);
@@ -230,18 +230,21 @@ describe( 'Embedding content', () => {
// has styles applied which depend on resize observer, wait for the
// expected size class to settle before clicking, since otherwise a race
// condition could occur on the placeholder layout vs. click intent.
- await page.waitForSelector(
+ await canvas().waitForSelector(
'.components-placeholder.is-large .components-placeholder__error'
);
- await clickButton( 'Convert to link' );
+ const button = await canvas().waitForXPath(
+ `//button[contains(text(), 'Convert to link')]`
+ );
+ await button.click();
expect( await getEditedPostContent() ).toMatchSnapshot();
} );
it( 'should retry embeds that could not be embedded with trailing slashes, without the trailing slashes', async () => {
await insertEmbed( 'https://twitter.com/notnownikki/' );
// The twitter block should appear correctly.
- await page.waitForSelector( 'figure.wp-block-embed' );
+ await canvas().waitForSelector( 'figure.wp-block-embed' );
expect( await getEditedPostContent() ).toMatchSnapshot();
} );
@@ -253,7 +256,7 @@ describe( 'Embedding content', () => {
// has styles applied which depend on resize observer, wait for the
// expected size class to settle before clicking, since otherwise a race
// condition could occur on the placeholder layout vs. click intent.
- await page.waitForSelector(
+ await canvas().waitForSelector(
'.components-placeholder.is-large .components-placeholder__error'
);
@@ -268,8 +271,11 @@ describe( 'Embedding content', () => {
),
},
] );
- await clickButton( 'Try again' );
- await page.waitForSelector( 'figure.wp-block-embed' );
+ const button = await canvas().waitForXPath(
+ `//button[contains(text(), 'Try again')]`
+ );
+ await button.click();
+ await canvas().waitForSelector( 'figure.wp-block-embed' );
expect( await getEditedPostContent() ).toMatchSnapshot();
} );
@@ -292,6 +298,6 @@ describe( 'Embedding content', () => {
await insertEmbed( postUrl );
// Check the block has become a WordPress block.
- await page.waitForSelector( 'figure.wp-block-embed' );
+ await canvas().waitForSelector( 'figure.wp-block-embed' );
} );
} );
diff --git a/packages/e2e-tests/specs/editor/various/inserting-blocks.test.js b/packages/e2e-tests/specs/editor/various/inserting-blocks.test.js
index 918737e2b38997..84c251d6534685 100644
--- a/packages/e2e-tests/specs/editor/various/inserting-blocks.test.js
+++ b/packages/e2e-tests/specs/editor/various/inserting-blocks.test.js
@@ -11,6 +11,7 @@ import {
searchForBlock,
setBrowserViewport,
pressKeyWithModifier,
+ canvas,
} from '@wordpress/e2e-test-utils';
/** @typedef {import('puppeteer-core').ElementHandle} ElementHandle */
@@ -165,7 +166,7 @@ describe( 'Inserting blocks', () => {
await page.keyboard.press( 'Enter' );
expect(
- await page.waitForSelector( '[data-type="core/tag-cloud"]' )
+ await canvas().waitForSelector( '[data-type="core/tag-cloud"]' )
).not.toBeNull();
} );
@@ -175,7 +176,7 @@ describe( 'Inserting blocks', () => {
await page.keyboard.type( '1.1' );
// After inserting the Buttons block the inner button block should be selected.
- const selectedButtonBlocks = await page.$$(
+ const selectedButtonBlocks = await canvas().$$(
'.wp-block-button.is-selected'
);
expect( selectedButtonBlocks.length ).toBe( 1 );
@@ -185,7 +186,7 @@ describe( 'Inserting blocks', () => {
window.wp.data.dispatch( 'core/block-editor' ).clearSelectedBlock()
);
// Specifically click the root container appender.
- await page.click(
+ await canvas().click(
'.block-editor-block-list__layout.is-root-container > .block-list-appender .block-editor-inserter__toggle'
);
@@ -222,7 +223,7 @@ describe( 'Inserting blocks', () => {
expect( await getEditedPostContent() ).toMatchSnapshot();
// Using the between inserter.
- const insertionPoint = await page.$( '[data-type="core/heading"]' );
+ const insertionPoint = await canvas().$( '[data-type="core/heading"]' );
const rect = await insertionPoint.boundingBox();
await page.mouse.move( rect.x + rect.width / 2, rect.y - 10, {
steps: 10,
@@ -247,7 +248,7 @@ describe( 'Inserting blocks', () => {
await insertBlock( 'Paragraph' );
await page.keyboard.type( 'First paragraph' );
await insertBlock( 'Image' );
- const paragraphBlock = await page.$(
+ const paragraphBlock = await canvas().$(
'p[aria-label="Paragraph block"]'
);
paragraphBlock.click();
@@ -278,14 +279,16 @@ describe( 'Inserting blocks', () => {
it( 'inserts a block in proper place after having clicked `Browse All` from block appender', async () => {
await insertBlock( 'Group' );
// Select the default, selected Group layout from the variation picker.
- await page.click(
+ await canvas().click(
'button[aria-label="Group: Gather blocks in a container."]'
);
await insertBlock( 'Paragraph' );
await page.keyboard.type( 'Paragraph after group' );
// Click the Group first to make the appender inside it clickable.
- await page.click( '[data-type="core/group"]' );
- await page.click( '[data-type="core/group"] [aria-label="Add block"]' );
+ await canvas().click( '[data-type="core/group"]' );
+ await canvas().click(
+ '[data-type="core/group"] [aria-label="Add block"]'
+ );
const browseAll = await page.waitForXPath(
'//button[text()="Browse all"]'
);
@@ -300,14 +303,16 @@ describe( 'Inserting blocks', () => {
'.block-editor-inserter__search input,.block-editor-inserter__search-input,input.block-editor-inserter__search';
await insertBlock( 'Group' );
// Select the default, selected Group layout from the variation picker.
- await page.click(
+ await canvas().click(
'button[aria-label="Group: Gather blocks in a container."]'
);
await insertBlock( 'Paragraph' );
await page.keyboard.type( 'Text' );
// Click the Group first to make the appender inside it clickable.
- await page.click( '[data-type="core/group"]' );
- await page.click( '[data-type="core/group"] [aria-label="Add block"]' );
+ await canvas().click( '[data-type="core/group"]' );
+ await canvas().click(
+ '[data-type="core/group"] [aria-label="Add block"]'
+ );
await page.waitForSelector( INSERTER_SEARCH_SELECTOR );
await page.focus( INSERTER_SEARCH_SELECTOR );
await pressKeyWithModifier( 'primary', 'a' );
@@ -337,7 +342,7 @@ describe( 'Inserting blocks', () => {
expect( inserterPanels.length ).toBe( 0 );
// The editable 'Read More' text should be focused.
- const isFocusInBlock = await page.evaluate( () =>
+ const isFocusInBlock = await canvas().evaluate( () =>
document
.querySelector( '[data-type="core/more"]' )
.contains( document.activeElement )
@@ -366,14 +371,14 @@ describe( 'Inserting blocks', () => {
async ( viewport ) => {
await setBrowserViewport( viewport );
- await page.type(
+ await canvas().type(
'.block-editor-default-block-appender__content',
'Testing inserted block focus'
);
await insertBlock( 'Image' );
- await page.waitForSelector( 'figure[data-type="core/image"]' );
+ await canvas().waitForSelector( 'figure[data-type="core/image"]' );
const selectedBlock = await page.evaluate( () => {
return wp.data.select( 'core/block-editor' ).getSelectedBlock();
diff --git a/packages/e2e-tests/specs/editor/various/invalid-block.test.js b/packages/e2e-tests/specs/editor/various/invalid-block.test.js
index ad08ac2f4c6b44..354c370434be92 100644
--- a/packages/e2e-tests/specs/editor/various/invalid-block.test.js
+++ b/packages/e2e-tests/specs/editor/various/invalid-block.test.js
@@ -7,6 +7,7 @@ import {
clickBlockAppender,
clickBlockToolbarButton,
setPostContent,
+ canvas,
} from '@wordpress/e2e-test-utils';
describe( 'invalid blocks', () => {
@@ -25,7 +26,7 @@ describe( 'invalid blocks', () => {
await clickMenuItem( 'Edit as HTML' );
// Focus on the textarea and enter an invalid paragraph
- await page.click(
+ await canvas().click(
'.block-editor-block-list__layout .block-editor-block-list__block .block-editor-block-list__block-html-textarea'
);
await page.keyboard.type( 'invalid paragraph' );
@@ -34,7 +35,7 @@ describe( 'invalid blocks', () => {
await page.click( '.editor-post-save-draft' );
// Click on the 'three-dots' menu toggle.
- await page.click(
+ await canvas().click(
'.block-editor-warning__actions button[aria-label="More options"]'
);
@@ -75,7 +76,7 @@ describe( 'invalid blocks', () => {
expect( hasAlert ).toBe( false );
} );
- it( 'should strip potentially malicious script tags', async () => {
+ it( 'should not trigger malicious script tags when using a shortcode block', async () => {
let hasAlert = false;
page.on( 'dialog', () => {
@@ -94,9 +95,6 @@ describe( 'invalid blocks', () => {
// Give the browser time to show the alert.
await page.evaluate( () => new Promise( window.requestIdleCallback ) );
-
- expect( console ).toHaveWarned();
- expect( console ).toHaveErrored();
expect( hasAlert ).toBe( false );
} );
} );
diff --git a/packages/e2e-tests/specs/editor/various/keyboard-navigable-blocks.test.js b/packages/e2e-tests/specs/editor/various/keyboard-navigable-blocks.test.js
index b19c4cb93f3e73..b3dccdf8bf20ad 100644
--- a/packages/e2e-tests/specs/editor/various/keyboard-navigable-blocks.test.js
+++ b/packages/e2e-tests/specs/editor/various/keyboard-navigable-blocks.test.js
@@ -8,13 +8,16 @@ import {
clickBlockAppender,
getEditedPostContent,
showBlockToolbar,
+ canvas,
} from '@wordpress/e2e-test-utils';
async function getActiveLabel() {
return await page.evaluate( () => {
+ const { activeElement } =
+ document.activeElement.contentDocument ?? document;
return (
- document.activeElement.getAttribute( 'aria-label' ) ||
- document.activeElement.innerHTML
+ activeElement.getAttribute( 'aria-label' ) ||
+ activeElement.innerHTML
);
} );
}
@@ -34,7 +37,11 @@ const tabThroughParagraphBlock = async ( paragraphText ) => {
await page.keyboard.press( 'Tab' );
await expect( await getActiveLabel() ).toBe( 'Paragraph block' );
await expect(
- await page.evaluate( () => document.activeElement.innerHTML )
+ await page.evaluate( () => {
+ const { activeElement } =
+ document.activeElement.contentDocument ?? document;
+ return activeElement.innerHTML;
+ } )
).toBe( paragraphText );
await page.keyboard.press( 'Tab' );
@@ -113,16 +120,12 @@ describe( 'Order of block keyboard navigation', () => {
}
// Clear the selected block.
- const paragraph = await page.$( '[data-type="core/paragraph"]' );
+ const paragraph = await canvas().$( '[data-type="core/paragraph"]' );
const box = await paragraph.boundingBox();
await page.mouse.click( box.x - 1, box.y );
await page.keyboard.press( 'Tab' );
- await expect(
- await page.evaluate( () => {
- return document.activeElement.getAttribute( 'aria-label' );
- } )
- ).toBe( 'Add title' );
+ await expect( await getActiveLabel() ).toBe( 'Add title' );
await page.keyboard.press( 'Tab' );
await expect( await getActiveLabel() ).toBe(
@@ -148,7 +151,7 @@ describe( 'Order of block keyboard navigation', () => {
}
// Clear the selected block.
- const paragraph = await page.$( '[data-type="core/paragraph"]' );
+ const paragraph = await canvas().$( '[data-type="core/paragraph"]' );
const box = await paragraph.boundingBox();
await page.mouse.click( box.x - 1, box.y );
@@ -176,11 +179,7 @@ describe( 'Order of block keyboard navigation', () => {
);
await pressKeyWithModifier( 'shift', 'Tab' );
- await expect(
- await page.evaluate( () => {
- return document.activeElement.getAttribute( 'aria-label' );
- } )
- ).toBe( 'Add title' );
+ await expect( await getActiveLabel() ).toBe( 'Add title' );
} );
it( 'should navigate correctly with multi selection', async () => {
@@ -217,7 +216,7 @@ describe( 'Order of block keyboard navigation', () => {
await insertBlock( 'Image' );
// Make sure the upload button has focus.
- const uploadButton = await page.waitForXPath(
+ const uploadButton = await canvas().waitForXPath(
'//button[contains( text(), "Upload" ) ]'
);
await expect( uploadButton ).toHaveFocus();
@@ -231,7 +230,7 @@ describe( 'Order of block keyboard navigation', () => {
// Insert a group block.
await insertBlock( 'Group' );
// Select the default, selected Group layout from the variation picker.
- await page.click(
+ await canvas().click(
'button[aria-label="Group: Gather blocks in a container."]'
);
// If active label matches, that means focus did not change from group block wrapper.
diff --git a/packages/e2e-tests/specs/editor/various/links.test.js b/packages/e2e-tests/specs/editor/various/links.test.js
index 9c3e8a722a7f0e..d902e35fc09ebd 100644
--- a/packages/e2e-tests/specs/editor/various/links.test.js
+++ b/packages/e2e-tests/specs/editor/various/links.test.js
@@ -9,6 +9,7 @@ import {
pressKeyWithModifier,
showBlockToolbar,
pressKeyTimes,
+ canvas,
} from '@wordpress/e2e-test-utils';
describe( 'Links', () => {
@@ -17,9 +18,16 @@ describe( 'Links', () => {
} );
const waitForURLFieldAutoFocus = async () => {
- await page.waitForFunction(
- () => !! document.activeElement.closest( '.block-editor-url-input' )
- );
+ await page.waitForFunction( () => {
+ const input = document.querySelector(
+ '.block-editor-url-input__input'
+ );
+ if ( input ) {
+ input.focus();
+ return true;
+ }
+ return false;
+ } );
};
it( 'will use Post title as link text if link to existing post is created without any text selected', async () => {
@@ -50,7 +58,7 @@ describe( 'Links', () => {
await page.keyboard.press( 'Enter' );
- const actualText = await page.evaluate(
+ const actualText = await canvas().evaluate(
() =>
document.querySelector( '.block-editor-rich-text__editable a' )
.textContent
@@ -103,48 +111,6 @@ describe( 'Links', () => {
expect( urlInputValue ).toBe( '' );
} );
- it( 'can be created by selecting text and using keyboard shortcuts', async () => {
- // Create a block with some text.
- await clickBlockAppender();
- await page.keyboard.type( 'This is Gutenberg' );
-
- // Select some text.
- await pressKeyWithModifier( 'shiftAlt', 'ArrowLeft' );
-
- // Press Cmd+K to insert a link.
- await pressKeyWithModifier( 'primary', 'K' );
-
- // Wait for the URL field to auto-focus.
- await waitForURLFieldAutoFocus();
-
- // Type a URL.
- await page.keyboard.type( 'https://wordpress.org/gutenberg' );
-
- // Open settings.
- await page.keyboard.press( 'Tab' );
- await page.keyboard.press( 'Space' );
-
- // Navigate to and toggle the "Open in new tab" checkbox.
- await page.keyboard.press( 'Tab' );
- await page.keyboard.press( 'Space' );
-
- // Toggle should still have focus and be checked.
- await page.waitForSelector(
- ':focus:checked.components-form-toggle__input'
- );
-
- // Ensure that the contents of the post have not been changed, since at
- // this point the link is still not inserted.
- expect( await getEditedPostContent() ).toMatchSnapshot();
-
- // Tab back to the Submit and apply the link.
- await page.keyboard.press( 'Tab' );
- await page.keyboard.press( 'Enter' );
-
- // The link should have been inserted.
- expect( await getEditedPostContent() ).toMatchSnapshot();
- } );
-
it( 'can be created without any text selected', async () => {
// Create a block with some text.
await clickBlockAppender();
@@ -207,7 +173,7 @@ describe( 'Links', () => {
await page.keyboard.type( 'https://wordpress.org/gutenberg' );
// Click somewhere else - it doesn't really matter where.
- await page.click( '.editor-post-title' );
+ await canvas().click( '.editor-post-title' );
} );
const createAndReselectLink = async () => {
@@ -346,7 +312,7 @@ describe( 'Links', () => {
const createPostWithTitle = async ( titleText ) => {
await createNewPost();
- await page.type( '.editor-post-title__input', titleText );
+ await canvas().type( '.editor-post-title__input', titleText );
await page.click( '.editor-post-publish-panel__toggle' );
// Disable reason: Wait for the animation to complete, since otherwise the
@@ -520,81 +486,6 @@ describe( 'Links', () => {
);
} );
- it( 'should contain a label when it should open in a new tab', async () => {
- await clickBlockAppender();
- await page.keyboard.type( 'This is WordPress' );
- // Select "WordPress".
- await pressKeyWithModifier( 'shiftAlt', 'ArrowLeft' );
- await pressKeyWithModifier( 'primary', 'k' );
- await waitForURLFieldAutoFocus();
- await page.keyboard.type( 'w.org' );
-
- // Link settings open
- await page.keyboard.press( 'Tab' );
- await page.keyboard.press( 'Space' );
-
- // Navigate to and toggle the "Open in new tab" checkbox.
- await page.keyboard.press( 'Tab' );
- await page.keyboard.press( 'Space' );
-
- // Confirm that focus was not prematurely returned to the paragraph on
- // a changing value of the setting.
- await page.waitForSelector( ':focus.components-form-toggle__input' );
-
- // Submit link. Expect that "Open in new tab" would have been applied
- // immediately.
- await page.keyboard.press( 'Tab' );
- await page.keyboard.press( 'Enter' );
-
- // Wait for Gutenberg to finish the job.
- await page.waitForXPath(
- '//a[contains(@href,"w.org") and @target="_blank"]'
- );
-
- 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
-
- // Move caret back into the link.
- await page.keyboard.press( 'ArrowLeft' );
- await page.keyboard.press( 'ArrowLeft' );
-
- // Edit link.
- await pressKeyWithModifier( 'primary', 'k' );
- await waitForURLFieldAutoFocus();
- await pressKeyWithModifier( 'primary', 'a' );
- await page.keyboard.type( 'wordpress.org' );
-
- // Update the link.
- await page.keyboard.press( 'Enter' );
-
- // Navigate back to the popover.
- await page.keyboard.press( 'ArrowLeft' );
- await page.keyboard.press( 'ArrowLeft' );
-
- // Navigate back to inputs to verify appears as changed.
- await pressKeyWithModifier( 'primary', 'k' );
- await waitForURLFieldAutoFocus();
-
- // Navigate to the "Open in new tab" checkbox.
- await page.keyboard.press( 'Tab' );
- await page.keyboard.press( 'Tab' );
- // Uncheck the checkbox.
- await page.keyboard.press( 'Space' );
-
- // Wait for Gutenberg to finish the job.
- await page.waitForXPath(
- '//a[contains(@href,"wordpress.org") and not(@target)]'
- );
-
- expect( await getEditedPostContent() ).toMatchSnapshot();
- } );
-
describe( 'Editing link text', () => {
it( 'should not display text input when initially creating the link', async () => {
// Create a block with some text.
@@ -637,11 +528,6 @@ describe( 'Links', () => {
await waitForURLFieldAutoFocus();
- const [ settingsToggle ] = await page.$x(
- '//button[contains(@aria-label, "Link Settings")]'
- );
- await settingsToggle.click();
-
await page.keyboard.press( 'Tab' );
// Tabbing should land us in the text input.
@@ -699,15 +585,6 @@ describe( 'Links', () => {
await editButton.click();
- await waitForURLFieldAutoFocus();
-
- const [ settingsToggle ] = await page.$x(
- '//button[contains(@aria-label, "Link Settings")]'
- );
- await settingsToggle.click();
-
- await page.keyboard.press( 'Tab' );
-
// Tabbing back should land us in the text input.
const textInputValue = await page.evaluate(
() => document.activeElement.value
@@ -734,14 +611,6 @@ describe( 'Links', () => {
'//button[contains(@aria-label, "Edit")]'
);
await editButton.click();
- await waitForURLFieldAutoFocus();
-
- const [ settingsToggle ] = await page.$x(
- '//button[contains(@aria-label, "Link Settings")]'
- );
- await settingsToggle.click();
-
- await page.keyboard.press( 'Tab' );
// Tabbing should land us in the text input.
const textInputValue = await page.evaluate(
@@ -762,7 +631,7 @@ describe( 'Links', () => {
await page.keyboard.press( 'Enter' );
// Check the created link reflects the link text.
- const actualLinkText = await page.evaluate(
+ const actualLinkText = await canvas().evaluate(
() =>
document.querySelector(
'.block-editor-rich-text__editable a'
@@ -792,8 +661,11 @@ describe( 'Links', () => {
);
await settingsToggle.click();
+ // Wait for settings to open.
+ await page.waitForXPath( `//label[text()='Open in new tab']` );
+
// Move focus back to RichText for the underlying link.
- await pressKeyTimes( 'Tab', 5 );
+ await pressKeyTimes( 'Tab', 4 );
// Make a selection within the RichText.
await pressKeyWithModifier( 'shift', 'ArrowRight' );
@@ -801,7 +673,7 @@ describe( 'Links', () => {
await pressKeyWithModifier( 'shift', 'ArrowRight' );
// Move back to the text input.
- await pressKeyTimes( 'Tab', 3 );
+ await pressKeyTimes( 'Tab', 2 );
// Tabbing back should land us in the text input.
const textInputValue = await page.evaluate(
@@ -1001,10 +873,6 @@ describe( 'Links', () => {
await waitForURLFieldAutoFocus();
- // Link settings open
- await page.keyboard.press( 'Tab' );
- await page.keyboard.press( 'Space' );
-
// Move to Link Text field.
await page.keyboard.press( 'Tab' );
@@ -1013,7 +881,7 @@ describe( 'Links', () => {
await page.keyboard.press( 'Enter' );
- const richTextText = await page.evaluate(
+ const richTextText = await canvas().evaluate(
() =>
document.querySelector(
'.block-editor-rich-text__editable'
@@ -1022,7 +890,7 @@ describe( 'Links', () => {
// Check that the correct (i.e. last) instance of "a" was replaced with "z".
expect( richTextText ).toBe( 'a b c z' );
- const richTextLink = await page.evaluate(
+ const richTextLink = await canvas().evaluate(
() =>
document.querySelector(
'.block-editor-rich-text__editable a'
diff --git a/packages/e2e-tests/specs/editor/various/navigable-toolbar.test.js b/packages/e2e-tests/specs/editor/various/navigable-toolbar.test.js
index 8fb2edf4f6768d..c2735f8fbf75c9 100644
--- a/packages/e2e-tests/specs/editor/various/navigable-toolbar.test.js
+++ b/packages/e2e-tests/specs/editor/various/navigable-toolbar.test.js
@@ -20,9 +20,10 @@ describe( 'Block Toolbar', () => {
it( 'should not scroll page', async () => {
while (
await page.evaluate( () => {
- const scrollable = wp.dom.getScrollContainer(
- document.activeElement
- );
+ const { activeElement } =
+ document.activeElement?.contentDocument ?? document;
+ const scrollable =
+ wp.dom.getScrollContainer( activeElement );
return ! scrollable || scrollable.scrollTop === 0;
} )
) {
@@ -31,21 +32,20 @@ describe( 'Block Toolbar', () => {
await page.keyboard.type( 'a' );
- const scrollTopBefore = await page.evaluate(
- () =>
- wp.dom.getScrollContainer( document.activeElement )
- .scrollTop
- );
+ const scrollTopBefore = await page.evaluate( () => {
+ const { activeElement } =
+ document.activeElement?.contentDocument ?? document;
+ window.scrollContainer =
+ wp.dom.getScrollContainer( activeElement );
+ return window.scrollContainer.scrollTop;
+ } );
await pressKeyWithModifier( 'alt', 'F10' );
expect( await isInBlockToolbar() ).toBe( true );
- const scrollTopAfter = await page.evaluate(
- () =>
- wp.dom.getScrollContainer( document.activeElement )
- .scrollTop
- );
-
+ const scrollTopAfter = await page.evaluate( () => {
+ return window.scrollContainer.scrollTop;
+ } );
expect( scrollTopBefore ).toBe( scrollTopAfter );
} );
diff --git a/packages/e2e-tests/specs/editor/various/nux.test.js b/packages/e2e-tests/specs/editor/various/nux.test.js
index 1edc92e9e85752..8ea151686d2eb0 100644
--- a/packages/e2e-tests/specs/editor/various/nux.test.js
+++ b/packages/e2e-tests/specs/editor/various/nux.test.js
@@ -1,7 +1,11 @@
/**
* WordPress dependencies
*/
-import { createNewPost, clickOnMoreMenuItem } from '@wordpress/e2e-test-utils';
+import {
+ createNewPost,
+ clickOnMoreMenuItem,
+ canvas,
+} from '@wordpress/e2e-test-utils';
describe( 'New User Experience (NUX)', () => {
it( 'should show the guide to first-time users', async () => {
@@ -128,7 +132,7 @@ describe( 'New User Experience (NUX)', () => {
await page.click( '[role="dialog"] button[aria-label="Close"]' );
// Focus should be in post title field.
- const postTitle = await page.waitForSelector(
+ const postTitle = await canvas().waitForSelector(
'h1[aria-label="Add title"'
);
await expect( postTitle ).toHaveFocus();
diff --git a/packages/e2e-tests/specs/editor/various/publish-button.test.js b/packages/e2e-tests/specs/editor/various/publish-button.test.js
index 2db6608331cb33..b6461ef11bc5b4 100644
--- a/packages/e2e-tests/specs/editor/various/publish-button.test.js
+++ b/packages/e2e-tests/specs/editor/various/publish-button.test.js
@@ -6,6 +6,7 @@ import {
disablePrePublishChecks,
enablePrePublishChecks,
createNewPost,
+ canvas,
} from '@wordpress/e2e-test-utils';
describe( 'PostPublishButton', () => {
@@ -32,7 +33,7 @@ describe( 'PostPublishButton', () => {
} );
it( 'should be disabled when post is being saved', async () => {
- await page.type( '.editor-post-title__input', 'E2E Test Post' ); // Make it saveable.
+ await canvas().type( '.editor-post-title__input', 'E2E Test Post' ); // Make it saveable.
expect(
await page.$( '.editor-post-publish-button[aria-disabled="true"]' )
).toBeNull();
@@ -44,7 +45,7 @@ describe( 'PostPublishButton', () => {
} );
it( 'should be disabled when metabox is being saved', async () => {
- await page.type( '.editor-post-title__input', 'E2E Test Post' ); // Make it saveable.
+ await canvas().type( '.editor-post-title__input', 'E2E Test Post' ); // Make it saveable.
expect(
await page.$( '.editor-post-publish-button[aria-disabled="true"]' )
).toBeNull();
diff --git a/packages/e2e-tests/specs/editor/various/publish-panel.test.js b/packages/e2e-tests/specs/editor/various/publish-panel.test.js
index 3a6aefd8f66870..333f2f1c2a8b3b 100644
--- a/packages/e2e-tests/specs/editor/various/publish-panel.test.js
+++ b/packages/e2e-tests/specs/editor/various/publish-panel.test.js
@@ -9,6 +9,7 @@ import {
openPublishPanel,
pressKeyWithModifier,
publishPost,
+ canvas,
} from '@wordpress/e2e-test-utils';
describe( 'PostPublishPanel', () => {
@@ -28,7 +29,7 @@ describe( 'PostPublishPanel', () => {
} );
it( 'PrePublish: publish button should have the focus', async () => {
- await page.type( '.editor-post-title__input', 'E2E Test Post' );
+ await canvas().type( '.editor-post-title__input', 'E2E Test Post' );
await openPublishPanel();
const focusedElementClassList = await page.$eval(
@@ -44,7 +45,7 @@ describe( 'PostPublishPanel', () => {
it( 'PostPublish: post link should have the focus', async () => {
const postTitle = 'E2E Test Post';
- await page.type( '.editor-post-title__input', postTitle );
+ await canvas().type( '.editor-post-title__input', postTitle );
await publishPost();
const focusedElementTag = await page.$eval(
@@ -64,7 +65,7 @@ describe( 'PostPublishPanel', () => {
} );
it( 'should retain focus within the panel', async () => {
- await page.type( '.editor-post-title__input', 'E2E Test Post' );
+ await canvas().type( '.editor-post-title__input', 'E2E Test Post' );
await openPublishPanel();
await pressKeyWithModifier( 'shift', 'Tab' );
diff --git a/packages/e2e-tests/specs/editor/various/publishing.test.js b/packages/e2e-tests/specs/editor/various/publishing.test.js
index 88c0d2b993db3f..fbac8cf98638bb 100644
--- a/packages/e2e-tests/specs/editor/various/publishing.test.js
+++ b/packages/e2e-tests/specs/editor/various/publishing.test.js
@@ -11,6 +11,7 @@ import {
setBrowserViewport,
openPublishPanel,
pressKeyWithModifier,
+ canvas,
} from '@wordpress/e2e-test-utils';
describe( 'Publishing', () => {
@@ -22,7 +23,7 @@ describe( 'Publishing', () => {
} );
it( `disables the publish button when a ${ postType } is locked`, async () => {
- await page.type(
+ await canvas().type(
'.editor-post-title__input',
'E2E Test Post lock check publish button'
);
@@ -42,7 +43,7 @@ describe( 'Publishing', () => {
} );
it( `disables the save shortcut when a ${ postType } is locked`, async () => {
- await page.type(
+ await canvas().type(
'.editor-post-title__input',
'E2E Test Post check save shortcut'
);
@@ -79,7 +80,7 @@ describe( 'Publishing', () => {
} );
it( `should publish the ${ postType } and close the panel once we start editing again.`, async () => {
- await page.type( '.editor-post-title__input', 'E2E Test Post' );
+ await canvas().type( '.editor-post-title__input', 'E2E Test Post' );
await publishPost();
@@ -89,7 +90,7 @@ describe( 'Publishing', () => {
).not.toBeNull();
// Start editing again.
- await page.type( '.editor-post-title__input', ' (Updated)' );
+ await canvas().type( '.editor-post-title__input', ' (Updated)' );
// The post-publishing panel is not visible anymore.
expect( await page.$( '.editor-post-publish-panel' ) ).toBeNull();
@@ -117,7 +118,10 @@ describe( 'Publishing', () => {
} );
it( `should publish the ${ postType } without opening the post-publish sidebar.`, async () => {
- await page.type( '.editor-post-title__input', 'E2E Test Post' );
+ await canvas().type(
+ '.editor-post-title__input',
+ 'E2E Test Post'
+ );
// The "Publish" button should be shown instead of the "Publish..." toggle.
expect(
diff --git a/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js b/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js
index a94cb1f7214267..3215e4185c08fa 100644
--- a/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js
+++ b/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js
@@ -16,6 +16,7 @@ import {
saveDraft,
createReusableBlock,
publishPost,
+ canvas,
} from '@wordpress/e2e-test-utils';
const reusableBlockNameInputSelector =
@@ -83,7 +84,7 @@ describe( 'Reusable blocks', () => {
await page.keyboard.type( 'Surprised greeting block' );
// Quickly focus the paragraph block.
- await page.click(
+ await canvas().click(
'.block-editor-block-list__block[data-type="core/block"] p'
);
await page.keyboard.press( 'Escape' ); // Enter navigation mode.
@@ -96,7 +97,7 @@ describe( 'Reusable blocks', () => {
await saveAllButDontPublish();
// Check that its content is up to date.
- const text = await page.$eval(
+ const text = await canvas().$eval(
'.block-editor-block-list__block[data-type="core/block"] p',
( element ) => element.innerText
);
@@ -111,13 +112,13 @@ describe( 'Reusable blocks', () => {
await clickBlockToolbarButton( 'Convert to regular block' );
// Check that we have a paragraph block on the page.
- const paragraphBlock = await page.$(
+ const paragraphBlock = await canvas().$(
'.block-editor-block-list__block[data-type="core/paragraph"]'
);
expect( paragraphBlock ).not.toBeNull();
// Check that its content is up to date.
- const paragraphContent = await page.$eval(
+ const paragraphContent = await canvas().$eval(
'.block-editor-block-list__block[data-type="core/paragraph"]',
( element ) => element.innerText
);
@@ -132,7 +133,7 @@ describe( 'Reusable blocks', () => {
);
// Make sure the reusable block has loaded properly before attempting to publish the post.
- await page.waitForSelector( 'p[aria-label="Paragraph block"]' );
+ await canvas().waitForSelector( 'p[aria-label="Paragraph block"]' );
await publishPost();
@@ -142,8 +143,8 @@ describe( 'Reusable blocks', () => {
await page.waitForSelector( closePublishPanelSelector );
await page.click( closePublishPanelSelector );
- await page.waitForSelector( 'p[aria-label="Paragraph block"]' );
- await page.focus( 'p[aria-label="Paragraph block"]' );
+ await canvas().waitForSelector( 'p[aria-label="Paragraph block"]' );
+ await canvas().focus( 'p[aria-label="Paragraph block"]' );
// Change the block's content.
await page.keyboard.type( 'Einen ' );
@@ -152,7 +153,7 @@ describe( 'Reusable blocks', () => {
await saveAll();
// Check that its content is up to date.
- const paragraphContent = await page.$eval(
+ const paragraphContent = await canvas().$eval(
'p[aria-label="Paragraph block"]',
( element ) => element.innerText
);
@@ -235,11 +236,12 @@ describe( 'Reusable blocks', () => {
await editButton.click();
await page.waitForNavigation();
+ await page.waitForSelector( 'iframe[name="editor-canvas"]' );
// Click the block to give it focus.
const blockSelector = 'p[data-title="Paragraph"]';
- await page.waitForSelector( blockSelector );
- await page.click( blockSelector );
+ await canvas().waitForSelector( blockSelector );
+ await canvas().click( blockSelector );
// Delete the block, leaving the reusable block empty.
await clickBlockToolbarButton( 'Options' );
@@ -277,7 +279,7 @@ describe( 'Reusable blocks', () => {
] );
} );
- await page.waitForXPath(
+ await canvas().waitForXPath(
'//*[contains(@class, "block-editor-warning")]/*[text()="Block has been deleted or is unavailable."]'
);
@@ -295,15 +297,16 @@ describe( 'Reusable blocks', () => {
await insertReusableBlock( 'Duplicated reusable block' );
await saveDraft();
await page.reload();
+ await page.waitForSelector( 'iframe[name="editor-canvas"]' );
// Wait for the paragraph to be loaded.
- await page.waitForSelector(
+ await canvas().waitForSelector(
'.block-editor-block-list__block[data-type="core/paragraph"]'
);
// The first click selects the reusable block wrapper.
// The second click selects the actual paragraph block.
- await page.click( '.wp-block-block' );
- await page.focus(
+ await canvas().click( '.wp-block-block' );
+ await canvas().focus(
'.block-editor-block-list__block[data-type="core/paragraph"]'
);
await pressKeyWithModifier( 'primary', 'a' );
@@ -333,8 +336,8 @@ describe( 'Reusable blocks', () => {
// Make an edit to the reusable block and assert that there's only a
// paragraph in a reusable block.
- await page.waitForSelector( 'p[aria-label="Paragraph block"]' );
- await page.click( 'p[aria-label="Paragraph block"]' );
+ await canvas().waitForSelector( 'p[aria-label="Paragraph block"]' );
+ await canvas().click( 'p[aria-label="Paragraph block"]' );
await page.keyboard.type( '2' );
const selector =
'//div[@aria-label="Block: Reusable block"]//p[@aria-label="Paragraph block"][.="12"]';
@@ -358,9 +361,10 @@ describe( 'Reusable blocks', () => {
insertBlock( 'Quote' );
await saveDraft();
await page.reload();
+ await page.waitForSelector( 'iframe[name="editor-canvas"]' );
// The quote block should have a visible preview in the sidebar for this test to be valid.
- const quoteBlock = await page.waitForSelector(
+ const quoteBlock = await canvas().waitForSelector(
'.block-editor-block-list__block[aria-label="Block: Quote"]'
);
// Select the quote block.
@@ -379,7 +383,7 @@ describe( 'Reusable blocks', () => {
await nameInput.click();
await page.keyboard.type( 'Block with styles' );
await page.keyboard.press( 'Enter' );
- const reusableBlock = await page.waitForSelector(
+ const reusableBlock = await canvas().waitForSelector(
'.block-editor-block-list__block[aria-label="Block: Reusable block"]'
);
expect( reusableBlock ).toBeTruthy();
diff --git a/packages/e2e-tests/specs/editor/various/rich-text.test.js b/packages/e2e-tests/specs/editor/various/rich-text.test.js
index ca7eac55471b1a..c2098098d9cc73 100644
--- a/packages/e2e-tests/specs/editor/various/rich-text.test.js
+++ b/packages/e2e-tests/specs/editor/various/rich-text.test.js
@@ -9,6 +9,7 @@ import {
pressKeyWithModifier,
showBlockToolbar,
clickBlockToolbarButton,
+ canvas,
} from '@wordpress/e2e-test-utils';
describe( 'RichText', () => {
@@ -74,7 +75,7 @@ describe( 'RichText', () => {
await pressKeyWithModifier( 'shift', 'ArrowLeft' );
await pressKeyWithModifier( 'primary', 'b' );
- const count = await page.evaluate(
+ const count = await canvas().evaluate(
() =>
document.querySelectorAll( '*[data-rich-text-format-boundary]' )
.length
@@ -173,7 +174,7 @@ describe( 'RichText', () => {
await pressKeyWithModifier( 'primary', 'b' );
await page.keyboard.type( '3' );
- await page.evaluate( () => {
+ await canvas().evaluate( () => {
let called;
const { body } = document;
const config = {
@@ -233,7 +234,7 @@ describe( 'RichText', () => {
await page.keyboard.type( '4' );
- await page.evaluate( () => {
+ await canvas().evaluate( () => {
// The selection change event should be called once. If there's only
// one item in `window.unsubscribes`, it means that only one
// function is present to disconnect the `mutationObserver`.
@@ -274,7 +275,7 @@ describe( 'RichText', () => {
await page.keyboard.press( 'Enter' );
// Wait for rich text editor to load.
- await page.waitForSelector( '.block-editor-rich-text__editable' );
+ await canvas().waitForSelector( '.block-editor-rich-text__editable' );
await pressKeyWithModifier( 'primary', 'b' );
await page.keyboard.type( '12' );
@@ -305,7 +306,7 @@ describe( 'RichText', () => {
await page.keyboard.type( '1' );
// Simulate moving focus to a different app, then moving focus back,
// without selection being changed.
- await page.evaluate( () => {
+ await canvas().evaluate( () => {
const activeElement = document.activeElement;
activeElement.blur();
activeElement.focus();
@@ -515,7 +516,7 @@ describe( 'RichText', () => {
// text in the DOM directly, setting selection in the right place, and
// firing `compositionend`.
// See https://github.com/puppeteer/puppeteer/issues/4981.
- await page.evaluate( async () => {
+ await canvas().evaluate( async () => {
document.activeElement.textContent = '`a`';
const selection = window.getSelection();
// The `selectionchange` and `compositionend` events should run in separate event
diff --git a/packages/e2e-tests/specs/editor/various/sidebar-permalink.test.js b/packages/e2e-tests/specs/editor/various/sidebar-permalink.test.js
index a89ac0469f27ae..e23bd830cee4fe 100644
--- a/packages/e2e-tests/specs/editor/various/sidebar-permalink.test.js
+++ b/packages/e2e-tests/specs/editor/various/sidebar-permalink.test.js
@@ -6,6 +6,7 @@ import {
createNewPost,
deactivatePlugin,
publishPost,
+ canvas,
} from '@wordpress/e2e-test-utils';
const urlButtonSelector = '*[aria-label^="Change URL"]';
@@ -28,7 +29,7 @@ describe( 'Sidebar Permalink', () => {
await page.keyboard.type( 'aaaaa' );
await publishPost();
// Start editing again.
- await page.type( '.editor-post-title__input', ' (Updated)' );
+ await canvas().type( '.editor-post-title__input', ' (Updated)' );
expect( await page.$( urlButtonSelector ) ).toBeNull();
} );
@@ -37,7 +38,7 @@ describe( 'Sidebar Permalink', () => {
await page.keyboard.type( 'aaaaa' );
await publishPost();
// Start editing again.
- await page.type( '.editor-post-title__input', ' (Updated)' );
+ await canvas().type( '.editor-post-title__input', ' (Updated)' );
expect( await page.$( urlButtonSelector ) ).toBeNull();
} );
@@ -46,7 +47,7 @@ describe( 'Sidebar Permalink', () => {
await page.keyboard.type( 'aaaaa' );
await publishPost();
// Start editing again.
- await page.type( '.editor-post-title__input', ' (Updated)' );
+ await canvas( 0 ).type( '.editor-post-title__input', ' (Updated)' );
expect( await page.$( urlButtonSelector ) ).not.toBeNull();
} );
} );
diff --git a/packages/e2e-tests/specs/editor/various/taxonomies.test.js b/packages/e2e-tests/specs/editor/various/taxonomies.test.js
index 551187d654dd93..a1804307fe74e5 100644
--- a/packages/e2e-tests/specs/editor/various/taxonomies.test.js
+++ b/packages/e2e-tests/specs/editor/various/taxonomies.test.js
@@ -6,6 +6,7 @@ import {
findSidebarPanelWithTitle,
openDocumentSettingsSidebar,
publishPost,
+ canvas,
} from '@wordpress/e2e-test-utils';
/**
@@ -113,7 +114,7 @@ describe( 'Taxonomies', () => {
expect( selectedCategories[ 0 ] ).toEqual( 'z rand category 1' );
// Type something in the title so we can publish the post.
- await page.type( '.editor-post-title__input', 'Hello World' );
+ await canvas().type( '.editor-post-title__input', 'Hello World' );
// Publish the post.
await publishPost();
@@ -171,7 +172,7 @@ describe( 'Taxonomies', () => {
expect( tags[ 0 ] ).toEqual( tagName );
// Type something in the title so we can publish the post.
- await page.type( '.editor-post-title__input', 'Hello World' );
+ await canvas().type( '.editor-post-title__input', 'Hello World' );
// Publish the post.
await publishPost();
@@ -230,7 +231,7 @@ describe( 'Taxonomies', () => {
expect( tags[ 0 ] ).toEqual( tagName );
// Type something in the title so we can publish the post.
- await page.type( '.editor-post-title__input', 'Hello World' );
+ await canvas().type( '.editor-post-title__input', 'Hello World' );
// Publish the post.
await publishPost();
diff --git a/packages/e2e-tests/specs/editor/various/typewriter.test.js b/packages/e2e-tests/specs/editor/various/typewriter.test.js
index f1eb0c7b689345..d935197b14f87f 100644
--- a/packages/e2e-tests/specs/editor/various/typewriter.test.js
+++ b/packages/e2e-tests/specs/editor/various/typewriter.test.js
@@ -9,7 +9,12 @@ describe( 'TypeWriter', () => {
} );
const getCaretPosition = async () =>
- await page.evaluate( () => wp.dom.computeCaretRect( window ).y );
+ await page.evaluate(
+ () =>
+ wp.dom.computeCaretRect(
+ document.activeElement?.contentWindow ?? window
+ ).y
+ );
// Allow the scroll position to be 1px off.
const BUFFER = 1;
@@ -33,11 +38,13 @@ describe( 'TypeWriter', () => {
// Create blocks until the typewriter effect kicks in.
while (
- await page.evaluate(
- () =>
- wp.dom.getScrollContainer( document.activeElement )
- .scrollTop === 0
- )
+ await page.evaluate( () => {
+ const { activeElement } =
+ document.activeElement?.contentDocument ?? document;
+ return (
+ wp.dom.getScrollContainer( activeElement ).scrollTop === 0
+ );
+ } )
) {
await page.keyboard.press( 'Enter' );
}
@@ -51,14 +58,14 @@ describe( 'TypeWriter', () => {
// Type until the text wraps.
while (
- await page.evaluate(
- () =>
- document.activeElement.clientHeight <=
- parseInt(
- getComputedStyle( document.activeElement ).lineHeight,
- 10
- )
- )
+ await page.evaluate( () => {
+ const { activeElement } =
+ document.activeElement?.contentDocument ?? document;
+ return (
+ activeElement.clientHeight <=
+ parseInt( getComputedStyle( activeElement ).lineHeight, 10 )
+ );
+ } )
) {
await page.keyboard.type( 'a' );
}
@@ -93,32 +100,35 @@ describe( 'TypeWriter', () => {
// Create zero or more blocks until there is a scrollable container.
// No blocks should be created if there's already a scrollbar.
while (
- await page.evaluate(
- () => ! wp.dom.getScrollContainer( document.activeElement )
- )
+ await page.evaluate( () => {
+ const { activeElement } =
+ document.activeElement?.contentDocument ?? document;
+ const scrollContainer =
+ wp.dom.getScrollContainer( activeElement );
+ return (
+ scrollContainer.scrollHeight ===
+ scrollContainer.clientHeight
+ );
+ } )
) {
await page.keyboard.press( 'Enter' );
}
- const scrollPosition = await page.evaluate(
- () => wp.dom.getScrollContainer( document.activeElement ).scrollTop
- );
+ const scrollPosition = await page.evaluate( () => {
+ const { activeElement } =
+ document.activeElement?.contentDocument ?? document;
+ return wp.dom.getScrollContainer( activeElement ).scrollTop;
+ } );
// Expect scrollbar to be at the top.
expect( scrollPosition ).toBe( 0 );
// Move the mouse to the scroll container, and scroll down
// a small amount to trigger the typewriter mode.
- const mouseMovePosition = await page.evaluate( () => {
- const caretRect = wp.dom.computeCaretRect( window );
- return [ Math.floor( caretRect.x ), Math.floor( caretRect.y ) ];
+ await page.evaluate( () => {
+ const { activeElement } =
+ document.activeElement?.contentDocument ?? document;
+ wp.dom.getScrollContainer( activeElement ).scrollTop += 2;
} );
- await page.mouse.move( ...mouseMovePosition );
- await page.mouse.wheel( { deltaY: 2 } );
- await page.waitForFunction(
- () =>
- wp.dom.getScrollContainer( document.activeElement )
- .scrollTop === 2
- );
// Wait for the caret rectangle to be recalculated.
await page.evaluate(
() => new Promise( window.requestAnimationFrame )
@@ -128,12 +138,12 @@ describe( 'TypeWriter', () => {
// coordinates should be the same.
const initialPosition = await getCaretPosition();
await page.keyboard.press( 'Enter' );
- await page.waitForFunction(
- () =>
- // Wait for the Typewriter to scroll down past the initial position.
- wp.dom.getScrollContainer( document.activeElement ).scrollTop >
- 2
- );
+ await page.waitForFunction( () => {
+ const { activeElement } =
+ document.activeElement?.contentDocument ?? document;
+ // Wait for the Typewriter to scroll down past the initial position.
+ return wp.dom.getScrollContainer( activeElement ).scrollTop > 2;
+ } );
expect( await getDiff( initialPosition ) ).toBe( 0 );
} );
@@ -164,9 +174,11 @@ describe( 'TypeWriter', () => {
// Create blocks until there is a scrollable container.
while (
- await page.evaluate(
- () => ! wp.dom.getScrollContainer( document.activeElement )
- )
+ await page.evaluate( () => {
+ const { activeElement } =
+ document.activeElement?.contentDocument ?? document;
+ return ! wp.dom.getScrollContainer( activeElement );
+ } )
) {
await page.keyboard.press( 'Enter' );
}
@@ -176,11 +188,13 @@ describe( 'TypeWriter', () => {
// Create blocks until the typewriter effect kicks in, create at
// least 10 blocks to properly test the .
while (
- ( await page.evaluate(
- () =>
- wp.dom.getScrollContainer( document.activeElement )
- .scrollTop === 0
- ) ) ||
+ ( await page.evaluate( () => {
+ const { activeElement } =
+ document.activeElement?.contentDocument ?? document;
+ return (
+ wp.dom.getScrollContainer( activeElement ).scrollTop === 0
+ );
+ } ) ) ||
count < 10
) {
await page.keyboard.press( 'Enter' );
@@ -190,9 +204,11 @@ describe( 'TypeWriter', () => {
// Scroll the active element to the very bottom of the scroll container,
// then scroll up, so the caret is partially hidden.
await page.evaluate( () => {
- document.activeElement.scrollIntoView( false );
- wp.dom.getScrollContainer( document.activeElement ).scrollTop -=
- document.activeElement.offsetHeight + 10;
+ const { activeElement } =
+ document.activeElement?.contentDocument ?? document;
+ activeElement.scrollIntoView( false );
+ wp.dom.getScrollContainer( activeElement ).scrollTop -=
+ activeElement.offsetHeight + 10;
} );
const bottomPostition = await getCaretPosition();
@@ -220,9 +236,11 @@ describe( 'TypeWriter', () => {
// Scroll the active element to the very top of the scroll container,
// then scroll down, so the caret is partially hidden.
await page.evaluate( () => {
- document.activeElement.scrollIntoView();
- wp.dom.getScrollContainer( document.activeElement ).scrollTop +=
- document.activeElement.offsetHeight + 10;
+ const { activeElement } =
+ document.activeElement?.contentDocument ?? document;
+ activeElement.scrollIntoView();
+ wp.dom.getScrollContainer( activeElement ).scrollTop +=
+ activeElement.offsetHeight + 10;
} );
const topPostition = await getCaretPosition();
diff --git a/packages/e2e-tests/specs/performance/site-editor.test.js b/packages/e2e-tests/specs/performance/site-editor.test.js
index 22c1af35ff7161..e8f705c6dc0d79 100644
--- a/packages/e2e-tests/specs/performance/site-editor.test.js
+++ b/packages/e2e-tests/specs/performance/site-editor.test.js
@@ -151,7 +151,8 @@ describe( 'Site Editor Performance', () => {
await enterEditMode();
// Insert a new paragraph right under the first one.
- await firstParagraph.focus();
+ await firstParagraph.click(); // Once to select the block overlay.
+ await firstParagraph.click(); // Once again to select the paragraph.
await insertBlock( 'Paragraph' );
// Start tracing.
diff --git a/packages/e2e-tests/specs/site-editor/settings-sidebar.test.js b/packages/e2e-tests/specs/site-editor/settings-sidebar.test.js
index 88bf954e86ce2c..ae28019cf0d992 100644
--- a/packages/e2e-tests/specs/site-editor/settings-sidebar.test.js
+++ b/packages/e2e-tests/specs/site-editor/settings-sidebar.test.js
@@ -27,11 +27,11 @@ async function getActiveTabLabel() {
async function getTemplateCard() {
return {
title: await page.$eval(
- '.edit-site-template-card__title',
+ '.edit-site-sidebar-card__title',
( element ) => element.innerText
),
description: await page.$eval(
- '.edit-site-template-card__description',
+ '.edit-site-sidebar-card__description',
( element ) => element.innerText
),
};
diff --git a/packages/edit-post/src/components/header/document-title/index.js b/packages/edit-post/src/components/header/document-title/index.js
new file mode 100644
index 00000000000000..619fa4ef290144
--- /dev/null
+++ b/packages/edit-post/src/components/header/document-title/index.js
@@ -0,0 +1,86 @@
+/**
+ * WordPress dependencies
+ */
+import { __, isRTL } from '@wordpress/i18n';
+import { useSelect, useDispatch } from '@wordpress/data';
+import { BlockIcon, store as blockEditorStore } from '@wordpress/block-editor';
+import {
+ Button,
+ VisuallyHidden,
+ __experimentalHStack as HStack,
+ __experimentalText as Text,
+} from '@wordpress/components';
+import { layout, chevronLeftSmall, chevronRightSmall } from '@wordpress/icons';
+import { store as commandsStore } from '@wordpress/commands';
+import { displayShortcut } from '@wordpress/keycodes';
+
+/**
+ * Internal dependencies
+ */
+import { store as editPostStore } from '../../../store';
+
+function DocumentTitle() {
+ const { template, isEditing } = useSelect( ( select ) => {
+ const { isEditingTemplate, getEditedPostTemplate } =
+ select( editPostStore );
+ const _isEditing = isEditingTemplate();
+
+ return {
+ template: _isEditing ? getEditedPostTemplate() : null,
+ isEditing: _isEditing,
+ };
+ }, [] );
+ const { clearSelectedBlock } = useDispatch( blockEditorStore );
+ const { setIsEditingTemplate } = useDispatch( editPostStore );
+ const { open: openCommandCenter } = useDispatch( commandsStore );
+
+ if ( ! isEditing || ! template ) {
+ return null;
+ }
+
+ let templateTitle = __( 'Default' );
+ if ( template?.title ) {
+ templateTitle = template.title;
+ } else if ( !! template ) {
+ templateTitle = template.slug;
+ }
+
+ return (
+
+
+ {
+ clearSelectedBlock();
+ setIsEditingTemplate( false );
+ } }
+ icon={ isRTL() ? chevronRightSmall : chevronLeftSmall }
+ >
+ { __( 'Back' ) }
+
+
+
+ openCommandCenter() }
+ >
+
+
+
+
+ { __( 'Editing template: ' ) }
+
+ { templateTitle }
+
+
+
+ openCommandCenter() }
+ >
+ { displayShortcut.primary( 'k' ) }
+
+
+ );
+}
+
+export default DocumentTitle;
diff --git a/packages/edit-post/src/components/header/document-title/style.scss b/packages/edit-post/src/components/header/document-title/style.scss
new file mode 100644
index 00000000000000..e39ecf607e4306
--- /dev/null
+++ b/packages/edit-post/src/components/header/document-title/style.scss
@@ -0,0 +1,61 @@
+.edit-post-document-title {
+ display: flex;
+ align-items: center;
+ gap: $grid-unit;
+ height: $button-size;
+ justify-content: space-between;
+ // Flex items will, by default, refuse to shrink below a minimum
+ // intrinsic width. In order to shrink this flexbox item, and
+ // subsequently truncate child text, we set an explicit min-width.
+ // See https://dev.w3.org/csswg/css-flexbox/#min-size-auto
+ min-width: 0;
+ background: $gray-100;
+ border-radius: 4px;
+ width: min(100%, 450px);
+
+ &:hover {
+ color: currentColor;
+ background: $gray-200;
+ }
+}
+
+.edit-post-document-title__title.components-button {
+ flex-grow: 1;
+ color: var(--wp-block-synced-color);
+ overflow: hidden;
+
+ &:hover {
+ color: var(--wp-block-synced-color);
+ }
+
+ h1 {
+ color: var(--wp-block-synced-color);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+}
+
+.edit-post-document-title__shortcut {
+ flex-shrink: 0;
+ color: $gray-700;
+ padding: 0 $grid-unit-15;
+
+ &:hover {
+ color: $gray-700;
+ }
+}
+
+.edit-post-document-title__left {
+ min-width: $button-size;
+ flex-shrink: 0;
+
+ .components-button.has-icon.has-text {
+ color: $gray-700;
+ gap: 0;
+
+ &:hover {
+ color: currentColor;
+ }
+ }
+}
diff --git a/packages/edit-post/src/components/header/header-toolbar/style.scss b/packages/edit-post/src/components/header/header-toolbar/style.scss
index 87aec00004c02b..694dcb5a2d678a 100644
--- a/packages/edit-post/src/components/header/header-toolbar/style.scss
+++ b/packages/edit-post/src/components/header/header-toolbar/style.scss
@@ -1,6 +1,5 @@
.edit-post-header-toolbar {
display: inline-flex;
- flex-grow: 1;
align-items: center;
border: none;
diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js
index 09a93424f6903d..3306a0fdf1606a 100644
--- a/packages/edit-post/src/components/header/index.js
+++ b/packages/edit-post/src/components/header/index.js
@@ -18,7 +18,7 @@ import { default as DevicePreview } from '../device-preview';
import ViewLink from '../view-link';
import MainDashboardButton from './main-dashboard-button';
import { store as editPostStore } from '../../store';
-import TemplateTitle from './template-title';
+import DocumentTitle from './document-title';
function Header( { setEntitiesSavedStatesCallback } ) {
const isLargeViewport = useViewportMatch( 'large' );
@@ -70,7 +70,9 @@ function Header( { setEntitiesSavedStatesCallback } ) {
className="edit-post-header__toolbar"
>
-
+
+
+
{
- const { isEditingTemplate, getEditedPostTemplate } =
- select( editPostStore );
- const _isEditing = isEditingTemplate();
- return {
- template: _isEditing ? getEditedPostTemplate() : null,
- };
- }, [] );
- const [ showConfirmDialog, setShowConfirmDialog ] = useState( false );
-
- if ( ! template || ! template.wp_id ) {
- return null;
- }
- let templateTitle = template.slug;
- if ( template?.title ) {
- templateTitle = template.title;
- }
-
- const isRevertable = template?.has_theme_file;
-
- const onDelete = () => {
- clearSelectedBlock();
- setIsEditingTemplate( false );
- setShowConfirmDialog( false );
-
- editPost( {
- template: '',
- } );
- const settings = getEditorSettings();
- const newAvailableTemplates = Object.fromEntries(
- Object.entries( settings.availableTemplates ?? {} ).filter(
- ( [ id ] ) => id !== template.slug
- )
- );
- updateEditorSettings( {
- availableTemplates: newAvailableTemplates,
- } );
- deleteEntityRecord( 'postType', 'wp_template', template.id, {
- throwOnError: true,
- } );
- };
-
- return (
-
- <>
- {
- setShowConfirmDialog( true );
- } }
- info={
- isRevertable
- ? __( 'Use the template as supplied by the theme.' )
- : undefined
- }
- >
- { isRevertable
- ? __( 'Clear customizations' )
- : __( 'Delete template' ) }
-
- {
- setShowConfirmDialog( false );
- } }
- >
- { sprintf(
- /* translators: %s: template name */
- __(
- 'Are you sure you want to delete the %s template? It may be used by other pages or posts.'
- ),
- templateTitle
- ) }
-
- >
-
- );
-}
diff --git a/packages/edit-post/src/components/header/template-title/edit-template-title.js b/packages/edit-post/src/components/header/template-title/edit-template-title.js
deleted file mode 100644
index 447ea5e4e02d72..00000000000000
--- a/packages/edit-post/src/components/header/template-title/edit-template-title.js
+++ /dev/null
@@ -1,78 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { __ } from '@wordpress/i18n';
-import { TextControl } from '@wordpress/components';
-import { useDispatch, useSelect } from '@wordpress/data';
-import { useState } from '@wordpress/element';
-import { store as editorStore } from '@wordpress/editor';
-import { store as coreStore } from '@wordpress/core-data';
-
-/**
- * Internal dependencies
- */
-import { store as editPostStore } from '../../../store';
-
-export default function EditTemplateTitle() {
- const [ forceEmpty, setForceEmpty ] = useState( false );
- const { template } = useSelect( ( select ) => {
- const { getEditedPostTemplate } = select( editPostStore );
- return {
- template: getEditedPostTemplate(),
- };
- }, [] );
-
- const { editEntityRecord } = useDispatch( coreStore );
- const { getEditorSettings } = useSelect( editorStore );
- const { updateEditorSettings } = useDispatch( editorStore );
-
- // Only user-created and non-default templates can change the name.
- if ( ! template.is_custom || template.has_theme_file ) {
- return null;
- }
-
- let templateTitle = __( 'Default' );
- if ( template?.title ) {
- templateTitle = template.title;
- } else if ( !! template ) {
- templateTitle = template.slug;
- }
-
- return (
-
- {
- // Allow having the field temporarily empty while typing.
- if ( ! newTitle && ! forceEmpty ) {
- setForceEmpty( true );
- return;
- }
- setForceEmpty( false );
-
- const settings = getEditorSettings();
- const newAvailableTemplates = Object.fromEntries(
- Object.entries( settings.availableTemplates ?? {} ).map(
- ( [ id, existingTitle ] ) => [
- id,
- id !== template.slug ? existingTitle : newTitle,
- ]
- )
- );
- updateEditorSettings( {
- availableTemplates: newAvailableTemplates,
- } );
- editEntityRecord( 'postType', 'wp_template', template.id, {
- title: newTitle,
- } );
- } }
- onBlur={ () => setForceEmpty( false ) }
- />
-
- );
-}
diff --git a/packages/edit-post/src/components/header/template-title/index.js b/packages/edit-post/src/components/header/template-title/index.js
deleted file mode 100644
index c0745dc0451b74..00000000000000
--- a/packages/edit-post/src/components/header/template-title/index.js
+++ /dev/null
@@ -1,115 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { __, sprintf } from '@wordpress/i18n';
-import { useSelect, useDispatch } from '@wordpress/data';
-import {
- Dropdown,
- Button,
- __experimentalText as Text,
-} from '@wordpress/components';
-import { chevronDown } from '@wordpress/icons';
-
-/**
- * Internal dependencies
- */
-import { store as editPostStore } from '../../../store';
-import { store as blockEditorStore } from '@wordpress/block-editor';
-import { store as editorStore } from '@wordpress/editor';
-import DeleteTemplate from './delete-template';
-import EditTemplateTitle from './edit-template-title';
-import TemplateDescription from './template-description';
-
-function TemplateTitle() {
- const { template, isEditing, title } = useSelect( ( select ) => {
- const { isEditingTemplate, getEditedPostTemplate } =
- select( editPostStore );
- const { getEditedPostAttribute } = select( editorStore );
-
- const _isEditing = isEditingTemplate();
-
- return {
- template: _isEditing ? getEditedPostTemplate() : null,
- isEditing: _isEditing,
- title: getEditedPostAttribute( 'title' )
- ? getEditedPostAttribute( 'title' )
- : __( 'Untitled' ),
- };
- }, [] );
-
- const { clearSelectedBlock } = useDispatch( blockEditorStore );
- const { setIsEditingTemplate } = useDispatch( editPostStore );
-
- if ( ! isEditing || ! template ) {
- return null;
- }
-
- let templateTitle = __( 'Default' );
- if ( template?.title ) {
- templateTitle = template.title;
- } else if ( !! template ) {
- templateTitle = template.slug;
- }
-
- const hasOptions = !! (
- template.custom ||
- template.wp_id ||
- template.description
- );
-
- return (
-
- {
- clearSelectedBlock();
- setIsEditingTemplate( false );
- } }
- >
- { title }
-
- { hasOptions ? (
- (
-
- { templateTitle }
-
- ) }
- renderContent={ () => (
- <>
-
-
-
- >
- ) }
- />
- ) : (
-
- { templateTitle }
-
- ) }
-
- );
-}
-
-export default TemplateTitle;
diff --git a/packages/edit-post/src/components/header/template-title/style.scss b/packages/edit-post/src/components/header/template-title/style.scss
deleted file mode 100644
index b5fe5120bfb64c..00000000000000
--- a/packages/edit-post/src/components/header/template-title/style.scss
+++ /dev/null
@@ -1,94 +0,0 @@
-.edit-post-template-top-area {
- display: flex;
- flex-direction: column;
- align-content: space-between;
- width: 100%;
- align-items: center;
-
- .edit-post-template-title,
- .edit-post-template-post-title {
- padding: 0;
- text-decoration: none;
- height: auto;
-
- &::before {
- height: 100%;
- }
-
- &.has-icon {
- svg {
- order: 1;
- margin-right: 0;
- }
- }
- }
-
- .edit-post-template-title {
- color: $gray-900;
- }
-
- .edit-post-template-post-title {
- margin-top: $grid-unit-05;
- max-width: 160px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- display: block;
-
- &::before {
- left: 0;
- right: 0;
- }
-
- @include break-xlarge() {
- max-width: 400px;
- }
- }
-}
-
-.edit-post-template-top-area__popover {
- .components-popover__content {
- min-width: 280px;
- padding: 0;
- }
-
- .edit-site-template-details__group {
- padding: $grid-unit-20;
-
- .components-base-control__help {
- margin-bottom: 0;
- }
- }
-
- .edit-post-template-details__description {
- color: $gray-700;
- }
-}
-
-.edit-post-template-top-area__second-menu-group {
- border-top: $border-width solid $gray-300;
- padding: $grid-unit-20 $grid-unit-10;
-
- .edit-post-template-top-area__delete-template-button {
- display: flex;
- justify-content: center;
- padding: $grid-unit-05 $grid-unit;
-
- &.is-destructive {
- padding: inherit;
- margin-left: $grid-unit-10;
- margin-right: $grid-unit-10;
- width: calc(100% - #{($grid-unit * 2)});
-
- .components-menu-item__item {
- width: auto;
- }
- }
-
- .components-menu-item__item {
- margin-right: 0;
- min-width: 0;
- width: 100%;
- }
- }
-}
diff --git a/packages/edit-post/src/components/header/template-title/template-description.js b/packages/edit-post/src/components/header/template-title/template-description.js
deleted file mode 100644
index 3513496852c339..00000000000000
--- a/packages/edit-post/src/components/header/template-title/template-description.js
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { useSelect } from '@wordpress/data';
-import {
- __experimentalHeading as Heading,
- __experimentalText as Text,
-} from '@wordpress/components';
-
-/**
- * Internal dependencies
- */
-import { store as editPostStore } from '../../../store';
-
-export default function TemplateDescription() {
- const { description, title } = useSelect( ( select ) => {
- const { getEditedPostTemplate } = select( editPostStore );
- return {
- title: getEditedPostTemplate().title,
- description: getEditedPostTemplate().description,
- };
- }, [] );
- if ( ! description ) {
- return null;
- }
-
- return (
-
-
- { title }
-
-
- { description }
-
-
- );
-}
diff --git a/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js b/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js
index 67cb2ad11c4cb1..a4e6e639fbd76d 100644
--- a/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js
+++ b/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js
@@ -1,13 +1,8 @@
-/**
- * External dependencies
- */
-import classnames from 'classnames';
-
/**
* WordPress dependencies
*/
import { __experimentalListView as ListView } from '@wordpress/block-editor';
-import { Button } from '@wordpress/components';
+import { Button, TabPanel } from '@wordpress/components';
import {
useFocusOnMount,
useFocusReturn,
@@ -30,7 +25,9 @@ import ListViewOutline from './list-view-outline';
export default function ListViewSidebar() {
const { setIsListViewOpened } = useDispatch( editPostStore );
+ // This hook handles focus when the sidebar first renders.
const focusOnMountRef = useFocusOnMount( 'firstElement' );
+ // The next 2 hooks handle focus for when the sidebar closes and returning focus to the element that had focus before sidebar opened.
const headerFocusReturnRef = useFocusReturn();
const contentFocusReturnRef = useFocusReturn();
@@ -41,17 +38,27 @@ export default function ListViewSidebar() {
}
}
+ // Use internal state instead of a ref to make sure that the component
+ // re-renders when the dropZoneElement updates.
+ const [ dropZoneElement, setDropZoneElement ] = useState( null );
+ // Tracks our current tab.
const [ tab, setTab ] = useState( 'list-view' );
// This ref refers to the sidebar as a whole.
const sidebarRef = useRef();
- // This ref refers to the list view tab button.
- const listViewTabRef = useRef();
- // This ref refers to the outline tab button.
- const outlineTabRef = useRef();
+ // This ref refers to the tab panel.
+ const tabPanelRef = useRef();
// This ref refers to the list view application area.
const listViewRef = useRef();
+ // Must merge the refs together so focus can be handled properly in the next function.
+ const listViewContainerRef = useMergeRefs( [
+ contentFocusReturnRef,
+ focusOnMountRef,
+ listViewRef,
+ setDropZoneElement,
+ ] );
+
/*
* Callback function to handle list view or outline focus.
*
@@ -60,9 +67,11 @@ export default function ListViewSidebar() {
* @return void
*/
function handleSidebarFocus( currentTab ) {
+ // Tab panel focus.
+ const tabPanelFocus = focus.tabbable.find( tabPanelRef.current )[ 0 ];
// List view tab is selected.
if ( currentTab === 'list-view' ) {
- // Either focus the list view or the list view tab button. Must have a fallback because the list view does not render when there are no blocks.
+ // Either focus the list view or the tab panel. Must have a fallback because the list view does not render when there are no blocks.
const listViewApplicationFocus = focus.tabbable.find(
listViewRef.current
)[ 0 ];
@@ -70,11 +79,11 @@ export default function ListViewSidebar() {
listViewApplicationFocus
)
? listViewApplicationFocus
- : listViewTabRef.current;
+ : tabPanelFocus;
listViewFocusArea.focus();
// Outline tab is selected.
} else {
- outlineTabRef.current.focus();
+ tabPanelFocus.focus();
}
}
@@ -93,6 +102,22 @@ export default function ListViewSidebar() {
}
} );
+ /**
+ * Render tab content for a given tab name.
+ *
+ * @param {string} tabName The name of the tab to render.
+ */
+ function renderTabContent( tabName ) {
+ if ( tabName === 'list-view' ) {
+ return (
+
+
+
+ );
+ }
+ return ;
+ }
+
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
-
setIsListViewOpened( false ) }
+ />
+
setTab( tabName ) }
+ selectOnMove={ false }
+ tabs={ [
+ {
+ name: 'list-view',
+ title: 'List View',
+ className: 'edit-post-sidebar__panel-tab',
+ },
+ {
+ name: 'outline',
+ title: 'Outline',
+ className: 'edit-post-sidebar__panel-tab',
+ },
+ ] }
>
- setIsListViewOpened( false ) }
- />
-
-
- {
- setTab( 'list-view' );
- } }
- className={ classnames(
- 'edit-post-sidebar__panel-tab',
- { 'is-active': tab === 'list-view' }
- ) }
- aria-current={ tab === 'list-view' }
- >
- { __( 'List View' ) }
-
-
-
- {
- setTab( 'outline' );
- } }
- className={ classnames(
- 'edit-post-sidebar__panel-tab',
- { 'is-active': tab === 'outline' }
- ) }
- aria-current={ tab === 'outline' }
- >
- { __( 'Outline' ) }
-
-
-
-
-
- { tab === 'list-view' && (
-
-
+ { ( currentTab ) => (
+
+ { renderTabContent( currentTab.name ) }
) }
- { tab === 'outline' &&
}
-
+
);
}
diff --git a/packages/edit-post/src/components/secondary-sidebar/style.scss b/packages/edit-post/src/components/secondary-sidebar/style.scss
index 63a3746e1b8443..d5ef3212bcfdd6 100644
--- a/packages/edit-post/src/components/secondary-sidebar/style.scss
+++ b/packages/edit-post/src/components/secondary-sidebar/style.scss
@@ -17,8 +17,29 @@
width: 350px;
}
- .edit-post-sidebar__panel-tabs {
- flex-direction: row-reverse;
+ .edit-post-editor__document-overview-panel__close-button {
+ position: absolute;
+ right: $grid-unit-10;
+ top: math.div($grid-unit-60 - $button-size, 2); // ( tab height - button size ) / 2
+ z-index: 1;
+ background: $white;
+ }
+
+ // The TabPanel style overrides in the following blocks should be removed when the new TabPanel is available.
+ .components-tab-panel__tabs {
+ border-bottom: $border-width solid $gray-300;
+ box-sizing: border-box;
+ display: flex;
+ width: 100%;
+ padding-right: $grid-unit-70;
+
+ .edit-post-sidebar__panel-tab {
+ width: 50%;
+ }
+ }
+
+ .components-tab-panel__tab-content {
+ height: calc(100% - #{$grid-unit-60});
}
}
@@ -37,34 +58,6 @@
}
}
-.edit-post-editor__document-overview-panel-header {
- border-bottom: $border-width solid $gray-300;
- display: flex;
- justify-content: space-between;
- height: $grid-unit-60;
- padding-left: $grid-unit-20;
- padding-right: $grid-unit-05;
- ul {
- width: calc(100% - #{ $grid-unit-50 });
- }
- li {
- width: 50%;
- button {
- width: 100%;
- text-align: initial;
- }
- }
- li:only-child {
- width: 100%;
- }
-
- &.components-panel__header.edit-post-sidebar__panel-tabs {
- .components-button.has-icon {
- display: flex;
- }
- }
-}
-
.edit-post-editor__list-view-panel-content,
.edit-post-editor__list-view-container > .document-outline,
.edit-post-editor__list-view-empty-headings {
@@ -118,5 +111,9 @@
.edit-post-editor__list-view-container {
display: flex;
flex-direction: column;
- height: calc(100% - #{$grid-unit-60});
+ height: 100%;
+}
+
+.edit-post-editor__document-overview-panel__tab-panel {
+ height: 100%;
}
diff --git a/packages/edit-post/src/components/visual-editor/index.js b/packages/edit-post/src/components/visual-editor/index.js
index ac8902f6a5f7a1..0f4363070ac57a 100644
--- a/packages/edit-post/src/components/visual-editor/index.js
+++ b/packages/edit-post/src/components/visual-editor/index.js
@@ -31,12 +31,10 @@ import {
__experimentaluseLayoutStyles as useLayoutStyles,
} from '@wordpress/block-editor';
import { useEffect, useRef, useMemo } from '@wordpress/element';
-import { Button, __unstableMotion as motion } from '@wordpress/components';
-import { useSelect, useDispatch } from '@wordpress/data';
+import { __unstableMotion as motion } from '@wordpress/components';
+import { useSelect } from '@wordpress/data';
import { useMergeRefs } from '@wordpress/compose';
-import { arrowLeft } from '@wordpress/icons';
-import { __ } from '@wordpress/i18n';
-import { parse } from '@wordpress/blocks';
+import { parse, store as blocksStore } from '@wordpress/blocks';
import { store as coreStore } from '@wordpress/core-data';
/**
@@ -114,6 +112,7 @@ export default function VisualEditor( { styles } ) {
wrapperBlockName,
wrapperUniqueId,
isBlockBasedTheme,
+ hasV3BlocksOnly,
} = useSelect( ( select ) => {
const {
isFeatureActive,
@@ -123,6 +122,7 @@ export default function VisualEditor( { styles } ) {
} = select( editPostStore );
const { getCurrentPostId, getCurrentPostType, getEditorSettings } =
select( editorStore );
+ const { getBlockTypes } = select( blocksStore );
const _isTemplateMode = isEditingTemplate();
let _wrapperBlockName;
@@ -153,6 +153,9 @@ export default function VisualEditor( { styles } ) {
wrapperBlockName: _wrapperBlockName,
wrapperUniqueId: getCurrentPostId(),
isBlockBasedTheme: editorSettings.__unstableIsBlockBasedTheme,
+ hasV3BlocksOnly: getBlockTypes().every( ( type ) => {
+ return type.apiVersion >= 3;
+ } ),
};
}, [] );
const { isCleanNewPost } = useSelect( editorStore );
@@ -175,8 +178,6 @@ export default function VisualEditor( { styles } ) {
_settings.__experimentalFeatures?.useRootPaddingAwareAlignments,
};
}, [] );
- const { clearSelectedBlock } = useDispatch( blockEditorStore );
- const { setIsEditingTemplate } = useDispatch( editPostStore );
const desktopCanvasStyles = {
height: '100%',
width: '100%',
@@ -349,18 +350,6 @@ export default function VisualEditor( { styles } ) {
} }
ref={ blockSelectionClearerRef }
>
- { isTemplateMode && (
-
{
- clearSelectedBlock();
- setIsEditingTemplate( false );
- } }
- >
- { __( 'Back' ) }
-
- ) }
+
+
+ );
+}
diff --git a/packages/edit-site/src/components/add-new-template/new-template.js b/packages/edit-site/src/components/add-new-template/new-template.js
index 0a3037603226fc..a0ea141bda52e3 100644
--- a/packages/edit-site/src/components/add-new-template/new-template.js
+++ b/packages/edit-site/src/components/add-new-template/new-template.js
@@ -13,6 +13,7 @@ import {
__experimentalText as Text,
__experimentalVStack as VStack,
} from '@wordpress/components';
+import { decodeEntities } from '@wordpress/html-entities';
import { useState } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
@@ -140,7 +141,7 @@ export default function NewTemplate( {
sprintf(
// translators: %s: Title of the created template e.g: "Category".
__( '"%s" successfully created.' ),
- newTemplate.title?.rendered || title
+ decodeEntities( newTemplate.title?.rendered || title )
),
{
type: 'snackbar',
diff --git a/packages/edit-site/src/components/block-editor/index.js b/packages/edit-site/src/components/block-editor/index.js
index cc5e7c8d9254df..a2409b3f1baa2c 100644
--- a/packages/edit-site/src/components/block-editor/index.js
+++ b/packages/edit-site/src/components/block-editor/index.js
@@ -38,6 +38,10 @@ import ResizableEditor from './resizable-editor';
import EditorCanvas from './editor-canvas';
import { unlock } from '../../private-apis';
import EditorCanvasContainer from '../editor-canvas-container';
+import {
+ PageContentLock,
+ usePageContentLockNotifications,
+} from '../page-content-lock';
const { ExperimentalBlockEditorProvider } = unlock( blockEditorPrivateApis );
@@ -49,20 +53,25 @@ const LAYOUT = {
export default function BlockEditor() {
const { setIsInserterOpened } = useDispatch( editSiteStore );
- const { storedSettings, templateType, canvasMode } = useSelect(
- ( select ) => {
- const { getSettings, getEditedPostType, getCanvasMode } = unlock(
- select( editSiteStore )
- );
-
- return {
- storedSettings: getSettings( setIsInserterOpened ),
- templateType: getEditedPostType(),
- canvasMode: getCanvasMode(),
- };
- },
- [ setIsInserterOpened ]
- );
+ const { storedSettings, templateType, canvasMode, hasPageContentLock } =
+ useSelect(
+ ( select ) => {
+ const {
+ getSettings,
+ getEditedPostType,
+ getCanvasMode,
+ hasPageContentLock: _hasPageContentLock,
+ } = unlock( select( editSiteStore ) );
+
+ return {
+ storedSettings: getSettings( setIsInserterOpened ),
+ templateType: getEditedPostType(),
+ canvasMode: getCanvasMode(),
+ hasPageContentLock: _hasPageContentLock(),
+ };
+ },
+ [ setIsInserterOpened ]
+ );
const settingsBlockPatterns =
storedSettings.__experimentalAdditionalBlockPatterns ?? // WP 6.0
@@ -137,6 +146,7 @@ export default function BlockEditor() {
contentRef,
useClipboardHandler(),
useTypingObserver(),
+ usePageContentLockNotifications(),
] );
const isMobileViewport = useViewportMatch( 'small', '<' );
const { clearSelectedBlock } = useDispatch( blockEditorStore );
@@ -162,6 +172,7 @@ export default function BlockEditor() {
onChange={ onChange }
useSubRegistry={ false }
>
+ { hasPageContentLock && }
diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js
index 72a8d41fb22f04..f70c6abd787d40 100644
--- a/packages/edit-site/src/components/editor/index.js
+++ b/packages/edit-site/src/components/editor/index.js
@@ -37,7 +37,6 @@ import WelcomeGuide from '../welcome-guide';
import StartTemplateOptions from '../start-template-options';
import { store as editSiteStore } from '../../store';
import { GlobalStylesRenderer } from '../global-styles-renderer';
-
import useTitle from '../routes/use-title';
import CanvasSpinner from '../canvas-spinner';
import { unlock } from '../../private-apis';
@@ -74,6 +73,7 @@ export default function Editor( { isLoading } ) {
isListViewOpen,
showIconLabels,
showBlockBreadcrumbs,
+ hasPageContentLock,
} = useSelect( ( select ) => {
const {
getEditedPostContext,
@@ -81,6 +81,7 @@ export default function Editor( { isLoading } ) {
getCanvasMode,
isInserterOpened,
isListViewOpened,
+ hasPageContentLock: _hasPageContentLock,
} = unlock( select( editSiteStore ) );
const { __unstableGetEditorMode } = select( blockEditorStore );
const { getActiveComplementaryArea } = select( interfaceStore );
@@ -105,6 +106,7 @@ export default function Editor( { isLoading } ) {
'core/edit-site',
'showBlockBreadcrumbs'
),
+ hasPageContentLock: _hasPageContentLock(),
};
}, [] );
const { setEditedPostContext } = useDispatch( editSiteStore );
@@ -122,9 +124,10 @@ export default function Editor( { isLoading } ) {
const secondarySidebarLabel = isListViewOpen
? __( 'List View' )
: __( 'Block Library' );
- const blockContext = useMemo(
- () => ( {
- ...context,
+ const blockContext = useMemo( () => {
+ const { postType, postId, ...nonPostFields } = context ?? {};
+ return {
+ ...( hasPageContentLock ? context : nonPostFields ),
queryContext: [
context?.queryContext || { page: 1 },
( newQueryContext ) =>
@@ -136,9 +139,8 @@ export default function Editor( { isLoading } ) {
},
} ),
],
- } ),
- [ context, setEditedPostContext ]
- );
+ };
+ }, [ hasPageContentLock, context, setEditedPostContext ] );
let title;
if ( hasLoadedPost ) {
@@ -227,7 +229,11 @@ export default function Editor( { isLoading } ) {
footer={
shouldShowBlockBreakcrumbs && (
)
}
diff --git a/packages/edit-site/src/components/editor/style.scss b/packages/edit-site/src/components/editor/style.scss
index 70a12694cc3f7e..17107820ffb83c 100644
--- a/packages/edit-site/src/components/editor/style.scss
+++ b/packages/edit-site/src/components/editor/style.scss
@@ -19,16 +19,11 @@
}
// Adjust the position of the notices
-.edit-site {
- .components-editor-notices__snackbar {
- position: fixed;
- right: 0;
- bottom: 0;
- padding: 16px;
- }
- .is-edit-mode .components-editor-notices__snackbar {
- bottom: 24px;
- }
+.edit-site .components-editor-notices__snackbar {
+ position: absolute;
+ right: 0;
+ bottom: 40px;
+ padding-left: 16px;
+ padding-right: 16px;
}
-
@include editor-left(".edit-site .components-editor-notices__snackbar")
diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js b/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js
index 94d9296989eee1..a35f9092248fde 100644
--- a/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js
+++ b/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js
@@ -5,10 +5,7 @@ import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { useContext, useMemo } from '@wordpress/element';
import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
-/**
- * External dependencies
- */
-import { isEmpty } from 'lodash';
+
/**
* Internal dependencies
*/
@@ -75,7 +72,12 @@ export default function useGlobalStylesRevisions() {
}
// Adds an item for unsaved changes.
- if ( isDirty && ! isEmpty( userConfig ) && currentUser ) {
+ if (
+ isDirty &&
+ userConfig &&
+ Object.keys( userConfig ).length > 0 &&
+ currentUser
+ ) {
const unsavedRevision = {
id: 'unsaved',
styles: userConfig?.styles,
diff --git a/packages/edit-site/src/components/global-styles/ui.js b/packages/edit-site/src/components/global-styles/ui.js
index f15fcff11aa560..953904e314928c 100644
--- a/packages/edit-site/src/components/global-styles/ui.js
+++ b/packages/edit-site/src/components/global-styles/ui.js
@@ -237,6 +237,33 @@ function GlobalStylesBlockLink() {
}, [ selectedBlockClientId, selectedBlockName, blockHasGlobalStyles ] );
}
+function GlobalStylesEditorCanvasContainerLink() {
+ const { goTo, location } = useNavigator();
+ const editorCanvasContainerView = useSelect(
+ ( select ) =>
+ unlock( select( editSiteStore ) ).getEditorCanvasContainerView(),
+ []
+ );
+
+ // If the user switches the editor canvas container view, redirect
+ // to the appropriate screen. This effectively allows deep linking to the
+ // desired screens from outside the global styles navigation provider.
+ useEffect( () => {
+ if ( editorCanvasContainerView === 'global-styles-revisions' ) {
+ // Switching to the revisions container view should
+ // redirect to the revisions screen.
+ goTo( '/revisions' );
+ } else if (
+ !! editorCanvasContainerView &&
+ location?.path === '/revisions'
+ ) {
+ // Switching to any container other than revisions should
+ // redirect from the revisions screen to the root global styles screen.
+ goTo( '/' );
+ }
+ }, [ editorCanvasContainerView, location?.path, goTo ] );
+}
+
function GlobalStylesUI() {
const blocks = getBlockTypes();
const editorCanvasContainerView = useSelect(
@@ -326,6 +353,7 @@ function GlobalStylesUI() {
+
);
}
diff --git a/packages/edit-site/src/components/header-edit-mode/document-actions/index.js b/packages/edit-site/src/components/header-edit-mode/document-actions/index.js
index 94f8358fda993f..c4f70f8b9c8c1e 100644
--- a/packages/edit-site/src/components/header-edit-mode/document-actions/index.js
+++ b/packages/edit-site/src/components/header-edit-mode/document-actions/index.js
@@ -1,8 +1,13 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+
/**
* WordPress dependencies
*/
import { sprintf, __ } from '@wordpress/i18n';
-import { useDispatch } from '@wordpress/data';
+import { useSelect, useDispatch } from '@wordpress/data';
import {
Button,
VisuallyHidden,
@@ -10,27 +15,88 @@ import {
__experimentalHStack as HStack,
} from '@wordpress/components';
import { BlockIcon } from '@wordpress/block-editor';
-import { privateApis as commandsPrivateApis } from '@wordpress/commands';
+import { store as commandsStore } from '@wordpress/commands';
+import {
+ chevronLeftSmall as chevronLeftSmallIcon,
+ page as pageIcon,
+} from '@wordpress/icons';
+import { useEntityRecord } from '@wordpress/core-data';
import { displayShortcut } from '@wordpress/keycodes';
+import { useState, useEffect, useRef } from '@wordpress/element';
/**
* Internal dependencies
*/
import useEditedEntityRecord from '../../use-edited-entity-record';
-import { unlock } from '../../../private-apis';
-
-const { store: commandsStore } = unlock( commandsPrivateApis );
+import { store as editSiteStore } from '../../../store';
export default function DocumentActions() {
- const { open: openCommandCenter } = useDispatch( commandsStore );
+ const isPage = useSelect( ( select ) => select( editSiteStore ).isPage() );
+ return isPage ? : ;
+}
+
+function PageDocumentActions() {
+ const { hasPageContentLock, context } = useSelect(
+ ( select ) => ( {
+ hasPageContentLock: select( editSiteStore ).hasPageContentLock(),
+ context: select( editSiteStore ).getEditedPostContext(),
+ } ),
+ []
+ );
+
+ const { hasResolved, editedRecord } = useEntityRecord(
+ 'postType',
+ context.postType,
+ context.postId
+ );
+
+ const { setHasPageContentLock } = useDispatch( editSiteStore );
+
+ const [ hasEditedTemplate, setHasEditedTemplate ] = useState( false );
+ const prevHasPageContentLock = useRef( false );
+ useEffect( () => {
+ if ( prevHasPageContentLock.current && ! hasPageContentLock ) {
+ setHasEditedTemplate( true );
+ }
+ prevHasPageContentLock.current = hasPageContentLock;
+ }, [ hasPageContentLock ] );
+
+ if ( ! hasResolved ) {
+ return null;
+ }
+
+ if ( ! editedRecord ) {
+ return (
+
+ { __( 'Document not found' ) }
+
+ );
+ }
+
+ return hasPageContentLock ? (
+
+ { editedRecord.title }
+
+ ) : (
+ setHasPageContentLock( true ) }
+ />
+ );
+}
+
+function TemplateDocumentActions( { className, onBack } ) {
const { isLoaded, record, getTitle, icon } = useEditedEntityRecord();
- // Return a simple loading indicator until we have information to show.
if ( ! isLoaded ) {
return null;
}
- // Return feedback that the template does not seem to exist.
if ( ! record ) {
return (
@@ -45,31 +111,59 @@ export default function DocumentActions() {
: __( 'template' );
return (
-
openCommandCenter() }
+
+
+ { sprintf(
+ /* translators: %s: the entity being edited, like "template"*/
+ __( 'Editing %s: ' ),
+ entityLabel
+ ) }
+
+ { getTitle() }
+
+ );
+}
+
+function BaseDocumentActions( { className, icon, children, onBack } ) {
+ const { open: openCommandCenter } = useDispatch( commandsStore );
+ return (
+
-
- {
+ event.stopPropagation();
+ onBack();
+ } }
+ >
+ { __( 'Back' ) }
+
+ ) }
+ openCommandCenter() }
>
-
-
-
- { sprintf(
- /* translators: %s: the entity being edited, like "template"*/
- __( 'Editing %s: ' ),
- entityLabel
- ) }
-
- { getTitle() }
-
-
-
- { displayShortcut.primary( 'k' ) }
-
-
+
+
+
+ { children }
+
+
+
+ { displayShortcut.primary( 'k' ) }
+
+
+
);
}
diff --git a/packages/edit-site/src/components/header-edit-mode/document-actions/style.scss b/packages/edit-site/src/components/header-edit-mode/document-actions/style.scss
index 247b901975fd8e..7421dc7a7b6dc0 100644
--- a/packages/edit-site/src/components/header-edit-mode/document-actions/style.scss
+++ b/packages/edit-site/src/components/header-edit-mode/document-actions/style.scss
@@ -1,10 +1,7 @@
.edit-site-document-actions {
- display: flex;
- align-items: center;
- gap: $grid-unit;
+ display: grid;
+ grid-template-columns: 1fr 2fr 1fr;
height: $button-size;
- padding: $grid-unit;
- justify-content: space-between;
// Flex items will, by default, refuse to shrink below a minimum
// intrinsic width. In order to shrink this flexbox item, and
// subsequently truncate child text, we set an explicit min-width.
@@ -13,6 +10,7 @@
background: $gray-100;
border-radius: 4px;
width: min(100%, 450px);
+ overflow: hidden;
&:hover {
color: currentColor;
@@ -20,29 +18,86 @@
}
}
+.edit-site-document-actions__command {
+ grid-column: 1 / -1;
+ display: grid;
+ grid-template-columns: 1fr 2fr 1fr;
+ grid-row: 1;
+}
+
+
.edit-site-document-actions__title {
flex-grow: 1;
color: var(--wp-block-synced-color);
overflow: hidden;
+ grid-column: 2 / 3;
h1 {
- color: var(--wp-block-synced-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+ color: var(--wp-block-synced-color);
+ }
+
+ .edit-site-document-actions.is-page & {
+ color: $gray-800;
+
+ h1 {
+ color: $gray-800;
+ }
+ }
+
+ .edit-site-document-actions.is-animated & {
+ animation: edit-site-document-actions__slide-in-left 0.3s;
+ @include reduce-motion("animation");
+ }
+
+ .edit-site-document-actions.is-animated.is-page & {
+ animation: edit-site-document-actions__slide-in-right 0.3s;
+ @include reduce-motion("animation");
}
}
.edit-site-document-actions__shortcut {
- flex-shrink: 0;
color: $gray-700;
- width: #{$grid-unit * 4.5};
+ text-align: right;
+
&:hover {
color: $gray-700;
}
}
-.edit-site-document-actions__left {
+.edit-site-document-actions__back {
min-width: $button-size;
flex-shrink: 0;
+ grid-column: 1 / 2;
+ grid-row: 1;
+ z-index: 1;
+
+ .edit-site-document-actions.is-animated & {
+ animation: edit-site-document-actions__slide-in-left 0.3s;
+ @include reduce-motion("animation");
+ }
+}
+
+@keyframes edit-site-document-actions__slide-in-right {
+ from {
+ transform: translateX(-15%);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+}
+
+@keyframes edit-site-document-actions__slide-in-left {
+ from {
+ transform: translateX(15%);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
}
diff --git a/packages/edit-site/src/components/header-edit-mode/tools-more-menu-group/index.js b/packages/edit-site/src/components/header-edit-mode/tools-more-menu-group/index.js
index d0f05487716b7e..8babbdd0c3dc71 100644
--- a/packages/edit-site/src/components/header-edit-mode/tools-more-menu-group/index.js
+++ b/packages/edit-site/src/components/header-edit-mode/tools-more-menu-group/index.js
@@ -1,8 +1,3 @@
-/**
- * External dependencies
- */
-import { isEmpty } from 'lodash';
-
/**
* WordPress dependencies
*/
@@ -14,7 +9,7 @@ const { Fill: ToolsMoreMenuGroup, Slot } = createSlotFill(
ToolsMoreMenuGroup.Slot = ( { fillProps } ) => (
- { ( fills ) => ! isEmpty( fills ) && fills }
+ { ( fills ) => fills && fills.length > 0 }
);
diff --git a/packages/edit-site/src/components/layout/index.js b/packages/edit-site/src/components/layout/index.js
index 6b1430fdc66c5c..83527bae6d7e3b 100644
--- a/packages/edit-site/src/components/layout/index.js
+++ b/packages/edit-site/src/components/layout/index.js
@@ -215,7 +215,7 @@ export default function Layout() {
whileHover={
isEditorPage && canvasMode === 'view'
? {
- scale: 1.006,
+ scale: 1.005,
transition: {
duration:
disableMotion ||
@@ -227,14 +227,8 @@ export default function Layout() {
}
: {}
}
- // Setting a transform property (in this case scale) on an element makes it act as a containing block for its descendants.
- // This means that the snackbar notices inside this component are repositioned to be relative to this element.
- // To avoid the snackbars jumping about we need to ensure that a transform property is always set.
- // Setting a scale of 1 is interpred by framer as no change, so once the animation completes
- // the transform property of this element is set to none, thus removing its role as a container block.
- // Instead we set the initial scale of this element to 1.0001 so that there is always a transform property set.
- // If we set the initial scale to less than 1.001 then JavaScript rounds it to 1 and the problem reoccurs.
- initial={ { scale: 1.001 } }
+ initial={ false }
+ layout="position"
className="edit-site-layout__canvas"
transition={ {
type: 'tween',
diff --git a/packages/edit-site/src/components/list/table.js b/packages/edit-site/src/components/list/table.js
index 59ff5a9187ab09..18198cb600ddae 100644
--- a/packages/edit-site/src/components/list/table.js
+++ b/packages/edit-site/src/components/list/table.js
@@ -13,8 +13,8 @@ import { decodeEntities } from '@wordpress/html-entities';
/**
* Internal dependencies
*/
+import TemplateActions from '../template-actions';
import Link from '../routes/link';
-import Actions from './actions';
import AddedBy from './added-by';
export default function Table( { templateType } ) {
@@ -126,7 +126,11 @@ export default function Table( { templateType } ) {
) : null }
-
+
) ) }
diff --git a/packages/edit-site/src/components/page-actions/delete-page-menu-item.js b/packages/edit-site/src/components/page-actions/delete-page-menu-item.js
new file mode 100644
index 00000000000000..d28e3e16dab5e5
--- /dev/null
+++ b/packages/edit-site/src/components/page-actions/delete-page-menu-item.js
@@ -0,0 +1,71 @@
+/**
+ * WordPress dependencies
+ */
+import { useDispatch, useSelect } from '@wordpress/data';
+import { decodeEntities } from '@wordpress/html-entities';
+import { useState } from '@wordpress/element';
+import { store as coreStore } from '@wordpress/core-data';
+import { __, sprintf } from '@wordpress/i18n';
+import {
+ MenuItem,
+ __experimentalConfirmDialog as ConfirmDialog,
+} from '@wordpress/components';
+import { store as noticesStore } from '@wordpress/notices';
+
+export default function DeletePageMenuItem( { postId, onRemove } ) {
+ const [ isModalOpen, setIsModalOpen ] = useState( false );
+ const { createSuccessNotice, createErrorNotice } =
+ useDispatch( noticesStore );
+ const { deleteEntityRecord } = useDispatch( coreStore );
+ const page = useSelect(
+ ( select ) =>
+ select( coreStore ).getEntityRecord( 'postType', 'page', postId ),
+ [ postId ]
+ );
+ async function removePage() {
+ try {
+ await deleteEntityRecord(
+ 'postType',
+ 'page',
+ postId,
+ {},
+ { throwOnError: true }
+ );
+ createSuccessNotice(
+ sprintf(
+ /* translators: The page's title. */
+ __( '"%s" deleted.' ),
+ decodeEntities( page.title.rendered )
+ ),
+ {
+ type: 'snackbar',
+ id: 'edit-site-page-removed',
+ }
+ );
+ onRemove?.();
+ } catch ( error ) {
+ const errorMessage =
+ error.message && error.code !== 'unknown_error'
+ ? error.message
+ : __( 'An error occurred while deleting the page.' );
+
+ createErrorNotice( errorMessage, { type: 'snackbar' } );
+ } finally {
+ setIsModalOpen( false );
+ }
+ }
+ return (
+ <>
+ setIsModalOpen( true ) } isDestructive>
+ { __( 'Delete' ) }
+
+ setIsModalOpen( false ) }
+ >
+ { __( 'Are you sure you want to delete this page?' ) }
+
+ >
+ );
+}
diff --git a/packages/edit-site/src/components/page-actions/index.js b/packages/edit-site/src/components/page-actions/index.js
new file mode 100644
index 00000000000000..6aaf9cadc211dd
--- /dev/null
+++ b/packages/edit-site/src/components/page-actions/index.js
@@ -0,0 +1,36 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { DropdownMenu, MenuGroup } from '@wordpress/components';
+import { moreVertical } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import DeletePageMenuItem from './delete-page-menu-item';
+
+export default function PageActions( {
+ postId,
+ className,
+ toggleProps,
+ onRemove,
+} ) {
+ return (
+
+ { () => (
+
+
+
+ ) }
+
+ );
+}
diff --git a/packages/edit-site/src/components/page-content-lock/constants.js b/packages/edit-site/src/components/page-content-lock/constants.js
new file mode 100644
index 00000000000000..668fe8af00d69d
--- /dev/null
+++ b/packages/edit-site/src/components/page-content-lock/constants.js
@@ -0,0 +1,5 @@
+export const CONTENT_BLOCK_TYPES = [
+ 'core/post-title',
+ 'core/post-featured-image',
+ 'core/post-content',
+];
diff --git a/packages/edit-site/src/components/page-content-lock/index.js b/packages/edit-site/src/components/page-content-lock/index.js
new file mode 100644
index 00000000000000..83d096fb39f5d0
--- /dev/null
+++ b/packages/edit-site/src/components/page-content-lock/index.js
@@ -0,0 +1,14 @@
+/**
+ * Internal dependencies
+ */
+import { useDisableNonContentBlocks } from './use-disable-non-content-blocks';
+
+/**
+ * Component that when rendered, locks the site editor so that only page content
+ * can be edited.
+ */
+export function PageContentLock() {
+ useDisableNonContentBlocks();
+}
+
+export { usePageContentLockNotifications } from './use-page-content-lock-notifications';
diff --git a/packages/edit-site/src/components/page-content-lock/use-disable-non-content-blocks.js b/packages/edit-site/src/components/page-content-lock/use-disable-non-content-blocks.js
new file mode 100644
index 00000000000000..ce198909877f6b
--- /dev/null
+++ b/packages/edit-site/src/components/page-content-lock/use-disable-non-content-blocks.js
@@ -0,0 +1,44 @@
+/**
+ * WordPress dependencies
+ */
+import { createHigherOrderComponent } from '@wordpress/compose';
+import { addFilter, removeFilter } from '@wordpress/hooks';
+import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
+import { useEffect } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { unlock } from '../../private-apis';
+import { CONTENT_BLOCK_TYPES } from './constants';
+
+const { useBlockEditingMode } = unlock( blockEditorPrivateApis );
+
+/**
+ * Disables non-content blocks using the `useBlockEditingMode` hook.
+ */
+export function useDisableNonContentBlocks() {
+ useBlockEditingMode( 'disabled' );
+ useEffect( () => {
+ addFilter(
+ 'editor.BlockEdit',
+ 'core/edit-site/disable-non-content-blocks',
+ withDisableNonContentBlocks
+ );
+ return () =>
+ removeFilter(
+ 'editor.BlockEdit',
+ 'core/edit-site/disable-non-content-blocks'
+ );
+ }, [] );
+}
+
+const withDisableNonContentBlocks = createHigherOrderComponent(
+ ( BlockEdit ) => ( props ) => {
+ const isContent = CONTENT_BLOCK_TYPES.includes( props.name );
+ const mode = isContent ? 'contentOnly' : undefined;
+ useBlockEditingMode( mode );
+ return ;
+ },
+ 'withBlockEditingMode'
+);
diff --git a/packages/edit-site/src/components/page-content-lock/use-page-content-lock-notifications.js b/packages/edit-site/src/components/page-content-lock/use-page-content-lock-notifications.js
new file mode 100644
index 00000000000000..2a800317a33a95
--- /dev/null
+++ b/packages/edit-site/src/components/page-content-lock/use-page-content-lock-notifications.js
@@ -0,0 +1,128 @@
+/**
+ * WordPress dependencies
+ */
+import { useSelect, useDispatch } from '@wordpress/data';
+import { useEffect, useRef } from '@wordpress/element';
+import { store as noticesStore } from '@wordpress/notices';
+import { __ } from '@wordpress/i18n';
+import { useRefEffect } from '@wordpress/compose';
+
+/**
+ * Internal dependencies
+ */
+import { store as editSiteStore } from '../../store';
+
+/**
+ * Hook that displays notifications that guide the user towards using the
+ * content vs. template editing modes.
+ *
+ * @return {import('react').RefObject} Ref which should be passed
+ * (using useMergeRefs()) to
+ * the editor iframe canvas.
+ */
+export function usePageContentLockNotifications() {
+ const ref = useEditTemplateNotification();
+ useBackToPageNotification();
+ return ref;
+}
+
+/**
+ * Hook that displays a 'Edit your template to edit this block' notification
+ * when the user is focusing on editing page content and clicks on a locked
+ * template block.
+ *
+ * @return {import('react').RefObject} Ref which should be passed
+ * (using useMergeRefs()) to
+ * the editor iframe canvas.
+ */
+function useEditTemplateNotification() {
+ const hasPageContentLock = useSelect(
+ ( select ) => select( editSiteStore ).hasPageContentLock(),
+ []
+ );
+
+ const alreadySeen = useRef( false );
+
+ const { createInfoNotice } = useDispatch( noticesStore );
+ const { setHasPageContentLock } = useDispatch( editSiteStore );
+
+ return useRefEffect(
+ ( node ) => {
+ const handleClick = ( event ) => {
+ if (
+ ! alreadySeen.current &&
+ hasPageContentLock &&
+ event.target.classList.contains( 'is-root-container' )
+ ) {
+ createInfoNotice(
+ __( 'Edit your template to edit this block' ),
+ {
+ isDismissible: true,
+ type: 'snackbar',
+ actions: [
+ {
+ label: __( 'Edit template' ),
+ onClick: () =>
+ setHasPageContentLock( false ),
+ },
+ ],
+ }
+ );
+ alreadySeen.current = true;
+ }
+ };
+ node.addEventListener( 'click', handleClick );
+ return () => node.removeEventListener( 'click', handleClick );
+ },
+ [
+ hasPageContentLock,
+ alreadySeen,
+ createInfoNotice,
+ setHasPageContentLock,
+ ]
+ );
+}
+
+/**
+ * Hook that displays a 'You are editing a template' notification when the user
+ * switches from focusing on editing page content to editing a template.
+ */
+function useBackToPageNotification() {
+ const hasPageContentLock = useSelect(
+ ( select ) => select( editSiteStore ).hasPageContentLock(),
+ []
+ );
+
+ const alreadySeen = useRef( false );
+ const prevHasPageContentLock = useRef( false );
+
+ const { createInfoNotice } = useDispatch( noticesStore );
+ const { setHasPageContentLock } = useDispatch( editSiteStore );
+
+ useEffect( () => {
+ if (
+ ! alreadySeen.current &&
+ prevHasPageContentLock.current &&
+ ! hasPageContentLock
+ ) {
+ createInfoNotice( __( 'You are editing a template' ), {
+ isDismissible: true,
+ type: 'snackbar',
+ actions: [
+ {
+ label: __( 'Back to page' ),
+ onClick: () => setHasPageContentLock( true ),
+ },
+ ],
+ } );
+ alreadySeen.current = true;
+ }
+ prevHasPageContentLock.current = hasPageContentLock;
+ }, [
+ alreadySeen,
+ prevHasPageContentLock,
+ hasPageContentLock,
+ createInfoNotice,
+ setHasPageContentLock,
+ ] );
+}
diff --git a/packages/edit-site/src/components/resizable-frame/index.js b/packages/edit-site/src/components/resizable-frame/index.js
index f5dff65f3749b5..5463cf357a31be 100644
--- a/packages/edit-site/src/components/resizable-frame/index.js
+++ b/packages/edit-site/src/components/resizable-frame/index.js
@@ -171,6 +171,18 @@ function ResizableFrame( {
},
};
+ const resizeHandleVariants = {
+ default: {
+ opacity: 1,
+ left: -16,
+ },
+ resizing: {
+ opacity: 1,
+ left: -16,
+ scaleY: 1.3,
+ },
+ };
+
return (
) : null,
} }
diff --git a/packages/edit-site/src/components/resizable-frame/style.scss b/packages/edit-site/src/components/resizable-frame/style.scss
index 2bd478b9bf9916..9a959afe845e47 100644
--- a/packages/edit-site/src/components/resizable-frame/style.scss
+++ b/packages/edit-site/src/components/resizable-frame/style.scss
@@ -28,42 +28,44 @@
}
.edit-site-resizable-frame__handle {
- position: absolute;
- width: 5px;
- height: 50px;
- background-color: rgba(255, 255, 255, 0.3);
- z-index: 100;
- border-radius: 5px;
+ align-items: center;
+ background-color: rgba($gray-700, 0.4);
+ border-radius: $grid-unit-05;
cursor: col-resize;
display: flex;
- align-items: center;
+ height: $grid-unit-80;
justify-content: flex-end;
- top: 50%;
+ position: absolute;
+ top: calc(50% - #{$grid-unit-40});
+ width: $grid-unit-05;
+ z-index: 100;
+
&::before {
- position: absolute;
- left: 100%;
- height: 100%;
- width: $grid-unit-30;
content: "";
+ height: 100%;
+ left: 100%;
+ position: absolute;
+ width: $grid-unit-40;
}
&::after {
+ content: "";
+ height: 100%;
position: absolute;
right: 100%;
- height: 100%;
- width: $grid-unit-30;
- content: "";
+ width: $grid-unit-40;
}
- &:hover {
+ &:hover,
+ .is-resizing & {
background-color: var(--wp-admin-theme-color);
}
.edit-site-resizable-frame__handle-label {
- border-radius: 2px;
background: var(--wp-admin-theme-color);
- padding: 4px 8px;
+ border-radius: 2px;
color: #fff;
margin-right: $grid-unit-10;
+ padding: 4px 8px;
}
}
diff --git a/packages/edit-site/src/components/revisions/index.js b/packages/edit-site/src/components/revisions/index.js
index 3e06b6415cc4ea..221166d2d998eb 100644
--- a/packages/edit-site/src/components/revisions/index.js
+++ b/packages/edit-site/src/components/revisions/index.js
@@ -1,8 +1,3 @@
-/**
- * External dependencies
- */
-import { isEmpty } from 'lodash';
-
/**
* WordPress dependencies
*/
@@ -30,6 +25,10 @@ import EditorCanvasContainer from '../editor-canvas-container';
const { ExperimentalBlockEditorProvider, useGlobalStylesOutputWithConfig } =
unlock( blockEditorPrivateApis );
+function isObjectEmpty( object ) {
+ return ! object || Object.keys( object ).length === 0;
+}
+
function Revisions( { onClose, userConfig, blocks } ) {
const { baseConfig } = useSelect(
( select ) => ( {
@@ -42,7 +41,7 @@ function Revisions( { onClose, userConfig, blocks } ) {
);
const mergedConfig = useMemo( () => {
- if ( ! isEmpty( userConfig ) && ! isEmpty( baseConfig ) ) {
+ if ( ! isObjectEmpty( userConfig ) && ! isObjectEmpty( baseConfig ) ) {
return mergeBaseAndUserConfigs( baseConfig, userConfig );
}
return {};
@@ -65,7 +64,7 @@ function Revisions( { onClose, userConfig, blocks } ) {
const [ globalStyles ] = useGlobalStylesOutputWithConfig( mergedConfig );
const editorStyles =
- ! isEmpty( globalStyles ) && ! isEmpty( userConfig )
+ ! isObjectEmpty( globalStyles ) && ! isObjectEmpty( userConfig )
? globalStyles
: settings.styles;
diff --git a/packages/edit-site/src/components/save-button/index.js b/packages/edit-site/src/components/save-button/index.js
index 0d78e32307797a..01a8972fc23315 100644
--- a/packages/edit-site/src/components/save-button/index.js
+++ b/packages/edit-site/src/components/save-button/index.js
@@ -17,6 +17,7 @@ export default function SaveButton( {
className = 'edit-site-save-button__button',
variant = 'primary',
showTooltip = true,
+ defaultLabel,
icon,
} ) {
const { isDirty, isSaving, isSaveViewOpen } = useSelect( ( select ) => {
@@ -38,10 +39,15 @@ export default function SaveButton( {
const disabled = isSaving || ! activateSaveEnabled;
const getLabel = () => {
+ if ( isSaving ) {
+ return __( 'Saving' );
+ }
if ( disabled ) {
return __( 'Saved' );
}
+ if ( defaultLabel ) return defaultLabel;
+
if ( isPreviewingTheme() && isDirty ) {
return __( 'Activate & Save' );
} else if ( isPreviewingTheme() ) {
diff --git a/packages/edit-site/src/components/save-hub/index.js b/packages/edit-site/src/components/save-hub/index.js
index 0c99cfd9d4d44c..4b4d1c92442a16 100644
--- a/packages/edit-site/src/components/save-hub/index.js
+++ b/packages/edit-site/src/components/save-hub/index.js
@@ -1,60 +1,165 @@
/**
* WordPress dependencies
*/
-import { useSelect } from '@wordpress/data';
-import { __experimentalHStack as HStack } from '@wordpress/components';
-import { sprintf, _n } from '@wordpress/i18n';
+import { useSelect, useDispatch } from '@wordpress/data';
+import { Button, __experimentalHStack as HStack } from '@wordpress/components';
+import { __, sprintf, _n } from '@wordpress/i18n';
import { store as coreStore } from '@wordpress/core-data';
+import { store as blockEditorStore } from '@wordpress/block-editor';
import { check } from '@wordpress/icons';
+import { privateApis as routerPrivateApis } from '@wordpress/router';
+import { store as noticesStore } from '@wordpress/notices';
/**
* Internal dependencies
*/
import SaveButton from '../save-button';
import { isPreviewingTheme } from '../../utils/is-previewing-theme';
+import { unlock } from '../../private-apis';
+
+const { useLocation } = unlock( routerPrivateApis );
+
+const PUBLISH_ON_SAVE_ENTITIES = [
+ {
+ kind: 'postType',
+ name: 'wp_navigation',
+ },
+];
export default function SaveHub() {
- const { countUnsavedChanges, isDirty, isSaving } = useSelect(
- ( select ) => {
- const {
- __experimentalGetDirtyEntityRecords,
- isSavingEntityRecord,
- } = select( coreStore );
- const dirtyEntityRecords = __experimentalGetDirtyEntityRecords();
- return {
- isDirty: dirtyEntityRecords.length > 0,
- isSaving: dirtyEntityRecords.some( ( record ) =>
- isSavingEntityRecord( record.kind, record.name, record.key )
- ),
- countUnsavedChanges: dirtyEntityRecords.length,
- };
- },
- []
- );
+ const { params } = useLocation();
+
+ const { __unstableMarkLastChangeAsPersistent } =
+ useDispatch( blockEditorStore );
+
+ const { createSuccessNotice, createErrorNotice } =
+ useDispatch( noticesStore );
+
+ const { dirtyCurrentEntity, countUnsavedChanges, isDirty, isSaving } =
+ useSelect(
+ ( select ) => {
+ const {
+ __experimentalGetDirtyEntityRecords,
+ isSavingEntityRecord,
+ } = select( coreStore );
+ const dirtyEntityRecords =
+ __experimentalGetDirtyEntityRecords();
+ let calcDirtyCurrentEntity = null;
+
+ if ( dirtyEntityRecords.length === 1 ) {
+ // if we are on global styles
+ if ( params.path?.includes( 'wp_global_styles' ) ) {
+ calcDirtyCurrentEntity = dirtyEntityRecords.find(
+ ( record ) => record.name === 'globalStyles'
+ );
+ }
+ // if we are on pages
+ else if ( params.postId ) {
+ calcDirtyCurrentEntity = dirtyEntityRecords.find(
+ ( record ) =>
+ record.name === params.postType &&
+ String( record.key ) === params.postId
+ );
+ }
+ }
+
+ return {
+ dirtyCurrentEntity: calcDirtyCurrentEntity,
+ isDirty: dirtyEntityRecords.length > 0,
+ isSaving: dirtyEntityRecords.some( ( record ) =>
+ isSavingEntityRecord(
+ record.kind,
+ record.name,
+ record.key
+ )
+ ),
+ countUnsavedChanges: dirtyEntityRecords.length,
+ };
+ },
+ [ params.path, params.postType, params.postId ]
+ );
+
+ const {
+ editEntityRecord,
+ saveEditedEntityRecord,
+ __experimentalSaveSpecifiedEntityEdits: saveSpecifiedEntityEdits,
+ } = useDispatch( coreStore );
const disabled = isSaving || ( ! isDirty && ! isPreviewingTheme() );
+ // if we have only one unsaved change and it matches current context, we can show a more specific label
+ let label = dirtyCurrentEntity
+ ? __( 'Save' )
+ : sprintf(
+ // translators: %d: number of unsaved changes (number).
+ _n(
+ 'Review %d change…',
+ 'Review %d changes…',
+ countUnsavedChanges
+ ),
+ countUnsavedChanges
+ );
+
+ if ( isSaving ) {
+ label = __( 'Saving' );
+ }
+
+ const saveCurrentEntity = async () => {
+ if ( ! dirtyCurrentEntity ) return;
+
+ const { kind, name, key, property } = dirtyCurrentEntity;
+
+ try {
+ if ( 'root' === dirtyCurrentEntity.kind && 'site' === name ) {
+ await saveSpecifiedEntityEdits( 'root', 'site', undefined, [
+ property,
+ ] );
+ } else {
+ if (
+ PUBLISH_ON_SAVE_ENTITIES.some(
+ ( typeToPublish ) =>
+ typeToPublish.kind === kind &&
+ typeToPublish.name === name
+ )
+ ) {
+ editEntityRecord( kind, name, key, { status: 'publish' } );
+ }
+
+ await saveEditedEntityRecord( kind, name, key );
+ }
+
+ __unstableMarkLastChangeAsPersistent();
+
+ createSuccessNotice( __( 'Site updated.' ), {
+ type: 'snackbar',
+ } );
+ } catch ( error ) {
+ createErrorNotice( `${ __( 'Saving failed.' ) } ${ error }` );
+ }
+ };
+
return (
- { isDirty && (
-
- { sprintf(
- // translators: %d: number of unsaved changes (number).
- _n(
- '%d unsaved change',
- '%d unsaved changes',
- countUnsavedChanges
- ),
- countUnsavedChanges
- ) }
-
+ { dirtyCurrentEntity ? (
+
+ { label }
+
+ ) : (
+
) }
-
);
}
diff --git a/packages/edit-site/src/components/save-hub/style.scss b/packages/edit-site/src/components/save-hub/style.scss
index 0b8fb9c510f7a9..5daefd70d896ff 100644
--- a/packages/edit-site/src/components/save-hub/style.scss
+++ b/packages/edit-site/src/components/save-hub/style.scss
@@ -1,9 +1,15 @@
.edit-site-save-hub {
color: $gray-600;
+ border-top: 1px solid $gray-800;
+ flex-shrink: 0;
+ margin: 0;
+ padding: $canvas-padding;
}
.edit-site-save-hub__button {
color: inherit;
+ width: 100%;
+ justify-content: center;
&[aria-disabled="true"] {
opacity: 1;
diff --git a/packages/edit-site/src/components/save-panel/index.js b/packages/edit-site/src/components/save-panel/index.js
index fa9516f3599d9b..04f97e2afcf0e5 100644
--- a/packages/edit-site/src/components/save-panel/index.js
+++ b/packages/edit-site/src/components/save-panel/index.js
@@ -7,10 +7,15 @@ import classnames from 'classnames';
* WordPress dependencies
*/
import { Button, Modal } from '@wordpress/components';
-import { EntitiesSavedStates } from '@wordpress/editor';
+import {
+ EntitiesSavedStates,
+ useEntitiesSavedStatesIsDirty,
+ privateApis,
+} from '@wordpress/editor';
import { useDispatch, useSelect } from '@wordpress/data';
-import { __ } from '@wordpress/i18n';
+import { __, sprintf } from '@wordpress/i18n';
import { NavigableRegion } from '@wordpress/interface';
+import { store as coreStore } from '@wordpress/core-data';
/**
* Internal dependencies
@@ -18,6 +23,59 @@ import { NavigableRegion } from '@wordpress/interface';
import { store as editSiteStore } from '../../store';
import { unlock } from '../../private-apis';
import { useActivateTheme } from '../../utils/use-activate-theme';
+import {
+ currentlyPreviewingTheme,
+ isPreviewingTheme,
+} from '../../utils/is-previewing-theme';
+
+const { EntitiesSavedStatesExtensible } = unlock( privateApis );
+
+const EntitiesSavedStatesForPreview = ( { onClose } ) => {
+ const isDirtyProps = useEntitiesSavedStatesIsDirty();
+ let activateSaveLabel;
+ if ( isDirtyProps.isDirty ) {
+ activateSaveLabel = __( 'Activate & Save' );
+ } else {
+ activateSaveLabel = __( 'Activate' );
+ }
+
+ const { getTheme } = useSelect( coreStore );
+ const theme = getTheme( currentlyPreviewingTheme() );
+ const additionalPrompt = (
+
+ { sprintf(
+ 'Saving your changes will change your active theme to %1$s.',
+ theme?.name?.rendered
+ ) }
+
+ );
+
+ const activateTheme = useActivateTheme();
+ const onSave = async ( values ) => {
+ await activateTheme();
+ return values;
+ };
+
+ return (
+
+ );
+};
+
+const _EntitiesSavedStates = ( { onClose } ) => {
+ if ( isPreviewingTheme() ) {
+ return ;
+ }
+ return ;
+};
export default function SavePanel() {
const { isSaveViewOpen, canvasMode } = useSelect( ( select ) => {
@@ -33,18 +91,7 @@ export default function SavePanel() {
};
}, [] );
const { setIsSaveViewOpened } = useDispatch( editSiteStore );
- const activateTheme = useActivateTheme();
const onClose = () => setIsSaveViewOpened( false );
- const onSave = async ( values ) => {
- await activateTheme();
- return values;
- };
-
- const entitySavedStates = window?.__experimentalEnableThemePreviews ? (
-
- ) : (
-
- );
if ( canvasMode === 'view' ) {
return isSaveViewOpen ? (
@@ -56,7 +103,7 @@ export default function SavePanel() {
'Save site, content, and template changes'
) }
>
- { entitySavedStates }
+ <_EntitiesSavedStates onClose={ onClose } />
) : null;
}
@@ -69,7 +116,7 @@ export default function SavePanel() {
ariaLabel={ __( 'Save panel' ) }
>
{ isSaveViewOpen ? (
- entitySavedStates
+ <_EntitiesSavedStates onClose={ onClose } />
) : (
);
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/index.js b/packages/edit-site/src/components/sidebar-edit-mode/index.js
index 5086981f871447..78ada88a4d5fa8 100644
--- a/packages/edit-site/src/components/sidebar-edit-mode/index.js
+++ b/packages/edit-site/src/components/sidebar-edit-mode/index.js
@@ -1,7 +1,7 @@
/**
* WordPress dependencies
*/
-import { createSlotFill, PanelBody, PanelRow } from '@wordpress/components';
+import { createSlotFill } from '@wordpress/components';
import { isRTL, __ } from '@wordpress/i18n';
import { drawerLeft, drawerRight } from '@wordpress/icons';
import { useEffect } from '@wordpress/element';
@@ -16,8 +16,8 @@ import DefaultSidebar from './default-sidebar';
import GlobalStylesSidebar from './global-styles-sidebar';
import { STORE_NAME } from '../../store/constants';
import SettingsHeader from './settings-header';
-import LastRevision from './template-revisions';
-import TemplateCard from './template-card';
+import PagePanels from './page-panels';
+import TemplatePanel from './template-panel';
import PluginTemplateSettingPanel from '../plugin-template-setting-panel';
import { SIDEBAR_BLOCK, SIDEBAR_TEMPLATE } from './constants';
import { store as editSiteStore } from '../../store';
@@ -33,6 +33,7 @@ export function SidebarComplementaryAreaFills() {
isEditorSidebarOpened,
hasBlockSelection,
supportsGlobalStyles,
+ hasPageContentLock,
} = useSelect( ( select ) => {
const _sidebar =
select( interfaceStore ).getActiveComplementaryArea( STORE_NAME );
@@ -47,18 +48,23 @@ export function SidebarComplementaryAreaFills() {
hasBlockSelection:
!! select( blockEditorStore ).getBlockSelectionStart(),
supportsGlobalStyles: ! settings?.supportsTemplatePartsMode,
+ hasPageContentLock: select( editSiteStore ).hasPageContentLock(),
};
}, [] );
const { enableComplementaryArea } = useDispatch( interfaceStore );
useEffect( () => {
- if ( ! isEditorSidebarOpened ) return;
+ // Don't automatically switch tab when the sidebar is closed or when we
+ // are focused on page content.
+ if ( ! isEditorSidebarOpened || hasPageContentLock ) {
+ return;
+ }
if ( hasBlockSelection ) {
enableComplementaryArea( STORE_NAME, SIDEBAR_BLOCK );
} else {
enableComplementaryArea( STORE_NAME, SIDEBAR_TEMPLATE );
}
- }, [ hasBlockSelection, isEditorSidebarOpened ] );
+ }, [ hasBlockSelection, isEditorSidebarOpened, hasPageContentLock ] );
let sidebarName = sidebar;
if ( ! isEditorSidebarOpened ) {
@@ -77,15 +83,11 @@ export function SidebarComplementaryAreaFills() {
>
{ sidebarName === SIDEBAR_TEMPLATE && (
<>
-
-
-
-
-
-
+ { hasPageContentLock ? (
+
+ ) : (
+
+ ) }
>
) }
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/content-blocks-list.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/content-blocks-list.js
new file mode 100644
index 00000000000000..9035c5677f91ac
--- /dev/null
+++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/content-blocks-list.js
@@ -0,0 +1,77 @@
+/**
+ * WordPress dependencies
+ */
+import { useSelect, useDispatch } from '@wordpress/data';
+import {
+ Button,
+ __experimentalVStack as VStack,
+ __experimentalHStack as HStack,
+ FlexItem,
+} from '@wordpress/components';
+import { getBlockType, __experimentalGetBlockLabel } from '@wordpress/blocks';
+import { store as blockEditorStore, BlockIcon } from '@wordpress/block-editor';
+
+/**
+ * Internal dependencies
+ */
+import { CONTENT_BLOCK_TYPES } from '../../page-content-lock/constants';
+
+// TODO: This overlaps a lot with BlockInspectorLockedBlocks in
+// @wordpress/block-editor. DRY them into a single component.
+export default function ContentBlocksList() {
+ const contentBlocks = useSelect( ( select ) => {
+ const {
+ getClientIdsWithDescendants,
+ getBlockName,
+ getBlock,
+ isBlockSelected,
+ hasSelectedInnerBlock,
+ } = select( blockEditorStore );
+ return getClientIdsWithDescendants().flatMap( ( clientId ) => {
+ const blockName = getBlockName( clientId );
+ if ( ! CONTENT_BLOCK_TYPES.includes( blockName ) ) {
+ return [];
+ }
+ return [
+ {
+ block: getBlock( clientId ),
+ isSelected:
+ isBlockSelected( clientId ) ||
+ hasSelectedInnerBlock( clientId, /* deep: */ true ),
+ },
+ ];
+ } );
+ }, [] );
+
+ const { selectBlock } = useDispatch( blockEditorStore );
+
+ if ( ! contentBlocks.length ) {
+ return null;
+ }
+
+ return (
+
+ { contentBlocks.map( ( { block, isSelected } ) => {
+ const blockType = getBlockType( block.name );
+ return (
+ selectBlock( block.clientId ) }
+ >
+
+
+
+ { __experimentalGetBlockLabel(
+ blockType,
+ block.attributes,
+ 'list-view'
+ ) }
+
+
+
+ );
+ } ) }
+
+ );
+}
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js
new file mode 100644
index 00000000000000..c913a689d54dc2
--- /dev/null
+++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js
@@ -0,0 +1,89 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ PanelBody,
+ __experimentalVStack as VStack,
+ Button,
+} from '@wordpress/components';
+import { page as pageIcon } from '@wordpress/icons';
+import { __, sprintf } from '@wordpress/i18n';
+import { humanTimeDiff } from '@wordpress/date';
+import { useSelect, useDispatch } from '@wordpress/data';
+import { useEntityRecord } from '@wordpress/core-data';
+import { BlockContextProvider, BlockPreview } from '@wordpress/block-editor';
+import { useMemo } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { store as editSiteStore } from '../../../store';
+import useEditedEntityRecord from '../../use-edited-entity-record';
+import SidebarCard from '../sidebar-card';
+import ContentBlocksList from './content-blocks-list';
+
+export default function PagePanels() {
+ const context = useSelect(
+ ( select ) => select( editSiteStore ).getEditedPostContext(),
+ []
+ );
+
+ const { hasResolved: hasPageResolved, editedRecord: page } =
+ useEntityRecord( 'postType', context.postType, context.postId );
+
+ const {
+ isLoaded: isTemplateLoaded,
+ getTitle: getTemplateTitle,
+ record: template,
+ } = useEditedEntityRecord();
+
+ const { setHasPageContentLock } = useDispatch( editSiteStore );
+
+ const blockContext = useMemo(
+ () => ( { ...context, postType: null, postId: null } ),
+ [ context ]
+ );
+
+ if ( ! hasPageResolved || ! isTemplateLoaded ) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ { getTemplateTitle() }
+
+
+
+
+
+ setHasPageContentLock( false ) }
+ >
+ { __( 'Edit template' ) }
+
+
+
+ >
+ );
+}
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss
new file mode 100644
index 00000000000000..58178303e48e5b
--- /dev/null
+++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss
@@ -0,0 +1,10 @@
+.edit-site-page-panels__edit-template-preview {
+ border: 1px solid $gray-200;
+ height: 200px;
+ max-height: 200px;
+ overflow: hidden;
+}
+
+.edit-site-page-panels__edit-template-button {
+ justify-content: center;
+}
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js b/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js
index 8e5e80d9fecc57..b11d9acb2314f0 100644
--- a/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js
+++ b/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js
@@ -1,9 +1,14 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+
/**
* WordPress dependencies
*/
import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
-import { useDispatch } from '@wordpress/data';
+import { useSelect, useDispatch } from '@wordpress/data';
import { store as interfaceStore } from '@wordpress/interface';
/**
@@ -11,27 +16,35 @@ import { store as interfaceStore } from '@wordpress/interface';
*/
import { STORE_NAME } from '../../../store/constants';
import { SIDEBAR_BLOCK, SIDEBAR_TEMPLATE } from '../constants';
+import { store as editSiteStore } from '../../../store';
const SettingsHeader = ( { sidebarName } ) => {
+ const hasPageContentLock = useSelect( ( select ) =>
+ select( editSiteStore ).hasPageContentLock()
+ );
+
const { enableComplementaryArea } = useDispatch( interfaceStore );
const openTemplateSettings = () =>
enableComplementaryArea( STORE_NAME, SIDEBAR_TEMPLATE );
const openBlockSettings = () =>
enableComplementaryArea( STORE_NAME, SIDEBAR_BLOCK );
- const [ templateAriaLabel, templateActiveClass ] =
- sidebarName === SIDEBAR_TEMPLATE
- ? // translators: ARIA label for the Template sidebar tab, selected.
- [ __( 'Template (selected)' ), 'is-active' ]
- : // translators: ARIA label for the Template Settings Sidebar tab, not selected.
- [ __( 'Template' ), '' ];
-
- const [ blockAriaLabel, blockActiveClass ] =
- sidebarName === SIDEBAR_BLOCK
- ? // translators: ARIA label for the Block Settings Sidebar tab, selected.
- [ __( 'Block (selected)' ), 'is-active' ]
- : // translators: ARIA label for the Block Settings Sidebar tab, not selected.
- [ __( 'Block' ), '' ];
+ let templateAriaLabel;
+ if ( hasPageContentLock ) {
+ templateAriaLabel =
+ sidebarName === SIDEBAR_TEMPLATE
+ ? // translators: ARIA label for the Template sidebar tab, selected.
+ __( 'Page (selected)' )
+ : // translators: ARIA label for the Template Settings Sidebar tab, not selected.
+ __( 'Page' );
+ } else {
+ templateAriaLabel =
+ sidebarName === SIDEBAR_TEMPLATE
+ ? // translators: ARIA label for the Template sidebar tab, selected.
+ __( 'Template (selected)' )
+ : // translators: ARIA label for the Template Settings Sidebar tab, not selected.
+ __( 'Template' );
+ }
/* Use a list so screen readers will announce how many tabs there are. */
return (
@@ -39,29 +52,39 @@ const SettingsHeader = ( { sidebarName } ) => {
- {
- // translators: Text label for the Template Settings Sidebar tab.
- __( 'Template' )
+ data-label={
+ hasPageContentLock ? __( 'Page' ) : __( 'Template' )
}
+ >
+ { hasPageContentLock ? __( 'Page' ) : __( 'Template' ) }
- {
- // translators: Text label for the Block Settings Sidebar tab.
- __( 'Block' )
- }
+ { __( 'Block' ) }
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/sidebar-card/index.js b/packages/edit-site/src/components/sidebar-edit-mode/sidebar-card/index.js
new file mode 100644
index 00000000000000..04e8d5667a2c20
--- /dev/null
+++ b/packages/edit-site/src/components/sidebar-edit-mode/sidebar-card/index.js
@@ -0,0 +1,34 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+
+/**
+ * WordPress dependencies
+ */
+import { Icon } from '@wordpress/components';
+
+export default function SidebarCard( {
+ className,
+ title,
+ icon,
+ description,
+ actions,
+ children,
+} ) {
+ return (
+
+
+
+
+
{ title }
+ { actions }
+
+
+ { description }
+
+ { children }
+
+
+ );
+}
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/sidebar-card/style.scss b/packages/edit-site/src/components/sidebar-edit-mode/sidebar-card/style.scss
new file mode 100644
index 00000000000000..718fe8fb5a0fbd
--- /dev/null
+++ b/packages/edit-site/src/components/sidebar-edit-mode/sidebar-card/style.scss
@@ -0,0 +1,34 @@
+.edit-site-sidebar-card {
+ display: flex;
+ align-items: flex-start;
+
+ &__content {
+ flex-grow: 1;
+ margin-bottom: $grid-unit-05;
+ }
+
+ &__title {
+ font-weight: 500;
+ line-height: $icon-size;
+ &.edit-site-sidebar-card__title {
+ margin: 0;
+ }
+ }
+
+ &__description {
+ font-size: $default-font-size;
+ }
+
+ &__icon {
+ flex: 0 0 $icon-size;
+ margin-right: $grid-unit-15;
+ width: $icon-size;
+ height: $icon-size;
+ }
+
+ &__header {
+ display: flex;
+ justify-content: space-between;
+ margin: 0 0 $grid-unit-05;
+ }
+}
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-card/index.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js
similarity index 60%
rename from packages/edit-site/src/components/sidebar-edit-mode/template-card/index.js
rename to packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js
index d43dca3b803f52..1c369703be5d72 100644
--- a/packages/edit-site/src/components/sidebar-edit-mode/template-card/index.js
+++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js
@@ -2,10 +2,11 @@
* WordPress dependencies
*/
import { useSelect } from '@wordpress/data';
-import { Icon } from '@wordpress/components';
+import { PanelRow, PanelBody } from '@wordpress/components';
import { store as editorStore } from '@wordpress/editor';
import { store as coreStore } from '@wordpress/core-data';
import { decodeEntities } from '@wordpress/html-entities';
+import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
@@ -13,8 +14,10 @@ import { decodeEntities } from '@wordpress/html-entities';
import { store as editSiteStore } from '../../../store';
import TemplateActions from './template-actions';
import TemplateAreas from './template-areas';
+import LastRevision from './last-revision';
+import SidebarCard from '../sidebar-card';
-export default function TemplateCard() {
+export default function TemplatePanel() {
const {
info: { title, description, icon },
template,
@@ -38,22 +41,22 @@ export default function TemplateCard() {
}
return (
- <>
-
-
-
-
-
- { decodeEntities( title ) }
-
-
-
-
- { decodeEntities( description ) }
-
-
-
-
- >
+
+ }
+ >
+
+
+
+
+
+
);
}
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-revisions/index.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/last-revision.js
similarity index 100%
rename from packages/edit-site/src/components/sidebar-edit-mode/template-revisions/index.js
rename to packages/edit-site/src/components/sidebar-edit-mode/template-panel/last-revision.js
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-card/style.scss b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/style.scss
similarity index 50%
rename from packages/edit-site/src/components/sidebar-edit-mode/template-card/style.scss
rename to packages/edit-site/src/components/sidebar-edit-mode/template-panel/style.scss
index 67054c25d2476c..4c8ef94855dcb1 100644
--- a/packages/edit-site/src/components/sidebar-edit-mode/template-card/style.scss
+++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/style.scss
@@ -1,30 +1,6 @@
.edit-site-template-card {
- display: flex;
- align-items: flex-start;
-
- &__content {
- flex-grow: 1;
- margin-bottom: $grid-unit-05;
- }
-
- &__title {
- font-weight: 500;
- line-height: $icon-size;
- &.edit-site-template-card__title {
- margin: 0;
- }
- }
-
- &__description {
- font-size: $default-font-size;
- margin: 0 0 $grid-unit-20;
- }
-
- &__icon {
- flex: 0 0 $icon-size;
- margin-right: $grid-unit-15;
- width: $icon-size;
- height: $icon-size;
+ &__template-areas {
+ margin-top: $grid-unit-20;
}
&__template-areas-list {
@@ -44,12 +20,6 @@
}
}
- &__header {
- display: flex;
- justify-content: space-between;
- margin: 0 0 $grid-unit-05;
- }
-
&__actions {
line-height: 0;
> .components-button.is-small.has-icon {
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-card/template-actions.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/template-actions.js
similarity index 100%
rename from packages/edit-site/src/components/sidebar-edit-mode/template-card/template-actions.js
rename to packages/edit-site/src/components/sidebar-edit-mode/template-panel/template-actions.js
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-card/template-areas.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/template-areas.js
similarity index 100%
rename from packages/edit-site/src/components/sidebar-edit-mode/template-card/template-areas.js
rename to packages/edit-site/src/components/sidebar-edit-mode/template-panel/template-areas.js
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js
index a7b8add9dd54fe..926f8b8611af0d 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js
@@ -2,11 +2,19 @@
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
-import { edit, seen } from '@wordpress/icons';
+import { backup, edit, seen } from '@wordpress/icons';
import { useSelect, useDispatch } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
-import { __experimentalNavigatorButton as NavigatorButton } from '@wordpress/components';
+import {
+ Icon,
+ __experimentalNavigatorButton as NavigatorButton,
+ __experimentalVStack as HStack,
+ __experimentalVStack as VStack,
+} from '@wordpress/components';
import { useViewportMatch } from '@wordpress/compose';
+import { BlockEditorProvider } from '@wordpress/block-editor';
+import { humanTimeDiff } from '@wordpress/date';
+import { useCallback } from '@wordpress/element';
/**
* Internal dependencies
@@ -18,6 +26,9 @@ import { store as editSiteStore } from '../../store';
import SidebarButton from '../sidebar-button';
import SidebarNavigationItem from '../sidebar-navigation-item';
import StyleBook from '../style-book';
+import useGlobalStylesRevisions from '../global-styles/screen-revisions/use-global-styles-revisions';
+
+const noop = () => {};
export function SidebarNavigationItemGlobalStyles( props ) {
const { openGeneralSidebar } = useDispatch( editSiteStore );
@@ -51,6 +62,84 @@ export function SidebarNavigationItemGlobalStyles( props ) {
);
}
+function SidebarNavigationScreenGlobalStylesContent() {
+ const { storedSettings } = useSelect( ( select ) => {
+ const { getSettings } = unlock( select( editSiteStore ) );
+
+ return {
+ storedSettings: getSettings( false ),
+ };
+ }, [] );
+
+ // Wrap in a BlockEditorProvider to ensure that the Iframe's dependencies are
+ // loaded. This is necessary because the Iframe component waits until
+ // the block editor store's `__internalIsInitialized` is true before
+ // rendering the iframe. Without this, the iframe previews will not render
+ // in mobile viewport sizes, where the editor canvas is hidden.
+ return (
+
+
+
+ );
+}
+
+function SidebarNavigationScreenGlobalStylesFooter( { onClickRevisions } ) {
+ const { revisions, isLoading } = useGlobalStylesRevisions();
+ const { revisionsCount } = useSelect( ( select ) => {
+ const { getEntityRecord, __experimentalGetCurrentGlobalStylesId } =
+ select( coreStore );
+
+ const globalStylesId = __experimentalGetCurrentGlobalStylesId();
+ const globalStyles = globalStylesId
+ ? getEntityRecord( 'root', 'globalStyles', globalStylesId )
+ : undefined;
+
+ return {
+ revisionsCount:
+ globalStyles?._links?.[ 'version-history' ]?.[ 0 ]?.count ?? 0,
+ };
+ }, [] );
+
+ const hasRevisions = revisionsCount >= 2;
+ const modified = revisions?.[ 0 ]?.modified;
+
+ if ( ! hasRevisions || isLoading || ! modified ) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ { __( 'Last modified' ) }
+
+
+
+ { humanTimeDiff( modified ) }
+
+
+
+
+
+
+ );
+}
+
export default function SidebarNavigationScreenGlobalStyles() {
const { openGeneralSidebar } = useDispatch( editSiteStore );
const isMobileViewport = useViewportMatch( 'medium', '<' );
@@ -65,19 +154,31 @@ export default function SidebarNavigationScreenGlobalStyles() {
[]
);
- const openGlobalStyles = async () =>
- Promise.all( [
- setCanvasMode( 'edit' ),
- openGeneralSidebar( 'edit-site/global-styles' ),
- ] );
+ const openGlobalStyles = useCallback(
+ async () =>
+ Promise.all( [
+ setCanvasMode( 'edit' ),
+ openGeneralSidebar( 'edit-site/global-styles' ),
+ ] ),
+ [ setCanvasMode, openGeneralSidebar ]
+ );
- const openStyleBook = async () => {
+ const openStyleBook = useCallback( async () => {
await openGlobalStyles();
// Open the Style Book once the canvas mode is set to edit,
// and the global styles sidebar is open. This ensures that
// the Style Book is not prematurely closed.
setEditorCanvasContainerView( 'style-book' );
- };
+ }, [ openGlobalStyles, setEditorCanvasContainerView ] );
+
+ const openRevisions = useCallback( async () => {
+ await openGlobalStyles();
+ // Open the global styles revisions once the canvas mode is set to edit,
+ // and the global styles sidebar is open. The global styles UI is responsible
+ // for redirecting to the revisions screen once the editor canvas container
+ // has been set to 'global-styles-revisions'.
+ setEditorCanvasContainerView( 'global-styles-revisions' );
+ }, [ openGlobalStyles, setEditorCanvasContainerView ] );
return (
<>
@@ -86,9 +187,14 @@ export default function SidebarNavigationScreenGlobalStyles() {
description={ __(
'Choose a different style combination for the theme styles.'
) }
- content={ }
+ content={ }
+ footer={
+
+ }
actions={
-
+ <>
{ ! isMobileViewport && (
await openGlobalStyles() }
/>
-
+ >
}
/>
{ isStyleBookOpened && ! isMobileViewport && (
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/style.scss b/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/style.scss
new file mode 100644
index 00000000000000..26ac157cb24bef
--- /dev/null
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/style.scss
@@ -0,0 +1,15 @@
+.edit-site-sidebar-navigation-screen-global-styles__footer {
+ padding-left: $grid-unit-15;
+}
+
+.edit-site-sidebar-navigation-screen-global-styles__revisions {
+ border-radius: $radius-block-ui;
+
+ &:not(:hover) {
+ color: $gray-200;
+
+ .edit-site-sidebar-navigation-screen-global-styles__revisions__label {
+ color: $gray-600;
+ }
+ }
+}
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/style.scss b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/style.scss
index e594fa0384e1db..6ecf61df710a47 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/style.scss
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/style.scss
@@ -1,5 +1,5 @@
.edit-site-sidebar-navigation-screen__description {
- margin: 0 0 $grid-unit-40 $grid-unit-20;
+ margin: 0 0 $grid-unit-40 0;
}
.edit-site-sidebar-navigation-screen-navigation-menus__content {
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js
index 5acfc98ffe52c1..176470b3576b84 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js
@@ -1,15 +1,28 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+
/**
* WordPress dependencies
*/
-import { __ } from '@wordpress/i18n';
-import { useDispatch } from '@wordpress/data';
+import { __, sprintf } from '@wordpress/i18n';
+import { useDispatch, useSelect } from '@wordpress/data';
import {
__experimentalUseNavigator as useNavigator,
+ __experimentalVStack as VStack,
ExternalLink,
+ __experimentalTruncate as Truncate,
+ __experimentalHStack as HStack,
+ __experimentalText as Text,
} from '@wordpress/components';
-import { useEntityRecord } from '@wordpress/core-data';
+import { store as coreStore, useEntityRecord } from '@wordpress/core-data';
import { decodeEntities } from '@wordpress/html-entities';
import { pencil } from '@wordpress/icons';
+import { humanTimeDiff } from '@wordpress/date';
+import { createInterpolateElement } from '@wordpress/element';
+import { __unstableStripHTML as stripHTML } from '@wordpress/dom';
+import { escapeAttribute } from '@wordpress/escape-html';
/**
* Internal dependencies
@@ -18,42 +31,142 @@ import SidebarNavigationScreen from '../sidebar-navigation-screen';
import { unlock } from '../../private-apis';
import { store as editSiteStore } from '../../store';
import SidebarButton from '../sidebar-button';
+import SidebarNavigationSubtitle from '../sidebar-navigation-subtitle';
+import PageDetails from './page-details';
+import PageActions from '../page-actions';
export default function SidebarNavigationScreenPage() {
+ const navigator = useNavigator();
const { setCanvasMode } = unlock( useDispatch( editSiteStore ) );
const {
params: { postId },
} = useNavigator();
const { record } = useEntityRecord( 'postType', 'page', postId );
- return (
+ const { featuredMediaAltText, featuredMediaSourceUrl } = useSelect(
+ ( select ) => {
+ const { getEntityRecord } = select( coreStore );
+ // Featured image.
+ const attachedMedia = record?.featured_media
+ ? getEntityRecord(
+ 'postType',
+ 'attachment',
+ record?.featured_media
+ )
+ : null;
+
+ return {
+ featuredMediaSourceUrl:
+ attachedMedia?.media_details.sizes?.medium?.source_url ||
+ attachedMedia?.source_url,
+ featuredMediaAltText: escapeAttribute(
+ attachedMedia?.alt_text ||
+ attachedMedia?.description?.raw ||
+ ''
+ ),
+ };
+ },
+ [ record ]
+ );
+
+ const featureImageAltText = featuredMediaAltText
+ ? decodeEntities( featuredMediaAltText )
+ : decodeEntities( record?.title?.rendered || __( 'Featured image' ) );
+
+ return record ? (
setCanvasMode( 'edit' ) }
- label={ __( 'Edit' ) }
- icon={ pencil }
- />
+ <>
+ {
+ navigator.goTo( '/page' );
+ } }
+ />
+ setCanvasMode( 'edit' ) }
+ label={ __( 'Edit' ) }
+ icon={ pencil }
+ />
+ >
+ }
+ meta={
+
+ { record.link.replace( /^(https?:\/\/)?/, '' ) }
+
}
- description={ __(
- 'Pages are static and are not listed by date. Pages do not use tags or categories.'
- ) }
content={
<>
- { record?.link ? (
-
+
- { record.link }
-
- ) : null }
- { record
- ? decodeEntities( record?.description?.rendered )
- : null }
+ { !! featuredMediaSourceUrl && (
+
+ ) }
+ { ! record?.featured_media && (
+
{ __( 'No featured image' ) }
+ ) }
+
+
+ { !! record?.excerpt?.rendered && (
+
+ { stripHTML( record.excerpt.rendered ) }
+
+ ) }
+
+ { __( 'Details' ) }
+
+
>
}
+ footer={
+ !! record?.modified && (
+
+
+ { __( 'Last modified' ) }
+
+
+ { createInterpolateElement(
+ sprintf(
+ /* translators: %s: is the relative time when the post was last modified. */
+ __( '%s ' ),
+ humanTimeDiff( record.modified )
+ ),
+ {
+ time: ,
+ }
+ ) }
+
+
+ )
+ }
/>
- );
+ ) : null;
}
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-page/page-details.js b/packages/edit-site/src/components/sidebar-navigation-screen-page/page-details.js
new file mode 100644
index 00000000000000..52491245770348
--- /dev/null
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-page/page-details.js
@@ -0,0 +1,163 @@
+/**
+ * WordPress dependencies
+ */
+import { __, _x, sprintf } from '@wordpress/i18n';
+import {
+ __experimentalHStack as HStack,
+ __experimentalText as Text,
+ __experimentalVStack as VStack,
+ __experimentalTruncate as Truncate,
+} from '@wordpress/components';
+import { count as wordCount } from '@wordpress/wordcount';
+import { useSelect } from '@wordpress/data';
+import { decodeEntities } from '@wordpress/html-entities';
+import { store as coreStore, useEntityRecord } from '@wordpress/core-data';
+
+/**
+ * Internal dependencies
+ */
+import StatusLabel from './status-label';
+import { unlock } from '../../private-apis';
+import { store as editSiteStore } from '../../store';
+
+// Taken from packages/editor/src/components/time-to-read/index.js.
+const AVERAGE_READING_RATE = 189;
+
+function getPageDetails( page ) {
+ if ( ! page ) {
+ return [];
+ }
+
+ const details = [
+ {
+ label: __( 'Status' ),
+ value: (
+
+ ),
+ },
+ {
+ label: __( 'Slug' ),
+ value: { page.slug } ,
+ },
+ ];
+
+ if ( page?.templateTitle ) {
+ details.push( {
+ label: __( 'Template' ),
+ value: decodeEntities( page.templateTitle ),
+ } );
+ }
+
+ details.push( {
+ label: __( 'Parent' ),
+ // `null` indicates no parent.
+ value:
+ null === page?.parentTitle
+ ? __( 'Top level' )
+ : decodeEntities( page?.parentTitle || __( '(no title)' ) ),
+ } );
+
+ /*
+ * translators: If your word count is based on single characters (e.g. East Asian characters),
+ * enter 'characters_excluding_spaces' or 'characters_including_spaces'. Otherwise, enter 'words'.
+ * Do not translate into your own language.
+ */
+ const wordCountType = _x( 'words', 'Word count type. Do not translate!' );
+ const wordsCounted = page?.content?.rendered
+ ? wordCount( page.content.rendered, wordCountType )
+ : 0;
+ const readingTime = Math.round( wordsCounted / AVERAGE_READING_RATE );
+
+ if ( wordsCounted ) {
+ details.push(
+ {
+ label: __( 'Words' ),
+ value: wordsCounted.toLocaleString() || __( 'Unknown' ),
+ },
+ {
+ label: __( 'Time to read' ),
+ value:
+ readingTime > 1
+ ? sprintf(
+ /* translators: %s: is the number of minutes. */
+ __( '%s mins' ),
+ readingTime.toLocaleString()
+ )
+ : __( '< 1 min' ),
+ }
+ );
+ }
+ return details;
+}
+
+export default function PageDetails( { id } ) {
+ const { record } = useEntityRecord( 'postType', 'page', id );
+
+ const { parentTitle, templateTitle } = useSelect(
+ ( select ) => {
+ const { getEditedPostContext, getSettings } = unlock(
+ select( editSiteStore )
+ );
+ const defaultTemplateTypes = getSettings()?.defaultTemplateTypes;
+ const postContext = getEditedPostContext();
+
+ // Template title.
+ const templateSlug =
+ // Checks that the post type matches the current theme's post type, otherwise
+ // the templateSlug returns 'home'.
+ postContext?.postType === 'page'
+ ? postContext?.templateSlug
+ : null;
+ const _templateTitle =
+ defaultTemplateTypes && templateSlug
+ ? defaultTemplateTypes.find(
+ ( template ) => template.slug === templateSlug
+ )?.title
+ : null;
+
+ // Parent page title.
+ const _parentTitle = record?.parent
+ ? select( coreStore ).getEntityRecord(
+ 'postType',
+ 'page',
+ record.parent,
+ {
+ _fields: [ 'title' ],
+ }
+ )?.title?.rendered
+ : null;
+
+ return {
+ parentTitle: _parentTitle,
+ templateTitle: _templateTitle,
+ };
+ },
+ [ record ]
+ );
+ return (
+
+ { getPageDetails( {
+ parentTitle,
+ templateTitle,
+ ...record,
+ } ).map( ( { label, value } ) => (
+
+
+ { label }
+
+
+ { value }
+
+
+ ) ) }
+
+ );
+}
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-page/status-label.js b/packages/edit-site/src/components/sidebar-navigation-screen-page/status-label.js
new file mode 100644
index 00000000000000..208d8adc59ab93
--- /dev/null
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-page/status-label.js
@@ -0,0 +1,101 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+
+/**
+ * WordPress dependencies
+ */
+import { __, sprintf } from '@wordpress/i18n';
+import { dateI18n, getDate, getSettings, humanTimeDiff } from '@wordpress/date';
+import { createInterpolateElement } from '@wordpress/element';
+import { Path, SVG } from '@wordpress/primitives';
+
+const publishedIcon = (
+
+
+
+);
+
+const draftIcon = (
+
+
+
+);
+
+const pendingIcon = (
+
+
+
+);
+
+export default function StatusLabel( { status, date } ) {
+ const relateToNow = humanTimeDiff( date );
+ let statusLabel = '';
+ let statusIcon = pendingIcon;
+ switch ( status ) {
+ case 'publish':
+ statusLabel = createInterpolateElement(
+ sprintf(
+ /* translators: %s: is the relative time when the post was published. */
+ __( 'Published %s ' ),
+ relateToNow
+ ),
+ { time: }
+ );
+ statusIcon = publishedIcon;
+ break;
+ case 'future':
+ const formattedDate = dateI18n(
+ getSettings().formats.date,
+ getDate( date )
+ );
+ statusLabel = createInterpolateElement(
+ sprintf(
+ /* translators: %s: is the formatted date and time on which the post is scheduled to be published. */
+ __( 'Scheduled for %s ' ),
+ formattedDate
+ ),
+ { time: }
+ );
+ break;
+ case 'draft':
+ statusLabel = __( 'Draft' );
+ statusIcon = draftIcon;
+ break;
+ case 'pending':
+ statusLabel = __( 'Pending' );
+ break;
+ case 'private':
+ statusLabel = __( 'Private' );
+ break;
+ case 'protected':
+ statusLabel = __( 'Password protected' );
+ break;
+ }
+
+ return (
+
+ { statusIcon } { statusLabel }
+
+ );
+}
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-page/style.scss b/packages/edit-site/src/components/sidebar-navigation-screen-page/style.scss
new file mode 100644
index 00000000000000..c58e9f09392465
--- /dev/null
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-page/style.scss
@@ -0,0 +1,80 @@
+.edit-site-sidebar-navigation-screen-page__featured-image-wrapper {
+ background-color: $gray-800;
+ margin-bottom: $grid-unit-20;
+ min-height: 128px;
+ border-radius: $grid-unit-05;
+}
+
+.edit-site-sidebar-navigation-screen-page__featured-image {
+ border-radius: 2px;
+ height: 128px;
+ overflow: hidden;
+ width: 100%;
+ background-size: cover;
+ background-position: 50% 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: $gray-600;
+ img {
+ object-fit: cover;
+ height: 100%;
+ width: 100%;
+ object-position: 50% 50%;
+ }
+}
+
+.edit-site-sidebar-navigation-screen-page__featured-image-description {
+ font-size: $helptext-font-size;
+}
+
+.edit-site-sidebar-navigation-screen-page__excerpt {
+ font-size: $helptext-font-size;
+}
+
+.edit-site-sidebar-navigation-screen-page__modified {
+ margin: 0 0 $grid-unit-20 0;
+ color: $gray-600;
+ margin-left: $grid-unit-20;
+ .components-text {
+ color: $gray-600;
+ }
+}
+
+.edit-site-sidebar-navigation-screen-page__status {
+ display: inline-flex;
+
+ time {
+ display: contents;
+ }
+
+ svg {
+ height: 16px;
+ width: 16px;
+ margin-right: $grid-unit-10;
+ fill: $alert-yellow;
+ }
+
+ &.has-publish-status svg,
+ &.has-future-status svg {
+ fill: $alert-green;
+ }
+}
+
+.edit-site-sidebar-navigation-screen-page__footer {
+ padding-top: $grid-unit-10;
+ padding-bottom: $grid-unit-10;
+ padding-left: $grid-unit-20;
+}
+
+.edit-site-sidebar-navigation-screen-page__details {
+ .edit-site-sidebar-navigation-screen-page__details-label {
+ color: $gray-600;
+ width: 100px;
+ }
+
+ .edit-site-sidebar-navigation-screen-page__details-value.edit-site-sidebar-navigation-screen-page__details-value {
+ color: $gray-200;
+ }
+}
+
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-pages/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-pages/index.js
index 5f4958f44d2514..25979fcd4a036b 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-pages/index.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-pages/index.js
@@ -4,10 +4,16 @@
import {
__experimentalItemGroup as ItemGroup,
__experimentalItem as Item,
+ __experimentalTruncate as Truncate,
+ __experimentalVStack as VStack,
} from '@wordpress/components';
+import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
-import { useEntityRecords } from '@wordpress/core-data';
+import { useEntityRecords, store as coreStore } from '@wordpress/core-data';
import { decodeEntities } from '@wordpress/html-entities';
+import { privateApis as routerPrivateApis } from '@wordpress/router';
+import { layout, page, home, loop, plus } from '@wordpress/icons';
+import { useSelect } from '@wordpress/data';
/**
* Internal dependencies
@@ -15,68 +21,198 @@ import { decodeEntities } from '@wordpress/html-entities';
import SidebarNavigationScreen from '../sidebar-navigation-screen';
import { useLink } from '../routes/link';
import SidebarNavigationItem from '../sidebar-navigation-item';
-import SidebarNavigationSubtitle from '../sidebar-navigation-subtitle';
+import SidebarButton from '../sidebar-button';
+import AddNewPageModal from '../add-new-page';
+import { unlock } from '../../private-apis';
-const PageItem = ( { postId, ...props } ) => {
+const { useHistory } = unlock( routerPrivateApis );
+
+const PageItem = ( { postType = 'page', postId, ...props } ) => {
const linkInfo = useLink( {
- postType: 'page',
+ postType,
postId,
} );
return ;
};
export default function SidebarNavigationScreenPages() {
- const { records: pages, isResolving: isLoading } = useEntityRecords(
+ const { records: pages, isResolving: isLoadingPages } = useEntityRecords(
'postType',
- 'page'
+ 'page',
+ {
+ status: 'any',
+ per_page: -1,
+ }
+ );
+ const { records: templates, isResolving: isLoadingTemplates } =
+ useEntityRecords( 'postType', 'wp_template', {
+ per_page: -1,
+ } );
+
+ const dynamicPageTemplates = templates?.filter( ( { slug } ) =>
+ [ '404', 'search' ].includes( slug )
);
+ const homeTemplate =
+ templates?.find( ( template ) => template.slug === 'front-page' ) ||
+ templates?.find( ( template ) => template.slug === 'home' ) ||
+ templates?.find( ( template ) => template.slug === 'index' );
+
+ const pagesAndTemplates = pages?.concat( dynamicPageTemplates, [
+ homeTemplate,
+ ] );
+
+ const { frontPage, postsPage } = useSelect( ( select ) => {
+ const { getEntityRecord } = select( coreStore );
+
+ const siteSettings = getEntityRecord( 'root', 'site' );
+ return {
+ frontPage: siteSettings?.page_on_front,
+ postsPage: siteSettings?.page_for_posts,
+ };
+ }, [] );
+
+ const isHomePageBlog = frontPage === postsPage;
+
+ const reorderedPages = pages && [ ...pages ];
+
+ if ( ! isHomePageBlog && reorderedPages?.length ) {
+ const homePageIndex = reorderedPages.findIndex(
+ ( item ) => item.id === frontPage
+ );
+ const homePage = reorderedPages.splice( homePageIndex, 1 );
+ reorderedPages?.splice( 0, 0, ...homePage );
+
+ const postsPageIndex = reorderedPages.findIndex(
+ ( item ) => item.id === postsPage
+ );
+
+ const blogPage = reorderedPages.splice( postsPageIndex, 1 );
+
+ reorderedPages.splice( 1, 0, ...blogPage );
+ }
+
+ const [ showAddPage, setShowAddPage ] = useState( false );
+
+ const history = useHistory();
+
+ const handleNewPage = ( { type, id } ) => {
+ // Navigate to the created template editor.
+ history.push( {
+ postId: id,
+ postType: type,
+ canvas: 'edit',
+ } );
+ setShowAddPage( false );
+ };
+
return (
-
- { isLoading && (
-
- - { __( 'Loading pages' ) }
-
- ) }
- { ! isLoading && (
- <>
-
- { __( 'Recent' ) }
-
+ <>
+ { showAddPage && (
+ setShowAddPage( false ) }
+ />
+ ) }
+ setShowAddPage( true ) }
+ />
+ }
+ content={
+ <>
+ { ( isLoadingPages || isLoadingTemplates ) && (
+
+ - { __( 'Loading pages' ) }
+
+ ) }
+ { ! ( isLoadingPages || isLoadingTemplates ) && (
- { ! pages?.length && (
+ { ! pagesAndTemplates?.length && (
- { __( 'No page found' ) }
) }
- { pages?.map( ( page ) => (
+ { isHomePageBlog && homeTemplate && (
- { decodeEntities(
- page.title?.rendered
- ) ?? __( '(no title)' ) }
+
+ { decodeEntities(
+ homeTemplate.title?.rendered ||
+ __( '(no title)' )
+ ) }
+
- ) ) }
- {
- document.location =
- 'edit.php?post_type=page';
- } }
- >
- { __( 'Manage all pages' ) }
-
+ ) }
+ { reorderedPages?.map( ( item ) => {
+ let itemIcon;
+ switch ( item.id ) {
+ case frontPage:
+ itemIcon = home;
+ break;
+ case postsPage:
+ itemIcon = loop;
+ break;
+ default:
+ itemIcon = page;
+ }
+ return (
+
+
+ { decodeEntities(
+ item?.title?.rendered ||
+ __( '(no title)' )
+ ) }
+
+
+ );
+ } ) }
+
+ { dynamicPageTemplates?.map( ( item ) => (
+
+
+ { decodeEntities(
+ item.title?.rendered ||
+ __( '(no title)' )
+ ) }
+
+
+ ) ) }
+ {
+ document.location =
+ 'edit.php?post_type=page';
+ } }
+ >
+ { __( 'Manage all pages' ) }
+
+
- >
- ) }
- >
- }
- />
+ ) }
+ >
+ }
+ />
+ >
);
}
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-pages/style.scss b/packages/edit-site/src/components/sidebar-navigation-screen-pages/style.scss
deleted file mode 100644
index 7bbdd103b6bcea..00000000000000
--- a/packages/edit-site/src/components/sidebar-navigation-screen-pages/style.scss
+++ /dev/null
@@ -1,4 +0,0 @@
-.edit-site-sidebar-navigation-screen-pages__see-all {
- /* Overrides the margin that comes from the Item component */
- margin-top: $grid-unit-20 !important;
-}
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-template-part/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-template-part/index.js
new file mode 100644
index 00000000000000..1823b97594f522
--- /dev/null
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-template-part/index.js
@@ -0,0 +1,103 @@
+/**
+ * WordPress dependencies
+ */
+import { __, sprintf, _x } from '@wordpress/i18n';
+import { useDispatch, useSelect } from '@wordpress/data';
+import { pencil } from '@wordpress/icons';
+import {
+ __experimentalUseNavigator as useNavigator,
+ Icon,
+} from '@wordpress/components';
+import { store as coreStore } from '@wordpress/core-data';
+
+/**
+ * Internal dependencies
+ */
+import SidebarNavigationScreen from '../sidebar-navigation-screen';
+import useEditedEntityRecord from '../use-edited-entity-record';
+import { unlock } from '../../private-apis';
+import { store as editSiteStore } from '../../store';
+import SidebarButton from '../sidebar-button';
+import { useAddedBy } from '../list/added-by';
+
+function useTemplateTitleAndDescription( postType, postId ) {
+ const { getDescription, getTitle, record } = useEditedEntityRecord(
+ postType,
+ postId
+ );
+ const currentTheme = useSelect(
+ ( select ) => select( coreStore ).getCurrentTheme(),
+ []
+ );
+ const addedBy = useAddedBy( postType, postId );
+ const isAddedByActiveTheme =
+ addedBy.type === 'theme' && record.theme === currentTheme?.stylesheet;
+ const title = getTitle();
+ let descriptionText = getDescription();
+
+ if ( ! descriptionText && addedBy.text ) {
+ descriptionText = sprintf(
+ // translators: %s: template part title e.g: "Header".
+ __( 'This is your %s template part.' ),
+ getTitle()
+ );
+ }
+
+ const description = (
+ <>
+ { descriptionText }
+
+ { addedBy.text && ! isAddedByActiveTheme && (
+
+
+
+ { addedBy.imageUrl ? (
+
+ ) : (
+
+ ) }
+
+ { addedBy.text }
+
+
+ { addedBy.isCustomized && (
+
+ { _x( '(Customized)', 'template part' ) }
+
+ ) }
+
+ ) }
+ >
+ );
+
+ return { title, description };
+}
+
+export default function SidebarNavigationScreenTemplatePart() {
+ const { params } = useNavigator();
+ const { postType, postId } = params;
+ const { setCanvasMode } = unlock( useDispatch( editSiteStore ) );
+ const { title, description } = useTemplateTitleAndDescription(
+ postType,
+ postId
+ );
+
+ return (
+ setCanvasMode( 'edit' ) }
+ label={ __( 'Edit' ) }
+ icon={ pencil }
+ />
+ }
+ description={ description }
+ />
+ );
+}
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-template/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-template/index.js
index 99139b55d87a5c..03cc7ecbec75bf 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-template/index.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-template/index.js
@@ -1,7 +1,7 @@
/**
* WordPress dependencies
*/
-import { __, sprintf, _x } from '@wordpress/i18n';
+import { __, _x } from '@wordpress/i18n';
import { useDispatch, useSelect } from '@wordpress/data';
import { pencil } from '@wordpress/icons';
import {
@@ -19,6 +19,7 @@ import { unlock } from '../../private-apis';
import { store as editSiteStore } from '../../store';
import SidebarButton from '../sidebar-button';
import { useAddedBy } from '../list/added-by';
+import TemplateActions from '../template-actions';
function useTemplateTitleAndDescription( postType, postId ) {
const { getDescription, getTitle, record } = useEditedEntityRecord(
@@ -36,17 +37,9 @@ function useTemplateTitleAndDescription( postType, postId ) {
let descriptionText = getDescription();
if ( ! descriptionText && addedBy.text ) {
- if ( record.type === 'wp_template' && record.is_custom ) {
- descriptionText = __(
- 'This is a custom template that can be applied manually to any Post or Page.'
- );
- } else if ( record.type === 'wp_template_part' ) {
- descriptionText = sprintf(
- // translators: %s: template part title e.g: "Header".
- __( 'This is your %s template part.' ),
- getTitle()
- );
- }
+ descriptionText = __(
+ 'This is a custom template that can be applied manually to any Post or Page.'
+ );
}
const description = (
@@ -73,9 +66,7 @@ function useTemplateTitleAndDescription( postType, postId ) {
{ addedBy.isCustomized && (
- { postType === 'wp_template'
- ? _x( '(Customized)', 'template' )
- : _x( '(Customized)', 'template part' ) }
+ { _x( '(Customized)', 'template' ) }
) }
@@ -87,8 +78,10 @@ function useTemplateTitleAndDescription( postType, postId ) {
}
export default function SidebarNavigationScreenTemplate() {
- const { params } = useNavigator();
- const { postType, postId } = params;
+ const navigator = useNavigator();
+ const {
+ params: { postType, postId },
+ } = navigator;
const { setCanvasMode } = unlock( useDispatch( editSiteStore ) );
const { title, description } = useTemplateTitleAndDescription(
postType,
@@ -99,11 +92,21 @@ export default function SidebarNavigationScreenTemplate() {
setCanvasMode( 'edit' ) }
- label={ __( 'Edit' ) }
- icon={ pencil }
- />
+ <>
+ {
+ navigator.goTo( `/${ postType }/all` );
+ } }
+ />
+ setCanvasMode( 'edit' ) }
+ label={ __( 'Edit' ) }
+ icon={ pencil }
+ />
+ >
}
description={ description }
/>
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-templates/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-templates/index.js
index c689ae063b15b5..fc0caffc213353 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-templates/index.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-templates/index.js
@@ -43,7 +43,7 @@ const config = {
manage: __( 'Manage all template parts' ),
reusableBlocks: __( 'Manage reusable blocks' ),
description: __(
- 'Manage what patterns are available when editing your site.'
+ 'Template Parts are small pieces of a layout that can be reused across multiple templates and always appear the same way. Common template parts include the site header, footer, or sidebar.'
),
},
sortCallback: ( items ) => {
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen/index.js b/packages/edit-site/src/components/sidebar-navigation-screen/index.js
index 77b9bedba3e655..ecdd6f712ac08e 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen/index.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen/index.js
@@ -27,7 +27,9 @@ export default function SidebarNavigationScreen( {
isRoot,
title,
actions,
+ meta,
content,
+ footer,
description,
} ) {
const { dashboardLink } = useSelect( ( select ) => {
@@ -40,58 +42,80 @@ export default function SidebarNavigationScreen( {
const theme = getTheme( currentlyPreviewingTheme() );
return (
-
-
+
- { ! isRoot ? (
-
- ) : (
-
- ) }
-
- { ! isPreviewingTheme()
- ? title
- : sprintf(
- 'Previewing %1$s: %2$s',
- theme?.name?.rendered,
- title
- ) }
-
- { actions }
-
-
-
- { description && (
-
- { description }
-
+ { ! isRoot ? (
+
+ ) : (
+
+ ) }
+
+ { ! isPreviewingTheme()
+ ? title
+ : sprintf(
+ 'Previewing %1$s: %2$s',
+ theme?.name?.rendered,
+ title
+ ) }
+
+ { actions && (
+
+ { actions }
+
+ ) }
+
+ { meta && (
+ <>
+
+ { meta }
+
+ >
) }
- { content }
-
-
+
+
+ { description && (
+
+ { description }
+
+ ) }
+ { content }
+
+
+ { footer && (
+
+ ) }
+ >
);
}
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen/style.scss b/packages/edit-site/src/components/sidebar-navigation-screen/style.scss
index b72edaba63f7df..a4aa61ba811417 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen/style.scss
+++ b/packages/edit-site/src/components/sidebar-navigation-screen/style.scss
@@ -5,10 +5,31 @@
position: relative;
}
+.edit-site-sidebar-navigation-screen__main {
+ // Ensure the sidebar is always at least as tall as the viewport.
+ // This allows the footer section to be sticky at the bottom of the viewport.
+ flex-grow: 1;
+}
+
.edit-site-sidebar-navigation-screen__content {
+ color: $gray-400;
+ padding: 0 $grid-unit-20;
+ .components-item-group {
+ margin-left: -$grid-unit-20;
+ margin-right: -$grid-unit-20;
+ }
+ .components-text {
+ color: $gray-400;
+ }
+}
+
+.edit-site-sidebar-navigation-screen__meta {
margin: 0 0 $grid-unit-20 0;
- color: $gray-600;
- //z-index: z-index(".edit-site-sidebar-navigation-screen__content");
+ color: $gray-400;
+ margin-left: $grid-unit-20;
+ .components-text {
+ color: $gray-400;
+ }
}
.edit-site-sidebar-navigation-screen__page-link {
@@ -22,7 +43,6 @@
.components-external-link__icon {
margin-left: $grid-unit-05;
}
- margin-left: $grid-unit-20;
display: inline-block;
}
@@ -42,8 +62,11 @@
padding: $grid-unit-15 * 0.5 0 0 0;
}
+.edit-site-sidebar-navigation-screen__actions {
+ flex-shrink: 0;
+}
+
.edit-site-sidebar-navigation-screen__content .edit-site-global-styles-style-variations-container {
- margin-left: $grid-unit-20;
.edit-site-global-styles-variations_item-preview {
border: $gray-900 $border-width solid;
}
@@ -58,3 +81,21 @@
border: var(--wp-admin-theme-color) var(--wp-admin-border-width-focus) solid;
}
}
+
+.edit-site-sidebar-navigation-screen__sticky-section.edit-site-sidebar-navigation-screen__sticky-section {
+ position: sticky;
+ bottom: 0;
+ background-color: $gray-900;
+ gap: 0;
+ padding: $grid-unit-20 0;
+ margin: $grid-unit-20 0 0;
+ border-top: 1px solid $gray-800;
+ box-shadow: 0 #{-$grid-unit-10} $grid-unit-20 $gray-900;
+}
+
+.edit-site-sidebar-navigation-screen__footer {
+ position: sticky;
+ bottom: 0;
+ background-color: $gray-900;
+ padding: $grid-unit-20 0;
+}
diff --git a/packages/edit-site/src/components/sidebar-navigation-subtitle/style.scss b/packages/edit-site/src/components/sidebar-navigation-subtitle/style.scss
index 735145ca1d80ce..b7ff9faba49f38 100644
--- a/packages/edit-site/src/components/sidebar-navigation-subtitle/style.scss
+++ b/packages/edit-site/src/components/sidebar-navigation-subtitle/style.scss
@@ -1,7 +1,7 @@
.edit-site-sidebar-navigation-subtitle {
- color: $gray-100;
+ color: $gray-400;
text-transform: uppercase;
font-weight: 500;
font-size: 11px;
- padding: $grid-unit-20 0 0 $grid-unit-20;
+ padding: $grid-unit-10 0;
}
diff --git a/packages/edit-site/src/components/sidebar/index.js b/packages/edit-site/src/components/sidebar/index.js
index 98fb77139090ee..c28a93d523dce8 100644
--- a/packages/edit-site/src/components/sidebar/index.js
+++ b/packages/edit-site/src/components/sidebar/index.js
@@ -14,6 +14,7 @@ import { privateApis as routerPrivateApis } from '@wordpress/router';
import SidebarNavigationScreenMain from '../sidebar-navigation-screen-main';
import SidebarNavigationScreenTemplates from '../sidebar-navigation-screen-templates';
import SidebarNavigationScreenTemplate from '../sidebar-navigation-screen-template';
+import SidebarNavigationScreenTemplatePart from '../sidebar-navigation-screen-template-part';
import useSyncPathWithURL, {
getPathFromURL,
} from '../sync-state-with-url/use-sync-path-with-url';
@@ -57,9 +58,12 @@ function SidebarScreens() {
-
+
+
+
+
>
);
}
@@ -76,9 +80,7 @@ function Sidebar() {
>
-
-
-
+
>
);
}
diff --git a/packages/edit-site/src/components/sidebar/style.scss b/packages/edit-site/src/components/sidebar/style.scss
index 08f6da48da6667..9a3644cc830d56 100644
--- a/packages/edit-site/src/components/sidebar/style.scss
+++ b/packages/edit-site/src/components/sidebar/style.scss
@@ -5,6 +5,9 @@
.components-navigator-screen {
@include custom-scrollbars-on-hover(transparent, $gray-700);
scrollbar-gutter: stable;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
}
}
@@ -15,10 +18,6 @@
padding: $canvas-padding 0;
}
-.edit-site-sidebar__content.edit-site-sidebar__content {
- overflow-x: unset;
-}
-
.edit-site-sidebar__content > div {
// This matches the logo padding
padding: 0 $grid-unit-15;
diff --git a/packages/edit-site/src/components/site-hub/index.js b/packages/edit-site/src/components/site-hub/index.js
index b477c5b14f1622..dd184286f7d9b3 100644
--- a/packages/edit-site/src/components/site-hub/index.js
+++ b/packages/edit-site/src/components/site-hub/index.js
@@ -19,8 +19,8 @@ import { store as blockEditorStore } from '@wordpress/block-editor';
import { store as coreStore } from '@wordpress/core-data';
import { decodeEntities } from '@wordpress/html-entities';
import { forwardRef } from '@wordpress/element';
-import { search } from '@wordpress/icons';
-import { privateApis as commandsPrivateApis } from '@wordpress/commands';
+import { search, external } from '@wordpress/icons';
+import { store as commandsStore } from '@wordpress/commands';
/**
* Internal dependencies
@@ -29,20 +29,23 @@ import { store as editSiteStore } from '../../store';
import SiteIcon from '../site-icon';
import { unlock } from '../../private-apis';
-const { store: commandsStore } = unlock( commandsPrivateApis );
-
const HUB_ANIMATION_DURATION = 0.3;
const SiteHub = forwardRef( ( props, ref ) => {
- const { canvasMode, dashboardLink } = useSelect( ( select ) => {
+ const { canvasMode, dashboardLink, homeUrl } = useSelect( ( select ) => {
const { getCanvasMode, getSettings } = unlock(
select( editSiteStore )
);
+ const {
+ getUnstableBase, // Site index.
+ } = select( coreStore );
+
return {
canvasMode: getCanvasMode(),
dashboardLink:
getSettings().__experimentalDashboardLink || 'index.php',
+ homeUrl: getUnstableBase()?.home,
};
}, [] );
const { open: openCommandCenter } = useDispatch( commandsStore );
@@ -87,7 +90,11 @@ const SiteHub = forwardRef( ( props, ref ) => {
ease: 'easeOut',
} }
>
-
+
{
{ decodeEntities( siteTitle ) }
+
{ canvasMode === 'view' && (
+ select( coreStore ).getEntityRecord( 'postType', postType, postId ),
+ [ postType, postId ]
+ );
const { removeTemplate, revertTemplate } = useDispatch( editSiteStore );
const { saveEditedEntityRecord } = useDispatch( coreStore );
const { createSuccessNotice, createErrorNotice } =
@@ -62,7 +73,8 @@ export default function Actions( { template } ) {
{ ( { onClose } ) => (
@@ -77,6 +89,7 @@ export default function Actions( { template } ) {
isTertiary
onClick={ () => {
removeTemplate( template );
+ onRemove?.();
onClose();
} }
>
diff --git a/packages/edit-site/src/components/list/actions/rename-menu-item.js b/packages/edit-site/src/components/template-actions/rename-menu-item.js
similarity index 100%
rename from packages/edit-site/src/components/list/actions/rename-menu-item.js
rename to packages/edit-site/src/components/template-actions/rename-menu-item.js
diff --git a/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js b/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js
index 5ee0efcd6fa7c3..11a765b9807d66 100644
--- a/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js
+++ b/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js
@@ -29,7 +29,9 @@ import { store as noticesStore } from '@wordpress/notices';
import { useSupportedStyles } from '../../components/global-styles/hooks';
import { unlock } from '../../private-apis';
-const { GlobalStylesContext } = unlock( blockEditorPrivateApis );
+const { GlobalStylesContext, useBlockEditingMode } = unlock(
+ blockEditorPrivateApis
+);
// TODO: Temporary duplication of constant in @wordpress/block-editor. Can be
// removed by moving PushChangesToGlobalStylesControl to
@@ -208,15 +210,19 @@ function PushChangesToGlobalStylesControl( {
}
const withPushChangesToGlobalStyles = createHigherOrderComponent(
- ( BlockEdit ) => ( props ) =>
- (
+ ( BlockEdit ) => ( props ) => {
+ const blockEditingMode = useBlockEditingMode();
+ return (
<>
-
-
-
+ { blockEditingMode === 'default' && (
+
+
+
+ ) }
>
- )
+ );
+ }
);
addFilter(
diff --git a/packages/edit-site/src/store/actions.js b/packages/edit-site/src/store/actions.js
index d1f41d5a62aca5..67fbec4811db44 100644
--- a/packages/edit-site/src/store/actions.js
+++ b/packages/edit-site/src/store/actions.js
@@ -12,6 +12,7 @@ import { store as interfaceStore } from '@wordpress/interface';
import { store as blockEditorStore } from '@wordpress/block-editor';
import { speak } from '@wordpress/a11y';
import { store as preferencesStore } from '@wordpress/preferences';
+import { decodeEntities } from '@wordpress/html-entities';
/**
* Internal dependencies
@@ -144,7 +145,7 @@ export const removeTemplate =
sprintf(
/* translators: The template/part's name. */
__( '"%s" deleted.' ),
- template.title.rendered
+ decodeEntities( template.title.rendered )
),
{ type: 'snackbar', id: 'site-editor-template-deleted-success' }
);
@@ -529,3 +530,21 @@ export const switchEditorMode =
speak( __( 'Code editor selected' ), 'assertive' );
}
};
+
+/**
+ * Sets whether or not the editor is locked so that only page content can be
+ * edited.
+ *
+ * @param {boolean} hasPageContentLock True to enable lock, false to disable.
+ */
+export const setHasPageContentLock =
+ ( hasPageContentLock ) =>
+ ( { dispatch, registry } ) => {
+ if ( hasPageContentLock ) {
+ registry.dispatch( blockEditorStore ).clearSelectedBlock();
+ }
+ dispatch( {
+ type: 'SET_HAS_PAGE_CONTENT_LOCK',
+ hasPageContentLock,
+ } );
+ };
diff --git a/packages/edit-site/src/store/reducer.js b/packages/edit-site/src/store/reducer.js
index a46d215f905074..a003ee958894e5 100644
--- a/packages/edit-site/src/store/reducer.js
+++ b/packages/edit-site/src/store/reducer.js
@@ -157,6 +157,25 @@ function editorCanvasContainerView( state = undefined, action ) {
return state;
}
+/**
+ * Reducer used to track whether the page content is locked.
+ *
+ * @param {boolean} state Current state.
+ * @param {Object} action Dispatched action.
+ *
+ * @return {boolean} Updated state.
+ */
+export function hasPageContentLock( state = false, action ) {
+ switch ( action.type ) {
+ case 'SET_EDITED_POST':
+ return !! action.context?.postId;
+ case 'SET_HAS_PAGE_CONTENT_LOCK':
+ return action.hasPageContentLock;
+ }
+
+ return state;
+}
+
export default combineReducers( {
deviceType,
settings,
@@ -166,4 +185,5 @@ export default combineReducers( {
saveViewPanel,
canvasMode,
editorCanvasContainerView,
+ hasPageContentLock,
} );
diff --git a/packages/edit-site/src/store/selectors.js b/packages/edit-site/src/store/selectors.js
index 583f37b55241bd..16b6dc588ea26f 100644
--- a/packages/edit-site/src/store/selectors.js
+++ b/packages/edit-site/src/store/selectors.js
@@ -321,3 +321,27 @@ export function isNavigationOpened() {
version: '6.4',
} );
}
+
+/**
+ * Whether or not the editor has a page loaded into it.
+ *
+ * @see setPage
+ *
+ * @param {Object} state Global application state.
+ *
+ * @return {boolean} Whether or not the editor has a page loaded into it.
+ */
+export function isPage( state ) {
+ return !! state.editedPost.context?.postId;
+}
+
+/**
+ * Whether or not the editor is locked so that only page content can be edited.
+ *
+ * @param {Object} state Global application state.
+ *
+ * @return {boolean} Whether or not the editor is locked.
+ */
+export function hasPageContentLock( state ) {
+ return isPage( state ) ? state.hasPageContentLock : false;
+}
diff --git a/packages/edit-site/src/store/test/actions.js b/packages/edit-site/src/store/test/actions.js
index 2df1cc72b66115..cca479e2776626 100644
--- a/packages/edit-site/src/store/test/actions.js
+++ b/packages/edit-site/src/store/test/actions.js
@@ -13,6 +13,7 @@ import { store as preferencesStore } from '@wordpress/preferences';
* Internal dependencies
*/
import { store as editSiteStore } from '..';
+import { setHasPageContentLock } from '../actions';
const ENTITY_TYPES = {
wp_template: {
@@ -215,4 +216,34 @@ describe( 'actions', () => {
);
} );
} );
+
+ describe( 'setHasPageContentLock', () => {
+ it( 'toggles the page content lock on', () => {
+ const dispatch = jest.fn();
+ const clearSelectedBlock = jest.fn();
+ const registry = {
+ dispatch: () => ( { clearSelectedBlock } ),
+ };
+ setHasPageContentLock( true )( { dispatch, registry } );
+ expect( clearSelectedBlock ).toHaveBeenCalled();
+ expect( dispatch ).toHaveBeenCalledWith( {
+ type: 'SET_HAS_PAGE_CONTENT_LOCK',
+ hasPageContentLock: true,
+ } );
+ } );
+
+ it( 'toggles the page content lock off', () => {
+ const dispatch = jest.fn();
+ const clearSelectedBlock = jest.fn();
+ const registry = {
+ dispatch: () => ( { clearSelectedBlock } ),
+ };
+ setHasPageContentLock( false )( { dispatch, registry } );
+ expect( clearSelectedBlock ).not.toHaveBeenCalled();
+ expect( dispatch ).toHaveBeenCalledWith( {
+ type: 'SET_HAS_PAGE_CONTENT_LOCK',
+ hasPageContentLock: false,
+ } );
+ } );
+ } );
} );
diff --git a/packages/edit-site/src/store/test/reducer.js b/packages/edit-site/src/store/test/reducer.js
index f6ce205ad63533..1ddc6bfb6fa7b7 100644
--- a/packages/edit-site/src/store/test/reducer.js
+++ b/packages/edit-site/src/store/test/reducer.js
@@ -11,6 +11,7 @@ import {
editedPost,
blockInserterPanel,
listViewPanel,
+ hasPageContentLock,
} from '../reducer';
import { setIsInserterOpened, setIsListViewOpened } from '../actions';
@@ -135,4 +136,47 @@ describe( 'state', () => {
);
} );
} );
+
+ describe( 'hasPageContentLocked()', () => {
+ it( 'defaults to false', () => {
+ expect( hasPageContentLock( undefined, {} ) ).toBe( false );
+ } );
+
+ it( 'becomes false when editing a template', () => {
+ expect(
+ hasPageContentLock( true, {
+ type: 'SET_EDITED_POST',
+ postType: 'wp_template',
+ } )
+ ).toBe( false );
+ } );
+
+ it( 'becomes true when editing a page', () => {
+ expect(
+ hasPageContentLock( false, {
+ type: 'SET_EDITED_POST',
+ postType: 'wp_template',
+ context: {
+ postType: 'page',
+ postId: 123,
+ },
+ } )
+ ).toBe( true );
+ } );
+
+ it( 'can be set', () => {
+ expect(
+ hasPageContentLock( false, {
+ type: 'SET_HAS_PAGE_CONTENT_LOCK',
+ hasPageContentLock: true,
+ } )
+ ).toBe( true );
+ expect(
+ hasPageContentLock( true, {
+ type: 'SET_HAS_PAGE_CONTENT_LOCK',
+ hasPageContentLock: false,
+ } )
+ ).toBe( false );
+ } );
+ } );
} );
diff --git a/packages/edit-site/src/store/test/selectors.js b/packages/edit-site/src/store/test/selectors.js
index 223bcd1f0ba041..d9ed31411ffcc2 100644
--- a/packages/edit-site/src/store/test/selectors.js
+++ b/packages/edit-site/src/store/test/selectors.js
@@ -15,6 +15,8 @@ import {
isInserterOpened,
isListViewOpened,
__unstableGetPreference,
+ isPage,
+ hasPageContentLock,
} from '../selectors';
describe( 'selectors', () => {
@@ -145,4 +147,59 @@ describe( 'selectors', () => {
expect( isListViewOpened( state ) ).toBe( false );
} );
} );
+
+ describe( 'isPage', () => {
+ it( 'returns true if the edited post type is a page', () => {
+ const state = {
+ editedPost: {
+ postType: 'wp_template',
+ context: { postType: 'page', postId: 123 },
+ },
+ };
+ expect( isPage( state ) ).toBe( true );
+ } );
+
+ it( 'returns false if the edited post type is a template', () => {
+ const state = {
+ editedPost: {
+ postType: 'wp_template',
+ },
+ };
+ expect( isPage( state ) ).toBe( false );
+ } );
+ } );
+
+ describe( 'hasPageContentLock', () => {
+ it( 'returns true if locked and the edited post type is a page', () => {
+ const state = {
+ editedPost: {
+ postType: 'wp_template',
+ context: { postType: 'page', postId: 123 },
+ },
+ hasPageContentLock: true,
+ };
+ expect( hasPageContentLock( state ) ).toBe( true );
+ } );
+
+ it( 'returns false if not locked and the edited post type is a page', () => {
+ const state = {
+ editedPost: {
+ postType: 'wp_template',
+ context: { postType: 'page', postId: 123 },
+ },
+ hasPageContentLock: false,
+ };
+ expect( hasPageContentLock( state ) ).toBe( false );
+ } );
+
+ it( 'returns false if locked and the edited post type is a template', () => {
+ const state = {
+ editedPost: {
+ postType: 'wp_template',
+ },
+ hasPageContentLock: true,
+ };
+ expect( hasPageContentLock( state ) ).toBe( false );
+ } );
+ } );
} );
diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss
index 3be15cd02d2599..1004684c17cc2c 100644
--- a/packages/edit-site/src/style.scss
+++ b/packages/edit-site/src/style.scss
@@ -10,8 +10,10 @@
@import "./components/header-edit-mode/document-actions/style.scss";
@import "./components/list/style.scss";
@import "./components/sidebar-edit-mode/style.scss";
+@import "./components/sidebar-edit-mode/page-panels/style.scss";
@import "./components/sidebar-edit-mode/settings-header/style.scss";
-@import "./components/sidebar-edit-mode/template-card/style.scss";
+@import "./components/sidebar-edit-mode/sidebar-card/style.scss";
+@import "./components/sidebar-edit-mode/template-panel/style.scss";
@import "./components/editor/style.scss";
@import "./components/create-template-part-modal/style.scss";
@import "./components/secondary-sidebar/style.scss";
@@ -25,7 +27,8 @@
@import "./components/sidebar-button/style.scss";
@import "./components/sidebar-navigation-item/style.scss";
@import "./components/sidebar-navigation-screen/style.scss";
-@import "./components/sidebar-navigation-screen-pages/style.scss";
+@import "./components/sidebar-navigation-screen-global-styles/style.scss";
+@import "./components/sidebar-navigation-screen-page/style.scss";
@import "./components/sidebar-navigation-screen-template/style.scss";
@import "./components/sidebar-navigation-screen-templates/style.scss";
@import "./components/sidebar-navigation-subtitle/style.scss";
diff --git a/packages/edit-site/src/utils/is-previewing-theme.js b/packages/edit-site/src/utils/is-previewing-theme.js
index 69388b67212a20..fe742ae2b95a8b 100644
--- a/packages/edit-site/src/utils/is-previewing-theme.js
+++ b/packages/edit-site/src/utils/is-previewing-theme.js
@@ -4,10 +4,7 @@
import { getQueryArg } from '@wordpress/url';
export function isPreviewingTheme() {
- return (
- window?.__experimentalEnableThemePreviews &&
- getQueryArg( window.location.href, 'theme_preview' ) !== undefined
- );
+ return getQueryArg( window.location.href, 'theme_preview' ) !== undefined;
}
export function currentlyPreviewingTheme() {
diff --git a/packages/edit-widgets/src/components/secondary-sidebar/list-view-sidebar.js b/packages/edit-widgets/src/components/secondary-sidebar/list-view-sidebar.js
index 6e828533ddb6aa..eeff1ea0bf0f3f 100644
--- a/packages/edit-widgets/src/components/secondary-sidebar/list-view-sidebar.js
+++ b/packages/edit-widgets/src/components/secondary-sidebar/list-view-sidebar.js
@@ -9,6 +9,7 @@ import {
useMergeRefs,
} from '@wordpress/compose';
import { useDispatch } from '@wordpress/data';
+import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { closeSmall } from '@wordpress/icons';
import { ESCAPE } from '@wordpress/keycodes';
@@ -21,9 +22,14 @@ import { store as editWidgetsStore } from '../../store';
export default function ListViewSidebar() {
const { setIsListViewOpened } = useDispatch( editWidgetsStore );
+ // Use internal state instead of a ref to make sure that the component
+ // re-renders when the dropZoneElement updates.
+ const [ dropZoneElement, setDropZoneElement ] = useState( null );
+
const focusOnMountRef = useFocusOnMount( 'firstElement' );
const headerFocusReturnRef = useFocusReturn();
const contentFocusReturnRef = useFocusReturn();
+
function closeOnEscape( event ) {
if ( event.keyCode === ESCAPE && ! event.defaultPrevented ) {
event.preventDefault();
@@ -53,9 +59,10 @@ export default function ListViewSidebar() {
ref={ useMergeRefs( [
contentFocusReturnRef,
focusOnMountRef,
+ setDropZoneElement,
] ) }
>
-
+
);
diff --git a/packages/edit-widgets/src/components/sidebar/index.js b/packages/edit-widgets/src/components/sidebar/index.js
index 087f781f69a5ba..27af07831e1eed 100644
--- a/packages/edit-widgets/src/components/sidebar/index.js
+++ b/packages/edit-widgets/src/components/sidebar/index.js
@@ -7,7 +7,7 @@ import classnames from 'classnames';
* WordPress dependencies
*/
import { useEffect, Platform } from '@wordpress/element';
-import { __, sprintf } from '@wordpress/i18n';
+import { isRTL, __, sprintf } from '@wordpress/i18n';
import {
ComplementaryArea,
store as interfaceStore,
@@ -17,7 +17,7 @@ import {
store as blockEditorStore,
} from '@wordpress/block-editor';
-import { cog } from '@wordpress/icons';
+import { drawerLeft, drawerRight } from '@wordpress/icons';
import { Button } from '@wordpress/components';
import { useSelect, useDispatch } from '@wordpress/data';
@@ -168,7 +168,7 @@ export default function Sidebar() {
closeLabel={ __( 'Close Settings' ) }
scope="core/edit-widgets"
identifier={ currentArea }
- icon={ cog }
+ icon={ isRTL() ? drawerLeft : drawerRight }
isActiveByDefault={ SIDEBAR_ACTIVE_BY_DEFAULT }
>
{ currentArea === WIDGET_AREAS_IDENTIFIER && (
diff --git a/packages/editor/src/components/entities-saved-states/hooks/use-is-dirty.js b/packages/editor/src/components/entities-saved-states/hooks/use-is-dirty.js
new file mode 100644
index 00000000000000..e1adc68c7a82ae
--- /dev/null
+++ b/packages/editor/src/components/entities-saved-states/hooks/use-is-dirty.js
@@ -0,0 +1,85 @@
+/**
+ * WordPress dependencies
+ */
+import { useSelect } from '@wordpress/data';
+import { store as coreStore } from '@wordpress/core-data';
+import { useState } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+
+const TRANSLATED_SITE_PROPERTIES = {
+ title: __( 'Title' ),
+ description: __( 'Tagline' ),
+ site_logo: __( 'Logo' ),
+ site_icon: __( 'Icon' ),
+ show_on_front: __( 'Show on front' ),
+ page_on_front: __( 'Page on front' ),
+};
+
+export const useIsDirty = () => {
+ const { dirtyEntityRecords } = useSelect( ( select ) => {
+ const dirtyRecords =
+ select( coreStore ).__experimentalGetDirtyEntityRecords();
+
+ // Remove site object and decouple into its edited pieces.
+ const dirtyRecordsWithoutSite = dirtyRecords.filter(
+ ( record ) => ! ( record.kind === 'root' && record.name === 'site' )
+ );
+
+ const siteEdits = select( coreStore ).getEntityRecordEdits(
+ 'root',
+ 'site'
+ );
+
+ const siteEditsAsEntities = [];
+ for ( const property in siteEdits ) {
+ siteEditsAsEntities.push( {
+ kind: 'root',
+ name: 'site',
+ title: TRANSLATED_SITE_PROPERTIES[ property ] || property,
+ property,
+ } );
+ }
+ const dirtyRecordsWithSiteItems = [
+ ...dirtyRecordsWithoutSite,
+ ...siteEditsAsEntities,
+ ];
+
+ return {
+ dirtyEntityRecords: dirtyRecordsWithSiteItems,
+ };
+ }, [] );
+
+ // Unchecked entities to be ignored by save function.
+ const [ unselectedEntities, _setUnselectedEntities ] = useState( [] );
+
+ const setUnselectedEntities = (
+ { kind, name, key, property },
+ checked
+ ) => {
+ if ( checked ) {
+ _setUnselectedEntities(
+ unselectedEntities.filter(
+ ( elt ) =>
+ elt.kind !== kind ||
+ elt.name !== name ||
+ elt.key !== key ||
+ elt.property !== property
+ )
+ );
+ } else {
+ _setUnselectedEntities( [
+ ...unselectedEntities,
+ { kind, name, key, property },
+ ] );
+ }
+ };
+
+ const isDirty = dirtyEntityRecords.length - unselectedEntities.length > 0;
+
+ return {
+ dirtyEntityRecords,
+ isDirty,
+ setUnselectedEntities,
+ unselectedEntities,
+ };
+};
diff --git a/packages/editor/src/components/entities-saved-states/index.js b/packages/editor/src/components/entities-saved-states/index.js
index 29bef117243036..924585fef3a6df 100644
--- a/packages/editor/src/components/entities-saved-states/index.js
+++ b/packages/editor/src/components/entities-saved-states/index.js
@@ -2,28 +2,19 @@
* WordPress dependencies
*/
import { Button, Flex, FlexItem } from '@wordpress/components';
-import { __, sprintf } from '@wordpress/i18n';
-import { useSelect, useDispatch } from '@wordpress/data';
-import { useState, useCallback, useRef } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import { useDispatch } from '@wordpress/data';
+import { useCallback, useRef } from '@wordpress/element';
import { store as coreStore } from '@wordpress/core-data';
import { store as blockEditorStore } from '@wordpress/block-editor';
import { __experimentalUseDialog as useDialog } from '@wordpress/compose';
import { store as noticesStore } from '@wordpress/notices';
-import { getQueryArg } from '@wordpress/url';
/**
* Internal dependencies
*/
import EntityTypeList from './entity-type-list';
-
-const TRANSLATED_SITE_PROPERTIES = {
- title: __( 'Title' ),
- description: __( 'Tagline' ),
- site_logo: __( 'Logo' ),
- site_icon: __( 'Icon' ),
- show_on_front: __( 'Show on front' ),
- page_on_front: __( 'Page on front' ),
-};
+import { useIsDirty } from './hooks/use-is-dirty';
const PUBLISH_ON_SAVE_ENTITIES = [
{
@@ -36,56 +27,26 @@ function identity( values ) {
return values;
}
-function isPreviewingTheme() {
+export default function EntitiesSavedStates( { close } ) {
+ const isDirtyProps = useIsDirty();
return (
- window?.__experimentalEnableThemePreviews &&
- getQueryArg( window.location.href, 'theme_preview' ) !== undefined
+
);
}
-function currentlyPreviewingTheme() {
- if ( isPreviewingTheme() ) {
- return getQueryArg( window.location.href, 'theme_preview' );
- }
- return null;
-}
-
-export default function EntitiesSavedStates( { close, onSave = identity } ) {
+export function EntitiesSavedStatesExtensible( {
+ additionalPrompt = undefined,
+ close,
+ onSave = identity,
+ saveEnabled: saveEnabledProp = undefined,
+ saveLabel = __( 'Save' ),
+
+ dirtyEntityRecords,
+ isDirty,
+ setUnselectedEntities,
+ unselectedEntities,
+} ) {
const saveButtonRef = useRef();
- const { getTheme } = useSelect( coreStore );
- const theme = getTheme( currentlyPreviewingTheme() );
- const { dirtyEntityRecords } = useSelect( ( select ) => {
- const dirtyRecords =
- select( coreStore ).__experimentalGetDirtyEntityRecords();
-
- // Remove site object and decouple into its edited pieces.
- const dirtyRecordsWithoutSite = dirtyRecords.filter(
- ( record ) => ! ( record.kind === 'root' && record.name === 'site' )
- );
-
- const siteEdits = select( coreStore ).getEntityRecordEdits(
- 'root',
- 'site'
- );
-
- const siteEditsAsEntities = [];
- for ( const property in siteEdits ) {
- siteEditsAsEntities.push( {
- kind: 'root',
- name: 'site',
- title: TRANSLATED_SITE_PROPERTIES[ property ] || property,
- property,
- } );
- }
- const dirtyRecordsWithSiteItems = [
- ...dirtyRecordsWithoutSite,
- ...siteEditsAsEntities,
- ];
-
- return {
- dirtyEntityRecords: dirtyRecordsWithSiteItems,
- };
- }, [] );
const {
editEntityRecord,
saveEditedEntityRecord,
@@ -122,32 +83,9 @@ export default function EntitiesSavedStates( { close, onSave = identity } ) {
...Object.values( contentSavables ),
].filter( Array.isArray );
- // Unchecked entities to be ignored by save function.
- const [ unselectedEntities, _setUnselectedEntities ] = useState( [] );
-
- const setUnselectedEntities = (
- { kind, name, key, property },
- checked
- ) => {
- if ( checked ) {
- _setUnselectedEntities(
- unselectedEntities.filter(
- ( elt ) =>
- elt.kind !== kind ||
- elt.name !== name ||
- elt.key !== key ||
- elt.property !== property
- )
- );
- } else {
- _setUnselectedEntities( [
- ...unselectedEntities,
- { kind, name, key, property },
- ] );
- }
- };
+ const saveEnabled = saveEnabledProp ?? isDirty;
- const saveCheckedEntitiesAndActivate = () => {
+ const saveCheckedEntities = () => {
const saveNoticeId = 'site-editor-save-success';
removeNotice( saveNoticeId );
const entitiesToSave = dirtyEntityRecords.filter(
@@ -227,18 +165,6 @@ export default function EntitiesSavedStates( { close, onSave = identity } ) {
onClose: () => dismissPanel(),
} );
- const isDirty = dirtyEntityRecords.length - unselectedEntities.length > 0;
- const activateSaveEnabled = isPreviewingTheme() || isDirty;
-
- let activateSaveLabel;
- if ( isPreviewingTheme() && isDirty ) {
- activateSaveLabel = __( 'Activate & Save' );
- } else if ( isPreviewingTheme() ) {
- activateSaveLabel = __( 'Activate' );
- } else {
- activateSaveLabel = __( 'Save' );
- }
-
return (
- { activateSaveLabel }
+ { saveLabel }
{ __( 'Are you ready to save?' ) }
- { isPreviewingTheme() && (
-
- { sprintf(
- 'Saving your changes will change your active theme to %1$s.',
- theme?.name?.rendered
- ) }
-
- ) }
+ { additionalPrompt }
{ isDirty && (
{ __(
diff --git a/packages/editor/src/components/entities-saved-states/test/use-is-dirty.js b/packages/editor/src/components/entities-saved-states/test/use-is-dirty.js
new file mode 100644
index 00000000000000..04b6b4e566ef1f
--- /dev/null
+++ b/packages/editor/src/components/entities-saved-states/test/use-is-dirty.js
@@ -0,0 +1,94 @@
+/**
+ * External dependencies
+ */
+import { act, renderHook } from '@testing-library/react';
+
+/**
+ * Internal dependencies
+ */
+import { useIsDirty } from '../hooks/use-is-dirty';
+
+jest.mock( '@wordpress/data', () => {
+ return {
+ useSelect: jest.fn().mockImplementation( ( fn ) => {
+ const select = () => {
+ return {
+ __experimentalGetDirtyEntityRecords: jest
+ .fn()
+ .mockReturnValue( [
+ {
+ kind: 'root',
+ name: 'site',
+ title: 'title',
+ property: 'property',
+ },
+ {
+ kind: 'postType',
+ name: 'post',
+ title: 'title',
+ property: 'property',
+ },
+ ] ),
+ getEntityRecordEdits: jest.fn().mockReturnValue( {
+ title: 'My Site',
+ } ),
+ };
+ };
+ return fn( select );
+ } ),
+ };
+} );
+
+jest.mock( '@wordpress/core-data', () => {
+ return {
+ store: {
+ __experimentalGetDirtyEntityRecords: jest.fn(),
+ getEntityRecordEdits: jest.fn(),
+ },
+ };
+} );
+
+describe( 'useIsDirty', () => {
+ it( 'should calculate dirtyEntityRecords', () => {
+ const { result } = renderHook( () => useIsDirty() );
+ expect( result.current.dirtyEntityRecords ).toEqual( [
+ {
+ kind: 'postType',
+ name: 'post',
+ property: 'property',
+ title: 'title',
+ },
+ { kind: 'root', name: 'site', property: 'title', title: 'Title' },
+ ] );
+ } );
+ it( 'should return `isDirty: true` when there are changes', () => {
+ const { result } = renderHook( () => useIsDirty() );
+ expect( result.current.isDirty ).toBeTruthy();
+ } );
+ it( 'should return `isDirty: false` when there are NO changes', async () => {
+ const { result } = renderHook( () => useIsDirty() );
+ act( () => {
+ result.current.setUnselectedEntities(
+ {
+ kind: 'postType',
+ name: 'post',
+ key: 'key',
+ property: 'property',
+ },
+ false
+ );
+ } );
+ act( () => {
+ result.current.setUnselectedEntities(
+ {
+ kind: 'root',
+ name: 'site',
+ key: 'key',
+ property: 'property',
+ },
+ false
+ );
+ } );
+ expect( result.current.isDirty ).toBeFalsy();
+ } );
+} );
diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js
index 3c1521f756554a..410fa6b38a61e8 100644
--- a/packages/editor/src/components/index.js
+++ b/packages/editor/src/components/index.js
@@ -13,6 +13,7 @@ export { default as EditorHistoryUndo } from './editor-history/undo';
export { default as EditorNotices } from './editor-notices';
export { default as EditorSnackbars } from './editor-snackbars';
export { default as EntitiesSavedStates } from './entities-saved-states';
+export { useIsDirty as useEntitiesSavedStatesIsDirty } from './entities-saved-states/hooks/use-is-dirty';
export { default as ErrorBoundary } from './error-boundary';
export { default as LocalAutosaveMonitor } from './local-autosave-monitor';
export { default as PageAttributesCheck } from './page-attributes/check';
diff --git a/packages/editor/src/components/post-saved-state/index.js b/packages/editor/src/components/post-saved-state/index.js
index d3c15b5596e3d9..24b88d4d96dee1 100644
--- a/packages/editor/src/components/post-saved-state/index.js
+++ b/packages/editor/src/components/post-saved-state/index.js
@@ -47,8 +47,10 @@ export default function PostSavedState( {
isDirty,
isNew,
isPending,
+ isPublished,
isSaveable,
isSaving,
+ isScheduled,
hasPublishAction,
} = useSelect(
( select ) => {
@@ -103,6 +105,10 @@ export default function PostSavedState( {
return null;
}
+ if ( isPublished || isScheduled ) {
+ return null;
+ }
+
/* translators: button label text should, if possible, be under 16 characters. */
const label = isPending ? __( 'Save as pending' ) : __( 'Save draft' );
diff --git a/packages/editor/src/components/post-title/index.js b/packages/editor/src/components/post-title/index.js
index b9143a29ff3c05..b9cc0adaa275ca 100644
--- a/packages/editor/src/components/post-title/index.js
+++ b/packages/editor/src/components/post-title/index.js
@@ -74,7 +74,8 @@ function PostTitle( _, forwardedRef ) {
return;
}
- const { ownerDocument } = ref.current;
+ const ownerDocument =
+ ref.current.ownerDocument.defaultView.top.document;
const { activeElement, body } = ownerDocument;
// Only autofocus the title when the post is entirely empty. This should
diff --git a/packages/editor/src/private-apis.js b/packages/editor/src/private-apis.js
index edfc3045f1100d..cdec27b0ab9021 100644
--- a/packages/editor/src/private-apis.js
+++ b/packages/editor/src/private-apis.js
@@ -3,8 +3,10 @@
*/
import { ExperimentalEditorProvider } from './components/provider';
import { lock } from './lockUnlock';
+import { EntitiesSavedStatesExtensible } from './components/entities-saved-states';
export const privateApis = {};
lock( privateApis, {
ExperimentalEditorProvider,
+ EntitiesSavedStatesExtensible,
} );
diff --git a/packages/env/CHANGELOG.md b/packages/env/CHANGELOG.md
index 54bbd44d2b4485..04a2030c1ab8a3 100644
--- a/packages/env/CHANGELOG.md
+++ b/packages/env/CHANGELOG.md
@@ -2,6 +2,14 @@
## Unreleased
+### New feature
+
+- Execute the local package's `wp-env` instead of the globally installed version if one is available.
+
+### Bug fix
+
+- Run `useradd` with `-l` option to prevent excessive Docker image sizes.
+
## 8.0.0 (2023-05-24)
### Breaking Change
diff --git a/packages/env/README.md b/packages/env/README.md
index 0bc36092830330..fb9e9751d9c666 100644
--- a/packages/env/README.md
+++ b/packages/env/README.md
@@ -44,9 +44,9 @@ If your project already has a package.json, it's also possible to use `wp-env` a
$ npm i @wordpress/env --save-dev
```
-At this point, you can use the local, project-level version of wp-env via [`npx`](https://www.npmjs.com/package/npx), a utility automatically installed with `npm`.`npx` finds binaries like wp-env installed through node modules. As an example: `npx wp-env start --update`.
+If you have also installed `wp-env` globally, running it will automatically execute the local, project-level package. Alternatively, you can execute `wp-env` via [`npx`](https://www.npmjs.com/package/npx), a utility automatically installed with `npm`.`npx` finds binaries like `wp-env` installed through node modules. As an example: `npx wp-env start --update`.
-If you don't wish to use `npx`, modify your package.json and add an extra command to npm `scripts` (https://docs.npmjs.com/misc/scripts):
+If you don't wish to use the global installation or `npx`, modify your `package.json` and add an extra command to npm `scripts` (https://docs.npmjs.com/misc/scripts):
```json
"scripts": {
diff --git a/packages/env/bin/wp-env b/packages/env/bin/wp-env
index 7ce3e39103bcd1..a6c4784a3e7fb5 100755
--- a/packages/env/bin/wp-env
+++ b/packages/env/bin/wp-env
@@ -1,4 +1,20 @@
#!/usr/bin/env node
'use strict';
-const command = process.argv.slice( 2 );
-require( '../lib/cli' )().parse( command.length ? command : [ '--help' ] );
+
+// Remove 'node' and the name of the script from the arguments.
+let command = process.argv.slice( 2 );
+// Default to help text when they aren't running any commands.
+if ( ! command.length ) {
+ command = [ '--help' ];
+}
+
+// Rather than just executing the current CLI we will attempt to find a local version
+// and execute that one instead. This prevents users from accidentally using the
+// global CLI when a potentially different local version is expected.
+const localPath = require.resolve( '@wordpress/env/lib/cli.js', {
+ paths: [ process.cwd(), __dirname ],
+} );
+const cli = require( localPath )();
+
+// Now we can execute the CLI with the given command.
+cli.parse( command );
diff --git a/packages/env/lib/cli.js b/packages/env/lib/cli.js
index 72a5eec911e087..1788315b60b9db 100644
--- a/packages/env/lib/cli.js
+++ b/packages/env/lib/cli.js
@@ -11,6 +11,7 @@ const { execSync } = require( 'child_process' );
/**
* Internal dependencies
*/
+const pkg = require( '../package.json' );
const env = require( './env' );
const parseXdebugMode = require( './parse-xdebug-mode' );
const {
@@ -110,6 +111,10 @@ module.exports = function cli() {
'populate--': true,
} );
+ // Since we might be running a different CLI version than the one that was called
+ // we need to set the version manually from the correct package.json.
+ yargs.version( pkg.version );
+
yargs.command(
'start',
wpGreen(
diff --git a/packages/env/lib/init-config.js b/packages/env/lib/init-config.js
index ee26ade75c665e..fad8d5b36f1834 100644
--- a/packages/env/lib/init-config.js
+++ b/packages/env/lib/init-config.js
@@ -133,8 +133,8 @@ ARG HOST_USERNAME
ARG HOST_UID
ARG HOST_GID
# When the IDs are already in use we can still safely move on.
-RUN groupadd -g $HOST_GID $HOST_USERNAME || true
-RUN useradd -m -u $HOST_UID -g $HOST_GID $HOST_USERNAME || true
+RUN groupadd -o -g $HOST_GID $HOST_USERNAME || true
+RUN useradd -mlo -u $HOST_UID -g $HOST_GID $HOST_USERNAME || true
# Install any dependencies we need in the container.
${ installDependencies( 'wordpress', env, config ) }`;
diff --git a/packages/icons/src/index.js b/packages/icons/src/index.js
index f3fc6c1d71f6ce..cd8058425a8d68 100644
--- a/packages/icons/src/index.js
+++ b/packages/icons/src/index.js
@@ -245,6 +245,14 @@ export { default as termDescription } from './library/term-description';
export { default as footer } from './library/footer';
export { default as header } from './library/header';
export { default as sidebar } from './library/sidebar';
+export { default as sidesAll } from './library/sides-all';
+export { default as sidesAxial } from './library/sides-axial';
+export { default as sidesBottom } from './library/sides-bottom';
+export { default as sidesHorizontal } from './library/sides-horizontal';
+export { default as sidesLeft } from './library/sides-left';
+export { default as sidesRight } from './library/sides-right';
+export { default as sidesTop } from './library/sides-top';
+export { default as sidesVertical } from './library/sides-vertical';
export { default as textColor } from './library/text-color';
export { default as tablet } from './library/tablet';
export { default as title } from './library/title';
diff --git a/packages/icons/src/library/sides-all.js b/packages/icons/src/library/sides-all.js
new file mode 100644
index 00000000000000..d6e027685640a6
--- /dev/null
+++ b/packages/icons/src/library/sides-all.js
@@ -0,0 +1,15 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const sidesAll = (
+
+
+
+);
+
+export default sidesAll;
diff --git a/packages/icons/src/library/sides-axial.js b/packages/icons/src/library/sides-axial.js
new file mode 100644
index 00000000000000..660d3ac20853d1
--- /dev/null
+++ b/packages/icons/src/library/sides-axial.js
@@ -0,0 +1,19 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const sidesAxial = (
+
+
+
+);
+
+export default sidesAxial;
diff --git a/packages/icons/src/library/sides-bottom.js b/packages/icons/src/library/sides-bottom.js
new file mode 100644
index 00000000000000..d1615ebc2a93b0
--- /dev/null
+++ b/packages/icons/src/library/sides-bottom.js
@@ -0,0 +1,16 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const sidesBottom = (
+
+
+
+
+);
+
+export default sidesBottom;
diff --git a/packages/icons/src/library/sides-horizontal.js b/packages/icons/src/library/sides-horizontal.js
new file mode 100644
index 00000000000000..87402487689e3b
--- /dev/null
+++ b/packages/icons/src/library/sides-horizontal.js
@@ -0,0 +1,19 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const sidesHorizontal = (
+
+
+
+
+);
+
+export default sidesHorizontal;
diff --git a/packages/icons/src/library/sides-left.js b/packages/icons/src/library/sides-left.js
new file mode 100644
index 00000000000000..ff2189f76a0dc9
--- /dev/null
+++ b/packages/icons/src/library/sides-left.js
@@ -0,0 +1,16 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const sidesLeft = (
+
+
+
+
+);
+
+export default sidesLeft;
diff --git a/packages/icons/src/library/sides-right.js b/packages/icons/src/library/sides-right.js
new file mode 100644
index 00000000000000..1abb83ad1bb689
--- /dev/null
+++ b/packages/icons/src/library/sides-right.js
@@ -0,0 +1,16 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const sidesRight = (
+
+
+
+
+);
+
+export default sidesRight;
diff --git a/packages/icons/src/library/sides-top.js b/packages/icons/src/library/sides-top.js
new file mode 100644
index 00000000000000..8519397ffd63e1
--- /dev/null
+++ b/packages/icons/src/library/sides-top.js
@@ -0,0 +1,16 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const sidesTop = (
+
+
+
+
+);
+
+export default sidesTop;
diff --git a/packages/icons/src/library/sides-vertical.js b/packages/icons/src/library/sides-vertical.js
new file mode 100644
index 00000000000000..09306cf72549a7
--- /dev/null
+++ b/packages/icons/src/library/sides-vertical.js
@@ -0,0 +1,19 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const sidesVertical = (
+
+
+
+
+);
+
+export default sidesVertical;
diff --git a/packages/notices/CHANGELOG.md b/packages/notices/CHANGELOG.md
index 15e471a65d36a5..08632b57fd7244 100644
--- a/packages/notices/CHANGELOG.md
+++ b/packages/notices/CHANGELOG.md
@@ -2,6 +2,11 @@
## Unreleased
+### New Feature
+
+- Add a new action `removeNotices` which allows bulk removal of notices by their IDs. ([#39940](https://github.com/WordPress/gutenberg/pull/39940))
+- Add a new action `removeAllNotices` which removes all notices from a given context. ([#44059](https://github.com/WordPress/gutenberg/pull/44059))
+
## 4.2.0 (2023-05-24)
## 4.1.0 (2023-05-10)
diff --git a/packages/notices/src/store/actions.js b/packages/notices/src/store/actions.js
index a8fdb13f9352fc..6ffa7aede2a884 100644
--- a/packages/notices/src/store/actions.js
+++ b/packages/notices/src/store/actions.js
@@ -313,3 +313,106 @@ export function removeNotice( id, context = DEFAULT_CONTEXT ) {
context,
};
}
+
+/**
+ * Removes all notices from a given context. Defaults to the default context.
+ *
+ * @param {string} noticeType The context to remove all notices from.
+ * @param {string} context The context to remove all notices from.
+ *
+ * @example
+ * ```js
+ * import { __ } from '@wordpress/i18n';
+ * import { useDispatch, useSelect } from '@wordpress/data';
+ * import { store as noticesStore } from '@wordpress/notices';
+ * import { Button } from '@wordpress/components';
+ *
+ * export const ExampleComponent = () => {
+ * const notices = useSelect( ( select ) =>
+ * select( noticesStore ).getNotices()
+ * );
+ * const { removeNotices } = useDispatch( noticesStore );
+ * return (
+ * <>
+ *
+ * { notices.map( ( notice ) => (
+ * { notice.content }
+ * ) ) }
+ *
+ *
+ * removeAllNotices()
+ * }
+ * >
+ * { __( 'Clear all notices', 'woo-gutenberg-products-block' ) }
+ *
+ *
+ * removeAllNotices( 'snackbar' )
+ * }
+ * >
+ * { __( 'Clear all snackbar notices', 'woo-gutenberg-products-block' ) }
+ *
+ * >
+ * );
+ * };
+ * ```
+ *
+ * @return {Object} Action object.
+ */
+export function removeAllNotices(
+ noticeType = 'default',
+ context = DEFAULT_CONTEXT
+) {
+ return {
+ type: 'REMOVE_ALL_NOTICES',
+ noticeType,
+ context,
+ };
+}
+
+/**
+ * Returns an action object used in signalling that several notices are to be removed.
+ *
+ * @param {string[]} ids List of unique notice identifiers.
+ * @param {string} [context='global'] Optional context (grouping) in which the notices are
+ * intended to appear. Defaults to default context.
+ * @example
+ * ```js
+ * import { __ } from '@wordpress/i18n';
+ * import { useDispatch, useSelect } from '@wordpress/data';
+ * import { store as noticesStore } from '@wordpress/notices';
+ * import { Button } from '@wordpress/components';
+ *
+ * const ExampleComponent = () => {
+ * const notices = useSelect( ( select ) =>
+ * select( noticesStore ).getNotices()
+ * );
+ * const { removeNotices } = useDispatch( noticesStore );
+ * return (
+ * <>
+ *
+ * { notices.map( ( notice ) => (
+ * { notice.content }
+ * ) ) }
+ *
+ *
+ * removeNotices( notices.map( ( { id } ) => id ) )
+ * }
+ * >
+ * { __( 'Clear all notices' ) }
+ *
+ * >
+ * );
+ * };
+ * ```
+ * @return {Object} Action object.
+ */
+export function removeNotices( ids, context = DEFAULT_CONTEXT ) {
+ return {
+ type: 'REMOVE_NOTICES',
+ ids,
+ context,
+ };
+}
diff --git a/packages/notices/src/store/reducer.js b/packages/notices/src/store/reducer.js
index ff2359b61cc013..5f4d88c04af299 100644
--- a/packages/notices/src/store/reducer.js
+++ b/packages/notices/src/store/reducer.js
@@ -23,6 +23,12 @@ const notices = onSubKey( 'context' )( ( state = [], action ) => {
case 'REMOVE_NOTICE':
return state.filter( ( { id } ) => id !== action.id );
+
+ case 'REMOVE_NOTICES':
+ return state.filter( ( { id } ) => ! action.ids.includes( id ) );
+
+ case 'REMOVE_ALL_NOTICES':
+ return state.filter( ( { type } ) => type !== action.noticeType );
}
return state;
diff --git a/packages/notices/src/store/test/actions.js b/packages/notices/src/store/test/actions.js
index db6aa72b468dec..37fefc5c3558f4 100644
--- a/packages/notices/src/store/test/actions.js
+++ b/packages/notices/src/store/test/actions.js
@@ -8,6 +8,8 @@ import {
createErrorNotice,
createWarningNotice,
removeNotice,
+ removeAllNotices,
+ removeNotices,
} from '../actions';
import { DEFAULT_CONTEXT, DEFAULT_STATUS } from '../constants';
@@ -215,4 +217,55 @@ describe( 'actions', () => {
} );
} );
} );
+
+ describe( 'removeNotices', () => {
+ it( 'should return action', () => {
+ const ids = [ 'id', 'id2' ];
+
+ expect( removeNotices( ids ) ).toEqual( {
+ type: 'REMOVE_NOTICES',
+ ids,
+ context: DEFAULT_CONTEXT,
+ } );
+ } );
+
+ it( 'should return action with custom context', () => {
+ const ids = [ 'id', 'id2' ];
+ const context = 'foo';
+
+ expect( removeNotices( ids, context ) ).toEqual( {
+ type: 'REMOVE_NOTICES',
+ ids,
+ context,
+ } );
+ } );
+ } );
+
+ describe( 'removeAllNotices', () => {
+ it( 'should return action', () => {
+ expect( removeAllNotices() ).toEqual( {
+ type: 'REMOVE_ALL_NOTICES',
+ noticeType: 'default',
+ context: DEFAULT_CONTEXT,
+ } );
+ } );
+
+ it( 'should return action with custom context', () => {
+ const context = 'foo';
+
+ expect( removeAllNotices( 'default', context ) ).toEqual( {
+ type: 'REMOVE_ALL_NOTICES',
+ noticeType: 'default',
+ context,
+ } );
+ } );
+
+ it( 'should return action with type', () => {
+ expect( removeAllNotices( 'snackbar' ) ).toEqual( {
+ type: 'REMOVE_ALL_NOTICES',
+ noticeType: 'snackbar',
+ context: DEFAULT_CONTEXT,
+ } );
+ } );
+ } );
} );
diff --git a/packages/notices/src/store/test/reducer.js b/packages/notices/src/store/test/reducer.js
index 52ba278c79d854..d807b814a51f47 100644
--- a/packages/notices/src/store/test/reducer.js
+++ b/packages/notices/src/store/test/reducer.js
@@ -7,7 +7,12 @@ import deepFreeze from 'deep-freeze';
* Internal dependencies
*/
import reducer from '../reducer';
-import { createNotice, removeNotice } from '../actions';
+import {
+ createNotice,
+ removeNotice,
+ removeNotices,
+ removeAllNotices,
+} from '../actions';
import { getNotices } from '../selectors';
import { DEFAULT_CONTEXT } from '../constants';
@@ -141,6 +146,44 @@ describe( 'reducer', () => {
expect( state[ DEFAULT_CONTEXT ] ).toHaveLength( 1 );
} );
+ it( 'should omit several removed notices', () => {
+ const action = createNotice( 'error', 'save error' );
+ const action2 = createNotice( 'error', 'second error' );
+ const stateWithOneNotice = reducer( undefined, action );
+ const original = deepFreeze( reducer( stateWithOneNotice, action2 ) );
+ const ids = [
+ getNotices( original )[ 0 ].id,
+ getNotices( original )[ 1 ].id,
+ ];
+
+ const state = reducer( original, removeNotices( ids ) );
+
+ expect( state ).toEqual( {
+ [ DEFAULT_CONTEXT ]: [],
+ } );
+ } );
+
+ it( 'should omit several removed notices across contexts', () => {
+ const action = createNotice( 'error', 'save error' );
+ const action2 = createNotice( 'error', 'second error', {
+ context: 'foo',
+ } );
+ const action3 = createNotice( 'error', 'third error', {
+ context: 'foo',
+ } );
+ const stateWithOneNotice = reducer( undefined, action );
+ const stateWithTwoNotices = reducer( stateWithOneNotice, action2 );
+ const original = deepFreeze( reducer( stateWithTwoNotices, action3 ) );
+ const ids = [
+ getNotices( original, 'foo' )[ 0 ].id,
+ getNotices( original, 'foo' )[ 1 ].id,
+ ];
+
+ const state = reducer( original, removeNotices( ids, 'foo' ) );
+
+ expect( state[ DEFAULT_CONTEXT ] ).toHaveLength( 1 );
+ } );
+
it( 'should dedupe distinct ids, preferring new', () => {
let action = createNotice( 'error', 'save error (1)', {
id: 'error-message',
@@ -170,4 +213,84 @@ describe( 'reducer', () => {
],
} );
} );
+
+ it( 'should remove all notices', () => {
+ let action = createNotice( 'error', 'save error' );
+ const original = deepFreeze( reducer( undefined, action ) );
+
+ action = createNotice( 'success', 'successfully saved' );
+ let state = reducer( original, action );
+ state = reducer( state, removeAllNotices() );
+
+ expect( state ).toEqual( {
+ [ DEFAULT_CONTEXT ]: [],
+ } );
+ } );
+
+ it( 'should remove all notices in a given context but leave other contexts intact', () => {
+ let action = createNotice( 'error', 'save error', {
+ context: 'foo',
+ id: 'foo-error',
+ } );
+ const original = deepFreeze( reducer( undefined, action ) );
+
+ action = createNotice( 'success', 'successfully saved', {
+ context: 'bar',
+ } );
+
+ let state = reducer( original, action );
+ state = reducer( state, removeAllNotices( 'default', 'bar' ) );
+
+ expect( state ).toEqual( {
+ bar: [],
+ foo: [
+ {
+ id: 'foo-error',
+ content: 'save error',
+ spokenMessage: 'save error',
+ __unstableHTML: undefined,
+ status: 'error',
+ isDismissible: true,
+ actions: [],
+ type: 'default',
+ icon: null,
+ explicitDismiss: false,
+ onDismiss: undefined,
+ },
+ ],
+ } );
+ } );
+
+ it( 'should remove all notices of a given type', () => {
+ let action = createNotice( 'error', 'save error', {
+ id: 'global-error',
+ } );
+ const original = deepFreeze( reducer( undefined, action ) );
+
+ action = createNotice( 'success', 'successfully saved', {
+ type: 'snackbar',
+ id: 'snackbar-success',
+ } );
+
+ let state = reducer( original, action );
+ state = reducer( state, removeAllNotices( 'default' ) );
+
+ expect( state ).toEqual( {
+ [ DEFAULT_CONTEXT ]: [
+ {
+ id: 'snackbar-success',
+ content: 'successfully saved',
+ spokenMessage: 'successfully saved',
+ __unstableHTML: undefined,
+ status: 'success',
+ isDismissible: true,
+ actions: [],
+ type: 'snackbar',
+ icon: null,
+ explicitDismiss: false,
+ onDismiss: undefined,
+ },
+ ],
+ } );
+ } );
} );
diff --git a/packages/preferences/src/components/preference-toggle-menu-item/index.js b/packages/preferences/src/components/preference-toggle-menu-item/index.js
index 2797e6455bc0b1..9d2b8d353e9448 100644
--- a/packages/preferences/src/components/preference-toggle-menu-item/index.js
+++ b/packages/preferences/src/components/preference-toggle-menu-item/index.js
@@ -25,7 +25,7 @@ export default function PreferenceToggleMenuItem( {
} ) {
const isActive = useSelect(
( select ) => !! select( preferencesStore ).get( scope, name ),
- [ name ]
+ [ scope, name ]
);
const { toggle } = useDispatch( preferencesStore );
const speakMessage = () => {
diff --git a/packages/primitives/src/horizontal-rule/index.native.js b/packages/primitives/src/horizontal-rule/index.native.js
index 853b57e76d0c1a..8841c8e6941889 100644
--- a/packages/primitives/src/horizontal-rule/index.native.js
+++ b/packages/primitives/src/horizontal-rule/index.native.js
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import Hr from 'react-native-hr';
+import { Text, View } from 'react-native';
/**
* WordPress dependencies
@@ -13,16 +13,46 @@ import { withPreferredColorScheme } from '@wordpress/compose';
*/
import styles from './styles.scss';
-const HR = ( { getStylesFromColorScheme, ...props } ) => {
- const lineStyle = getStylesFromColorScheme( styles.line, styles.lineDark );
+const HR = ( {
+ getStylesFromColorScheme,
+ lineStyle,
+ marginLeft,
+ marginRight,
+ textStyle,
+ text,
+ ...props
+} ) => {
+ const renderLine = ( key ) => (
+
+ );
+
+ const renderText = ( key ) => (
+
+ { text }
+
+ );
+
+ const renderInner = () => {
+ if ( ! text ) {
+ return renderLine();
+ }
+ return [ renderLine( 1 ), renderText( 2 ), renderLine( 3 ) ];
+ };
return (
-
+ >
+ { renderInner() }
+
);
};
diff --git a/packages/primitives/src/horizontal-rule/styles.native.scss b/packages/primitives/src/horizontal-rule/styles.native.scss
index dabb62a8336542..20959ab73042e9 100644
--- a/packages/primitives/src/horizontal-rule/styles.native.scss
+++ b/packages/primitives/src/horizontal-rule/styles.native.scss
@@ -1,8 +1,23 @@
+.container {
+ align-items: center;
+ flex-direction: row;
+ margin-left: 0;
+ margin-right: 0;
+}
+
.line {
background-color: $gray-lighten-20;
+ flex: 1;
height: 2;
}
.lineDark {
background-color: $gray-50;
}
+
+.text {
+ flex: 1;
+ margin-left: 15px;
+ margin-right: 15px;
+ text-align: center;
+}
diff --git a/packages/react-native-aztec/android/build.gradle b/packages/react-native-aztec/android/build.gradle
index 991fb23e51b9ad..b7d2080b2e463e 100644
--- a/packages/react-native-aztec/android/build.gradle
+++ b/packages/react-native-aztec/android/build.gradle
@@ -9,7 +9,7 @@ buildscript {
jSoupVersion = '1.10.3'
wordpressUtilsVersion = '3.3.0'
espressoVersion = '3.0.1'
- aztecVersion = 'v1.6.3'
+ aztecVersion = 'v1.6.4'
}
}
@@ -34,11 +34,11 @@ List dirs = [
'template'] // boilerplate code that is generated by the sample template process
android {
- compileSdkVersion 31
+ compileSdkVersion 33
defaultConfig {
minSdkVersion 24
- targetSdkVersion 31
+ targetSdkVersion 33
}
compileOptions {
diff --git a/packages/react-native-aztec/android/settings.gradle b/packages/react-native-aztec/android/settings.gradle
index 8b0c3d17fa200d..365441bced0ebf 100644
--- a/packages/react-native-aztec/android/settings.gradle
+++ b/packages/react-native-aztec/android/settings.gradle
@@ -1,5 +1,5 @@
pluginManagement {
- gradle.ext.kotlinVersion = '1.5.32'
+ gradle.ext.kotlinVersion = '1.6.10'
plugins {
id "com.android.library" version "7.2.1"
diff --git a/packages/react-native-aztec/ios/RNTAztecView/RCTAztecView.swift b/packages/react-native-aztec/ios/RNTAztecView/RCTAztecView.swift
index c6e928b3964404..3504da36af422a 100644
--- a/packages/react-native-aztec/ios/RNTAztecView/RCTAztecView.swift
+++ b/packages/react-native-aztec/ios/RNTAztecView/RCTAztecView.swift
@@ -95,12 +95,6 @@ class RCTAztecView: Aztec.TextView {
let placeholderWidthInset = 2 * leftTextInset
return placeholderLabel.widthAnchor.constraint(equalTo: widthAnchor, constant: -placeholderWidthInset)
}()
-
- /// If a dictation start with an empty UITextView,
- /// the dictation engine refreshes the TextView with an empty string when the dictation finishes.
- /// This helps to avoid propagating that unwanted empty string to RN. (Solving #606)
- /// on `textViewDidChange` and `textViewDidChangeSelection`
- private var isInsertingDictationResult = false
// MARK: - Font
@@ -355,16 +349,18 @@ class RCTAztecView: Aztec.TextView {
}
// MARK: - Dictation
-
- override func dictationRecordingDidEnd() {
- isInsertingDictationResult = true
- }
-
- public override func insertDictationResult(_ dictationResult: [UIDictationPhrase]) {
- let objectPlaceholder = "\u{FFFC}"
- let dictationText = dictationResult.reduce("") { $0 + $1.text }
- isInsertingDictationResult = false
- self.text = self.text?.replacingOccurrences(of: objectPlaceholder, with: dictationText)
+
+ func removeUnicodeAndRestoreCursor(from textView: UITextView) {
+ // Capture current cursor position
+ let originalPosition = textView.offset(from: textView.beginningOfDocument, to: textView.selectedTextRange?.start ?? textView.beginningOfDocument)
+
+ // Replace occurrences of the obj symbol ("\u{FFFC}")
+ textView.text = textView.text?.replacingOccurrences(of: "\u{FFFC}", with: "")
+
+ if let newPosition = textView.position(from: textView.beginningOfDocument, offset: originalPosition) {
+ // Move the cursor to the correct, new position following dictation
+ textView.selectedTextRange = textView.textRange(from: newPosition, to: newPosition)
+ }
}
// MARK: - Custom Edit Intercepts
@@ -771,7 +767,7 @@ class RCTAztecView: Aztec.TextView {
extension RCTAztecView: UITextViewDelegate {
func textViewDidChangeSelection(_ textView: UITextView) {
- guard isFirstResponder, isInsertingDictationResult == false else {
+ guard isFirstResponder else {
return
}
@@ -784,10 +780,13 @@ extension RCTAztecView: UITextViewDelegate {
}
func textViewDidChange(_ textView: UITextView) {
- guard isInsertingDictationResult == false else {
- return
+ // Workaround for RN dictation bug that adds obj symbol.
+ // Ref: https://github.com/facebook/react-native/issues/36521
+ // TODO: Remove workaround when RN issue is fixed
+ if textView.text?.contains("\u{FFFC}") == true {
+ removeUnicodeAndRestoreCursor(from: textView)
}
-
+
propagateContentChanges()
updatePlaceholderVisibility()
//Necessary to send height information to JS after pasting text.
diff --git a/packages/react-native-aztec/package.json b/packages/react-native-aztec/package.json
index c9f769f306a705..b25575de05e8cc 100644
--- a/packages/react-native-aztec/package.json
+++ b/packages/react-native-aztec/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/react-native-aztec",
- "version": "1.95.0",
+ "version": "1.96.1",
"description": "Aztec view for react-native.",
"private": true,
"author": "The WordPress Contributors",
diff --git a/packages/react-native-bridge/android/react-native-bridge/build.gradle b/packages/react-native-bridge/android/react-native-bridge/build.gradle
index ae22807d9495ad..07e8f7973da46b 100644
--- a/packages/react-native-bridge/android/react-native-bridge/build.gradle
+++ b/packages/react-native-bridge/android/react-native-bridge/build.gradle
@@ -14,11 +14,11 @@ group='org.wordpress-mobile.gutenberg-mobile'
def buildAssetsFolder = 'build/assets'
android {
- compileSdkVersion 31
+ compileSdkVersion 33
defaultConfig {
minSdkVersion 24
- targetSdkVersion 31
+ targetSdkVersion 33
buildConfigField "boolean", "SHOULD_ATTACH_JS_BUNDLE", willPublishReactNativeBridgeBinary.toString()
}
@@ -82,7 +82,7 @@ dependencies {
// Published by `wordpress-mobile/react-native-libraries-publisher`
// See the documentation for this value in `build.gradle.kts` of `wordpress-mobile/react-native-libraries-publisher`
- def reactNativeLibrariesPublisherVersion = "v1"
+ def reactNativeLibrariesPublisherVersion = "v2"
def reactNativeLibrariesGroupId = "org.wordpress-mobile.react-native-libraries.$reactNativeLibrariesPublisherVersion"
implementation "$reactNativeLibrariesGroupId:react-native-get-random-values:${extractPackageVersion(packageJson, 'react-native-get-random-values', 'dependencies')}"
implementation "$reactNativeLibrariesGroupId:react-native-safe-area-context:${extractPackageVersion(packageJson, 'react-native-safe-area-context', 'dependencies')}"
diff --git a/packages/react-native-bridge/android/settings.gradle b/packages/react-native-bridge/android/settings.gradle
index d7374ea3743bbf..c7a944de425ae1 100644
--- a/packages/react-native-bridge/android/settings.gradle
+++ b/packages/react-native-bridge/android/settings.gradle
@@ -1,5 +1,5 @@
pluginManagement {
- gradle.ext.kotlinVersion = '1.5.32'
+ gradle.ext.kotlinVersion = '1.6.10'
plugins {
id "com.android.library" version "7.2.1"
diff --git a/packages/react-native-bridge/common/gutenberg-web-single-block/editor-behavior-overrides.js b/packages/react-native-bridge/common/gutenberg-web-single-block/editor-behavior-overrides.js
index 09dcd6447824d7..1a4f4422a47ba2 100644
--- a/packages/react-native-bridge/common/gutenberg-web-single-block/editor-behavior-overrides.js
+++ b/packages/react-native-bridge/common/gutenberg-web-single-block/editor-behavior-overrides.js
@@ -77,9 +77,8 @@ if ( isAndroid() ) {
manageTextSelectonContextMenu();
}
-const editor = document.querySelector( '#editor' );
-
function _toggleBlockSelectedClass( isBlockSelected ) {
+ const editor = document.querySelector( '#editor' );
if ( isBlockSelected ) {
editor.classList.add( 'is-block-selected' );
} else {
diff --git a/packages/react-native-bridge/package.json b/packages/react-native-bridge/package.json
index e632331fa2485e..ec7cdadba56b4c 100644
--- a/packages/react-native-bridge/package.json
+++ b/packages/react-native-bridge/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/react-native-bridge",
- "version": "1.95.0",
+ "version": "1.96.1",
"description": "Native bridge library used to integrate the block editor into a native App.",
"private": true,
"author": "The WordPress Contributors",
diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md
index 725bf7615c5353..1ec6340bcd01fd 100644
--- a/packages/react-native-editor/CHANGELOG.md
+++ b/packages/react-native-editor/CHANGELOG.md
@@ -10,10 +10,19 @@ For each user feature we should also add a importance categorization label to i
-->
## Unreleased
+- [*] [internal] Upgrade compile and target sdk version to Android API 33 [#50731]
+
+## 1.96.1
+- [**] Fix Android-only issue related to block toolbar not being displayed on some blocks in UBE [#51131]
+
+## 1.96.0
- [**] Tapping on all nested blocks gets focus directly instead of having to tap multiple times depending on the nesting levels. [#50672]
- [*] Add disabled style to `Cell` component [#50665]
+- [**] Fix undo/redo history when inserting a link configured to open in a new tab [#50460]
- [*] [List block] Fix an issue when merging a list item into a Paragraph would remove its nested list items. [#50701]
+- [**] [iOS] Fix dictation regression, in which typing/dictating at the same time caused content loss. [#49452]
+
## 1.95.0
- [*] Fix crash when trying to convert to regular blocks an undefined/deleted reusable block [#50475]
- [**] Tapping on nested text blocks gets focus directly instead of having to tap multiple times depeding on the nesting levels. [#50108]
diff --git a/packages/react-native-editor/android/app/build.gradle b/packages/react-native-editor/android/app/build.gradle
index d143a36952ee1c..c91c3c07238196 100644
--- a/packages/react-native-editor/android/app/build.gradle
+++ b/packages/react-native-editor/android/app/build.gradle
@@ -259,7 +259,7 @@ dependencies {
// Published by `wordpress-mobile/react-native-libraries-publisher`
// See the documentation for this value in `build.gradle.kts` of `wordpress-mobile/react-native-libraries-publisher`
- def reactNativeLibrariesPublisherVersion = "v1"
+ def reactNativeLibrariesPublisherVersion = "v2"
def reactNativeLibrariesGroupId = "org.wordpress-mobile.react-native-libraries.$reactNativeLibrariesPublisherVersion"
implementation "$reactNativeLibrariesGroupId:react-native-get-random-values:${extractPackageVersion(packageJson, 'react-native-get-random-values', 'dependencies')}"
implementation "$reactNativeLibrariesGroupId:react-native-safe-area-context:${extractPackageVersion(packageJson, 'react-native-safe-area-context', 'dependencies')}"
diff --git a/packages/react-native-editor/android/build.gradle b/packages/react-native-editor/android/build.gradle
index 4db14e2cd92918..873328466b3999 100644
--- a/packages/react-native-editor/android/build.gradle
+++ b/packages/react-native-editor/android/build.gradle
@@ -3,11 +3,11 @@ import org.apache.tools.ant.taskdefs.condition.Os
buildscript {
ext {
gradlePluginVersion = '7.2.1'
- kotlinVersion = '1.5.32'
- buildToolsVersion = "31.0.0"
+ kotlinVersion = '1.6.10'
+ buildToolsVersion = "33.0.0"
minSdkVersion = 24
- compileSdkVersion = 31
- targetSdkVersion = 31
+ compileSdkVersion = 33
+ targetSdkVersion = 33
supportLibVersion = '28.0.0'
gradleDownloadTask = '5.0.1'
diff --git a/packages/react-native-editor/bin/i18n-translations-download.js b/packages/react-native-editor/bin/i18n-translations-download.js
index 714c7c0d99f8ca..24911a3ed64b3a 100644
--- a/packages/react-native-editor/bin/i18n-translations-download.js
+++ b/packages/react-native-editor/bin/i18n-translations-download.js
@@ -60,23 +60,54 @@ const supportedLocales = [
'zh-tw', // Chinese (Taiwan)
];
+const MAX_RETRIES = 5;
+const RETRY_DELAY = 2000;
+
const getLanguageUrl = ( locale, projectSlug ) =>
- `https://translate.wordpress.org/projects/${ projectSlug }/dev/${ locale }/default/export-translations\?format\=json`;
+ `https://translate.wordpress.org/projects/${ projectSlug }/dev/${ locale }/default/export-translations/\?format\=json`;
const getTranslationFilePath = ( locale ) => `./data/${ locale }.json`;
const fetchTranslation = ( locale, projectSlug ) => {
+ let retryCount = MAX_RETRIES;
const localeUrl = getLanguageUrl( locale, projectSlug );
- return fetch( localeUrl )
- .then( ( response ) => response.json() )
- .then( ( body ) => {
- return { response: body, locale };
- } )
- .catch( () => {
- console.error(
- `Could not find translation file ${ localeUrl } for project slug ${ projectSlug }`
- );
- } );
+ const request = () =>
+ fetch( localeUrl )
+ .then( ( response ) => {
+ if ( ! response.ok ) {
+ const { status, statusText } = response;
+
+ // Retry when encountering "429 - Too Many Requests" error
+ if ( status === 429 && retryCount > 0 ) {
+ console.log(
+ `Translation file ${ localeUrl } for project slug ${ projectSlug } failed with error 429 - Too Many Requests, retrying (${ retryCount })...`
+ );
+ retryCount--;
+ return new Promise( ( resolve ) =>
+ setTimeout(
+ () => request().then( resolve ),
+ RETRY_DELAY
+ )
+ );
+ }
+
+ console.error(
+ `Could not find translation file ${ localeUrl } for project slug ${ projectSlug }`,
+ { status, statusText }
+ );
+ return { locale, status, statusText };
+ }
+ return response.json();
+ } )
+ .then( ( body ) => {
+ return { response: body, locale };
+ } )
+ .catch( () => {
+ console.error(
+ `Could not find translation file ${ localeUrl } for project slug ${ projectSlug }`
+ );
+ } );
+ return request();
};
const fetchTranslations = ( {
@@ -104,7 +135,16 @@ const fetchTranslations = ( {
let extraTranslations = [];
return Promise.all( fetchPromises ).then( ( results ) => {
- const fetchedTranslations = results.filter( Boolean );
+ const fetchedTranslations = results.filter(
+ ( result ) => result.response
+ );
+
+ // Abort process if any translation can't be fetched
+ if ( fetchedTranslations.length !== supportedLocales.length ) {
+ process.exit( 1 );
+ return;
+ }
+
const translationFilePromises = fetchedTranslations.map(
( languageResult ) => {
return new Promise( ( resolve, reject ) => {
diff --git a/packages/react-native-editor/ios/Podfile.lock b/packages/react-native-editor/ios/Podfile.lock
index 8fcaecffe9aa3d..bcb1231b9c4886 100644
--- a/packages/react-native-editor/ios/Podfile.lock
+++ b/packages/react-native-editor/ios/Podfile.lock
@@ -1,6 +1,6 @@
PODS:
- boost (1.76.0)
- - BVLinearGradient (2.5.6-wp-3):
+ - BVLinearGradient (2.5.6-wp-4):
- React-Core
- DoubleConversion (1.1.6)
- FBLazyVector (0.69.4)
@@ -13,7 +13,7 @@ PODS:
- ReactCommon/turbomodule/core (= 0.69.4)
- fmt (6.2.1)
- glog (0.3.5)
- - Gutenberg (1.95.0):
+ - Gutenberg (1.96.1):
- React-Core (= 0.69.4)
- React-CoreModules (= 0.69.4)
- React-RCTImage (= 0.69.4)
@@ -244,14 +244,14 @@ PODS:
- React-Core
- react-native-safe-area-context (3.2.0):
- React-Core
- - react-native-slider (3.0.2-wp-3):
+ - react-native-slider (3.0.2-wp-4):
- React-Core
- - react-native-video (5.2.0-wp-5):
+ - react-native-video (5.2.0-wp-6):
- React-Core
- - react-native-video/Video (= 5.2.0-wp-5)
- - react-native-video/Video (5.2.0-wp-5):
+ - react-native-video/Video (= 5.2.0-wp-6)
+ - react-native-video/Video (5.2.0-wp-6):
- React-Core
- - react-native-webview (11.6.2):
+ - react-native-webview (11.26.1):
- React-Core
- React-perflogger (0.69.4)
- React-RCTActionSheet (0.69.4):
@@ -327,9 +327,9 @@ PODS:
- React-Core
- SDWebImage (~> 5.11.1)
- SDWebImageWebPCoder (~> 0.8.4)
- - RNGestureHandler (2.3.2-wp-2):
+ - RNGestureHandler (2.3.2-wp-3):
- React-Core
- - RNReanimated (2.9.1-wp-3):
+ - RNReanimated (2.9.1-wp-4):
- DoubleConversion
- FBLazyVector
- FBReactNativeSpec
@@ -360,7 +360,7 @@ PODS:
- React-Core
- RNSVG (9.13.6):
- React-Core
- - RNTAztecView (1.95.0):
+ - RNTAztecView (1.96.1):
- React-Core
- WordPress-Aztec-iOS (~> 1.19.8)
- SDWebImage (5.11.1):
@@ -534,13 +534,13 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
boost: a7c83b31436843459a1961bfd74b96033dc77234
- BVLinearGradient: ace34fab72158c068ae989a0ebdbf86cb4ef0e49
+ BVLinearGradient: f9b5ebce56b36e3e20a433aa271d6dce2c10b06a
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
FBLazyVector: c71b8c429a8af2aff1013934a7152e9d9d0c937d
FBReactNativeSpec: 2ff441cbe6e58c1778d8a5cf3311831a6a8c0809
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: 3d02b25ca00c2d456734d0bcff864cbc62f6ae1a
- Gutenberg: 98fc15135123cdfcfa6ad1fc35c8012014dab488
+ Gutenberg: 5cd4d25aa41b725c7802394e4f3b8eeddf90f370
libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c
RCT-Folly: b9d9fe1fc70114b751c076104e52f3b1b5e5a95a
RCTRequired: bd9d2ab0fda10171fcbcf9ba61a7df4dc15a28f4
@@ -560,9 +560,9 @@ SPEC CHECKSUMS:
react-native-get-random-values: b6fb85e7169b9822976793e467458c151c3e8b69
react-native-safe-area: c9cf765aa2dd96159476a99633e7d462ce5bb94f
react-native-safe-area-context: f0906bf8bc9835ac9a9d3f97e8bde2a997d8da79
- react-native-slider: a433f1c13c5da3c17a587351bff7371f65cc9a07
- react-native-video: cc1982bfac4d256fb56302642d968b6e72ffbeb7
- react-native-webview: 193d233c29eacce1f42ca2637dab7ba79c25a6de
+ react-native-slider: dff0d8a46f368a8d1bacd8638570d75b9b0be400
+ react-native-video: 6dee623307ed9d04d1be2de87494f9a0fa2041d1
+ react-native-webview: 9f111dfbcfc826084d6c507f569e5e03342ee1c1
React-perflogger: cb386fd44c97ec7f8199c04c12b22066b0f2e1e0
React-RCTActionSheet: f803a85e46cf5b4066c2ac5e122447f918e9c6e5
React-RCTAnimation: 19c80fa950ccce7f4db76a2a7f2cf79baae07fc7
@@ -578,11 +578,11 @@ SPEC CHECKSUMS:
RNCClipboard: 99fc8ad669a376b756fbc8098ae2fd05c0ed0668
RNCMaskedView: c298b644a10c0c142055b3ae24d83879ecb13ccd
RNFastImage: 1f2cab428712a4baaf78d6169eaec7f622556dd7
- RNGestureHandler: 3e0ea0c115175e66680032904339696bab928ca3
- RNReanimated: bea6acb5fdcbd8ca27641180579d09e3434f803c
+ RNGestureHandler: f5c389f7c9947057ee47d16ca1d7d170289b2c2a
+ RNReanimated: 5740ec9926f80bccd404bacd3e71108e87c94afa
RNScreens: 953633729a42e23ad0c93574d676b361e3335e8b
RNSVG: 36a7359c428dcb7c6bce1cc546fbfebe069809b0
- RNTAztecView: 95adb6b60e5d430ecf5eb710ff7813794d17ddc8
+ RNTAztecView: 59e8ceb3dd9473ad7f1e6cca3232caf3cd620009
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
WordPress-Aztec-iOS: 7d11d598f14c82c727c08b56bd35fbeb7dafb504
diff --git a/packages/react-native-editor/package.json b/packages/react-native-editor/package.json
index c70d9e013734e4..9aca508d859f3b 100644
--- a/packages/react-native-editor/package.json
+++ b/packages/react-native-editor/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/react-native-editor",
- "version": "1.95.0",
+ "version": "1.96.1",
"description": "Mobile WordPress gutenberg editor.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
@@ -32,7 +32,7 @@
"@babel/runtime": "^7.16.0",
"@react-native-clipboard/clipboard": "1.9.0",
"@react-native-community/blur": "4.2.0",
- "@react-native-community/slider": "https://raw.githubusercontent.com/wordpress-mobile/react-native-slider/v3.0.2-wp-3/react-native-community-slider-3.0.2-wp-3.tgz",
+ "@react-native-community/slider": "https://raw.githubusercontent.com/wordpress-mobile/react-native-slider/v3.0.2-wp-4/react-native-community-slider-3.0.2-wp-4.tgz",
"@react-native-masked-view/masked-view": "0.2.6",
"@react-navigation/core": "5.12.0",
"@react-navigation/native": "5.7.0",
@@ -58,22 +58,21 @@
"node-fetch": "^2.6.0",
"react-native": "0.69.4",
"react-native-fast-image": "8.5.11",
- "react-native-gesture-handler": "https://raw.githubusercontent.com/wordpress-mobile/react-native-gesture-handler/2.3.2-wp-2/react-native-gesture-handler-2.3.2-wp-2.tgz",
+ "react-native-gesture-handler": "https://raw.githubusercontent.com/wordpress-mobile/react-native-gesture-handler/2.3.2-wp-3/react-native-gesture-handler-2.3.2-wp-3.tgz",
"react-native-get-random-values": "1.4.0",
- "react-native-hr": "https://raw.githubusercontent.com/wordpress-mobile/react-native-hr/1.1.3-wp-1/react-native-hr-1.1.3.tgz",
- "react-native-hsv-color-picker": "https://raw.githubusercontent.com/wordpress-mobile/react-native-hsv-color-picker/v1.0.1-wp-3/react-native-hsv-color-picker-1.0.1-wp-3.tgz",
- "react-native-linear-gradient": "https://raw.githubusercontent.com/wordpress-mobile/react-native-linear-gradient/v2.5.6-wp-3/react-native-linear-gradient-2.5.6-wp-3.tgz",
+ "react-native-hsv-color-picker": "https://raw.githubusercontent.com/wordpress-mobile/react-native-hsv-color-picker/v1.0.1-wp-4/react-native-hsv-color-picker-1.0.1-wp-4.tgz",
+ "react-native-linear-gradient": "https://raw.githubusercontent.com/wordpress-mobile/react-native-linear-gradient/v2.5.6-wp-4/react-native-linear-gradient-2.5.6-wp-4.tgz",
"react-native-modal": "^11.10.0",
- "react-native-prompt-android": "https://raw.githubusercontent.com/wordpress-mobile/react-native-prompt-android/v1.0.0-wp-3/react-native-prompt-android-1.0.0-wp-3.tgz",
- "react-native-reanimated": "https://raw.githubusercontent.com/wordpress-mobile/react-native-reanimated/2.9.1-wp-3/react-native-reanimated-2.9.1-wp-3.tgz",
+ "react-native-prompt-android": "https://raw.githubusercontent.com/wordpress-mobile/react-native-prompt-android/v1.0.0-wp-4/react-native-prompt-android-1.0.0-wp-4.tgz",
+ "react-native-reanimated": "https://raw.githubusercontent.com/wordpress-mobile/react-native-reanimated/2.9.1-wp-4/react-native-reanimated-2.9.1-wp-4.tgz",
"react-native-safe-area": "^0.5.0",
"react-native-safe-area-context": "3.2.0",
"react-native-sass-transformer": "^1.1.1",
"react-native-screens": "2.9.0",
"react-native-svg": "9.13.6",
"react-native-url-polyfill": "^1.1.2",
- "react-native-video": "https://raw.githubusercontent.com/wordpress-mobile/react-native-video/5.2.0-wp-5/react-native-video-5.2.0-wp-5.tgz",
- "react-native-webview": "11.6.2"
+ "react-native-video": "https://raw.githubusercontent.com/wordpress-mobile/react-native-video/5.2.0-wp-6/react-native-video-5.2.0-wp-6.tgz",
+ "react-native-webview": "11.26.1"
},
"publishConfig": {
"access": "public"
diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md
index f255969c0865fe..48b96674fd3ef7 100644
--- a/packages/scripts/CHANGELOG.md
+++ b/packages/scripts/CHANGELOG.md
@@ -2,6 +2,15 @@
## Unreleased
+### Enhancements
+
+- The bundled `terser-webpack-plugin` dependency has been updated from requiring `^5.1.4` to requiring `^5.3.9` ([#50994](https://github.com/WordPress/gutenberg/pull/50994)).
+- Optimize updating render paths when developing blocks with the `start` command ([#51162](https://github.com/WordPress/gutenberg/pull/51162)).
+
+### Bug Fixes
+
+- Ensure files listed in `render` field of `block.json` files are always copied to the build folder when using the `start` command ([#50939](https://github.com/WordPress/gutenberg/pull/50939)).
+
## 26.5.0 (2023-05-24)
## 26.4.0 (2023-05-10)
diff --git a/packages/scripts/config/webpack.config.js b/packages/scripts/config/webpack.config.js
index f6998ab99cdeb7..c02fbfcbea2c96 100644
--- a/packages/scripts/config/webpack.config.js
+++ b/packages/scripts/config/webpack.config.js
@@ -38,8 +38,27 @@ if ( ! browserslist.findConfig( '.' ) ) {
}
const hasReactFastRefresh = hasArgInCLI( '--hot' ) && ! isProduction;
-// Get paths of the `render` props included in `block.json` files
-const renderPaths = getRenderPropPaths();
+/**
+ * The plugin recomputes the render paths once on each compilation. It is necessary to avoid repeating processing
+ * when filtering every discovered PHP file in the source folder. This is the most performant way to ensure that
+ * changes in `block.json` files are picked up in watch mode.
+ */
+class RenderPathsPlugin {
+ /**
+ * Paths with the `render` props included in `block.json` files.
+ *
+ * @type {string[]}
+ */
+ static renderPaths;
+
+ apply( compiler ) {
+ const pluginName = this.constructor.name;
+
+ compiler.hooks.thisCompilation.tap( pluginName, () => {
+ this.constructor.renderPaths = getRenderPropPaths();
+ } );
+ }
+}
const cssLoaders = [
{
@@ -234,6 +253,7 @@ const config = {
// multiple configurations returned in the webpack config.
cleanStaleWebpackAssets: false,
} ),
+ new RenderPathsPlugin(),
new CopyWebpackPlugin( {
patterns: [
{
@@ -277,7 +297,7 @@ const config = {
filter: ( filepath ) => {
return (
process.env.WP_COPY_PHP_FILES_TO_DIST ||
- renderPaths.includes( filepath )
+ RenderPathsPlugin.renderPaths.includes( filepath )
);
},
},
diff --git a/packages/scripts/package.json b/packages/scripts/package.json
index b189bb0dd61965..76a9dba2c70d25 100644
--- a/packages/scripts/package.json
+++ b/packages/scripts/package.json
@@ -82,7 +82,7 @@
"sass-loader": "^12.1.0",
"source-map-loader": "^3.0.0",
"stylelint": "^14.2.0",
- "terser-webpack-plugin": "^5.1.4",
+ "terser-webpack-plugin": "^5.3.9",
"url-loader": "^4.1.1",
"webpack": "^5.47.1",
"webpack-bundle-analyzer": "^4.4.2",
diff --git a/packages/server-side-render/README.md b/packages/server-side-render/README.md
index 9c780868931475..ba6fae302ca0a8 100644
--- a/packages/server-side-render/README.md
+++ b/packages/server-side-render/README.md
@@ -170,7 +170,7 @@ If you pass `attributes` to `ServerSideRender`, the block must also be registere
register_block_type(
'core/archives',
array(
- 'api_version' => 2,
+ 'api_version' => 3,
'attributes' => array(
'showPostCounts' => array(
'type' => 'boolean',
diff --git a/packages/widgets/src/blocks/legacy-widget/block.json b/packages/widgets/src/blocks/legacy-widget/block.json
index 30b60c6448835e..6b0c1e2a916fdd 100644
--- a/packages/widgets/src/blocks/legacy-widget/block.json
+++ b/packages/widgets/src/blocks/legacy-widget/block.json
@@ -1,5 +1,5 @@
{
- "apiVersion": 2,
+ "apiVersion": 3,
"name": "core/legacy-widget",
"title": "Legacy Widget",
"category": "widgets",
diff --git a/packages/widgets/src/blocks/widget-group/block.json b/packages/widgets/src/blocks/widget-group/block.json
index ec48d90eda5ca7..c29e811554ac11 100644
--- a/packages/widgets/src/blocks/widget-group/block.json
+++ b/packages/widgets/src/blocks/widget-group/block.json
@@ -1,5 +1,5 @@
{
- "apiVersion": 2,
+ "apiVersion": 3,
"name": "core/widget-group",
"category": "widgets",
"attributes": {
diff --git a/phpunit/block-supports/border-test.php b/phpunit/block-supports/border-test.php
index 7c8bd9975d35bb..858e4e92cc1740 100644
--- a/phpunit/block-supports/border-test.php
+++ b/phpunit/block-supports/border-test.php
@@ -36,7 +36,7 @@ private function register_bordered_block_with_support( $block_name, $supports =
register_block_type(
$this->test_block_name,
array(
- 'api_version' => 2,
+ 'api_version' => 3,
'attributes' => array(
'borderColor' => array(
'type' => 'string',
diff --git a/phpunit/block-supports/colors-test.php b/phpunit/block-supports/colors-test.php
index f21f6477f4162a..310b7e6c525cbf 100644
--- a/phpunit/block-supports/colors-test.php
+++ b/phpunit/block-supports/colors-test.php
@@ -28,7 +28,7 @@ public function test_color_slugs_with_numbers_are_kebab_cased_properly() {
register_block_type(
$this->test_block_name,
array(
- 'api_version' => 2,
+ 'api_version' => 3,
'attributes' => array(
'textColor' => array(
'type' => 'string',
@@ -69,7 +69,7 @@ public function test_color_with_skipped_serialization_block_supports() {
register_block_type(
$this->test_block_name,
array(
- 'api_version' => 2,
+ 'api_version' => 3,
'attributes' => array(
'style' => array(
'type' => 'object',
@@ -107,7 +107,7 @@ public function test_gradient_with_individual_skipped_serialization_block_suppor
register_block_type(
$this->test_block_name,
array(
- 'api_version' => 2,
+ 'api_version' => 3,
'attributes' => array(
'style' => array(
'type' => 'object',
diff --git a/phpunit/block-supports/dimensions-test.php b/phpunit/block-supports/dimensions-test.php
index 2ec0859b7cb7f8..d428977c2c0a6d 100644
--- a/phpunit/block-supports/dimensions-test.php
+++ b/phpunit/block-supports/dimensions-test.php
@@ -28,7 +28,7 @@ public function test_dimensions_style_is_applied() {
register_block_type(
$this->test_block_name,
array(
- 'api_version' => 2,
+ 'api_version' => 3,
'attributes' => array(
'style' => array(
'type' => 'object',
@@ -64,7 +64,7 @@ public function test_dimensions_with_skipped_serialization_block_supports() {
register_block_type(
$this->test_block_name,
array(
- 'api_version' => 2,
+ 'api_version' => 3,
'attributes' => array(
'style' => array(
'type' => 'object',
@@ -99,7 +99,7 @@ public function test_min_height_with_individual_skipped_serialization_block_supp
register_block_type(
$this->test_block_name,
array(
- 'api_version' => 2,
+ 'api_version' => 3,
'attributes' => array(
'style' => array(
'type' => 'object',
diff --git a/phpunit/block-supports/position-test.php b/phpunit/block-supports/position-test.php
index c304ffa68e7de5..d9f97060cf6478 100644
--- a/phpunit/block-supports/position-test.php
+++ b/phpunit/block-supports/position-test.php
@@ -78,7 +78,7 @@ public function test_position_block_support( $theme_name, $block_name, $position
register_block_type(
$this->test_block_name,
array(
- 'api_version' => 2,
+ 'api_version' => 3,
'attributes' => array(
'style' => array(
'type' => 'object',
diff --git a/phpunit/block-supports/spacing-test.php b/phpunit/block-supports/spacing-test.php
index 1ac0c86e620428..93df6f55ac109e 100644
--- a/phpunit/block-supports/spacing-test.php
+++ b/phpunit/block-supports/spacing-test.php
@@ -28,7 +28,7 @@ public function test_spacing_style_is_applied() {
register_block_type(
$this->test_block_name,
array(
- 'api_version' => 2,
+ 'api_version' => 3,
'attributes' => array(
'style' => array(
'type' => 'object',
@@ -73,7 +73,7 @@ public function test_spacing_with_skipped_serialization_block_supports() {
register_block_type(
$this->test_block_name,
array(
- 'api_version' => 2,
+ 'api_version' => 3,
'attributes' => array(
'style' => array(
'type' => 'object',
@@ -117,7 +117,7 @@ public function test_margin_with_individual_skipped_serialization_block_supports
register_block_type(
$this->test_block_name,
array(
- 'api_version' => 2,
+ 'api_version' => 3,
'attributes' => array(
'style' => array(
'type' => 'object',
diff --git a/phpunit/block-supports/typography-test.php b/phpunit/block-supports/typography-test.php
index 6fd81bcbeca6ca..5a2f52f780f1df 100644
--- a/phpunit/block-supports/typography-test.php
+++ b/phpunit/block-supports/typography-test.php
@@ -78,7 +78,7 @@ public function test_should_kebab_case_font_size_slug_with_numbers() {
register_block_type(
$this->test_block_name,
array(
- 'api_version' => 2,
+ 'api_version' => 3,
'attributes' => array(
'fontSize' => array(
'type' => 'string',
@@ -112,7 +112,7 @@ public function test_should_generate_font_family_with_legacy_inline_styles_using
register_block_type(
$this->test_block_name,
array(
- 'api_version' => 2,
+ 'api_version' => 3,
'attributes' => array(
'style' => array(
'type' => 'object',
@@ -145,7 +145,7 @@ public function test_should_skip_serialization_for_typography_block_supports() {
register_block_type(
$this->test_block_name,
array(
- 'api_version' => 2,
+ 'api_version' => 3,
'attributes' => array(
'style' => array(
'type' => 'object',
@@ -191,7 +191,7 @@ public function test_should_skip_serialization_for_letter_spacing_block_supports
register_block_type(
$this->test_block_name,
array(
- 'api_version' => 2,
+ 'api_version' => 3,
'attributes' => array(
'style' => array(
'type' => 'object',
@@ -227,7 +227,7 @@ public function test_should_generate_css_var_for_font_family_with_legacy_inline_
register_block_type(
$this->test_block_name,
array(
- 'api_version' => 2,
+ 'api_version' => 3,
'attributes' => array(
'style' => array(
'type' => 'object',
@@ -260,7 +260,7 @@ public function test_should_generate_classname_for_font_family() {
register_block_type(
$this->test_block_name,
array(
- 'api_version' => 2,
+ 'api_version' => 3,
'attributes' => array(
'style' => array(
'type' => 'object',
@@ -432,6 +432,30 @@ public function data_generate_font_size_preset_fixtures() {
'expected_output' => 'clamp(17.905px, 1.119rem + ((1vw - 3.2px) * 0.789), 28px)',
),
+ 'returns clamp value where min and max fluid values defined' => array(
+ 'font_size' => array(
+ 'size' => '80px',
+ 'fluid' => array(
+ 'min' => '70px',
+ 'max' => '125px',
+ ),
+ ),
+ 'should_use_fluid_typography' => true,
+ 'expected_output' => 'clamp(70px, 4.375rem + ((1vw - 3.2px) * 4.297), 125px)',
+ ),
+
+ 'returns clamp value where max is equal to size' => array(
+ 'font_size' => array(
+ 'size' => '7.8125rem',
+ 'fluid' => array(
+ 'min' => '4.375rem',
+ 'max' => '7.8125rem',
+ ),
+ ),
+ 'should_use_fluid_typography' => true,
+ 'expected_output' => 'clamp(4.375rem, 4.375rem + ((1vw - 0.2rem) * 4.298), 7.8125rem)',
+ ),
+
'returns clamp value if min font size is greater than max' => array(
'font_size' => array(
'size' => '3rem',
@@ -601,7 +625,7 @@ public function test_should_covert_font_sizes_to_fluid_values( $font_size_value,
register_block_type(
$this->test_block_name,
array(
- 'api_version' => 2,
+ 'api_version' => 3,
'attributes' => array(
'style' => array(
'type' => 'object',
diff --git a/phpunit/blocks/render-block-file-test.php b/phpunit/blocks/render-block-file-test.php
index 7fdeb60a707a9e..1dffc320d9671e 100644
--- a/phpunit/blocks/render-block-file-test.php
+++ b/phpunit/blocks/render-block-file-test.php
@@ -27,7 +27,13 @@ public function test_render_block_core_file() {
);
$content = '';
- $new_content = gutenberg_render_block_core_file( $attributes, $content );
+ $parsed_blocks = parse_blocks(
+ ''
+ );
+ $parsed_block = $parsed_blocks[0];
+ $block = new WP_Block( $parsed_block );
+
+ $new_content = gutenberg_render_block_core_file( $attributes, $content, $block );
$this->assertStringContainsString( 'aria-label="Embed of yolo."', $new_content );
}
@@ -45,7 +51,13 @@ public function test_render_block_core_file_custom_filename() {
);
$content = '';
- $new_content = gutenberg_render_block_core_file( $attributes, $content );
+ $parsed_blocks = parse_blocks(
+ ''
+ );
+ $parsed_block = $parsed_blocks[0];
+ $block = new WP_Block( $parsed_block );
+
+ $new_content = gutenberg_render_block_core_file( $attributes, $content, $block );
$this->assertStringContainsString( 'aria-label="Embed of custom filename."', $new_content );
}
@@ -63,7 +75,13 @@ public function test_render_block_core_file_empty_filename() {
);
$content = '';
- $new_content = gutenberg_render_block_core_file( $attributes, $content );
+ $parsed_blocks = parse_blocks(
+ ''
+ );
+ $parsed_block = $parsed_blocks[0];
+ $block = new WP_Block( $parsed_block );
+
+ $new_content = gutenberg_render_block_core_file( $attributes, $content, $block );
$this->assertStringContainsString( 'aria-label="PDF embed"', $new_content );
}
}
diff --git a/phpunit/blocks/render-comment-template-test.php b/phpunit/blocks/render-comment-template-test.php
new file mode 100644
index 00000000000000..c297d1729d0dc5
--- /dev/null
+++ b/phpunit/blocks/render-comment-template-test.php
@@ -0,0 +1,175 @@
+ $original_value ) {
+ update_option( $option, $original_value );
+ }
+
+ parent::tear_down();
+ }
+
+ public function set_up() {
+ parent::set_up();
+
+ update_option( 'page_comments', true );
+ update_option( 'comments_per_page', self::$per_page );
+
+ self::$custom_post = self::factory()->post->create_and_get(
+ array(
+ 'post_type' => 'dogs',
+ 'post_status' => 'publish',
+ 'post_name' => 'metaldog',
+ 'post_title' => 'Metal Dog',
+ 'post_content' => 'Metal Dog content',
+ 'post_excerpt' => 'Metal Dog',
+ )
+ );
+
+ self::$comment_ids = self::factory()->comment->create_post_comments(
+ self::$custom_post->ID,
+ 1,
+ array(
+ 'comment_author' => 'Test',
+ 'comment_author_email' => 'test@example.org',
+ 'comment_author_url' => 'http://example.com/author-url/',
+ 'comment_content' => 'Hello world',
+ )
+ );
+ }
+
+ public function test_rendering_comment_template_sets_comment_id_context() {
+ $parsed_comment_author_name_block = parse_blocks( '' )[0];
+ $comment_author_name_block = new WP_Block(
+ $parsed_comment_author_name_block,
+ array(
+ 'commentId' => self::$comment_ids[0],
+ )
+ );
+ $comment_author_name_block_markup = $comment_author_name_block->render();
+ $this->assertNotEmpty(
+ $comment_author_name_block_markup,
+ 'Comment Author Name block rendered markup is empty.'
+ );
+
+ $render_block_callback = static function( $block_content, $block ) use ( $parsed_comment_author_name_block ) {
+ // Insert a Comment Author Name block (which requires `commentId`
+ // block context to work) after the Comment Content block.
+ if ( 'core/comment-content' !== $block['blockName'] ) {
+ return $block_content;
+ }
+
+ $inserted_content = render_block( $parsed_comment_author_name_block );
+ return $inserted_content . $block_content;
+ };
+
+ add_filter( 'render_block', $render_block_callback, 10, 3 );
+ $parsed_blocks = parse_blocks(
+ ''
+ );
+ $block = new WP_Block(
+ $parsed_blocks[0],
+ array(
+ 'postId' => self::$custom_post->ID,
+ )
+ );
+ $markup = $block->render();
+ remove_filter( 'render_block', $render_block_callback );
+
+ $this->assertStringContainsString(
+ $comment_author_name_block_markup,
+ $markup,
+ "Rendered markup doesn't contain Comment Author Name block."
+ );
+ }
+
+ public function test_inner_block_inserted_by_render_block_data_is_retained() {
+ $render_block_callback = new MockAction();
+ add_filter( 'render_block', array( $render_block_callback, 'filter' ), 10, 3 );
+
+ $render_block_data_callback = static function( $parsed_block ) {
+ // Add a Social Links block to a Comment Template block's inner blocks.
+ if ( 'core/comment-template' === $parsed_block['blockName'] ) {
+ $inserted_block_markup = <<
+
+'
+END;
+
+ $inserted_blocks = parse_blocks( $inserted_block_markup );
+
+ $parsed_block['innerBlocks'][] = $inserted_blocks[0];
+ }
+ return $parsed_block;
+ };
+
+ add_filter( 'render_block_data', $render_block_data_callback, 10, 1 );
+ $parsed_blocks = parse_blocks(
+ ''
+ );
+ $block = new WP_Block(
+ $parsed_blocks[0],
+ array(
+ 'postId' => self::$custom_post->ID,
+ )
+ );
+ $block->render();
+ remove_filter( 'render_block_data', $render_block_data_callback );
+
+ $this->assertSame( 5, $render_block_callback->get_call_count() );
+
+ $args = $render_block_callback->get_args();
+ $this->assertSame( 'core/comment-content', $args[0][2]->name );
+ $this->assertSame( 'core/comment-template', $args[1][2]->name );
+ $this->assertCount( 2, $args[1][2]->inner_blocks, "Inner block inserted by render_block_data filter wasn't retained." );
+ $this->assertInstanceOf(
+ 'WP_Block',
+ $args[1][2]->inner_blocks[1],
+ "Inner block inserted by render_block_data isn't a WP_Block class instance."
+ );
+ $this->assertSame(
+ 'core/social-links',
+ $args[1][2]->inner_blocks[1]->name,
+ "Inner block inserted by render_block_data isn't named as expected."
+ );
+ }
+}
diff --git a/phpunit/blocks/render-post-template-test.php b/phpunit/blocks/render-post-template-test.php
new file mode 100644
index 00000000000000..13b5623cdd5dda
--- /dev/null
+++ b/phpunit/blocks/render-post-template-test.php
@@ -0,0 +1,73 @@
+post->create_and_get(
+ array(
+ 'post_type' => 'post',
+ 'post_status' => 'publish',
+ 'post_name' => 'metaldog',
+ 'post_title' => 'Metal Dog',
+ 'post_content' => 'Metal Dog content',
+ 'post_excerpt' => 'Metal Dog',
+ )
+ );
+
+ self::$other_post = self::factory()->post->create_and_get(
+ array(
+ 'post_type' => 'post',
+ 'post_status' => 'publish',
+ 'post_name' => 'ceilingcat',
+ 'post_title' => 'Ceiling Cat',
+ 'post_content' => 'Ceiling Cat content',
+ 'post_excerpt' => 'Ceiling Cat',
+ )
+ );
+ }
+
+ public function test_rendering_post_template() {
+ $parsed_blocks = parse_blocks(
+ ''
+ );
+ $block = new WP_Block( $parsed_blocks[0] );
+ $markup = $block->render();
+
+ $post_id = self::$post->ID;
+ $other_post_id = self::$other_post->ID;
+
+ $expected = <<
+
+ Ceiling Cat
+
+
+
+ Metal Dog
+
+
+
+END;
+ $this->assertSame(
+ str_replace( array( "\n", "\t" ), '', $expected ),
+ str_replace( array( "\n", "\t" ), '', $markup )
+ );
+ }
+}
diff --git a/phpunit/bootstrap.php b/phpunit/bootstrap.php
index e15509e43143eb..c11233b1e89525 100644
--- a/phpunit/bootstrap.php
+++ b/phpunit/bootstrap.php
@@ -105,7 +105,7 @@ function gutenberg_register_test_block_for_feature_selectors() {
WP_Block_Type_Registry::get_instance()->register(
'test/test',
array(
- 'api_version' => 2,
+ 'api_version' => 3,
'attributes' => array(
'textColor' => array(
'type' => 'string',
diff --git a/phpunit/class-block-context-test.php b/phpunit/class-block-context-test.php
index 4cf30478aa5682..6c06cfd099bd4f 100644
--- a/phpunit/class-block-context-test.php
+++ b/phpunit/class-block-context-test.php
@@ -104,7 +104,7 @@ public function test_provides_block_context() {
'gutenberg/contextWithAssigned',
'gutenberg/contextWithoutDefault',
),
- 'render_callback' => function( $attributes, $content, $block ) use ( &$provided_context ) {
+ 'render_callback' => static function( $attributes, $content, $block ) use ( &$provided_context ) {
$provided_context[] = $block->context;
return '';
@@ -142,7 +142,7 @@ public function test_provides_default_context() {
'gutenberg/test-context-consumer',
array(
'uses_context' => array( 'postId', 'postType' ),
- 'render_callback' => function( $attributes, $content, $block ) use ( &$provided_context ) {
+ 'render_callback' => static function( $attributes, $content, $block ) use ( &$provided_context ) {
$provided_context[] = $block->context;
return '';
@@ -173,7 +173,7 @@ public function test_default_context_is_filterable() {
'gutenberg/test-context-consumer',
array(
'uses_context' => array( 'example' ),
- 'render_callback' => function( $attributes, $content, $block ) use ( &$provided_context ) {
+ 'render_callback' => static function( $attributes, $content, $block ) use ( &$provided_context ) {
$provided_context[] = $block->context;
return '';
@@ -181,7 +181,7 @@ public function test_default_context_is_filterable() {
)
);
- $filter_block_context = function( $context ) {
+ $filter_block_context = static function( $context ) {
$context['example'] = 'ok';
return $context;
};
diff --git a/phpunit/class-wp-rest-pattern-directory-controller-test.php b/phpunit/class-wp-rest-pattern-directory-controller-test.php
index 5f9561a1d5f27c..fb42db35ac9c4f 100644
--- a/phpunit/class-wp-rest-pattern-directory-controller-test.php
+++ b/phpunit/class-wp-rest-pattern-directory-controller-test.php
@@ -185,7 +185,7 @@ public function data_get_items_query_args() {
private static function capture_http_urls() {
add_filter(
'pre_http_request',
- function ( $preempt, $args, $url ) {
+ static function ( $preempt, $args, $url ) {
if ( 'api.wordpress.org' !== wp_parse_url( $url, PHP_URL_HOST ) ) {
return $preempt;
}
diff --git a/phpunit/class-wp-theme-json-resolver-test.php b/phpunit/class-wp-theme-json-resolver-test.php
index 4cc34bf97b32c7..1588aa4d603265 100644
--- a/phpunit/class-wp-theme-json-resolver-test.php
+++ b/phpunit/class-wp-theme-json-resolver-test.php
@@ -396,7 +396,7 @@ public function test_get_merged_data_returns_origin( $origin, $core_palette, $co
register_block_type(
'my/block-with-styles',
array(
- 'api_version' => 2,
+ 'api_version' => 3,
'attributes' => array(
'borderColor' => array(
'type' => 'string',
diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php
index 7d63f62a2c1f0e..043cd4916eac80 100644
--- a/phpunit/class-wp-theme-json-test.php
+++ b/phpunit/class-wp-theme-json-test.php
@@ -215,7 +215,7 @@ public function test_get_stylesheet_generates_base_fallback_gap_layout_styles( $
// Note the `base-layout-styles` includes a fallback gap for the Columns block for backwards compatibility.
$this->assertEquals(
- ':where(.is-layout-flex){gap: 0.5em;}body .is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-flex{display: flex;}body .is-layout-flex{flex-wrap: wrap;align-items: center;}:where(.wp-block-columns.is-layout-flex){gap: 2em;}',
+ ':where(.is-layout-flex){gap: 0.5em;}body .is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-flex{display: flex;}body .is-layout-flex{flex-wrap: wrap;align-items: center;}:where(.wp-block-columns.is-layout-flex){gap: 2em;}:where(.wp-block-post-template.is-layout-flex){gap: 1.25em;}',
$stylesheet
);
}
diff --git a/phpunit/fixtures/block.json b/phpunit/fixtures/block.json
index 41904ae8119623..2ab91788dc0d43 100644
--- a/phpunit/fixtures/block.json
+++ b/phpunit/fixtures/block.json
@@ -1,5 +1,5 @@
{
- "apiVersion": 2,
+ "apiVersion": 3,
"name": "my-plugin/notice",
"title": "Notice",
"category": "common",
diff --git a/phpunit/fonts-api/wp-fonts-testcase.php b/phpunit/fonts-api/wp-fonts-testcase.php
index f7f7fb04b50625..b7bff9234b9ed0 100644
--- a/phpunit/fonts-api/wp-fonts-testcase.php
+++ b/phpunit/fonts-api/wp-fonts-testcase.php
@@ -248,7 +248,7 @@ protected function get_reflection_method( $method_name ) {
protected function setup_registration_mocks( array $inputs, WP_Fonts $wp_fonts ) {
$mocks = array();
- $build_mock = function ( $font_family, $is_font_family = false ) use ( &$mocks, $wp_fonts ) {
+ $build_mock = static function ( $font_family, $is_font_family = false ) use ( &$mocks, $wp_fonts ) {
$mock = new stdClass();
$mock->deps = array();
$mock->extra = array( 'is_font_family' => $is_font_family );
diff --git a/phpunit/fonts-api/gutenbergAddRegisteredFontsToThemeJson-test.php b/phpunit/fonts-api/wpFontsResolver/addMissingFontsToThemeJson-test.php
similarity index 92%
rename from phpunit/fonts-api/gutenbergAddRegisteredFontsToThemeJson-test.php
rename to phpunit/fonts-api/wpFontsResolver/addMissingFontsToThemeJson-test.php
index 61ada8a403ea10..31bb4b1d409cc4 100644
--- a/phpunit/fonts-api/gutenbergAddRegisteredFontsToThemeJson-test.php
+++ b/phpunit/fonts-api/wpFontsResolver/addMissingFontsToThemeJson-test.php
@@ -1,18 +1,18 @@
assertInstanceOf( WP_Theme_JSON_Gutenberg::class, $actual, 'Instance of WP_Theme_JSON_Gutenberg should be returned' );
}
@@ -60,7 +60,7 @@ public function test_should_bail_out_when_no_registered_fonts( $theme ) {
switch_theme( $theme );
$data = new WP_Theme_JSON_Gutenberg( self::$theme_json_data[ $theme ] );
- $actual = gutenberg_add_registered_fonts_to_theme_json( $data );
+ $actual = WP_Fonts_Resolver::add_missing_fonts_to_theme_json( $data );
$this->assertEmpty( wp_fonts()->get_registered_font_families(), 'No fonts should be registered in Fonts API' );
$this->assertSame( $data, $actual, 'Same instance of WP_Theme_JSON_Gutenberg should be returned' );
@@ -92,7 +92,7 @@ public function test_should_add_non_theme_json_fonts( $theme, $fonts, $expected
wp_register_fonts( $fonts );
$data = new WP_Theme_JSON_Gutenberg( self::$theme_json_data[ $theme ] );
- $actual = gutenberg_add_registered_fonts_to_theme_json( $data );
+ $actual = WP_Fonts_Resolver::add_missing_fonts_to_theme_json( $data );
$this->assertNotSame( $data, $actual, 'New instance of WP_Theme_JSON_Gutenberg should be returned' );
$actual_raw_data = $actual->get_raw_data();
@@ -104,7 +104,7 @@ public function test_should_add_non_theme_json_fonts( $theme, $fonts, $expected
$this->assertContains(
$expected,
$actual_raw_data['settings']['typography']['fontFamilies']['theme'],
- 'Fonts should be added after running gutenberg_add_registered_fonts_to_theme_json()'
+ 'Fonts should be added after running WP_Fonts_Resolver::add_missing_fonts_to_theme_json()'
);
}
diff --git a/phpunit/fonts-api/gutenbergRegisterFontsFromThemeJson-test.php b/phpunit/fonts-api/wpFontsResolver/registerFontsFromThemeJson-test.php
similarity index 90%
rename from phpunit/fonts-api/gutenbergRegisterFontsFromThemeJson-test.php
rename to phpunit/fonts-api/wpFontsResolver/registerFontsFromThemeJson-test.php
index d9c7fbaee5fff1..c3bedd9e63e9e6 100644
--- a/phpunit/fonts-api/gutenbergRegisterFontsFromThemeJson-test.php
+++ b/phpunit/fonts-api/wpFontsResolver/registerFontsFromThemeJson-test.php
@@ -1,18 +1,18 @@
array(
@@ -33,7 +33,7 @@ public static function set_up_before_class() {
public function test_should_bails_out_when_no_fonts_defined() {
switch_theme( 'block-theme' );
- gutenberg_register_fonts_from_theme_json();
+ WP_Fonts_Resolver::register_fonts_from_theme_json();
$wp_fonts = wp_fonts();
$this->assertEmpty( $wp_fonts->get_registered() );
@@ -43,7 +43,7 @@ public function test_should_bails_out_when_no_fonts_defined() {
public function test_should_register_and_enqueue_style_variation_fonts() {
switch_theme( static::FONTS_THEME );
- gutenberg_register_fonts_from_theme_json();
+ WP_Fonts_Resolver::register_fonts_from_theme_json();
$wp_fonts = wp_fonts();
$this->assertContains( 'open-sans', $wp_fonts->get_registered_font_families(), 'Font families should be registered' );
@@ -57,7 +57,7 @@ public function test_should_register_and_enqueue_style_variation_fonts() {
public function test_should_register_and_enqueue_all_defined_font_families() {
switch_theme( static::FONTS_THEME );
- gutenberg_register_fonts_from_theme_json();
+ WP_Fonts_Resolver::register_fonts_from_theme_json();
$wp_fonts = wp_fonts();
$expected = static::FONT_FAMILIES[ static::FONTS_THEME ];
@@ -74,7 +74,7 @@ public function test_should_register_and_enqueue_all_defined_font_families() {
public function test_should_not_reregister_duplicate_fonts_from_style_variations() {
switch_theme( static::FONTS_THEME );
- gutenberg_register_fonts_from_theme_json();
+ WP_Fonts_Resolver::register_fonts_from_theme_json();
$wp_fonts = wp_fonts();
// Font families are not duplicated.
@@ -117,7 +117,7 @@ public function test_should_not_reregister_duplicate_fonts_from_style_variations
public function test_should_replace_src_file_placeholder( $handle, $expected ) {
switch_theme( static::FONTS_THEME );
- gutenberg_register_fonts_from_theme_json();
+ WP_Fonts_Resolver::register_fonts_from_theme_json();
$variation = wp_fonts()->registered[ $handle ];
$actual = array_pop( $variation->src );
@@ -185,7 +185,7 @@ public function data_should_replace_src_file_placeholder() {
public function test_should_convert_font_face_properties_into_kebab_case() {
switch_theme( static::FONTS_THEME );
- gutenberg_register_fonts_from_theme_json();
+ WP_Fonts_Resolver::register_fonts_from_theme_json();
// Testing only one variation since this theme's fonts use the same properties.
$variation = wp_fonts()->registered['dm-sans-400-normal'];
@@ -202,12 +202,12 @@ public function test_should_convert_font_face_properties_into_kebab_case() {
}
/**
- * Tests that gutenberg_register_fonts_from_theme_json() skips fonts that are already registered
+ * Tests that WP_Fonts_Resolver::register_fonts_from_theme_json() skips fonts that are already registered
* in the Fonts API. How does it do that? Using the 'origin' property when checking each variation.
* This property is added when WP_Theme_JSON_Resolver_Gutenberg::get_merged_data() runs.
*
* To simulate this scenario, a font is registered first, but not enqueued. Then after running,
- * it checks if the gutenberg_register_fonts_from_theme_json() enqueued the font. If no, then
+ * it checks if the WP_Fonts_Resolver::register_fonts_from_theme_json() enqueued the font. If no, then
* it was skipped as expected.
*/
public function test_should_skip_registered_fonts() {
@@ -234,13 +234,13 @@ public function test_should_skip_registered_fonts() {
);
// Pre-check to ensure no fonts are enqueued.
- $this->assertEmpty( wp_fonts()->get_enqueued(), 'No fonts should be enqueued before running gutenberg_register_fonts_from_theme_json()' );
+ $this->assertEmpty( wp_fonts()->get_enqueued(), 'No fonts should be enqueued before running WP_Fonts_Resolver::register_fonts_from_theme_json()' );
/*
* When this function runs, it invokes WP_Theme_JSON_Resolver_Gutenberg::get_merged_data(),
* which will include the Lato fonts with a 'origin' property set in each variation.
*/
- gutenberg_register_fonts_from_theme_json();
+ WP_Fonts_Resolver::register_fonts_from_theme_json();
$actual_enqueued_fonts = wp_fonts()->get_enqueued();
@@ -254,7 +254,7 @@ public function test_should_skip_when_font_face_not_defined() {
/**
* Callback that removes the 'fontFace' of the expected font family from the theme's theme.json data.
- * This callback is invoked at the start of gutenberg_register_fonts_from_theme_json() before processing
+ * This callback is invoked at the start of WP_Fonts_Resolver::register_fonts_from_theme_json() before processing
* within that function. How? It's in the call stack of WP_Theme_JSON_Resolver_Gutenberg::get_merged_data().
*
* @param WP_Theme_JSON_Data_Gutenberg| WP_Theme_JSON_Data $theme_json_data Instance of the Data object.
@@ -288,7 +288,7 @@ public function test_should_skip_when_font_face_not_defined() {
};
add_filter( 'wp_theme_json_data_theme', $remove_expected_font_family );
- gutenberg_register_fonts_from_theme_json();
+ WP_Fonts_Resolver::register_fonts_from_theme_json();
remove_filter( 'wp_theme_json_data_theme', $remove_expected_font_family );
diff --git a/schemas/json/block.json b/schemas/json/block.json
index 5b92a654fbc4a5..6ca40ba90f9615 100644
--- a/schemas/json/block.json
+++ b/schemas/json/block.json
@@ -17,9 +17,9 @@
},
"apiVersion": {
"type": "integer",
- "description": "The version of the Block API used by the block. The most recent version is 2 and it was introduced in WordPress 5.6.\n\n See the API versions documentation at https://developer.wordpress.org/block-editor/reference-guides/block-api/block-api-versions/ for more details.",
+ "description": "The version of the Block API used by the block. The most recent version is 3 and it was introduced in WordPress 6.3.\n\n See the API versions documentation at https://developer.wordpress.org/block-editor/reference-guides/block-api/block-api-versions/ for more details.",
"default": 1,
- "enum": [ 1, 2 ]
+ "enum": [ 1, 2, 3 ]
},
"name": {
"type": "string",
diff --git a/schemas/json/theme.json b/schemas/json/theme.json
index 5f9fd13c041575..8c2f06d5c7c22f 100644
--- a/schemas/json/theme.json
+++ b/schemas/json/theme.json
@@ -487,7 +487,7 @@
"type": "string"
},
"fluid": {
- "description": "Specifics the minimum and maximum font size value of a fluid font size. Set to `false` to bypass fluid calculations and use the static `size` value.",
+ "description": "Specifies the minimum and maximum font size value of a fluid font size. Set to `false` to bypass fluid calculations and use the static `size` value.",
"oneOf": [
{
"type": "object",
diff --git a/test/e2e/specs/editor/blocks/avatar.spec.js b/test/e2e/specs/editor/blocks/avatar.spec.js
index bbce1eede94da3..8bf39a7a60dbac 100644
--- a/test/e2e/specs/editor/blocks/avatar.spec.js
+++ b/test/e2e/specs/editor/blocks/avatar.spec.js
@@ -37,7 +37,7 @@ test.describe( 'Avatar', () => {
const username = 'Gravatar Gravatar';
- const avatarBlock = page.locator(
+ const avatarBlock = editor.canvas.locator(
'role=document[name="Block: Avatar"i]'
);
diff --git a/test/e2e/specs/editor/blocks/buttons.spec.js b/test/e2e/specs/editor/blocks/buttons.spec.js
index 13d5759ee7db9c..8eacb7e2bed2e9 100644
--- a/test/e2e/specs/editor/blocks/buttons.spec.js
+++ b/test/e2e/specs/editor/blocks/buttons.spec.js
@@ -27,7 +27,7 @@ test.describe( 'Buttons', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '/buttons' );
await page.keyboard.press( 'Enter' );
await page.keyboard.type( 'Content' );
@@ -56,7 +56,7 @@ test.describe( 'Buttons', () => {
).toBeFocused();
await page.keyboard.press( 'Escape' );
await expect(
- page.locator( 'role=textbox[name="Button text"i]' )
+ editor.canvas.locator( 'role=textbox[name="Button text"i]' )
).toBeFocused();
await page.keyboard.type( 'WordPress' );
@@ -91,7 +91,7 @@ test.describe( 'Buttons', () => {
// Focus should move from the link control to the button block's text.
await expect(
- page.locator( 'role=textbox[name="Button text"i]' )
+ editor.canvas.locator( 'role=textbox[name="Button text"i]' )
).toBeFocused();
// The link control should still be visible when a URL is set.
diff --git a/test/e2e/specs/editor/blocks/classic.spec.js b/test/e2e/specs/editor/blocks/classic.spec.js
index cba1472caf916d..4403706444a7e9 100644
--- a/test/e2e/specs/editor/blocks/classic.spec.js
+++ b/test/e2e/specs/editor/blocks/classic.spec.js
@@ -18,8 +18,15 @@ test.use( {
} );
test.describe( 'Classic', () => {
- test.beforeEach( async ( { admin } ) => {
+ test.beforeEach( async ( { admin, page } ) => {
await admin.createNewPost();
+ // To do: run with iframe.
+ await page.evaluate( () => {
+ window.wp.blocks.registerBlockType( 'test/v2', {
+ apiVersion: '2',
+ title: 'test',
+ } );
+ } );
} );
test.afterAll( async ( { requestUtils } ) => {
@@ -126,6 +133,14 @@ test.describe( 'Classic', () => {
await page.reload();
await page.unroute( '**' );
+ // To do: run with iframe.
+ await page.evaluate( () => {
+ window.wp.blocks.registerBlockType( 'test/v2', {
+ apiVersion: '2',
+ title: 'test',
+ } );
+ } );
+
const errors = [];
page.on( 'pageerror', ( exception ) => {
errors.push( exception );
diff --git a/test/e2e/specs/editor/blocks/code.spec.js b/test/e2e/specs/editor/blocks/code.spec.js
index 80f41779b9131e..c4037d50b7dd51 100644
--- a/test/e2e/specs/editor/blocks/code.spec.js
+++ b/test/e2e/specs/editor/blocks/code.spec.js
@@ -12,7 +12,7 @@ test.describe( 'Code', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '```' );
await page.keyboard.press( 'Enter' );
await page.keyboard.type( ' {
} ) => {
// Open Columns
await editor.insertBlock( { name: 'core/columns' } );
- await page.locator( '[aria-label="Two columns; equal split"]' ).click();
+ await editor.canvas
+ .locator( '[aria-label="Two columns; equal split"]' )
+ .click();
// Open List view toggle
await page.locator( 'role=button[name="Document Overview"i]' ).click();
@@ -51,13 +53,15 @@ test.describe( 'Columns', () => {
} ) => {
// Open Columns
await editor.insertBlock( { name: 'core/columns' } );
- await page
+ await editor.canvas
.locator( '[aria-label="Three columns; equal split"]' )
.click();
// Lock last column block
await editor.selectBlocks(
- page.locator( 'role=document[name="Block: Column (3 of 3)"i]' )
+ editor.canvas.locator(
+ 'role=document[name="Block: Column (3 of 3)"i]'
+ )
);
await editor.clickBlockToolbarButton( 'Options' );
await page.click( 'role=menuitem[name="Lock"i]' );
@@ -66,7 +70,7 @@ test.describe( 'Columns', () => {
// Select columns block
await editor.selectBlocks(
- page.locator( 'role=document[name="Block: Columns"i]' )
+ editor.canvas.locator( 'role=document[name="Block: Columns"i]' )
);
await editor.openDocumentSettingsSidebar();
diff --git a/test/e2e/specs/editor/blocks/comments.spec.js b/test/e2e/specs/editor/blocks/comments.spec.js
index 55a4cdca8c1c0c..4b0dbf66b2a304 100644
--- a/test/e2e/specs/editor/blocks/comments.spec.js
+++ b/test/e2e/specs/editor/blocks/comments.spec.js
@@ -169,7 +169,7 @@ test.describe( 'Comments', () => {
'role=button[name="Switch to editable mode"i]'
);
- const commentTemplate = block.locator(
+ const commentTemplate = editor.canvas.locator(
'role=document[name="Block: Comment Template"i]'
);
await expect( block ).toHaveClass( /has-vivid-purple-color/ );
diff --git a/test/e2e/specs/editor/blocks/cover.spec.js b/test/e2e/specs/editor/blocks/cover.spec.js
index 02176167813450..90555eca548c2b 100644
--- a/test/e2e/specs/editor/blocks/cover.spec.js
+++ b/test/e2e/specs/editor/blocks/cover.spec.js
@@ -25,12 +25,11 @@ test.describe( 'Cover', () => {
} );
test( 'can set overlay color using color picker on block placeholder', async ( {
- page,
editor,
coverBlockUtils,
} ) => {
await editor.insertBlock( { name: 'core/cover' } );
- const coverBlock = page.getByRole( 'document', {
+ const coverBlock = editor.canvas.getByRole( 'document', {
name: 'Block: Cover',
} );
@@ -56,12 +55,11 @@ test.describe( 'Cover', () => {
} );
test( 'can set background image using image upload on block placeholder', async ( {
- page,
editor,
coverBlockUtils,
} ) => {
await editor.insertBlock( { name: 'core/cover' } );
- const coverBlock = page.getByRole( 'document', {
+ const coverBlock = editor.canvas.getByRole( 'document', {
name: 'Block: Cover',
} );
@@ -80,12 +78,11 @@ test.describe( 'Cover', () => {
} );
test( 'dims background image down by 50% by default', async ( {
- page,
editor,
coverBlockUtils,
} ) => {
await editor.insertBlock( { name: 'core/cover' } );
- const coverBlock = page.getByRole( 'document', {
+ const coverBlock = editor.canvas.getByRole( 'document', {
name: 'Block: Cover',
} );
@@ -104,11 +101,11 @@ test.describe( 'Cover', () => {
expect( backgroundDimOpacity ).toBe( '0.5' );
} );
- test( 'can have the title edited', async ( { page, editor } ) => {
+ test( 'can have the title edited', async ( { editor } ) => {
const titleText = 'foo';
await editor.insertBlock( { name: 'core/cover' } );
- const coverBlock = page.getByRole( 'document', {
+ const coverBlock = editor.canvas.getByRole( 'document', {
name: 'Block: Cover',
} );
@@ -134,7 +131,7 @@ test.describe( 'Cover', () => {
test( 'can be resized using drag & drop', async ( { page, editor } ) => {
await editor.insertBlock( { name: 'core/cover' } );
- const coverBlock = page.getByRole( 'document', {
+ const coverBlock = editor.canvas.getByRole( 'document', {
name: 'Block: Cover',
} );
await coverBlock
@@ -205,13 +202,12 @@ test.describe( 'Cover', () => {
} );
test( 'dims the background image down by 50% when transformed from the Image block', async ( {
- page,
editor,
coverBlockUtils,
} ) => {
await editor.insertBlock( { name: 'core/image' } );
- const imageBlock = page.getByRole( 'document', {
+ const imageBlock = editor.canvas.getByRole( 'document', {
name: 'Block: Image',
} );
@@ -220,14 +216,14 @@ test.describe( 'Cover', () => {
);
await expect(
- page
+ editor.canvas
.getByRole( 'document', { name: 'Block: Image' } )
.locator( 'img' )
).toBeVisible();
await editor.transformBlockTo( 'core/cover' );
- const coverBlock = page.getByRole( 'document', {
+ const coverBlock = editor.canvas.getByRole( 'document', {
name: 'Block: Cover',
} );
diff --git a/test/e2e/specs/editor/blocks/gallery.spec.js b/test/e2e/specs/editor/blocks/gallery.spec.js
index 1a71d3e46e6dd4..f950036539c11b 100644
--- a/test/e2e/specs/editor/blocks/gallery.spec.js
+++ b/test/e2e/specs/editor/blocks/gallery.spec.js
@@ -51,10 +51,10 @@ test.describe( 'Gallery', () => {
plainText: `[gallery ids="${ uploadedMedia.id }"]`,
} );
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await pageUtils.pressKeys( 'primary+v' );
- const img = page.locator(
+ const img = editor.canvas.locator(
'role=document[name="Block: Image"i] >> role=img'
);
@@ -87,12 +87,11 @@ test.describe( 'Gallery', () => {
test( 'can be created using uploaded images', async ( {
admin,
editor,
- page,
galleryBlockUtils,
} ) => {
await admin.createNewPost();
await editor.insertBlock( { name: 'core/gallery' } );
- const galleryBlock = page.locator(
+ const galleryBlock = editor.canvas.locator(
'role=document[name="Block: Gallery"i]'
);
await expect( galleryBlock ).toBeVisible();
@@ -132,7 +131,9 @@ test.describe( 'Gallery', () => {
],
} );
- const gallery = page.locator( 'role=document[name="Block: Gallery"i]' );
+ const gallery = editor.canvas.locator(
+ 'role=document[name="Block: Gallery"i]'
+ );
await expect( gallery ).toBeVisible();
await editor.selectBlocks( gallery );
@@ -173,7 +174,7 @@ test.describe( 'Gallery', () => {
],
} );
- const galleryImage = page.locator(
+ const galleryImage = editor.canvas.locator(
'role=document[name="Block: Gallery"i] >> role=document[name="Block: Image"i]'
);
const imageCaption = galleryImage.locator(
@@ -203,7 +204,7 @@ test.describe( 'Gallery', () => {
} ) => {
await admin.createNewPost();
await editor.insertBlock( { name: 'core/gallery' } );
- await page.click( 'role=button[name="Media Library"i]' );
+ await editor.canvas.click( 'role=button[name="Media Library"i]' );
const mediaLibrary = page.locator(
'role=dialog[name="Create gallery"i]'
diff --git a/test/e2e/specs/editor/blocks/group.spec.js b/test/e2e/specs/editor/blocks/group.spec.js
index 4cf284a0cdf69a..2de22245eb5b08 100644
--- a/test/e2e/specs/editor/blocks/group.spec.js
+++ b/test/e2e/specs/editor/blocks/group.spec.js
@@ -29,7 +29,7 @@ test.describe( 'Group', () => {
);
// Select the default, selected Group layout from the variation picker.
- await page.click(
+ await editor.canvas.click(
'role=button[name="Group: Gather blocks in a container."i]'
);
@@ -40,7 +40,7 @@ test.describe( 'Group', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '/group' );
await expect(
page.locator( 'role=option[name="Group"i][selected]' )
@@ -48,7 +48,7 @@ test.describe( 'Group', () => {
await page.keyboard.press( 'Enter' );
// Select the default, selected Group layout from the variation picker.
- await page.click(
+ await editor.canvas.click(
'role=button[name="Group: Gather blocks in a container."i]'
);
@@ -60,10 +60,10 @@ test.describe( 'Group', () => {
page,
} ) => {
await editor.insertBlock( { name: 'core/group' } );
- await page.click(
+ await editor.canvas.click(
'button[aria-label="Group: Gather blocks in a container."]'
);
- await page.click( 'role=button[name="Add block"i]' );
+ await editor.canvas.click( 'role=button[name="Add block"i]' );
await page.click(
'role=listbox[name="Blocks"i] >> role=option[name="Paragraph"i]'
);
diff --git a/test/e2e/specs/editor/blocks/heading.spec.js b/test/e2e/specs/editor/blocks/heading.spec.js
index e1b97d780f2929..1413b4cad9e9d3 100644
--- a/test/e2e/specs/editor/blocks/heading.spec.js
+++ b/test/e2e/specs/editor/blocks/heading.spec.js
@@ -12,7 +12,7 @@ test.describe( 'Heading', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '### 3' );
await expect.poll( editor.getBlocks ).toMatchObject( [
@@ -27,7 +27,7 @@ test.describe( 'Heading', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '4' );
await page.keyboard.press( 'ArrowLeft' );
await page.keyboard.type( '#### ' );
@@ -44,7 +44,7 @@ test.describe( 'Heading', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '## 1. H' );
await expect.poll( editor.getBlocks ).toMatchObject( [
@@ -59,7 +59,7 @@ test.describe( 'Heading', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '## `code`' );
await expect.poll( editor.getBlocks ).toMatchObject( [
@@ -115,7 +115,7 @@ test.describe( 'Heading', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '### Heading' );
await editor.openDocumentSettingsSidebar();
@@ -147,7 +147,7 @@ test.describe( 'Heading', () => {
} );
test( 'should correctly apply named colors', async ( { editor, page } ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '## Heading' );
await editor.openDocumentSettingsSidebar();
@@ -185,7 +185,7 @@ test.describe( 'Heading', () => {
page,
pageUtils,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '## Heading' );
// Change text alignment
@@ -216,7 +216,7 @@ test.describe( 'Heading', () => {
page,
pageUtils,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'Paragraph' );
// Change text alignment
@@ -247,7 +247,7 @@ test.describe( 'Heading', () => {
page,
pageUtils,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '## Heading' );
// Change text alignment
diff --git a/test/e2e/specs/editor/blocks/html.spec.js b/test/e2e/specs/editor/blocks/html.spec.js
index 77e9d2a9186a75..99a875f0810183 100644
--- a/test/e2e/specs/editor/blocks/html.spec.js
+++ b/test/e2e/specs/editor/blocks/html.spec.js
@@ -10,7 +10,7 @@ test.describe( 'HTML block', () => {
test( 'can be created by typing "/html"', async ( { editor, page } ) => {
// Create a Custom HTML block with the slash shortcut.
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '/html' );
await expect(
page.locator( 'role=option[name="Custom HTML"i][selected]' )
@@ -33,7 +33,7 @@ test.describe( 'HTML block', () => {
test( 'should not encode <', async ( { editor, page } ) => {
// Create a Custom HTML block with the slash shortcut.
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '/html' );
await expect(
page.locator( 'role=option[name="Custom HTML"i][selected]' )
@@ -42,8 +42,9 @@ test.describe( 'HTML block', () => {
await page.keyboard.type( '1 < 2' );
await editor.publishPost();
await page.reload();
+ await page.waitForSelector( '[name="editor-canvas"]' );
await expect(
- page.locator( '[data-type="core/html"] textarea' )
+ editor.canvas.locator( '[data-type="core/html"] textarea' )
).toBeVisible();
} );
} );
diff --git a/test/e2e/specs/editor/blocks/image.spec.js b/test/e2e/specs/editor/blocks/image.spec.js
index 123d734f58279d..2516a6d9c5ceaa 100644
--- a/test/e2e/specs/editor/blocks/image.spec.js
+++ b/test/e2e/specs/editor/blocks/image.spec.js
@@ -32,10 +32,10 @@ test.describe( 'Image', () => {
await requestUtils.deleteAllMedia();
} );
- test( 'can be inserted', async ( { editor, page, imageBlockUtils } ) => {
+ test( 'can be inserted', async ( { editor, imageBlockUtils } ) => {
await editor.insertBlock( { name: 'core/image' } );
- const imageBlock = page.locator(
+ const imageBlock = editor.canvas.locator(
'role=document[name="Block: Image"i]'
);
await expect( imageBlock ).toBeVisible();
@@ -63,7 +63,7 @@ test.describe( 'Image', () => {
} ) => {
await editor.insertBlock( { name: 'core/image' } );
- const imageBlock = page.locator(
+ const imageBlock = editor.canvas.locator(
'role=document[name="Block: Image"i]'
);
const image = imageBlock.locator( 'role=img' );
@@ -134,7 +134,7 @@ test.describe( 'Image', () => {
{
// Focus outside the block to avoid the image caption being selected
// It can happen on CI specially.
- await page.click( 'role=textbox[name="Add title"i]' );
+ await editor.canvas.click( 'role=textbox[name="Add title"i]' );
await image.click();
await page.keyboard.press( 'Backspace' );
@@ -149,7 +149,7 @@ test.describe( 'Image', () => {
} ) => {
await editor.insertBlock( { name: 'core/image' } );
- const imageBlock = page.locator(
+ const imageBlock = editor.canvas.locator(
'role=document[name="Block: Image"i]'
);
const image = imageBlock.locator( 'role=img' );
@@ -165,7 +165,9 @@ test.describe( 'Image', () => {
await page.keyboard.type( '2' );
expect(
- await page.evaluate( () => document.activeElement.innerHTML )
+ await editor.canvas.evaluate(
+ () => document.activeElement.innerHTML
+ )
).toBe( '12' );
} );
@@ -176,7 +178,7 @@ test.describe( 'Image', () => {
} ) => {
await editor.insertBlock( { name: 'core/image' } );
- const imageBlock = page.locator(
+ const imageBlock = editor.canvas.locator(
'role=document[name="Block: Image"i]'
);
const image = imageBlock.locator( 'role=img' );
@@ -193,7 +195,9 @@ test.describe( 'Image', () => {
await page.keyboard.press( 'Enter' );
expect(
- await page.evaluate( () => document.activeElement.innerHTML )
+ await editor.canvas.evaluate(
+ () => document.activeElement.innerHTML
+ )
).toBe( '1 2' );
} );
@@ -205,7 +209,7 @@ test.describe( 'Image', () => {
} ) => {
await editor.insertBlock( { name: 'core/image' } );
- const imageBlock = page.locator(
+ const imageBlock = editor.canvas.locator(
'role=document[name="Block: Image"i]'
);
const image = imageBlock.locator( 'role=img' );
@@ -245,7 +249,9 @@ test.describe( 'Image', () => {
await page.keyboard.press( 'ArrowRight' );
expect(
- await page.evaluate( () => document.activeElement.innerHTML )
+ await editor.canvas.evaluate(
+ () => document.activeElement.innerHTML
+ )
).toBe( 'a ' );
} );
@@ -256,7 +262,7 @@ test.describe( 'Image', () => {
} ) => {
await editor.insertBlock( { name: 'core/image' } );
- const imageBlock = page.locator(
+ const imageBlock = editor.canvas.locator(
'role=document[name="Block: Image"i]'
);
const image = imageBlock.locator( 'role=img' );
@@ -300,7 +306,7 @@ test.describe( 'Image', () => {
// Insert the block, upload a file and crop.
await editor.insertBlock( { name: 'core/image' } );
- const imageBlock = page.locator(
+ const imageBlock = editor.canvas.locator(
'role=document[name="Block: Image"i]'
);
const image = imageBlock.locator( 'role=img' );
@@ -339,7 +345,7 @@ test.describe( 'Image', () => {
// Wait for the cropping tools to disappear.
await expect(
- page.locator( 'role=button[name="Apply"i]' )
+ page.locator( 'role=button[name="Save"i]' )
).toBeHidden();
// Assert that the image is edited.
@@ -366,7 +372,7 @@ test.describe( 'Image', () => {
// Insert the block, upload a file and crop.
await editor.insertBlock( { name: 'core/image' } );
- const imageBlock = page.locator(
+ const imageBlock = editor.canvas.locator(
'role=document[name="Block: Image"i]'
);
const image = imageBlock.locator( 'role=img' );
@@ -396,7 +402,7 @@ test.describe( 'Image', () => {
// Wait for the cropping tools to disappear.
await expect(
- page.locator( 'role=button[name="Apply"i]' )
+ page.locator( 'role=button[name="Save"i]' )
).toBeHidden();
// Assert that the image is edited.
@@ -423,7 +429,7 @@ test.describe( 'Image', () => {
// Insert the block, upload a file and crop.
await editor.insertBlock( { name: 'core/image' } );
- const imageBlock = page.locator(
+ const imageBlock = editor.canvas.locator(
'role=document[name="Block: Image"i]'
);
const image = imageBlock.locator( 'role=img' );
@@ -441,7 +447,7 @@ test.describe( 'Image', () => {
// Wait for the cropping tools to disappear.
await expect(
- page.locator( 'role=button[name="Apply"i]' )
+ page.locator( 'role=button[name="Save"i]' )
).toBeHidden();
// Assert that the image is edited.
@@ -459,7 +465,7 @@ test.describe( 'Image', () => {
} ) => {
await editor.insertBlock( { name: 'core/image' } );
- const imageBlock = page.locator(
+ const imageBlock = editor.canvas.locator(
'role=document[name="Block: Image"i]'
);
const image = imageBlock.locator( 'role=img' );
@@ -498,7 +504,7 @@ test.describe( 'Image', () => {
await page.click( 'role=button[name="Edit"i]' );
// Replace the url.
await page.fill( 'role=combobox[name="URL"i]', imageUrl );
- await page.click( 'role=button[name="Apply"i]' );
+ await page.click( 'role=button[name="Save"i]' );
const regex = new RegExp(
`
@@ -513,13 +519,12 @@ test.describe( 'Image', () => {
test( 'should undo without broken temporary state', async ( {
editor,
- page,
pageUtils,
imageBlockUtils,
} ) => {
await editor.insertBlock( { name: 'core/image' } );
- const imageBlock = page.locator(
+ const imageBlock = editor.canvas.locator(
'role=document[name="Block: Image"i]'
);
const image = imageBlock.locator( 'role=img' );
@@ -529,7 +534,7 @@ test.describe( 'Image', () => {
);
await expect( image ).toHaveAttribute( 'src', new RegExp( filename ) );
- await page.focus( '.wp-block-image' );
+ await editor.canvas.focus( '.wp-block-image' );
await pageUtils.pressKeys( 'primary+z' );
// Expect an empty image block (placeholder) rather than one with a
@@ -543,8 +548,15 @@ test.describe( 'Image', () => {
page,
editor,
} ) => {
+ // To do: run with iframe.
+ await page.evaluate( () => {
+ window.wp.blocks.registerBlockType( 'test/v2', {
+ apiVersion: '2',
+ title: 'test',
+ } );
+ } );
await editor.insertBlock( { name: 'core/image' } );
- const imageBlock = page.getByRole( 'document', {
+ const imageBlock = editor.canvas.getByRole( 'document', {
name: 'Block: Image',
} );
const blockLibrary = page.getByRole( 'region', {
@@ -637,7 +649,7 @@ test.describe( 'Image', () => {
editor,
} ) => {
await editor.insertBlock( { name: 'core/image' } );
- const imageBlock = page.getByRole( 'document', {
+ const imageBlock = editor.canvas.getByRole( 'document', {
name: 'Block: Image',
} );
@@ -698,7 +710,7 @@ test.describe( 'Image', () => {
page,
} ) => {
await editor.insertBlock( { name: 'core/image' } );
- const imageBlock = page.locator(
+ const imageBlock = editor.canvas.locator(
'role=document[name="Block: Image"i]'
);
await expect( imageBlock ).toBeVisible();
@@ -756,7 +768,7 @@ test.describe( 'Image - interactivity', () => {
await admin.createNewPost();
await editor.insertBlock( { name: 'core/image' } );
- const imageBlock = page.locator(
+ const imageBlock = editor.canvas.locator(
'role=document[name="Block: Image"i]'
);
await expect( imageBlock ).toBeVisible();
@@ -828,17 +840,15 @@ test.describe( 'Image - interactivity', () => {
const lightbox = page.locator( '.wp-lightbox-overlay' );
await expect( lightbox ).toBeHidden();
+ await page.getByRole( 'button', { name: 'Enlarge image' } ).click();
+
const image = lightbox.locator( 'img' );
await expect( image ).toHaveAttribute( 'src', new RegExp( filename ) );
- await page
- .getByRole( 'button', { name: 'Open image lightbox' } )
- .click();
-
await expect( lightbox ).toBeVisible();
- const closeButton = page.getByRole( 'button', {
- name: 'Close lightbox',
+ const closeButton = lightbox.getByRole( 'button', {
+ name: 'Close',
} );
await closeButton.click();
@@ -860,11 +870,11 @@ test.describe( 'Image - interactivity', () => {
await page.goto( `/?p=${ postId }` );
openLightboxButton = page.getByRole( 'button', {
- name: 'Open image lightbox',
+ name: 'Enlarge image',
} );
lightbox = page.getByRole( 'dialog' );
closeButton = lightbox.getByRole( 'button', {
- name: 'Close lightbox',
+ name: 'Close',
} );
} );
diff --git a/test/e2e/specs/editor/blocks/links.spec.js b/test/e2e/specs/editor/blocks/links.spec.js
new file mode 100644
index 00000000000000..6493b9effe8aab
--- /dev/null
+++ b/test/e2e/specs/editor/blocks/links.spec.js
@@ -0,0 +1,133 @@
+/**
+ * WordPress dependencies
+ */
+const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+
+test.describe( 'Links', () => {
+ test.beforeEach( async ( { admin } ) => {
+ await admin.createNewPost();
+ } );
+
+ test( `can be created by selecting text and using keyboard shortcuts`, async ( {
+ page,
+ editor,
+ pageUtils,
+ } ) => {
+ // Create a block with some text.
+ await editor.insertBlock( {
+ name: 'core/paragraph',
+ } );
+ await page.keyboard.type( 'This is Gutenberg' );
+
+ // Select some text.
+ await pageUtils.pressKeys( 'shiftAlt+ArrowLeft' );
+
+ // Press Cmd+K to insert a link.
+ await pageUtils.pressKeys( 'primary+K' );
+
+ // Type a URL.
+ await page.keyboard.type( 'https://wordpress.org/gutenberg' );
+
+ // Open settings.
+ await page.getByRole( 'button', { name: 'Link Settings' } ).click();
+
+ // Navigate to and toggle the "Open in new tab" checkbox.
+ const checkbox = page.getByLabel( 'Open in new tab' );
+ await checkbox.click();
+
+ // Toggle should still have focus and be checked.
+ await expect( checkbox ).toBeChecked();
+ await expect( checkbox ).toBeFocused();
+
+ // Ensure that the contents of the post have not been changed, since at
+ // this point the link is still not inserted.
+ await expect.poll( editor.getBlocks ).toMatchObject( [
+ {
+ name: 'core/paragraph',
+ attributes: { content: 'This is Gutenberg' },
+ },
+ ] );
+
+ // Tab back to the Submit and apply the link.
+ await page
+ //TODO: change to a better selector when https://github.com/WordPress/gutenberg/issues/51060 is resolved.
+ .locator( '.block-editor-link-control' )
+ .getByRole( 'button', { name: 'Save' } )
+ .click();
+
+ // The link should have been inserted.
+ await expect.poll( editor.getBlocks ).toMatchObject( [
+ {
+ name: 'core/paragraph',
+ attributes: {
+ content:
+ 'This is Gutenberg ',
+ },
+ },
+ ] );
+ } );
+
+ test( 'can update the url of an existing link', async ( {
+ page,
+ editor,
+ pageUtils,
+ } ) => {
+ // Create a block with some text.
+ await editor.insertBlock( {
+ name: 'core/paragraph',
+ } );
+ await page.keyboard.type( 'This is WordPress' );
+ // Select "WordPress".
+ await pageUtils.pressKeys( 'shiftAlt+ArrowLeft' );
+ await pageUtils.pressKeys( 'primary+k' );
+ await page.keyboard.type( 'w.org' );
+
+ await page
+ //TODO: change to a better selector when https://github.com/WordPress/gutenberg/issues/51060 is resolved.
+ .locator( '.block-editor-link-control' )
+ .getByRole( 'button', { name: 'Save' } )
+ .click();
+
+ await expect.poll( editor.getBlocks ).toMatchObject( [
+ {
+ name: 'core/paragraph',
+ attributes: {
+ content: 'This is WordPress ',
+ },
+ },
+ ] );
+
+ // Move caret back into the link.
+ await page.keyboard.press( 'ArrowLeft' );
+ await page.keyboard.press( 'ArrowLeft' );
+
+ // Edit link.
+ await pageUtils.pressKeys( 'primary+k' );
+ await page.getByPlaceholder( 'Search or type url' ).fill( '' );
+ await page.keyboard.type( 'wordpress.org' );
+
+ // Update the link.
+ await page.keyboard.press( 'Enter' );
+
+ // Navigate back to the popover.
+ await page.keyboard.press( 'ArrowLeft' );
+ await page.keyboard.press( 'ArrowLeft' );
+
+ // Navigate back to inputs to verify appears as changed.
+ await pageUtils.pressKeys( 'primary+k' );
+ const urlInputValue = await page
+ .getByPlaceholder( 'Search or type url' )
+ .inputValue();
+ expect( urlInputValue ).toContain( 'wordpress.org' );
+
+ await expect.poll( editor.getBlocks ).toMatchObject( [
+ {
+ name: 'core/paragraph',
+ attributes: {
+ content:
+ 'This is WordPress ',
+ },
+ },
+ ] );
+ } );
+} );
diff --git a/test/e2e/specs/editor/blocks/list.spec.js b/test/e2e/specs/editor/blocks/list.spec.js
index a4af98f0ba0578..c8be29ac2a64e8 100644
--- a/test/e2e/specs/editor/blocks/list.spec.js
+++ b/test/e2e/specs/editor/blocks/list.spec.js
@@ -13,7 +13,7 @@ test.describe( 'List (@firefox)', () => {
page,
} ) => {
// Create a block with some text that will trigger a list creation.
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '* A list item' );
// Create a second list item.
@@ -38,7 +38,7 @@ test.describe( 'List (@firefox)', () => {
pageUtils,
} ) => {
// Create a list with the slash block shortcut.
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'test' );
await pageUtils.pressKeys( 'ArrowLeft', { times: 4 } );
await page.keyboard.type( '* ' );
@@ -56,7 +56,7 @@ test.describe( 'List (@firefox)', () => {
page,
} ) => {
// Create a block with some text that will trigger a list creation.
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '1) A list item' );
await expect.poll( editor.getEditedPostContent ).toBe(
@@ -73,7 +73,7 @@ test.describe( 'List (@firefox)', () => {
page,
pageUtils,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '1. ' );
await pageUtils.pressKeys( 'primary+z' );
@@ -88,7 +88,7 @@ test.describe( 'List (@firefox)', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '* ' );
await page.keyboard.press( 'Backspace' );
@@ -103,9 +103,11 @@ test.describe( 'List (@firefox)', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '* ' );
- await expect( page.locator( '[data-type="core/list"]' ) ).toBeVisible();
+ await expect(
+ editor.canvas.locator( '[data-type="core/list"]' )
+ ).toBeVisible();
await page.keyboard.press( 'Backspace' );
await expect.poll( editor.getEditedPostContent ).toBe(
@@ -119,7 +121,7 @@ test.describe( 'List (@firefox)', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '* ' );
await editor.showBlockToolbar();
await page.keyboard.press( 'Backspace' );
@@ -135,10 +137,12 @@ test.describe( 'List (@firefox)', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.evaluate( () => delete window.requestIdleCallback );
await page.keyboard.type( '* ' );
- await expect( page.locator( '[data-type="core/list"]' ) ).toBeVisible();
+ await expect(
+ editor.canvas.locator( '[data-type="core/list"]' )
+ ).toBeVisible();
await page.keyboard.press( 'Backspace' );
await expect.poll( editor.getEditedPostContent ).toBe(
@@ -152,7 +156,7 @@ test.describe( 'List (@firefox)', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '* ' );
await page.keyboard.press( 'Escape' );
@@ -167,7 +171,7 @@ test.describe( 'List (@firefox)', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '* a' );
await page.keyboard.press( 'Backspace' );
await page.keyboard.press( 'Backspace' );
@@ -179,9 +183,11 @@ test.describe( 'List (@firefox)', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '* ' );
- await expect( page.locator( '[data-type="core/list"]' ) ).toBeVisible();
+ await expect(
+ editor.canvas.locator( '[data-type="core/list"]' )
+ ).toBeVisible();
// Wait until the automatic change is marked as "final", which is done
// with an idle callback, see __unstableMarkAutomaticChange.
await page.evaluate( () => new Promise( window.requestIdleCallback ) );
@@ -194,7 +200,7 @@ test.describe( 'List (@firefox)', () => {
test( 'can be created by typing "/list"', async ( { editor, page } ) => {
// Create a list with the slash block shortcut.
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '/list' );
await expect(
page.locator( 'role=option[name="List"i][selected]' )
@@ -215,7 +221,7 @@ test.describe( 'List (@firefox)', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'test' );
await editor.transformBlockTo( 'core/list' );
@@ -232,12 +238,12 @@ test.describe( 'List (@firefox)', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'one' );
await page.keyboard.press( 'Enter' );
await page.keyboard.type( 'two' );
await page.keyboard.down( 'Shift' );
- await page.click( '[data-type="core/paragraph"] >> nth=0' );
+ await editor.canvas.click( '[data-type="core/paragraph"] >> nth=0' );
await page.keyboard.up( 'Shift' );
await editor.transformBlockTo( 'core/list' );
@@ -259,7 +265,7 @@ test.describe( 'List (@firefox)', () => {
page,
pageUtils,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'one' );
await pageUtils.pressKeys( 'shift+Enter' );
await page.keyboard.type( 'two' );
@@ -283,14 +289,14 @@ test.describe( 'List (@firefox)', () => {
page,
pageUtils,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'one' );
await pageUtils.pressKeys( 'shift+Enter' );
await page.keyboard.type( '...' );
await page.keyboard.press( 'Enter' );
await page.keyboard.type( 'two' );
await page.keyboard.down( 'Shift' );
- await page.click( '[data-type="core/paragraph"] >> nth=0' );
+ await editor.canvas.click( '[data-type="core/paragraph"] >> nth=0' );
await page.keyboard.up( 'Shift' );
await editor.transformBlockTo( 'core/list' );
@@ -555,7 +561,7 @@ test.describe( 'List (@firefox)', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '1. one' );
await page.keyboard.press( 'Enter' );
await page.keyboard.type( 'two' );
@@ -729,6 +735,13 @@ test.describe( 'List (@firefox)', () => {
} );
test( 'should indent and outdent level 2', async ( { editor, page } ) => {
+ // To do: run with iframe.
+ await page.evaluate( () => {
+ window.wp.blocks.registerBlockType( 'test/v2', {
+ apiVersion: '2',
+ title: 'test',
+ } );
+ } );
await editor.insertBlock( { name: 'core/list' } );
await page.keyboard.type( 'a' );
await page.keyboard.press( 'Enter' );
@@ -897,7 +910,7 @@ test.describe( 'List (@firefox)', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '* 1' ); // Should be at level 0.
await page.keyboard.press( 'Enter' );
@@ -1011,7 +1024,7 @@ test.describe( 'List (@firefox)', () => {
page,
pageUtils,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '* 1' );
await page.keyboard.press( 'Enter' );
await page.keyboard.type( ' a' );
@@ -1042,7 +1055,7 @@ test.describe( 'List (@firefox)', () => {
page,
pageUtils,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '* 1' );
await page.keyboard.press( 'Enter' );
@@ -1065,7 +1078,7 @@ test.describe( 'List (@firefox)', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
// Tests the shortcut with a non breaking space.
await page.keyboard.type( '*\u00a0' );
@@ -1081,7 +1094,7 @@ test.describe( 'List (@firefox)', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
// Tests the shortcut with a non breaking space.
await page.keyboard.type( '* 1' );
@@ -1145,7 +1158,7 @@ test.describe( 'List (@firefox)', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '* 1' );
await page.keyboard.press( 'Enter' );
await page.keyboard.type( '2' );
@@ -1170,7 +1183,7 @@ test.describe( 'List (@firefox)', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '* 1' );
await page.keyboard.press( 'Enter' );
await page.keyboard.type( '2' );
@@ -1200,7 +1213,7 @@ test.describe( 'List (@firefox)', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '1. a' );
await page.keyboard.press( 'Enter' );
await page.keyboard.type( 'b' );
@@ -1255,7 +1268,7 @@ test.describe( 'List (@firefox)', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '* a' );
await page.keyboard.press( 'Enter' );
await page.keyboard.type( 'b' );
@@ -1298,7 +1311,7 @@ test.describe( 'List (@firefox)', () => {
} );
test( 'can be exited to selected paragraph', async ( { editor, page } ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '* ' );
await page.keyboard.press( 'Enter' );
await page.keyboard.type( '1' );
@@ -1324,7 +1337,7 @@ test.describe( 'List (@firefox)', () => {
} );
await editor.selectBlocks(
- page.locator( 'role=document[name="Block: List"i]' )
+ editor.canvas.locator( 'role=document[name="Block: List"i]' )
);
await page.getByRole( 'button', { name: 'List', exact: true } ).click();
diff --git a/test/e2e/specs/editor/blocks/paragraph.spec.js b/test/e2e/specs/editor/blocks/paragraph.spec.js
index ecade19a1aaa40..1c35f6ddc8958c 100644
--- a/test/e2e/specs/editor/blocks/paragraph.spec.js
+++ b/test/e2e/specs/editor/blocks/paragraph.spec.js
@@ -28,7 +28,7 @@ test.describe( 'Paragraph', () => {
} );
await page.keyboard.type( '1' );
- const firstBlockTagName = await page.evaluate( () => {
+ const firstBlockTagName = await editor.canvas.evaluate( () => {
return document.querySelector( '[data-block]' ).tagName;
} );
@@ -59,10 +59,16 @@ test.describe( 'Paragraph', () => {
test( 'should allow dropping an image on an empty paragraph block', async ( {
editor,
- page,
pageUtils,
draggingUtils,
+ page,
} ) => {
+ await page.evaluate( () => {
+ window.wp.blocks.registerBlockType( 'test/v2', {
+ apiVersion: '2',
+ title: 'test',
+ } );
+ } );
await editor.insertBlock( { name: 'core/paragraph' } );
const testImageName = '10x10_e2e_test_image_z9T8jK.png';
@@ -76,14 +82,18 @@ test.describe( 'Paragraph', () => {
testImagePath
);
- await dragOver( '[data-type="core/paragraph"]' );
+ await dragOver(
+ editor.canvas.locator( '[data-type="core/paragraph"]' )
+ );
await expect( draggingUtils.dropZone ).toBeVisible();
await expect( draggingUtils.insertionIndicator ).not.toBeVisible();
- await drop();
+ await drop(
+ editor.canvas.locator( '[data-type="core/paragraph"]' )
+ );
- const imageBlock = page.locator(
+ const imageBlock = editor.canvas.locator(
'role=document[name="Block: Image"i]'
);
await expect( imageBlock ).toBeVisible();
@@ -103,7 +113,7 @@ test.describe( 'Paragraph', () => {
attributes: { content: 'My Heading' },
} );
await editor.insertBlock( { name: 'core/paragraph' } );
- await page.focus( 'text=My Heading' );
+ await editor.canvas.focus( 'text=My Heading' );
await editor.showBlockToolbar();
const dragHandle = page.locator(
@@ -112,7 +122,7 @@ test.describe( 'Paragraph', () => {
await dragHandle.hover();
await page.mouse.down();
- const emptyParagraph = page.locator(
+ const emptyParagraph = editor.canvas.locator(
'[data-type="core/paragraph"][data-empty="true"]'
);
const boundingBox = await emptyParagraph.boundingBox();
@@ -140,7 +150,7 @@ test.describe( 'Paragraph', () => {
'My Heading '
);
- const emptyParagraph = page.locator(
+ const emptyParagraph = editor.canvas.locator(
'[data-type="core/paragraph"][data-empty="true"]'
);
const boundingBox = await emptyParagraph.boundingBox();
@@ -160,7 +170,6 @@ test.describe( 'Paragraph', () => {
test.describe( 'Dragging positions', () => {
test( 'Only the first block is an empty paragraph block', async ( {
editor,
- page,
draggingUtils,
} ) => {
await editor.setContent( `
@@ -173,10 +182,10 @@ test.describe( 'Paragraph', () => {
` );
- const emptyParagraph = page.locator(
+ const emptyParagraph = editor.canvas.locator(
'[data-type="core/paragraph"]'
);
- const heading = page.locator( 'text=Heading' );
+ const heading = editor.canvas.locator( 'text=Heading' );
await draggingUtils.simulateDraggingHTML(
'Draggable '
@@ -271,7 +280,6 @@ test.describe( 'Paragraph', () => {
test( 'Only the second block is an empty paragraph block', async ( {
editor,
- page,
draggingUtils,
} ) => {
await editor.setContent( `
@@ -284,10 +292,10 @@ test.describe( 'Paragraph', () => {
` );
- const emptyParagraph = page.locator(
+ const emptyParagraph = editor.canvas.locator(
'[data-type="core/paragraph"]'
);
- const heading = page.locator( 'text=Heading' );
+ const heading = editor.canvas.locator( 'text=Heading' );
await draggingUtils.simulateDraggingHTML(
'Draggable '
@@ -382,7 +390,6 @@ test.describe( 'Paragraph', () => {
test( 'Both blocks are empty paragraph blocks', async ( {
editor,
- page,
draggingUtils,
} ) => {
await editor.setContent( `
@@ -395,10 +402,10 @@ test.describe( 'Paragraph', () => {
` );
- const firstEmptyParagraph = page
+ const firstEmptyParagraph = editor.canvas
.locator( '[data-type="core/paragraph"]' )
.first();
- const secondEmptyParagraph = page
+ const secondEmptyParagraph = editor.canvas
.locator( '[data-type="core/paragraph"]' )
.nth( 1 );
diff --git a/test/e2e/specs/editor/blocks/pullquote.spec.js b/test/e2e/specs/editor/blocks/pullquote.spec.js
index 9b20204a624ece..f2a6698f5065ff 100644
--- a/test/e2e/specs/editor/blocks/pullquote.spec.js
+++ b/test/e2e/specs/editor/blocks/pullquote.spec.js
@@ -12,7 +12,7 @@ test.describe( 'Quote', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'test' );
await editor.transformBlockTo( 'core/quote' );
diff --git a/test/e2e/specs/editor/blocks/quote.spec.js b/test/e2e/specs/editor/blocks/quote.spec.js
index bff5bb69685357..44645005ff05e2 100644
--- a/test/e2e/specs/editor/blocks/quote.spec.js
+++ b/test/e2e/specs/editor/blocks/quote.spec.js
@@ -33,7 +33,7 @@ test.describe( 'Quote', () => {
page,
} ) => {
// Create a block with some text that will trigger a paragraph creation.
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '> A quote' );
// Create a second paragraph.
await page.keyboard.press( 'Enter' );
@@ -56,7 +56,7 @@ test.describe( 'Quote', () => {
page,
pageUtils,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'test' );
await pageUtils.pressKeys( 'ArrowLeft', { times: 'test'.length } );
await page.keyboard.type( '> ' );
@@ -71,7 +71,7 @@ test.describe( 'Quote', () => {
test( 'can be created by typing "/quote"', async ( { editor, page } ) => {
// Create a list with the slash block shortcut.
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '/quote' );
await page.keyboard.press( 'Enter' );
await page.keyboard.type( 'I’m a quote' );
@@ -88,7 +88,7 @@ test.describe( 'Quote', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'test' );
await editor.transformBlockTo( 'core/quote' );
expect( await editor.getEditedPostContent() ).toBe(
@@ -104,12 +104,12 @@ test.describe( 'Quote', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'one' );
await page.keyboard.press( 'Enter' );
await page.keyboard.type( 'two' );
await page.keyboard.down( 'Shift' );
- await page.click(
+ await editor.canvas.click(
'role=document[name="Paragraph block"i] >> text=one'
);
await page.keyboard.up( 'Shift' );
diff --git a/test/e2e/specs/editor/blocks/separator.spec.js b/test/e2e/specs/editor/blocks/separator.spec.js
index 8f195392641c87..a2e088e14c3983 100644
--- a/test/e2e/specs/editor/blocks/separator.spec.js
+++ b/test/e2e/specs/editor/blocks/separator.spec.js
@@ -12,7 +12,7 @@ test.describe( 'Separator', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '---' );
await page.keyboard.press( 'Enter' );
diff --git a/test/e2e/specs/editor/blocks/spacer.spec.js b/test/e2e/specs/editor/blocks/spacer.spec.js
index 41e63c2e4e9dec..77e978a0df3027 100644
--- a/test/e2e/specs/editor/blocks/spacer.spec.js
+++ b/test/e2e/specs/editor/blocks/spacer.spec.js
@@ -10,7 +10,7 @@ test.describe( 'Spacer', () => {
test( 'can be created by typing "/spacer"', async ( { editor, page } ) => {
// Create a spacer with the slash block shortcut.
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '/spacer' );
await page.keyboard.press( 'Enter' );
@@ -22,11 +22,11 @@ test.describe( 'Spacer', () => {
editor,
} ) => {
// Create a spacer with the slash block shortcut.
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '/spacer' );
await page.keyboard.press( 'Enter' );
- const resizableHandle = page.locator(
+ const resizableHandle = editor.canvas.locator(
// Use class name selector until we have `data-testid` for the resize handles.
'role=document[name="Block: Spacer"i] >> css=.components-resizable-box__handle'
);
@@ -39,7 +39,7 @@ test.describe( 'Spacer', () => {
expect( await editor.getEditedPostContent() ).toMatchSnapshot();
await expect(
- page.locator( 'role=document[name="Block: Spacer"i]' )
+ editor.canvas.locator( 'role=document[name="Block: Spacer"i]' )
).toBeFocused();
} );
} );
diff --git a/test/e2e/specs/editor/blocks/table.spec.js b/test/e2e/specs/editor/blocks/table.spec.js
index d206089e3f4e3f..689989f9022a3d 100644
--- a/test/e2e/specs/editor/blocks/table.spec.js
+++ b/test/e2e/specs/editor/blocks/table.spec.js
@@ -15,7 +15,7 @@ test.describe( 'Table', () => {
await editor.insertBlock( { name: 'core/table' } );
// Check for existence of the column count field.
- const columnCountInput = page.locator(
+ const columnCountInput = editor.canvas.locator(
'role=spinbutton[name="Column count"i]'
);
await expect( columnCountInput ).toBeVisible();
@@ -27,7 +27,7 @@ test.describe( 'Table', () => {
await page.keyboard.type( '5' );
// Check for existence of the row count field.
- const rowCountInput = page.locator(
+ const rowCountInput = editor.canvas.locator(
'role=spinbutton[name="Row count"i]'
);
await expect( rowCountInput ).toBeVisible();
@@ -39,7 +39,7 @@ test.describe( 'Table', () => {
await page.keyboard.type( '10' );
// Create the table.
- await page.click( 'role=button[name="Create Table"i]' );
+ await editor.canvas.click( 'role=button[name="Create Table"i]' );
// Expect the post content to have a correctly sized table.
expect( await editor.getEditedPostContent() ).toMatchSnapshot();
@@ -49,10 +49,12 @@ test.describe( 'Table', () => {
await editor.insertBlock( { name: 'core/table' } );
// Create the table.
- await page.click( 'role=button[name="Create Table"i]' );
+ await editor.canvas.click( 'role=button[name="Create Table"i]' );
// Click the first cell and add some text.
- await page.click( 'role=textbox[name="Body cell text"i] >> nth=0' );
+ await editor.canvas.click(
+ 'role=textbox[name="Body cell text"i] >> nth=0'
+ );
await page.keyboard.type( 'This' );
// Navigate to the next cell and add some text.
@@ -90,7 +92,7 @@ test.describe( 'Table', () => {
await expect( footerSwitch ).toBeHidden();
// // Create the table.
- await page.click( 'role=button[name="Create Table"i]' );
+ await editor.canvas.click( 'role=button[name="Create Table"i]' );
// Expect the header and footer switches to be present now that the table has been created.
await page.click(
@@ -103,17 +105,17 @@ test.describe( 'Table', () => {
await headerSwitch.check();
await footerSwitch.check();
- await page.click(
+ await editor.canvas.click(
'role=rowgroup >> nth=0 >> role=textbox[name="Header cell text"i] >> nth=0'
);
await page.keyboard.type( 'header' );
- await page.click(
+ await editor.canvas.click(
'role=rowgroup >> nth=1 >> role=textbox[name="Body cell text"i] >> nth=0'
);
await page.keyboard.type( 'body' );
- await page.click(
+ await editor.canvas.click(
'role=rowgroup >> nth=2 >> role=textbox[name="Footer cell text"i] >> nth=0'
);
await page.keyboard.type( 'footer' );
@@ -137,7 +139,7 @@ test.describe( 'Table', () => {
await editor.openDocumentSettingsSidebar();
// Create the table.
- await page.click( 'role=button[name="Create Table"i]' );
+ await editor.canvas.click( 'role=button[name="Create Table"i]' );
// Toggle on the switches and add some content.
await page.click(
@@ -145,7 +147,9 @@ test.describe( 'Table', () => {
);
await page.locator( 'role=checkbox[name="Header section"i]' ).check();
await page.locator( 'role=checkbox[name="Footer section"i]' ).check();
- await page.click( 'role=textbox[name="Body cell text"i] >> nth=0' );
+ await editor.canvas.click(
+ 'role=textbox[name="Body cell text"i] >> nth=0'
+ );
// Add a column.
await editor.clickBlockToolbarButton( 'Edit table' );
@@ -154,7 +158,9 @@ test.describe( 'Table', () => {
// Expect the table to have 3 columns across the header, body and footer.
expect( await editor.getEditedPostContent() ).toMatchSnapshot();
- await page.click( 'role=textbox[name="Body cell text"i] >> nth=0' );
+ await editor.canvas.click(
+ 'role=textbox[name="Body cell text"i] >> nth=0'
+ );
// Delete a column.
await editor.clickBlockToolbarButton( 'Edit table' );
@@ -167,15 +173,17 @@ test.describe( 'Table', () => {
test( 'allows columns to be aligned', async ( { editor, page } ) => {
await editor.insertBlock( { name: 'core/table' } );
- await page.click( 'role=spinbutton[name="Column count"i]' );
+ await editor.canvas.click( 'role=spinbutton[name="Column count"i]' );
await page.keyboard.press( 'Backspace' );
await page.keyboard.type( '4' );
// Create the table.
- await page.click( 'role=button[name="Create Table"i]' );
+ await editor.canvas.click( 'role=button[name="Create Table"i]' );
// Click the first cell and add some text. Don't align.
- const cells = page.locator( 'role=textbox[name="Body cell text"i]' );
+ const cells = editor.canvas.locator(
+ 'role=textbox[name="Body cell text"i]'
+ );
await cells.nth( 0 ).click();
await page.keyboard.type( 'None' );
@@ -210,7 +218,7 @@ test.describe( 'Table', () => {
await editor.openDocumentSettingsSidebar();
// Create the table.
- await page.click( 'role=button[name="Create Table"i]' );
+ await editor.canvas.click( 'role=button[name="Create Table"i]' );
// Enable fixed width as it exacerbates the amount of empty space around the RichText.
await page.click(
@@ -221,11 +229,13 @@ test.describe( 'Table', () => {
.check();
// Add multiple new lines to the first cell to make it taller.
- await page.click( 'role=textbox[name="Body cell text"i] >> nth=0' );
+ await editor.canvas.click(
+ 'role=textbox[name="Body cell text"i] >> nth=0'
+ );
await page.keyboard.type( '\n\n\n\n' );
// Get the bounding client rect for the second cell.
- const { x: secondCellX, y: secondCellY } = await page
+ const { x: secondCellX, y: secondCellY } = await editor.canvas
.locator( 'role=textbox[name="Body cell text"] >> nth=1' )
.boundingBox();
@@ -241,10 +251,12 @@ test.describe( 'Table', () => {
await editor.insertBlock( { name: 'core/table' } );
// Create the table.
- await page.click( 'role=button[name="Create Table"i]' );
+ await editor.canvas.click( 'role=button[name="Create Table"i]' );
// Click the first cell and add some text.
- await page.click( 'role=document[name="Block: Table"i] >> figcaption' );
+ await editor.canvas.click(
+ 'role=document[name="Block: Table"i] >> figcaption'
+ );
await page.keyboard.type( 'Caption!' );
expect( await editor.getEditedPostContent() ).toMatchSnapshot();
} );
@@ -252,7 +264,7 @@ test.describe( 'Table', () => {
test( 'up and down arrow navigation', async ( { editor, page } ) => {
await editor.insertBlock( { name: 'core/table' } );
// Create the table.
- await page.click( 'role=button[name="Create Table"i]' );
+ await editor.canvas.click( 'role=button[name="Create Table"i]' );
await page.keyboard.type( '1' );
await page.keyboard.press( 'ArrowDown' );
await page.keyboard.type( '2' );
@@ -263,19 +275,18 @@ test.describe( 'Table', () => {
expect( await editor.getEditedPostContent() ).toMatchSnapshot();
} );
- test( 'should not have focus loss after creation', async ( {
- editor,
- page,
- } ) => {
+ test( 'should not have focus loss after creation', async ( { editor } ) => {
// Insert table block.
await editor.insertBlock( { name: 'core/table' } );
// Create the table.
- await page.click( 'role=button[name="Create Table"i]' );
+ await editor.canvas.click( 'role=button[name="Create Table"i]' );
// Focus should be in first td.
await expect(
- page.locator( 'role=textbox[name="Body cell text"i] >> nth=0' )
+ editor.canvas.locator(
+ 'role=textbox[name="Body cell text"i] >> nth=0'
+ )
).toBeFocused();
} );
} );
diff --git a/test/e2e/specs/editor/plugins/custom-post-types.spec.js b/test/e2e/specs/editor/plugins/custom-post-types.spec.js
index 07d32755632821..55207df05175b0 100644
--- a/test/e2e/specs/editor/plugins/custom-post-types.spec.js
+++ b/test/e2e/specs/editor/plugins/custom-post-types.spec.js
@@ -20,7 +20,7 @@ test.describe( 'Test Custom Post Types', () => {
page,
} ) => {
await admin.createNewPost( { postType: 'hierar-no-title' } );
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'Parent Post' );
await editor.publishPost();
@@ -53,7 +53,7 @@ test.describe( 'Test Custom Post Types', () => {
await page.getByRole( 'listbox' ).getByRole( 'option' ).first().click();
const parentPage = await parentPageLocator.inputValue();
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'Child Post' );
await editor.publishPost();
await page.reload();
@@ -68,7 +68,7 @@ test.describe( 'Test Custom Post Types', () => {
page,
} ) => {
await admin.createNewPost( { postType: 'leg_block_in_tpl' } );
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'Hello there' );
await expect.poll( editor.getBlocks ).toMatchObject( [
diff --git a/test/e2e/specs/editor/plugins/format-api.spec.js b/test/e2e/specs/editor/plugins/format-api.spec.js
index 21e942ddc2199b..f98d8292ea8f6f 100644
--- a/test/e2e/specs/editor/plugins/format-api.spec.js
+++ b/test/e2e/specs/editor/plugins/format-api.spec.js
@@ -21,7 +21,7 @@ test.describe( 'Using Format API', () => {
page,
pageUtils,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'First paragraph' );
await pageUtils.pressKeys( 'shiftAlt+ArrowLeft' );
await editor.clickBlockToolbarButton( 'More' );
diff --git a/test/e2e/specs/editor/plugins/hooks-api.spec.js b/test/e2e/specs/editor/plugins/hooks-api.spec.js
index 675b3861ee2a2a..7257ccde729642 100644
--- a/test/e2e/specs/editor/plugins/hooks-api.spec.js
+++ b/test/e2e/specs/editor/plugins/hooks-api.spec.js
@@ -19,8 +19,9 @@ test.describe( 'Using Hooks API', () => {
test( 'Should contain a reset block button on the sidebar', async ( {
page,
+ editor,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'First paragraph' );
await page.click(
`role=region[name="Editor settings"i] >> role=tab[name="Settings"i]`
@@ -34,10 +35,10 @@ test.describe( 'Using Hooks API', () => {
editor,
page,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'First paragraph' );
- const paragraphBlock = page.locator(
+ const paragraphBlock = editor.canvas.locator(
'role=document[name="Paragraph block"i]'
);
await expect( paragraphBlock ).toHaveText( 'First paragraph' );
diff --git a/test/e2e/specs/editor/plugins/iframed-block.spec.js b/test/e2e/specs/editor/plugins/iframed-block.spec.js
index 55b67cb70fe86e..c5529d9108e9f6 100644
--- a/test/e2e/specs/editor/plugins/iframed-block.spec.js
+++ b/test/e2e/specs/editor/plugins/iframed-block.spec.js
@@ -18,7 +18,9 @@ test.describe( 'Iframed block', () => {
expect( await editor.getEditedPostContent() ).toMatchSnapshot();
await expect(
- page.locator( 'role=document[name="Block: Iframed Block"i]' )
+ editor.canvas.locator(
+ 'role=document[name="Block: Iframed Block"i]'
+ )
).toContainText( 'Iframed Block (set with jQuery)' );
// open page from sidebar settings
diff --git a/test/e2e/specs/editor/plugins/image-size.spec.js b/test/e2e/specs/editor/plugins/image-size.spec.js
index a7502c93a2476c..f2ba0e91b6733a 100644
--- a/test/e2e/specs/editor/plugins/image-size.spec.js
+++ b/test/e2e/specs/editor/plugins/image-size.spec.js
@@ -52,7 +52,7 @@ test.describe( 'changing image size', () => {
// Verify that the custom size was applied to the image.
await expect(
- page.locator( `role=img[name="${ filename }"]` )
+ editor.canvas.locator( `role=img[name="${ filename }"]` )
).toHaveCSS( 'width', '499px' );
await expect(
page.locator( 'role=spinbutton[name="Width"i]' )
diff --git a/test/e2e/specs/editor/plugins/post-type-templates.spec.js b/test/e2e/specs/editor/plugins/post-type-templates.spec.js
index 2caffdf52abe2e..c743d08d8ae681 100644
--- a/test/e2e/specs/editor/plugins/post-type-templates.spec.js
+++ b/test/e2e/specs/editor/plugins/post-type-templates.spec.js
@@ -35,7 +35,7 @@ test.describe( 'Post type templates', () => {
// Remove a block from the template to verify that it's not
// re-added after saving and reloading the editor.
- await page.focus( 'role=textbox[name="Add title"i]' );
+ await editor.canvas.focus( 'role=textbox[name="Add title"i]' );
await page.keyboard.press( 'ArrowDown' );
await page.keyboard.press( 'Backspace' );
await page.click( 'role=button[name="Save draft"i]' );
@@ -64,7 +64,7 @@ test.describe( 'Post type templates', () => {
} ) => {
// Remove all blocks from the template to verify that they're not
// re-added after saving and reloading the editor.
- await page.fill(
+ await editor.canvas.fill(
'role=textbox[name="Add title"i]',
'My Empty Book'
);
@@ -125,11 +125,11 @@ test.describe( 'Post type templates', () => {
// Remove the default block template to verify that it's not
// re-added after saving and reloading the editor.
- await page.fill(
+ await editor.canvas.fill(
'role=textbox[name="Add title"i]',
'My Image Format'
);
- await page.focus( 'role=document[name="Block: Image"i]' );
+ await editor.canvas.focus( 'role=document[name="Block: Image"i]' );
await page.keyboard.press( 'Backspace' );
await page.click( 'role=button[name="Save draft"i]' );
await expect(
diff --git a/test/e2e/specs/editor/plugins/wp-editor-meta-box.spec.js b/test/e2e/specs/editor/plugins/wp-editor-meta-box.spec.js
index 27cf0b7b160bde..13720de509e3c8 100644
--- a/test/e2e/specs/editor/plugins/wp-editor-meta-box.spec.js
+++ b/test/e2e/specs/editor/plugins/wp-editor-meta-box.spec.js
@@ -20,7 +20,10 @@ test.describe( 'WP Editor Meta Boxes', () => {
await admin.createNewPost();
// Add title to enable valid non-empty post save.
- await page.type( 'role=textbox[name="Add title"i]', 'Hello Meta' );
+ await editor.canvas.type(
+ 'role=textbox[name="Add title"i]',
+ 'Hello Meta'
+ );
// Type something.
await page.click( 'role=button[name="Text"i]' );
diff --git a/test/e2e/specs/editor/various/a11y.spec.js b/test/e2e/specs/editor/various/a11y.spec.js
index 5725f216d1cf24..8b819d3866b6ca 100644
--- a/test/e2e/specs/editor/various/a11y.spec.js
+++ b/test/e2e/specs/editor/various/a11y.spec.js
@@ -20,10 +20,18 @@ test.describe( 'a11y (@firefox, @webkit)', () => {
test( 'navigating through the Editor regions four times should land on the Editor top bar region', async ( {
page,
pageUtils,
+ editor,
} ) => {
+ // To do: run with iframe.
+ await page.evaluate( () => {
+ window.wp.blocks.registerBlockType( 'test/v2', {
+ apiVersion: '2',
+ title: 'test',
+ } );
+ } );
// On a new post, initial focus is set on the Post title.
await expect(
- page.locator( 'role=textbox[name=/Add title/i]' )
+ editor.canvas.locator( 'role=textbox[name=/Add title/i]' )
).toBeFocused();
// Navigate to the 'Editor settings' region.
await pageUtils.pressKeys( 'ctrl+`' );
@@ -47,6 +55,13 @@ test.describe( 'a11y (@firefox, @webkit)', () => {
page,
pageUtils,
} ) => {
+ // To do: run with iframe.
+ await page.evaluate( () => {
+ window.wp.blocks.registerBlockType( 'test/v2', {
+ apiVersion: '2',
+ title: 'test',
+ } );
+ } );
// Open keyboard shortcuts modal.
await pageUtils.pressKeys( 'access+h' );
diff --git a/test/e2e/specs/editor/various/behaviors.spec.js b/test/e2e/specs/editor/various/behaviors.spec.js
index b219ebfb809c13..dc03dd166b001e 100644
--- a/test/e2e/specs/editor/various/behaviors.spec.js
+++ b/test/e2e/specs/editor/various/behaviors.spec.js
@@ -8,23 +8,45 @@ const path = require( 'path' );
*/
const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
-test.describe( 'Testing behaviors functionality', () => {
- const filename = '1024x768_e2e_test_image_size.jpeg';
- const filepath = path.join( './test/e2e/assets', filename );
+test.use( {
+ behaviorUtils: async ( { page, requestUtils }, use ) => {
+ await use( new BehaviorUtils( { page, requestUtils } ) );
+ },
+} );
- const createMedia = async ( { admin, requestUtils } ) => {
- await admin.createNewPost();
- const media = await requestUtils.uploadMedia( filepath );
- return media;
- };
+const filename = '1024x768_e2e_test_image_size.jpeg';
+test.describe( 'Testing behaviors functionality', () => {
test.afterAll( async ( { requestUtils } ) => {
await requestUtils.activateTheme( 'twentytwentyone' );
await requestUtils.deleteAllPosts();
} );
+ test.beforeEach( async ( { admin, page, requestUtils } ) => {
+ await requestUtils.deleteAllMedia();
+ await admin.visitAdminPage(
+ '/admin.php',
+ 'page=gutenberg-experiments'
+ );
+
+ await page
+ .locator( `#gutenberg-interactivity-api-core-blocks` )
+ .setChecked( true );
+ await page.locator( `input[name="submit"]` ).click();
+ await page.waitForLoadState();
+ } );
- test.afterEach( async ( { requestUtils } ) => {
+ test.afterEach( async ( { admin, page, requestUtils } ) => {
await requestUtils.deleteAllMedia();
+ await admin.visitAdminPage(
+ '/admin.php',
+ 'page=gutenberg-experiments'
+ );
+
+ await page
+ .locator( `#gutenberg-interactivity-api-core-blocks` )
+ .setChecked( false );
+ await page.locator( `input[name="submit"]` ).click();
+ await page.waitForLoadState();
} );
test( '`No Behaviors` should be the default as defined in the core theme.json', async ( {
@@ -32,9 +54,11 @@ test.describe( 'Testing behaviors functionality', () => {
editor,
requestUtils,
page,
+ behaviorUtils,
} ) => {
await requestUtils.activateTheme( 'twentytwentyone' );
- const media = await createMedia( { admin, requestUtils } );
+ await admin.createNewPost();
+ const media = await behaviorUtils.createMedia();
await editor.insertBlock( {
name: 'core/image',
attributes: {
@@ -44,16 +68,22 @@ test.describe( 'Testing behaviors functionality', () => {
},
} );
- await page.getByRole( 'button', { name: 'Advanced' } ).click();
- const select = page.getByLabel( 'Behavior' );
+ await editor.openDocumentSettingsSidebar();
+ const editorSettings = page.getByRole( 'region', {
+ name: 'Editor settings',
+ } );
+ await editorSettings
+ .getByRole( 'button', { name: 'Advanced' } )
+ .click();
+ const select = editorSettings.getByRole( 'combobox', {
+ name: 'Behavior',
+ } );
// By default, no behaviors should be selected.
- await expect( select ).toHaveCount( 1 );
await expect( select ).toHaveValue( '' );
// By default, you should be able to select the Lightbox behavior.
- const options = select.locator( 'option' );
- await expect( options ).toHaveCount( 2 );
+ await expect( select.getByRole( 'option' ) ).toHaveCount( 2 );
} );
test( 'Behaviors UI can be disabled in the `theme.json`', async ( {
@@ -61,12 +91,14 @@ test.describe( 'Testing behaviors functionality', () => {
editor,
requestUtils,
page,
+ behaviorUtils,
} ) => {
// { "lightbox": true } is the default behavior setting, so we activate the
// `behaviors-ui-disabled` theme where it is disabled by default. Change if we change
// the default value in the core theme.json file.
await requestUtils.activateTheme( 'behaviors-ui-disabled' );
- const media = await createMedia( { admin, requestUtils } );
+ await admin.createNewPost();
+ const media = await behaviorUtils.createMedia();
await editor.insertBlock( {
name: 'core/image',
@@ -77,10 +109,20 @@ test.describe( 'Testing behaviors functionality', () => {
},
} );
- await page.getByRole( 'button', { name: 'Advanced' } ).click();
+ await editor.openDocumentSettingsSidebar();
+ const editorSettings = page.getByRole( 'region', {
+ name: 'Editor settings',
+ } );
+ await editorSettings
+ .getByRole( 'button', { name: 'Advanced' } )
+ .click();
// No behaviors dropdown should be present.
- await expect( page.getByLabel( 'Behavior' ) ).toHaveCount( 0 );
+ await expect(
+ editorSettings.getByRole( 'combobox', {
+ name: 'Behavior',
+ } )
+ ).toBeHidden();
} );
test( "Block's value for behaviors takes precedence over the theme's value", async ( {
@@ -88,9 +130,11 @@ test.describe( 'Testing behaviors functionality', () => {
editor,
requestUtils,
page,
+ behaviorUtils,
} ) => {
await requestUtils.activateTheme( 'twentytwentyone' );
- const media = await createMedia( { admin, requestUtils } );
+ await admin.createNewPost();
+ const media = await behaviorUtils.createMedia();
await editor.insertBlock( {
name: 'core/image',
@@ -103,17 +147,23 @@ test.describe( 'Testing behaviors functionality', () => {
},
} );
- await page.getByRole( 'button', { name: 'Advanced' } ).click();
- const select = page.getByLabel( 'Behavior' );
+ await editor.openDocumentSettingsSidebar();
+ const editorSettings = page.getByRole( 'region', {
+ name: 'Editor settings',
+ } );
+ await editorSettings
+ .getByRole( 'button', { name: 'Advanced' } )
+ .click();
+ const select = editorSettings.getByRole( 'combobox', {
+ name: 'Behavior',
+ } );
// The lightbox should be selected because the value from the block's
// attributes takes precedence over the theme's value.
- await expect( select ).toHaveCount( 1 );
await expect( select ).toHaveValue( 'lightbox' );
// There should be 2 options available: `No behaviors` and `Lightbox`.
- const options = select.locator( 'option' );
- await expect( options ).toHaveCount( 2 );
+ await expect( select.getByRole( 'option' ) ).toHaveCount( 2 );
// We can change the value of the behaviors dropdown to `No behaviors`.
await select.selectOption( { label: 'No behaviors' } );
@@ -128,10 +178,12 @@ test.describe( 'Testing behaviors functionality', () => {
editor,
requestUtils,
page,
+ behaviorUtils,
} ) => {
// In this theme, the default value for settings.behaviors.blocks.core/image.lightbox is `true`.
await requestUtils.activateTheme( 'behaviors-enabled' );
- const media = await createMedia( { admin, requestUtils } );
+ await admin.createNewPost();
+ const media = await behaviorUtils.createMedia();
await editor.insertBlock( {
name: 'core/image',
@@ -142,20 +194,138 @@ test.describe( 'Testing behaviors functionality', () => {
},
} );
- await page.getByRole( 'button', { name: 'Advanced' } ).click();
- const select = page.getByLabel( 'Behavior' );
+ await editor.openDocumentSettingsSidebar();
+ const editorSettings = page.getByRole( 'region', {
+ name: 'Editor settings',
+ } );
+ await editorSettings
+ .getByRole( 'button', { name: 'Advanced' } )
+ .click();
+ const select = editorSettings.getByRole( 'combobox', {
+ name: 'Behavior',
+ } );
// The behaviors dropdown should be present and the value should be set to
// `lightbox`.
- await expect( select ).toHaveCount( 1 );
await expect( select ).toHaveValue( 'lightbox' );
// There should be 2 options available: `No behaviors` and `Lightbox`.
- const options = select.locator( 'option' );
- await expect( options ).toHaveCount( 2 );
+ await expect( select.getByRole( 'option' ) ).toHaveCount( 2 );
// We can change the value of the behaviors dropdown to `No behaviors`.
await select.selectOption( { label: 'No behaviors' } );
await expect( select ).toHaveValue( '' );
} );
+
+ test( 'Lightbox behavior is disabled if the Image has a link', async ( {
+ admin,
+ editor,
+ requestUtils,
+ page,
+ behaviorUtils,
+ } ) => {
+ // In this theme, the default value for settings.behaviors.blocks.core/image.lightbox is `true`.
+ await requestUtils.activateTheme( 'behaviors-enabled' );
+ await admin.createNewPost();
+ const media = await behaviorUtils.createMedia();
+
+ await editor.insertBlock( {
+ name: 'core/image',
+ attributes: {
+ alt: filename,
+ id: media.id,
+ url: media.source_url,
+ linkDestination: 'custom',
+ },
+ } );
+
+ await editor.openDocumentSettingsSidebar();
+ const editorSettings = page.getByRole( 'region', {
+ name: 'Editor settings',
+ } );
+ await editorSettings
+ .getByRole( 'button', { name: 'Advanced' } )
+ .click();
+ const select = editorSettings.getByRole( 'combobox', {
+ name: 'Behavior',
+ } );
+
+ // The behaviors dropdown should be present but disabled.
+ await expect( select ).toBeDisabled();
+ } );
+
+ test( 'Lightbox behavior control has a Reset button that removes the markup', async ( {
+ admin,
+ editor,
+ requestUtils,
+ page,
+ behaviorUtils,
+ } ) => {
+ const date = new Date();
+ const year = date.getFullYear();
+ const month = ( date.getMonth() + 1 ).toString().padStart( 2, '0' );
+ await requestUtils.activateTheme( 'behaviors-enabled' );
+ await admin.createNewPost();
+ const media = await behaviorUtils.createMedia();
+
+ await editor.insertBlock( {
+ name: 'core/image',
+ attributes: {
+ alt: filename,
+ id: media.id,
+ url: media.source_url,
+ behaviors: { lightbox: true },
+ },
+ } );
+ expect( await editor.getEditedPostContent() )
+ .toBe( `
+
+` );
+
+ await editor.openDocumentSettingsSidebar();
+
+ const editorSettings = page.getByRole( 'region', {
+ name: 'Editor settings',
+ } );
+
+ await editorSettings
+ .getByRole( 'button', { name: 'Advanced' } )
+ .last()
+ .click();
+
+ const resetButton = editorSettings.getByRole( 'button', {
+ name: 'Reset',
+ } );
+
+ expect( resetButton ).toBeDefined();
+
+ await resetButton.last().click();
+ expect( await editor.getEditedPostContent() )
+ .toBe( `
+
+` );
+ } );
} );
+
+class BehaviorUtils {
+ constructor( { page, requestUtils } ) {
+ this.page = page;
+ this.requestUtils = requestUtils;
+
+ this.TEST_IMAGE_FILE_PATH = path.join(
+ __dirname,
+ '..',
+ '..',
+ '..',
+ 'assets',
+ filename
+ );
+ }
+
+ async createMedia() {
+ const media = await this.requestUtils.uploadMedia(
+ this.TEST_IMAGE_FILE_PATH
+ );
+ return media;
+ }
+}
diff --git a/test/e2e/specs/editor/various/block-deletion.spec.js b/test/e2e/specs/editor/various/block-deletion.spec.js
index d02a9167c0c3af..9fcacae05b63fa 100644
--- a/test/e2e/specs/editor/various/block-deletion.spec.js
+++ b/test/e2e/specs/editor/various/block-deletion.spec.js
@@ -37,7 +37,7 @@ test.describe( 'Block deletion', () => {
// Remove the current paragraph via the Block Toolbar options menu.
await editor.showBlockToolbar();
- await editor.canvas
+ await page
.getByRole( 'toolbar', { name: 'Block tools' } )
.getByRole( 'button', { name: 'Options' } )
.click();
@@ -84,7 +84,7 @@ test.describe( 'Block deletion', () => {
// Remove the current paragraph via the Block Toolbar options menu.
await editor.showBlockToolbar();
- await editor.canvas
+ await page
.getByRole( 'toolbar', { name: 'Block tools' } )
.getByRole( 'button', { name: 'Options' } )
.click();
@@ -313,7 +313,7 @@ test.describe( 'Block deletion', () => {
// Remove that paragraph via its options menu.
await editor.showBlockToolbar();
- await editor.canvas
+ await page
.getByRole( 'toolbar', { name: 'Block tools' } )
.getByRole( 'button', { name: 'Options' } )
.click();
diff --git a/test/e2e/specs/editor/various/block-locking.spec.js b/test/e2e/specs/editor/various/block-locking.spec.js
index 2e977690a1708d..2c0a9d3f0bf565 100644
--- a/test/e2e/specs/editor/various/block-locking.spec.js
+++ b/test/e2e/specs/editor/various/block-locking.spec.js
@@ -73,7 +73,7 @@ test.describe( 'Block Locking', () => {
await page.click( 'role=checkbox[name="Lock all"]' );
await page.click( 'role=button[name="Apply"]' );
- await editor.clickBlockToolbarButton( 'Unlock Paragraph' );
+ await editor.clickBlockToolbarButton( 'Unlock' );
await page.click( 'role=checkbox[name="Lock all"]' );
await page.click( 'role=button[name="Apply"]' );
diff --git a/test/e2e/specs/editor/various/block-mover.spec.js b/test/e2e/specs/editor/various/block-mover.spec.js
index e90ee5f7f0122d..4ed90191f25585 100644
--- a/test/e2e/specs/editor/various/block-mover.spec.js
+++ b/test/e2e/specs/editor/various/block-mover.spec.js
@@ -23,7 +23,7 @@ test.describe( 'block mover', () => {
} );
// Select a block so the block mover is rendered.
- await page.focus( 'text=First Paragraph' );
+ await editor.canvas.focus( 'text=First Paragraph' );
await editor.showBlockToolbar();
const moveDownButton = page.locator(
@@ -47,7 +47,7 @@ test.describe( 'block mover', () => {
attributes: { content: 'First Paragraph' },
} );
// Select a block so the block mover has the possibility of being rendered.
- await page.focus( 'text=First Paragraph' );
+ await editor.canvas.focus( 'text=First Paragraph' );
await editor.showBlockToolbar();
// Ensure no block mover exists when only one block exists on the page.
diff --git a/test/e2e/specs/editor/various/compatibility-classic-editor.spec.js b/test/e2e/specs/editor/various/compatibility-classic-editor.spec.js
index 0ea1908cd90ef5..a1efbfa579b9a7 100644
--- a/test/e2e/specs/editor/various/compatibility-classic-editor.spec.js
+++ b/test/e2e/specs/editor/various/compatibility-classic-editor.spec.js
@@ -17,7 +17,7 @@ test.describe( 'Compatibility with classic editor', () => {
editor,
} ) => {
await editor.insertBlock( { name: 'core/html' } );
- await page.focus( 'role=textbox[name="HTML"i]' );
+ await editor.canvas.focus( 'role=textbox[name="HTML"i]' );
await page.keyboard.type( '' );
await page.keyboard.type( 'Random Link' );
await page.keyboard.type( ' ' );
diff --git a/test/e2e/specs/editor/various/content-only-lock.spec.js b/test/e2e/specs/editor/various/content-only-lock.spec.js
index c215da33f3ec57..962fe0a6279094 100644
--- a/test/e2e/specs/editor/various/content-only-lock.spec.js
+++ b/test/e2e/specs/editor/various/content-only-lock.spec.js
@@ -23,8 +23,8 @@ test.describe( 'Content-only lock', () => {
` );
await pageUtils.pressKeys( 'secondary+M' );
-
- await page.click( 'role=document[name="Paragraph block"i]' );
+ await page.waitForSelector( 'iframe[name="editor-canvas"]' );
+ await editor.canvas.click( 'role=document[name="Paragraph block"i]' );
await page.keyboard.type( ' World' );
expect( await editor.getEditedPostContent() ).toMatchSnapshot();
} );
diff --git a/test/e2e/specs/editor/various/copy-cut-paste.spec.js b/test/e2e/specs/editor/various/copy-cut-paste.spec.js
index cb040504de4c81..823926c1121a02 100644
--- a/test/e2e/specs/editor/various/copy-cut-paste.spec.js
+++ b/test/e2e/specs/editor/various/copy-cut-paste.spec.js
@@ -13,7 +13,7 @@ test.describe( 'Copy/cut/paste', () => {
page,
pageUtils,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'Copy - collapsed selection' );
await page.keyboard.press( 'Enter' );
await page.keyboard.type( '2' );
@@ -31,7 +31,14 @@ test.describe( 'Copy/cut/paste', () => {
page,
pageUtils,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ // To do: run with iframe.
+ await page.evaluate( () => {
+ window.wp.blocks.registerBlockType( 'test/v2', {
+ apiVersion: '2',
+ title: 'test',
+ } );
+ } );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'Cut - collapsed selection' );
await page.keyboard.press( 'Enter' );
await page.keyboard.type( '2' );
@@ -59,7 +66,7 @@ test.describe( 'Copy/cut/paste', () => {
await page.evaluate( () => {
window.wp.data.dispatch( 'core/block-editor' ).clearSelectedBlock();
} );
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await pageUtils.pressKeys( 'primary+v' );
expect( await editor.getEditedPostContent() ).toMatchSnapshot();
} );
@@ -78,7 +85,7 @@ test.describe( 'Copy/cut/paste', () => {
await page.evaluate( () => {
window.wp.data.dispatch( 'core/block-editor' ).clearSelectedBlock();
} );
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await pageUtils.pressKeys( 'primary+v' );
expect( await editor.getEditedPostContent() ).toMatchSnapshot();
} );
@@ -88,7 +95,7 @@ test.describe( 'Copy/cut/paste', () => {
page,
pageUtils,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'First block' );
await page.keyboard.press( 'Enter' );
@@ -240,7 +247,7 @@ test.describe( 'Copy/cut/paste', () => {
page,
pageUtils,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'A block' );
await page.keyboard.press( 'Enter' );
await page.keyboard.type( 'B block' );
@@ -252,7 +259,7 @@ test.describe( 'Copy/cut/paste', () => {
await pageUtils.pressKeys( 'primary+ArrowLeft' );
// Sometimes the caret has not moved to the correct position before pressing Enter.
// @see https://github.com/WordPress/gutenberg/issues/40303#issuecomment-1109434887
- await page.waitForFunction(
+ await editor.canvas.waitForFunction(
() => window.getSelection().type === 'Caret'
);
// Create a new block at the top of the document to paste there.
@@ -267,7 +274,7 @@ test.describe( 'Copy/cut/paste', () => {
page,
pageUtils,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'A block' );
await editor.insertBlock( { name: 'core/spacer' } );
await page.keyboard.press( 'Enter' );
@@ -280,7 +287,7 @@ test.describe( 'Copy/cut/paste', () => {
await pageUtils.pressKeys( 'primary+ArrowLeft' );
// Sometimes the caret has not moved to the correct position before pressing Enter.
// @see https://github.com/WordPress/gutenberg/issues/40303#issuecomment-1109434887
- await page.waitForFunction(
+ await editor.canvas.waitForFunction(
() => window.getSelection().type === 'Caret'
);
// Create a new block at the top of the document to paste there.
@@ -295,7 +302,7 @@ test.describe( 'Copy/cut/paste', () => {
page,
pageUtils,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'A block' );
await page.keyboard.press( 'Enter' );
await page.keyboard.type( 'B block' );
@@ -307,7 +314,7 @@ test.describe( 'Copy/cut/paste', () => {
await pageUtils.pressKeys( 'primary+ArrowLeft' );
// Sometimes the caret has not moved to the correct position before pressing Enter.
// @see https://github.com/WordPress/gutenberg/issues/40303#issuecomment-1109434887
- await page.waitForFunction(
+ await editor.canvas.waitForFunction(
() => window.getSelection().type === 'Caret'
);
// Create a new block at the top of the document to paste there.
@@ -322,7 +329,7 @@ test.describe( 'Copy/cut/paste', () => {
page,
pageUtils,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'A block' );
await editor.insertBlock( { name: 'core/spacer' } );
await page.keyboard.press( 'Enter' );
@@ -335,7 +342,7 @@ test.describe( 'Copy/cut/paste', () => {
await pageUtils.pressKeys( 'primary+ArrowLeft' );
// Sometimes the caret has not moved to the correct position before pressing Enter.
// @see https://github.com/WordPress/gutenberg/issues/40303#issuecomment-1109434887
- await page.waitForFunction(
+ await editor.canvas.waitForFunction(
() => window.getSelection().type === 'Caret'
);
// Create a new block at the top of the document to paste there.
@@ -362,7 +369,7 @@ test.describe( 'Copy/cut/paste', () => {
await pageUtils.pressKeys( 'primary+ArrowLeft' );
// Sometimes the caret has not moved to the correct position before pressing Enter.
// @see https://github.com/WordPress/gutenberg/issues/40303#issuecomment-1109434887
- await page.waitForFunction(
+ await editor.canvas.waitForFunction(
() => window.getSelection().type === 'Caret'
);
// Create a new block at the top of the document to paste there.
@@ -389,7 +396,7 @@ test.describe( 'Copy/cut/paste', () => {
await pageUtils.pressKeys( 'primary+ArrowLeft' );
// Sometimes the caret has not moved to the correct position before pressing Enter.
// @see https://github.com/WordPress/gutenberg/issues/40303#issuecomment-1109434887
- await page.waitForFunction(
+ await editor.canvas.waitForFunction(
() => window.getSelection().type === 'Caret'
);
// Create a new code block to paste there.
@@ -399,8 +406,8 @@ test.describe( 'Copy/cut/paste', () => {
} );
test( 'should paste single line in post title', async ( {
- page,
pageUtils,
+ editor,
} ) => {
// This test checks whether we are correctly handling single line
// pasting in the post title. Previously we were accidentally falling
@@ -413,13 +420,16 @@ test.describe( 'Copy/cut/paste', () => {
await pageUtils.pressKeys( 'primary+v' );
// Expect the span to be filtered out.
expect(
- await page.evaluate( () => document.activeElement.innerHTML )
+ await editor.canvas.evaluate(
+ () => document.activeElement.innerHTML
+ )
).toMatchSnapshot();
} );
test( 'should paste single line in post title with existing content', async ( {
page,
pageUtils,
+ editor,
} ) => {
await page.keyboard.type( 'ab' );
await page.keyboard.press( 'ArrowLeft' );
@@ -430,7 +440,9 @@ test.describe( 'Copy/cut/paste', () => {
// Ensure the selection is correct.
await page.keyboard.type( 'y' );
expect(
- await page.evaluate( () => document.activeElement.innerHTML )
+ await editor.canvas.evaluate(
+ () => document.activeElement.innerHTML
+ )
).toBe( 'axyb' );
} );
diff --git a/test/e2e/specs/editor/various/draggable-blocks.spec.js b/test/e2e/specs/editor/various/draggable-blocks.spec.js
index d73040bafa48c9..a66efe4540f38a 100644
--- a/test/e2e/specs/editor/various/draggable-blocks.spec.js
+++ b/test/e2e/specs/editor/various/draggable-blocks.spec.js
@@ -42,7 +42,9 @@ test.describe( 'Draggable block', () => {
2
` );
- await page.focus( 'role=document[name="Paragraph block"i] >> text=2' );
+ await editor.canvas.focus(
+ 'role=document[name="Paragraph block"i] >> text=2'
+ );
await editor.showBlockToolbar();
const dragHandle = page.locator(
@@ -54,7 +56,7 @@ test.describe( 'Draggable block', () => {
await page.mouse.down();
// Move to and hover on the upper half of the paragraph block to trigger the indicator.
- const firstParagraph = page.locator(
+ const firstParagraph = editor.canvas.locator(
'role=document[name="Paragraph block"i] >> text=1'
);
const firstParagraphBound = await firstParagraph.boundingBox();
@@ -112,7 +114,9 @@ test.describe( 'Draggable block', () => {
2
` );
- await page.focus( 'role=document[name="Paragraph block"i] >> text=1' );
+ await editor.canvas.focus(
+ 'role=document[name="Paragraph block"i] >> text=1'
+ );
await editor.showBlockToolbar();
const dragHandle = page.locator(
@@ -124,7 +128,7 @@ test.describe( 'Draggable block', () => {
await page.mouse.down();
// Move to and hover on the bottom half of the paragraph block to trigger the indicator.
- const secondParagraph = page.locator(
+ const secondParagraph = editor.canvas.locator(
'role=document[name="Paragraph block"i] >> text=2'
);
const secondParagraphBound = await secondParagraph.boundingBox();
@@ -193,7 +197,9 @@ test.describe( 'Draggable block', () => {
],
} );
- await page.focus( 'role=document[name="Paragraph block"i] >> text=2' );
+ await editor.canvas.focus(
+ 'role=document[name="Paragraph block"i] >> text=2'
+ );
await editor.showBlockToolbar();
const dragHandle = page.locator(
@@ -205,7 +211,7 @@ test.describe( 'Draggable block', () => {
await page.mouse.down();
// Move to and hover on the left half of the paragraph block to trigger the indicator.
- const firstParagraph = page.locator(
+ const firstParagraph = editor.canvas.locator(
'role=document[name="Paragraph block"i] >> text=1'
);
const firstParagraphBound = await firstParagraph.boundingBox();
@@ -272,7 +278,9 @@ test.describe( 'Draggable block', () => {
],
} );
- await page.focus( 'role=document[name="Paragraph block"i] >> text=1' );
+ await editor.canvas.focus(
+ 'role=document[name="Paragraph block"i] >> text=1'
+ );
await editor.showBlockToolbar();
const dragHandle = page.locator(
@@ -284,7 +292,7 @@ test.describe( 'Draggable block', () => {
await page.mouse.down();
// Move to and hover on the right half of the paragraph block to trigger the indicator.
- const secondParagraph = page.locator(
+ const secondParagraph = editor.canvas.locator(
'role=document[name="Paragraph block"i] >> text=2'
);
const secondParagraphBound = await secondParagraph.boundingBox();
@@ -334,6 +342,13 @@ test.describe( 'Draggable block', () => {
editor,
pageUtils,
} ) => {
+ // To do: run with iframe.
+ await page.evaluate( () => {
+ window.wp.blocks.registerBlockType( 'test/v2', {
+ apiVersion: '2',
+ title: 'test',
+ } );
+ } );
// Insert a row.
await editor.insertBlock( {
name: 'core/group',
diff --git a/test/e2e/specs/editor/various/font-size-picker.spec.js b/test/e2e/specs/editor/various/font-size-picker.spec.js
index e63a5984443bca..ddc47e3ee6de66 100644
--- a/test/e2e/specs/editor/various/font-size-picker.spec.js
+++ b/test/e2e/specs/editor/various/font-size-picker.spec.js
@@ -24,7 +24,9 @@ test.describe( 'Font Size Picker', () => {
page,
} ) => {
await editor.openDocumentSettingsSidebar();
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click(
+ 'role=button[name="Add default block"i]'
+ );
await page.keyboard.type( 'Paragraph to be made "small"' );
await page.click(
'role=region[name="Editor settings"i] >> role=button[name="Set custom size"i]'
@@ -45,7 +47,9 @@ test.describe( 'Font Size Picker', () => {
pageUtils,
} ) => {
await editor.openDocumentSettingsSidebar();
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click(
+ 'role=button[name="Add default block"i]'
+ );
await page.keyboard.type( 'Paragraph reset - custom size' );
await page.click(
'role=region[name="Editor settings"i] >> role=button[name="Set custom size"i]'
@@ -135,7 +139,9 @@ test.describe( 'Font Size Picker', () => {
pageUtils,
} ) => {
await editor.openDocumentSettingsSidebar();
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click(
+ 'role=button[name="Add default block"i]'
+ );
await page.keyboard.type( 'Paragraph to be made "large"' );
await page.click(
'role=group[name="Font size"i] >> role=button[name="Font size"i]'
@@ -155,7 +161,9 @@ test.describe( 'Font Size Picker', () => {
pageUtils,
} ) => {
await editor.openDocumentSettingsSidebar();
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click(
+ 'role=button[name="Add default block"i]'
+ );
await page.keyboard.type(
'Paragraph with font size reset using tools panel menu'
);
@@ -186,7 +194,9 @@ test.describe( 'Font Size Picker', () => {
pageUtils,
} ) => {
await editor.openDocumentSettingsSidebar();
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click(
+ 'role=button[name="Add default block"i]'
+ );
await page.keyboard.type(
'Paragraph with font size reset using input field'
);
@@ -221,7 +231,9 @@ test.describe( 'Font Size Picker', () => {
page,
} ) => {
await editor.openDocumentSettingsSidebar();
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click(
+ 'role=button[name="Add default block"i]'
+ );
await page.keyboard.type( 'Paragraph to be made "large"' );
await page.click(
'role=radiogroup[name="Font size"i] >> role=radio[name="Large"i]'
@@ -238,7 +250,9 @@ test.describe( 'Font Size Picker', () => {
page,
} ) => {
await editor.openDocumentSettingsSidebar();
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click(
+ 'role=button[name="Add default block"i]'
+ );
await page.keyboard.type(
'Paragraph with font size reset using tools panel menu'
);
@@ -267,7 +281,9 @@ test.describe( 'Font Size Picker', () => {
pageUtils,
} ) => {
await editor.openDocumentSettingsSidebar();
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click(
+ 'role=button[name="Add default block"i]'
+ );
await page.keyboard.type(
'Paragraph with font size reset using input field'
);
diff --git a/test/e2e/specs/editor/various/inner-blocks-templates.spec.js b/test/e2e/specs/editor/various/inner-blocks-templates.spec.js
index 87ad2604281983..e1588e57beb105 100644
--- a/test/e2e/specs/editor/various/inner-blocks-templates.spec.js
+++ b/test/e2e/specs/editor/various/inner-blocks-templates.spec.js
@@ -28,7 +28,7 @@ test.describe( 'Inner blocks templates', () => {
name: 'test/test-inner-blocks-async-template',
} );
- const blockWithTemplateContent = page.locator(
+ const blockWithTemplateContent = editor.canvas.locator(
'role=document[name="Block: Test Inner Blocks Async Template"i] >> text=OneTwo'
);
diff --git a/test/e2e/specs/editor/various/inserting-blocks.spec.js b/test/e2e/specs/editor/various/inserting-blocks.spec.js
index 0ddb2b6b59228c..39c159b00b75c0 100644
--- a/test/e2e/specs/editor/various/inserting-blocks.spec.js
+++ b/test/e2e/specs/editor/various/inserting-blocks.spec.js
@@ -15,8 +15,16 @@ test.use( {
} );
test.describe( 'Inserting blocks (@firefox, @webkit)', () => {
- test.beforeEach( async ( { admin } ) => {
+ test.beforeEach( async ( { admin, page } ) => {
await admin.createNewPost();
+ // To do: some drag an drop tests are failing, so run them without
+ // iframe for now.
+ await page.evaluate( () => {
+ window.wp.blocks.registerBlockType( 'test/v2', {
+ apiVersion: '2',
+ title: 'test',
+ } );
+ } );
} );
test.afterAll( async ( { requestUtils } ) => {
@@ -39,7 +47,7 @@ test.describe( 'Inserting blocks (@firefox, @webkit)', () => {
name: 'core/paragraph',
attributes: { content: 'Dummy text' },
} );
- const paragraphBlock = page.locator(
+ const paragraphBlock = editor.canvas.locator(
'[data-type="core/paragraph"] >> text=Dummy text'
);
@@ -116,7 +124,7 @@ test.describe( 'Inserting blocks (@firefox, @webkit)', () => {
const beforeContent = await editor.getEditedPostContent();
- const paragraphBlock = page.locator(
+ const paragraphBlock = editor.canvas.locator(
'[data-type="core/paragraph"] >> text=Dummy text'
);
@@ -176,7 +184,7 @@ test.describe( 'Inserting blocks (@firefox, @webkit)', () => {
attributes: { content: 'Dummy text' },
} );
- const paragraphBlock = page.locator(
+ const paragraphBlock = editor.canvas.locator(
'[data-type="core/paragraph"] >> text=Dummy text'
);
@@ -244,7 +252,7 @@ test.describe( 'Inserting blocks (@firefox, @webkit)', () => {
const beforeContent = await editor.getEditedPostContent();
- const paragraphBlock = page.locator(
+ const paragraphBlock = editor.canvas.locator(
'[data-type="core/paragraph"] >> text=Dummy text'
);
diff --git a/test/e2e/specs/editor/various/keep-styles-on-block-transforms.spec.js b/test/e2e/specs/editor/various/keep-styles-on-block-transforms.spec.js
index e4857f84d46c36..35895f05209be1 100644
--- a/test/e2e/specs/editor/various/keep-styles-on-block-transforms.spec.js
+++ b/test/e2e/specs/editor/various/keep-styles-on-block-transforms.spec.js
@@ -12,7 +12,7 @@ test.describe( 'Keep styles on block transforms', () => {
page,
editor,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '## Heading' );
await editor.openDocumentSettingsSidebar();
await page.click( 'role=button[name="Color Text styles"i]' );
@@ -37,8 +37,15 @@ test.describe( 'Keep styles on block transforms', () => {
pageUtils,
editor,
} ) => {
+ // To do: run with iframe.
+ await page.evaluate( () => {
+ window.wp.blocks.registerBlockType( 'test/v2', {
+ apiVersion: '2',
+ title: 'test',
+ } );
+ } );
// Create a paragraph block with some content.
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'Line 1 to be made large' );
await page.keyboard.press( 'Enter' );
await page.keyboard.type( 'Line 2 to be made large' );
@@ -71,7 +78,7 @@ test.describe( 'Keep styles on block transforms', () => {
editor,
} ) => {
// Create a paragraph block with some content.
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'Line 1 to be made large' );
await page.click( 'role=radio[name="Large"i]' );
await editor.showBlockToolbar();
diff --git a/test/e2e/specs/editor/various/list-view.spec.js b/test/e2e/specs/editor/various/list-view.spec.js
index b7021752ea8cfb..40f7cce283210c 100644
--- a/test/e2e/specs/editor/various/list-view.spec.js
+++ b/test/e2e/specs/editor/various/list-view.spec.js
@@ -4,6 +4,12 @@
const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
test.describe( 'List View', () => {
+ test.use( {
+ listViewUtils: async ( { page, pageUtils, editor }, use ) => {
+ await use( new ListViewUtils( { page, pageUtils, editor } ) );
+ },
+ } );
+
test.beforeEach( async ( { admin } ) => {
await admin.createNewPost();
} );
@@ -115,146 +121,6 @@ test.describe( 'List View', () => {
await expect( listView.getByRole( 'row' ) ).toHaveCount( 2 );
} );
- // Check for regression of https://github.com/WordPress/gutenberg/issues/39026.
- test( 'selects the previous block after removing the selected one', async ( {
- editor,
- page,
- pageUtils,
- } ) => {
- // Insert a couple of blocks of different types.
- await editor.insertBlock( { name: 'core/image' } );
- await editor.insertBlock( { name: 'core/heading' } );
- await editor.insertBlock( { name: 'core/paragraph' } );
-
- // Open List View.
- await pageUtils.pressKeys( 'access+o' );
- const listView = page.getByRole( 'treegrid', {
- name: 'Block navigation structure',
- } );
-
- // The last inserted block should be selected.
- await expect(
- listView.getByRole( 'gridcell', {
- name: 'Paragraph',
- exact: true,
- selected: true,
- } )
- ).toBeVisible();
-
- // Remove the Paragraph block via its options menu in List View.
- await listView
- .getByRole( 'button', { name: 'Options for Paragraph' } )
- .click();
- await page.getByRole( 'menuitem', { name: /Delete/i } ).click();
-
- // Heading block should be selected as previous block.
- await expect(
- editor.canvas.getByRole( 'document', {
- name: 'Block: Heading',
- } )
- ).toBeFocused();
- } );
-
- // Check for regression of https://github.com/WordPress/gutenberg/issues/39026.
- test( 'selects the next block after removing the very first block', async ( {
- editor,
- page,
- pageUtils,
- } ) => {
- // Insert a couple of blocks of different types.
- await editor.insertBlock( { name: 'core/image' } );
- await editor.insertBlock( { name: 'core/heading' } );
- await editor.insertBlock( { name: 'core/paragraph' } );
-
- // Open List View.
- await pageUtils.pressKeys( 'access+o' );
- const listView = page.getByRole( 'treegrid', {
- name: 'Block navigation structure',
- } );
-
- // The last inserted block should be selected.
- await expect(
- listView.getByRole( 'gridcell', {
- name: 'Paragraph',
- exact: true,
- selected: true,
- } )
- ).toBeVisible();
-
- // Select the image block in List View.
- await pageUtils.pressKeys( 'ArrowUp', { times: 2 } );
- await expect(
- listView.getByRole( 'link', {
- name: 'Image',
- } )
- ).toBeFocused();
- await page.keyboard.press( 'Enter' );
-
- // Remove the Image block via its options menu in List View.
- await listView
- .getByRole( 'button', { name: 'Options for Image' } )
- .click();
- await page.getByRole( 'menuitem', { name: /Delete/i } ).click();
-
- // Heading block should be selected as previous block.
- await expect(
- editor.canvas.getByRole( 'document', {
- name: 'Block: Heading',
- } )
- ).toBeFocused();
- } );
-
- /**
- * When all the blocks gets removed from the editor, it inserts a default
- * paragraph block; make sure that paragraph block gets selected after
- * removing blocks from ListView.
- */
- test( 'selects the default paragraph block after removing all blocks', async ( {
- editor,
- page,
- pageUtils,
- } ) => {
- // Insert a couple of blocks of different types.
- await editor.insertBlock( { name: 'core/image' } );
- await editor.insertBlock( { name: 'core/heading' } );
-
- // Open List View.
- await pageUtils.pressKeys( 'access+o' );
- const listView = page.getByRole( 'treegrid', {
- name: 'Block navigation structure',
- } );
-
- // The last inserted block should be selected.
- await expect(
- listView.getByRole( 'gridcell', {
- name: 'Heading',
- exact: true,
- selected: true,
- } )
- ).toBeVisible();
-
- // Select the Image block as well.
- await pageUtils.pressKeys( 'shift+ArrowUp' );
- await expect(
- listView.getByRole( 'gridcell', {
- name: 'Image',
- exact: true,
- selected: true,
- } )
- ).toBeVisible();
-
- // Remove both blocks.
- await listView
- .getByRole( 'button', { name: 'Options for Image' } )
- .click();
- await page.getByRole( 'menuitem', { name: /Delete blocks/i } ).click();
-
- // Newly created paragraph block should be selected.
- await expect(
- editor.canvas.getByRole( 'document', { name: /Empty block/i } )
- ).toBeFocused();
- } );
-
test( 'expands nested list items', async ( {
editor,
page,
@@ -395,6 +261,13 @@ test.describe( 'List View', () => {
page,
pageUtils,
} ) => {
+ // To do: run with iframe.
+ await page.evaluate( () => {
+ window.wp.blocks.registerBlockType( 'test/v2', {
+ apiVersion: '2',
+ title: 'test',
+ } );
+ } );
await editor.insertBlock( { name: 'core/image' } );
await editor.insertBlock( {
name: 'core/paragraph',
@@ -465,13 +338,12 @@ test.describe( 'List View', () => {
// Focus the list view close button and make sure the shortcut will
// close the list view. This is to catch a bug where elements could be
- // out of range of the sidebar region. Must shift+tab 3 times to reach
- // close button before tabs.
- await pageUtils.pressKeys( 'shift+Tab' );
+ // out of range of the sidebar region. Must shift+tab 2 times to reach
+ // close button before tab panel.
await pageUtils.pressKeys( 'shift+Tab' );
await pageUtils.pressKeys( 'shift+Tab' );
await expect(
- editor.canvas
+ page
.getByRole( 'region', { name: 'Document Overview' } )
.getByRole( 'button', {
name: 'Close',
@@ -488,7 +360,8 @@ test.describe( 'List View', () => {
// Focus the outline tab and select it. This test ensures the outline
// tab receives similar focus events based on the shortcut.
await pageUtils.pressKeys( 'shift+Tab' );
- const outlineButton = editor.canvas.getByRole( 'button', {
+ await page.keyboard.press( 'ArrowRight' );
+ const outlineButton = page.getByRole( 'tab', {
name: 'Outline',
} );
await expect( outlineButton ).toBeFocused();
@@ -557,4 +430,418 @@ test.describe( 'List View', () => {
} )
).toBeFocused();
} );
+
+ test( 'should delete blocks using keyboard', async ( {
+ editor,
+ page,
+ pageUtils,
+ listViewUtils,
+ } ) => {
+ // Insert some blocks of different types.
+ await editor.insertBlock( {
+ name: 'core/group',
+ innerBlocks: [ { name: 'core/pullquote' } ],
+ } );
+ await editor.insertBlock( {
+ name: 'core/columns',
+ innerBlocks: [
+ {
+ name: 'core/column',
+ innerBlocks: [
+ { name: 'core/heading' },
+ { name: 'core/paragraph' },
+ ],
+ },
+ {
+ name: 'core/column',
+ innerBlocks: [ { name: 'core/verse' } ],
+ },
+ ],
+ } );
+ await editor.insertBlock( { name: 'core/file' } );
+
+ // Open List View.
+ const listView = await listViewUtils.openListView();
+
+ await expect
+ .poll(
+ listViewUtils.getBlocksWithA11yAttributes,
+ 'The last inserted block should be selected and focused.'
+ )
+ .toMatchObject( [
+ { name: 'core/group' },
+ { name: 'core/columns' },
+ { name: 'core/file', selected: true, focused: true },
+ ] );
+
+ await page.keyboard.press( 'Delete' );
+ await expect
+ .poll(
+ listViewUtils.getBlocksWithA11yAttributes,
+ 'Deleting a block should move focus and selection to the previous block'
+ )
+ .toMatchObject( [
+ { name: 'core/group' },
+ { name: 'core/columns', selected: true, focused: true },
+ ] );
+
+ // Expand the current column.
+ await page.keyboard.press( 'ArrowRight' );
+ await page.keyboard.press( 'ArrowDown' );
+ await page.keyboard.press( 'ArrowDown' );
+ await expect
+ .poll(
+ listViewUtils.getBlocksWithA11yAttributes,
+ 'Move focus but do not select the second column'
+ )
+ .toMatchObject( [
+ { name: 'core/group' },
+ {
+ name: 'core/columns',
+ selected: true,
+ innerBlocks: [
+ { name: 'core/column' },
+ { name: 'core/column', focused: true },
+ ],
+ },
+ ] );
+
+ await page.keyboard.press( 'Delete' );
+ await expect
+ .poll(
+ listViewUtils.getBlocksWithA11yAttributes,
+ 'Deleting a inner block moves focus to the previous inner block'
+ )
+ .toMatchObject( [
+ { name: 'core/group' },
+ {
+ name: 'core/columns',
+ selected: true,
+ innerBlocks: [
+ {
+ name: 'core/column',
+ selected: false,
+ focused: true,
+ },
+ ],
+ },
+ ] );
+
+ // Expand the current column.
+ await page.keyboard.press( 'ArrowRight' );
+ // Move focus and select the Heading block.
+ await listView
+ .getByRole( 'gridcell', { name: 'Heading', exact: true } )
+ .dblclick();
+ // Select both inner blocks in the column.
+ await page.keyboard.press( 'Shift+ArrowDown' );
+
+ await page.keyboard.press( 'Backspace' );
+ await expect
+ .poll(
+ listViewUtils.getBlocksWithA11yAttributes,
+ 'Deleting multiple blocks moves focus to the parent block'
+ )
+ .toMatchObject( [
+ { name: 'core/group' },
+ {
+ name: 'core/columns',
+ innerBlocks: [
+ {
+ name: 'core/column',
+ selected: true,
+ focused: true,
+ innerBlocks: [],
+ },
+ ],
+ },
+ ] );
+
+ // Move focus and select the first block.
+ await listView
+ .getByRole( 'gridcell', { name: 'Group', exact: true } )
+ .dblclick();
+ await page.keyboard.press( 'Backspace' );
+ await expect
+ .poll(
+ listViewUtils.getBlocksWithA11yAttributes,
+ 'Deleting the first block moves focus to the second block'
+ )
+ .toMatchObject( [
+ {
+ name: 'core/columns',
+ selected: true,
+ focused: true,
+ },
+ ] );
+
+ // Keyboard shortcut should also work.
+ await pageUtils.pressKeys( 'access+z' );
+ await expect
+ .poll(
+ listViewUtils.getBlocksWithA11yAttributes,
+ 'Deleting the only block left will create a default block and focus/select it'
+ )
+ .toMatchObject( [
+ {
+ name: 'core/paragraph',
+ selected: true,
+ focused: true,
+ },
+ ] );
+
+ await editor.insertBlock( { name: 'core/heading' } );
+ await page.evaluate( () =>
+ window.wp.data.dispatch( 'core/block-editor' ).clearSelectedBlock()
+ );
+ await listView
+ .getByRole( 'gridcell', { name: 'Paragraph' } )
+ .getByRole( 'link' )
+ .focus();
+ await expect
+ .poll(
+ listViewUtils.getBlocksWithA11yAttributes,
+ 'Block selection is cleared and focus is on the paragraph block'
+ )
+ .toMatchObject( [
+ { name: 'core/paragraph', selected: false, focused: true },
+ { name: 'core/heading', selected: false },
+ ] );
+
+ await pageUtils.pressKeys( 'access+z' );
+ await expect
+ .poll(
+ listViewUtils.getBlocksWithA11yAttributes,
+ 'Deleting blocks without existing selection will not select blocks'
+ )
+ .toMatchObject( [
+ { name: 'core/heading', selected: false, focused: true },
+ ] );
+
+ // Insert a block that is locked and cannot be removed.
+ await editor.insertBlock( {
+ name: 'core/file',
+ attributes: { lock: { move: false, remove: true } },
+ } );
+ // Click on the Heading block to select it.
+ await listView
+ .getByRole( 'gridcell', { name: 'Heading', exact: true } )
+ .click();
+ await listView
+ .getByRole( 'gridcell', { name: 'File' } )
+ .getByRole( 'link' )
+ .focus();
+ for ( const keys of [ 'Delete', 'Backspace', 'access+z' ] ) {
+ await pageUtils.pressKeys( keys );
+ await expect
+ .poll(
+ listViewUtils.getBlocksWithA11yAttributes,
+ 'Trying to delete locked blocks should not do anything'
+ )
+ .toMatchObject( [
+ { name: 'core/heading', selected: true, focused: false },
+ { name: 'core/file', selected: false, focused: true },
+ ] );
+ }
+ } );
+
+ test( 'block settings dropdown menu', async ( {
+ editor,
+ page,
+ pageUtils,
+ listViewUtils,
+ } ) => {
+ // Insert some blocks of different types.
+ await editor.insertBlock( { name: 'core/heading' } );
+ await editor.insertBlock( { name: 'core/file' } );
+
+ // Open List View.
+ const listView = await listViewUtils.openListView();
+
+ await listView
+ .getByRole( 'button', { name: 'Options for Heading' } )
+ .click();
+
+ await page
+ .getByRole( 'menu', { name: 'Options for Heading' } )
+ .getByRole( 'menuitem', { name: 'Duplicate' } )
+ .click();
+ await expect
+ .poll(
+ listViewUtils.getBlocksWithA11yAttributes,
+ 'Should duplicate a block and move focus'
+ )
+ .toMatchObject( [
+ { name: 'core/heading', selected: false },
+ { name: 'core/heading', selected: false, focused: true },
+ { name: 'core/file', selected: true },
+ ] );
+
+ await page.keyboard.press( 'Shift+ArrowUp' );
+ await listView
+ .getByRole( 'button', { name: 'Options for Heading' } )
+ .first()
+ .click();
+ await page
+ .getByRole( 'menu', { name: 'Options for Heading' } )
+ .getByRole( 'menuitem', { name: 'Delete blocks' } )
+ .click();
+ await expect
+ .poll(
+ listViewUtils.getBlocksWithA11yAttributes,
+ 'Should delete multiple selected blocks using the dropdown menu'
+ )
+ .toMatchObject( [
+ { name: 'core/file', selected: true, focused: true },
+ ] );
+
+ await page.keyboard.press( 'ArrowRight' );
+ const optionsForFileToggle = listView
+ .getByRole( 'row' )
+ .filter( {
+ has: page.getByRole( 'gridcell', { name: 'File' } ),
+ } )
+ .getByRole( 'button', { name: 'Options for File' } );
+ const optionsForFileMenu = page.getByRole( 'menu', {
+ name: 'Options for File',
+ } );
+ await expect(
+ optionsForFileToggle,
+ 'Pressing arrow right should move focus to the menu dropdown toggle button'
+ ).toBeFocused();
+
+ await page.keyboard.press( 'Enter' );
+ await expect(
+ optionsForFileMenu,
+ 'Pressing Enter should open the menu dropdown'
+ ).toBeVisible();
+
+ await page.keyboard.press( 'Escape' );
+ await expect(
+ optionsForFileMenu,
+ 'Pressing Escape should close the menu dropdown'
+ ).toBeHidden();
+ await expect(
+ optionsForFileToggle,
+ 'Should move focus back to the toggle button'
+ ).toBeFocused();
+
+ await page.keyboard.press( 'Space' );
+ await expect(
+ optionsForFileMenu,
+ 'Pressing Space should also open the menu dropdown'
+ ).toBeVisible();
+
+ await pageUtils.pressKeys( 'primaryAlt+t' ); // Keyboard shortcut for Insert before.
+ await expect
+ .poll(
+ listViewUtils.getBlocksWithA11yAttributes,
+ 'Pressing keyboard shortcut should also work when the menu is opened and focused'
+ )
+ .toMatchObject( [
+ { name: 'core/paragraph', selected: true, focused: false },
+ { name: 'core/file', selected: false, focused: false },
+ ] );
+ await expect(
+ optionsForFileMenu,
+ 'The menu should be closed after pressing keyboard shortcut'
+ ).toBeHidden();
+
+ await optionsForFileToggle.click();
+ await pageUtils.pressKeys( 'access+z' ); // Keyboard shortcut for Delete.
+ await expect
+ .poll(
+ listViewUtils.getBlocksWithA11yAttributes,
+ 'Deleting blocks should move focus and selection'
+ )
+ .toMatchObject( [
+ { name: 'core/paragraph', selected: true, focused: true },
+ ] );
+
+ // Insert a block that is locked and cannot be removed.
+ await editor.insertBlock( {
+ name: 'core/file',
+ attributes: { lock: { move: false, remove: true } },
+ } );
+ await optionsForFileToggle.click();
+ await expect(
+ optionsForFileMenu.getByRole( 'menuitem', { name: 'Delete' } ),
+ 'The delete menu item should be hidden for locked blocks'
+ ).toBeHidden();
+ await pageUtils.pressKeys( 'access+z' );
+ await expect
+ .poll(
+ listViewUtils.getBlocksWithA11yAttributes,
+ 'Pressing keyboard shortcut should not delete locked blocks either'
+ )
+ .toMatchObject( [
+ { name: 'core/paragraph' },
+ { name: 'core/file', selected: true },
+ ] );
+ await expect(
+ optionsForFileMenu,
+ 'The dropdown menu should also be visible'
+ ).toBeVisible();
+ } );
} );
+
+/** @typedef {import('@playwright/test').Locator} Locator */
+class ListViewUtils {
+ #page;
+ #pageUtils;
+ #editor;
+
+ constructor( { page, pageUtils, editor } ) {
+ this.#page = page;
+ this.#pageUtils = pageUtils;
+ this.#editor = editor;
+
+ /** @type {Locator} */
+ this.listView = page.getByRole( 'treegrid', {
+ name: 'Block navigation structure',
+ } );
+ }
+
+ /**
+ * @return {Promise
} The list view locator.
+ */
+ openListView = async () => {
+ await this.#pageUtils.pressKeys( 'access+o' );
+ return this.listView;
+ };
+
+ getBlocksWithA11yAttributes = async () => {
+ const selectedRows = await this.listView
+ .getByRole( 'row' )
+ .filter( {
+ has: this.#page.getByRole( 'gridcell', { selected: true } ),
+ } )
+ .all();
+ const selectedClientIds = await Promise.all(
+ selectedRows.map( ( row ) => row.getAttribute( 'data-block' ) )
+ );
+ const focusedRows = await this.listView
+ .getByRole( 'row' )
+ .filter( { has: this.#page.locator( ':focus' ) } )
+ .all();
+ const focusedClientId =
+ focusedRows.length > 0
+ ? await focusedRows[ focusedRows.length - 1 ].getAttribute(
+ 'data-block'
+ )
+ : null;
+ // Don't use the util to get the unmodified default block when it's empty.
+ const blocks = await this.#page.evaluate( () =>
+ window.wp.data.select( 'core/block-editor' ).getBlocks()
+ );
+ function recursivelyApplyAttributes( _blocks ) {
+ return _blocks.map( ( block ) => ( {
+ name: block.name,
+ selected: selectedClientIds.includes( block.clientId ),
+ focused: block.clientId === focusedClientId,
+ innerBlocks: recursivelyApplyAttributes( block.innerBlocks ),
+ } ) );
+ }
+ return recursivelyApplyAttributes( blocks );
+ };
+}
diff --git a/test/e2e/specs/editor/various/mentions.spec.js b/test/e2e/specs/editor/various/mentions.spec.js
index b7e75e046c471a..061b8d67a0801f 100644
--- a/test/e2e/specs/editor/various/mentions.spec.js
+++ b/test/e2e/specs/editor/various/mentions.spec.js
@@ -23,7 +23,7 @@ test.describe( 'autocomplete mentions', () => {
} );
test( 'should insert mention', async ( { page, editor } ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'I am @ad' );
await expect(
page.locator( 'role=listbox >> role=option[name=/admin/i]' )
@@ -42,7 +42,7 @@ test.describe( 'autocomplete mentions', () => {
editor,
pageUtils,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'Stuck in the middle with you' );
await pageUtils.pressKeys( 'ArrowLeft', { times: 'you'.length } );
await page.keyboard.type( '@j' );
@@ -62,7 +62,7 @@ test.describe( 'autocomplete mentions', () => {
page,
editor,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'I am @j' );
await expect(
page.locator( 'role=listbox >> role=option[name=/testuser/i]' )
diff --git a/test/e2e/specs/editor/various/multi-block-selection.spec.js b/test/e2e/specs/editor/various/multi-block-selection.spec.js
index d72ab2c6bf7dbf..26dee1291ba2f4 100644
--- a/test/e2e/specs/editor/various/multi-block-selection.spec.js
+++ b/test/e2e/specs/editor/various/multi-block-selection.spec.js
@@ -247,6 +247,13 @@ test.describe( 'Multi-block selection', () => {
editor,
multiBlockSelectionUtils,
} ) => {
+ // To do: run with iframe.
+ await page.evaluate( () => {
+ window.wp.blocks.registerBlockType( 'test/v2', {
+ apiVersion: '2',
+ title: 'test',
+ } );
+ } );
await editor.canvas
.getByRole( 'button', { name: 'Add default block' } )
.click();
@@ -292,6 +299,13 @@ test.describe( 'Multi-block selection', () => {
editor,
pageUtils,
} ) => {
+ // To do: run with iframe.
+ await page.evaluate( () => {
+ window.wp.blocks.registerBlockType( 'test/v2', {
+ apiVersion: '2',
+ title: 'test',
+ } );
+ } );
await editor.insertBlock( {
name: 'core/paragraph',
attributes: { content: 'test' },
@@ -304,6 +318,13 @@ test.describe( 'Multi-block selection', () => {
.filter( { hasText: 'Draft saved' } )
).toBeVisible();
await page.reload();
+ // To do: run with iframe.
+ await page.evaluate( () => {
+ window.wp.blocks.registerBlockType( 'test/v2', {
+ apiVersion: '2',
+ title: 'test',
+ } );
+ } );
await editor.canvas
.getByRole( 'document', { name: 'Paragraph block' } )
@@ -867,7 +888,6 @@ test.describe( 'Multi-block selection', () => {
} );
test( 'should select title if the cursor is on title', async ( {
- page,
editor,
pageUtils,
multiBlockSelectionUtils,
@@ -890,7 +910,7 @@ test.describe( 'Multi-block selection', () => {
.toEqual( [] );
await expect
.poll( () =>
- page.evaluate( () => window.getSelection().toString() )
+ editor.canvas.evaluate( () => window.getSelection().toString() )
)
.toBe( 'Post title' );
} );
@@ -1142,6 +1162,13 @@ test.describe( 'Multi-block selection', () => {
page,
editor,
} ) => {
+ // To do: run with iframe.
+ await page.evaluate( () => {
+ window.wp.blocks.registerBlockType( 'test/v2', {
+ apiVersion: '2',
+ title: 'test',
+ } );
+ } );
await editor.insertBlock( {
name: 'core/paragraph',
attributes: { content: '1 [' },
diff --git a/test/e2e/specs/editor/various/new-post-default-content.spec.js b/test/e2e/specs/editor/various/new-post-default-content.spec.js
index 82c8e3a948f31b..db9e3c38dc2962 100644
--- a/test/e2e/specs/editor/various/new-post-default-content.spec.js
+++ b/test/e2e/specs/editor/various/new-post-default-content.spec.js
@@ -27,7 +27,7 @@ test.describe( 'new editor filtered state', () => {
// Assert they match what the plugin set.
await expect(
- page.locator( 'role=textbox[name="Add title"i]' )
+ editor.canvas.locator( 'role=textbox[name="Add title"i]' )
).toHaveText( 'My default title' );
await expect
.poll( editor.getEditedPostContent )
diff --git a/test/e2e/specs/editor/various/new-post.spec.js b/test/e2e/specs/editor/various/new-post.spec.js
index e58e8ed94ffc56..4b192693c07b07 100644
--- a/test/e2e/specs/editor/various/new-post.spec.js
+++ b/test/e2e/specs/editor/various/new-post.spec.js
@@ -26,7 +26,9 @@ test.describe( 'new editor state', () => {
await expect( page ).toHaveURL( /post-new.php/ );
// Should display the blank title.
- const title = page.locator( 'role=textbox[name="Add title"i]' );
+ const title = editor.canvas.locator(
+ 'role=textbox[name="Add title"i]'
+ );
await expect( title ).toBeEditable();
await expect( title ).toHaveText( '' );
@@ -55,23 +57,24 @@ test.describe( 'new editor state', () => {
test( 'should focus the title if the title is empty', async ( {
admin,
- page,
+ editor,
} ) => {
await admin.createNewPost();
await expect(
- page.locator( 'role=textbox[name="Add title"i]' )
+ editor.canvas.locator( 'role=textbox[name="Add title"i]' )
).toBeFocused();
} );
test( 'should not focus the title if the title exists', async ( {
admin,
page,
+ editor,
} ) => {
await admin.createNewPost();
// Enter a title for this post.
- await page.type(
+ await editor.canvas.type(
'role=textbox[name="Add title"i]',
'Here is the title'
);
diff --git a/test/e2e/specs/editor/various/post-editor-template-mode.spec.js b/test/e2e/specs/editor/various/post-editor-template-mode.spec.js
index c7aed497dc25cc..9bbecb51223fc0 100644
--- a/test/e2e/specs/editor/various/post-editor-template-mode.spec.js
+++ b/test/e2e/specs/editor/various/post-editor-template-mode.spec.js
@@ -101,188 +101,6 @@ test.describe( 'Post Editor Template mode', () => {
)
).toBeVisible();
} );
-
- test( 'Allow editing the title of a new custom template', async ( {
- page,
- postEditorTemplateMode,
- } ) => {
- async function editTemplateTitle( newTitle ) {
- await page
- .getByRole( 'button', { name: 'Template Options' } )
- .click();
-
- await page
- .getByRole( 'textbox', { name: 'Title' } )
- .fill( newTitle );
-
- const editorContent = page.getByLabel( 'Editor Content' );
- await editorContent.click();
- }
-
- await postEditorTemplateMode.createPostAndSaveDraft();
- await postEditorTemplateMode.createNewTemplate( 'Foobar' );
- await editTemplateTitle( 'Barfoo' );
-
- await expect(
- page.getByRole( 'button', { name: 'Template Options' } )
- ).toHaveText( 'Barfoo' );
- } );
-
- test.describe( 'Delete Post Template Confirmation Dialog', () => {
- test.beforeAll( async ( { requestUtils } ) => {
- await requestUtils.activateTheme( 'twentytwentyone' );
- } );
-
- test.beforeEach( async ( { postEditorTemplateMode } ) => {
- await postEditorTemplateMode.createPostAndSaveDraft();
- } );
-
- [ 'large', 'small' ].forEach( ( viewport ) => {
- test( `should retain template if deletion is canceled when the viewport is ${ viewport }`, async ( {
- editor,
- page,
- pageUtils,
- postEditorTemplateMode,
- } ) => {
- await pageUtils.setBrowserViewport( viewport );
-
- await postEditorTemplateMode.disableTemplateWelcomeGuide();
-
- const templateTitle = `${ viewport } Viewport Deletion Test`;
-
- await postEditorTemplateMode.createNewTemplate( templateTitle );
-
- // Close the settings in small viewport.
- if ( viewport === 'small' ) {
- await page.click( 'role=button[name="Close settings"i]' );
- }
-
- // Edit the template.
- await editor.insertBlock( { name: 'core/paragraph' } );
- await page.keyboard.type(
- 'Just a random paragraph added to the template'
- );
-
- await postEditorTemplateMode.saveTemplateWithoutPublishing();
-
- // Test deletion dialog.
- {
- const templateDropdown =
- postEditorTemplateMode.editorTopBar.locator(
- 'role=button[name="Template Options"i]'
- );
- await templateDropdown.click();
- await page.click(
- 'role=menuitem[name="Delete template"i]'
- );
-
- const confirmDeletionDialog = page.locator( 'role=dialog' );
- await expect( confirmDeletionDialog ).toBeFocused();
- await expect(
- confirmDeletionDialog.locator(
- `text=Are you sure you want to delete the ${ templateTitle } template? It may be used by other pages or posts.`
- )
- ).toBeVisible();
-
- await confirmDeletionDialog
- .locator( 'role=button[name="Cancel"i]' )
- .click();
- }
-
- // Exit template mode.
- await page.click( 'role=button[name="Back"i]' );
-
- await editor.openDocumentSettingsSidebar();
-
- // Move focus to the "Post" panel in the editor sidebar.
- const postPanel =
- postEditorTemplateMode.editorSettingsSidebar.locator(
- 'role=button[name="Post"i]'
- );
- await postPanel.click();
-
- await postEditorTemplateMode.openTemplatePopover();
-
- const templateSelect = page.locator(
- 'role=combobox[name="Template"i]'
- );
- await expect( templateSelect ).toHaveValue(
- `${ viewport }-viewport-deletion-test`
- );
- } );
-
- test( `should delete template if deletion is confirmed when the viewport is ${ viewport }`, async ( {
- editor,
- page,
- pageUtils,
- postEditorTemplateMode,
- } ) => {
- const templateTitle = `${ viewport } Viewport Deletion Test`;
-
- await pageUtils.setBrowserViewport( viewport );
-
- await postEditorTemplateMode.createNewTemplate( templateTitle );
-
- // Close the settings in small viewport.
- if ( viewport === 'small' ) {
- await page.click( 'role=button[name="Close settings"i]' );
- }
-
- // Edit the template.
- await editor.insertBlock( { name: 'core/paragraph' } );
- await page.keyboard.type(
- 'Just a random paragraph added to the template'
- );
-
- await postEditorTemplateMode.saveTemplateWithoutPublishing();
-
- {
- const templateDropdown =
- postEditorTemplateMode.editorTopBar.locator(
- 'role=button[name="Template Options"i]'
- );
- await templateDropdown.click();
- await page.click(
- 'role=menuitem[name="Delete template"i]'
- );
-
- const confirmDeletionDialog = page.locator( 'role=dialog' );
- await expect( confirmDeletionDialog ).toBeFocused();
- await expect(
- confirmDeletionDialog.locator(
- `text=Are you sure you want to delete the ${ templateTitle } template? It may be used by other pages or posts.`
- )
- ).toBeVisible();
-
- await confirmDeletionDialog
- .locator( 'role=button[name="OK"i]' )
- .click();
- }
-
- // Saving isn't technically necessary, but for themes without any specified templates,
- // the removal of the Templates dropdown is delayed. A save and reload allows for this
- // delay and prevents flakiness
- {
- await page.click( 'role=button[name="Save draft"i]' );
- await page.waitForSelector(
- 'role=button[name="Dismiss this notice"] >> text=Draft saved'
- );
- await page.reload();
- }
-
- const templateOptions =
- postEditorTemplateMode.editorSettingsSidebar.locator(
- 'role=combobox[name="Template:"i] >> role=menuitem'
- );
- const availableTemplates =
- await templateOptions.allTextContents();
-
- expect( availableTemplates ).not.toContain(
- `${ viewport } Viewport Deletion Test`
- );
- } );
- } );
- } );
} );
class PostEditorTemplateMode {
@@ -331,7 +149,9 @@ class PostEditorTemplateMode {
'role=button[name="Dismiss this notice"] >> text=Editing template. Changes made here affect all posts and pages that use the template.'
);
- await expect( this.editorTopBar ).toHaveText( /Just an FSE Post/ );
+ await expect(
+ this.editorTopBar.getByRole( 'heading[level=1]' )
+ ).toHaveText( 'Editing template: Singular' );
}
async createPostAndSaveDraft() {
diff --git a/test/e2e/specs/editor/various/post-visibility.spec.js b/test/e2e/specs/editor/various/post-visibility.spec.js
index 611e260e17de54..3f83221c27b819 100644
--- a/test/e2e/specs/editor/various/post-visibility.spec.js
+++ b/test/e2e/specs/editor/various/post-visibility.spec.js
@@ -78,7 +78,7 @@ test.describe( 'Post visibility', () => {
await admin.createNewPost();
// Enter a title for this post.
- await page.type( 'role=textbox[name="Add title"i]', 'Title' );
+ await editor.canvas.type( 'role=textbox[name="Add title"i]', 'Title' );
await editor.openDocumentSettingsSidebar();
diff --git a/test/e2e/specs/editor/various/preview.spec.js b/test/e2e/specs/editor/various/preview.spec.js
index d71d9ada9b5103..3f5e6eef60d3dc 100644
--- a/test/e2e/specs/editor/various/preview.spec.js
+++ b/test/e2e/specs/editor/various/preview.spec.js
@@ -27,7 +27,7 @@ test.describe( 'Preview', () => {
editorPage.locator( 'role=button[name="Preview"i]' )
).toBeDisabled();
- await editorPage.type(
+ await editor.canvas.type(
'role=textbox[name="Add title"i]',
'Hello World'
);
@@ -48,7 +48,7 @@ test.describe( 'Preview', () => {
// Return to editor to change title.
await editorPage.bringToFront();
- await editorPage.type( 'role=textbox[name="Add title"i]', '!' );
+ await editor.canvas.type( 'role=textbox[name="Add title"i]', '!' );
await previewUtils.waitForPreviewNavigation( previewPage );
// Title in preview should match updated input.
@@ -70,7 +70,7 @@ test.describe( 'Preview', () => {
// Return to editor to change title.
await editorPage.bringToFront();
- await editorPage.fill(
+ await editor.canvas.fill(
'role=textbox[name="Add title"i]',
'Hello World! And more.'
);
@@ -107,7 +107,7 @@ test.describe( 'Preview', () => {
const editorPage = page;
// Type aaaaa in the title field.
- await editorPage.type( 'role=textbox[name="Add title"]', 'aaaaa' );
+ await editor.canvas.type( 'role=textbox[name="Add title"]', 'aaaaa' );
await editorPage.keyboard.press( 'Tab' );
// Save the post as a draft.
@@ -127,7 +127,7 @@ test.describe( 'Preview', () => {
await editorPage.bringToFront();
// Append bbbbb to the title, and tab away from the title so blur event is triggered.
- await editorPage.fill(
+ await editor.canvas.fill(
'role=textbox[name="Add title"i]',
'aaaaabbbbb'
);
@@ -155,7 +155,7 @@ test.describe( 'Preview', () => {
const editorPage = page;
// Type Lorem in the title field.
- await editorPage.type( 'role=textbox[name="Add title"i]', 'Lorem' );
+ await editor.canvas.type( 'role=textbox[name="Add title"i]', 'Lorem' );
// Open the preview page.
const previewPage = await editor.openPreviewPage( editorPage );
@@ -172,7 +172,7 @@ test.describe( 'Preview', () => {
await page.click( 'role=button[name="Close panel"i]' );
// Change the title and preview again.
- await editorPage.type( 'role=textbox[name="Add title"i]', ' Ipsum' );
+ await editor.canvas.type( 'role=textbox[name="Add title"i]', ' Ipsum' );
await previewUtils.waitForPreviewNavigation( previewPage );
// Title in preview should match updated input.
@@ -191,7 +191,7 @@ test.describe( 'Preview', () => {
).toBeVisible();
// Change the title.
- await editorPage.type( 'role=textbox[name="Add title"i]', ' Draft' );
+ await editor.canvas.type( 'role=textbox[name="Add title"i]', ' Draft' );
// Open the preview page.
await previewUtils.waitForPreviewNavigation( previewPage );
@@ -222,7 +222,10 @@ test.describe( 'Preview with Custom Fields enabled', () => {
const editorPage = page;
// Add an initial title and content.
- await editorPage.type( 'role=textbox[name="Add title"i]', 'title 1' );
+ await editor.canvas.type(
+ 'role=textbox[name="Add title"i]',
+ 'title 1'
+ );
await editor.insertBlock( {
name: 'core/paragraph',
attributes: { content: 'content 1' },
@@ -246,8 +249,11 @@ test.describe( 'Preview with Custom Fields enabled', () => {
// Return to editor and modify the title and content.
await editorPage.bringToFront();
- await editorPage.fill( 'role=textbox[name="Add title"i]', 'title 2' );
- await editorPage.fill(
+ await editor.canvas.fill(
+ 'role=textbox[name="Add title"i]',
+ 'title 2'
+ );
+ await editor.canvas.fill(
'role=document >> text="content 1"',
'content 2'
);
diff --git a/test/e2e/specs/editor/various/rtl.spec.js b/test/e2e/specs/editor/various/rtl.spec.js
index 899dfd3c87ddec..8475605e339fcb 100644
--- a/test/e2e/specs/editor/various/rtl.spec.js
+++ b/test/e2e/specs/editor/various/rtl.spec.js
@@ -150,7 +150,7 @@ test.describe( 'RTL', () => {
page,
pageUtils,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await pageUtils.pressKeys( 'primary+b' );
await page.keyboard.type( ARABIC_ONE );
await pageUtils.pressKeys( 'primary+b' );
diff --git a/test/e2e/specs/editor/various/splitting-merging.spec.js b/test/e2e/specs/editor/various/splitting-merging.spec.js
index 08aa33ec095522..16b5225cfc8d38 100644
--- a/test/e2e/specs/editor/various/splitting-merging.spec.js
+++ b/test/e2e/specs/editor/various/splitting-merging.spec.js
@@ -306,11 +306,11 @@ test.describe( 'splitting and merging blocks', () => {
// There is a default block and post title:
await expect(
- page.locator( 'role=document[name=/Empty block/i]' )
+ editor.canvas.locator( 'role=document[name=/Empty block/i]' )
).toBeVisible();
await expect(
- page.locator( 'role=textbox[name="Add title"i]' )
+ editor.canvas.locator( 'role=textbox[name="Add title"i]' )
).toBeVisible();
// But the effective saved content is still empty:
@@ -318,7 +318,7 @@ test.describe( 'splitting and merging blocks', () => {
// And focus is retained:
await expect(
- page.locator( 'role=document[name=/Empty block/i]' )
+ editor.canvas.locator( 'role=document[name=/Empty block/i]' )
).toBeFocused();
} );
diff --git a/test/e2e/specs/editor/various/toolbar-roving-tabindex.spec.js b/test/e2e/specs/editor/various/toolbar-roving-tabindex.spec.js
index ccb5952a571256..834bed77e87425 100644
--- a/test/e2e/specs/editor/various/toolbar-roving-tabindex.spec.js
+++ b/test/e2e/specs/editor/various/toolbar-roving-tabindex.spec.js
@@ -104,7 +104,7 @@ test.describe( 'Toolbar roving tabindex', () => {
// Move focus to the first toolbar item.
await page.keyboard.press( 'Home' );
await ToolbarRovingTabindexUtils.expectLabelToHaveFocus( 'Table' );
- await page.click( `role=button[name="Create Table"i]` );
+ await editor.canvas.click( `role=button[name="Create Table"i]` );
await pageUtils.pressKeys( 'Tab' );
await ToolbarRovingTabindexUtils.testBlockToolbarKeyboardNavigation(
'Body cell text',
@@ -188,15 +188,19 @@ class ToolbarRovingTabindexUtils {
}
async expectLabelToHaveFocus( label ) {
- let ariaLabel = await this.page.evaluate( () =>
- document.activeElement.getAttribute( 'aria-label' )
- );
+ let ariaLabel = await this.page.evaluate( () => {
+ const { activeElement } =
+ document.activeElement.contentDocument ?? document;
+ return activeElement.getAttribute( 'aria-label' );
+ } );
// If the labels don't match, try pressing Up Arrow to focus the block wrapper in non-content editable block.
if ( ariaLabel !== label ) {
await this.page.keyboard.press( 'ArrowUp' );
- ariaLabel = await this.page.evaluate( () =>
- document.activeElement.getAttribute( 'aria-label' )
- );
+ ariaLabel = await this.page.evaluate( () => {
+ const { activeElement } =
+ document.activeElement.contentDocument ?? document;
+ return activeElement.getAttribute( 'aria-label' );
+ } );
}
expect( ariaLabel ).toBe( label );
}
diff --git a/test/e2e/specs/editor/various/undo.spec.js b/test/e2e/specs/editor/various/undo.spec.js
index 29b34ea416ff29..5c4355882ee890 100644
--- a/test/e2e/specs/editor/various/undo.spec.js
+++ b/test/e2e/specs/editor/various/undo.spec.js
@@ -20,7 +20,7 @@ test.describe( 'undo', () => {
pageUtils,
undoUtils,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'before pause' );
await editor.page.waitForTimeout( 1000 );
await page.keyboard.type( ' after pause' );
@@ -88,7 +88,7 @@ test.describe( 'undo', () => {
pageUtils,
undoUtils,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'before keyboard ' );
await pageUtils.pressKeys( 'primary+b' );
@@ -159,8 +159,8 @@ test.describe( 'undo', () => {
} );
} );
- test( 'should undo bold', async ( { page, pageUtils } ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ test( 'should undo bold', async ( { page, pageUtils, editor } ) => {
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'test' );
await page.click( 'role=button[name="Save draft"i]' );
await expect(
@@ -169,11 +169,12 @@ test.describe( 'undo', () => {
)
).toBeVisible();
await page.reload();
- await page.click( '[data-type="core/paragraph"]' );
+ await page.waitForSelector( 'iframe[name="editor-canvas"]' );
+ await editor.canvas.click( '[data-type="core/paragraph"]' );
await pageUtils.pressKeys( 'primary+a' );
await pageUtils.pressKeys( 'primary+b' );
await pageUtils.pressKeys( 'primary+z' );
- const activeElementLocator = page.locator( ':focus' );
+ const activeElementLocator = editor.canvas.locator( ':focus' );
await expect( activeElementLocator ).toHaveText( 'test' );
} );
@@ -183,7 +184,7 @@ test.describe( 'undo', () => {
pageUtils,
undoUtils,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
const firstBlock = await editor.getEditedPostContent();
@@ -326,7 +327,7 @@ test.describe( 'undo', () => {
// See: https://github.com/WordPress/gutenberg/issues/14950
// Issue is demonstrated from an edited post: create, save, and reload.
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( 'original' );
await page.click( 'role=button[name="Save draft"i]' );
await expect(
@@ -335,10 +336,11 @@ test.describe( 'undo', () => {
)
).toBeVisible();
await page.reload();
+ await page.waitForSelector( 'iframe[name="editor-canvas"]' );
// Issue is demonstrated by forcing state merges (multiple inputs) on
// an existing text after a fresh reload.
- await page.click( '[data-type="core/paragraph"] >> nth=0' );
+ await editor.canvas.click( '[data-type="core/paragraph"] >> nth=0' );
await page.keyboard.type( 'modified' );
// The issue is demonstrated after the one second delay to trigger the
@@ -351,7 +353,7 @@ test.describe( 'undo', () => {
// regression present was accurate, it would produce the correct
// content. The issue had manifested in the form of what was shown to
// the user since the blocks state failed to sync to block editor.
- const activeElementLocator = page.locator( ':focus' );
+ const activeElementLocator = editor.canvas.locator( ':focus' );
await expect( activeElementLocator ).toHaveText( 'original' );
} );
@@ -360,7 +362,7 @@ test.describe( 'undo', () => {
page,
pageUtils,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '1' );
await page.click( 'role=button[name="Save draft"i]' );
await expect(
@@ -378,7 +380,7 @@ test.describe( 'undo', () => {
page,
pageUtils,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '1' );
await editor.publishPost();
await pageUtils.pressKeys( 'primary+z' );
@@ -391,7 +393,7 @@ test.describe( 'undo', () => {
page,
pageUtils,
} ) => {
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '1' );
await page.click( 'role=button[name="Save draft"i]' );
@@ -406,7 +408,7 @@ test.describe( 'undo', () => {
await expect(
page.locator( 'role=button[name="Undo"]' )
).toBeDisabled();
- await page.click( '[data-type="core/paragraph"]' );
+ await editor.canvas.click( '[data-type="core/paragraph"]' );
await page.keyboard.type( '2' );
@@ -436,7 +438,7 @@ test.describe( 'undo', () => {
// block attribute as in the previous action and results in transient edits
// and skipping `undo` history steps.
const text = 'tonis';
- await page.click( 'role=button[name="Add default block"i]' );
+ await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( text );
await editor.publishPost();
await pageUtils.pressKeys( 'primary+z' );
@@ -455,6 +457,40 @@ test.describe( 'undo', () => {
},
] );
} );
+
+ // @see https://github.com/WordPress/gutenberg/issues/12075
+ test( 'should be able to undo and redo property cross property changes', async ( {
+ page,
+ pageUtils,
+ editor,
+ } ) => {
+ await editor.canvas
+ .getByRole( 'textbox', { name: 'Add title' } )
+ .type( 'a' ); // First step.
+ await page.keyboard.press( 'Backspace' ); // Second step.
+ await editor.canvas
+ .getByRole( 'button', { name: 'Add default block' } )
+ .click(); // third step.
+
+ // Title should be empty
+ await expect(
+ editor.canvas.getByRole( 'textbox', { name: 'Add title' } )
+ ).toHaveText( '' );
+
+ // First undo removes the block.
+ // Second undo restores the title.
+ await pageUtils.pressKeys( 'primary+z' );
+ await pageUtils.pressKeys( 'primary+z' );
+ await expect(
+ editor.canvas.getByRole( 'textbox', { name: 'Add title' } )
+ ).toHaveText( 'a' );
+
+ // Redoing the "backspace" should clear the title again.
+ await pageUtils.pressKeys( 'primaryShift+z' );
+ await expect(
+ editor.canvas.getByRole( 'textbox', { name: 'Add title' } )
+ ).toHaveText( '' );
+ } );
} );
class UndoUtils {
diff --git a/test/e2e/specs/editor/various/writing-flow.spec.js b/test/e2e/specs/editor/various/writing-flow.spec.js
index 80e3fb3b126827..99de69819d9f33 100644
--- a/test/e2e/specs/editor/various/writing-flow.spec.js
+++ b/test/e2e/specs/editor/various/writing-flow.spec.js
@@ -4,8 +4,8 @@
const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
test.use( {
- writingFlowUtils: async ( { page }, use ) => {
- await use( new WritingFlowUtils( { page } ) );
+ writingFlowUtils: async ( { page, editor }, use ) => {
+ await use( new WritingFlowUtils( { page, editor } ) );
},
} );
@@ -29,7 +29,7 @@ test.describe( 'Writing Flow', () => {
// See: https://github.com/WordPress/gutenberg/issues/18928
await writingFlowUtils.addDemoContent();
- const activeElementLocator = page.locator( ':focus' );
+ const activeElementLocator = editor.canvas.locator( ':focus' );
// Arrow up into nested context focuses last text input.
await page.keyboard.press( 'ArrowUp' );
@@ -46,7 +46,7 @@ test.describe( 'Writing Flow', () => {
.poll( writingFlowUtils.getActiveBlockName )
.toBe( 'core/column' );
await page.keyboard.press( 'ArrowUp' );
- const activeElementBlockType = await page.evaluate( () =>
+ const activeElementBlockType = await editor.canvas.evaluate( () =>
document.activeElement.getAttribute( 'data-type' )
);
expect( activeElementBlockType ).toBe( 'core/columns' );
@@ -317,25 +317,25 @@ test.describe( 'Writing Flow', () => {
await editor.insertBlock( { name: 'core/paragraph' } );
await page.keyboard.type( 'abc' ); // Need content to remove placeholder label.
await editor.selectBlocks(
- page.locator( 'role=document[name="Block: Shortcode"i]' )
+ editor.canvas.locator( 'role=document[name="Block: Shortcode"i]' )
);
// Should remain in title upon ArrowRight:
await page.keyboard.press( 'ArrowRight' );
await expect(
- page.locator( 'role=document[name="Block: Shortcode"i]' )
+ editor.canvas.locator( 'role=document[name="Block: Shortcode"i]' )
).toHaveClass( /is-selected/ );
// Should remain in title upon modifier + ArrowDown:
await pageUtils.pressKeys( 'primary+ArrowDown' );
await expect(
- page.locator( 'role=document[name="Block: Shortcode"i]' )
+ editor.canvas.locator( 'role=document[name="Block: Shortcode"i]' )
).toHaveClass( /is-selected/ );
// Should navigate to the next block.
await page.keyboard.press( 'ArrowDown' );
await expect(
- page.locator( 'role=document[name="Paragraph block"i]' )
+ editor.canvas.locator( 'role=document[name="Paragraph block"i]' )
).toHaveClass( /is-selected/ );
} );
@@ -447,12 +447,12 @@ test.describe( 'Writing Flow', () => {
} ) => {
await page.keyboard.press( 'Enter' );
await page.keyboard.press( 'Enter' );
- await page.evaluate( () => {
+ await editor.canvas.evaluate( () => {
document.activeElement.style.paddingTop = '100px';
} );
await page.keyboard.press( 'ArrowUp' );
await page.keyboard.type( '1' );
- await page.evaluate( () => {
+ await editor.canvas.evaluate( () => {
document.activeElement.style.paddingBottom = '100px';
} );
await page.keyboard.press( 'ArrowDown' );
@@ -467,7 +467,7 @@ test.describe( 'Writing Flow', () => {
} ) => {
await page.keyboard.press( 'Enter' );
await page.keyboard.press( 'Enter' );
- await page.evaluate( () => {
+ await editor.canvas.evaluate( () => {
document.activeElement.style.lineHeight = 'normal';
} );
await page.keyboard.press( 'ArrowUp' );
@@ -650,7 +650,7 @@ test.describe( 'Writing Flow', () => {
} ) => {
await page.keyboard.press( 'Enter' );
await page.keyboard.press( 'Enter' );
- await page.evaluate( () => {
+ await editor.canvas.evaluate( () => {
document.activeElement.style.paddingLeft = '100px';
} );
await page.keyboard.press( 'Enter' );
@@ -696,7 +696,7 @@ test.describe( 'Writing Flow', () => {
await page.keyboard.type( '2' );
await page.keyboard.press( 'ArrowUp' );
- const paragraphBlock = page
+ const paragraphBlock = editor.canvas
.locator( 'role=document[name="Paragraph block"i]' )
.first();
const paragraphRect = await paragraphBlock.boundingBox();
@@ -761,7 +761,7 @@ test.describe( 'Writing Flow', () => {
` );
- const paragraphBlock = page.locator(
+ const paragraphBlock = editor.canvas.locator(
'role=document[name="Paragraph block"i]'
);
@@ -784,7 +784,7 @@ test.describe( 'Writing Flow', () => {
await page.mouse.click( x, lowerInserterY );
await expect(
- page.locator( 'role=document[name="Block: Image"i]' )
+ editor.canvas.locator( 'role=document[name="Block: Image"i]' )
).toHaveClass( /is-selected/ );
} );
@@ -802,7 +802,7 @@ test.describe( 'Writing Flow', () => {
// Create the table.
await page.keyboard.press( 'Space' );
await expect(
- page.locator( 'role=document[name="Block: Table"i]' )
+ editor.canvas.locator( 'role=document[name="Block: Table"i]' )
).toBeVisible();
// Navigate to the second cell.
await page.keyboard.press( 'ArrowRight' );
@@ -867,7 +867,7 @@ test.describe( 'Writing Flow', () => {
await page.mouse.up();
await expect(
- page.locator( 'role=document[name="Paragraph block"i]' )
+ editor.canvas.locator( 'role=document[name="Paragraph block"i]' )
).toHaveClass( /is-selected/ );
} );
@@ -901,12 +901,13 @@ test.describe( 'Writing Flow', () => {
test( 'should move to the start of the first line on ArrowUp', async ( {
page,
+ editor,
} ) => {
await page.keyboard.press( 'Enter' );
await page.keyboard.type( 'a' );
async function getHeight() {
- return await page.evaluate(
+ return await editor.canvas.evaluate(
() => document.activeElement.offsetHeight
);
}
@@ -928,18 +929,19 @@ test.describe( 'Writing Flow', () => {
// Expect the "." to be added at the start of the paragraph
await expect(
- page.locator( 'role=document[name="Paragraph block"i]' )
+ editor.canvas.locator( 'role=document[name="Paragraph block"i]' )
).toHaveText( /^\.a+$/ );
} );
test( 'should vertically move the caret from corner to corner', async ( {
page,
+ editor,
} ) => {
await page.keyboard.press( 'Enter' );
await page.keyboard.type( 'a' );
async function getHeight() {
- return await page.evaluate(
+ return await editor.canvas.evaluate(
() => document.activeElement.offsetHeight
);
}
@@ -961,19 +963,20 @@ test.describe( 'Writing Flow', () => {
// Expect the "." to be added at the start of the paragraph
await expect(
- page.locator( 'role=document[name="Paragraph block"i]' )
+ editor.canvas.locator( 'role=document[name="Paragraph block"i]' )
).toHaveText( /^a+\.a$/ );
} );
test( 'should vertically move the caret when pressing Alt', async ( {
page,
pageUtils,
+ editor,
} ) => {
await page.keyboard.press( 'Enter' );
await page.keyboard.type( 'a' );
async function getHeight() {
- return await page.evaluate(
+ return await editor.canvas.evaluate(
() => document.activeElement.offsetHeight
);
}
@@ -995,14 +998,17 @@ test.describe( 'Writing Flow', () => {
// Expect the "." to be added at the start of the paragraph
await expect(
- page.locator( 'role=document[name="Paragraph block"i] >> nth = 0' )
+ editor.canvas.locator(
+ 'role=document[name="Paragraph block"i] >> nth = 0'
+ )
).toHaveText( /^.a+$/ );
} );
} );
class WritingFlowUtils {
- constructor( { page } ) {
+ constructor( { page, editor } ) {
this.page = page;
+ this.editor = editor;
this.getActiveBlockName = this.getActiveBlockName.bind( this );
}
@@ -1021,19 +1027,19 @@ class WritingFlowUtils {
await this.page.keyboard.press( 'Enter' );
await this.page.keyboard.type( '/columns' );
await this.page.keyboard.press( 'Enter' );
- await this.page.click(
+ await this.editor.canvas.click(
'role=button[name="Two columns; equal split"i]'
);
- await this.page.click( 'role=button[name="Add block"i]' );
+ await this.editor.canvas.click( 'role=button[name="Add block"i]' );
await this.page.click(
'role=listbox[name="Blocks"i] >> role=option[name="Paragraph"i]'
);
await this.page.keyboard.type( '1st col' ); // If this text is too long, it may wrap to a new line and cause test failure. That's why we're using "1st" instead of "First" here.
- await this.page.focus(
+ await this.editor.canvas.focus(
'role=document[name="Block: Column (2 of 2)"i]'
);
- await this.page.click( 'role=button[name="Add block"i]' );
+ await this.editor.canvas.click( 'role=button[name="Add block"i]' );
await this.page.click(
'role=listbox[name="Blocks"i] >> role=option[name="Paragraph"i]'
);
diff --git a/test/e2e/specs/site-editor/command-center.spec.js b/test/e2e/specs/site-editor/command-center.spec.js
index 9661a91a6abc78..57b07faf4d9b48 100644
--- a/test/e2e/specs/site-editor/command-center.spec.js
+++ b/test/e2e/specs/site-editor/command-center.spec.js
@@ -28,6 +28,9 @@ test.describe( 'Site editor command center', () => {
await page.getByRole( 'option', { name: 'Add new page' } ).click();
await page.waitForSelector( 'iframe[name="editor-canvas"]' );
const frame = page.frame( 'editor-canvas' );
+ await expect( page ).toHaveURL(
+ '/wp-admin/post-new.php?post_type=page'
+ );
await expect(
frame.getByRole( 'textbox', { name: 'Add title' } )
).toBeVisible();
diff --git a/test/e2e/specs/site-editor/pages.spec.js b/test/e2e/specs/site-editor/pages.spec.js
new file mode 100644
index 00000000000000..ba4f1986050300
--- /dev/null
+++ b/test/e2e/specs/site-editor/pages.spec.js
@@ -0,0 +1,30 @@
+/**
+ * WordPress dependencies
+ */
+const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+
+test.describe( 'Pages', () => {
+ test.beforeAll( async ( { requestUtils } ) => {
+ await requestUtils.activateTheme( 'emptytheme' );
+ } );
+ test( 'Create a new page', async ( { admin, page } ) => {
+ const pageName = 'demo';
+ await admin.visitSiteEditor();
+ await page.getByRole( 'button', { name: 'Pages' } ).click();
+ await page.getByRole( 'button', { name: 'Draft a new page' } ).click();
+ // Fill the page title and submit.
+ const newPageDialog = page.locator(
+ 'role=dialog[name="Draft a new page"i]'
+ );
+ const pageTitleInput = newPageDialog.locator(
+ 'role=textbox[name="Page title"i]'
+ );
+ await pageTitleInput.fill( pageName );
+ await page.keyboard.press( 'Enter' );
+ await expect(
+ page.locator(
+ `role=button[name="Dismiss this notice"i] >> text="${ pageName }" successfully created.`
+ )
+ ).toBeVisible();
+ } );
+} );
diff --git a/test/e2e/specs/widgets/customizing-widgets.spec.js b/test/e2e/specs/widgets/customizing-widgets.spec.js
index 110ce28948f228..8b2a585dc4e66d 100644
--- a/test/e2e/specs/widgets/customizing-widgets.spec.js
+++ b/test/e2e/specs/widgets/customizing-widgets.spec.js
@@ -36,7 +36,11 @@ test.describe( 'Widgets Customizer', () => {
await requestUtils.activateTheme( 'twentytwentyone' );
} );
- test( 'should add blocks', async ( { page, widgetsCustomizerPage } ) => {
+ test( 'should add blocks', async ( {
+ page,
+ widgetsCustomizerPage,
+ editor,
+ } ) => {
const previewFrame = widgetsCustomizerPage.previewFrame;
await widgetsCustomizerPage.visitCustomizerPage();
@@ -82,7 +86,7 @@ test.describe( 'Widgets Customizer', () => {
await page.click( 'role=option[name="Search"i]' );
- await page.focus(
+ await editor.canvas.focus(
'role=document[name="Block: Search"i] >> role=textbox[name="Label text"i]'
);
@@ -229,6 +233,7 @@ test.describe( 'Widgets Customizer', () => {
page,
requestUtils,
widgetsCustomizerPage,
+ editor,
} ) => {
await requestUtils.addWidgetBlock(
`\nFirst Paragraph
\n`,
@@ -277,7 +282,7 @@ test.describe( 'Widgets Customizer', () => {
await headingWidget.click(); // noop click on the widget text to unfocus the editor and hide toolbar
await editHeadingWidget.click();
- const headingBlock = page.locator(
+ const headingBlock = editor.canvas.locator(
'role=document[name="Block: Heading"i] >> text="First Heading"'
);
await expect( headingBlock ).toBeFocused();
@@ -583,12 +588,13 @@ test.describe( 'Widgets Customizer', () => {
test( 'preserves content in the Custom HTML block', async ( {
page,
widgetsCustomizerPage,
+ editor,
} ) => {
await widgetsCustomizerPage.visitCustomizerPage();
await widgetsCustomizerPage.expandWidgetArea( 'Footer #1' );
await widgetsCustomizerPage.addBlock( 'Custom HTML' );
- const HTMLBlockTextarea = page.locator(
+ const HTMLBlockTextarea = editor.canvas.locator(
'role=document[name="Block: Custom HTML"i] >> role=textbox[name="HTML"i]'
);
await HTMLBlockTextarea.type( 'hello' );
diff --git a/test/integration/fixtures/blocks/core__details.html b/test/integration/fixtures/blocks/core__details.html
new file mode 100644
index 00000000000000..855ea3f0a4f556
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__details.html
@@ -0,0 +1,7 @@
+
+Details Summary
+
+ Details Content
+
+
+
diff --git a/test/integration/fixtures/blocks/core__details.json b/test/integration/fixtures/blocks/core__details.json
new file mode 100644
index 00000000000000..e3873e4702db3f
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__details.json
@@ -0,0 +1,22 @@
+[
+ {
+ "name": "core/details",
+ "isValid": true,
+ "attributes": {
+ "showContent": false,
+ "summary": "Details Summary"
+ },
+ "innerBlocks": [
+ {
+ "name": "core/paragraph",
+ "isValid": true,
+ "attributes": {
+ "content": "Details Content",
+ "dropCap": false,
+ "placeholder": "Type / to add a hidden block"
+ },
+ "innerBlocks": []
+ }
+ ]
+ }
+]
diff --git a/test/integration/fixtures/blocks/core__details.parsed.json b/test/integration/fixtures/blocks/core__details.parsed.json
new file mode 100644
index 00000000000000..3240c013e8e866
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__details.parsed.json
@@ -0,0 +1,25 @@
+[
+ {
+ "blockName": "core/details",
+ "attrs": {
+ "summary": "Details Summary"
+ },
+ "innerBlocks": [
+ {
+ "blockName": "core/paragraph",
+ "attrs": {
+ "placeholder": "Type / to add a hidden block"
+ },
+ "innerBlocks": [],
+ "innerHTML": "\n\tDetails Content
\n\t",
+ "innerContent": [ "\n\tDetails Content
\n\t" ]
+ }
+ ],
+ "innerHTML": "\nDetails Summary \n\t\n \n",
+ "innerContent": [
+ "\nDetails Summary \n\t",
+ null,
+ "\n \n"
+ ]
+ }
+]
diff --git a/test/integration/fixtures/blocks/core__details.serialized.html b/test/integration/fixtures/blocks/core__details.serialized.html
new file mode 100644
index 00000000000000..d5d169983bbf38
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__details.serialized.html
@@ -0,0 +1,5 @@
+
+Details Summary
+Details Content
+
+
diff --git a/test/integration/fixtures/blocks/core__query-pagination.json b/test/integration/fixtures/blocks/core__query-pagination.json
index 66b604f14b8a17..49d6992bac3a53 100644
--- a/test/integration/fixtures/blocks/core__query-pagination.json
+++ b/test/integration/fixtures/blocks/core__query-pagination.json
@@ -3,7 +3,8 @@
"name": "core/query-pagination",
"isValid": true,
"attributes": {
- "paginationArrow": "none"
+ "paginationArrow": "none",
+ "showLabel": true
},
"innerBlocks": []
}
diff --git a/test/integration/fixtures/blocks/core__query.json b/test/integration/fixtures/blocks/core__query.json
index 4c7ce920a04506..fb545c16ea6953 100644
--- a/test/integration/fixtures/blocks/core__query.json
+++ b/test/integration/fixtures/blocks/core__query.json
@@ -18,10 +18,7 @@
"taxQuery": null,
"parents": []
},
- "tagName": "div",
- "displayLayout": {
- "type": "list"
- }
+ "tagName": "div"
},
"innerBlocks": []
}
diff --git a/test/integration/fixtures/blocks/core__query.serialized.html b/test/integration/fixtures/blocks/core__query.serialized.html
index 049ea7dd2bb73d..3bc4085f4f090d 100644
--- a/test/integration/fixtures/blocks/core__query.serialized.html
+++ b/test/integration/fixtures/blocks/core__query.serialized.html
@@ -1,3 +1,3 @@
-
+
diff --git a/test/integration/fixtures/blocks/core__query__deprecated-1.serialized.html b/test/integration/fixtures/blocks/core__query__deprecated-1.serialized.html
index 39f889cfae97e1..915726d992a8f9 100644
--- a/test/integration/fixtures/blocks/core__query__deprecated-1.serialized.html
+++ b/test/integration/fixtures/blocks/core__query__deprecated-1.serialized.html
@@ -1,3 +1,3 @@
-
+
diff --git a/test/integration/fixtures/blocks/core__query__deprecated-2-with-colors.json b/test/integration/fixtures/blocks/core__query__deprecated-2-with-colors.json
index 82bc41a40fb1b5..8a048667f55afd 100644
--- a/test/integration/fixtures/blocks/core__query__deprecated-2-with-colors.json
+++ b/test/integration/fixtures/blocks/core__query__deprecated-2-with-colors.json
@@ -21,10 +21,7 @@
"post_tag": [ 6 ]
}
},
- "tagName": "div",
- "displayLayout": {
- "type": "list"
- }
+ "tagName": "div"
},
"innerBlocks": [
{
@@ -50,7 +47,11 @@
{
"name": "core/post-template",
"isValid": true,
- "attributes": {},
+ "attributes": {
+ "layout": {
+ "type": "default"
+ }
+ },
"innerBlocks": [
{
"name": "core/post-title",
diff --git a/test/integration/fixtures/blocks/core__query__deprecated-2-with-colors.serialized.html b/test/integration/fixtures/blocks/core__query__deprecated-2-with-colors.serialized.html
index f86b4f26ecc1d1..b9e6b50deb0677 100644
--- a/test/integration/fixtures/blocks/core__query__deprecated-2-with-colors.serialized.html
+++ b/test/integration/fixtures/blocks/core__query__deprecated-2-with-colors.serialized.html
@@ -1,6 +1,6 @@
-
+
-
diff --git a/test/integration/fixtures/blocks/core__query__deprecated-2.json b/test/integration/fixtures/blocks/core__query__deprecated-2.json
index a63ad1c007b6b1..b0a1aea41ea506 100644
--- a/test/integration/fixtures/blocks/core__query__deprecated-2.json
+++ b/test/integration/fixtures/blocks/core__query__deprecated-2.json
@@ -21,16 +21,17 @@
"post_tag": [ 6 ]
}
},
- "tagName": "div",
- "displayLayout": {
- "type": "list"
- }
+ "tagName": "div"
},
"innerBlocks": [
{
"name": "core/post-template",
"isValid": true,
- "attributes": {},
+ "attributes": {
+ "layout": {
+ "type": "default"
+ }
+ },
"innerBlocks": [
{
"name": "core/post-title",
diff --git a/test/integration/fixtures/blocks/core__query__deprecated-2.serialized.html b/test/integration/fixtures/blocks/core__query__deprecated-2.serialized.html
index 5804c54e577f14..2016bea9635928 100644
--- a/test/integration/fixtures/blocks/core__query__deprecated-2.serialized.html
+++ b/test/integration/fixtures/blocks/core__query__deprecated-2.serialized.html
@@ -1,5 +1,5 @@
-
-
+
+
diff --git a/test/integration/fixtures/blocks/core__query__deprecated-3.json b/test/integration/fixtures/blocks/core__query__deprecated-3.json
index 2c682de49bdda6..bb9a9de34a4b78 100644
--- a/test/integration/fixtures/blocks/core__query__deprecated-3.json
+++ b/test/integration/fixtures/blocks/core__query__deprecated-3.json
@@ -18,10 +18,6 @@
"inherit": false
},
"tagName": "div",
- "displayLayout": {
- "type": "flex",
- "columns": 3
- },
"align": "wide"
},
"innerBlocks": [
@@ -49,7 +45,11 @@
"name": "core/post-template",
"isValid": true,
"attributes": {
- "fontSize": "large"
+ "fontSize": "large",
+ "layout": {
+ "type": "grid",
+ "columnCount": 3
+ }
},
"innerBlocks": [
{
@@ -87,7 +87,8 @@
"name": "core/query-pagination",
"isValid": true,
"attributes": {
- "paginationArrow": "none"
+ "paginationArrow": "none",
+ "showLabel": true
},
"innerBlocks": [
{
diff --git a/test/integration/fixtures/blocks/core__query__deprecated-3.serialized.html b/test/integration/fixtures/blocks/core__query__deprecated-3.serialized.html
index edbf5b1a0557b3..86c87dde71c3bd 100644
--- a/test/integration/fixtures/blocks/core__query__deprecated-3.serialized.html
+++ b/test/integration/fixtures/blocks/core__query__deprecated-3.serialized.html
@@ -1,6 +1,6 @@
-
+
-
+
diff --git a/test/integration/fixtures/blocks/core__query__deprecated-4.html b/test/integration/fixtures/blocks/core__query__deprecated-4.html
index 20aec9638688e5..9a2b39db018f56 100644
--- a/test/integration/fixtures/blocks/core__query__deprecated-4.html
+++ b/test/integration/fixtures/blocks/core__query__deprecated-4.html
@@ -1,4 +1,4 @@
-
+
diff --git a/test/integration/fixtures/blocks/core__query__deprecated-4.json b/test/integration/fixtures/blocks/core__query__deprecated-4.json
index 2870009875a018..620e1ecefef130 100644
--- a/test/integration/fixtures/blocks/core__query__deprecated-4.json
+++ b/test/integration/fixtures/blocks/core__query__deprecated-4.json
@@ -1,7 +1,7 @@
[
{
"name": "core/query",
- "isValid": false,
+ "isValid": true,
"attributes": {
"queryId": 0,
"query": {
@@ -17,19 +17,21 @@
"sticky": "",
"inherit": true
},
- "tagName": "main",
- "displayLayout": {
- "type": "list"
- },
+ "tagName": "div",
"layout": {
- "inherit": true
+ "contentSize": null,
+ "type": "constrained"
}
},
"innerBlocks": [
{
"name": "core/post-template",
"isValid": true,
- "attributes": {},
+ "attributes": {
+ "layout": {
+ "type": "default"
+ }
+ },
"innerBlocks": [
{
"name": "core/post-title",
diff --git a/test/integration/fixtures/blocks/core__query__deprecated-4.parsed.json b/test/integration/fixtures/blocks/core__query__deprecated-4.parsed.json
index 529a440b12eeb2..1ce908fa81ac70 100644
--- a/test/integration/fixtures/blocks/core__query__deprecated-4.parsed.json
+++ b/test/integration/fixtures/blocks/core__query__deprecated-4.parsed.json
@@ -16,7 +16,6 @@
"sticky": "",
"inherit": true
},
- "tagName": "main",
"displayLayout": {
"type": "list"
},
diff --git a/test/integration/fixtures/blocks/core__query__deprecated-4.serialized.html b/test/integration/fixtures/blocks/core__query__deprecated-4.serialized.html
index f86c70b104550d..1ab2470d36159a 100644
--- a/test/integration/fixtures/blocks/core__query__deprecated-4.serialized.html
+++ b/test/integration/fixtures/blocks/core__query__deprecated-4.serialized.html
@@ -1,7 +1,5 @@
-
-
diff --git a/test/integration/fixtures/blocks/core__query__deprecated-5.html b/test/integration/fixtures/blocks/core__query__deprecated-5.html
new file mode 100644
index 00000000000000..d040961172cb98
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__query__deprecated-5.html
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/integration/fixtures/blocks/core__query__deprecated-5.json b/test/integration/fixtures/blocks/core__query__deprecated-5.json
new file mode 100644
index 00000000000000..f74e9496b23902
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__query__deprecated-5.json
@@ -0,0 +1,50 @@
+[
+ {
+ "name": "core/query",
+ "isValid": true,
+ "attributes": {
+ "queryId": 0,
+ "query": {
+ "perPage": 10,
+ "pages": 0,
+ "offset": 0,
+ "postType": "post",
+ "order": "desc",
+ "orderBy": "date",
+ "author": "",
+ "search": "",
+ "exclude": [],
+ "sticky": "",
+ "inherit": true
+ },
+ "tagName": "div",
+ "layout": {
+ "type": "constrained"
+ }
+ },
+ "innerBlocks": [
+ {
+ "name": "core/post-template",
+ "isValid": true,
+ "attributes": {
+ "layout": {
+ "type": "default"
+ }
+ },
+ "innerBlocks": [
+ {
+ "name": "core/post-title",
+ "isValid": true,
+ "attributes": {
+ "level": 2,
+ "isLink": false,
+ "rel": "",
+ "linkTarget": "_self"
+ },
+ "innerBlocks": []
+ }
+ ]
+ }
+ ]
+ }
+]
diff --git a/test/integration/fixtures/blocks/core__query__deprecated-5.parsed.json b/test/integration/fixtures/blocks/core__query__deprecated-5.parsed.json
new file mode 100644
index 00000000000000..54a9d08581cb90
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__query__deprecated-5.parsed.json
@@ -0,0 +1,50 @@
+[
+ {
+ "blockName": "core/query",
+ "attrs": {
+ "queryId": 0,
+ "query": {
+ "perPage": 10,
+ "pages": 0,
+ "offset": 0,
+ "postType": "post",
+ "order": "desc",
+ "orderBy": "date",
+ "author": "",
+ "search": "",
+ "exclude": [],
+ "sticky": "",
+ "inherit": true
+ },
+ "displayLayout": {
+ "type": "list"
+ },
+ "layout": {
+ "type": "constrained"
+ }
+ },
+ "innerBlocks": [
+ {
+ "blockName": "core/post-template",
+ "attrs": {},
+ "innerBlocks": [
+ {
+ "blockName": "core/post-title",
+ "attrs": {},
+ "innerBlocks": [],
+ "innerHTML": "",
+ "innerContent": []
+ }
+ ],
+ "innerHTML": "\n \n ",
+ "innerContent": [ "\n ", null, "\n " ]
+ }
+ ],
+ "innerHTML": "\n
\n
\n",
+ "innerContent": [
+ "\n
",
+ null,
+ "\n
\n"
+ ]
+ }
+]
diff --git a/test/integration/fixtures/blocks/core__query__deprecated-5.serialized.html b/test/integration/fixtures/blocks/core__query__deprecated-5.serialized.html
new file mode 100644
index 00000000000000..a185cf3285299b
--- /dev/null
+++ b/test/integration/fixtures/blocks/core__query__deprecated-5.serialized.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/test/integration/fixtures/blocks/core__search.json b/test/integration/fixtures/blocks/core__search.json
index 7b6125b5016b37..f692eac10993d8 100644
--- a/test/integration/fixtures/blocks/core__search.json
+++ b/test/integration/fixtures/blocks/core__search.json
@@ -7,7 +7,9 @@
"placeholder": "",
"buttonPosition": "button-outside",
"buttonUseIcon": false,
- "query": {}
+ "query": {},
+ "buttonBehavior": "expand-searchfield",
+ "isSearchFieldHidden": false
},
"innerBlocks": []
}
diff --git a/test/integration/fixtures/blocks/core__search__custom-text.json b/test/integration/fixtures/blocks/core__search__custom-text.json
index 6e874946117966..c763cb60f65e86 100644
--- a/test/integration/fixtures/blocks/core__search__custom-text.json
+++ b/test/integration/fixtures/blocks/core__search__custom-text.json
@@ -9,7 +9,9 @@
"buttonText": "Custom button text",
"buttonPosition": "button-outside",
"buttonUseIcon": false,
- "query": {}
+ "query": {},
+ "buttonBehavior": "expand-searchfield",
+ "isSearchFieldHidden": false
},
"innerBlocks": []
}
diff --git a/test/native/__mocks__/react-native-hr/index.js b/test/native/__mocks__/react-native-hr/index.js
deleted file mode 100644
index e69de29bb2d1d6..00000000000000
diff --git a/test/native/setup.js b/test/native/setup.js
index be643a1fb90175..817eb93e0c4396 100644
--- a/test/native/setup.js
+++ b/test/native/setup.js
@@ -2,7 +2,7 @@
* External dependencies
*/
import 'react-native-gesture-handler/jestSetup';
-import { Image, NativeModules as RNNativeModules } from 'react-native';
+import { Image } from 'react-native';
// React Native sets up a global navigator, but that is not executed in the
// testing environment: https://github.com/facebook/react-native/blob/6c19dc3266b84f47a076b647a1c93b3c3b69d2c5/Libraries/Core/setUpNavigator.js#L17
@@ -19,24 +19,37 @@ global.ReanimatedDataMock = {
now: () => 0,
};
-RNNativeModules.UIManager = RNNativeModules.UIManager || {};
-RNNativeModules.UIManager.RCTView = RNNativeModules.UIManager.RCTView || {};
-RNNativeModules.RNGestureHandlerModule =
- RNNativeModules.RNGestureHandlerModule || {
- State: {
- BEGAN: 'BEGAN',
- FAILED: 'FAILED',
- ACTIVE: 'ACTIVE',
- END: 'END',
- },
- attachGestureHandler: jest.fn(),
- createGestureHandler: jest.fn(),
- dropGestureHandler: jest.fn(),
- updateGestureHandler: jest.fn(),
+jest.mock( 'react-native', () => {
+ const ReactNative = jest.requireActual( 'react-native' );
+ const RNNativeModules = ReactNative.NativeModules;
+
+ // Mock React Native modules
+ RNNativeModules.UIManager = RNNativeModules.UIManager || {};
+ RNNativeModules.UIManager.RCTView = RNNativeModules.UIManager.RCTView || {};
+ RNNativeModules.RNGestureHandlerModule =
+ RNNativeModules.RNGestureHandlerModule || {
+ State: {
+ BEGAN: 'BEGAN',
+ FAILED: 'FAILED',
+ ACTIVE: 'ACTIVE',
+ END: 'END',
+ },
+ attachGestureHandler: jest.fn(),
+ createGestureHandler: jest.fn(),
+ dropGestureHandler: jest.fn(),
+ updateGestureHandler: jest.fn(),
+ };
+ RNNativeModules.PlatformConstants = RNNativeModules.PlatformConstants || {
+ forceTouchAvailable: false,
};
-RNNativeModules.PlatformConstants = RNNativeModules.PlatformConstants || {
- forceTouchAvailable: false,
-};
+
+ // Mock WebView native module from `react-native-webview`
+ RNNativeModules.RNCWebView = {
+ isFileUploadSupported: jest.fn(),
+ };
+
+ return ReactNative;
+} );
// Mock component to render with props rather than merely a string name so that
// we may assert against it. ...args is used avoid warnings about ignoring
@@ -120,8 +133,6 @@ jest.mock(
props.isVisible ? mockComponent( 'Modal' )( props ) : null
);
-jest.mock( 'react-native-hr', () => () => 'Hr' );
-
jest.mock( 'react-native-svg', () => {
const { forwardRef } = require( 'react' );
return {
diff --git a/test/storybook-playwright/specs/button.spec.ts b/test/storybook-playwright/specs/button.spec.ts
new file mode 100644
index 00000000000000..66049d8c63f4cc
--- /dev/null
+++ b/test/storybook-playwright/specs/button.spec.ts
@@ -0,0 +1,57 @@
+/**
+ * External dependencies
+ */
+import { expect, test } from '@playwright/test';
+
+/**
+ * Internal dependencies
+ */
+import {
+ gotoStoryId,
+ getAllPropsPermutations,
+ testSnapshotForPropsConfig,
+} from '../utils';
+
+test.describe( 'Button', () => {
+ test.describe( 'variant states', () => {
+ test.beforeEach( async ( { page } ) => {
+ gotoStoryId( page, 'components-button--variant-states', {
+ decorators: { customE2EControls: 'show' },
+ } );
+ } );
+
+ getAllPropsPermutations( [
+ {
+ propName: '__next40pxDefaultSize',
+ valuesToTest: [ true, false ],
+ },
+ ] ).forEach( ( propsConfig ) => {
+ test( `should render with ${ JSON.stringify(
+ propsConfig
+ ) }`, async ( { page } ) => {
+ await testSnapshotForPropsConfig( page, propsConfig );
+ } );
+ } );
+ } );
+
+ test.describe( 'icon', () => {
+ test.beforeEach( async ( { page } ) => {
+ gotoStoryId( page, 'components-button--icon', {
+ decorators: { customE2EControls: 'show' },
+ } );
+ } );
+
+ getAllPropsPermutations( [
+ {
+ propName: '__next40pxDefaultSize',
+ valuesToTest: [ true, false ],
+ },
+ ] ).forEach( ( propsConfig ) => {
+ test( `should render with ${ JSON.stringify(
+ propsConfig
+ ) }`, async ( { page } ) => {
+ await testSnapshotForPropsConfig( page, propsConfig );
+ } );
+ } );
+ } );
+} );
diff --git a/test/storybook-playwright/utils.ts b/test/storybook-playwright/utils.ts
index d3f37aa7df26bc..1f0fe7235abeed 100644
--- a/test/storybook-playwright/utils.ts
+++ b/test/storybook-playwright/utils.ts
@@ -62,9 +62,10 @@ export const getAllPropsPermutations = (
// Test all values for the given prop.
for ( const value of propObject.valuesToTest ) {
+ const valueAsString = value === undefined ? 'undefined' : value;
const newAccProps = {
...accProps,
- [ propObject.propName ]: value,
+ [ propObject.propName ]: valueAsString,
};
if ( restProps.length === 0 ) {
@@ -99,5 +100,7 @@ export const testSnapshotForPropsConfig = async (
await submitButton.click();
- expect( await page.screenshot() ).toMatchSnapshot();
+ expect(
+ await page.screenshot( { animations: 'disabled' } )
+ ).toMatchSnapshot();
};