From 001241bbb3ff1b0c24c025815d7897ef353d2d74 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Thu, 19 Sep 2024 12:12:19 +0200 Subject: [PATCH 01/15] Interactivity API: Add `getServerState()` and `getServerContext()` (#65151) * Expose client and server context from provider * Create `getServerContext` * Add simple test for server context * Implement `getServerState` * Add tests for read-only state proxies * Add e2e tests for `getServerState()` * Avoid PHPCS UndefinedVariable error * Add e2e tests for `getServerContext` WIP * Finish e2e tests for `getServerContext()` * Update `getServerState()` tests * Revert "Add simple test for server context" This reverts commit 7e6f530a72dd9117946dd7e7fa9ab5c29f01d13d. * Update TSDocs Co-authored-by: DAreRodz Co-authored-by: luisherranz Co-authored-by: sethrubenstein Co-authored-by: michalczaplinski --- .../directive-priorities/view.js | 4 +- .../get-server-context/block.json | 15 ++ .../get-server-context/render.php | 51 +++++ .../get-server-context/view.asset.php | 9 + .../get-server-context/view.js | 46 ++++ .../get-server-state/block.json | 15 ++ .../get-server-state/render.php | 50 +++++ .../get-server-state/view.asset.php | 9 + .../get-server-state/view.js | 33 +++ packages/interactivity/src/directives.tsx | 43 ++-- packages/interactivity/src/hooks.tsx | 6 +- packages/interactivity/src/index.ts | 4 +- packages/interactivity/src/proxies/state.ts | 35 ++- .../src/proxies/test/state-proxy.ts | 199 +++++++++++++++++- packages/interactivity/src/scopes.ts | 44 ++++ packages/interactivity/src/store.ts | 35 +++ .../interactivity/get-sever-context.spec.ts | 166 +++++++++++++++ .../interactivity/get-sever-state.spec.ts | 119 +++++++++++ 18 files changed, 858 insertions(+), 25 deletions(-) create mode 100644 packages/e2e-tests/plugins/interactive-blocks/get-server-context/block.json create mode 100644 packages/e2e-tests/plugins/interactive-blocks/get-server-context/render.php create mode 100644 packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.asset.php create mode 100644 packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.js create mode 100644 packages/e2e-tests/plugins/interactive-blocks/get-server-state/block.json create mode 100644 packages/e2e-tests/plugins/interactive-blocks/get-server-state/render.php create mode 100644 packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.asset.php create mode 100644 packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.js create mode 100644 test/e2e/specs/interactivity/get-sever-context.spec.ts create mode 100644 test/e2e/specs/interactivity/get-sever-state.spec.ts diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-priorities/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-priorities/view.js index 5a46908f77d87..77f2f25c5f9a4 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-priorities/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-priorities/view.js @@ -41,13 +41,13 @@ directive( 'test-context', ( { context: { Provider }, props: { children } } ) => { executionProof( 'context' ); - const value = { + const client = { [ namespace ]: proxifyState( namespace, { attribute: 'from context', text: 'from context', } ), }; - return h( Provider, { value }, children ); + return h( Provider, { value: { client } }, children ); }, { priority: 8 } ); diff --git a/packages/e2e-tests/plugins/interactive-blocks/get-server-context/block.json b/packages/e2e-tests/plugins/interactive-blocks/get-server-context/block.json new file mode 100644 index 0000000000000..c635846328b9e --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/get-server-context/block.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "test/get-server-context", + "title": "E2E Interactivity tests - getServerContext", + "category": "text", + "icon": "heart", + "description": "", + "supports": { + "interactivity": true + }, + "textdomain": "e2e-interactivity", + "viewScriptModule": "file:./view.js", + "render": "file:./render.php" +} diff --git a/packages/e2e-tests/plugins/interactive-blocks/get-server-context/render.php b/packages/e2e-tests/plugins/interactive-blocks/get-server-context/render.php new file mode 100644 index 0000000000000..a71ced20dc46a --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/get-server-context/render.php @@ -0,0 +1,51 @@ + + + + +
+> +
+ > +
+
+
+
+
+
+ + +
+
diff --git a/packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.asset.php b/packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.asset.php new file mode 100644 index 0000000000000..bdaec8d1b67a9 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.asset.php @@ -0,0 +1,9 @@ + array( + '@wordpress/interactivity', + array( + 'id' => '@wordpress/interactivity-router', + 'import' => 'dynamic', + ), + ), +); diff --git a/packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.js b/packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.js new file mode 100644 index 0000000000000..83f016e2eac16 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.js @@ -0,0 +1,46 @@ +/** + * WordPress dependencies + */ +import { store, getContext, getServerContext } from '@wordpress/interactivity'; + +store( 'test/get-server-context', { + actions: { + *navigate( e ) { + e.preventDefault(); + const { actions } = yield import( + '@wordpress/interactivity-router' + ); + yield actions.navigate( e.target.href ); + }, + attemptModification() { + try { + getServerContext().prop = 'updated from client'; + getContext().result = 'unexpectedly modified ❌'; + } catch ( e ) { + getContext().result = 'not modified ✅'; + } + }, + }, + callbacks: { + updateServerContextParent() { + const ctx = getContext(); + const { prop, newProp, nested, inherited } = getServerContext(); + ctx.prop = prop; + ctx.newProp = newProp; + ctx.nested.prop = nested.prop; + ctx.nested.newProp = nested.newProp; + ctx.inherited.prop = inherited.prop; + ctx.inherited.newProp = inherited.newProp; + }, + updateServerContextChild() { + const ctx = getContext(); + const { prop, newProp, nested, inherited } = getServerContext(); + ctx.prop = prop; + ctx.newProp = newProp; + ctx.nested.prop = nested.prop; + ctx.nested.newProp = nested.newProp; + ctx.inherited.prop = inherited.prop; + ctx.inherited.newProp = inherited.newProp; + }, + }, +} ); diff --git a/packages/e2e-tests/plugins/interactive-blocks/get-server-state/block.json b/packages/e2e-tests/plugins/interactive-blocks/get-server-state/block.json new file mode 100644 index 0000000000000..abf76eb9beddc --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/get-server-state/block.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "test/get-server-state", + "title": "E2E Interactivity tests - getServerState", + "category": "text", + "icon": "heart", + "description": "", + "supports": { + "interactivity": true + }, + "textdomain": "e2e-interactivity", + "viewScriptModule": "file:./view.js", + "render": "file:./render.php" +} diff --git a/packages/e2e-tests/plugins/interactive-blocks/get-server-state/render.php b/packages/e2e-tests/plugins/interactive-blocks/get-server-state/render.php new file mode 100644 index 0000000000000..abc4efd8272d5 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/get-server-state/render.php @@ -0,0 +1,50 @@ + + +
+
+
+
+
+ + + + + +
diff --git a/packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.asset.php b/packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.asset.php new file mode 100644 index 0000000000000..bdaec8d1b67a9 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.asset.php @@ -0,0 +1,9 @@ + array( + '@wordpress/interactivity', + array( + 'id' => '@wordpress/interactivity-router', + 'import' => 'dynamic', + ), + ), +); diff --git a/packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.js b/packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.js new file mode 100644 index 0000000000000..db2992ec4a586 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.js @@ -0,0 +1,33 @@ +/** + * WordPress dependencies + */ +import { store, getServerState, getContext } from '@wordpress/interactivity'; + +const { state } = store( 'test/get-server-state', { + actions: { + *navigate( e ) { + e.preventDefault(); + const { actions } = yield import( + '@wordpress/interactivity-router' + ); + yield actions.navigate( e.target.href ); + }, + attemptModification() { + try { + getServerState().prop = 'updated from client'; + getContext().result = 'unexpectedly modified ❌'; + } catch ( e ) { + getContext().result = 'not modified ✅'; + } + }, + }, + callbacks: { + updateState() { + const { prop, newProp, nested } = getServerState(); + state.prop = prop; + state.newProp = newProp; + state.nested.prop = nested.prop; + state.nested.newProp = nested.newProp; + }, + }, +} ); diff --git a/packages/interactivity/src/directives.tsx b/packages/interactivity/src/directives.tsx index cde39d830499a..340880954683d 100644 --- a/packages/interactivity/src/directives.tsx +++ b/packages/interactivity/src/directives.tsx @@ -142,14 +142,19 @@ export default () => { const defaultEntry = context.find( ( { suffix } ) => suffix === 'default' ); - const inheritedValue = useContext( inheritedContext ); + const { client: inheritedClient, server: inheritedServer } = + useContext( inheritedContext ); const ns = defaultEntry!.namespace; - const currentValue = useRef( proxifyState( ns, {} ) ); + const client = useRef( proxifyState( ns, {} ) ); + const server = useRef( proxifyState( ns, {}, { readOnly: true } ) ); // No change should be made if `defaultEntry` does not exist. const contextStack = useMemo( () => { - const result = { ...inheritedValue }; + const result = { + client: { ...inheritedClient }, + server: { ...inheritedServer }, + }; if ( defaultEntry ) { const { namespace, value } = defaultEntry; // Check that the value is a JSON object. Send a console warning if not. @@ -159,17 +164,22 @@ export default () => { ); } deepMerge( - currentValue.current, + client.current, deepClone( value ) as object, false ); - result[ namespace ] = proxifyContext( - currentValue.current, - inheritedValue[ namespace ] + deepMerge( server.current, deepClone( value ) as object ); + result.client[ namespace ] = proxifyContext( + client.current, + inheritedClient[ namespace ] + ); + result.server[ namespace ] = proxifyContext( + server.current, + inheritedServer[ namespace ] ); } return result; - }, [ defaultEntry, inheritedValue ] ); + }, [ defaultEntry, inheritedClient, inheritedServer ] ); return createElement( Provider, { value: contextStack }, children ); }, @@ -563,17 +573,24 @@ export default () => { suffix === 'default' ? 'item' : kebabToCamelCase( suffix ); const itemContext = proxifyContext( proxifyState( namespace, {} ), - inheritedValue[ namespace ] + inheritedValue.client[ namespace ] ); const mergedContext = { - ...inheritedValue, - [ namespace ]: itemContext, + client: { + ...inheritedValue.client, + [ namespace ]: itemContext, + }, + server: { ...inheritedValue.server }, }; // Set the item after proxifying the context. - mergedContext[ namespace ][ itemProp ] = item; + mergedContext.client[ namespace ][ itemProp ] = item; - const scope = { ...getScope(), context: mergedContext }; + const scope = { + ...getScope(), + context: mergedContext.client, + serverContext: mergedContext.server, + }; const key = eachKey ? getEvaluate( { scope } )( eachKey[ 0 ] ) : item; diff --git a/packages/interactivity/src/hooks.tsx b/packages/interactivity/src/hooks.tsx index 215da8afef9b5..44dc2645da2c8 100644 --- a/packages/interactivity/src/hooks.tsx +++ b/packages/interactivity/src/hooks.tsx @@ -93,7 +93,7 @@ interface DirectivesProps { } // Main context. -const context = createContext< any >( {} ); +const context = createContext< any >( { client: {}, server: {} } ); // WordPress Directives. const directiveCallbacks: Record< string, DirectiveCallback > = {}; @@ -253,7 +253,9 @@ const Directives = ( { // element ref, state and props. const scope = useRef< Scope >( {} as Scope ).current; scope.evaluate = useCallback( getEvaluate( { scope } ), [] ); - scope.context = useContext( context ); + const { client, server } = useContext( context ); + scope.context = client; + scope.serverContext = server; /* eslint-disable react-hooks/rules-of-hooks */ scope.ref = previousScope?.ref || useRef( null ); /* eslint-enable react-hooks/rules-of-hooks */ diff --git a/packages/interactivity/src/index.ts b/packages/interactivity/src/index.ts index 336c2a97226db..9d013e4e744ed 100644 --- a/packages/interactivity/src/index.ts +++ b/packages/interactivity/src/index.ts @@ -16,8 +16,8 @@ import { getNamespace } from './namespaces'; import { parseServerData, populateServerData } from './store'; import { proxifyState } from './proxies'; -export { store, getConfig } from './store'; -export { getContext, getElement } from './scopes'; +export { store, getConfig, getServerState } from './store'; +export { getContext, getServerContext, getElement } from './scopes'; export { withScope, useWatch, diff --git a/packages/interactivity/src/proxies/state.ts b/packages/interactivity/src/proxies/state.ts index ec49c4b27c4ad..c91d8f6ab90a5 100644 --- a/packages/interactivity/src/proxies/state.ts +++ b/packages/interactivity/src/proxies/state.ts @@ -46,6 +46,8 @@ const proxyToProps: WeakMap< export const hasPropSignal = ( proxy: object, key: string ) => proxyToProps.has( proxy ) && proxyToProps.get( proxy )!.has( key ); +const readOnlyProxies = new WeakSet(); + /** * Returns the {@link PropSignal | `PropSignal`} instance associated with the * specified prop in the passed proxy. @@ -77,8 +79,11 @@ const getPropSignal = ( if ( get ) { prop.setGetter( get ); } else { + const readOnly = readOnlyProxies.has( proxy ); prop.setValue( - shouldProxy( value ) ? proxifyState( ns, value ) : value + shouldProxy( value ) + ? proxifyState( ns, value, { readOnly } ) + : value ); } } @@ -148,6 +153,9 @@ const stateHandlers: ProxyHandler< object > = { value: unknown, receiver: object ): boolean { + if ( readOnlyProxies.has( receiver ) ) { + return false; + } setNamespace( getNamespaceFromProxy( receiver ) ); try { return Reflect.set( target, key, value, receiver ); @@ -161,6 +169,10 @@ const stateHandlers: ProxyHandler< object > = { key: string, desc: PropertyDescriptor ): boolean { + if ( readOnlyProxies.has( getProxyFromObject( target )! ) ) { + return false; + } + const isNew = ! ( key in target ); const result = Reflect.defineProperty( target, key, desc ); @@ -199,6 +211,10 @@ const stateHandlers: ProxyHandler< object > = { }, deleteProperty( target: object, key: string ): boolean { + if ( readOnlyProxies.has( getProxyFromObject( target )! ) ) { + return false; + } + const result = Reflect.deleteProperty( target, key ); if ( result ) { @@ -230,8 +246,10 @@ const stateHandlers: ProxyHandler< object > = { * Returns the proxy associated with the given state object, creating it if it * does not exist. * - * @param namespace The namespace that will be associated to this proxy. - * @param obj The object to proxify. + * @param namespace The namespace that will be associated to this proxy. + * @param obj The object to proxify. + * @param options Options. + * @param options.readOnly Read-only. * * @throws Error if the object cannot be proxified. Use {@link shouldProxy} to * check if a proxy can be created for a specific object. @@ -240,8 +258,15 @@ const stateHandlers: ProxyHandler< object > = { */ export const proxifyState = < T extends object >( namespace: string, - obj: T -): T => createProxy( namespace, obj, stateHandlers ) as T; + obj: T, + options?: { readOnly?: boolean } +): T => { + const proxy = createProxy( namespace, obj, stateHandlers ) as T; + if ( options?.readOnly ) { + readOnlyProxies.add( proxy ); + } + return proxy; +}; /** * Reads the value of the specified property without subscribing to it. diff --git a/packages/interactivity/src/proxies/test/state-proxy.ts b/packages/interactivity/src/proxies/test/state-proxy.ts index 92500189fc830..4b0d2b0a708c3 100644 --- a/packages/interactivity/src/proxies/test/state-proxy.ts +++ b/packages/interactivity/src/proxies/test/state-proxy.ts @@ -9,7 +9,7 @@ import { effect } from '@preact/signals'; /** * Internal dependencies */ -import { proxifyState, peek } from '../'; +import { proxifyState, peek, deepMerge } from '../'; import { setScope, resetScope, getContext, getElement } from '../../scopes'; import { setNamespace, resetNamespace } from '../../namespaces'; @@ -1265,5 +1265,202 @@ describe( 'Interactivity API', () => { expect( x ).toBe( undefined ); } ); } ); + + describe( 'read-only', () => { + it( "should not allow modifying a prop's value", () => { + const readOnlyState = proxifyState( + 'test', + { prop: 'value', nested: { prop: 'value' } }, + { readOnly: true } + ); + + expect( () => { + readOnlyState.prop = 'new value'; + } ).toThrow(); + expect( () => { + readOnlyState.nested.prop = 'new value'; + } ).toThrow(); + } ); + + it( 'should not allow modifying a prop descriptor', () => { + const readOnlyState = proxifyState( + 'test', + { prop: 'value', nested: { prop: 'value' } }, + { readOnly: true } + ); + + expect( () => { + Object.defineProperty( readOnlyState, 'prop', { + get: () => 'value from getter', + writable: true, + enumerable: false, + } ); + } ).toThrow(); + expect( () => { + Object.defineProperty( readOnlyState.nested, 'prop', { + get: () => 'value from getter', + writable: true, + enumerable: false, + } ); + } ).toThrow(); + } ); + + it( 'should not allow adding new props', () => { + const readOnlyState = proxifyState< any >( + 'test', + { prop: 'value', nested: { prop: 'value' } }, + { readOnly: true } + ); + + expect( () => { + readOnlyState.newProp = 'value'; + } ).toThrow(); + expect( () => { + readOnlyState.nested.newProp = 'value'; + } ).toThrow(); + } ); + + it( 'should not allow removing props', () => { + const readOnlyState = proxifyState< any >( + 'test', + { prop: 'value', nested: { prop: 'value' } }, + { readOnly: true } + ); + + expect( () => { + delete readOnlyState.prop; + } ).toThrow(); + expect( () => { + delete readOnlyState.nested.prop; + } ).toThrow(); + } ); + + it( 'should not allow adding items to an array', () => { + const readOnlyState = proxifyState( + 'test', + { array: [ 1, 2, 3 ], nested: { array: [ 1, 2, 3 ] } }, + { readOnly: true } + ); + + expect( () => readOnlyState.array.push( 4 ) ).toThrow(); + expect( () => readOnlyState.nested.array.push( 4 ) ).toThrow(); + } ); + + it( 'should not allow removing items from an array', () => { + const readOnlyState = proxifyState( + 'test', + { array: [ 1, 2, 3 ], nested: { array: [ 1, 2, 3 ] } }, + { readOnly: true } + ); + + expect( () => readOnlyState.array.pop() ).toThrow(); + expect( () => readOnlyState.nested.array.pop() ).toThrow(); + } ); + + it( 'should allow subscribing to prop changes', () => { + const readOnlyState = proxifyState( + 'test', + { + prop: 'value', + nested: { prop: 'value' }, + }, + { readOnly: true } + ); + + const spy1 = jest.fn( () => readOnlyState.prop ); + const spy2 = jest.fn( () => readOnlyState.nested.prop ); + + effect( spy1 ); + effect( spy2 ); + expect( spy1 ).toHaveBeenCalledTimes( 1 ); + expect( spy2 ).toHaveBeenCalledTimes( 1 ); + expect( spy1 ).toHaveLastReturnedWith( 'value' ); + expect( spy2 ).toHaveLastReturnedWith( 'value' ); + + deepMerge( readOnlyState, { prop: 'new value' } ); + + expect( spy1 ).toHaveBeenCalledTimes( 2 ); + expect( spy2 ).toHaveBeenCalledTimes( 1 ); + expect( spy1 ).toHaveLastReturnedWith( 'new value' ); + expect( spy2 ).toHaveLastReturnedWith( 'value' ); + + deepMerge( readOnlyState, { nested: { prop: 'new value' } } ); + + expect( spy1 ).toHaveBeenCalledTimes( 2 ); + expect( spy2 ).toHaveBeenCalledTimes( 2 ); + expect( spy1 ).toHaveLastReturnedWith( 'new value' ); + expect( spy2 ).toHaveLastReturnedWith( 'new value' ); + } ); + + it( 'should allow subscribing to new props', () => { + const readOnlyState = proxifyState< any >( + 'test', + { + prop: 'value', + nested: { prop: 'value' }, + }, + { readOnly: true } + ); + + const spy1 = jest.fn( () => readOnlyState.newProp ); + const spy2 = jest.fn( () => readOnlyState.nested.newProp ); + + effect( spy1 ); + effect( spy2 ); + expect( spy1 ).toHaveBeenCalledTimes( 1 ); + expect( spy2 ).toHaveBeenCalledTimes( 1 ); + expect( spy1 ).toHaveLastReturnedWith( undefined ); + expect( spy2 ).toHaveLastReturnedWith( undefined ); + + deepMerge( readOnlyState, { newProp: 'value' } ); + + expect( spy1 ).toHaveBeenCalledTimes( 2 ); + expect( spy2 ).toHaveBeenCalledTimes( 1 ); + expect( spy1 ).toHaveLastReturnedWith( 'value' ); + expect( spy2 ).toHaveLastReturnedWith( undefined ); + + deepMerge( readOnlyState, { nested: { newProp: 'value' } } ); + + expect( spy1 ).toHaveBeenCalledTimes( 2 ); + expect( spy2 ).toHaveBeenCalledTimes( 2 ); + expect( spy1 ).toHaveLastReturnedWith( 'value' ); + expect( spy2 ).toHaveLastReturnedWith( 'value' ); + } ); + + it( 'should allow subscribing to array changes', () => { + const readOnlyState = proxifyState< any >( + 'test', + { + array: [ 1, 2, 3 ], + nested: { array: [ 1, 2, 3 ] }, + }, + { readOnly: true } + ); + + const spy1 = jest.fn( () => readOnlyState.array[ 0 ] ); + const spy2 = jest.fn( () => readOnlyState.nested.array[ 0 ] ); + + effect( spy1 ); + effect( spy2 ); + expect( spy1 ).toHaveBeenCalledTimes( 1 ); + expect( spy2 ).toHaveBeenCalledTimes( 1 ); + expect( spy1 ).toHaveLastReturnedWith( 1 ); + expect( spy2 ).toHaveLastReturnedWith( 1 ); + + deepMerge( readOnlyState, { array: [ 4, 5, 6 ] } ); + + expect( spy1 ).toHaveBeenCalledTimes( 2 ); + expect( spy2 ).toHaveBeenCalledTimes( 1 ); + expect( spy1 ).toHaveLastReturnedWith( 4 ); + expect( spy2 ).toHaveLastReturnedWith( 1 ); + + deepMerge( readOnlyState, { nested: { array: [] } } ); + + expect( spy1 ).toHaveBeenCalledTimes( 2 ); + expect( spy2 ).toHaveBeenCalledTimes( 2 ); + expect( spy1 ).toHaveLastReturnedWith( 4 ); + expect( spy2 ).toHaveLastReturnedWith( undefined ); + } ); + } ); } ); } ); diff --git a/packages/interactivity/src/scopes.ts b/packages/interactivity/src/scopes.ts index 2e78755ec4bbe..722305f6bee11 100644 --- a/packages/interactivity/src/scopes.ts +++ b/packages/interactivity/src/scopes.ts @@ -12,6 +12,7 @@ import type { Evaluate } from './hooks'; export interface Scope { evaluate: Evaluate; context: object; + serverContext: object; ref: RefObject< HTMLElement >; attributes: createElement.JSX.HTMLAttributes; } @@ -96,3 +97,46 @@ export const getElement = () => { attributes: deepImmutable( attributes ), } ); }; + +/** + * Retrieves the part of the inherited context defined and updated from the + * server. + * + * The object returned is read-only, and includes the context defined in PHP + * with `wp_interactivity_data_wp_context()`, including the corresponding + * inherited properties. When `actions.navigate()` is called, this object is + * updated to reflect the changes in the new visited page, without affecting the + * context returned by `getContext()`. Directives can subscribe to those changes + * to update the context if needed. + * + * @example + * ```js + * store('...', { + * callbacks: { + * updateServerContext() { + * const context = getContext(); + * const serverContext = getServerContext(); + * // Override some property with the new value that came from the server. + * context.overridableProp = serverContext.overridableProp; + * }, + * }, + * }); + * ``` + * + * @param namespace Store namespace. By default, the namespace where the calling + * function exists is used. + * @return The server context content. + */ +export const getServerContext = < T extends object >( + namespace?: string +): T => { + const scope = getScope(); + if ( globalThis.SCRIPT_DEBUG ) { + if ( ! scope ) { + throw Error( + 'Cannot call `getServerContext()` when there is no scope. If you are using an async function, please consider using a generator instead. If you are using some sort of async callbacks, like `setTimeout`, please wrap the callback with `withScope(callback)`.' + ); + } + } + return scope.serverContext[ namespace || getNamespace() ]; +}; diff --git a/packages/interactivity/src/store.ts b/packages/interactivity/src/store.ts index c74764b902e19..b1ad07459c62c 100644 --- a/packages/interactivity/src/store.ts +++ b/packages/interactivity/src/store.ts @@ -12,6 +12,7 @@ export const stores = new Map(); const rawStores = new Map(); const storeLocks = new Map(); const storeConfigs = new Map(); +const serverStates = new Map(); /** * Get the defined config for the store with the passed namespace. @@ -22,6 +23,39 @@ const storeConfigs = new Map(); export const getConfig = ( namespace?: string ) => storeConfigs.get( namespace || getNamespace() ) || {}; +/** + * Get the part of the state defined and updated from the server. + * + * The object returned is read-only, and includes the state defined in PHP with + * `wp_interactivity_state()`. When using `actions.navigate()`, this object is + * updated to reflect the changes in its properites, without affecting the state + * returned by `store()`. Directives can subscribe to those changes to update + * the state if needed. + * + * @example + * ```js + * const { state } = store('myStore', { + * callbacks: { + * updateServerState() { + * const serverState = getServerState(); + * // Override some property with the new value that came from the server. + * state.overridableProp = serverState.overridableProp; + * }, + * }, + * }); + * ``` + * + * @param namespace Store's namespace from which to retrieve the server state. + * @return The server state for the given namespace. + */ +export const getServerState = ( namespace?: string ) => { + const ns = namespace || getNamespace(); + if ( ! serverStates.has( ns ) ) { + serverStates.set( ns, proxifyState( ns, {}, { readOnly: true } ) ); + } + return serverStates.get( ns ); +}; + interface StoreOptions { /** * Property to block/unblock private store namespaces. @@ -187,6 +221,7 @@ export const populateServerData = ( data?: { Object.entries( data!.state ).forEach( ( [ namespace, state ] ) => { const st = store< any >( namespace, {}, { lock: universalUnlock } ); deepMerge( st.state, state, false ); + deepMerge( getServerState( namespace ), state ); } ); } if ( isPlainObject( data?.config ) ) { diff --git a/test/e2e/specs/interactivity/get-sever-context.spec.ts b/test/e2e/specs/interactivity/get-sever-context.spec.ts new file mode 100644 index 0000000000000..d7bc4075f9760 --- /dev/null +++ b/test/e2e/specs/interactivity/get-sever-context.spec.ts @@ -0,0 +1,166 @@ +/** + * Internal dependencies + */ +import { test, expect } from './fixtures'; + +test.describe( 'getServerContext()', () => { + test.beforeAll( async ( { interactivityUtils: utils } ) => { + const parent = { + prop: 'parent', + nested: { + prop: 'parent', + }, + inherited: { + prop: 'parent', + }, + }; + + const parentModified = { + prop: 'parentModified', + nested: { + prop: 'parentModified', + }, + inherited: { + prop: 'parentModified', + }, + }; + + const parentNewProps = { + prop: 'parent', + newProp: 'parent', + nested: { + prop: 'parent', + newProp: 'parent', + }, + inherited: { + prop: 'parent', + newProp: 'parent', + }, + }; + + const child = { + prop: 'child', + nested: { + prop: 'child', + }, + }; + + const childModified = { + prop: 'childModified', + nested: { + prop: 'childModified', + }, + }; + + const childNewProps = { + prop: 'child', + newProp: 'child', + nested: { + prop: 'child', + newProp: 'child', + }, + }; + + await utils.activatePlugins(); + const link1 = await utils.addPostWithBlock( 'test/get-server-context', { + alias: 'getServerContext() - modified', + attributes: { + parentContext: parentModified, + childContext: childModified, + }, + } ); + const link2 = await utils.addPostWithBlock( 'test/get-server-context', { + alias: 'getServerContext() - new props', + attributes: { + parentContext: parentNewProps, + childContext: childNewProps, + }, + } ); + await utils.addPostWithBlock( 'test/get-server-context', { + alias: 'getServerContext() - main', + attributes: { + links: { modified: link1, newProps: link2 }, + parentContext: parent, + childContext: child, + }, + } ); + } ); + + test.beforeEach( async ( { interactivityUtils: utils, page } ) => { + await page.goto( utils.getLink( 'getServerContext() - main' ) ); + } ); + + test.afterAll( async ( { interactivityUtils: utils } ) => { + await utils.deactivatePlugins(); + await utils.deleteAllPosts(); + } ); + + test( 'should update modified props on navigation', async ( { page } ) => { + const prop = page.getByTestId( 'prop' ); + const nestedProp = page.getByTestId( 'nested.prop' ); + const inheritedProp = page.getByTestId( 'inherited.prop' ); + + await expect( prop ).toHaveText( 'child' ); + await expect( nestedProp ).toHaveText( 'child' ); + await expect( inheritedProp ).toHaveText( 'parent' ); + + await page.getByTestId( 'modified' ).click(); + + await expect( prop ).toHaveText( 'childModified' ); + await expect( nestedProp ).toHaveText( 'childModified' ); + await expect( inheritedProp ).toHaveText( 'parentModified' ); + + await page.goBack(); + + await expect( prop ).toHaveText( 'child' ); + await expect( nestedProp ).toHaveText( 'child' ); + await expect( inheritedProp ).toHaveText( 'parent' ); + } ); + + test( 'should add new props on navigation', async ( { page } ) => { + const newProp = page.getByTestId( 'newProp' ); + const nestedNewProp = page.getByTestId( 'nested.newProp' ); + const inheritedNewProp = page.getByTestId( 'inherited.newProp' ); + + await expect( newProp ).toBeEmpty(); + await expect( nestedNewProp ).toBeEmpty(); + await expect( inheritedNewProp ).toBeEmpty(); + + await page.getByTestId( 'newProps' ).click(); + + await expect( newProp ).toHaveText( 'child' ); + await expect( nestedNewProp ).toHaveText( 'child' ); + await expect( inheritedNewProp ).toHaveText( 'parent' ); + } ); + + test( 'should keep new props on navigation', async ( { page } ) => { + const newProp = page.getByTestId( 'newProp' ); + const nestedNewProp = page.getByTestId( 'nested.newProp' ); + const inheritedNewProp = page.getByTestId( 'inherited.newProp' ); + + await page.getByTestId( 'newProps' ).click(); + + await expect( newProp ).toHaveText( 'child' ); + await expect( nestedNewProp ).toHaveText( 'child' ); + await expect( inheritedNewProp ).toHaveText( 'parent' ); + + await page.goBack(); + + await expect( newProp ).toHaveText( 'child' ); + await expect( nestedNewProp ).toHaveText( 'child' ); + await expect( inheritedNewProp ).toHaveText( 'parent' ); + } ); + + test( 'should prevent any manual modifications', async ( { page } ) => { + const prop = page.getByTestId( 'prop' ); + const button = page.getByTestId( 'tryToModifyServerContext' ); + + await expect( prop ).toHaveText( 'child' ); + await expect( button ).toHaveText( 'modify' ); + + await button.click(); + + await expect( prop ).toHaveText( 'child' ); + await expect( button ).toHaveText( 'not modified ✅' ); + } ); +} ); diff --git a/test/e2e/specs/interactivity/get-sever-state.spec.ts b/test/e2e/specs/interactivity/get-sever-state.spec.ts new file mode 100644 index 0000000000000..16406c1d82446 --- /dev/null +++ b/test/e2e/specs/interactivity/get-sever-state.spec.ts @@ -0,0 +1,119 @@ +/** + * Internal dependencies + */ +import { test, expect } from './fixtures'; + +test.describe( 'getServerState()', () => { + test.beforeAll( async ( { interactivityUtils: utils } ) => { + await utils.activatePlugins(); + const link1 = await utils.addPostWithBlock( 'test/get-server-state', { + alias: 'getServerState() - link 1', + attributes: { + state: { + prop: 'link 1', + newProp: 'link 1', + nested: { + prop: 'link 1', + newProp: 'link 1', + }, + }, + }, + } ); + const link2 = await utils.addPostWithBlock( 'test/get-server-state', { + alias: 'getServerState() - link 2', + attributes: { + state: { + prop: 'link 2', + newProp: 'link 2', + nested: { + prop: 'link 2', + newProp: 'link 2', + }, + }, + }, + } ); + await utils.addPostWithBlock( 'test/get-server-state', { + alias: 'getServerState() - main', + attributes: { + title: 'Main', + links: [ link1, link2 ], + state: { + prop: 'main', + nested: { + prop: 'main', + }, + }, + }, + } ); + } ); + + test.beforeEach( async ( { interactivityUtils: utils, page } ) => { + await page.goto( utils.getLink( 'getServerState() - main' ) ); + } ); + + test.afterAll( async ( { interactivityUtils: utils } ) => { + await utils.deactivatePlugins(); + await utils.deleteAllPosts(); + } ); + + test( 'should update existing state props on navigation', async ( { + page, + } ) => { + const prop = page.getByTestId( 'prop' ); + const nestedProp = page.getByTestId( 'nested.prop' ); + + await expect( prop ).toHaveText( 'main' ); + await expect( nestedProp ).toHaveText( 'main' ); + + await page.getByTestId( 'link 1' ).click(); + + await expect( prop ).toHaveText( 'link 1' ); + await expect( nestedProp ).toHaveText( 'link 1' ); + + await page.goBack(); + await expect( prop ).toHaveText( 'main' ); + await expect( nestedProp ).toHaveText( 'main' ); + + await page.getByTestId( 'link 2' ).click(); + + await expect( prop ).toHaveText( 'link 2' ); + await expect( nestedProp ).toHaveText( 'link 2' ); + } ); + + test( 'should add new state props and keep them on navigation', async ( { + page, + } ) => { + const newProp = page.getByTestId( 'newProp' ); + const nestedNewProp = page.getByTestId( 'nested.newProp' ); + + await expect( newProp ).toBeEmpty(); + await expect( nestedNewProp ).toBeEmpty(); + + await page.getByTestId( 'link 1' ).click(); + + await expect( newProp ).toHaveText( 'link 1' ); + await expect( nestedNewProp ).toHaveText( 'link 1' ); + + await page.goBack(); + await expect( newProp ).toHaveText( 'link 1' ); + await expect( nestedNewProp ).toHaveText( 'link 1' ); + + await page.getByTestId( 'link 2' ).click(); + + await expect( newProp ).toHaveText( 'link 2' ); + await expect( nestedNewProp ).toHaveText( 'link 2' ); + } ); + + test( 'should prevent any manual modifications', async ( { page } ) => { + const prop = page.getByTestId( 'prop' ); + const button = page.getByTestId( 'tryToModifyServerState' ); + + await expect( prop ).toHaveText( 'main' ); + await expect( button ).toHaveText( 'modify' ); + + await button.click(); + + await expect( prop ).toHaveText( 'main' ); + await expect( button ).toHaveText( 'not modified ✅' ); + } ); +} ); From 02e0ff3528c8a161c0e2c91df463ddafd3969490 Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Thu, 19 Sep 2024 13:18:29 +0300 Subject: [PATCH 02/15] Components: Cleanup unused ToggleGroupControl config values (#65456) Co-authored-by: tyxla Co-authored-by: ciampo --- .../toggle-group-control-option-base/styles.ts | 2 +- packages/components/src/utils/config-values.js | 16 +--------------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts b/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts index 86efc5224077f..020468991225c 100644 --- a/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts +++ b/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts @@ -70,7 +70,7 @@ export const buttonView = ( { } &:active { - background: ${ CONFIG.toggleGroupControlBackgroundColor }; + background: ${ CONFIG.controlBackgroundColor }; } ${ isDeselectable && deselectable } diff --git a/packages/components/src/utils/config-values.js b/packages/components/src/utils/config-values.js index 2040f479a231c..1bc3945f9b3b1 100644 --- a/packages/components/src/utils/config-values.js +++ b/packages/components/src/utils/config-values.js @@ -7,18 +7,13 @@ import { COLORS } from './colors-values'; const CONTROL_HEIGHT = '36px'; const CONTROL_PROPS = { - controlSurfaceColor: COLORS.white, - controlTextActiveColor: COLORS.theme.accent, - // These values should be shared with TextControl. controlPaddingX: 12, controlPaddingXSmall: 8, controlPaddingXLarge: 12 * 1.3334, // TODO: Deprecate controlBackgroundColor: COLORS.white, - controlBoxShadow: 'transparent', controlBoxShadowFocus: `0 0 0 0.5px ${ COLORS.theme.accent }`, - controlDestructiveBorderColor: COLORS.alert.red, controlHeight: CONTROL_HEIGHT, controlHeightXSmall: `calc( ${ CONTROL_HEIGHT } * 0.6 )`, controlHeightSmall: `calc( ${ CONTROL_HEIGHT } * 0.8 )`, @@ -26,18 +21,9 @@ const CONTROL_PROPS = { controlHeightXLarge: `calc( ${ CONTROL_HEIGHT } * 1.4 )`, }; -const TOGGLE_GROUP_CONTROL_PROPS = { - toggleGroupControlBackgroundColor: CONTROL_PROPS.controlBackgroundColor, - toggleGroupControlBorderColor: COLORS.ui.border, - toggleGroupControlBackdropBackgroundColor: - CONTROL_PROPS.controlSurfaceColor, - toggleGroupControlBackdropBorderColor: COLORS.ui.border, - toggleGroupControlButtonColorActive: CONTROL_PROPS.controlBackgroundColor, -}; - // Using Object.assign to avoid creating circular references when emitting // TypeScript type declarations. -export default Object.assign( {}, CONTROL_PROPS, TOGGLE_GROUP_CONTROL_PROPS, { +export default Object.assign( {}, CONTROL_PROPS, { colorDivider: 'rgba(0, 0, 0, 0.1)', colorScrollbarThumb: 'rgba(0, 0, 0, 0.2)', colorScrollbarThumbHover: 'rgba(0, 0, 0, 0.5)', From e68646a0503d32807a152f2f494e295676259d45 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 19 Sep 2024 12:28:10 +0200 Subject: [PATCH 03/15] Babel preset: Add missing files to package (#65481) Fix an issue where some files are not included in the published package: @wordpress/babel-preset-default@8.8.0 --- Co-authored-by: sirreal Co-authored-by: noisysocks Co-authored-by: gziolo Co-authored-by: sgomes --- packages/babel-preset-default/CHANGELOG.md | 12 ++++++++++-- packages/babel-preset-default/package.json | 4 +++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/babel-preset-default/CHANGELOG.md b/packages/babel-preset-default/CHANGELOG.md index c8c3fdb66ecb0..b31be6ffd8d56 100644 --- a/packages/babel-preset-default/CHANGELOG.md +++ b/packages/babel-preset-default/CHANGELOG.md @@ -1,8 +1,16 @@ -## Internal +## Unreleased -- Added `addPolyfillComments` option. When used, it will automatically add magic comments to mark files that need `wp-polyfill`. +### Bug Fixes + +- Fix a bug in 8.8.1 due to missing files in the published package ([#65481](https://github.com/WordPress/gutenberg/pull/65481)). + +## 8.8.0 (2024-09-19) + +### Internal + +- Added `addPolyfillComments` option. When used, it will automatically add magic comments to mark files that need `wp-polyfill` ([#65292](https://github.com/WordPress/gutenberg/pull/65292)). ## 8.7.0 (2024-09-05) diff --git a/packages/babel-preset-default/package.json b/packages/babel-preset-default/package.json index f0f015cb2203f..1203586ec2029 100644 --- a/packages/babel-preset-default/package.json +++ b/packages/babel-preset-default/package.json @@ -26,7 +26,9 @@ }, "files": [ "build", - "index.js" + "index.js", + "polyfill-exclusions.js", + "replace-polyfills.js" ], "main": "index.js", "dependencies": { From f8dffcf2e82365078b2251ae8383404d632b6abf Mon Sep 17 00:00:00 2001 From: Andrei Draganescu Date: Thu, 19 Sep 2024 13:35:35 +0300 Subject: [PATCH 04/15] Handle zoom out when changing device preview (#65444) --- packages/editor/src/components/preview-dropdown/index.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/components/preview-dropdown/index.js b/packages/editor/src/components/preview-dropdown/index.js index ecc5bc610a302..38565f4b04abe 100644 --- a/packages/editor/src/components/preview-dropdown/index.js +++ b/packages/editor/src/components/preview-dropdown/index.js @@ -26,6 +26,7 @@ import { ActionItem } from '@wordpress/interface'; * Internal dependencies */ import { store as editorStore } from '../../store'; +import { store as blockEditorStore } from '@wordpress/block-editor'; import PostPreviewButton from '../post-preview-button'; export default function PreviewDropdown( { forceIsAutosaveable, disabled } ) { @@ -44,6 +45,12 @@ export default function PreviewDropdown( { forceIsAutosaveable, disabled } ) { }; }, [] ); const { setDeviceType } = useDispatch( editorStore ); + const { __unstableSetEditorMode } = useDispatch( blockEditorStore ); + + const handleDevicePreviewChange = ( newDeviceType ) => { + setDeviceType( newDeviceType ); + __unstableSetEditorMode( 'edit' ); + }; const isMobile = useViewportMatch( 'medium', '<' ); if ( isMobile ) { @@ -113,7 +120,7 @@ export default function PreviewDropdown( { forceIsAutosaveable, disabled } ) { { isTemplate && ( From d912271fb57a171936613ad8e1543ae9878c3495 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Thu, 19 Sep 2024 13:08:39 +0200 Subject: [PATCH 05/15] Search block: reset size correctly when clearing unit control (#65468) * Search block: reset size correctly when clearing unit control * Make empty string handling more explicit --- Co-authored-by: ciampo Co-authored-by: mirka <0mirka00@git.wordpress.org> --- packages/block-library/src/search/edit.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/block-library/src/search/edit.js b/packages/block-library/src/search/edit.js index e2f3bb3999e42..d4ed5b7e3a405 100644 --- a/packages/block-library/src/search/edit.js +++ b/packages/block-library/src/search/edit.js @@ -424,13 +424,12 @@ export default function SearchEdit( { } step={ 1 } onChange={ ( newWidth ) => { - const filteredWidth = - widthUnit === '%' && - parseInt( newWidth, 10 ) > 100 - ? 100 - : newWidth; + const parsedNewWidth = + newWidth === '' + ? undefined + : parseInt( newWidth, 10 ); setAttributes( { - width: parseInt( filteredWidth, 10 ), + width: parsedNewWidth, } ); } } onUnitChange={ ( newUnit ) => { @@ -566,7 +565,11 @@ export default function SearchEdit( { Date: Thu, 19 Sep 2024 14:32:37 +0200 Subject: [PATCH 06/15] Make the Settings panel toggle button show its keyboard shortcut in its tooltip (#65322) * Make the Settings panel toggle button show its keyboard shortcut in its tooltip. * Do not pass shortcut prop to PluginSidebar and simplify. Co-authored-by: afercia Co-authored-by: youknowriad --- packages/editor/src/components/plugin-sidebar/index.js | 7 +------ .../src/components/complementary-area-toggle/index.js | 4 ++++ .../interface/src/components/complementary-area/index.js | 1 + 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/editor/src/components/plugin-sidebar/index.js b/packages/editor/src/components/plugin-sidebar/index.js index b9c0177e30fc4..56a954cadffb6 100644 --- a/packages/editor/src/components/plugin-sidebar/index.js +++ b/packages/editor/src/components/plugin-sidebar/index.js @@ -3,7 +3,6 @@ */ import { useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; -import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; import { ComplementaryArea } from '@wordpress/interface'; /** @@ -77,12 +76,9 @@ import { store as editorStore } from '../../store'; * ``` */ export default function PluginSidebar( { className, ...props } ) { - const { postTitle, shortcut } = useSelect( ( select ) => { + const { postTitle } = useSelect( ( select ) => { return { postTitle: select( editorStore ).getEditedPostAttribute( 'title' ), - shortcut: select( - keyboardShortcutsStore - ).getShortcutRepresentation( 'core/editor/toggle-sidebar' ), }; }, [] ); return ( @@ -91,7 +87,6 @@ export default function PluginSidebar( { className, ...props } ) { className="editor-sidebar" smallScreenTitle={ postTitle || __( '(no title)' ) } scope="core" - toggleShortcut={ shortcut } { ...props } /> ); diff --git a/packages/interface/src/components/complementary-area-toggle/index.js b/packages/interface/src/components/complementary-area-toggle/index.js index b6690b7df5fc5..15f9e48d71083 100644 --- a/packages/interface/src/components/complementary-area-toggle/index.js +++ b/packages/interface/src/components/complementary-area-toggle/index.js @@ -17,6 +17,7 @@ function ComplementaryAreaToggle( { icon, selectedIcon, name, + shortcut, ...props } ) { const ComponentToUse = as; @@ -26,8 +27,10 @@ function ComplementaryAreaToggle( { identifier, [ identifier, scope ] ); + const { enableComplementaryArea, disableComplementaryArea } = useDispatch( interfaceStore ); + return ( ); diff --git a/packages/interface/src/components/complementary-area/index.js b/packages/interface/src/components/complementary-area/index.js index 363a6ee9dea76..d9fa8e71acb23 100644 --- a/packages/interface/src/components/complementary-area/index.js +++ b/packages/interface/src/components/complementary-area/index.js @@ -275,6 +275,7 @@ function ComplementaryArea( { showTooltip={ ! showIconLabels } variant={ showIconLabels ? 'tertiary' : undefined } size="compact" + shortcut={ toggleShortcut } /> ) } From 2205e9e9f8aae8a2cfd542f7d57296e651dc12cb Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Thu, 19 Sep 2024 13:39:43 +0100 Subject: [PATCH 07/15] Make sections contentOnly in Zoom Out (#65396) --- packages/block-editor/src/store/selectors.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 30fdb76bdbe78..5780b2afe2a67 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -2926,6 +2926,7 @@ export const getBlockEditingMode = createRegistrySelector( if ( clientId === null ) { clientId = ''; } + // In zoom-out mode, override the behavior set by // __unstableSetBlockEditingMode to only allow editing the top-level // sections. @@ -2943,9 +2944,13 @@ export const getBlockEditingMode = createRegistrySelector( state, sectionRootClientId ); - if ( ! sectionsClientIds?.includes( clientId ) ) { - return 'disabled'; + + // Sections are always contentOnly. + if ( sectionsClientIds?.includes( clientId ) ) { + return 'contentOnly'; } + + return 'disabled'; } const blockEditingMode = state.blockEditingModes.get( clientId ); From 2ddd1145ff8674796532188b2e2fc8883071f387 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Thu, 19 Sep 2024 14:22:42 +0100 Subject: [PATCH 08/15] Hide block transforms in contentOnly mode for non-content blocks (#65394) * Hide transforms in contentOnly * Apply only to non-content blocks Co-authored-by: getdave Co-authored-by: andrewserong --- .../block-variation-transforms/index.js | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/block-editor/src/components/block-variation-transforms/index.js b/packages/block-editor/src/components/block-variation-transforms/index.js index 97a3f98054184..8f29effb14e67 100644 --- a/packages/block-editor/src/components/block-variation-transforms/index.js +++ b/packages/block-editor/src/components/block-variation-transforms/index.js @@ -140,19 +140,30 @@ function VariationsToggleGroupControl( { function __experimentalBlockVariationTransforms( { blockClientId } ) { const { updateBlockAttributes } = useDispatch( blockEditorStore ); - const { activeBlockVariation, variations } = useSelect( + const { activeBlockVariation, variations, isContentOnly } = useSelect( ( select ) => { - const { getActiveBlockVariation, getBlockVariations } = - select( blocksStore ); - const { getBlockName, getBlockAttributes } = + const { + getActiveBlockVariation, + getBlockVariations, + __experimentalHasContentRoleAttribute, + } = select( blocksStore ); + const { getBlockName, getBlockAttributes, getBlockEditingMode } = select( blockEditorStore ); + const name = blockClientId && getBlockName( blockClientId ); + + const isContentBlock = + __experimentalHasContentRoleAttribute( name ); + return { activeBlockVariation: getActiveBlockVariation( name, getBlockAttributes( blockClientId ) ), variations: name && getBlockVariations( name, 'transform' ), + isContentOnly: + getBlockEditingMode( blockClientId ) === 'contentOnly' && + ! isContentBlock, }; }, [ blockClientId ] @@ -181,8 +192,7 @@ function __experimentalBlockVariationTransforms( { blockClientId } ) { } ); }; - // Skip rendering if there are no variations - if ( ! variations?.length ) { + if ( ! variations?.length || isContentOnly ) { return null; } From 56bfef82bb93d2bba226e0631aed03eb16057cfd Mon Sep 17 00:00:00 2001 From: Dani Guardiola Date: Thu, 19 Sep 2024 15:37:29 +0200 Subject: [PATCH 09/15] Remove `useEvent` from components package (#65388) * Replace imports of `useEvent` with `@wordpress/compose` * Remove `useEvent` from components package Co-authored-by: DaniGuardiola Co-authored-by: tyxla --- packages/components/src/utils/element-rect.ts | 6 +-- .../components/src/utils/hooks/use-event.ts | 38 ------------------- .../src/utils/hooks/use-on-value-update.ts | 5 +-- 3 files changed, 2 insertions(+), 47 deletions(-) delete mode 100644 packages/components/src/utils/hooks/use-event.ts diff --git a/packages/components/src/utils/element-rect.ts b/packages/components/src/utils/element-rect.ts index 550ec35b0bc93..a96c25ecfac94 100644 --- a/packages/components/src/utils/element-rect.ts +++ b/packages/components/src/utils/element-rect.ts @@ -3,11 +3,7 @@ * WordPress dependencies */ import { useLayoutEffect, useRef, useState } from '@wordpress/element'; -import { useResizeObserver } from '@wordpress/compose'; -/** - * Internal dependencies - */ -import { useEvent } from './hooks/use-event'; +import { useEvent, useResizeObserver } from '@wordpress/compose'; /** * The position and dimensions of an element, relative to its offset parent. diff --git a/packages/components/src/utils/hooks/use-event.ts b/packages/components/src/utils/hooks/use-event.ts deleted file mode 100644 index eefac9478a8b4..0000000000000 --- a/packages/components/src/utils/hooks/use-event.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* eslint-disable jsdoc/require-param */ -/** - * WordPress dependencies - */ -import { useRef, useInsertionEffect, useCallback } from '@wordpress/element'; - -/** - * Any function. - */ -export type AnyFunction = ( ...args: any ) => any; - -/** - * Creates a stable callback function that has access to the latest state and - * can be used within event handlers and effect callbacks. Throws when used in - * the render phase. - * - * @example - * - * ```tsx - * function Component(props) { - * const onClick = useEvent(props.onClick); - * React.useEffect(() => {}, [onClick]); - * } - * ``` - */ -export function useEvent< T extends AnyFunction >( callback?: T ) { - const ref = useRef< AnyFunction | undefined >( () => { - throw new Error( 'Cannot call an event handler while rendering.' ); - } ); - useInsertionEffect( () => { - ref.current = callback; - } ); - return useCallback< AnyFunction >( - ( ...args ) => ref.current?.( ...args ), - [] - ) as T; -} -/* eslint-enable jsdoc/require-param */ diff --git a/packages/components/src/utils/hooks/use-on-value-update.ts b/packages/components/src/utils/hooks/use-on-value-update.ts index 5726f3977daf0..05c7173d092fa 100644 --- a/packages/components/src/utils/hooks/use-on-value-update.ts +++ b/packages/components/src/utils/hooks/use-on-value-update.ts @@ -2,11 +2,8 @@ /** * WordPress dependencies */ +import { useEvent } from '@wordpress/compose'; import { useRef, useEffect } from '@wordpress/element'; -/** - * Internal dependencies - */ -import { useEvent } from './use-event'; /** * Context object for the `onUpdate` callback of `useOnValueUpdate`. From 3910eab3e8db99a4e049a838549d908f22e2ac95 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Thu, 19 Sep 2024 16:59:56 +0200 Subject: [PATCH 10/15] Dataviews config dropdown: remove style overrides (#65373) * Remove duplicate classname * Use `DropdownContentWrapper` and avoid style overrides * Extract popover props to const object * Move Dropdown under DataviewsViewConfigDropdown component * Add aria-expanded and aria-controls attributes to the dropdown toggle * Sort imports * Move DATAVIEWS_CONFIG_POPOVER_PROPS declaration at the top of the file * Do not memoize popover props --- Co-authored-by: ciampo Co-authored-by: stokesman Co-authored-by: tyxla --- .../dataviews-view-config/index.tsx | 92 +++++++++++-------- .../dataviews-view-config/style.scss | 13 +-- 2 files changed, 57 insertions(+), 48 deletions(-) diff --git a/packages/dataviews/src/components/dataviews-view-config/index.tsx b/packages/dataviews/src/components/dataviews-view-config/index.tsx index 48fdf6906b077..02e81b2b0913d 100644 --- a/packages/dataviews/src/components/dataviews-view-config/index.tsx +++ b/packages/dataviews/src/components/dataviews-view-config/index.tsx @@ -8,6 +8,7 @@ import type { ChangeEvent } from 'react'; */ import { Button, + __experimentalDropdownContentWrapper as DropdownContentWrapper, Dropdown, __experimentalToggleGroupControl as ToggleGroupControl, __experimentalToggleGroupControlOption as ToggleGroupControlOption, @@ -27,6 +28,7 @@ import { __, _x, sprintf } from '@wordpress/i18n'; import { memo, useContext, useMemo } from '@wordpress/element'; import { chevronDown, chevronUp, cog, seen, unseen } from '@wordpress/icons'; import warning from '@wordpress/warning'; +import { useInstanceId } from '@wordpress/compose'; /** * Internal dependencies @@ -55,6 +57,8 @@ interface ViewTypeMenuProps { defaultLayouts?: SupportedLayouts; } +const DATAVIEWS_CONFIG_POPOVER_PROPS = { placement: 'bottom-end', offset: 9 }; + function ViewTypeMenu( { defaultLayouts = { list: {}, grid: {}, table: {} }, }: ViewTypeMenuProps ) { @@ -510,7 +514,7 @@ function SettingsSection( { ); } -function DataviewsViewConfigContent( { +function DataviewsViewConfigDropdown( { density, setDensity, }: { @@ -518,25 +522,52 @@ function DataviewsViewConfigContent( { setDensity: React.Dispatch< React.SetStateAction< number > >; } ) { const { view } = useContext( DataViewsContext ); + const popoverId = useInstanceId( + _DataViewsViewConfig, + 'dataviews-view-config-dropdown' + ); + return ( - - - - - - - { view.type === LAYOUT_GRID && ( - { + return ( + - - ) } - - - ); -} - -export default forwardRef( BlockSelectionButton ); diff --git a/packages/block-editor/src/components/block-tools/block-toolbar-breadcrumb.js b/packages/block-editor/src/components/block-tools/block-toolbar-breadcrumb.js deleted file mode 100644 index ae03bdb4f5164..0000000000000 --- a/packages/block-editor/src/components/block-tools/block-toolbar-breadcrumb.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * External dependencies - */ -import clsx from 'clsx'; - -/** - * WordPress dependencies - */ -import { forwardRef } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import BlockSelectionButton from './block-selection-button'; -import { PrivateBlockPopover } from '../block-popover'; -import useBlockToolbarPopoverProps from './use-block-toolbar-popover-props'; -import useSelectedBlockToolProps from './use-selected-block-tool-props'; - -function BlockToolbarBreadcrumb( { clientId, __unstableContentRef }, ref ) { - const { - capturingClientId, - isInsertionPointVisible, - lastClientId, - rootClientId, - } = useSelectedBlockToolProps( clientId ); - - const popoverProps = useBlockToolbarPopoverProps( { - contentElement: __unstableContentRef?.current, - clientId, - } ); - - return ( - - - - ); -} - -export default forwardRef( BlockToolbarBreadcrumb ); diff --git a/packages/block-editor/src/components/block-tools/index.js b/packages/block-editor/src/components/block-tools/index.js index 24f60dbbf970a..099323925384b 100644 --- a/packages/block-editor/src/components/block-tools/index.js +++ b/packages/block-editor/src/components/block-tools/index.js @@ -19,7 +19,6 @@ import { default as InsertionPoint, } from './insertion-point'; import BlockToolbarPopover from './block-toolbar-popover'; -import BlockToolbarBreadcrumb from './block-toolbar-breadcrumb'; import ZoomOutPopover from './zoom-out-popover'; import { store as blockEditorStore } from '../../store'; import usePopoverScroll from '../block-popover/use-popover-scroll'; @@ -78,7 +77,6 @@ export default function BlockTools( { const { getGroupingBlockName } = useSelect( blocksStore ); const { showEmptyBlockSideInserter, - showBreadcrumb, showBlockToolbarPopover, showZoomOutToolbar, } = useShowBlockTools(); @@ -223,14 +221,6 @@ export default function BlockTools( { /> ) } - { showBreadcrumb && ( - - ) } - { showZoomOutToolbar && ( { const { @@ -48,7 +47,6 @@ function InbetweenInsertionPointPopover( { getPreviousBlockClientId, getNextBlockClientId, getSettings, - isNavigationMode: _isNavigationMode, __unstableGetEditorMode, } = select( blockEditorStore ); const insertionPoint = getBlockInsertionPoint(); @@ -78,7 +76,6 @@ function InbetweenInsertionPointPopover( { getBlockListSettings( insertionPoint.rootClientId ) ?.orientation || 'vertical', rootClientId: insertionPoint.rootClientId, - isNavigationMode: _isNavigationMode(), isDistractionFree: settings.isDistractionFree, isInserterShown: insertionPoint?.__unstableWithInserter, isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', @@ -144,7 +141,7 @@ function InbetweenInsertionPointPopover( { }, }; - if ( isDistractionFree && ! isNavigationMode ) { + if ( isDistractionFree ) { return null; } diff --git a/packages/block-editor/src/components/block-tools/style.scss b/packages/block-editor/src/components/block-tools/style.scss index 9f1325d7f95a1..a3d9153273e98 100644 --- a/packages/block-editor/src/components/block-tools/style.scss +++ b/packages/block-editor/src/components/block-tools/style.scss @@ -84,84 +84,6 @@ } } -/** - * Block Label for Navigation/Selection Mode - */ - -.block-editor-block-list__block-selection-button { - display: inline-flex; - padding: 0 $grid-unit-15; - z-index: z-index(".block-editor-block-list__block-selection-button"); - - // Dark block UI appearance. - border-radius: $radius-small; - background-color: $gray-900; - - font-size: $default-font-size; - height: $block-toolbar-height; - - .block-editor-block-list__block-selection-button__content { - margin: auto; - display: inline-flex; - align-items: center; - - > .components-flex__item { - margin-right: $grid-unit-15 * 0.5; - } - } - .components-button.has-icon.block-selection-button_drag-handle { - cursor: grab; - padding: 0; - height: $grid-unit-30; - min-width: $grid-unit-30; - margin-left: -2px; - - // Drag handle is smaller than the others. - svg { - min-width: 18px; - min-height: 18px; - } - } - - .block-editor-block-icon { - font-size: $default-font-size; - color: $white; - height: $block-toolbar-height; - } - - // The button here has a special style to appear as a toolbar. - .components-button { - min-width: $button-size; - color: $white; - height: $block-toolbar-height; - - // When button is focused, it receives a box-shadow instead of the border. - &:focus { - box-shadow: none; - border: none; - } - - &:active { - color: $white; - } - - // Make sure the button has no hover style when it's disabled. - &[aria-disabled="true"]:hover { - color: $white; - } - - display: flex; - } - .block-selection-button_select-button.components-button { - padding: 0; - } - - .block-editor-block-mover { - background: unset; - border: none; - } -} - // Hide the popover block editor list while dragging. // Using a hacky animation to delay hiding the element. // It's needed because if we hide the element immediately upon dragging, @@ -178,14 +100,10 @@ .components-popover.block-editor-block-list__block-popover { // Position the block toolbar. - .block-editor-block-list__block-selection-button, .block-editor-block-contextual-toolbar { pointer-events: all; margin-top: $grid-unit-10; margin-bottom: $grid-unit-10; - } - - .block-editor-block-contextual-toolbar { border: $border-width solid $gray-900; border-radius: $radius-small; overflow: visible; // allow the parent selector to be visible diff --git a/packages/block-editor/src/components/block-tools/use-show-block-tools.js b/packages/block-editor/src/components/block-tools/use-show-block-tools.js index 07e0ebd16a64b..02a8f0583bcdd 100644 --- a/packages/block-editor/src/components/block-tools/use-show-block-tools.js +++ b/packages/block-editor/src/components/block-tools/use-show-block-tools.js @@ -22,7 +22,6 @@ export function useShowBlockTools() { getBlock, getBlockMode, getSettings, - hasMultiSelection, __unstableGetEditorMode, isTyping, } = select( blockEditorStore ); @@ -42,29 +41,20 @@ export function useShowBlockTools() { ! isTyping() && editorMode === 'edit' && isEmptyDefaultBlock; - const maybeShowBreadcrumb = - hasSelectedBlock && - ! hasMultiSelection() && - editorMode === 'navigation'; - const isZoomOut = editorMode === 'zoom-out'; const _showZoomOutToolbar = isZoomOut && block?.attributes?.align === 'full' && - ! _showEmptyBlockSideInserter && - ! maybeShowBreadcrumb; + ! _showEmptyBlockSideInserter; const _showBlockToolbarPopover = ! _showZoomOutToolbar && ! getSettings().hasFixedToolbar && ! _showEmptyBlockSideInserter && hasSelectedBlock && - ! isEmptyDefaultBlock && - ! maybeShowBreadcrumb; + ! isEmptyDefaultBlock; return { showEmptyBlockSideInserter: _showEmptyBlockSideInserter, - showBreadcrumb: - ! _showEmptyBlockSideInserter && maybeShowBreadcrumb, showBlockToolbarPopover: _showBlockToolbarPopover, showZoomOutToolbar: _showZoomOutToolbar, }; diff --git a/packages/block-editor/src/components/block-tools/zoom-out-toolbar.js b/packages/block-editor/src/components/block-tools/zoom-out-toolbar.js index a3c46c4b4c970..560bfc6ebb58d 100644 --- a/packages/block-editor/src/components/block-tools/zoom-out-toolbar.js +++ b/packages/block-editor/src/components/block-tools/zoom-out-toolbar.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import clsx from 'clsx'; - /** * WordPress dependencies */ @@ -26,7 +21,6 @@ export default function ZoomOutToolbar( { clientId, __unstableContentRef } ) { ( select ) => { const { getBlock, - hasBlockMovingClientId, getNextBlockClientId, getPreviousBlockClientId, canRemoveBlock, @@ -62,7 +56,6 @@ export default function ZoomOutToolbar( { clientId, __unstableContentRef } ) { } return { - blockMovingMode: hasBlockMovingClientId(), isBlockTemplatePart, isNextBlockTemplatePart, isPrevBlockTemplatePart, @@ -75,7 +68,6 @@ export default function ZoomOutToolbar( { clientId, __unstableContentRef } ) { ); const { - blockMovingMode, isBlockTemplatePart, isNextBlockTemplatePart, isPrevBlockTemplatePart, @@ -87,15 +79,11 @@ export default function ZoomOutToolbar( { clientId, __unstableContentRef } ) { const { removeBlock, __unstableSetEditorMode } = useDispatch( blockEditorStore ); - const classNames = clsx( 'zoom-out-toolbar', { - 'is-block-moving-mode': !! blockMovingMode, - } ); - const showBlockDraggable = canMove && ! isBlockTemplatePart; return ( { const { getBlockName, - isBlockSelected, - hasSelectedInnerBlock, __unstableGetEditorMode, getTemplateLock, getBlockRootClientId, getBlockEditingMode, getBlockSettings, - isDragging, getSectionRootClientId, } = unlock( select( blockEditorStore ) ); let _isDropZoneDisabled; @@ -213,8 +210,6 @@ export function useInnerBlocksProps( props = {}, options = {} ) { const { hasBlockSupport, getBlockType } = select( blocksStore ); const blockName = getBlockName( clientId ); - const enableClickThrough = - __unstableGetEditorMode() === 'navigation'; const blockEditingMode = getBlockEditingMode( clientId ); const parentClientId = getBlockRootClientId( clientId ); const [ defaultLayout ] = getBlockSettings( clientId, 'layout' ); @@ -236,12 +231,6 @@ export function useInnerBlocksProps( props = {}, options = {} ) { '__experimentalExposeControlsToChildren', false ), - hasOverlay: - blockName !== 'core/template' && - ! isBlockSelected( clientId ) && - ! hasSelectedInnerBlock( clientId, true ) && - enableClickThrough && - ! isDragging(), name: blockName, blockType: getBlockType( blockName ), parentLock: getTemplateLock( parentClientId ), @@ -254,7 +243,6 @@ export function useInnerBlocksProps( props = {}, options = {} ) { ); const { __experimentalCaptureToolbars, - hasOverlay, name, blockType, parentLock, @@ -299,10 +287,7 @@ export function useInnerBlocksProps( props = {}, options = {} ) { className: clsx( props.className, 'block-editor-block-list__layout', - __unstableDisableLayoutClassNames ? '' : layoutClassNames, - { - 'has-overlay': hasOverlay, - } + __unstableDisableLayoutClassNames ? '' : layoutClassNames ), children: clientId ? ( diff --git a/packages/block-editor/src/components/list-view/block-contents.js b/packages/block-editor/src/components/list-view/block-contents.js index 91bfbd7eddaa0..0ed2b162b127b 100644 --- a/packages/block-editor/src/components/list-view/block-contents.js +++ b/packages/block-editor/src/components/list-view/block-contents.js @@ -1,12 +1,6 @@ -/** - * External dependencies - */ -import clsx from 'clsx'; - /** * WordPress dependencies */ -import { useSelect } from '@wordpress/data'; import { forwardRef } from '@wordpress/element'; /** @@ -14,7 +8,6 @@ import { forwardRef } from '@wordpress/element'; */ import ListViewBlockSelectButton from './block-select-button'; import BlockDraggable from '../block-draggable'; -import { store as blockEditorStore } from '../../store'; import { useListViewContext } from './context'; const ListViewBlockContents = forwardRef( @@ -34,29 +27,9 @@ const ListViewBlockContents = forwardRef( ref ) => { const { clientId } = block; - - const { blockMovingClientId, selectedBlockInBlockEditor } = useSelect( - ( select ) => { - const { hasBlockMovingClientId, getSelectedBlockClientId } = - select( blockEditorStore ); - return { - blockMovingClientId: hasBlockMovingClientId(), - selectedBlockInBlockEditor: getSelectedBlockClientId(), - }; - }, - [] - ); - const { AdditionalBlockContent, insertedBlock, setInsertedBlock } = useListViewContext(); - const isBlockMoveTarget = - blockMovingClientId && selectedBlockInBlockEditor === clientId; - - const className = clsx( 'block-editor-list-view-block-contents', { - 'is-dropping-before': isBlockMoveTarget, - } ); - // Only include all selected blocks if the currently clicked on block // is one of the selected blocks. This ensures that if a user attempts // to drag a block that isn't part of the selection, they're still able @@ -82,7 +55,7 @@ const ListViewBlockContents = forwardRef( { ( { draggable, onDragStart, onDragEnd } ) => ( ( <> - + - - { __( 'Edit' ) } + { selectIcon } + { __( 'Design' ) } ), + info: __( + 'Full control over layout and styling.' + ), }, { value: 'navigation', label: ( <> - { selectIcon } - { __( 'Select' ) } + + { __( 'Edit' ) } ), + info: __( 'Focus on content.' ), }, ] } /> diff --git a/packages/block-editor/src/components/tool-selector/style.scss b/packages/block-editor/src/components/tool-selector/style.scss index 03774fe0f6b9d..07ca91d346d90 100644 --- a/packages/block-editor/src/components/tool-selector/style.scss +++ b/packages/block-editor/src/components/tool-selector/style.scss @@ -8,3 +8,8 @@ color: $gray-700; min-width: 280px; } + +.block-editor-tool-selector__menu .components-menu-item__info { + margin-left: $grid-unit-30 + $grid-unit-15; // icon size + margin + text-align: left; +} diff --git a/packages/block-editor/src/components/use-block-commands/index.js b/packages/block-editor/src/components/use-block-commands/index.js index 8a09999ecdff3..c88ec4e537892 100644 --- a/packages/block-editor/src/components/use-block-commands/index.js +++ b/packages/block-editor/src/components/use-block-commands/index.js @@ -16,7 +16,6 @@ import { plus as add, group, ungroup, - moveTo as move, } from '@wordpress/icons'; /** @@ -127,59 +126,6 @@ export const useTransformCommands = () => { return { isLoading: false, commands }; }; -const useActionsCommands = () => { - const { clientIds } = useSelect( ( select ) => { - const { getSelectedBlockClientIds } = select( blockEditorStore ); - const selectedBlockClientIds = getSelectedBlockClientIds(); - - return { - clientIds: selectedBlockClientIds, - }; - }, [] ); - - const { getBlockRootClientId, canMoveBlocks, getBlockCount } = - useSelect( blockEditorStore ); - - const { setBlockMovingClientId, setNavigationMode, selectBlock } = - useDispatch( blockEditorStore ); - - if ( ! clientIds || clientIds.length < 1 ) { - return { isLoading: false, commands: [] }; - } - - const rootClientId = getBlockRootClientId( clientIds[ 0 ] ); - - const canMove = - canMoveBlocks( clientIds ) && getBlockCount( rootClientId ) !== 1; - - const commands = []; - - if ( canMove ) { - commands.push( { - name: 'move-to', - label: __( 'Move to' ), - callback: () => { - setNavigationMode( true ); - selectBlock( clientIds[ 0 ] ); - setBlockMovingClientId( clientIds[ 0 ] ); - }, - icon: move, - } ); - } - - return { - isLoading: false, - commands: commands.map( ( command ) => ( { - ...command, - name: 'core/block-editor/action-' + command.name, - callback: ( { close } ) => { - command.callback(); - close(); - }, - } ) ), - }; -}; - const useQuickActionsCommands = () => { const { clientIds, isUngroupable, isGroupable } = useSelect( ( select ) => { const { @@ -344,10 +290,6 @@ export const useBlockCommands = () => { name: 'core/block-editor/blockTransforms', hook: useTransformCommands, } ); - useCommandLoader( { - name: 'core/block-editor/blockActions', - hook: useActionsCommands, - } ); useCommandLoader( { name: 'core/block-editor/blockQuickActions', hook: useQuickActionsCommands, diff --git a/packages/block-editor/src/components/writing-flow/use-tab-nav.js b/packages/block-editor/src/components/writing-flow/use-tab-nav.js index b321d7c8d2995..3788c7021fd66 100644 --- a/packages/block-editor/src/components/writing-flow/use-tab-nav.js +++ b/packages/block-editor/src/components/writing-flow/use-tab-nav.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { focus, isFormElement } from '@wordpress/dom'; -import { TAB, ESCAPE } from '@wordpress/keycodes'; +import { TAB } from '@wordpress/keycodes'; import { useSelect, useDispatch } from '@wordpress/data'; import { useRefEffect, useMergeRefs } from '@wordpress/compose'; import { useRef } from '@wordpress/element'; @@ -21,19 +21,9 @@ export default function useTabNav() { const { hasMultiSelection, getSelectedBlockClientId, getBlockCount } = useSelect( blockEditorStore ); - const { setNavigationMode, setLastFocus } = unlock( - useDispatch( blockEditorStore ) - ); - const isNavigationMode = useSelect( - ( select ) => select( blockEditorStore ).isNavigationMode(), - [] - ); - + const { setLastFocus } = unlock( useDispatch( blockEditorStore ) ); const { getLastFocus } = unlock( useSelect( blockEditorStore ) ); - // Don't allow tabbing to this element in Navigation mode. - const focusCaptureTabIndex = ! isNavigationMode ? '0' : undefined; - // Reference that holds the a flag for enabling or disabling // capturing on the focus capture elements. const noCaptureRef = useRef(); @@ -56,8 +46,6 @@ export default function useTabNav() { .focus(); } } else { - setNavigationMode( true ); - const canvasElement = container.current.ownerDocument === event.target.ownerDocument ? container.current @@ -82,7 +70,7 @@ export default function useTabNav() { const before = (
); @@ -90,7 +78,7 @@ export default function useTabNav() { const after = (
); @@ -101,12 +89,6 @@ export default function useTabNav() { return; } - if ( event.keyCode === ESCAPE && ! hasMultiSelection() ) { - event.preventDefault(); - setNavigationMode( true ); - return; - } - // In Edit mode, Tab should focus the first tabbable element after // the content, which is normally the sidebar (with block controls) // and Shift+Tab should focus the first tabbable element before the @@ -119,20 +101,6 @@ export default function useTabNav() { const isShift = event.shiftKey; const direction = isShift ? 'findPrevious' : 'findNext'; - - if ( ! hasMultiSelection() && ! getSelectedBlockClientId() ) { - // Preserve the behaviour of entering navigation mode when - // tabbing into the content without a block selection. - // `onFocusCapture` already did this previously, but we need to - // do it again here because after clearing block selection, - // focus land on the writing flow container and pressing Tab - // will no longer send focus through the focus capture element. - if ( event.target === node ) { - setNavigationMode( true ); - } - return; - } - const nextTabbable = focus.tabbable[ direction ]( event.target ); // We want to constrain the tabbing to the block and its child blocks. diff --git a/packages/block-editor/src/content.scss b/packages/block-editor/src/content.scss index 36d428dca6b76..1ef4e118fb1bb 100644 --- a/packages/block-editor/src/content.scss +++ b/packages/block-editor/src/content.scss @@ -8,7 +8,6 @@ @import "./components/button-block-appender/content.scss"; @import "./components/default-block-appender/content.scss"; @import "./components/iframe/content.scss"; -@import "./components/inner-blocks/content.scss"; @import "./components/media-placeholder/content.scss"; @import "./components/plain-text/content.scss"; @import "./components/rich-text/content.scss"; diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index e91f997ca6783..ee11838395ec5 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -1728,23 +1728,24 @@ export const __unstableSetEditorMode = }; /** - * Action that enables or disables the block moving mode. + * Set the block moving client ID. * - * @param {string|null} hasBlockMovingClientId Enable/Disable block moving mode. + * @deprecated + * + * @return {Object} Action object. */ -export const setBlockMovingClientId = - ( hasBlockMovingClientId = null ) => - ( { dispatch } ) => { - dispatch( { type: 'SET_BLOCK_MOVING_MODE', hasBlockMovingClientId } ); - - if ( hasBlockMovingClientId ) { - speak( - __( - 'Use the Tab key and Arrow keys to choose new block location. Use Left and Right Arrow keys to move between nesting levels. Once location is selected press Enter or Space to move the block.' - ) - ); +export function setBlockMovingClientId() { + deprecated( + 'wp.data.dispatch( "core/block-editor" ).setBlockMovingClientId', + { + since: '6.7', + hint: 'Block moving mode feature has been removed', } + ); + return { + type: 'DO_NOTHING', }; +} /** * Action that duplicates a list of blocks. diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index 01ad8f69febc9..0e77e8e2ed433 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -15,6 +15,7 @@ import { getBlockName, getTemplateLock, getClientIdsWithDescendants, + isNavigationMode, } from './selectors'; import { checkAllowListRecursive, @@ -479,19 +480,74 @@ export const getContentLockingParent = createSelector( ( state, clientId ) => { let current = clientId; let result; - while ( ( current = state.blocks.parents.get( current ) ) ) { - if ( - getBlockName( state, current ) === 'core/block' || - getTemplateLock( state, current ) === 'contentOnly' - ) { + while ( + ! result && + ( current = state.blocks.parents.get( current ) ) + ) { + if ( getTemplateLock( state, current ) === 'contentOnly' ) { result = current; } } return result; }, - ( state ) => [ state.blocks.parents, state.blockListSettings ] + ( state ) => [ + state.blocks.parents, + state.blockListSettings, + state.settings.templateLock, + ] ); +/** + * Retrieves the client ID of the parent section block. + * + * @param {Object} state Global application state. + * @param {Object} clientId Client Id of the block. + * + * @return {?string} Client ID of the ancestor block that is content locking the block. + */ +export const getParentSectionBlock = createSelector( + ( state, clientId ) => { + let current = clientId; + let result; + while ( + ! result && + ( current = state.blocks.parents.get( current ) ) + ) { + if ( isSectionBlock( state, current ) ) { + result = current; + } + } + return result; + }, + ( state ) => [ + state.blocks.parents, + state.blocks.order, + state.blockListSettings, + state.editorMode, + state.settings.templateLock, + state.blocks.byClientId, + getSectionRootClientId( state ), + ] +); + +/** + * Retrieves the client ID is a content locking parent + * + * @param {Object} state Global application state. + * @param {Object} clientId Client Id of the block. + * + * @return {boolean} Whether the block is a content locking parent. + */ +export function isSectionBlock( state, clientId ) { + const sectionRootClientId = getSectionRootClientId( state ); + const sectionClientIds = getBlockOrder( state, sectionRootClientId ); + return ( + getBlockName( state, clientId ) === 'core/block' || + getTemplateLock( state, clientId ) === 'contentOnly' || + ( isNavigationMode( state ) && sectionClientIds.includes( clientId ) ) + ); +} + /** * Retrieves the client ID of the block that is content locked but is * currently being temporarily edited as a non-locked block. diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index cd4569c45e580..3a99d18ef17ee 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1795,11 +1795,6 @@ export const blockListSettings = ( state = {}, action ) => { * @return {string} Updated state. */ export function editorMode( state = 'edit', action ) { - // Let inserting block in navigation mode always trigger Edit mode. - if ( action.type === 'INSERT_BLOCKS' && state === 'navigation' ) { - return 'edit'; - } - if ( action.type === 'SET_EDITOR_MODE' ) { return action.mode; } @@ -1807,26 +1802,6 @@ export function editorMode( state = 'edit', action ) { return state; } -/** - * Reducer returning whether the block moving mode is enabled or not. - * - * @param {string|null} state Current state. - * @param {Object} action Dispatched action. - * - * @return {string|null} Updated state. - */ -export function hasBlockMovingClientId( state = null, action ) { - if ( action.type === 'SET_BLOCK_MOVING_MODE' ) { - return action.hasBlockMovingClientId; - } - - if ( action.type === 'SET_EDITOR_MODE' ) { - return null; - } - - return state; -} - /** * Reducer return an updated state representing the most recent block attribute * update. The state is structured as an object where the keys represent the @@ -2104,7 +2079,6 @@ const combinedReducers = combineReducers( { lastBlockAttributesChange, lastFocus, editorMode, - hasBlockMovingClientId, expandedBlock, highlightedBlock, lastBlockInserted, diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 5780b2afe2a67..806e2b5f5993e 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -38,6 +38,8 @@ import { getTemporarilyEditingAsBlocks, getTemporarilyEditingFocusModeToRevert, getSectionRootClientId, + isSectionBlock, + getParentSectionBlock, } from './private-selectors'; /** @@ -1582,6 +1584,11 @@ const canInsertBlockTypeUnmemoized = ( return false; } + const _isSectionBlock = !! isSectionBlock( state, rootClientId ); + if ( _isSectionBlock ) { + return false; + } + if ( getBlockEditingMode( state, rootClientId ?? '' ) === 'disabled' ) { return false; } @@ -1733,6 +1740,11 @@ export function canRemoveBlock( state, clientId ) { return false; } + const isBlockWithinSection = !! getParentSectionBlock( state, clientId ); + if ( isBlockWithinSection ) { + return false; + } + return getBlockEditingMode( state, rootClientId ) !== 'disabled'; } @@ -2674,12 +2686,17 @@ export function __unstableGetEditorMode( state ) { /** * Returns whether block moving mode is enabled. * - * @param {Object} state Editor state. - * - * @return {string} Client Id of moving block. + * @deprecated */ -export function hasBlockMovingClientId( state ) { - return state.hasBlockMovingClientId; +export function hasBlockMovingClientId() { + deprecated( + 'wp.data.select( "core/block-editor" ).hasBlockMovingClientId', + { + since: '6.7', + hint: 'Block moving mode feature has been removed', + } + ); + return false; } /** @@ -2862,11 +2879,9 @@ export function __unstableHasActiveBlockOverlayActive( state, clientId ) { '__experimentalDisableBlockOverlay', false ); - const shouldEnableIfUnselected = - editorMode === 'navigation' || - ( blockSupportDisable - ? false - : areInnerBlocksControlled( state, clientId ) ); + const shouldEnableIfUnselected = blockSupportDisable + ? false + : areInnerBlocksControlled( state, clientId ); return ( shouldEnableIfUnselected && @@ -2960,9 +2975,29 @@ export const getBlockEditingMode = createRegistrySelector( if ( ! clientId ) { return 'default'; } + const sectionRootClientId = getSectionRootClientId( state ); + if ( + editorMode === 'navigation' && + clientId === sectionRootClientId + ) { + return 'default'; + } + const sectionsClientIds = getBlockOrder( + state, + sectionRootClientId + ); const rootClientId = getBlockRootClientId( state, clientId ); const templateLock = getTemplateLock( state, rootClientId ); - if ( templateLock === 'contentOnly' ) { + if ( + templateLock === 'contentOnly' || + editorMode === 'navigation' + ) { + // Sections should always be contentOnly in navigation mode. + // This will also cause them to display in List View providing + // a structure. + if ( sectionsClientIds.includes( clientId ) ) { + return 'contentOnly'; + } const name = getBlockName( state, clientId ); const isContent = select( blocksStore ).__experimentalHasContentRoleAttribute( diff --git a/packages/block-editor/src/store/test/private-selectors.js b/packages/block-editor/src/store/test/private-selectors.js index 45432b750bb9e..5f427e79cf699 100644 --- a/packages/block-editor/src/store/test/private-selectors.js +++ b/packages/block-editor/src/store/test/private-selectors.js @@ -394,6 +394,10 @@ describe( 'private selectors', () => { parents: new Map( [ [ '6cf70164-9097-4460-bcbf-200560546988', '' ], ] ), + order: new Map( [ + [ '6cf70164-9097-4460-bcbf-200560546988', [] ], + [ '', [ '6cf70164-9097-4460-bcbf-200560546988' ] ], + ] ), }, blockEditingModes: new Map(), }; @@ -424,6 +428,21 @@ describe( 'private selectors', () => { 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', ], ] ), + + order: new Map( [ + [ + 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', + [ + '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', + 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', + ], + ], + [ + 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', + [ '4c2b7140-fffd-44b4-b2a7-820c670a6514' ], + ], + [ '', [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337' ] ], + ] ), }, blockEditingModes: new Map( [ [ '', 'disabled' ], @@ -461,6 +480,21 @@ describe( 'private selectors', () => { 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', ], ] ), + order: new Map( [ + [ + 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', + [ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f' ], + ], + [ + '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', + [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c' ], + ], + [ + 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', + [ '4c2b7140-fffd-44b4-b2a7-820c670a6514' ], + ], + [ '', [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337' ] ], + ] ), }, blockEditingModes: new Map( [ [ '', 'disabled' ], diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 85006621c4701..04899a9ee243f 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -2694,6 +2694,7 @@ describe( 'selectors', () => { byClientId: new Map(), attributes: new Map(), parents: new Map(), + order: new Map(), }, blockListSettings: {}, settings: { @@ -2711,6 +2712,7 @@ describe( 'selectors', () => { blocks: { byClientId: new Map(), attributes: new Map(), + order: new Map(), }, blockListSettings: {}, settings: { @@ -2728,6 +2730,7 @@ describe( 'selectors', () => { byClientId: new Map(), attributes: new Map(), parents: new Map(), + order: new Map(), }, blockListSettings: {}, settings: {}, @@ -2748,6 +2751,7 @@ describe( 'selectors', () => { byClientId: new Map(), attributes: new Map(), parents: new Map(), + order: new Map(), }, blockListSettings: {}, settings: {}, @@ -2772,6 +2776,7 @@ describe( 'selectors', () => { } ) ), parents: new Map(), + order: new Map(), }, blockListSettings: {}, settings: {}, @@ -2796,6 +2801,7 @@ describe( 'selectors', () => { } ) ), parents: new Map(), + order: new Map(), }, blockListSettings: { block1: {}, @@ -2822,6 +2828,7 @@ describe( 'selectors', () => { } ) ), parents: new Map(), + order: new Map(), }, blockListSettings: { block1: {}, @@ -2848,6 +2855,7 @@ describe( 'selectors', () => { } ) ), parents: new Map(), + order: new Map(), }, blockListSettings: { block1: { @@ -2876,6 +2884,7 @@ describe( 'selectors', () => { } ) ), parents: new Map(), + order: new Map(), }, blockListSettings: { block1: { @@ -2904,6 +2913,7 @@ describe( 'selectors', () => { } ) ), parents: new Map(), + order: new Map(), }, blockListSettings: {}, settings: {}, @@ -2932,6 +2942,7 @@ describe( 'selectors', () => { } ) ), parents: new Map(), + order: new Map(), }, blockListSettings: { block1: { @@ -2960,6 +2971,7 @@ describe( 'selectors', () => { } ) ), parents: new Map(), + order: new Map(), }, blockListSettings: {}, settings: {}, @@ -2976,6 +2988,7 @@ describe( 'selectors', () => { byClientId: new Map(), attributes: new Map(), parents: new Map(), + order: new Map(), }, blockListSettings: {}, settings: {}, @@ -2992,7 +3005,7 @@ describe( 'selectors', () => { byClientId: new Map( Object.entries( { block1: { name: 'core/test-block-ancestor' }, - block2: { name: 'core/block' }, + block2: { name: 'core/block1' }, } ) ), attributes: new Map( @@ -3006,6 +3019,10 @@ describe( 'selectors', () => { block2: 'block1', } ) ), + order: new Map( [ + [ '', [ 'block1' ] ], + [ 'block1', [ 'block2' ] ], + ] ), }, blockListSettings: { block1: {}, @@ -3023,6 +3040,37 @@ describe( 'selectors', () => { ).toBe( true ); } ); + it( 'should prevent blocks from being inserted within sections', () => { + const state = { + blocks: { + byClientId: new Map( + Object.entries( { + block1: { name: 'core/block' }, // reusable blocks are always sections. + } ) + ), + attributes: new Map( + Object.entries( { + block1: {}, + } ) + ), + parents: new Map( + Object.entries( { + block1: '', + } ) + ), + order: new Map( [ [ '', [ 'block1' ] ] ] ), + }, + blockListSettings: { + block1: {}, + }, + settings: {}, + blockEditingModes: new Map(), + }; + expect( + canInsertBlockType( state, 'core/test-block-a', 'block1' ) + ).toBe( false ); + } ); + it( 'should allow blocks to be inserted if both parent and ancestor restrictions are met', () => { const state = { blocks: { @@ -3046,6 +3094,11 @@ describe( 'selectors', () => { block3: 'block2', } ) ), + order: new Map( [ + [ '', [ 'block1' ] ], + [ 'block1', [ 'block2' ] ], + [ 'block2', [ 'block3' ] ], + ] ), }, blockListSettings: { block1: {}, @@ -3086,6 +3139,11 @@ describe( 'selectors', () => { block3: 'block2', } ) ), + order: new Map( [ + [ '', [ 'block1' ] ], + [ 'block1', [ 'block2' ] ], + [ 'block2', [ 'block3' ] ], + ] ), }, blockListSettings: { block1: {}, @@ -3126,6 +3184,11 @@ describe( 'selectors', () => { block3: 'block2', } ) ), + order: new Map( [ + [ '', [ 'block1' ] ], + [ 'block1', [ 'block2' ] ], + [ 'block2', [ 'block3' ] ], + ] ), }, blockListSettings: { block1: {}, @@ -3159,11 +3222,14 @@ describe( 'selectors', () => { block2: {}, } ) ), - parents: new Map( - Object.entries( { - block2: 'block1', - } ) - ), + order: new Map( [ + [ '', [ 'block1' ] ], + [ 'block1', [ 'block2' ] ], + ] ), + parents: new Map( [ + [ 'block2', 'block1' ], + [ 'block1', '' ], + ] ), }, blockListSettings: { block1: {}, @@ -3203,6 +3269,10 @@ describe( 'selectors', () => { block2: 'block1', } ) ), + order: new Map( [ + [ '', [ 'block1' ] ], + [ 'block1', [ 'block2' ] ], + ] ), }, blockListSettings: { block1: {}, @@ -3240,6 +3310,7 @@ describe( 'selectors', () => { } ) ), parents: new Map(), + order: new Map(), }, blockListSettings: { 1: { @@ -3273,6 +3344,7 @@ describe( 'selectors', () => { } ) ), parents: new Map(), + order: new Map(), }, blockListSettings: { 1: { diff --git a/packages/block-editor/src/store/utils.js b/packages/block-editor/src/store/utils.js index b630912a5163d..af991608238e2 100644 --- a/packages/block-editor/src/store/utils.js +++ b/packages/block-editor/src/store/utils.js @@ -117,5 +117,6 @@ export function getInsertBlockTypeDependants( state, rootClientId ) { state.settings.allowedBlockTypes, state.settings.templateLock, state.blockEditingModes, + state.editorMode, ]; } diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index d7e8b19122989..6607700b2d2c2 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -9,6 +9,7 @@ - `Tabs`: restore vertical indicator ([#65385](https://github.com/WordPress/gutenberg/pull/65385)). - `Tabs`: indicator positioning under RTL direction ([#64926](https://github.com/WordPress/gutenberg/pull/64926)). - `Popover`: Update `toolbar` variant radius to match block toolbar ([#65263](https://github.com/WordPress/gutenberg/pull/65263)). +- `MenuItemsChoice`: Allow menu items height to adapt to its content ([#65204](https://github.com/WordPress/gutenberg/pull/65204)). - `BoxControl`: Unify input filed width whether linked or not ([#65348](https://github.com/WordPress/gutenberg/pull/65348)). ### Deprecations diff --git a/packages/components/src/menu-items-choice/style.scss b/packages/components/src/menu-items-choice/style.scss index 5de8363be0d6e..383eb4066ba86 100644 --- a/packages/components/src/menu-items-choice/style.scss +++ b/packages/components/src/menu-items-choice/style.scss @@ -1,5 +1,7 @@ .components-menu-items-choice, .components-menu-items-choice.components-button { + height: auto; + svg { margin-right: $grid-unit-15; } diff --git a/packages/editor/src/components/block-settings-menu/content-only-settings-menu.js b/packages/editor/src/components/block-settings-menu/content-only-settings-menu.js index fcf7adfa77635..c772a062b9e3b 100644 --- a/packages/editor/src/components/block-settings-menu/content-only-settings-menu.js +++ b/packages/editor/src/components/block-settings-menu/content-only-settings-menu.js @@ -17,21 +17,17 @@ import { __, _x } from '@wordpress/i18n'; */ import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; +import usePostContentBlocks from '../provider/use-post-content-blocks'; function ContentOnlySettingsMenuItems( { clientId, onClose } ) { + const postContentBlocks = usePostContentBlocks(); const { entity, onNavigateToEntityRecord, canEditTemplates } = useSelect( ( select ) => { const { - getBlockEditingMode, getBlockParentsByBlockName, getSettings, getBlockAttributes, } = select( blockEditorStore ); - const contentOnly = - getBlockEditingMode( clientId ) === 'contentOnly'; - if ( ! contentOnly ) { - return {}; - } const patternParent = getBlockParentsByBlockName( clientId, 'core/block', @@ -48,10 +44,14 @@ function ContentOnlySettingsMenuItems( { clientId, onClose } ) { } else { const { getCurrentTemplateId } = select( editorStore ); const templateId = getCurrentTemplateId(); - const { getContentLockingParent } = unlock( + const { getBlockParents } = unlock( select( blockEditorStore ) ); - if ( ! getContentLockingParent( clientId ) && templateId ) { + if ( + ! getBlockParents( clientId ).some( ( parent ) => + postContentBlocks.includes( parent ) + ) + ) { record = select( coreStore ).getEntityRecord( 'postType', 'wp_template', @@ -59,6 +59,9 @@ function ContentOnlySettingsMenuItems( { clientId, onClose } ) { ); } } + if ( ! record ) { + return {}; + } const _canEditTemplates = select( coreStore ).canUser( 'create', { kind: 'postType', name: 'wp_template', @@ -70,7 +73,7 @@ function ContentOnlySettingsMenuItems( { clientId, onClose } ) { getSettings().onNavigateToEntityRecord, }; }, - [ clientId ] + [ clientId, postContentBlocks ] ); if ( ! entity ) { diff --git a/packages/editor/src/components/provider/disable-non-page-content-blocks.js b/packages/editor/src/components/provider/disable-non-page-content-blocks.js index 9abb0e14079d5..ae4fd1075fc26 100644 --- a/packages/editor/src/components/provider/disable-non-page-content-blocks.js +++ b/packages/editor/src/components/provider/disable-non-page-content-blocks.js @@ -3,52 +3,32 @@ */ import { useSelect, useRegistry } from '@wordpress/data'; import { store as blockEditorStore } from '@wordpress/block-editor'; -import { useEffect, useMemo } from '@wordpress/element'; -import { applyFilters } from '@wordpress/hooks'; +import { useEffect } from '@wordpress/element'; /** * Internal dependencies */ -import { store as editorStore } from '../../store'; -import { unlock } from '../../lock-unlock'; - -const POST_CONTENT_BLOCK_TYPES = [ - 'core/post-title', - 'core/post-featured-image', - 'core/post-content', -]; +import usePostContentBlocks from './use-post-content-blocks'; /** * Component that when rendered, makes it so that the site editor allows only * page content to be edited. */ export default function DisableNonPageContentBlocks() { - const contentOnlyBlockTypes = useMemo( - () => [ - ...applyFilters( - 'editor.postContentBlockTypes', - POST_CONTENT_BLOCK_TYPES - ), - 'core/template-part', - ], - [] - ); - - // Note that there are two separate subscriptions because the result for each - // returns a new array. - const contentOnlyIds = useSelect( + const contentOnlyIds = usePostContentBlocks(); + const templateParts = useSelect( ( select ) => { + const { getBlocksByName } = select( blockEditorStore ); + return getBlocksByName( 'core/template-part' ); + }, [] ); + const disabledIds = useSelect( ( select ) => { - const { getPostBlocksByName } = unlock( select( editorStore ) ); - return getPostBlocksByName( contentOnlyBlockTypes ); + const { getBlockOrder } = select( blockEditorStore ); + return templateParts.flatMap( ( clientId ) => + getBlockOrder( clientId ) + ); }, - [ contentOnlyBlockTypes ] + [ templateParts ] ); - const disabledIds = useSelect( ( select ) => { - const { getBlocksByName, getBlockOrder } = select( blockEditorStore ); - return getBlocksByName( 'core/template-part' ).flatMap( ( clientId ) => - getBlockOrder( clientId ) - ); - }, [] ); const registry = useRegistry(); @@ -61,6 +41,9 @@ export default function DisableNonPageContentBlocks() { for ( const clientId of contentOnlyIds ) { setBlockEditingMode( clientId, 'contentOnly' ); } + for ( const clientId of templateParts ) { + setBlockEditingMode( clientId, 'contentOnly' ); + } for ( const clientId of disabledIds ) { setBlockEditingMode( clientId, 'disabled' ); } @@ -72,12 +55,15 @@ export default function DisableNonPageContentBlocks() { for ( const clientId of contentOnlyIds ) { unsetBlockEditingMode( clientId ); } + for ( const clientId of templateParts ) { + unsetBlockEditingMode( clientId ); + } for ( const clientId of disabledIds ) { unsetBlockEditingMode( clientId ); } } ); }; - }, [ contentOnlyIds, disabledIds, registry ] ); + }, [ templateParts, contentOnlyIds, disabledIds, registry ] ); return null; } diff --git a/packages/editor/src/components/provider/use-post-content-blocks.js b/packages/editor/src/components/provider/use-post-content-blocks.js new file mode 100644 index 0000000000000..bdd277157e47e --- /dev/null +++ b/packages/editor/src/components/provider/use-post-content-blocks.js @@ -0,0 +1,42 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { useMemo } from '@wordpress/element'; +import { applyFilters } from '@wordpress/hooks'; + +/** + * Internal dependencies + */ +import { store as editorStore } from '../../store'; +import { unlock } from '../../lock-unlock'; + +const POST_CONTENT_BLOCK_TYPES = [ + 'core/post-title', + 'core/post-featured-image', + 'core/post-content', +]; + +export default function usePostContentBlocks() { + const contentOnlyBlockTypes = useMemo( + () => [ + ...applyFilters( + 'editor.postContentBlockTypes', + POST_CONTENT_BLOCK_TYPES + ), + ], + [] + ); + + // Note that there are two separate subscriptions because the result for each + // returns a new array. + const contentOnlyIds = useSelect( + ( select ) => { + const { getPostBlocksByName } = unlock( select( editorStore ) ); + return getPostBlocksByName( contentOnlyBlockTypes ); + }, + [ contentOnlyBlockTypes ] + ); + + return contentOnlyIds; +} diff --git a/packages/widgets/src/blocks/legacy-widget/edit/index.js b/packages/widgets/src/blocks/legacy-widget/edit/index.js index f371786c106d6..c5ca43211e58e 100644 --- a/packages/widgets/src/blocks/legacy-widget/edit/index.js +++ b/packages/widgets/src/blocks/legacy-widget/edit/index.js @@ -11,13 +11,11 @@ import { BlockControls, InspectorControls, BlockIcon, - store as blockEditorStore, } from '@wordpress/block-editor'; import { Flex, FlexBlock, Spinner, Placeholder } from '@wordpress/components'; import { brush as brushIcon } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; import { useState, useCallback } from '@wordpress/element'; -import { useSelect } from '@wordpress/data'; import { useEntityRecord } from '@wordpress/core-data'; /** @@ -102,11 +100,6 @@ function NotEmpty( { const { record: widgetType, hasResolved: hasResolvedWidgetType } = useEntityRecord( 'root', 'widgetType', widgetTypeId ); - const isNavigationMode = useSelect( - ( select ) => select( blockEditorStore ).isNavigationMode(), - [] - ); - const setInstance = useCallback( ( nextInstance ) => { setAttributes( { instance: nextInstance } ); }, [] ); @@ -130,8 +123,7 @@ function NotEmpty( { ); } - const mode = - idBase && ( isNavigationMode || ! isSelected ) ? 'preview' : 'edit'; + const mode = idBase && ! isSelected ? 'preview' : 'edit'; return ( <> diff --git a/test/e2e/specs/editor/various/block-moving-mode.spec.js b/test/e2e/specs/editor/various/block-moving-mode.spec.js deleted file mode 100644 index 5b8ef6bdcd051..0000000000000 --- a/test/e2e/specs/editor/various/block-moving-mode.spec.js +++ /dev/null @@ -1,155 +0,0 @@ -/** - * WordPress dependencies - */ -const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); - -test.describe( 'Block moving mode', () => { - test.beforeEach( async ( { admin } ) => { - await admin.createNewPost(); - } ); - - test.afterEach( async ( { requestUtils } ) => { - await requestUtils.deleteAllPosts(); - } ); - - test( 'can move block', async ( { editor, page } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { content: 'First Paragraph' }, - } ); - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { content: 'Second Paragraph' }, - } ); - - // Move the second block in front of the first block. - await editor.showBlockToolbar(); - await page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Options' } ) - .click(); - await page.getByRole( 'menuitem', { name: 'Move to' } ).click(); - await page.keyboard.press( 'ArrowUp' ); - await page.keyboard.press( 'Enter' ); - - await expect.poll( editor.getBlocks ).toMatchObject( [ - { - name: 'core/paragraph', - attributes: { content: 'Second Paragraph' }, - }, - { - name: 'core/paragraph', - attributes: { content: 'First Paragraph' }, - }, - ] ); - } ); - - test( 'can move block in the nested block', async ( { editor, page } ) => { - // Create two group blocks with some blocks. - await editor.insertBlock( { name: 'core/group' } ); - await editor.canvas - .locator( - 'role=button[name="Group: Gather blocks in a container."i]' - ) - .click(); - await page.keyboard.press( 'ArrowDown' ); - await page.keyboard.press( 'Enter' ); - await page.getByRole( 'option', { name: 'Paragraph' } ).click(); - await page.keyboard.type( 'First Paragraph' ); - await page.keyboard.press( 'Enter' ); - await page.keyboard.type( 'Second Paragraph' ); - - await editor.insertBlock( { name: 'core/group' } ); - await editor.canvas - .locator( - 'role=button[name="Group: Gather blocks in a container."i]' - ) - .click(); - await page.keyboard.press( 'ArrowDown' ); - await page.keyboard.press( 'Enter' ); - await page.getByRole( 'option', { name: 'Paragraph' } ).click(); - await page.keyboard.type( 'Third Paragraph' ); - await page.keyboard.press( 'Enter' ); - await page.keyboard.type( 'Fourth Paragraph' ); - - // Move a paragraph block in the first group block into the second group block. - const paragraphBlock = editor.canvas.locator( - 'text="First Paragraph"' - ); - await paragraphBlock.focus(); - await editor.showBlockToolbar(); - await page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Options' } ) - .click(); - await page.getByRole( 'menuitem', { name: 'Move to' } ).click(); - await page.keyboard.press( 'ArrowLeft' ); // Select the first group block. - await page.keyboard.press( 'ArrowDown' ); // Select the second group block. - await page.keyboard.press( 'ArrowRight' ); // Enter the second group block. - await page.keyboard.press( 'ArrowDown' ); // Move down in the second group block. - await page.keyboard.press( 'Enter' ); - - await expect.poll( editor.getBlocks ).toMatchObject( [ - { - name: 'core/group', - innerBlocks: [ - { - name: 'core/paragraph', - attributes: { content: 'Second Paragraph' }, - }, - ], - }, - { - name: 'core/group', - innerBlocks: [ - { - name: 'core/paragraph', - attributes: { content: 'Third Paragraph' }, - }, - { - name: 'core/paragraph', - attributes: { content: 'First Paragraph' }, - }, - { - name: 'core/paragraph', - attributes: { content: 'Fourth Paragraph' }, - }, - ], - }, - ] ); - } ); - - test( 'can not move inside its own block', async ( { editor, page } ) => { - // Create a paragraph block and a group block. - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { content: 'First Paragraph' }, - } ); - await editor.insertBlock( { name: 'core/group' } ); - await editor.canvas - .locator( - 'role=button[name="Group: Gather blocks in a container."i]' - ) - .click(); - await page.keyboard.press( 'ArrowDown' ); - await page.keyboard.press( 'Enter' ); - await page.getByRole( 'option', { name: 'Paragraph' } ).click(); - await page.keyboard.type( 'Second Paragraph' ); - await page.keyboard.press( 'Enter' ); - await page.keyboard.type( 'Third Paragraph' ); - - // Trying to move the group block into its own. - const groupBlock = editor.canvas.locator( - 'role=document[name="Block: Group"i]' - ); - await groupBlock.focus(); - await editor.showBlockToolbar(); - await page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Options' } ) - .click(); - await page.getByRole( 'menuitem', { name: 'Move to' } ).click(); - await page.keyboard.press( 'ArrowRight' ); - await expect( groupBlock ).toHaveClass( /is-selected/ ); - } ); -} ); diff --git a/test/e2e/specs/editor/various/keyboard-navigable-blocks.spec.js b/test/e2e/specs/editor/various/keyboard-navigable-blocks.spec.js index 6a7125d04f7a6..e1ca121040b97 100644 --- a/test/e2e/specs/editor/various/keyboard-navigable-blocks.spec.js +++ b/test/e2e/specs/editor/various/keyboard-navigable-blocks.spec.js @@ -17,108 +17,26 @@ test.describe( 'Order of block keyboard navigation', () => { await editor.openDocumentSettingsSidebar(); } ); - test( 'permits tabbing through paragraph blocks in the expected order', async ( { + test( 'permits tabbing through the block toolbar of the paragraph block', async ( { editor, KeyboardNavigableBlocks, page, + pageUtils, } ) => { - const paragraphBlocks = [ 'Paragraph 0', 'Paragraph 1', 'Paragraph 2' ]; - - // Create 3 paragraphs blocks with some content. - for ( const paragraphBlock of paragraphBlocks ) { + // Insert three paragraph blocks. + for ( let i = 0; i < 3; i++ ) { await editor.insertBlock( { name: 'core/paragraph' } ); - await page.keyboard.type( paragraphBlock ); + await page.keyboard.type( `Paragraph ${ i + 1 }` ); } - - // Select the middle block. + // Select the middle paragraph block. await page.keyboard.press( 'ArrowUp' ); await editor.showBlockToolbar(); - await KeyboardNavigableBlocks.navigateToContentEditorTop(); - await KeyboardNavigableBlocks.tabThroughParagraphBlock( 'Paragraph 1' ); - - // Repeat the same steps to ensure that there is no change introduced in how the focus is handled. - // This prevents the previous regression explained in: https://github.com/WordPress/gutenberg/issues/11773. - await KeyboardNavigableBlocks.navigateToContentEditorTop(); - await KeyboardNavigableBlocks.tabThroughParagraphBlock( 'Paragraph 1' ); - } ); - - test( 'allows tabbing in navigation mode if no block is selected', async ( { - editor, - KeyboardNavigableBlocks, - page, - } ) => { - const paragraphBlocks = [ '0', '1' ]; - - // Create 2 paragraphs blocks with some content. - for ( const paragraphBlock of paragraphBlocks ) { - await editor.insertBlock( { name: 'core/paragraph' } ); - await page.keyboard.type( paragraphBlock ); - } - - // Clear the selected block. - const paragraph = editor.canvas - .locator( '[data-type="core/paragraph"]' ) - .getByText( '1' ); - const box = await paragraph.boundingBox(); - await page.mouse.click( box.x - 1, box.y ); - - await page.keyboard.press( 'Tab' ); - await KeyboardNavigableBlocks.expectLabelToHaveFocus( 'Add title' ); - - await page.keyboard.press( 'Tab' ); - await KeyboardNavigableBlocks.expectLabelToHaveFocus( - 'Paragraph Block. Row 1. 0' - ); - - await page.keyboard.press( 'Tab' ); - await KeyboardNavigableBlocks.expectLabelToHaveFocus( - 'Paragraph Block. Row 2. 1' - ); - - await page.keyboard.press( 'Tab' ); - await KeyboardNavigableBlocks.expectLabelToHaveFocus( 'Post' ); - } ); - - test( 'allows tabbing in navigation mode if no block is selected (reverse)', async ( { - editor, - KeyboardNavigableBlocks, - page, - pageUtils, - } ) => { - const paragraphBlocks = [ '0', '1' ]; - - // Create 2 paragraphs blocks with some content. - for ( const paragraphBlock of paragraphBlocks ) { - await editor.insertBlock( { name: 'core/paragraph' } ); - await page.keyboard.type( paragraphBlock ); - } - - // Clear the selected block. - const paragraph = editor.canvas - .locator( '[data-type="core/paragraph"]' ) - .getByText( '1' ); - const box = await paragraph.boundingBox(); - await page.mouse.click( box.x - 1, box.y ); - - // Put focus behind the block list. - await page.evaluate( () => { - document - .querySelector( '.interface-interface-skeleton__sidebar' ) - .focus(); - } ); - - await pageUtils.pressKeys( 'shift+Tab' ); - await KeyboardNavigableBlocks.expectLabelToHaveFocus( - 'Paragraph Block. Row 2. 1' - ); - await pageUtils.pressKeys( 'shift+Tab' ); + await KeyboardNavigableBlocks.navigateThroughBlockToolbar(); + await page.keyboard.press( 'Tab' ); await KeyboardNavigableBlocks.expectLabelToHaveFocus( - 'Paragraph Block. Row 1. 0' + 'Block: Paragraph' ); - - await pageUtils.pressKeys( 'shift+Tab' ); - await KeyboardNavigableBlocks.expectLabelToHaveFocus( 'Add title' ); } ); test( 'should navigate correctly with multi selection', async ( { @@ -208,31 +126,7 @@ class KeyboardNavigableBlocks { expect( ariaLabel ).toBe( label ); } - async navigateToContentEditorTop() { - // Use 'Ctrl+`' to return to the top of the editor. - await this.pageUtils.pressKeys( 'ctrl+`', { times: 5 } ); - } - - async tabThroughParagraphBlock( paragraphText ) { - await this.tabThroughBlockToolbar(); - - await this.page.keyboard.press( 'Tab' ); - await this.expectLabelToHaveFocus( 'Block: Paragraph' ); - - const activeElement = this.editor.canvas.locator( ':focus' ); - - await expect( activeElement ).toHaveText( paragraphText ); - - await this.page.keyboard.press( 'Tab' ); - await this.expectLabelToHaveFocus( 'Block' ); - - // Need to shift+tab here to end back in the block. If not, we'll be in the next region and it will only require 4 region jumps instead of 5. - await this.pageUtils.pressKeys( 'shift+Tab' ); - await this.expectLabelToHaveFocus( 'Block: Paragraph' ); - } - - async tabThroughBlockToolbar() { - await this.page.keyboard.press( 'Tab' ); + async navigateThroughBlockToolbar() { await this.expectLabelToHaveFocus( 'Paragraph' ); await this.page.keyboard.press( 'ArrowRight' ); diff --git a/test/e2e/specs/editor/various/shortcut-focus-toolbar.spec.js b/test/e2e/specs/editor/various/shortcut-focus-toolbar.spec.js index a8e49f7a6b84d..cfaf4e0be9188 100644 --- a/test/e2e/specs/editor/various/shortcut-focus-toolbar.spec.js +++ b/test/e2e/specs/editor/various/shortcut-focus-toolbar.spec.js @@ -50,32 +50,6 @@ test.describe( 'Focus toolbar shortcut (alt + F10)', () => { await expect( toolbarUtils.documentToolbarTooltip ).toBeHidden(); } ); - test( 'Focuses correct toolbar in default view options in select mode', async ( { - editor, - page, - toolbarUtils, - } ) => { - // Test: Focus the document toolbar from title - await toolbarUtils.useSelectMode(); - await toolbarUtils.moveToToolbarShortcut(); - await expect( toolbarUtils.documentToolbarButton ).toBeFocused(); - - // Test: Focus the top level toolbar from empty block - await editor.insertBlock( { name: 'core/paragraph' } ); - await toolbarUtils.useSelectMode(); - await toolbarUtils.moveToToolbarShortcut(); - await expect( toolbarUtils.documentToolbarButton ).toBeFocused(); - - // Test: Focus the top level toolbar from paragraph block - await editor.insertBlock( { name: 'core/paragraph' } ); - await page.keyboard.type( - 'Focus top level toolbar from paragraph block in select mode.' - ); - await toolbarUtils.useSelectMode(); - await toolbarUtils.moveToToolbarShortcut(); - await expect( toolbarUtils.documentToolbarButton ).toBeFocused(); - } ); - test.describe( 'In Top Toolbar option:', () => { test.beforeEach( async ( { editor } ) => { // Ensure the fixed toolbar option is on diff --git a/test/e2e/specs/editor/various/writing-flow.spec.js b/test/e2e/specs/editor/various/writing-flow.spec.js index bd1552ad4cb66..4077d6dcc5820 100644 --- a/test/e2e/specs/editor/various/writing-flow.spec.js +++ b/test/e2e/specs/editor/various/writing-flow.spec.js @@ -106,48 +106,6 @@ test.describe( 'Writing Flow (@firefox, @webkit)', () => { ] ); } ); - test( 'Should navigate between inner and root blocks in navigation mode', async ( { - page, - writingFlowUtils, - } ) => { - await writingFlowUtils.addDemoContent(); - - // Switch to navigation mode. - await page.keyboard.press( 'Escape' ); - // Arrow up to Columns block. - await page.keyboard.press( 'ArrowUp' ); - await expect - .poll( writingFlowUtils.getActiveBlockName ) - .toBe( 'core/columns' ); - // Arrow right into Column block. - await page.keyboard.press( 'ArrowRight' ); - await expect - .poll( writingFlowUtils.getActiveBlockName ) - .toBe( 'core/column' ); - // Arrow down to reach second Column block. - await page.keyboard.press( 'ArrowDown' ); - // Arrow right again into Paragraph block. - await page.keyboard.press( 'ArrowRight' ); - await expect - .poll( writingFlowUtils.getActiveBlockName ) - .toBe( 'core/paragraph' ); - // Arrow left back to Column block. - await page.keyboard.press( 'ArrowLeft' ); - await expect - .poll( writingFlowUtils.getActiveBlockName ) - .toBe( 'core/column' ); - // Arrow left back to Columns block. - await page.keyboard.press( 'ArrowLeft' ); - await expect - .poll( writingFlowUtils.getActiveBlockName ) - .toBe( 'core/columns' ); - // Arrow up to first paragraph. - await page.keyboard.press( 'ArrowUp' ); - await expect - .poll( writingFlowUtils.getActiveBlockName ) - .toBe( 'core/paragraph' ); - } ); - test( 'should navigate around inline boundaries', async ( { editor, page, @@ -958,32 +916,6 @@ test.describe( 'Writing Flow (@firefox, @webkit)', () => { ` ); } ); - test( 'escape should set select mode and then focus the canvas', async ( { - page, - writingFlowUtils, - } ) => { - await page.keyboard.press( 'Enter' ); - await page.keyboard.type( 'Random Paragraph' ); - - // First escape enters navigation mode. - await page.keyboard.press( 'Escape' ); - const navigationButton = page.getByLabel( - 'Paragraph Block. Row 1. Random Paragraph' - ); - await expect( navigationButton ).toBeVisible(); - await expect - .poll( writingFlowUtils.getActiveBlockName ) - .toBe( 'core/paragraph' ); - - // Second escape should send focus to the canvas - await page.keyboard.press( 'Escape' ); - // The navigation button should be hidden. - await expect( navigationButton ).toBeHidden(); - await expect( - page.getByRole( 'region', { name: 'Editor content' } ) - ).toBeFocused(); - } ); - // Checks for regressions of https://github.com/WordPress/gutenberg/issues/40091. test( 'does not deselect the block when selecting text outside the editor canvas', async ( { editor, @@ -1222,11 +1154,11 @@ class WritingFlowUtils { 'role=listbox[name="Blocks"i] >> role=option[name="Paragraph"i]' ); await this.page.keyboard.type( '2nd col' ); // If this text is too long, it may wrap to a new line and cause test failure. That's why we're using "2nd" instead of "Second" here. - - await this.page.keyboard.press( 'Escape' ); // Enter navigation mode. - await this.page.keyboard.press( 'ArrowLeft' ); // Move to the column block. - await this.page.keyboard.press( 'ArrowLeft' ); // Move to the columns block. - await this.page.keyboard.press( 'Enter' ); // Enter edit mode with the columns block selected. + await this.editor.showBlockToolbar(); + await this.page.keyboard.press( 'Shift+Tab' ); // Move to toolbar to select parent + await this.page.keyboard.press( 'Enter' ); // Selects the column block. + await this.page.keyboard.press( 'Shift+Tab' ); // Move to toolbar to select parent + await this.page.keyboard.press( 'Enter' ); // Selects the columns block. await this.page.keyboard.press( 'Enter' ); // Creates a paragraph after the columns block. await this.page.keyboard.type( 'Second paragraph' ); } diff --git a/test/e2e/specs/site-editor/navigation.spec.js b/test/e2e/specs/site-editor/navigation.spec.js index 4db860b703892..1b92ef2e850e6 100644 --- a/test/e2e/specs/site-editor/navigation.spec.js +++ b/test/e2e/specs/site-editor/navigation.spec.js @@ -83,19 +83,6 @@ test.describe( 'Site editor navigation', () => { // The button role should have been removed from the iframe. await expect( editorCanvasButton ).toBeHidden(); - // Test to make sure a Tab keypress works as expected. - // As of this writing, we are in select mode and a tab - // keypress will reveal the header template select mode - // button. This test is not documenting that we _want_ - // that action, but checking that we are within the site - // editor and keypresses work as intened. - await pageUtils.pressKeys( 'Tab' ); - await expect( - page.getByRole( 'button', { - name: 'Template Part Block. Row 1. header', - } ) - ).toBeFocused(); - // Test: We can go back to the main navigation from the editor frame // Move to the document toolbar await pageUtils.pressKeys( 'alt+F10' ); diff --git a/test/e2e/specs/widgets/customizing-widgets.spec.js b/test/e2e/specs/widgets/customizing-widgets.spec.js index 53b4da7be2d61..38e9d3ee2c58a 100644 --- a/test/e2e/specs/widgets/customizing-widgets.spec.js +++ b/test/e2e/specs/widgets/customizing-widgets.spec.js @@ -465,16 +465,6 @@ test.describe( 'Widgets Customizer', () => { await expect( paragraphBlock ).toBeVisible(); await paragraphBlock.focus(); - - // Expect pressing the Escape key to enter navigation mode, - // but not close the editor. - await page.keyboard.press( 'Escape' ); - await expect( - page.locator( - 'css=.block-editor-block-list__layout.is-navigate-mode' - ) - ).toBeVisible(); - await expect( paragraphBlock ).toBeVisible(); } ); test( 'should move (inner) blocks to another sidebar', async ( { From f0cd2174238a40bbc15812e9270bed6336fb8be8 Mon Sep 17 00:00:00 2001 From: Michal Date: Thu, 19 Sep 2024 22:20:41 +0200 Subject: [PATCH 12/15] DEWP: Handle cyclical module dependencies (#65291) * Add tests for cyclical dependencies * Add the externals and make the test fail * Add cycle detection in dependency path checking Enhanced the dependency resolution logic to detect cycles in the module graph, preventing infinite loops during static dependency path checks. Introduced a Set to track visited blocks and avoid revisiting them. * Revert changes * Propose static WeakSet/WeakMap implementation. * Add CHANGELOG entry * Remove redundant plugin config in test * Revert "Remove redundant plugin config in test" This reverts commit b5e33dbc0e6db4d4aab07de4d52b42ae6b9672ee. * Remove redundant plugin config in test * Updated the snapshot files --------- Co-authored-by: michalczaplinski Co-authored-by: sirreal --- .../CHANGELOG.md | 4 ++ .../lib/index.js | 46 +++++++++++++++++-- .../test/__snapshots__/build.js.snap | 33 +++++++++++++ .../test/fixtures/cyclic-external-deps/a.js | 8 ++++ .../fixtures/cyclic-external-deps/index.js | 18 ++++++++ .../cyclic-external-deps/webpack.config.js | 8 ++++ 6 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-external-deps/a.js create mode 100644 packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-external-deps/index.js create mode 100644 packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-external-deps/webpack.config.js diff --git a/packages/dependency-extraction-webpack-plugin/CHANGELOG.md b/packages/dependency-extraction-webpack-plugin/CHANGELOG.md index 85498d539317f..1c0aa630495a0 100644 --- a/packages/dependency-extraction-webpack-plugin/CHANGELOG.md +++ b/packages/dependency-extraction-webpack-plugin/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Bug Fixes + +- Fix a bug where cycles in dependent modules could enter infinite recursion ([#65291](https://github.com/WordPress/gutenberg/pull/65291)). + ## 6.8.0 (2024-09-19) ## 6.7.0 (2024-09-05) diff --git a/packages/dependency-extraction-webpack-plugin/lib/index.js b/packages/dependency-extraction-webpack-plugin/lib/index.js index 575882a1dfbeb..529fb339d15a1 100644 --- a/packages/dependency-extraction-webpack-plugin/lib/index.js +++ b/packages/dependency-extraction-webpack-plugin/lib/index.js @@ -369,6 +369,9 @@ class DependencyExtractionWebpackPlugin { } } + static #staticDepsCurrent = new WeakSet(); + static #staticDepsCache = new WeakMap(); + /** * Can we trace a line of static dependencies from an entry to a module * @@ -378,6 +381,20 @@ class DependencyExtractionWebpackPlugin { * @return {boolean} True if there is a static import path to the root */ static hasStaticDependencyPathToRoot( compilation, block ) { + if ( DependencyExtractionWebpackPlugin.#staticDepsCache.has( block ) ) { + return DependencyExtractionWebpackPlugin.#staticDepsCache.get( + block + ); + } + + if ( + DependencyExtractionWebpackPlugin.#staticDepsCurrent.has( block ) + ) { + return false; + } + + DependencyExtractionWebpackPlugin.#staticDepsCurrent.add( block ); + const incomingConnections = [ ...compilation.moduleGraph.getIncomingConnections( block ), ].filter( @@ -391,6 +408,13 @@ class DependencyExtractionWebpackPlugin { // If we don't have non-entry, non-library incoming connections, // we've reached a root of if ( ! incomingConnections.length ) { + DependencyExtractionWebpackPlugin.#staticDepsCache.set( + block, + true + ); + DependencyExtractionWebpackPlugin.#staticDepsCurrent.delete( + block + ); return true; } @@ -409,16 +433,28 @@ class DependencyExtractionWebpackPlugin { // All the dependencies were Async, the module was reached via a dynamic import if ( ! staticDependentModules.length ) { + DependencyExtractionWebpackPlugin.#staticDepsCache.set( + block, + false + ); + DependencyExtractionWebpackPlugin.#staticDepsCurrent.delete( + block + ); return false; } // Continue to explore any static dependencies - return staticDependentModules.some( ( parentStaticDependentModule ) => - DependencyExtractionWebpackPlugin.hasStaticDependencyPathToRoot( - compilation, - parentStaticDependentModule - ) + const result = staticDependentModules.some( + ( parentStaticDependentModule ) => + DependencyExtractionWebpackPlugin.hasStaticDependencyPathToRoot( + compilation, + parentStaticDependentModule + ) ); + + DependencyExtractionWebpackPlugin.#staticDepsCache.set( block, result ); + DependencyExtractionWebpackPlugin.#staticDepsCurrent.delete( block ); + return result; } } 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 c4b450683572e..903c9658250b1 100644 --- a/packages/dependency-extraction-webpack-plugin/test/__snapshots__/build.js.snap +++ b/packages/dependency-extraction-webpack-plugin/test/__snapshots__/build.js.snap @@ -55,6 +55,21 @@ exports[`DependencyExtractionWebpackPlugin modules Webpack \`cyclic-dynamic-depe ] `; +exports[`DependencyExtractionWebpackPlugin modules Webpack \`cyclic-external-deps\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = ` +" array(array('id' => '@wordpress/interactivity', 'import' => 'dynamic')), 'version' => 'e1033c1bd62e8cb8d4c9', 'type' => 'module'); +" +`; + +exports[`DependencyExtractionWebpackPlugin modules Webpack \`cyclic-external-deps\` should produce expected output: External modules should match snapshot 1`] = ` +[ + { + "externalType": "module", + "request": "@wordpress/interactivity", + "userRequest": "@wordpress/interactivity", + }, +] +`; + exports[`DependencyExtractionWebpackPlugin modules Webpack \`dynamic-import\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = ` " array(array('id' => '@wordpress/blob', 'import' => 'dynamic')), 'version' => '4f59b7847b70a07b2710', 'type' => 'module'); " @@ -419,6 +434,24 @@ exports[`DependencyExtractionWebpackPlugin scripts Webpack \`cyclic-dynamic-depe ] `; +exports[`DependencyExtractionWebpackPlugin scripts Webpack \`cyclic-external-deps\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = ` +" array('wp-interactivity'), 'version' => '455f3cab924853d41b8b'); +" +`; + +exports[`DependencyExtractionWebpackPlugin scripts Webpack \`cyclic-external-deps\` should produce expected output: External modules should match snapshot 1`] = ` +[ + { + "externalType": "window", + "request": [ + "wp", + "interactivity", + ], + "userRequest": "@wordpress/interactivity", + }, +] +`; + exports[`DependencyExtractionWebpackPlugin scripts Webpack \`dynamic-import\` should produce expected output: Asset file 'main.asset.php' should match snapshot 1`] = ` " array('wp-blob'), 'version' => 'c0e8a6f22065ea096606'); " diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-external-deps/a.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-external-deps/a.js new file mode 100644 index 0000000000000..1f0edffe27efd --- /dev/null +++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-external-deps/a.js @@ -0,0 +1,8 @@ +/** + * Internal dependencies + */ +import { someFunction } from '.'; + +someFunction(); + +export const a = 'test'; diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-external-deps/index.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-external-deps/index.js new file mode 100644 index 0000000000000..01d7eff466bfb --- /dev/null +++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-external-deps/index.js @@ -0,0 +1,18 @@ +/** + * Internal dependencies + */ +import { a } from './a'; + +/** + * WordPress dependencies + */ +import { store } from '@wordpress/interactivity'; + +export const someFunction = () => { + store( 'test', { + state: { + a, + }, + } ); + return a; +}; diff --git a/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-external-deps/webpack.config.js b/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-external-deps/webpack.config.js new file mode 100644 index 0000000000000..bfffff3ae7831 --- /dev/null +++ b/packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-external-deps/webpack.config.js @@ -0,0 +1,8 @@ +/** + * Internal dependencies + */ +const DependencyExtractionWebpackPlugin = require( '../../..' ); + +module.exports = { + plugins: [ new DependencyExtractionWebpackPlugin() ], +}; From cc29fd1b10b7f787b8e24e6a8a26544f0946f929 Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 20 Sep 2024 10:50:14 +1000 Subject: [PATCH 13/15] Command Palette: "Add new page" within the site editor creates new page in site editor (#65476) The add page command will now create a new page depending on context: in the post editor it will redirect to `postType=page` in the post editor as it did previously. In the site editor however, it will open a new page without redirecting to the post editor. --------- Co-authored-by: ramonjd Co-authored-by: andrewserong Co-authored-by: youknowriad --- package-lock.json | 2 + packages/core-commands/package.json | 1 + .../src/admin-navigation-commands.js | 85 +++++++++++++++++-- .../specs/site-editor/command-center.spec.js | 6 +- 4 files changed, 85 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index f61d8acd98274..035018e97f13f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53340,6 +53340,7 @@ "@wordpress/html-entities": "file:../html-entities", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", + "@wordpress/notices": "file:../notices", "@wordpress/private-apis": "file:../private-apis", "@wordpress/router": "file:../router", "@wordpress/url": "file:../url" @@ -68362,6 +68363,7 @@ "@wordpress/html-entities": "file:../html-entities", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", + "@wordpress/notices": "file:../notices", "@wordpress/private-apis": "file:../private-apis", "@wordpress/router": "file:../router", "@wordpress/url": "file:../url" diff --git a/packages/core-commands/package.json b/packages/core-commands/package.json index 8334ef97c9244..a2d3c76ebe5d9 100644 --- a/packages/core-commands/package.json +++ b/packages/core-commands/package.json @@ -37,6 +37,7 @@ "@wordpress/html-entities": "file:../html-entities", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", + "@wordpress/notices": "file:../notices", "@wordpress/private-apis": "file:../private-apis", "@wordpress/router": "file:../router", "@wordpress/url": "file:../url" diff --git a/packages/core-commands/src/admin-navigation-commands.js b/packages/core-commands/src/admin-navigation-commands.js index 0ffa7ba7eb628..be5e19386b65b 100644 --- a/packages/core-commands/src/admin-navigation-commands.js +++ b/packages/core-commands/src/admin-navigation-commands.js @@ -1,9 +1,83 @@ /** * WordPress dependencies */ -import { useCommand } from '@wordpress/commands'; +import { useCommand, useCommandLoader } from '@wordpress/commands'; import { __ } from '@wordpress/i18n'; import { plus } from '@wordpress/icons'; +import { getPath } from '@wordpress/url'; +import { store as coreStore } from '@wordpress/core-data'; +import { useDispatch } from '@wordpress/data'; +import { useMemo } from '@wordpress/element'; +import { store as noticesStore } from '@wordpress/notices'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; + +/** + * Internal dependencies + */ +import { unlock } from './lock-unlock'; + +const { useHistory } = unlock( routerPrivateApis ); + +function useAddNewPageCommand() { + const isSiteEditor = getPath( window.location.href )?.includes( + 'site-editor.php' + ); + const history = useHistory(); + const { saveEntityRecord } = useDispatch( coreStore ); + const { createErrorNotice } = useDispatch( noticesStore ); + + const createPageEntity = async ( { close } ) => { + try { + const page = await saveEntityRecord( + 'postType', + 'page', + { + status: 'draft', + }, + { + throwOnError: true, + } + ); + if ( page?.id ) { + history.push( { + postId: page.id, + postType: 'page', + canvas: 'edit', + } ); + } + } catch ( error ) { + const errorMessage = + error.message && error.code !== 'unknown_error' + ? error.message + : __( 'An error occurred while creating the item.' ); + + createErrorNotice( errorMessage, { + type: 'snackbar', + } ); + } finally { + close(); + } + }; + + const commands = useMemo( () => { + const addNewPage = isSiteEditor + ? createPageEntity + : () => ( document.location.href = 'post-new.php?post_type=page' ); + return [ + { + name: 'core/add-new-page', + label: __( 'Add new page' ), + icon: plus, + callback: addNewPage, + }, + ]; + }, [ createPageEntity, isSiteEditor ] ); + + return { + isLoading: false, + commands, + }; +} export function useAdminNavigationCommands() { useCommand( { @@ -14,12 +88,9 @@ export function useAdminNavigationCommands() { document.location.href = 'post-new.php'; }, } ); - useCommand( { + + useCommandLoader( { name: 'core/add-new-page', - label: __( 'Add new page' ), - icon: plus, - callback: () => { - document.location.href = 'post-new.php?post_type=page'; - }, + hook: useAddNewPageCommand, } ); } diff --git a/test/e2e/specs/site-editor/command-center.spec.js b/test/e2e/specs/site-editor/command-center.spec.js index 5b049cda252a8..19318081aa171 100644 --- a/test/e2e/specs/site-editor/command-center.spec.js +++ b/test/e2e/specs/site-editor/command-center.spec.js @@ -28,10 +28,12 @@ test.describe( 'Site editor command palette', () => { await page.keyboard.type( 'new page' ); await page.getByRole( 'option', { name: 'Add new page' } ).click(); await expect( page ).toHaveURL( - '/wp-admin/post-new.php?post_type=page' + /\/wp-admin\/site-editor.php\?postId=(\d+)&postType=page&canvas=edit/ ); await expect( - editor.canvas.getByRole( 'textbox', { name: 'Add title' } ) + editor.canvas + .getByLabel( 'Block: Title' ) + .locator( '[data-rich-text-placeholder="No title"]' ) ).toBeVisible(); } ); From 8bc4b6214d33e53fb031f7b4f245ef5bd2bb8082 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Fri, 20 Sep 2024 16:04:13 +1000 Subject: [PATCH 14/15] Time To Read: Add block example --- packages/block-library/src/post-time-to-read/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/block-library/src/post-time-to-read/index.js b/packages/block-library/src/post-time-to-read/index.js index 95b379f55f0b3..039923161ca81 100644 --- a/packages/block-library/src/post-time-to-read/index.js +++ b/packages/block-library/src/post-time-to-read/index.js @@ -12,6 +12,7 @@ export { metadata, name }; export const settings = { icon, edit, + example: {}, }; export const init = () => initBlock( { name, metadata, settings } ); From 1bbe4c4936a7961d5482e826d5d59aa996906e17 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Fri, 20 Sep 2024 16:51:30 +1000 Subject: [PATCH 15/15] Revert: Time To Read: Add block example (#65510) This reverts commit 8bc4b6214d33e53fb031f7b4f245ef5bd2bb8082. Co-authored-by: aaronrobertshaw Co-authored-by: andrewserong --- packages/block-library/src/post-time-to-read/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/block-library/src/post-time-to-read/index.js b/packages/block-library/src/post-time-to-read/index.js index 039923161ca81..95b379f55f0b3 100644 --- a/packages/block-library/src/post-time-to-read/index.js +++ b/packages/block-library/src/post-time-to-read/index.js @@ -12,7 +12,6 @@ export { metadata, name }; export const settings = { icon, edit, - example: {}, }; export const init = () => initBlock( { name, metadata, settings } );