diff --git a/docs/README.md b/docs/README.md
index ba6b35a761f6e0..94f640ae46bfcf 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,6 +1,6 @@
# Block Editor Handbook
-👋 Welcome to the Block Editor Handbook.
+Welcome to the Block Editor Handbook.
The [**Block Editor**](https://wordpress.org/gutenberg/) is a modern and up-to-date paradigm for WordPress site building and publishing. It uses a modular system of **Blocks** to compose and format content and is designed to create rich and flexible layouts for websites and digital products.
diff --git a/docs/contributors/code/back-merging-to-wp-core.md b/docs/contributors/code/back-merging-to-wp-core.md
new file mode 100644
index 00000000000000..2b1ec77df1e550
--- /dev/null
+++ b/docs/contributors/code/back-merging-to-wp-core.md
@@ -0,0 +1,31 @@
+# Back-merging code to WordPress Core
+
+For major releases of the WordPress software, Gutenberg features need to be merged into WordPress Core. Typically this involves taking changes made in `.php` files within the Gutenberg repository and making the equivalent updates in the WP Core codebase.
+
+## Files/Directories
+
+Changes to files within the following files/directories will typically require back-merging to WP Core:
+
+- `lib/`
+- `phpunit/`
+
+## Ignored directories/files
+
+The following directories/files do _not_ require back-merging to WP Core:
+
+- `lib/load.php` - Plugin specific code.
+- `lib/experiments-page.php` - experiments are Plugin specific.
+- `packages/block-library` - this is handled automatically during the packages sync process.
+- `packages/e2e-tests/plugins` - PHP files related to e2e tests only. Mostly fixture data generators.
+- `phpunit/blocks` - the code is maintained in Gutenberg so the test should be as well.
+
+Please note this list is not exhaustive.
+
+## Pull Request Criteria
+
+In general, all PHP code committed to the Gutenberg repository since the date of the final Gutenberg release that was included in [the _last_ stable WP Core release](https://developer.wordpress.org/block-editor/contributors/versions-in-wordpress/) should be considered for back merging to WP Core.
+
+There are however certain exceptions to that rule. PRs with the following criteria do _not_ require back-merging to WP Core:
+
+- Does not contain changes to PHP code.
+- Has label `Backport from WordPress Core` - this code is already in WP Core.
diff --git a/package-lock.json b/package-lock.json
index d8b4d1038d8a48..56374e1eeb89a3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -55598,6 +55598,7 @@
"@wordpress/components": "file:../components",
"@wordpress/compose": "file:../compose",
"@wordpress/data": "file:../data",
+ "@wordpress/deprecated": "file:../deprecated",
"@wordpress/element": "file:../element",
"@wordpress/i18n": "file:../i18n",
"@wordpress/icons": "file:../icons",
@@ -70290,6 +70291,7 @@
"@wordpress/components": "file:../components",
"@wordpress/compose": "file:../compose",
"@wordpress/data": "file:../data",
+ "@wordpress/deprecated": "file:../deprecated",
"@wordpress/element": "file:../element",
"@wordpress/i18n": "file:../i18n",
"@wordpress/icons": "file:../icons",
diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js
index af96f0e3483d1c..7690386764f6cc 100644
--- a/packages/block-editor/src/components/block-list/block.js
+++ b/packages/block-editor/src/components/block-list/block.js
@@ -6,7 +6,7 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
-import { useCallback, RawHTML, useContext } from '@wordpress/element';
+import { memo, useCallback, RawHTML, useContext } from '@wordpress/element';
import {
getBlockType,
getSaveContent,
@@ -21,7 +21,7 @@ import {
} from '@wordpress/blocks';
import { withFilters } from '@wordpress/components';
import { withDispatch, useDispatch, useSelect } from '@wordpress/data';
-import { compose, pure } from '@wordpress/compose';
+import { compose } from '@wordpress/compose';
import { safeHTML } from '@wordpress/dom';
/**
@@ -739,4 +739,4 @@ function BlockListBlockProvider( props ) {
);
}
-export default pure( BlockListBlockProvider );
+export default memo( BlockListBlockProvider );
diff --git a/packages/block-editor/src/components/block-list/block.native.js b/packages/block-editor/src/components/block-list/block.native.js
index c4f86cfe25a428..1c0de681bd4c29 100644
--- a/packages/block-editor/src/components/block-list/block.native.js
+++ b/packages/block-editor/src/components/block-list/block.native.js
@@ -7,7 +7,13 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
-import { useCallback, useMemo, useState, useRef } from '@wordpress/element';
+import {
+ useCallback,
+ useMemo,
+ useState,
+ useRef,
+ memo,
+} from '@wordpress/element';
import {
GlobalStylesContext,
getMergedGlobalStyles,
@@ -29,7 +35,7 @@ import {
withDispatch,
withSelect,
} from '@wordpress/data';
-import { compose, ifCondition, pure } from '@wordpress/compose';
+import { compose, ifCondition } from '@wordpress/compose';
/**
* Internal dependencies
@@ -682,7 +688,7 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => {
} );
export default compose(
- pure,
+ memo,
applyWithSelect,
applyWithDispatch,
// Block is sometimes not mounted at the right time, causing it be undefined
diff --git a/packages/block-editor/src/components/block-preview/auto.js b/packages/block-editor/src/components/block-preview/auto.js
index 8972370cac6897..b4fa5e27b072ef 100644
--- a/packages/block-editor/src/components/block-preview/auto.js
+++ b/packages/block-editor/src/components/block-preview/auto.js
@@ -1,9 +1,9 @@
/**
* WordPress dependencies
*/
-import { useResizeObserver, pure, useRefEffect } from '@wordpress/compose';
+import { useResizeObserver, useRefEffect } from '@wordpress/compose';
import { useSelect } from '@wordpress/data';
-import { useMemo } from '@wordpress/element';
+import { memo, useMemo } from '@wordpress/element';
import { Disabled } from '@wordpress/components';
/**
@@ -55,7 +55,7 @@ function ScaledBlockPreview( {
}, [ styles, additionalStyles ] );
// Initialize on render instead of module top level, to avoid circular dependency issues.
- MemoizedBlockList = MemoizedBlockList || pure( BlockList );
+ MemoizedBlockList = MemoizedBlockList || memo( BlockList );
const scale = containerWidth / viewportWidth;
const aspectRatio = contentHeight
diff --git a/packages/block-editor/src/components/provider/index.js b/packages/block-editor/src/components/provider/index.js
index c3a87dfb5ff004..b140a2af8c095e 100644
--- a/packages/block-editor/src/components/provider/index.js
+++ b/packages/block-editor/src/components/provider/index.js
@@ -46,7 +46,9 @@ export const ExperimentalBlockEditorProvider = withRegistryProvider(
return (
-
+ { ! settings.__unstableIsPreviewMode && (
+
+ ) }
{ children }
);
diff --git a/packages/block-editor/src/hooks/typography.native.js b/packages/block-editor/src/hooks/typography.native.js
index d8cbf71d84e13f..f0e9c9c10913d7 100644
--- a/packages/block-editor/src/hooks/typography.native.js
+++ b/packages/block-editor/src/hooks/typography.native.js
@@ -2,7 +2,7 @@
* WordPress dependencies
*/
import { useSelect } from '@wordpress/data';
-import { pure } from '@wordpress/compose';
+import { memo } from '@wordpress/element';
import { PanelBody } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
@@ -57,4 +57,4 @@ function TypographyPanelPure( { clientId, setAttributes, settings } ) {
// We don't want block controls to re-render when typing inside a block. `pure`
// will prevent re-renders unless props change, so only pass the needed props
// and not the whole attributes object.
-export const TypographyPanel = pure( TypographyPanelPure );
+export const TypographyPanel = memo( TypographyPanelPure );
diff --git a/packages/block-editor/src/hooks/utils.js b/packages/block-editor/src/hooks/utils.js
index cd660c85826c28..e63029e4e34e81 100644
--- a/packages/block-editor/src/hooks/utils.js
+++ b/packages/block-editor/src/hooks/utils.js
@@ -2,9 +2,9 @@
* WordPress dependencies
*/
import { getBlockSupport } from '@wordpress/blocks';
-import { useMemo, useEffect, useId, useState } from '@wordpress/element';
+import { memo, useMemo, useEffect, useId, useState } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
-import { createHigherOrderComponent, pure } from '@wordpress/compose';
+import { createHigherOrderComponent } from '@wordpress/compose';
import { addFilter } from '@wordpress/hooks';
/**
@@ -402,10 +402,10 @@ export function useBlockSettings( name, parentLayout ) {
export function createBlockEditFilter( features ) {
// We don't want block controls to re-render when typing inside a block.
- // `pure` will prevent re-renders unless props change, so only pass the
+ // `memo` will prevent re-renders unless props change, so only pass the
// needed props and not the whole attributes object.
features = features.map( ( settings ) => {
- return { ...settings, Edit: pure( settings.edit ) };
+ return { ...settings, Edit: memo( settings.edit ) };
} );
const withBlockEditHooks = createHigherOrderComponent(
( OriginalBlockEdit ) => ( props ) => {
@@ -488,7 +488,7 @@ function BlockProps( { index, useBlockProps, setAllWrapperProps, ...props } ) {
return null;
}
-const BlockPropsPure = pure( BlockProps );
+const BlockPropsPure = memo( BlockProps );
export function createBlockListBlockFilter( features ) {
const withBlockListBlockHooks = createHigherOrderComponent(
diff --git a/packages/block-library/src/embed/edit.js b/packages/block-library/src/embed/edit.js
index 2945fb0fbe888b..0b166ebd1e9c72 100644
--- a/packages/block-library/src/embed/edit.js
+++ b/packages/block-library/src/embed/edit.js
@@ -127,16 +127,17 @@ const EmbedEdit = ( props ) => {
};
useEffect( () => {
- if ( ! preview?.html || ! cannotEmbed || fetching ) {
+ if ( preview?.html || ! cannotEmbed || fetching ) {
return;
}
+
// At this stage, we're not fetching the preview and know it can't be embedded,
// so try removing any trailing slash, and resubmit.
const newURL = attributesUrl.replace( /\/$/, '' );
setURL( newURL );
setIsEditingURL( false );
setAttributes( { url: newURL } );
- }, [ preview?.html, attributesUrl, cannotEmbed, fetching ] );
+ }, [ preview?.html, attributesUrl, cannotEmbed, fetching, setAttributes ] );
// Try a different provider in case the embed url is not supported.
useEffect( () => {
diff --git a/packages/block-library/src/footnotes/edit.js b/packages/block-library/src/footnotes/edit.js
index 111e0ba5d3a0ee..88254657ced3c4 100644
--- a/packages/block-library/src/footnotes/edit.js
+++ b/packages/block-library/src/footnotes/edit.js
@@ -14,10 +14,11 @@ export default function FootnotesEdit( { context: { postType, postId } } ) {
'meta',
postId
);
+ const footnotesSupported = 'string' === typeof meta?.footnotes;
const footnotes = meta?.footnotes ? JSON.parse( meta.footnotes ) : [];
const blockProps = useBlockProps();
- if ( postType !== 'post' && postType !== 'page' ) {
+ if ( ! footnotesSupported ) {
return (
0;
}, [] );
+ const [ meta ] = useEntityProp( 'postType', postType, 'meta', postId );
+ const footnotesSupported = 'string' === typeof meta?.footnotes;
+
const { selectionChange, insertBlock } =
useDispatch( blockEditorStore );
@@ -81,7 +85,7 @@ export const format = {
return null;
}
- if ( postType !== 'post' && postType !== 'page' ) {
+ if ( ! footnotesSupported ) {
return null;
}
diff --git a/packages/block-library/src/footnotes/index.php b/packages/block-library/src/footnotes/index.php
index bc6291dd21c38b..0cd2ad73ef3d42 100644
--- a/packages/block-library/src/footnotes/index.php
+++ b/packages/block-library/src/footnotes/index.php
@@ -68,17 +68,26 @@ function render_block_core_footnotes( $attributes, $content, $block ) {
* @since 6.3.0
*/
function register_block_core_footnotes() {
- foreach ( array( 'post', 'page' ) as $post_type ) {
- register_post_meta(
- $post_type,
- 'footnotes',
- array(
- 'show_in_rest' => true,
- 'single' => true,
- 'type' => 'string',
- 'revisions_enabled' => true,
- )
- );
+ $post_types = get_post_types(
+ array(
+ 'show_in_rest' => true,
+ 'public' => true,
+ )
+ );
+ foreach ( $post_types as $post_type ) {
+ // Only register the meta field if the post type supports the editor, custom fields, and revisions.
+ if ( post_type_supports( $post_type, 'editor' ) && post_type_supports( $post_type, 'custom-fields' ) && post_type_supports( $post_type, 'revisions' ) ) {
+ register_post_meta(
+ $post_type,
+ 'footnotes',
+ array(
+ 'show_in_rest' => true,
+ 'single' => true,
+ 'type' => 'string',
+ 'revisions_enabled' => true,
+ )
+ );
+ }
}
register_block_type_from_metadata(
__DIR__ . '/footnotes',
diff --git a/packages/block-library/src/video/edit.native.js b/packages/block-library/src/video/edit.native.js
index b974e61e109605..e37db3feb63d3b 100644
--- a/packages/block-library/src/video/edit.native.js
+++ b/packages/block-library/src/video/edit.native.js
@@ -212,7 +212,7 @@ class VideoEdit extends Component {
render() {
const { setAttributes, attributes, isSelected, wasBlockJustInserted } =
this.props;
- const { id, src } = attributes;
+ const { id, src, guid } = attributes;
const { videoContainerHeight } = this.state;
const toolbarEditButton = (
@@ -236,7 +236,10 @@ class VideoEdit extends Component {
>
);
- if ( ! src ) {
+ // NOTE: `guid` is not part of the block's attribute definition. This case
+ // handled here is a temporary fix until a we find a better approach.
+ const isSourcePresent = src || ( guid && id );
+ if ( ! isSourcePresent ) {
return (
{
expect( screen.getByText( 'Invalid URL.' ) ).toBeVisible();
} );
+
+ it( 'should render empty state when source is not present', async () => {
+ await initializeEditor( {
+ initialHtml: `
+
+
+
+ `,
+ } );
+ const addVideoButton = screen.queryByText( 'Add video' );
+ expect( addVideoButton ).toBeVisible();
+ } );
+
+ it( 'should not render empty state when video source is present', async () => {
+ await initializeEditor( {
+ initialHtml: `
+
+
+
+ `,
+ } );
+ const addVideoButton = screen.queryByText( 'Add video' );
+ expect( addVideoButton ).toBeNull();
+ } );
+
+ it( `should not render empty state when 'guid' and 'id' attributes are present`, async () => {
+ await initializeEditor( {
+ initialHtml: `
+
+
+
+ `,
+ } );
+ const addVideoButton = screen.queryByText( 'Add video' );
+ expect( addVideoButton ).toBeNull();
+ } );
} );
diff --git a/packages/compose/CHANGELOG.md b/packages/compose/CHANGELOG.md
index 25ea99778733f6..54ff6a16252e37 100644
--- a/packages/compose/CHANGELOG.md
+++ b/packages/compose/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+### Deprecations
+
+- The `pure` HoC has been deprecated. Use `memo` or `PureComponent` instead ([#57173](https://github.com/WordPress/gutenberg/pull/57173)).
+
## 6.26.0 (2024-01-10)
## 6.25.0 (2023-12-13)
diff --git a/packages/compose/README.md b/packages/compose/README.md
index ce393f2b5fd18c..7eb70a7300f07f 100644
--- a/packages/compose/README.md
+++ b/packages/compose/README.md
@@ -141,6 +141,8 @@ _Related_
### pure
+> **Deprecated** Use `memo` or `PureComponent` instead.
+
Given a component returns the enhanced component augmented with a component only re-rendering when its props/state change
### throttle
diff --git a/packages/compose/src/higher-order/pure/index.tsx b/packages/compose/src/higher-order/pure/index.tsx
index 65684738b708ac..38a85a5eebe9ab 100644
--- a/packages/compose/src/higher-order/pure/index.tsx
+++ b/packages/compose/src/higher-order/pure/index.tsx
@@ -17,6 +17,8 @@ import { createHigherOrderComponent } from '../../utils/create-higher-order-comp
/**
* Given a component returns the enhanced component augmented with a component
* only re-rendering when its props/state change
+ *
+ * @deprecated Use `memo` or `PureComponent` instead.
*/
const pure = createHigherOrderComponent( function < Props extends {} >(
WrappedComponent: ComponentType< Props >
diff --git a/packages/data/src/factory.js b/packages/data/src/factory.js
index 389c4a7e2b33ab..be4ef8cf673c5e 100644
--- a/packages/data/src/factory.js
+++ b/packages/data/src/factory.js
@@ -39,15 +39,19 @@
* @return {Function} Registry selector that can be registered with a store.
*/
export function createRegistrySelector( registrySelector ) {
- let selector;
- let lastRegistry;
+ const selectorsByRegistry = new WeakMap();
// Create a selector function that is bound to the registry referenced by `selector.registry`
// and that has the same API as a regular selector. Binding it in such a way makes it
// possible to call the selector directly from another selector.
const wrappedSelector = ( ...args ) => {
- if ( ! selector || lastRegistry !== wrappedSelector.registry ) {
+ let selector = selectorsByRegistry.get( wrappedSelector.registry );
+ // We want to make sure the cache persists even when new registry
+ // instances are created. For example patterns create their own editors
+ // with their own core/block-editor stores, so we should keep track of
+ // the cache for each registry instance.
+ if ( ! selector ) {
selector = registrySelector( wrappedSelector.registry.select );
- lastRegistry = wrappedSelector.registry;
+ selectorsByRegistry.set( wrappedSelector.registry, selector );
}
return selector( ...args );
};
diff --git a/packages/data/src/redux-store/index.js b/packages/data/src/redux-store/index.js
index e8394087215428..7fdc9331a2474b 100644
--- a/packages/data/src/redux-store/index.js
+++ b/packages/data/src/redux-store/index.js
@@ -237,6 +237,11 @@ export default function createReduxStore( key, options ) {
const boundSelector = ( ...args ) => {
args = normalize( selector, args );
const state = store.__unstableOriginalGetState();
+ // Before calling the selector, switch to the correct
+ // registry.
+ if ( selector.isRegistrySelector ) {
+ selector.registry = registry;
+ }
return selector( state.root, ...args );
};
diff --git a/packages/data/src/test/registry-selectors.js b/packages/data/src/test/registry-selectors.js
index bc567474541e24..edcadef8356c6b 100644
--- a/packages/data/src/test/registry-selectors.js
+++ b/packages/data/src/test/registry-selectors.js
@@ -78,8 +78,7 @@ describe( 'createRegistrySelector', () => {
expect( registry.select( uiStore ).getElementCount() ).toBe( 1 );
} );
- // Even without createSelector, this fails in trunk.
- it.skip( 'can bind one selector to multiple registries', () => {
+ it( 'can bind one selector to multiple registries (createRegistrySelector)', () => {
const registry1 = createRegistry();
const registry2 = createRegistry();
@@ -102,6 +101,26 @@ describe( 'createRegistrySelector', () => {
expect( registry2.select( uiStore ).getElementCount() ).toBe( 1 );
} );
+ it( 'can bind one selector to multiple registries (createRegistrySelector + createSelector)', () => {
+ const registry1 = createRegistry();
+ registry1.register( elementsStore );
+ registry1.register( uiStore );
+ registry1.dispatch( elementsStore ).add( 'Carbon' );
+
+ const registry2 = createRegistry();
+ registry2.register( elementsStore );
+ registry2.register( uiStore );
+ registry2.dispatch( elementsStore ).add( 'Helium' );
+
+ // Expects the `getFilteredElements` to be bound separately and independently to the two registries
+ expect( registry1.select( uiStore ).getFilteredElements() ).toEqual( [
+ 'Carbon',
+ ] );
+ expect( registry2.select( uiStore ).getFilteredElements() ).toEqual( [
+ 'Helium',
+ ] );
+ } );
+
it( 'can bind a memoized selector to a registry', () => {
const registry = createRegistry();
registry.register( elementsStore );
diff --git a/packages/e2e-tests/plugins/interactive-blocks/with-scope/block.json b/packages/e2e-tests/plugins/interactive-blocks/with-scope/block.json
new file mode 100644
index 00000000000000..b9d6a4fc3cf7c6
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/with-scope/block.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 2,
+ "name": "test/with-scope",
+ "title": "E2E Interactivity tests - with scope",
+ "category": "text",
+ "icon": "heart",
+ "description": "",
+ "supports": {
+ "interactivity": true
+ },
+ "textdomain": "e2e-interactivity",
+ "viewScript": "with-scope-view",
+ "render": "file:./render.php"
+}
diff --git a/packages/e2e-tests/plugins/interactive-blocks/with-scope/render.php b/packages/e2e-tests/plugins/interactive-blocks/with-scope/render.php
new file mode 100644
index 00000000000000..e1a844e33246a9
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/with-scope/render.php
@@ -0,0 +1,14 @@
+
+
+
diff --git a/packages/e2e-tests/plugins/interactive-blocks/with-scope/view.js b/packages/e2e-tests/plugins/interactive-blocks/with-scope/view.js
new file mode 100644
index 00000000000000..9df421a6f50b74
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/with-scope/view.js
@@ -0,0 +1,20 @@
+/**
+ * WordPress dependencies
+ */
+import { store, getContext, withScope } from '@wordpress/interactivity';
+
+store( 'with-scope', {
+ callbacks: {
+ asyncInit: () => {
+ setTimeout( withScope(function*() {
+ yield new Promise(resolve => setTimeout(resolve, 1));
+ const context = getContext()
+ context.asyncCounter += 1;
+ }, 1 ));
+ },
+ syncInit: () => {
+ const context = getContext()
+ context.syncCounter += 1;
+ }
+ },
+} );
diff --git a/packages/edit-site/src/components/layout/index.js b/packages/edit-site/src/components/layout/index.js
index e8c45ccdb02a6d..163d4fe6e938ff 100644
--- a/packages/edit-site/src/components/layout/index.js
+++ b/packages/edit-site/src/components/layout/index.js
@@ -31,17 +31,14 @@ import {
useBlockCommands,
store as blockEditorStore,
} from '@wordpress/block-editor';
-import { privateApis as routerPrivateApis } from '@wordpress/router';
import { privateApis as coreCommandsPrivateApis } from '@wordpress/core-commands';
/**
* Internal dependencies
*/
import Sidebar from '../sidebar';
-import Editor from '../editor';
import ErrorBoundary from '../error-boundary';
import { store as editSiteStore } from '../../store';
-import getIsListPage from '../../utils/get-is-list-page';
import Header from '../header-edit-mode';
import useInitEditedEntityFromURL from '../sync-state-with-url/use-init-edited-entity-from-url';
import SiteHub from '../site-hub';
@@ -53,12 +50,11 @@ import KeyboardShortcutsRegister from '../keyboard-shortcuts/register';
import KeyboardShortcutsGlobal from '../keyboard-shortcuts/global';
import { useCommonCommands } from '../../hooks/commands/use-common-commands';
import { useEditModeCommands } from '../../hooks/commands/use-edit-mode-commands';
-import PageMain from '../page-main';
import { useIsSiteEditorLoading } from './hooks';
+import useLayoutAreas from './router';
const { useCommands } = unlock( coreCommandsPrivateApis );
const { useCommandContext } = unlock( commandsPrivateApis );
-const { useLocation } = unlock( routerPrivateApis );
const { useGlobalStyle } = unlock( blockEditorPrivateApis );
const ANIMATION_DURATION = 0.5;
@@ -72,10 +68,7 @@ export default function Layout() {
useCommonCommands();
useBlockCommands();
- const { params } = useLocation();
const isMobileViewport = useViewportMatch( 'medium', '<' );
- const isListPage = getIsListPage( params, isMobileViewport );
- const isEditorPage = ! isListPage;
const {
isDistractionFree,
@@ -109,26 +102,17 @@ export default function Layout() {
select( blockEditorStore ).getBlockSelectionStart(),
};
}, [] );
- const isEditing = canvasMode === 'edit';
const navigateRegionsProps = useNavigateRegions( {
previous: previousShortcut,
next: nextShortcut,
} );
const disableMotion = useReducedMotion();
- const showSidebar =
- ( isMobileViewport && canvasMode === 'view' && ! isListPage ) ||
- ( ! isMobileViewport && ( canvasMode === 'view' || ! isEditorPage ) );
- const showCanvas =
- ( isMobileViewport && isEditorPage && isEditing ) ||
- ! isMobileViewport ||
- ! isEditorPage;
- const isFullCanvas =
- ( isMobileViewport && isListPage ) || ( isEditorPage && isEditing );
const [ canvasResizer, canvasSize ] = useResizeObserver();
const [ fullResizer ] = useResizeObserver();
const isEditorLoading = useIsSiteEditorLoading();
const [ isResizableFrameOversized, setIsResizableFrameOversized ] =
useState( false );
+ const { areas, widths } = useLayoutAreas();
// This determines which animation variant should apply to the header.
// There is also a `isDistractionFreeHovering` state that gets priority
@@ -154,7 +138,7 @@ export default function Layout() {
// Sets the right context for the command palette
let commandContext = 'site-editor';
- if ( canvasMode === 'edit' && isEditorPage ) {
+ if ( canvasMode === 'edit' ) {
commandContext = 'site-editor-edit';
}
if ( hasBlockSelected ) {
@@ -185,9 +169,9 @@ export default function Layout() {
'edit-site-layout',
navigateRegionsProps.className,
{
- 'is-distraction-free': isDistractionFree && isEditing,
- 'is-full-canvas': isFullCanvas,
- 'is-edit-mode': isEditing,
+ 'is-distraction-free':
+ isDistractionFree && canvasMode === 'edit',
+ 'is-full-canvas': canvasMode === 'edit',
'has-fixed-toolbar': hasFixedToolbar,
'is-block-toolbar-visible': hasBlockSelected,
}
@@ -228,7 +212,7 @@ export default function Layout() {
/>
- { isEditorPage && isEditing && (
+ { canvasMode === 'edit' && (
- { showSidebar && (
+ { canvasMode === 'view' && (
- { showCanvas && (
- <>
- { isListPage && }
- { isEditorPage && (
-
- { canvasResizer }
- { !! canvasSize.width && (
-
+ { areas.content }
+
+ ) }
+
+ { areas.preview && (
+
+ { canvasResizer }
+ { !! canvasSize.width && (
+
+
+
-
-
-
-
-
-
- ) }
-
+ { areas.preview }
+
+
+
) }
- >
+
) }
diff --git a/packages/edit-site/src/components/layout/router.js b/packages/edit-site/src/components/layout/router.js
new file mode 100644
index 00000000000000..3f9a8be7f82de4
--- /dev/null
+++ b/packages/edit-site/src/components/layout/router.js
@@ -0,0 +1,103 @@
+/**
+ * WordPress dependencies
+ */
+import { privateApis as routerPrivateApis } from '@wordpress/router';
+
+/**
+ * Internal dependencies
+ */
+import { unlock } from '../../lock-unlock';
+import { useIsSiteEditorLoading } from './hooks';
+import Editor from '../editor';
+import DataviewsPatterns from '../page-patterns/dataviews-patterns';
+import PagePages from '../page-pages';
+import PagePatterns from '../page-patterns';
+import PageTemplateParts from '../page-template-parts';
+import PageTemplates from '../page-templates';
+
+const { useLocation } = unlock( routerPrivateApis );
+
+export default function useLayoutAreas() {
+ const isSiteEditorLoading = useIsSiteEditorLoading();
+ const { params } = useLocation();
+ const { postType, postId, path, layout, isCustom } = params ?? {};
+
+ // Regular page
+ if ( path === '/page' ) {
+ const isListLayout =
+ isCustom !== 'true' && ( ! layout || layout === 'list' );
+ return {
+ areas: {
+ content: window.__experimentalAdminViews ? (
+
+ ) : undefined,
+ preview: ( isListLayout ||
+ ! window.__experimentalAdminViews ) && (
+
+ ),
+ },
+ widths: {
+ content:
+ window.__experimentalAdminViews && isListLayout
+ ? 380
+ : undefined,
+ },
+ };
+ }
+
+ // Regular other post types
+ if ( postType && postId ) {
+ return {
+ areas: {
+ preview: ,
+ },
+ };
+ }
+
+ // Templates
+ if (
+ path === '/wp_template/all' ||
+ ( path === '/wp_template' && window?.__experimentalAdminViews )
+ ) {
+ const isListLayout =
+ isCustom !== 'true' && ( ! layout || layout === 'list' );
+ return {
+ areas: {
+ content: ,
+ preview: isListLayout && (
+
+ ),
+ },
+ widths: {
+ content: isListLayout ? 380 : undefined,
+ },
+ };
+ }
+
+ // Template parts
+ if ( path === '/wp_template_part/all' ) {
+ return {
+ areas: {
+ content: ,
+ },
+ };
+ }
+
+ // Patterns
+ if ( path === '/patterns' ) {
+ return {
+ areas: {
+ content: window?.__experimentalAdminViews ? (
+
+ ) : (
+
+ ),
+ },
+ };
+ }
+
+ // Fallback shows the home page preview
+ return {
+ areas: { preview: },
+ };
+}
diff --git a/packages/edit-site/src/components/layout/style.scss b/packages/edit-site/src/components/layout/style.scss
index 53d3ed82a88bc2..c14d4c2f68274b 100644
--- a/packages/edit-site/src/components/layout/style.scss
+++ b/packages/edit-site/src/components/layout/style.scss
@@ -14,11 +14,6 @@
height: $header-height;
z-index: z-index(".edit-site-layout__hub");
- .edit-site-layout.is-full-canvas.is-edit-mode & {
- width: $header-height;
- padding-right: 0;
- }
-
@include break-medium {
width: calc(#{$nav-sidebar-width} - #{$grid-unit-30});
}
@@ -30,7 +25,7 @@
box-shadow: none;
@include break-medium {
- width: auto;
+ width: $header-height;
padding-right: 0;
}
}
@@ -156,20 +151,13 @@
}
}
-.edit-site-template-pages-list-view,
-.edit-site-page-pages-list-view {
- max-width: $nav-sidebar-width;
-}
-
// This shouldn't be necessary (we should have a way to say that a skeletton is relative
.edit-site-layout__canvas .interface-interface-skeleton,
-.edit-site-page-pages-preview .interface-interface-skeleton,
.edit-site-template-pages-preview .interface-interface-skeleton {
position: relative !important;
min-height: 100% !important;
}
-.edit-site-page-pages-preview,
.edit-site-template-pages-preview {
height: 100%;
}
@@ -187,7 +175,7 @@
justify-content: center;
border-bottom: 1px solid transparent;
- .edit-site-layout.is-full-canvas.is-edit-mode & {
+ .edit-site-layout.is-full-canvas & {
border-bottom-color: $gray-200;
transition: border-bottom-color 0.15s 0.4s ease-out;
}
@@ -261,8 +249,7 @@
}
}
-.is-edit-mode.is-distraction-free {
-
+.edit-site-layout.is-distraction-free {
.edit-site-layout__header-container {
height: $header-height;
position: absolute;
@@ -300,3 +287,16 @@
}
}
+
+.edit-site-layout__area {
+ color: $gray-800;
+ background: $white;
+ flex-grow: 1;
+ overflow: auto;
+ margin: 0;
+ margin-top: $header-height;
+ @include break-medium() {
+ border-radius: 8px;
+ margin: $canvas-padding $canvas-padding $canvas-padding 0;
+ }
+}
diff --git a/packages/edit-site/src/components/page-main/index.js b/packages/edit-site/src/components/page-main/index.js
deleted file mode 100644
index fead1b9dbc11e0..00000000000000
--- a/packages/edit-site/src/components/page-main/index.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { privateApis as routerPrivateApis } from '@wordpress/router';
-
-/**
- * Internal dependencies
- */
-import PagePatterns from '../page-patterns';
-import DataviewsPatterns from '../page-patterns/dataviews-patterns';
-import PageTemplateParts from '../page-template-parts';
-import PageTemplates from '../page-templates';
-import PagePages from '../page-pages';
-import { unlock } from '../../lock-unlock';
-
-const { useLocation } = unlock( routerPrivateApis );
-
-export default function PageMain() {
- const {
- params: { path },
- } = useLocation();
-
- if (
- path === '/wp_template/all' ||
- ( path === '/wp_template' && window?.__experimentalAdminViews )
- ) {
- return ;
- } else if ( path === '/wp_template_part/all' ) {
- return ;
- } else if ( path === '/patterns' ) {
- return window?.__experimentalAdminViews ? (
-
- ) : (
-
- );
- } else if ( window?.__experimentalAdminViews && path === '/page' ) {
- return ;
- }
-
- return null;
-}
diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js
index 35382a25039fa8..048821b048f6aa 100644
--- a/packages/edit-site/src/components/page-pages/index.js
+++ b/packages/edit-site/src/components/page-pages/index.js
@@ -10,7 +10,6 @@ import { dateI18n, getDate, getSettings } from '@wordpress/date';
import { privateApis as routerPrivateApis } from '@wordpress/router';
import { useSelect, useDispatch } from '@wordpress/data';
import { DataViews } from '@wordpress/dataviews';
-import { ENTER, SPACE } from '@wordpress/keycodes';
/**
* Internal dependencies
@@ -38,7 +37,6 @@ import {
viewPostAction,
useEditPostAction,
} from '../actions';
-import PostPreview from '../post-preview';
import AddNewPageModal from '../add-new-page';
import Media from '../media';
import { unlock } from '../../lock-unlock';
@@ -46,13 +44,24 @@ const { useLocation, useHistory } = unlock( routerPrivateApis );
const EMPTY_ARRAY = [];
-function useView( type ) {
- const {
- params: { activeView = 'all', isCustom = 'false' },
- } = useLocation();
- const selectedDefaultView =
- isCustom === 'false' &&
- DEFAULT_VIEWS[ type ].find( ( { slug } ) => slug === activeView )?.view;
+function useView( postType ) {
+ const { params } = useLocation();
+ const { activeView = 'all', isCustom = 'false', layout } = params;
+ const history = useHistory();
+ const selectedDefaultView = useMemo( () => {
+ const defaultView =
+ isCustom === 'false' &&
+ DEFAULT_VIEWS[ postType ].find(
+ ( { slug } ) => slug === activeView
+ )?.view;
+ if ( isCustom === 'false' && layout ) {
+ return {
+ ...defaultView,
+ type: layout,
+ };
+ }
+ return defaultView;
+ }, [ isCustom, activeView, layout, postType ] );
const [ view, setView ] = useState( selectedDefaultView );
useEffect( () => {
@@ -107,13 +116,26 @@ function useView( type ) {
[ editEntityRecord, editedViewRecord?.id ]
);
+ const setDefaultViewAndUpdateUrl = useCallback(
+ ( viewToSet ) => {
+ if ( viewToSet.type !== view?.type ) {
+ history.push( {
+ ...params,
+ layout: viewToSet.type,
+ } );
+ }
+ setView( viewToSet );
+ },
+ [ params, view?.type, history ]
+ );
+
if ( isCustom === 'false' ) {
- return [ view, setView ];
+ return [ view, setDefaultViewAndUpdateUrl ];
} else if ( isCustom === 'true' && customView ) {
return [ customView, setCustomView ];
}
// Loading state where no the view was not found on custom views or default views.
- return [ DEFAULT_VIEWS[ type ][ 0 ].view, setView ];
+ return [ DEFAULT_VIEWS[ postType ][ 0 ].view, setDefaultViewAndUpdateUrl ];
}
// See https://github.com/WordPress/gutenberg/issues/55886
@@ -131,12 +153,20 @@ const DEFAULT_STATUSES = 'draft,future,pending,private,publish'; // All but 'tra
export default function PagePages() {
const postType = 'page';
const [ view, setView ] = useView( postType );
- const [ pageId, setPageId ] = useState( null );
const history = useHistory();
+ const { params } = useLocation();
+ const { isCustom = 'false' } = params;
const onSelectionChange = useCallback(
- ( items ) => setPageId( items?.length === 1 ? items[ 0 ].id : null ),
- [ setPageId ]
+ ( items ) => {
+ if ( isCustom === 'false' && view?.type === LAYOUT_LIST ) {
+ history.push( {
+ ...params,
+ postId: items.length === 1 ? items[ 0 ].id : undefined,
+ } );
+ }
+ },
+ [ history, params, view?.type, isCustom ]
);
const onDetailsChange = useCallback(
@@ -366,85 +396,33 @@ export default function PagePages() {
// TODO: we need to handle properly `data={ data || EMPTY_ARRAY }` for when `isLoading`.
return (
- <>
-
-
- { showAddPageModal && (
-
- ) }
- >
- }
- >
-
-
- { view.type === LAYOUT_LIST && (
-
- {
- const { keyCode } = event;
- if ( keyCode === ENTER || keyCode === SPACE ) {
- history.push( {
- postId: pageId,
- postType,
- canvas: 'edit',
- } );
- }
- } }
- onClick={ () =>
- history.push( {
- postId: pageId,
- postType,
- canvas: 'edit',
- } )
- }
- >
- { pageId !== null ? (
-
- ) : (
-
-
{ __( 'Select a page to preview' ) }
-
- ) }
-
-
- ) }
- >
+
+
+ { showAddPageModal && (
+
+ ) }
+ >
+ }
+ >
+
+
);
}
diff --git a/packages/edit-site/src/components/page-patterns/style.scss b/packages/edit-site/src/components/page-patterns/style.scss
index 6be2f904a65ec5..b5d092f438e8e7 100644
--- a/packages/edit-site/src/components/page-patterns/style.scss
+++ b/packages/edit-site/src/components/page-patterns/style.scss
@@ -1,10 +1,11 @@
.edit-site-patterns {
+ background: #1e1e1e;
border-left: 1px solid $gray-800;
- background: none;
margin: $header-height 0 0;
border-radius: 0;
padding: 0;
overflow-x: auto;
+ min-height: 100%;
.components-base-control {
width: 100%;
diff --git a/packages/edit-site/src/components/page-templates/index.js b/packages/edit-site/src/components/page-templates/index.js
index 4ef099aed6d8d3..7deec674f9b419 100644
--- a/packages/edit-site/src/components/page-templates/index.js
+++ b/packages/edit-site/src/components/page-templates/index.js
@@ -16,7 +16,6 @@ import { __ } from '@wordpress/i18n';
import { useState, useMemo, useCallback } from '@wordpress/element';
import { useEntityRecords } from '@wordpress/core-data';
import { decodeEntities } from '@wordpress/html-entities';
-import { ENTER, SPACE } from '@wordpress/keycodes';
import { parse } from '@wordpress/blocks';
import {
BlockPreview,
@@ -53,12 +52,11 @@ import {
import { postRevisionsAction } from '../actions';
import usePatternSettings from '../page-patterns/use-pattern-settings';
import { unlock } from '../../lock-unlock';
-import PostPreview from '../post-preview';
const { ExperimentalBlockEditorProvider, useGlobalStyle } = unlock(
blockEditorPrivateApis
);
-const { useHistory } = unlock( routerPrivateApis );
+const { useHistory, useLocation } = unlock( routerPrivateApis );
const EMPTY_ARRAY = [];
@@ -156,18 +154,30 @@ function TemplatePreview( { content, viewType } ) {
}
export default function DataviewsTemplates() {
- const [ templateId, setTemplateId ] = useState( null );
- const [ view, setView ] = useState( DEFAULT_VIEW );
+ const { params } = useLocation();
+ const { layout } = params;
+ const defaultView = useMemo( () => {
+ return {
+ ...DEFAULT_VIEW,
+ type: layout ?? DEFAULT_VIEW.type,
+ };
+ }, [ layout ] );
+ const [ view, setView ] = useState( defaultView );
const { records: allTemplates, isResolving: isLoadingData } =
useEntityRecords( 'postType', TEMPLATE_POST_TYPE, {
per_page: -1,
} );
const history = useHistory();
-
const onSelectionChange = useCallback(
- ( items ) =>
- setTemplateId( items?.length === 1 ? items[ 0 ].id : null ),
- [ setTemplateId ]
+ ( items ) => {
+ if ( view?.type === LAYOUT_LIST ) {
+ history.push( {
+ ...params,
+ postId: items.length === 1 ? items[ 0 ].id : undefined,
+ } );
+ }
+ },
+ [ history, params, view?.type ]
);
const onDetailsChange = useCallback(
@@ -348,90 +358,41 @@ export default function DataviewsTemplates() {
...defaultConfigPerViewType[ newView.type ],
},
};
+
+ history.push( {
+ ...params,
+ layout: newView.type,
+ } );
}
setView( newView );
},
- [ view.type, setView ]
+ [ view.type, setView, history, params ]
);
return (
- <>
-
- }
- >
-
-
- { view.type === LAYOUT_LIST && (
-
- {
- const { keyCode } = event;
- if ( keyCode === ENTER || keyCode === SPACE ) {
- history.push( {
- postId: templateId,
- postType: TEMPLATE_POST_TYPE,
- canvas: 'edit',
- } );
- }
- } }
- onClick={ () =>
- history.push( {
- postId: templateId,
- postType: TEMPLATE_POST_TYPE,
- canvas: 'edit',
- } )
- }
- >
- { templateId !== null ? (
-
- ) : (
-
-
{ __( 'Select a template to preview' ) }
-
- ) }
-
-
- ) }
- >
+ }
+ >
+
+
);
}
diff --git a/packages/edit-site/src/components/post-preview/index.js b/packages/edit-site/src/components/post-preview/index.js
deleted file mode 100644
index 8f325c26275948..00000000000000
--- a/packages/edit-site/src/components/post-preview/index.js
+++ /dev/null
@@ -1,16 +0,0 @@
-/**
- * Internal dependencies
- */
-import Editor from '../editor';
-import { useInitEditedEntity } from '../sync-state-with-url/use-init-edited-entity-from-url';
-import { useIsSiteEditorLoading } from '../layout/hooks';
-
-export default function PostPreview( { postType, postId } ) {
- useInitEditedEntity( {
- postId,
- postType,
- } );
- const isEditorLoading = useIsSiteEditorLoading();
-
- return ;
-}
diff --git a/packages/edit-site/src/components/site-icon/style.scss b/packages/edit-site/src/components/site-icon/style.scss
index ac16279c9fe184..fc680166bf2691 100644
--- a/packages/edit-site/src/components/site-icon/style.scss
+++ b/packages/edit-site/src/components/site-icon/style.scss
@@ -9,7 +9,7 @@
object-fit: cover;
background: #333;
- .edit-site-layout.is-full-canvas.is-edit-mode & {
+ .edit-site-layout.is-full-canvas & {
border-radius: 0;
}
}
diff --git a/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js b/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js
index 36056cf4b43bda..11c065cf862dba 100644
--- a/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js
+++ b/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js
@@ -27,7 +27,7 @@ const postTypesWithoutParentTemplate = [
PATTERN_TYPES.user,
];
-function useResolveEditedEntityAndContext( { postId, postType } ) {
+function useResolveEditedEntityAndContext( { path, postId, postType } ) {
const { hasLoadedAllDependencies, homepageId, url, frontPageTemplateId } =
useSelect( ( select ) => {
const { getSite, getUnstableBase, getEntityRecords } =
@@ -159,6 +159,11 @@ function useResolveEditedEntityAndContext( { postId, postType } ) {
return resolveTemplateForPostTypeAndId( postType, postId );
}
+ // Some URLs in list views are different
+ if ( path === '/page' && postId ) {
+ return resolveTemplateForPostTypeAndId( 'page', postId );
+ }
+
// If we're rendering the home page, and we have a static home page, resolve its template.
if ( homepageId ) {
return resolveTemplateForPostTypeAndId( 'page', homepageId );
@@ -176,6 +181,7 @@ function useResolveEditedEntityAndContext( { postId, postType } ) {
url,
postId,
postType,
+ path,
frontPageTemplateId,
]
);
@@ -189,12 +195,25 @@ function useResolveEditedEntityAndContext( { postId, postType } ) {
return { postType, postId };
}
+ // Some URLs in list views are different
+ if ( path === '/page' && postId ) {
+ return { postType: 'page', postId };
+ }
+
if ( homepageId ) {
return { postType: 'page', postId: homepageId };
}
return {};
- }, [ homepageId, postType, postId ] );
+ }, [ homepageId, postType, postId, path ] );
+
+ if (
+ ( path === '/wp_template/all' ||
+ ( path === '/wp_template' && window?.__experimentalAdminViews ) ) &&
+ postId
+ ) {
+ return { isReady: true, postType: 'wp_template', postId, context };
+ }
if ( postTypesWithoutParentTemplate.includes( postType ) ) {
return { isReady: true, postType, postId, context };
@@ -212,7 +231,8 @@ function useResolveEditedEntityAndContext( { postId, postType } ) {
return { isReady: false };
}
-export function useInitEditedEntity( params ) {
+export default function useInitEditedEntityFromURL() {
+ const { params = {} } = useLocation();
const { postType, postId, context, isReady } =
useResolveEditedEntityAndContext( params );
@@ -224,8 +244,3 @@ export function useInitEditedEntity( params ) {
}
}, [ isReady, postType, postId, context, setEditedEntity ] );
}
-
-export default function useInitEditedEntityFromURL() {
- const { params = {} } = useLocation();
- return useInitEditedEntity( params );
-}
diff --git a/packages/edit-site/src/components/sync-state-with-url/use-sync-path-with-url.js b/packages/edit-site/src/components/sync-state-with-url/use-sync-path-with-url.js
index 5f176dff8198d3..3eb6d6e6f4fadf 100644
--- a/packages/edit-site/src/components/sync-state-with-url/use-sync-path-with-url.js
+++ b/packages/edit-site/src/components/sync-state-with-url/use-sync-path-with-url.js
@@ -82,6 +82,7 @@ export default function useSyncPathWithURL() {
postType: navigatorParams?.postType,
postId: navigatorParams?.postId,
path: undefined,
+ layout: undefined,
} );
} else if (
navigatorLocation.path.startsWith( '/page/' ) &&
@@ -91,6 +92,7 @@ export default function useSyncPathWithURL() {
postType: 'page',
postId: navigatorParams?.postId,
path: undefined,
+ layout: undefined,
} );
} else if ( navigatorLocation.path === '/patterns' ) {
updateUrlParams( {
@@ -99,12 +101,29 @@ export default function useSyncPathWithURL() {
canvas: undefined,
path: navigatorLocation.path,
} );
+ } else if (
+ // These sidebar paths are special in the sense that the url in these pages may or may not have a postId and we need to retain it if it has.
+ // The "type" property should be kept as well.
+ ( navigatorLocation.path === '/page' &&
+ window?.__experimentalAdminViews ) ||
+ ( navigatorLocation.path === '/wp_template' &&
+ window?.__experimentalAdminViews ) ||
+ ( navigatorLocation.path === '/wp_template/all' &&
+ ! window?.__experimentalAdminViews )
+ ) {
+ updateUrlParams( {
+ postType: undefined,
+ categoryType: undefined,
+ categoryId: undefined,
+ path: navigatorLocation.path,
+ } );
} else {
updateUrlParams( {
postType: undefined,
postId: undefined,
categoryType: undefined,
categoryId: undefined,
+ layout: undefined,
path:
navigatorLocation.path === '/'
? undefined
diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md
index f3f37c6f70b82c..252b200f9d4d01 100644
--- a/packages/interactivity/CHANGELOG.md
+++ b/packages/interactivity/CHANGELOG.md
@@ -5,12 +5,13 @@
### Enhancements
- Prevent the usage of Preact components in `wp-text`. ([#57879](https://github.com/WordPress/gutenberg/pull/57879))
-- Update `preact`, `@preact/signals` and `deepsignal` dependencies. ([57891](https://github.com/WordPress/gutenberg/pull/57891))
+- Update `preact`, `@preact/signals` and `deepsignal` dependencies. ([#57891](https://github.com/WordPress/gutenberg/pull/57891))
+- Export `withScope()` and allow to use it with asynchronous operations. ([#58013](https://github.com/WordPress/gutenberg/pull/58013))
### New Features
-- Add the `data-wp-run` directive along with the `useInit` and `useWatch` hooks. ([57805](https://github.com/WordPress/gutenberg/pull/57805))
-- Add `wp-data-on-window` and `wp-data-on-document` directives. ([57931](https://github.com/WordPress/gutenberg/pull/57931))
+- Add the `data-wp-run` directive along with the `useInit` and `useWatch` hooks. ([#57805](https://github.com/WordPress/gutenberg/pull/57805))
+- Add `wp-data-on-window` and `wp-data-on-document` directives. ([#57931](https://github.com/WordPress/gutenberg/pull/57931))
- Add the `data-wp-each` directive to render lists of items using a template. ([57859](https://github.com/WordPress/gutenberg/pull/57859))
### Breaking Changes
diff --git a/packages/interactivity/src/directives.js b/packages/interactivity/src/directives.js
index dd482045e3022c..c1879af1fe19a6 100644
--- a/packages/interactivity/src/directives.js
+++ b/packages/interactivity/src/directives.js
@@ -132,6 +132,7 @@ export default () => {
// data-wp-init--[name]
directive( 'init', ( { directives: { init }, evaluate } ) => {
init.forEach( ( entry ) => {
+ // TODO: Replace with useEffect to prevent unneeded scopes.
useInit( () => evaluate( entry ) );
} );
} );
diff --git a/packages/interactivity/src/hooks.tsx b/packages/interactivity/src/hooks.tsx
index ae543506c01feb..c133eb9981880b 100644
--- a/packages/interactivity/src/hooks.tsx
+++ b/packages/interactivity/src/hooks.tsx
@@ -156,6 +156,8 @@ export const resetScope = () => {
scopeStack.pop();
};
+export const getNamespace = () => namespaceStack.slice( -1 )[ 0 ];
+
export const setNamespace = ( namespace: string ) => {
namespaceStack.push( namespace );
};
diff --git a/packages/interactivity/src/index.js b/packages/interactivity/src/index.js
index cf0b4c88cac4bf..0864291455310d 100644
--- a/packages/interactivity/src/index.js
+++ b/packages/interactivity/src/index.js
@@ -5,9 +5,10 @@ import registerDirectives from './directives';
import { init } from './router';
export { store } from './store';
-export { directive, getContext, getElement } from './hooks';
+export { directive, getContext, getElement, getNamespace } from './hooks';
export { navigate, prefetch } from './router';
export {
+ withScope,
useWatch,
useInit,
useEffect,
diff --git a/packages/interactivity/src/utils.js b/packages/interactivity/src/utils.js
index 021df983cb4f0a..84e04803cea4f5 100644
--- a/packages/interactivity/src/utils.js
+++ b/packages/interactivity/src/utils.js
@@ -12,7 +12,14 @@ import { effect } from '@preact/signals';
/**
* Internal dependencies
*/
-import { getScope, setScope, resetScope } from './hooks';
+import {
+ getScope,
+ setScope,
+ resetScope,
+ getNamespace,
+ setNamespace,
+ resetNamespace,
+} from './hooks';
const afterNextFrame = ( callback ) => {
return new Promise( ( resolve ) => {
@@ -71,13 +78,40 @@ export function useSignalEffect( callback ) {
* @param {Function} func The passed function.
* @return {Function} The wrapped function.
*/
-const withScope = ( func ) => {
+export const withScope = ( func ) => {
const scope = getScope();
+ const ns = getNamespace();
+ if ( func?.constructor?.name === 'GeneratorFunction' ) {
+ return async ( ...args ) => {
+ const gen = func( ...args );
+ let value;
+ let it;
+ while ( true ) {
+ setNamespace( ns );
+ setScope( scope );
+ try {
+ it = gen.next( value );
+ } finally {
+ resetNamespace();
+ resetScope();
+ }
+ try {
+ value = await it.value;
+ } catch ( e ) {
+ gen.throw( e );
+ }
+ if ( it.done ) break;
+ }
+ return value;
+ };
+ }
return ( ...args ) => {
+ setNamespace( ns );
setScope( scope );
try {
return func( ...args );
} finally {
+ resetNamespace();
resetScope();
}
};
diff --git a/packages/preferences/package.json b/packages/preferences/package.json
index ac1f1c9ade95cc..7cab99bfab0329 100644
--- a/packages/preferences/package.json
+++ b/packages/preferences/package.json
@@ -33,6 +33,7 @@
"@wordpress/components": "file:../components",
"@wordpress/compose": "file:../compose",
"@wordpress/data": "file:../data",
+ "@wordpress/deprecated": "file:../deprecated",
"@wordpress/element": "file:../element",
"@wordpress/i18n": "file:../i18n",
"@wordpress/icons": "file:../icons",
diff --git a/packages/preferences/src/store/selectors.js b/packages/preferences/src/store/selectors.js
index 58b4d18bc362aa..e249acf7bbdd33 100644
--- a/packages/preferences/src/store/selectors.js
+++ b/packages/preferences/src/store/selectors.js
@@ -1,3 +1,43 @@
+/**
+ * WordPress dependencies
+ */
+import deprecated from '@wordpress/deprecated';
+
+const withDeprecatedKeys = ( originalGet ) => ( state, scope, name ) => {
+ const settingsToMoveToCore = [
+ 'allowRightClickOverrides',
+ 'distractionFree',
+ 'editorMode',
+ 'fixedToolbar',
+ 'focusMode',
+ 'hiddenBlockTypes',
+ 'inactivePanels',
+ 'keepCaretInsideBlock',
+ 'mostUsedBlocks',
+ 'openPanels',
+ 'showBlockBreadcrumbs',
+ 'showIconLabels',
+ 'showListViewByDefault',
+ ];
+
+ if (
+ settingsToMoveToCore.includes( name ) &&
+ [ 'core/edit-post', 'core/edit-site' ].includes( scope )
+ ) {
+ deprecated(
+ `wp.data.select( 'core/preferences' ).get( '${ scope }', '${ name }' )`,
+ {
+ since: '6.5',
+ alternative: `wp.data.select( 'core/preferences' ).get( 'core', '${ name }' )`,
+ }
+ );
+
+ return originalGet( state, 'core', name );
+ }
+
+ return originalGet( state, scope, name );
+};
+
/**
* Returns a boolean indicating whether a prefer is active for a particular
* scope.
@@ -8,7 +48,7 @@
*
* @return {*} Is the feature enabled?
*/
-export function get( state, scope, name ) {
+export const get = withDeprecatedKeys( ( state, scope, name ) => {
const value = state.preferences[ scope ]?.[ name ];
return value !== undefined ? value : state.defaults[ scope ]?.[ name ];
-}
+} );
diff --git a/packages/react-native-aztec/android/build.gradle b/packages/react-native-aztec/android/build.gradle
index 18093ff1c2c136..6c1f4e5b02cce3 100644
--- a/packages/react-native-aztec/android/build.gradle
+++ b/packages/react-native-aztec/android/build.gradle
@@ -36,7 +36,7 @@ plugins {
// import the `readReactNativeVersion()` function
apply from: 'https://gist.githubusercontent.com/hypest/742448b9588b3a0aa580a5e80ae95bdf/raw/8eb62d40ee7a5104d2fcaeff21ce6f29bd93b054/readReactNativeVersion.gradle'
-group = 'org.wordpress-mobile.gutenberg-mobile'
+group = 'org.wordpress.gutenberg-mobile'
// The sample build uses multiple directories to
// keep boilerplate and common code separate from
@@ -119,7 +119,7 @@ project.afterEvaluate {
ReactNativeAztecPublication(MavenPublication) {
artifact bundleReleaseAar
- groupId 'org.wordpress-mobile.gutenberg-mobile'
+ groupId 'org.wordpress.gutenberg-mobile'
artifactId 'react-native-aztec'
// version is set by 'publish-to-s3' plugin
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 7800be076c842a..c54e73b9a4b6a1 100644
--- a/packages/react-native-bridge/android/react-native-bridge/build.gradle
+++ b/packages/react-native-bridge/android/react-native-bridge/build.gradle
@@ -25,7 +25,7 @@ plugins {
apply from: 'https://gist.githubusercontent.com/hypest/742448b9588b3a0aa580a5e80ae95bdf/raw/8eb62d40ee7a5104d2fcaeff21ce6f29bd93b054/readReactNativeVersion.gradle'
apply from: '../extractPackageVersion.gradle'
-group='org.wordpress-mobile.gutenberg-mobile'
+group='org.wordpress.gutenberg-mobile'
def buildAssetsFolder = 'build/assets'
@@ -93,8 +93,8 @@ 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 = "v3"
- def reactNativeLibrariesGroupId = "org.wordpress-mobile.react-native-libraries.$reactNativeLibrariesPublisherVersion"
+ def reactNativeLibrariesPublisherVersion = "v4"
+ def reactNativeLibrariesGroupId = "org.wordpress.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')}"
implementation "$reactNativeLibrariesGroupId:react-native-screens:${extractPackageVersion(packageJson, 'react-native-screens', 'dependencies')}"
@@ -110,7 +110,7 @@ dependencies {
runtimeOnly "com.facebook.react:hermes-android:$rnVersion"
if (willPublishReactNativeBridgeBinary) {
- implementation "org.wordpress-mobile.gutenberg-mobile:react-native-aztec:$reactNativeAztecVersion"
+ implementation "org.wordpress.gutenberg-mobile:react-native-aztec:$reactNativeAztecVersion"
} else {
api project(':@wordpress_react-native-aztec')
}
@@ -122,7 +122,7 @@ project.afterEvaluate {
ReactNativeBridgePublication(MavenPublication) {
artifact bundleReleaseAar
- groupId 'org.wordpress-mobile.gutenberg-mobile'
+ groupId 'org.wordpress.gutenberg-mobile'
artifactId 'react-native-gutenberg-bridge'
// version is set by 'publish-to-s3' plugin
diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md
index c5973ddace958d..fba0781e594fb5 100644
--- a/packages/react-native-editor/CHANGELOG.md
+++ b/packages/react-native-editor/CHANGELOG.md
@@ -10,6 +10,7 @@ For each user feature we should also add a importance categorization label to i
-->
## Unreleased
+- [**] Video block: Fix logic for displaying empty state based on source presence [#58015]
## 1.111.0
- [**] Image block media uploads display a custom error message when there is no internet connection [#56937]
diff --git a/packages/react-native-editor/__device-tests__/helpers/utils.js b/packages/react-native-editor/__device-tests__/helpers/utils.js
index 8af4a0b1a60d71..5af85b9e6847d0 100644
--- a/packages/react-native-editor/__device-tests__/helpers/utils.js
+++ b/packages/react-native-editor/__device-tests__/helpers/utils.js
@@ -306,19 +306,11 @@ const clickBeginningOfElement = async ( driver, element ) => {
.perform();
};
-// Long press to activate context menu.
-const longPressMiddleOfElement = async (
+async function longPressElement(
driver,
element,
- waitTime = 1000,
- customElementSize
-) => {
- const location = await element.getLocation();
- const size = customElementSize || ( await element.getSize() );
-
- const x = location.x + size.width / 2;
- const y = location.y + size.height / 2;
-
+ { waitTime = 1000, offset = { x: 0, y: 0 } } = {}
+) {
// Focus on the element first, otherwise on iOS it fails to open the context menu.
// We can't do it all in one action because it detects it as a force press and it
// is not supported by the simulator.
@@ -331,16 +323,43 @@ const longPressMiddleOfElement = async (
.up()
.perform();
+ const location = await element.getLocation();
+ const size = await element.getSize();
+
+ let offsetX = offset.x;
+ if ( typeof offset.x === 'function' ) {
+ offsetX = offset.x( size.width );
+ }
+ let offsetY = offset.y;
+ if ( typeof offset.y === 'function' ) {
+ offsetY = offset.y( size.height );
+ }
+
// Long-press
await driver
.action( 'pointer', {
parameters: { pointerType: 'touch' },
} )
- .move( { x, y } )
+ .move( { x: location.x + offsetX, y: location.y + offsetY } )
.down()
.pause( waitTime )
.up()
.perform();
+}
+
+// Long press to activate context menu.
+const longPressMiddleOfElement = async (
+ driver,
+ element,
+ { waitTime = 1000 } = {}
+) => {
+ await longPressElement( driver, element, {
+ waitTime,
+ offset: {
+ x: ( width ) => width / 2,
+ y: ( height ) => height / 2,
+ },
+ } );
};
const tapStatusBariOS = async ( driver ) => {
@@ -717,6 +736,7 @@ module.exports = {
isElementVisible,
isLocalEnvironment,
launchApp,
+ longPressElement,
longPressMiddleOfElement,
selectTextFromElement,
setupDriver,
diff --git a/packages/react-native-editor/__device-tests__/pages/editor-page.js b/packages/react-native-editor/__device-tests__/pages/editor-page.js
index b00be20458e802..fe3f410601a8dc 100644
--- a/packages/react-native-editor/__device-tests__/pages/editor-page.js
+++ b/packages/react-native-editor/__device-tests__/pages/editor-page.js
@@ -15,6 +15,8 @@ const {
clickIfClickable,
launchApp,
tapStatusBariOS,
+ longPressElement,
+ longPressMiddleOfElement,
} = require( '../helpers/utils' );
const ADD_BLOCK_ID = isAndroid() ? 'Add block' : 'add-block-button';
@@ -105,6 +107,47 @@ class EditorPage {
await typeString( this.driver, block, text, clear );
}
+ async pasteClipboardToTextBlock( element, { timeout = 1000 } = {} ) {
+ if ( this.driver.isAndroid ) {
+ await longPressMiddleOfElement( this.driver, element );
+ } else {
+ await longPressElement( this.driver, element );
+ }
+
+ if ( this.driver.isAndroid ) {
+ // Long pressing seemingly results in drag-and-drop blurring the input, so
+ // we tap again to re-focus the input.
+ await this.driver
+ .action( 'pointer', {
+ parameters: { pointerType: 'touch' },
+ } )
+ .move( { origin: element } )
+ .down()
+ .up()
+ .perform();
+
+ const location = await element.getLocation();
+ const approximatePasteMenuLocation = {
+ x: location.x + 30,
+ y: location.y - 120,
+ };
+ await this.driver
+ .action( 'pointer', {
+ parameters: { pointerType: 'touch' },
+ } )
+ .move( approximatePasteMenuLocation )
+ .down()
+ .up()
+ .perform();
+ } else {
+ const pasteMenuItem = await this.driver.$(
+ '//XCUIElementTypeMenuItem[@name="Paste"]'
+ );
+ await pasteMenuItem.waitForDisplayed( { timeout } );
+ await pasteMenuItem.click();
+ }
+ }
+
// Finds the wd element for new block that was added and sets the element attribute
// and accessibilityId attributes on this object and selects the block
// position uses one based numbering.
diff --git a/packages/react-native-editor/android/app/build.gradle b/packages/react-native-editor/android/app/build.gradle
index ecb63589bcf1c4..514c645354232d 100644
--- a/packages/react-native-editor/android/app/build.gradle
+++ b/packages/react-native-editor/android/app/build.gradle
@@ -132,7 +132,7 @@ android {
dependencies {
def packageJson = '../../package.json'
- implementation "org.wordpress-mobile.gutenberg-mobile:react-native-bridge"
+ implementation "org.wordpress.gutenberg-mobile:react-native-bridge"
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation "com.google.android.material:material:1.9.0"
// The version of react-native is set by the React Native Gradle Plugin
@@ -145,8 +145,8 @@ 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 = "v3"
- def reactNativeLibrariesGroupId = "org.wordpress-mobile.react-native-libraries.$reactNativeLibrariesPublisherVersion"
+ def reactNativeLibrariesPublisherVersion = "v4"
+ def reactNativeLibrariesGroupId = "org.wordpress.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')}"
implementation "$reactNativeLibrariesGroupId:react-native-screens:${extractPackageVersion(packageJson, 'react-native-screens', 'dependencies')}"
diff --git a/packages/react-native-editor/android/build.gradle b/packages/react-native-editor/android/build.gradle
index 7baee554d330c7..f08019a79c9007 100644
--- a/packages/react-native-editor/android/build.gradle
+++ b/packages/react-native-editor/android/build.gradle
@@ -7,7 +7,7 @@ buildscript {
compileSdkVersion = 34
targetSdkVersion = 33
supportLibVersion = '28.0.0'
-
+
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
ndkVersion = "23.1.7779620"
}
@@ -30,8 +30,7 @@ allprojects {
content {
includeGroup "org.wordpress"
includeGroup "org.wordpress.aztec"
- includeGroup "org.wordpress-mobile"
- includeGroupByRegex "org.wordpress-mobile.react-native-libraries.*"
+ includeGroupByRegex "org.wordpress.react-native-libraries.*"
}
}
maven { url 'https://www.jitpack.io' }
diff --git a/schemas/json/font-collection.json b/schemas/json/font-collection.json
index a6ca2b1412e6d2..4973c71312b605 100644
--- a/schemas/json/font-collection.json
+++ b/schemas/json/font-collection.json
@@ -7,11 +7,6 @@
"description": "JSON schema URI for font-collection.json.",
"type": "string"
},
- "version": {
- "description": "Version of font-collection.json schema to use.",
- "type": "integer",
- "enum": [ 1 ]
- },
"font_families": {
"type": "array",
"description": "Array of font families ready to be installed",
@@ -55,5 +50,5 @@
}
},
"additionalProperties": false,
- "required": [ "$schema", "version", "font_families" ]
+ "required": [ "$schema", "font_families" ]
}
diff --git a/test/e2e/specs/editor/various/embedding.spec.js b/test/e2e/specs/editor/various/embedding.spec.js
index 2fa963f66685d2..3e04960e7fbf87 100644
--- a/test/e2e/specs/editor/various/embedding.spec.js
+++ b/test/e2e/specs/editor/various/embedding.spec.js
@@ -122,7 +122,7 @@ test.describe( 'Embedding content', () => {
await expect(
currenEmbedBlock.getByRole( 'textbox', { name: 'Embed URL' } ),
'WordPress invalid content. Should render failed, edit state.'
- ).toHaveValue( 'https://wordpress.org/gutenberg/handbook/' );
+ ).toHaveValue( 'https://wordpress.org/gutenberg/handbook' );
await embedUtils.insertEmbed( 'https://twitter.com/thatbunty' );
await expect(
@@ -189,7 +189,7 @@ test.describe( 'Embedding content', () => {
} );
// Reason: A possible regression of https://github.com/WordPress/gutenberg/pull/14705.
- test.skip( 'should retry embeds that could not be embedded with trailing slashes, without the trailing slashes', async ( {
+ test( 'should retry embeds that could not be embedded with trailing slashes, without the trailing slashes', async ( {
editor,
embedUtils,
} ) => {
diff --git a/test/e2e/specs/interactivity/with-scope.spec.ts b/test/e2e/specs/interactivity/with-scope.spec.ts
new file mode 100644
index 00000000000000..1cb73cc915aca7
--- /dev/null
+++ b/test/e2e/specs/interactivity/with-scope.spec.ts
@@ -0,0 +1,27 @@
+/**
+ * Internal dependencies
+ */
+import { test, expect } from './fixtures';
+
+test.describe( 'withScope', () => {
+ test.beforeAll( async ( { interactivityUtils: utils } ) => {
+ await utils.activatePlugins();
+ await utils.addPostWithBlock( 'test/with-scope' );
+ } );
+ test.beforeEach( async ( { interactivityUtils: utils, page } ) => {
+ await page.goto( utils.getLink( 'test/with-scope' ) );
+ } );
+ test.afterAll( async ( { interactivityUtils: utils } ) => {
+ await utils.deactivatePlugins();
+ await utils.deleteAllPosts();
+ } );
+
+ test( 'directives using withScope should work with async and sync functions', async ( {
+ page,
+ } ) => {
+ const asyncCounter = page.getByTestId( 'asyncCounter' );
+ await expect( asyncCounter ).toHaveText( '1' );
+ const syncCounter = page.getByTestId( 'syncCounter' );
+ await expect( syncCounter ).toHaveText( '1' );
+ } );
+} );