diff --git a/packages/block-editor/src/components/block-edit/edit.js b/packages/block-editor/src/components/block-edit/edit.js
index 83d0e3f406f829..27d3650f3a0902 100644
--- a/packages/block-editor/src/components/block-edit/edit.js
+++ b/packages/block-editor/src/components/block-edit/edit.js
@@ -6,18 +6,27 @@ import clsx from 'clsx';
/**
* WordPress dependencies
*/
-import { withFilters } from '@wordpress/components';
import {
getBlockDefaultClassName,
- hasBlockSupport,
getBlockType,
+ hasBlockSupport,
+ store as blocksStore,
} from '@wordpress/blocks';
-import { useContext, useMemo } from '@wordpress/element';
+import { withFilters } from '@wordpress/components';
+import { useRegistry, useSelect } from '@wordpress/data';
+import { useCallback, useContext, useMemo } from '@wordpress/element';
/**
* Internal dependencies
*/
import BlockContext from '../block-context';
+import isURLLike from '../link-control/is-url-like';
+import {
+ canBindAttribute,
+ hasPatternOverridesDefaultBinding,
+ replacePatternOverridesDefaultBinding,
+} from '../../utils/block-bindings';
+import { unlock } from '../../lock-unlock';
/**
* Default value used for blocks which do not define their own context needs,
@@ -48,27 +57,223 @@ const Edit = ( props ) => {
const EditWithFilters = withFilters( 'editor.BlockEdit' )( Edit );
const EditWithGeneratedProps = ( props ) => {
- const { attributes = {}, name } = props;
+ const { name, clientId, attributes, setAttributes } = props;
+ const registry = useRegistry();
const blockType = getBlockType( name );
const blockContext = useContext( BlockContext );
+ const registeredSources = useSelect(
+ ( select ) =>
+ unlock( select( blocksStore ) ).getAllBlockBindingsSources(),
+ []
+ );
- // Assign context values using the block type's declared context needs.
- const context = useMemo( () => {
- return blockType && blockType.usesContext
+ const { blockBindings, context, hasPatternOverrides } = useMemo( () => {
+ // Assign context values using the block type's declared context needs.
+ const computedContext = blockType?.usesContext
? Object.fromEntries(
Object.entries( blockContext ).filter( ( [ key ] ) =>
blockType.usesContext.includes( key )
)
)
: DEFAULT_BLOCK_CONTEXT;
- }, [ blockType, blockContext ] );
+ // Add context requested by Block Bindings sources.
+ if ( attributes?.metadata?.bindings ) {
+ Object.values( attributes?.metadata?.bindings || {} ).forEach(
+ ( binding ) => {
+ registeredSources[ binding?.source ]?.usesContext?.forEach(
+ ( key ) => {
+ computedContext[ key ] = blockContext[ key ];
+ }
+ );
+ }
+ );
+ }
+ return {
+ blockBindings: replacePatternOverridesDefaultBinding(
+ name,
+ attributes?.metadata?.bindings
+ ),
+ context: computedContext,
+ hasPatternOverrides: hasPatternOverridesDefaultBinding(
+ attributes?.metadata?.bindings
+ ),
+ };
+ }, [
+ name,
+ blockType?.usesContext,
+ blockContext,
+ attributes?.metadata?.bindings,
+ registeredSources,
+ ] );
+
+ const computedAttributes = useSelect(
+ ( select ) => {
+ if ( ! blockBindings ) {
+ return attributes;
+ }
+
+ const attributesFromSources = {};
+ const blockBindingsBySource = new Map();
+
+ for ( const [ attributeName, binding ] of Object.entries(
+ blockBindings
+ ) ) {
+ const { source: sourceName, args: sourceArgs } = binding;
+ const source = registeredSources[ sourceName ];
+ if ( ! source || ! canBindAttribute( name, attributeName ) ) {
+ continue;
+ }
+
+ blockBindingsBySource.set( source, {
+ ...blockBindingsBySource.get( source ),
+ [ attributeName ]: {
+ args: sourceArgs,
+ },
+ } );
+ }
+
+ if ( blockBindingsBySource.size ) {
+ for ( const [ source, bindings ] of blockBindingsBySource ) {
+ // Get values in batch if the source supports it.
+ let values = {};
+ if ( ! source.getValues ) {
+ Object.keys( bindings ).forEach( ( attr ) => {
+ // Default to the the source label when `getValues` doesn't exist.
+ values[ attr ] = source.label;
+ } );
+ } else {
+ values = source.getValues( {
+ select,
+ context,
+ clientId,
+ bindings,
+ } );
+ }
+ for ( const [ attributeName, value ] of Object.entries(
+ values
+ ) ) {
+ if (
+ attributeName === 'url' &&
+ ( ! value || ! isURLLike( value ) )
+ ) {
+ // Return null if value is not a valid URL.
+ attributesFromSources[ attributeName ] = null;
+ } else {
+ attributesFromSources[ attributeName ] = value;
+ }
+ }
+ }
+ }
+
+ return {
+ ...attributes,
+ ...attributesFromSources,
+ };
+ },
+ [
+ attributes,
+ blockBindings,
+ clientId,
+ context,
+ name,
+ registeredSources,
+ ]
+ );
+
+ const setBoundAttributes = useCallback(
+ ( nextAttributes ) => {
+ if ( ! blockBindings ) {
+ setAttributes( nextAttributes );
+ return;
+ }
+
+ registry.batch( () => {
+ const keptAttributes = { ...nextAttributes };
+ const blockBindingsBySource = new Map();
+
+ // Loop only over the updated attributes to avoid modifying the bound ones that haven't changed.
+ for ( const [ attributeName, newValue ] of Object.entries(
+ keptAttributes
+ ) ) {
+ if (
+ ! blockBindings[ attributeName ] ||
+ ! canBindAttribute( name, attributeName )
+ ) {
+ continue;
+ }
+
+ const binding = blockBindings[ attributeName ];
+ const source = registeredSources[ binding?.source ];
+ if ( ! source?.setValues ) {
+ continue;
+ }
+ blockBindingsBySource.set( source, {
+ ...blockBindingsBySource.get( source ),
+ [ attributeName ]: {
+ args: binding.args,
+ newValue,
+ },
+ } );
+ delete keptAttributes[ attributeName ];
+ }
+
+ if ( blockBindingsBySource.size ) {
+ for ( const [
+ source,
+ bindings,
+ ] of blockBindingsBySource ) {
+ source.setValues( {
+ select: registry.select,
+ dispatch: registry.dispatch,
+ context,
+ clientId,
+ bindings,
+ } );
+ }
+ }
+
+ const hasParentPattern = !! context[ 'pattern/overrides' ];
+
+ if (
+ // Don't update non-connected attributes if the block is using pattern overrides
+ // and the editing is happening while overriding the pattern (not editing the original).
+ ! ( hasPatternOverrides && hasParentPattern ) &&
+ Object.keys( keptAttributes ).length
+ ) {
+ // Don't update caption and href until they are supported.
+ if ( hasPatternOverrides ) {
+ delete keptAttributes.caption;
+ delete keptAttributes.href;
+ }
+ setAttributes( keptAttributes );
+ }
+ } );
+ },
+ [
+ blockBindings,
+ clientId,
+ context,
+ hasPatternOverrides,
+ setAttributes,
+ registeredSources,
+ name,
+ registry,
+ ]
+ );
if ( ! blockType ) {
return null;
}
if ( blockType.apiVersion > 1 ) {
- return ;
+ return (
+
+ );
}
// Generate a class name for the block's editable form.
@@ -77,15 +282,17 @@ const EditWithGeneratedProps = ( props ) => {
: null;
const className = clsx(
generatedClassName,
- attributes.className,
+ attributes?.className,
props.className
);
return (
);
};
diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.js b/packages/block-editor/src/components/block-list/use-block-props/index.js
index 554adccdc22c9d..14cda82fe7cd26 100644
--- a/packages/block-editor/src/components/block-list/use-block-props/index.js
+++ b/packages/block-editor/src/components/block-list/use-block-props/index.js
@@ -29,7 +29,7 @@ import { useBlockRefProvider } from './use-block-refs';
import { useIntersectionObserver } from './use-intersection-observer';
import { useScrollIntoView } from './use-scroll-into-view';
import { useFlashEditableBlocks } from '../../use-flash-editable-blocks';
-import { canBindBlock } from '../../../hooks/use-bindings-attributes';
+import { canBindBlock } from '../../../utils/block-bindings';
import { useFirefoxDraggableCompatibility } from './use-firefox-draggable-compatibility';
/**
diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js
index bc8eca6ea94d05..768ffbb0cdd2dc 100644
--- a/packages/block-editor/src/components/rich-text/index.js
+++ b/packages/block-editor/src/components/rich-text/index.js
@@ -39,7 +39,7 @@ import FormatEdit from './format-edit';
import { getAllowedFormats } from './utils';
import { Content, valueToHTMLString } from './content';
import { withDeprecations } from './with-deprecations';
-import { canBindBlock } from '../../hooks/use-bindings-attributes';
+import { canBindBlock } from '../../utils/block-bindings';
import BlockContext from '../block-context';
export const keyboardShortcutContext = createContext();
diff --git a/packages/block-editor/src/hooks/block-bindings.js b/packages/block-editor/src/hooks/block-bindings.js
index e10696cc1257d7..2dab67d6293328 100644
--- a/packages/block-editor/src/hooks/block-bindings.js
+++ b/packages/block-editor/src/hooks/block-bindings.js
@@ -26,12 +26,12 @@ import { useViewportMatch } from '@wordpress/compose';
import {
canBindAttribute,
getBindableAttributes,
-} from '../hooks/use-bindings-attributes';
+ useBlockBindingsUtils,
+} from '../utils/block-bindings';
import { unlock } from '../lock-unlock';
import InspectorControls from '../components/inspector-controls';
import BlockContext from '../components/block-context';
import { useBlockEditContext } from '../components/block-edit';
-import { useBlockBindingsUtils } from '../utils/block-bindings';
import { store as blockEditorStore } from '../store';
const { Menu } = unlock( componentsPrivateApis );
diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js
index 66ff60b691b66f..7f9b29376ad1fb 100644
--- a/packages/block-editor/src/hooks/index.js
+++ b/packages/block-editor/src/hooks/index.js
@@ -32,7 +32,6 @@ import './metadata';
import blockHooks from './block-hooks';
import blockBindingsPanel from './block-bindings';
import './block-renaming';
-import './use-bindings-attributes';
import './grid-visualizer';
createBlockEditFilter(
diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js
deleted file mode 100644
index fdc617fda20c05..00000000000000
--- a/packages/block-editor/src/hooks/use-bindings-attributes.js
+++ /dev/null
@@ -1,322 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { store as blocksStore } from '@wordpress/blocks';
-import { createHigherOrderComponent } from '@wordpress/compose';
-import { useRegistry, useSelect } from '@wordpress/data';
-import { useCallback, useMemo, useContext } from '@wordpress/element';
-import { addFilter } from '@wordpress/hooks';
-
-/**
- * Internal dependencies
- */
-import isURLLike from '../components/link-control/is-url-like';
-import { unlock } from '../lock-unlock';
-import BlockContext from '../components/block-context';
-
-/** @typedef {import('@wordpress/compose').WPHigherOrderComponent} WPHigherOrderComponent */
-/** @typedef {import('@wordpress/blocks').WPBlockSettings} WPBlockSettings */
-
-/**
- * Given a binding of block attributes, returns a higher order component that
- * overrides its `attributes` and `setAttributes` props to sync any changes needed.
- *
- * @return {WPHigherOrderComponent} Higher-order component.
- */
-
-const BLOCK_BINDINGS_ALLOWED_BLOCKS = {
- 'core/paragraph': [ 'content' ],
- 'core/heading': [ 'content' ],
- 'core/image': [ 'id', 'url', 'title', 'alt' ],
- 'core/button': [ 'url', 'text', 'linkTarget', 'rel' ],
-};
-
-const DEFAULT_ATTRIBUTE = '__default';
-
-/**
- * Returns the bindings with the `__default` binding for pattern overrides
- * replaced with the full-set of supported attributes. e.g.:
- *
- * bindings passed in: `{ __default: { source: 'core/pattern-overrides' } }`
- * bindings returned: `{ content: { source: 'core/pattern-overrides' } }`
- *
- * @param {string} blockName The block name (e.g. 'core/paragraph').
- * @param {Object} bindings A block's bindings from the metadata attribute.
- *
- * @return {Object} The bindings with default replaced for pattern overrides.
- */
-function replacePatternOverrideDefaultBindings( blockName, bindings ) {
- // The `__default` binding currently only works for pattern overrides.
- if (
- bindings?.[ DEFAULT_ATTRIBUTE ]?.source === 'core/pattern-overrides'
- ) {
- const supportedAttributes = BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ];
- const bindingsWithDefaults = {};
- for ( const attributeName of supportedAttributes ) {
- // If the block has mixed binding sources, retain any non pattern override bindings.
- const bindingSource = bindings[ attributeName ]
- ? bindings[ attributeName ]
- : { source: 'core/pattern-overrides' };
- bindingsWithDefaults[ attributeName ] = bindingSource;
- }
-
- return bindingsWithDefaults;
- }
-
- return bindings;
-}
-
-/**
- * Based on the given block name,
- * check if it is possible to bind the block.
- *
- * @param {string} blockName - The block name.
- * @return {boolean} Whether it is possible to bind the block to sources.
- */
-export function canBindBlock( blockName ) {
- return blockName in BLOCK_BINDINGS_ALLOWED_BLOCKS;
-}
-
-/**
- * Based on the given block name and attribute name,
- * check if it is possible to bind the block attribute.
- *
- * @param {string} blockName - The block name.
- * @param {string} attributeName - The attribute name.
- * @return {boolean} Whether it is possible to bind the block attribute.
- */
-export function canBindAttribute( blockName, attributeName ) {
- return (
- canBindBlock( blockName ) &&
- BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ].includes( attributeName )
- );
-}
-
-export function getBindableAttributes( blockName ) {
- return BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ];
-}
-
-export const withBlockBindingSupport = createHigherOrderComponent(
- ( BlockEdit ) => ( props ) => {
- const registry = useRegistry();
- const blockContext = useContext( BlockContext );
- const sources = useSelect( ( select ) =>
- unlock( select( blocksStore ) ).getAllBlockBindingsSources()
- );
- const { name, clientId, context, setAttributes } = props;
- const blockBindings = useMemo(
- () =>
- replacePatternOverrideDefaultBindings(
- name,
- props.attributes.metadata?.bindings
- ),
- [ props.attributes.metadata?.bindings, name ]
- );
-
- // While this hook doesn't directly call any selectors, `useSelect` is
- // used purposely here to ensure `boundAttributes` is updated whenever
- // there are attribute updates.
- // `source.getValues` may also call a selector via `registry.select`.
- const updatedContext = {};
- const boundAttributes = useSelect(
- ( select ) => {
- if ( ! blockBindings ) {
- return;
- }
-
- const attributes = {};
-
- const blockBindingsBySource = new Map();
-
- for ( const [ attributeName, binding ] of Object.entries(
- blockBindings
- ) ) {
- const { source: sourceName, args: sourceArgs } = binding;
- const source = sources[ sourceName ];
- if (
- ! source ||
- ! canBindAttribute( name, attributeName )
- ) {
- continue;
- }
-
- // Populate context.
- for ( const key of source.usesContext || [] ) {
- updatedContext[ key ] = blockContext[ key ];
- }
-
- blockBindingsBySource.set( source, {
- ...blockBindingsBySource.get( source ),
- [ attributeName ]: {
- args: sourceArgs,
- },
- } );
- }
-
- if ( blockBindingsBySource.size ) {
- for ( const [
- source,
- bindings,
- ] of blockBindingsBySource ) {
- // Get values in batch if the source supports it.
- let values = {};
- if ( ! source.getValues ) {
- Object.keys( bindings ).forEach( ( attr ) => {
- // Default to the the source label when `getValues` doesn't exist.
- values[ attr ] = source.label;
- } );
- } else {
- values = source.getValues( {
- select,
- context: updatedContext,
- clientId,
- bindings,
- } );
- }
- for ( const [ attributeName, value ] of Object.entries(
- values
- ) ) {
- if (
- attributeName === 'url' &&
- ( ! value || ! isURLLike( value ) )
- ) {
- // Return null if value is not a valid URL.
- attributes[ attributeName ] = null;
- } else {
- attributes[ attributeName ] = value;
- }
- }
- }
- }
-
- return attributes;
- },
- [ blockBindings, name, clientId, updatedContext, sources ]
- );
-
- const hasParentPattern = !! updatedContext[ 'pattern/overrides' ];
- const hasPatternOverridesDefaultBinding =
- props.attributes.metadata?.bindings?.[ DEFAULT_ATTRIBUTE ]
- ?.source === 'core/pattern-overrides';
-
- const _setAttributes = useCallback(
- ( nextAttributes ) => {
- registry.batch( () => {
- if ( ! blockBindings ) {
- setAttributes( nextAttributes );
- return;
- }
-
- const keptAttributes = { ...nextAttributes };
- const blockBindingsBySource = new Map();
-
- // Loop only over the updated attributes to avoid modifying the bound ones that haven't changed.
- for ( const [ attributeName, newValue ] of Object.entries(
- keptAttributes
- ) ) {
- if (
- ! blockBindings[ attributeName ] ||
- ! canBindAttribute( name, attributeName )
- ) {
- continue;
- }
-
- const binding = blockBindings[ attributeName ];
- const source = sources[ binding?.source ];
- if ( ! source?.setValues ) {
- continue;
- }
- blockBindingsBySource.set( source, {
- ...blockBindingsBySource.get( source ),
- [ attributeName ]: {
- args: binding.args,
- newValue,
- },
- } );
- delete keptAttributes[ attributeName ];
- }
-
- if ( blockBindingsBySource.size ) {
- for ( const [
- source,
- bindings,
- ] of blockBindingsBySource ) {
- source.setValues( {
- select: registry.select,
- dispatch: registry.dispatch,
- context: updatedContext,
- clientId,
- bindings,
- } );
- }
- }
-
- if (
- // Don't update non-connected attributes if the block is using pattern overrides
- // and the editing is happening while overriding the pattern (not editing the original).
- ! (
- hasPatternOverridesDefaultBinding &&
- hasParentPattern
- ) &&
- Object.keys( keptAttributes ).length
- ) {
- // Don't update caption and href until they are supported.
- if ( hasPatternOverridesDefaultBinding ) {
- delete keptAttributes?.caption;
- delete keptAttributes?.href;
- }
- setAttributes( keptAttributes );
- }
- } );
- },
- [
- registry,
- blockBindings,
- name,
- clientId,
- updatedContext,
- setAttributes,
- sources,
- hasPatternOverridesDefaultBinding,
- hasParentPattern,
- ]
- );
-
- return (
- <>
-
- >
- );
- },
- 'withBlockBindingSupport'
-);
-
-/**
- * Filters a registered block's settings to enhance a block's `edit` component
- * to upgrade bound attributes.
- *
- * @param {WPBlockSettings} settings - Registered block settings.
- * @param {string} name - Block name.
- * @return {WPBlockSettings} Filtered block settings.
- */
-function shimAttributeSource( settings, name ) {
- if ( ! canBindBlock( name ) ) {
- return settings;
- }
-
- return {
- ...settings,
- edit: withBlockBindingSupport( settings.edit ),
- };
-}
-
-addFilter(
- 'blocks.registerBlockType',
- 'core/editor/custom-sources-backwards-compatibility/shim-attribute-source',
- shimAttributeSource
-);
diff --git a/packages/block-editor/src/utils/block-bindings.js b/packages/block-editor/src/utils/block-bindings.js
index dcf80d985473b2..9a4c6acf9a9032 100644
--- a/packages/block-editor/src/utils/block-bindings.js
+++ b/packages/block-editor/src/utils/block-bindings.js
@@ -9,10 +9,105 @@ import { useDispatch, useRegistry } from '@wordpress/data';
import { store as blockEditorStore } from '../store';
import { useBlockEditContext } from '../components/block-edit';
+const DEFAULT_ATTRIBUTE = '__default';
+const PATTERN_OVERRIDES_SOURCE = 'core/pattern-overrides';
+const BLOCK_BINDINGS_ALLOWED_BLOCKS = {
+ 'core/paragraph': [ 'content' ],
+ 'core/heading': [ 'content' ],
+ 'core/image': [ 'id', 'url', 'title', 'alt' ],
+ 'core/button': [ 'url', 'text', 'linkTarget', 'rel' ],
+};
+
+/**
+ * Checks if the given object is empty.
+ *
+ * @param {?Object} object The object to check.
+ *
+ * @return {boolean} Whether the object is empty.
+ */
function isObjectEmpty( object ) {
return ! object || Object.keys( object ).length === 0;
}
+/**
+ * Based on the given block name, checks if it is possible to bind the block.
+ *
+ * @param {string} blockName The name of the block.
+ *
+ * @return {boolean} Whether it is possible to bind the block to sources.
+ */
+export function canBindBlock( blockName ) {
+ return blockName in BLOCK_BINDINGS_ALLOWED_BLOCKS;
+}
+
+/**
+ * Based on the given block name and attribute name, checks if it is possible to bind the block attribute.
+ *
+ * @param {string} blockName The name of the block.
+ * @param {string} attributeName The name of attribute.
+ *
+ * @return {boolean} Whether it is possible to bind the block attribute.
+ */
+export function canBindAttribute( blockName, attributeName ) {
+ return (
+ canBindBlock( blockName ) &&
+ BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ].includes( attributeName )
+ );
+}
+
+/**
+ * Gets the bindable attributes for a given block.
+ *
+ * @param {string} blockName The name of the block.
+ *
+ * @return {string[]} The bindable attributes for the block.
+ */
+export function getBindableAttributes( blockName ) {
+ return BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ];
+}
+
+/**
+ * Checks if the block has the `__default` binding for pattern overrides.
+ *
+ * @param {?Record} bindings A block's bindings from the metadata attribute.
+ *
+ * @return {boolean} Whether the block has the `__default` binding for pattern overrides.
+ */
+export function hasPatternOverridesDefaultBinding( bindings ) {
+ return bindings?.[ DEFAULT_ATTRIBUTE ]?.source === PATTERN_OVERRIDES_SOURCE;
+}
+
+/**
+ * Returns the bindings with the `__default` binding for pattern overrides
+ * replaced with the full-set of supported attributes. e.g.:
+ *
+ * - bindings passed in: `{ __default: { source: 'core/pattern-overrides' } }`
+ * - bindings returned: `{ content: { source: 'core/pattern-overrides' } }`
+ *
+ * @param {string} blockName The block name (e.g. 'core/paragraph').
+ * @param {?Record} bindings A block's bindings from the metadata attribute.
+ *
+ * @return {Object} The bindings with default replaced for pattern overrides.
+ */
+export function replacePatternOverridesDefaultBinding( blockName, bindings ) {
+ // The `__default` binding currently only works for pattern overrides.
+ if ( hasPatternOverridesDefaultBinding( bindings ) ) {
+ const supportedAttributes = BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ];
+ const bindingsWithDefaults = {};
+ for ( const attributeName of supportedAttributes ) {
+ // If the block has mixed binding sources, retain any non pattern override bindings.
+ const bindingSource = bindings[ attributeName ]
+ ? bindings[ attributeName ]
+ : { source: PATTERN_OVERRIDES_SOURCE };
+ bindingsWithDefaults[ attributeName ] = bindingSource;
+ }
+
+ return bindingsWithDefaults;
+ }
+
+ return bindings;
+}
+
/**
* Contains utils to update the block `bindings` metadata.
*
diff --git a/packages/e2e-tests/plugins/block-bindings.php b/packages/e2e-tests/plugins/block-bindings.php
index b86673c2c523d0..1fd6d8468c77db 100644
--- a/packages/e2e-tests/plugins/block-bindings.php
+++ b/packages/e2e-tests/plugins/block-bindings.php
@@ -41,7 +41,11 @@ function gutenberg_test_block_bindings_registration() {
plugins_url( 'block-bindings/index.js', __FILE__ ),
array(
'wp-blocks',
- 'wp-private-apis',
+ 'wp-block-editor',
+ 'wp-components',
+ 'wp-compose',
+ 'wp-element',
+ 'wp-hooks',
),
filemtime( plugin_dir_path( __FILE__ ) . 'block-bindings/index.js' ),
true
diff --git a/packages/e2e-tests/plugins/block-bindings/index.js b/packages/e2e-tests/plugins/block-bindings/index.js
index 5c364257caed19..63c463e197fa8a 100644
--- a/packages/e2e-tests/plugins/block-bindings/index.js
+++ b/packages/e2e-tests/plugins/block-bindings/index.js
@@ -1,4 +1,9 @@
const { registerBlockBindingsSource } = wp.blocks;
+const { InspectorControls } = wp.blockEditor;
+const { PanelBody, TextControl } = wp.components;
+const { createHigherOrderComponent } = wp.compose;
+const { createElement: el, Fragment } = wp.element;
+const { addFilter } = wp.hooks;
const { fieldsList } = window.testingBindings || {};
const getValues = ( { bindings } ) => {
@@ -46,3 +51,43 @@ registerBlockBindingsSource( {
getValues,
canUserEditValue: () => true,
} );
+
+const withBlockBindingsInspectorControl = createHigherOrderComponent(
+ ( BlockEdit ) => {
+ return ( props ) => {
+ if ( ! props.attributes?.metadata?.bindings?.content ) {
+ return el( BlockEdit, props );
+ }
+
+ return el(
+ Fragment,
+ {},
+ el( BlockEdit, props ),
+ el(
+ InspectorControls,
+ {},
+ el(
+ PanelBody,
+ { title: 'Bindings' },
+ el( TextControl, {
+ __next40pxDefaultSize: true,
+ __nextHasNoMarginBottom: true,
+ label: 'Content',
+ value: props.attributes.content,
+ onChange: ( content ) =>
+ props.setAttributes( {
+ content,
+ } ),
+ } )
+ )
+ )
+ );
+ };
+ }
+);
+
+addFilter(
+ 'editor.BlockEdit',
+ 'testing/bindings-inspector-control',
+ withBlockBindingsInspectorControl
+);
diff --git a/test/e2e/specs/editor/various/block-bindings/post-meta.spec.js b/test/e2e/specs/editor/various/block-bindings/post-meta.spec.js
index 32334bfc777f2a..318707e22f098d 100644
--- a/test/e2e/specs/editor/various/block-bindings/post-meta.spec.js
+++ b/test/e2e/specs/editor/various/block-bindings/post-meta.spec.js
@@ -524,6 +524,47 @@ test.describe( 'Post Meta source', () => {
previewPage.locator( '#connected-paragraph' )
).toHaveText( 'new value' );
} );
+
+ test( 'should be possible to edit the value of the connected custom fields in the inspector control registered by the plugin', async ( {
+ editor,
+ page,
+ } ) => {
+ await editor.insertBlock( {
+ name: 'core/paragraph',
+ attributes: {
+ anchor: 'connected-paragraph',
+ content: 'fallback content',
+ metadata: {
+ bindings: {
+ content: {
+ source: 'core/post-meta',
+ args: {
+ key: 'movie_field',
+ },
+ },
+ },
+ },
+ },
+ } );
+ const contentInput = page.getByRole( 'textbox', {
+ name: 'Content',
+ } );
+ await expect( contentInput ).toHaveValue(
+ 'Movie field default value'
+ );
+ await contentInput.fill( 'new value' );
+ // Check that the paragraph content attribute didn't change.
+ const [ paragraphBlockObject ] = await editor.getBlocks();
+ expect( paragraphBlockObject.attributes.content ).toBe(
+ 'fallback content'
+ );
+ // Check the value of the custom field is being updated by visiting the frontend.
+ const previewPage = await editor.openPreviewPage();
+ await expect(
+ previewPage.locator( '#connected-paragraph' )
+ ).toHaveText( 'new value' );
+ } );
+
test( 'should be possible to connect movie fields through the attributes panel', async ( {
editor,
page,