diff --git a/backport-changelog/6.9/9933.md b/backport-changelog/6.9/9933.md
new file mode 100644
index 00000000000000..35411c74e63851
--- /dev/null
+++ b/backport-changelog/6.9/9933.md
@@ -0,0 +1,3 @@
+https://github.com/WordPress/wordpress-develop/pull/9933
+
+* https://github.com/WordPress/gutenberg/pull/71542
diff --git a/lib/compat/wordpress-6.9/post-data-block-bindings.php b/lib/compat/wordpress-6.9/post-data-block-bindings.php
index 7541b2f564eb17..4359b56c5e4b37 100644
--- a/lib/compat/wordpress-6.9/post-data-block-bindings.php
+++ b/lib/compat/wordpress-6.9/post-data-block-bindings.php
@@ -65,7 +65,7 @@ function gutenberg_register_block_bindings_post_data_source() {
array(
'label' => _x( 'Post Data', 'block bindings source' ),
'get_value_callback' => 'gutenberg_block_bindings_post_data_get_value',
- 'uses_context' => array( 'postId' ),
+ 'uses_context' => array( 'postId', 'postType' ),
)
);
}
diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js
index ca330dd4e71589..2a2ec59d3d0c87 100644
--- a/packages/block-editor/src/components/rich-text/index.js
+++ b/packages/block-editor/src/components/rich-text/index.js
@@ -2,6 +2,7 @@
* External dependencies
*/
import clsx from 'clsx';
+import fastDeepEqual from 'fast-deep-equal/es6';
/**
* WordPress dependencies
@@ -218,13 +219,19 @@ export function RichTextWrapper(
const { getBlockAttributes } = select( blockEditorStore );
const blockAttributes = getBlockAttributes( clientId );
- const fieldsList = blockBindingsSource?.getFieldsList?.( {
- select,
- context: blockBindingsContext,
- } );
+ let clientSideFieldLabel = null;
+ if ( blockBindingsSource?.editorUI ) {
+ const editorUIResult = blockBindingsSource.editorUI( {
+ select,
+ context: blockBindingsContext,
+ } );
+ clientSideFieldLabel = editorUIResult.data?.find( ( item ) =>
+ fastDeepEqual( item.args, relatedBinding?.args )
+ )?.label;
+ }
+
const bindingKey =
- fieldsList?.[ relatedBinding?.args?.key ]?.label ??
- blockBindingsSource?.label;
+ clientSideFieldLabel ?? blockBindingsSource?.label;
const _bindingsPlaceholder = _disableBoundBlock
? bindingKey
diff --git a/packages/block-editor/src/hooks/block-bindings.js b/packages/block-editor/src/hooks/block-bindings.js
index 2911aac1b256c1..87ab4e36321028 100644
--- a/packages/block-editor/src/hooks/block-bindings.js
+++ b/packages/block-editor/src/hooks/block-bindings.js
@@ -1,12 +1,13 @@
+/**
+ * External dependencies
+ */
+import fastDeepEqual from 'fast-deep-equal/es6';
+
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
-import {
- getBlockBindingsSource,
- getBlockBindingsSources,
- getBlockType,
-} from '@wordpress/blocks';
+import { getBlockBindingsSources, getBlockType } from '@wordpress/blocks';
import {
__experimentalItemGroup as ItemGroup,
__experimentalItem as Item,
@@ -14,10 +15,11 @@ import {
__experimentalToolsPanel as ToolsPanel,
__experimentalToolsPanelItem as ToolsPanelItem,
__experimentalVStack as VStack,
+ Modal,
privateApis as componentsPrivateApis,
} from '@wordpress/components';
import { useSelect } from '@wordpress/data';
-import { useContext, Fragment } from '@wordpress/element';
+import { useContext, useState } from '@wordpress/element';
import { useViewportMatch } from '@wordpress/compose';
/**
@@ -47,72 +49,166 @@ const useToolsPanelDropdownMenuProps = () => {
: {};
};
-function BlockBindingsPanelMenuContent( { fieldsList, attribute, binding } ) {
+function BlockBindingsPanelMenuContent( {
+ attribute,
+ binding,
+ sources,
+ onOpenModal,
+} ) {
const { clientId } = useBlockEditContext();
- const registeredSources = getBlockBindingsSources();
const { updateBlockBindings } = useBlockBindingsUtils();
- const currentKey = binding?.args?.key;
- const attributeType = useSelect(
- ( select ) => {
+ const isMobile = useViewportMatch( 'medium', '<' );
+ const blockContext = useContext( BlockContext );
+ const { attributeType, select } = useSelect(
+ ( _select ) => {
const { name: blockName } =
- select( blockEditorStore ).getBlock( clientId );
+ _select( blockEditorStore ).getBlock( clientId );
const _attributeType =
getBlockType( blockName ).attributes?.[ attribute ]?.type;
- return _attributeType === 'rich-text' ? 'string' : _attributeType;
+ return {
+ attributeType:
+ _attributeType === 'rich-text' ? 'string' : _attributeType,
+ select: _select,
+ };
},
[ clientId, attribute ]
);
return (
- <>
- { Object.entries( fieldsList ).map( ( [ name, fields ], i ) => (
-
-
- { Object.keys( fieldsList ).length > 1 && (
-
- { registeredSources[ name ].label }
-
- ) }
- { Object.entries( fields )
- .filter(
- ( [ , args ] ) => args?.type === attributeType
- )
- .map( ( [ key, args ] ) => (
-
- updateBlockBindings( {
- [ attribute ]: {
- source: name,
- args: { key },
- },
- } )
- }
- name={ attribute + '-binding' }
- value={ key }
- checked={ key === currentKey }
- >
+
- ) ) }
-
- { i !== Object.keys( fieldsList ).length - 1 && (
-
- ) }
-
- ) ) }
- >
+
+ ) }
+
+ { ! noItemsAvailable && (
+
+
+ { sourceDataItems.map( ( item ) => {
+ const itemBindings = {
+ source: sourceKey,
+ args: item?.args || {
+ key: item.key,
+ },
+ };
+ const values = source.getValues( {
+ select,
+ context: blockContext,
+ bindings: {
+ [ attribute ]: itemBindings,
+ },
+ } );
+ return (
+ {
+ const isCurrentlySelected =
+ fastDeepEqual(
+ binding?.args,
+ item.args
+ ) ??
+ // Deprecate key dependency in 7.0.
+ item.key ===
+ binding?.args
+ ?.key;
+
+ if (
+ isCurrentlySelected
+ ) {
+ // Unset if the same item is selected again.
+ updateBlockBindings(
+ {
+ [ attribute ]:
+ undefined,
+ }
+ );
+ } else {
+ updateBlockBindings(
+ {
+ [ attribute ]:
+ itemBindings,
+ }
+ );
+ }
+ } }
+ name={
+ attribute + '-binding'
+ }
+ value={
+ values[ attribute ]
+ }
+ checked={
+ fastDeepEqual(
+ binding?.args,
+ item.args
+ ) ??
+ // Deprecate key dependency in 7.0.
+ item.key ===
+ binding?.args?.key
+ }
+ >
+
+ { item?.label }
+
+
+ { values[ attribute ] }
+
+
+ );
+ } ) }
+
+
+ ) }
+
+ );
+ }
+
+ if ( source.mode === 'modal' ) {
+ return (
+ onOpenModal( { sourceKey } ) }
+ >
+ { source.label }
+
+ );
+ }
+
+ return null;
+ } ) }
+
);
}
-function BlockBindingsAttribute( { attribute, binding, fieldsList } ) {
+function BlockBindingsAttribute( { attribute, binding, source } ) {
const { source: sourceName, args } = binding || {};
- const sourceProps = getBlockBindingsSource( sourceName );
- const isSourceInvalid = ! sourceProps;
+ const isSourceInvalid = ! source;
return (
{ attribute }
@@ -124,8 +220,10 @@ function BlockBindingsAttribute( { attribute, binding, fieldsList } ) {
>
{ isSourceInvalid
? __( 'Invalid source' )
- : fieldsList?.[ sourceName ]?.[ args?.key ]?.label ||
- sourceProps?.label ||
+ : source?.data?.find( ( item ) =>
+ fastDeepEqual( item.args, args )
+ )?.label ||
+ source?.label ||
sourceName }
) }
@@ -133,68 +231,61 @@ function BlockBindingsAttribute( { attribute, binding, fieldsList } ) {
);
}
-function ReadOnlyBlockBindingsPanelItems( { bindings, fieldsList } ) {
+function ReadOnlyBlockBindingsPanelItem( { attribute, binding, source } ) {
return (
- <>
- { Object.entries( bindings ).map( ( [ attribute, binding ] ) => (
- -
-
-
- ) ) }
- >
+ !! binding } label={ attribute }>
+ -
+
+
+
);
}
-function EditableBlockBindingsPanelItems( {
- attributes,
- bindings,
- fieldsList,
+function EditableBlockBindingsPanelItem( {
+ attribute,
+ binding,
+ sources,
+ setModalState,
} ) {
const { updateBlockBindings } = useBlockBindingsUtils();
const isMobile = useViewportMatch( 'medium', '<' );
+
+ const handleOpenModal = ( { sourceKey } ) => {
+ setModalState( { attribute, sourceKey } );
+ };
+
return (
- <>
- { attributes.map( ( attribute ) => {
- const binding = bindings[ attribute ];
- return (
- !! binding }
- label={ attribute }
- onDeselect={ () => {
- updateBlockBindings( {
- [ attribute ]: undefined,
- } );
- } }
- >
-
- }>
-
-
-
-
-
-
-
- );
- } ) }
- >
+ !! binding }
+ label={ attribute }
+ onDeselect={ () => {
+ updateBlockBindings( {
+ [ attribute ]: undefined,
+ } );
+ } }
+ >
+
+ }>
+
+
+
+
+
+
+
);
}
@@ -202,85 +293,169 @@ export const BlockBindingsPanel = ( { name: blockName, metadata } ) => {
const blockContext = useContext( BlockContext );
const { removeAllBlockBindings } = useBlockBindingsUtils();
const dropdownMenuProps = useToolsPanelDropdownMenuProps();
+ const [ modalState, setModalState ] = useState( null );
- // `useSelect` is used purposely here to ensure `getFieldsList`
- // is updated whenever there are updates in block context.
- // `source.getFieldsList` may also call a selector via `select`.
- const _fieldsList = {};
- const { bindableAttributes, fieldsList, canUpdateBlockBindings } =
- useSelect(
- ( select ) => {
- const { __experimentalBlockBindingsSupportedAttributes } =
- select( blockEditorStore ).getSettings();
- const _bindableAttributes =
- __experimentalBlockBindingsSupportedAttributes?.[
- blockName
- ];
- if (
- ! _bindableAttributes ||
- _bindableAttributes.length === 0
- ) {
- return EMPTY_OBJECT;
- }
- const registeredSources = getBlockBindingsSources();
- Object.entries( registeredSources ).forEach(
- ( [ sourceName, { getFieldsList, usesContext } ] ) => {
- if ( getFieldsList ) {
- // Populate context.
- const context = {};
- if ( usesContext?.length ) {
- for ( const key of usesContext ) {
- context[ key ] = blockContext[ key ];
- }
+ const handleCloseModal = () => {
+ setModalState( null );
+ };
+
+ // Use useSelect to ensure sources are updated whenever there are updates in block context
+ // or when underlying data changes.
+ // Still needs a fix regarding _sources scope.
+ const _sources = {};
+ const { sources, canUpdateBlockBindings, bindableAttributes } = useSelect(
+ ( select ) => {
+ const { __experimentalBlockBindingsSupportedAttributes } =
+ select( blockEditorStore ).getSettings();
+ const _bindableAttributes =
+ __experimentalBlockBindingsSupportedAttributes?.[ blockName ];
+ if ( ! _bindableAttributes || _bindableAttributes.length === 0 ) {
+ return EMPTY_OBJECT;
+ }
+
+ const registeredSources = getBlockBindingsSources();
+ Object.entries( registeredSources ).forEach(
+ ( [
+ sourceName,
+ { editorUI, getFieldsList, usesContext, label, getValues },
+ ] ) => {
+ if ( editorUI ) {
+ // Populate context.
+ const context = {};
+ if ( usesContext?.length ) {
+ for ( const key of usesContext ) {
+ context[ key ] = blockContext[ key ];
}
- const sourceList = getFieldsList( {
- select,
- context,
- } );
- // Only add source if the list is not empty.
- if ( Object.keys( sourceList || {} ).length ) {
- _fieldsList[ sourceName ] = { ...sourceList };
+ }
+
+ const editorUIResult = editorUI( {
+ select,
+ context,
+ } );
+ const hasCompatibleData = _bindableAttributes.some(
+ ( attribute ) => {
+ const _attributeType =
+ getBlockType( blockName ).attributes?.[
+ attribute
+ ]?.type;
+ const attributeType =
+ _attributeType === 'rich-text'
+ ? 'string'
+ : _attributeType;
+
+ return editorUIResult.data?.some(
+ ( item ) => item?.type === attributeType
+ );
}
+ );
+
+ if ( hasCompatibleData ) {
+ _sources[ sourceName ] = {
+ ...editorUIResult,
+ label,
+ getValues,
+ };
}
+ } else if ( getFieldsList ) {
+ // Backward compatibility: Convert getFieldsList to editorUI format
+ const context = {};
+ if ( usesContext?.length ) {
+ for ( const key of usesContext ) {
+ context[ key ] = blockContext[ key ];
+ }
+ }
+
+ const fieldsListResult = getFieldsList( {
+ select,
+ context,
+ } );
+
+ if ( fieldsListResult ) {
+ // Convert getFieldsList format to editorUI format
+ const data = Object.entries( fieldsListResult ).map(
+ ( [ key, field ] ) => ( {
+ label: field.label || key,
+ type: field.type || 'string',
+ args: { key },
+ } )
+ );
+
+ const hasCompatibleData = _bindableAttributes.some(
+ ( attribute ) => {
+ const _attributeType =
+ getBlockType( blockName ).attributes?.[
+ attribute
+ ]?.type;
+ const attributeType =
+ _attributeType === 'rich-text'
+ ? 'string'
+ : _attributeType;
+
+ return data.some(
+ ( item ) => item?.type === attributeType
+ );
+ }
+ );
+
+ if ( hasCompatibleData ) {
+ _sources[ sourceName ] = {
+ mode: 'dropdown', // Default mode for backward compatibility
+ data,
+ label,
+ getValues,
+ };
+ }
+ }
+ } else {
+ /*
+ * Include sources without editorUI if they are introduced
+ * by other means (e.g. code editor).
+ */
+ _sources[ sourceName ] = {
+ label,
+ getValues,
+ };
}
- );
- return {
- bindableAttributes: _bindableAttributes,
- fieldsList:
- Object.values( _fieldsList ).length > 0
- ? _fieldsList
- : EMPTY_OBJECT,
- canUpdateBlockBindings:
- select( blockEditorStore ).getSettings()
- .canUpdateBlockBindings,
- };
- },
- [ blockContext ]
- );
+ }
+ );
+
+ return {
+ sources:
+ Object.values( _sources ).length > 0
+ ? _sources
+ : EMPTY_OBJECT,
+ canUpdateBlockBindings:
+ select( blockEditorStore ).getSettings()
+ .canUpdateBlockBindings,
+ bindableAttributes: _bindableAttributes,
+ };
+ },
+ [ blockContext, blockName ]
+ );
// Return early if there are no bindable attributes.
if ( ! bindableAttributes || bindableAttributes.length === 0 ) {
return null;
}
- // Filter bindings to only show bindable attributes and remove pattern overrides.
+ // Filter bindings to only show bindable attributes.
const { bindings } = metadata || {};
const filteredBindings = { ...bindings };
Object.keys( filteredBindings ).forEach( ( key ) => {
- if (
- ! bindableAttributes.includes( key ) &&
- filteredBindings[ key ].source === 'core/pattern-overrides'
- ) {
+ if ( ! bindableAttributes.includes( key ) ) {
delete filteredBindings[ key ];
}
} );
// Lock the UI when the user can't update bindings or there are no fields to connect to.
const readOnly =
- ! canUpdateBlockBindings || ! Object.keys( fieldsList ).length;
+ ! canUpdateBlockBindings || ! Object.keys( sources ).length;
if ( readOnly && Object.keys( filteredBindings ).length === 0 ) {
return null;
}
+ const RenderModalContent =
+ sources[ modalState?.sourceKey ]?.renderModalContent;
+
return (
{
className="block-editor-bindings__panel"
>
- { readOnly ? (
-
- ) : (
-
- ) }
+ { bindableAttributes.map( ( attribute ) => {
+ const binding = filteredBindings[ attribute ];
+ const hasCompatibleData = Object.values( sources ).some(
+ ( source ) => source.data
+ );
+
+ return readOnly || ! hasCompatibleData ? (
+
+ ) : (
+
+ );
+ } ) }
{ /*
Use a div element to make the ToolsPanelHiddenInnerWrapper
@@ -317,6 +503,14 @@ export const BlockBindingsPanel = ( { name: blockName, metadata } ) => {
+ { RenderModalContent && (
+
+
+
+ ) }
);
};
diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js
index 249e39e8779688..93e47280f39319 100644
--- a/packages/blocks/src/api/registration.js
+++ b/packages/blocks/src/api/registration.js
@@ -123,6 +123,28 @@ import { unlock } from '../lock-unlock';
* then no preview is shown.
*/
+/**
+ * An object describing data for a field in block bindings EditorUI.
+ *
+ * @typedef {Object} WPBlockBindingsEditorUIDataItem
+ *
+ * @property {string} key The unique identifier for the field.
+ * @property {string} [label] Human-readable label for the field.
+ * @property {string} [type] The data type of the field (e.g., 'string', 'number').
+ * @property {Object} [args] Source-specific arguments used to retrieve the value from the source.
+ */
+
+/**
+ * An object describing the EditorUI configuration for block bindings.
+ *
+ * @typedef {Object} WPBlockBindingsEditorUI
+ *
+ * @property {string} mode The UI mode - either 'dropdown' or 'modal'.
+ * @property {Array} [data] Array of field data items.
+ * @property {Function} [renderModalContent] Required for modal mode. Function that renders the modal content.
+ * Receives an object with the `attribute` that is being changed, and a `closeModal` function to close the modal.
+ */
+
/**
* An object describing a Block Bindings source.
*
@@ -134,6 +156,7 @@ import { unlock } from '../lock-unlock';
* @property {Function} [getValues] Optional function to get the values from the source.
* @property {Function} [setValues] Optional function to update multiple values connected to the source.
* @property {Function} [canUserEditValue] Optional function to determine if the user can edit the value.
+ * @property {Function} [editorUI] Optional function that returns WPBlockBindingsEditorUI configuration.
*/
function isObject( object ) {
@@ -808,6 +831,7 @@ export const registerBlockBindingsSource = ( source ) => {
setValues,
canUserEditValue,
getFieldsList,
+ editorUI,
} = source;
const existingSource = unlock(
@@ -907,6 +931,109 @@ export const registerBlockBindingsSource = ( source ) => {
return;
}
+ if ( editorUI ) {
+ if ( typeof editorUI === 'function' ) {
+ // If it's a function, we can't validate the returned object at registration time
+ // since it may depend on runtime context, so we just validate it's a function
+ return unlock( dispatch( blocksStore ) ).addBlockBindingsSource(
+ source
+ );
+ }
+ if ( typeof editorUI !== 'object' || Array.isArray( editorUI ) ) {
+ warning( 'EditorUI must be an object or a function' );
+ return;
+ }
+ const { mode, data } = editorUI;
+
+ if ( mode && ! [ 'dropdown', 'modal' ].includes( mode ) ) {
+ warning( 'EditorUI mode must be either "dropdown" or "modal"' );
+ return;
+ }
+
+ if ( mode === 'dropdown' ) {
+ if ( ! data || ! Array.isArray( data ) ) {
+ warning(
+ 'EditorUI data must be an array of field objects for dropdown mode'
+ );
+ return;
+ }
+
+ for ( const field of data ) {
+ if ( ! field || typeof field !== 'object' ) {
+ warning( 'Each field must be an object' );
+ return;
+ }
+
+ if ( ! field.label || ! field.args ) {
+ warning(
+ 'Each field must have "label" and "args" properties'
+ );
+ return;
+ }
+
+ if ( typeof field.label !== 'string' ) {
+ warning( 'Field "label" property must be a string' );
+ return;
+ }
+
+ // Validate type field if provided (follows WordPress block attributes type validation).
+ if ( field.type ) {
+ const validTypes = [
+ 'null',
+ 'boolean',
+ 'object',
+ 'array',
+ 'string',
+ 'integer',
+ 'number',
+ ];
+ if ( ! validTypes.includes( field.type ) ) {
+ warning(
+ 'Field "type" must be one of: ' +
+ validTypes.join( ', ' )
+ );
+ return;
+ }
+
+ if ( field.format ) {
+ if ( field.type !== 'string' ) {
+ warning(
+ 'Field "format" can only be used with string type'
+ );
+ return;
+ }
+
+ const validFormats = [
+ 'date-time',
+ 'uri',
+ 'email',
+ 'ip',
+ 'uuid',
+ 'hex-color',
+ ];
+ if ( ! validFormats.includes( field.format ) ) {
+ warning(
+ 'Field "format" must be one of: ' +
+ validFormats.join( ', ' )
+ );
+ return;
+ }
+ }
+ }
+ }
+ } else if ( mode === 'modal' ) {
+ if ( ! editorUI.renderModalContent ) {
+ warning( 'Modal mode requires renderModalContent function' );
+ return;
+ }
+
+ if ( typeof editorUI.renderModalContent !== 'function' ) {
+ warning( 'renderModalContent must be a function' );
+ return;
+ }
+ }
+ }
+
return unlock( dispatch( blocksStore ) ).addBlockBindingsSource( source );
};
diff --git a/packages/blocks/src/api/test/registration.js b/packages/blocks/src/api/test/registration.js
index 406aa7b241c32e..8a84e0c6b99630 100644
--- a/packages/blocks/src/api/test/registration.js
+++ b/packages/blocks/src/api/test/registration.js
@@ -1730,6 +1730,184 @@ describe( 'blocks', () => {
expect( getBlockBindingsSource( 'core/testing' ) ).toBeUndefined();
} );
+ // Check EditorUI object validation
+ it( 'should reject EditorUI with invalid mode', () => {
+ registerBlockBindingsSource( {
+ name: 'core/testing',
+ label: 'testing',
+ editorUI: {
+ mode: 'invalid-mode',
+ },
+ } );
+ expect( console ).toHaveWarnedWith(
+ 'EditorUI mode must be either "dropdown" or "modal"'
+ );
+ expect( getBlockBindingsSource( 'core/testing' ) ).toBeUndefined();
+ } );
+
+ it( 'should reject dropdown mode without data array', () => {
+ registerBlockBindingsSource( {
+ name: 'core/testing',
+ label: 'testing',
+ editorUI: {
+ mode: 'dropdown',
+ data: 'not-an-array',
+ },
+ } );
+ expect( console ).toHaveWarnedWith(
+ 'EditorUI data must be an array of field objects for dropdown mode'
+ );
+ expect( getBlockBindingsSource( 'core/testing' ) ).toBeUndefined();
+ } );
+
+ it( 'should reject dropdown data with invalid field structure', () => {
+ registerBlockBindingsSource( {
+ name: 'core/testing',
+ label: 'testing',
+ editorUI: {
+ mode: 'dropdown',
+ data: [
+ { label: 'Field 1' }, // missing 'args'
+ ],
+ },
+ } );
+ expect( console ).toHaveWarnedWith(
+ 'Each field must have "label" and "args" properties'
+ );
+ expect( getBlockBindingsSource( 'core/testing' ) ).toBeUndefined();
+ } );
+
+ it( 'should reject dropdown data with invalid type', () => {
+ registerBlockBindingsSource( {
+ name: 'core/testing',
+ label: 'testing',
+ editorUI: {
+ mode: 'dropdown',
+ data: [
+ {
+ label: 'Field 1',
+ args: { testArg: 'test' },
+ type: 'invalid-type',
+ },
+ ],
+ },
+ } );
+ expect( console ).toHaveWarnedWith(
+ 'Field "type" must be one of: null, boolean, object, array, string, integer, number'
+ );
+ expect( getBlockBindingsSource( 'core/testing' ) ).toBeUndefined();
+ } );
+
+ it( 'should reject dropdown data with invalid field type', () => {
+ registerBlockBindingsSource( {
+ name: 'core/testing',
+ label: 'testing',
+ editorUI: {
+ mode: 'dropdown',
+ data: [
+ {
+ label: 'Field 1',
+ args: { testArg: 'test' },
+ type: 'invalid-type',
+ },
+ ],
+ },
+ } );
+ expect( console ).toHaveWarnedWith(
+ 'Field "type" must be one of: null, boolean, object, array, string, integer, number'
+ );
+ expect( getBlockBindingsSource( 'core/testing' ) ).toBeUndefined();
+ } );
+
+ it( 'should accept valid dropdown data with type validation', () => {
+ registerBlockBindingsSource( {
+ name: 'core/testing',
+ label: 'testing',
+ editorUI: {
+ mode: 'dropdown',
+ data: [
+ {
+ label: 'String Field',
+ args: { exampleArg: 'example' },
+ type: 'string',
+ },
+ {
+ label: 'Number Field',
+ args: { exampleArg: 'numberExample' },
+ type: 'number',
+ },
+ {
+ label: 'Boolean Field',
+ args: { exampleArg: 'boolExample' },
+ type: 'boolean',
+ },
+ ],
+ },
+ } );
+ expect( getBlockBindingsSource( 'core/testing' ) ).toBeDefined();
+ unregisterBlockBindingsSource( 'core/testing' );
+ } );
+
+ it( 'should reject modal mode without renderModalContent function', () => {
+ registerBlockBindingsSource( {
+ name: 'core/testing',
+ label: 'testing',
+ editorUI: {
+ mode: 'modal',
+ },
+ } );
+ expect( console ).toHaveWarnedWith(
+ 'Modal mode requires renderModalContent function'
+ );
+ expect( getBlockBindingsSource( 'core/testing' ) ).toBeUndefined();
+ } );
+
+ it( 'should reject modal mode with invalid renderModalContent', () => {
+ registerBlockBindingsSource( {
+ name: 'core/testing',
+ label: 'testing',
+ editorUI: {
+ mode: 'modal',
+ renderModalContent: 'not-a-function',
+ },
+ } );
+ expect( console ).toHaveWarnedWith(
+ 'renderModalContent must be a function'
+ );
+ expect( getBlockBindingsSource( 'core/testing' ) ).toBeUndefined();
+ } );
+
+ it( 'should successfully register valid dropdown mode source', () => {
+ registerBlockBindingsSource( {
+ name: 'core/testing',
+ label: 'testing',
+ editorUI: {
+ mode: 'dropdown',
+ data: [
+ { label: 'Field 1', args: { testArg: 'abc' } },
+ { label: 'Field 2', args: { testArg: 'def' } },
+ ],
+ },
+ } );
+ expect( getBlockBindingsSource( 'core/testing' ) ).toBeDefined();
+ unregisterBlockBindingsSource( 'core/testing' );
+ } );
+
+ it( 'should successfully register valid modal mode source', () => {
+ registerBlockBindingsSource( {
+ name: 'core/testing',
+ label: 'testing',
+ editorUI: {
+ mode: 'modal',
+ renderModalContent: () => {
+ return 'mock-react-element';
+ },
+ },
+ } );
+ expect( getBlockBindingsSource( 'core/testing' ) ).toBeDefined();
+ unregisterBlockBindingsSource( 'core/testing' );
+ } );
+
// Check correct sources are registered as expected.
it( 'should register a valid source', () => {
const sourceProperties = {
@@ -1738,9 +1916,6 @@ describe( 'blocks', () => {
getValues: () => 'value',
setValues: () => 'new values',
canUserEditValue: () => true,
- getFieldsList: () => {
- return { field: 'value' };
- },
};
registerBlockBindingsSource( {
name: 'core/valid-source',
diff --git a/packages/blocks/src/store/private-actions.js b/packages/blocks/src/store/private-actions.js
index 33f29a93c34832..38dd13695967d9 100644
--- a/packages/blocks/src/store/private-actions.js
+++ b/packages/blocks/src/store/private-actions.js
@@ -56,6 +56,7 @@ export function addBlockBindingsSource( source ) {
setValues: source.setValues,
canUserEditValue: source.canUserEditValue,
getFieldsList: source.getFieldsList,
+ editorUI: source.editorUI,
};
}
diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js
index ac652b91890319..8acf27b7cf9014 100644
--- a/packages/blocks/src/store/reducer.js
+++ b/packages/blocks/src/store/reducer.js
@@ -414,6 +414,7 @@ export function blockBindingsSources( state = {}, action ) {
canUserEditValue:
action.setValues && action.canUserEditValue,
getFieldsList,
+ editorUI: action.editorUI,
},
};
case 'REMOVE_BLOCK_BINDINGS_SOURCE':
diff --git a/packages/e2e-tests/plugins/block-bindings.php b/packages/e2e-tests/plugins/block-bindings.php
index 1fd6d8468c77db..3a1db066dd0779 100644
--- a/packages/e2e-tests/plugins/block-bindings.php
+++ b/packages/e2e-tests/plugins/block-bindings.php
@@ -69,7 +69,9 @@ function gutenberg_test_block_bindings_registration() {
if ( ! isset( $source_args['key'] ) || ! isset( $fields_list[ $source_args['key'] ] ) ) {
return null;
}
- return $fields_list[ $source_args['key'] ]['value']; },
+ return $fields_list[ $source_args['key'] ]['value'];
+ },
+ 'uses_context' => array( 'postType', 'postId' ),
)
);
register_block_bindings_source(
diff --git a/packages/e2e-tests/plugins/block-bindings/index.js b/packages/e2e-tests/plugins/block-bindings/index.js
index 63c463e197fa8a..5221cc805c9dd3 100644
--- a/packages/e2e-tests/plugins/block-bindings/index.js
+++ b/packages/e2e-tests/plugins/block-bindings/index.js
@@ -13,13 +13,20 @@ const getValues = ( { bindings } ) => {
}
return newValues;
};
-const setValues = ( { dispatch, bindings } ) => {
+const setValues = ( { dispatch, context, bindings } ) => {
+ const newMeta = {};
Object.values( bindings ).forEach( ( { args, newValue } ) => {
- // Example of what could be done.
- dispatch( 'core' ).editEntityRecord( 'postType', 'post', 1, {
- meta: { [ args?.key ]: newValue },
- } );
+ newMeta[ args.key ] = newValue;
} );
+
+ dispatch( 'core' ).editEntityRecord(
+ 'postType',
+ context?.postType,
+ context?.postId,
+ {
+ meta: newMeta,
+ }
+ );
};
registerBlockBindingsSource( {
@@ -27,7 +34,90 @@ registerBlockBindingsSource( {
getValues,
setValues,
canUserEditValue: () => true,
- getFieldsList: () => fieldsList,
+ editorUI() {
+ return {
+ mode: 'dropdown',
+ data: Object.entries( fieldsList || {} ).map(
+ ( [ key, field ] ) => ( {
+ label: field?.label || key,
+ type: field?.type || 'string',
+ args: {
+ key,
+ },
+ } )
+ ),
+ };
+ },
+} );
+
+const ModalButton = ( { fieldKey, fieldLabel, attribute, closeModal } ) => {
+ const { updateBlockBindings } = wp.blockEditor.useBlockBindingsUtils();
+
+ return el(
+ 'button',
+ {
+ onClick: () => {
+ updateBlockBindings( {
+ [ attribute ]: {
+ source: 'testing/modal-source',
+ args: { key: fieldKey },
+ },
+ } );
+ closeModal();
+ },
+ style: {
+ display: 'block',
+ margin: '5px 0',
+ padding: '10px',
+ width: '100%',
+ },
+ },
+ fieldLabel
+ );
+};
+
+registerBlockBindingsSource( {
+ name: 'testing/modal-source',
+ label: 'Modal Source',
+ getValues,
+ setValues,
+ canUserEditValue: () => true,
+ editorUI() {
+ return {
+ mode: 'modal',
+ data: Object.entries( fieldsList || {} ).map(
+ ( [ key, field ] ) => ( {
+ label: field?.label || key,
+ type: field?.type || 'string',
+ args: {
+ key,
+ },
+ } )
+ ),
+ renderModalContent( { attribute, closeModal } ) {
+ return el(
+ 'div',
+ { style: { padding: '20px' } },
+ el( 'h3', null, 'Select a field from the modal' ),
+ el(
+ 'p',
+ null,
+ 'This is a modal interface for selecting fields.'
+ ),
+ Object.entries( fieldsList || {} ).map(
+ ( [ key, field ] ) =>
+ el( ModalButton, {
+ key,
+ fieldKey: key,
+ fieldLabel: field?.label || key,
+ attribute,
+ closeModal,
+ } )
+ )
+ );
+ },
+ };
+ },
} );
registerBlockBindingsSource( {
diff --git a/packages/editor/src/bindings/post-data.js b/packages/editor/src/bindings/post-data.js
index ca9d0088fc533a..2112680b657361 100644
--- a/packages/editor/src/bindings/post-data.js
+++ b/packages/editor/src/bindings/post-data.js
@@ -123,6 +123,29 @@ export default {
return true;
},
getFieldsList( { select, context } ) {
+ // Deprecated, will be removed after 6.9.
return getPostDataFields( select, context );
},
+ editorUI( { select, context } ) {
+ const selectedBlock = select( 'core/block-editor' ).getSelectedBlock();
+ if ( selectedBlock?.name !== 'core/post-date' ) {
+ return {};
+ }
+ const postDataFields = Object.entries(
+ getPostDataFields( select, context ) || {}
+ ).map( ( [ key, field ] ) => ( {
+ label: field.label,
+ args: {
+ key,
+ },
+ type: field.type,
+ } ) );
+ /*
+ * We need to define the data as [{ label: string, value: any, type: https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/#type-validation }]
+ */
+ return {
+ mode: 'dropdown',
+ data: postDataFields,
+ };
+ },
};
diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js
index 6e5f271c373761..644ea36f06d376 100644
--- a/packages/editor/src/bindings/post-meta.js
+++ b/packages/editor/src/bindings/post-meta.js
@@ -142,6 +142,25 @@ export default {
return true;
},
getFieldsList( { select, context } ) {
+ // Deprecated, will be removed after 6.9.
return getPostMetaFields( select, context );
},
+ editorUI( { select, context } ) {
+ const metaFields = Object.entries(
+ getPostMetaFields( select, context ) || {}
+ ).map( ( [ key, field ] ) => ( {
+ label: field.label,
+ args: {
+ key,
+ },
+ type: field.type,
+ } ) );
+ /*
+ * We need to define the data as [{ label: string, value: any, type: https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/#type-validation }]
+ */
+ return {
+ mode: 'dropdown',
+ data: metaFields,
+ };
+ },
};
diff --git a/packages/editor/src/bindings/term-data.js b/packages/editor/src/bindings/term-data.js
index 5d0c4e8d7358a4..3f5baef7a0eff9 100644
--- a/packages/editor/src/bindings/term-data.js
+++ b/packages/editor/src/bindings/term-data.js
@@ -154,6 +154,25 @@ export default {
return false;
},
getFieldsList( { select, context } ) {
+ // Deprecated, will be removed after 6.9.
return getTermDataFields( select, context );
},
+ editorUI( { select, context } ) {
+ const termDataFields = Object.entries(
+ getTermDataFields( select, context ) || {}
+ ).map( ( [ key, field ] ) => ( {
+ label: field.label,
+ type: field.type,
+ args: {
+ key,
+ },
+ } ) );
+ /*
+ * We need to define the data as [{ label: string, value: any, type: https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/#type-validation }]
+ */
+ return {
+ mode: 'dropdown',
+ data: termDataFields,
+ };
+ },
};
diff --git a/test/e2e/specs/editor/various/block-bindings/custom-sources.spec.js b/test/e2e/specs/editor/various/block-bindings/custom-sources.spec.js
index c402ef8797a566..afdb7fcb2e68fc 100644
--- a/test/e2e/specs/editor/various/block-bindings/custom-sources.spec.js
+++ b/test/e2e/specs/editor/various/block-bindings/custom-sources.spec.js
@@ -763,7 +763,7 @@ test.describe( 'Registered sources', () => {
} );
} );
- test.describe( 'getFieldsList', () => {
+ test.describe( 'UI Editor', () => {
test( 'should be possible to update attribute value through bindings UI', async ( {
editor,
page,
@@ -779,7 +779,12 @@ test.describe( 'Registered sources', () => {
.click();
await page.getByRole( 'button', { name: 'content' } ).click();
await page
- .getByRole( 'menuitemradio' )
+ .getByRole( 'menuitem', {
+ name: 'Complete Source',
+ } )
+ .click();
+ await page
+ .getByRole( 'menuitemcheckbox' )
.filter( { hasText: 'Text Field Label' } )
.click();
const paragraphBlock = editor.canvas.getByRole( 'document', {
@@ -914,13 +919,18 @@ test.describe( 'Registered sources', () => {
},
} );
await page.getByRole( 'button', { name: 'content' } ).click();
+ await page
+ .getByRole( 'menuitem', {
+ name: 'Complete Source',
+ } )
+ .click();
const textField = page
- .getByRole( 'menuitemradio' )
+ .getByRole( 'menuitemcheckbox' )
.filter( { hasText: 'Text Field Label' } );
await expect( textField ).toBeVisible();
await expect( textField ).toBeChecked();
const urlField = page
- .getByRole( 'menuitemradio' )
+ .getByRole( 'menuitemcheckbox' )
.filter( { hasText: 'URL Field Label' } );
await expect( urlField ).toBeVisible();
await expect( urlField ).not.toBeChecked();
@@ -1209,4 +1219,117 @@ test.describe( 'Registered sources', () => {
} );
await expect( contentButton ).toContainText( 'Invalid source' );
} );
+
+ test.describe( 'Modal source', () => {
+ test( 'should open modal and allow field selection', async ( {
+ editor,
+ page,
+ } ) => {
+ await editor.insertBlock( {
+ name: 'core/paragraph',
+ } );
+
+ // Open the bindings panel
+ await page.getByLabel( 'Attributes options' ).click();
+ await page
+ .getByRole( 'menuitemcheckbox', {
+ name: 'Show content',
+ } )
+ .click();
+
+ // Click on the content binding button
+ await page
+ .getByRole( 'button', {
+ name: 'content',
+ } )
+ .click();
+
+ // Click on the modal source
+ await page
+ .getByRole( 'menuitem', {
+ name: 'Modal Source',
+ } )
+ .click();
+
+ // Modal should be visible
+ const modal = page.getByRole( 'dialog' );
+ await expect( modal ).toBeVisible();
+
+ // Check modal content
+ await expect( modal ).toContainText(
+ 'Select a field from the modal'
+ );
+ await expect( modal ).toContainText(
+ 'This is a modal interface for selecting fields.'
+ );
+
+ // Click on a field button in the modal
+ const fieldButton = modal.getByRole( 'button', {
+ name: 'Text Field Label',
+ } );
+ await expect( fieldButton ).toBeVisible();
+ await fieldButton.click();
+
+ // Modal should close and binding should be applied
+ await expect( modal ).toBeHidden();
+
+ // Check that the paragraph shows the bound value
+ const paragraphBlock = editor.canvas.getByRole( 'document', {
+ name: 'Block: Paragraph',
+ } );
+ await expect( paragraphBlock ).toHaveText( 'Text Field Value' );
+ } );
+
+ test( 'should show modal source in attributes panel', async ( {
+ editor,
+ page,
+ } ) => {
+ await editor.insertBlock( {
+ name: 'core/paragraph',
+ attributes: {
+ content: 'fallback content',
+ metadata: {
+ bindings: {
+ content: {
+ source: 'testing/modal-source',
+ args: {
+ key: 'text_field',
+ },
+ },
+ },
+ },
+ },
+ } );
+
+ // Check that the binding shows the modal source label
+ const contentButton = page.getByRole( 'button', {
+ name: 'content',
+ } );
+ await expect( contentButton ).toContainText( 'Text Field Label' );
+ } );
+ } );
+
+ test.describe( 'Source compatibility filtering', () => {
+ test( 'should show compatible sources enabled and incompatible sources disabled', async ( {
+ editor,
+ page,
+ } ) => {
+ // Test string attribute - paragraph content
+ await editor.insertBlock( { name: 'core/image' } );
+ await page.getByLabel( 'Attributes options' ).click();
+ await page
+ .getByRole( 'menuitemcheckbox', { name: 'Show id' } )
+ .click();
+ await page.getByRole( 'button', { name: 'id' } ).click();
+
+ // String sources enabled, number source disabled for string attribute
+ await expect(
+ page.getByRole( 'menuitem', { name: 'Complete Source' } )
+ ).toBeDisabled();
+
+ await expect(
+ page.getByRole( 'menuitem', { name: 'Modal Source' } )
+ ).toBeEnabled();
+ } );
+ } );
} );
diff --git a/test/e2e/specs/editor/various/block-bindings/post-data.spec.js b/test/e2e/specs/editor/various/block-bindings/post-data.spec.js
new file mode 100644
index 00000000000000..ac22338d0af5e3
--- /dev/null
+++ b/test/e2e/specs/editor/various/block-bindings/post-data.spec.js
@@ -0,0 +1,91 @@
+/**
+ * WordPress dependencies
+ */
+const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+
+test.describe( 'Post Data source', () => {
+ test.beforeAll( async ( { requestUtils } ) => {
+ await requestUtils.activateTheme(
+ 'gutenberg-test-themes/block-bindings'
+ );
+ await requestUtils.activatePlugin( 'gutenberg-test-block-bindings' );
+ } );
+
+ test.beforeEach( async ( { admin } ) => {
+ await admin.createNewPost( { title: 'Test bindings' } );
+ } );
+
+ test.afterEach( async ( { requestUtils } ) => {
+ await requestUtils.deleteAllPosts();
+ } );
+
+ test.afterEach( async ( { requestUtils } ) => {
+ await requestUtils.deleteAllPosts();
+ } );
+
+ test.afterAll( async ( { requestUtils } ) => {
+ await requestUtils.activateTheme( 'twentytwentyone' );
+ await requestUtils.deactivatePlugin( 'gutenberg-test-block-bindings' );
+ } );
+ test.describe( 'Post Data bindings UI.', () => {
+ test( 'should not include post data fields in UI to connect attributes on non date blocks', async ( {
+ editor,
+ page,
+ } ) => {
+ await editor.insertBlock( {
+ name: 'core/paragraph',
+ attributes: {
+ content: 'fallback content',
+ metadata: {
+ bindings: {
+ content: {
+ source: 'core/post-data',
+ args: {
+ key: 'modified',
+ },
+ },
+ },
+ },
+ },
+ } );
+ await page
+ .getByRole( 'button', {
+ name: 'content',
+ } )
+ .click();
+ const postDataMenuItem = page.getByRole( 'menuitem', {
+ name: 'Post Data',
+ } );
+ await expect( postDataMenuItem ).toBeHidden();
+ // Check the fields registered by other sources are there.
+ } );
+
+ test( 'should include post data fields in UI to connect attributes on date blocks', async ( {
+ editor,
+ page,
+ } ) => {
+ await editor.insertBlock( {
+ name: 'core/post-date',
+ attributes: {
+ metadata: {
+ bindings: {
+ datetime: {
+ source: 'core/post-data',
+ args: { key: 'date' },
+ },
+ },
+ },
+ },
+ } );
+ await page
+ .getByRole( 'button', {
+ name: 'datetime',
+ } )
+ .click();
+ const postDataMenuItem = page.getByRole( 'menuitem', {
+ name: 'Post Data',
+ } );
+ await expect( postDataMenuItem ).toBeVisible();
+ } );
+ } );
+} );
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 0794a8fe5d34fe..611f88bde77473 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
@@ -164,7 +164,12 @@ test.describe( 'Post Meta source', () => {
} );
await contentBinding.click();
await page
- .getByRole( 'menuitemradio' )
+ .getByRole( 'menuitem', {
+ name: 'Post Meta',
+ } )
+ .click();
+ await page
+ .getByRole( 'menuitemcheckbox' )
.filter( { hasText: 'Movie field label' } )
.click();
await expect( contentBinding ).toContainText(
@@ -195,7 +200,12 @@ test.describe( 'Post Meta source', () => {
} );
await contentBinding.click();
await page
- .getByRole( 'menuitemradio' )
+ .getByRole( 'menuitem', {
+ name: 'Post Meta',
+ } )
+ .click();
+ await page
+ .getByRole( 'menuitemcheckbox' )
.filter( { hasText: 'field_without_label_or_default' } )
.click();
await expect( contentBinding ).toContainText(
@@ -221,13 +231,18 @@ test.describe( 'Post Meta source', () => {
name: 'content',
} )
.click();
+ await page
+ .getByRole( 'menuitem', {
+ name: 'Post Meta',
+ } )
+ .click();
} );
test( 'should include movie fields in UI to connect attributes', async ( {
page,
} ) => {
const movieField = page
- .getByRole( 'menuitemradio' )
+ .getByRole( 'menuitemcheckbox' )
.filter( { hasText: 'Movie field label' } );
await expect( movieField ).toBeVisible();
} );
@@ -235,23 +250,23 @@ test.describe( 'Post Meta source', () => {
page,
} ) => {
const globalField = page
- .getByRole( 'menuitemradio' )
+ .getByRole( 'menuitemcheckbox' )
.filter( { hasText: 'text_custom_field' } );
await expect( globalField ).toBeVisible();
} );
test( 'should not include protected fields', async ( { page } ) => {
// Ensure the fields have loaded by checking the field is visible.
const globalField = page
- .getByRole( 'menuitemradio' )
+ .getByRole( 'menuitemcheckbox' )
.filter( { hasText: 'text_custom_field' } );
await expect( globalField ).toBeVisible();
// Check the protected fields are not visible.
const protectedField = page
- .getByRole( 'menuitemradio' )
+ .getByRole( 'menuitemcheckbox' )
.filter( { hasText: '_protected_field' } );
await expect( protectedField ).toBeHidden();
const showInRestFalseField = page
- .getByRole( 'menuitemradio' )
+ .getByRole( 'menuitemcheckbox' )
.filter( { hasText: 'show_in_rest_false_field' } );
await expect( showInRestFalseField ).toBeHidden();
} );
@@ -259,20 +274,22 @@ test.describe( 'Post Meta source', () => {
page,
} ) => {
const fieldButton = page
- .getByRole( 'menuitemradio' )
+ .getByRole( 'menuitemcheckbox' )
.filter( { hasText: 'Movie field label' } );
await expect( fieldButton ).toContainText(
'Movie field default value'
);
} );
+ // We need to discuss this approach. As now is showing the label, like post-meta getValues function on the editor.
test( 'should not show anything if the default value is not defined', async ( {
page,
} ) => {
const fieldButton = page
- .getByRole( 'menuitemradio' )
+ .getByRole( 'menuitemcheckbox' )
.filter( { hasText: 'Field with only label' } );
- // Check it only contains the field label.
- await expect( fieldButton ).toHaveText(
+
+ // Check that the field contains the label text
+ await expect( fieldButton ).toContainText(
'Field with only label'
);
} );
@@ -314,19 +331,25 @@ test.describe( 'Post Meta source', () => {
name: 'content',
} )
.click();
+ await page
+ .getByRole( 'menuitem', {
+ name: 'Complete Source',
+ } )
+ .click();
// Check the fields registered by other sources are there.
const customSourceField = page
- .getByRole( 'menuitemradio' )
+ .getByRole( 'menuitemcheckbox' )
.filter( { hasText: 'Text Field Label' } );
await expect( customSourceField ).toBeVisible();
// Check the post meta fields are not visible.
const globalField = page
- .getByRole( 'menuitemradio' )
+ .getByRole( 'menuitemcheckbox' )
.filter( { hasText: 'text_custom_field' } );
await expect( globalField ).toBeHidden();
- const movieField = page
- .getByRole( 'menuitemradio' )
- .filter( { hasText: 'Movie field label' } );
+ const movieField = page.getByRole( 'menuitem', {
+ name: 'Post Meta',
+ } );
+
await expect( movieField ).toBeHidden();
} );
test( 'should show the key in attributes connected to post meta', async ( {
@@ -583,8 +606,13 @@ test.describe( 'Post Meta source', () => {
name: 'content',
} )
.click();
+ await page
+ .getByRole( 'menuitem', {
+ name: 'Post Meta',
+ } )
+ .click();
const movieField = page
- .getByRole( 'menuitemradio' )
+ .getByRole( 'menuitemcheckbox' )
.filter( { hasText: 'Movie field label' } );
await expect( movieField ).toBeVisible();
} );
@@ -606,33 +634,38 @@ test.describe( 'Post Meta source', () => {
name: 'content',
} )
.click();
+ await page
+ .getByRole( 'menuitem', {
+ name: 'Post Meta',
+ } )
+ .click();
await expect(
- page.getByRole( 'menuitemradio', {
+ page.getByRole( 'menuitemcheckbox', {
name: 'String custom field',
} )
).toBeVisible();
await expect(
- page.getByRole( 'menuitemradio', {
+ page.getByRole( 'menuitemcheckbox', {
name: 'Number custom field',
} )
).toBeHidden();
await expect(
- page.getByRole( 'menuitemradio', {
+ page.getByRole( 'menuitemcheckbox', {
name: 'Integer custom field',
} )
).toBeHidden();
await expect(
- page.getByRole( 'menuitemradio', {
+ page.getByRole( 'menuitemcheckbox', {
name: 'Boolean custom field',
} )
).toBeHidden();
await expect(
- page.getByRole( 'menuitemradio', {
+ page.getByRole( 'menuitemcheckbox', {
name: 'Object custom field',
} )
).toBeHidden();
await expect(
- page.getByRole( 'menuitemradio', {
+ page.getByRole( 'menuitemcheckbox', {
name: 'Array custom field',
} )
).toBeHidden();