diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php
index 09ac4b6990d919..065264fd124e8c 100644
--- a/lib/experimental/editor-settings.php
+++ b/lib/experimental/editor-settings.php
@@ -34,6 +34,9 @@ function gutenberg_enable_experiments() {
if ( $gutenberg_experiments && array_key_exists( 'gutenberg-quick-edit-dataviews', $gutenberg_experiments ) ) {
wp_add_inline_script( 'wp-block-editor', 'window.__experimentalQuickEditDataViews = true', 'before' );
}
+ if ( $gutenberg_experiments && array_key_exists( 'gutenberg-block-bindings-ui', $gutenberg_experiments ) ) {
+ wp_add_inline_script( 'wp-block-editor', 'window.__experimentalBlockBindingsUI = true', 'before' );
+ }
}
add_action( 'admin_init', 'gutenberg_enable_experiments' );
diff --git a/lib/experiments-page.php b/lib/experiments-page.php
index 7cc4198c14ef91..acb095b47fde30 100644
--- a/lib/experiments-page.php
+++ b/lib/experiments-page.php
@@ -163,6 +163,18 @@ function gutenberg_initialize_experiments_settings() {
)
);
+ add_settings_field(
+ 'gutenberg-block-bindings-ui',
+ __( 'UI to create block bindings', 'gutenberg' ),
+ 'gutenberg_display_experiment_field',
+ 'gutenberg-experiments',
+ 'gutenberg_experiments_section',
+ array(
+ 'label' => __( 'Add UI to create and update block bindings in block inspector controls.', 'gutenberg' ),
+ 'id' => 'gutenberg-block-bindings-ui',
+ )
+ );
+
register_setting(
'gutenberg-experiments',
'gutenberg-experiments'
diff --git a/packages/block-editor/src/hooks/block-bindings.js b/packages/block-editor/src/hooks/block-bindings.js
index c61f586575a530..ce3fdd2f327c40 100644
--- a/packages/block-editor/src/hooks/block-bindings.js
+++ b/packages/block-editor/src/hooks/block-bindings.js
@@ -2,42 +2,199 @@
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
-import { store as blocksStore } from '@wordpress/blocks';
+import { privateApis as blocksPrivateApis } from '@wordpress/blocks';
import {
- BaseControl,
- PanelBody,
- __experimentalHStack as HStack,
__experimentalItemGroup as ItemGroup,
__experimentalItem as Item,
+ __experimentalText as Text,
+ __experimentalToolsPanel as ToolsPanel,
+ __experimentalToolsPanelItem as ToolsPanelItem,
+ __experimentalTruncate as Truncate,
+ __experimentalVStack as VStack,
+ privateApis as componentsPrivateApis,
} from '@wordpress/components';
-import { useSelect } from '@wordpress/data';
+import { useSelect, useDispatch, useRegistry } from '@wordpress/data';
+import { useContext, Fragment } from '@wordpress/element';
+import { useViewportMatch } from '@wordpress/compose';
/**
* Internal dependencies
*/
-import { canBindAttribute } from '../hooks/use-bindings-attributes';
+import {
+ canBindAttribute,
+ getBindableAttributes,
+} from '../hooks/use-bindings-attributes';
+import { store as blockEditorStore } from '../store';
import { unlock } from '../lock-unlock';
import InspectorControls from '../components/inspector-controls';
+import BlockContext from '../components/block-context';
+
+const {
+ DropdownMenuV2: DropdownMenu,
+ DropdownMenuGroupV2: DropdownMenuGroup,
+ DropdownMenuRadioItemV2: DropdownMenuRadioItem,
+ DropdownMenuItemLabelV2: DropdownMenuItemLabel,
+ DropdownMenuItemHelpTextV2: DropdownMenuItemHelpText,
+ DropdownMenuSeparatorV2: DropdownMenuSeparator,
+} = unlock( componentsPrivateApis );
+
+const useToolsPanelDropdownMenuProps = () => {
+ const isMobile = useViewportMatch( 'medium', '<' );
+ return ! isMobile
+ ? {
+ popoverProps: {
+ placement: 'left-start',
+ // For non-mobile, inner sidebar width (248px) - button width (24px) - border (1px) + padding (16px) + spacing (20px)
+ offset: 259,
+ },
+ }
+ : {};
+};
+
+function BlockBindingsPanelDropdown( {
+ fieldsList,
+ addConnection,
+ attribute,
+ binding,
+} ) {
+ const currentKey = binding?.args?.key;
+ return (
+ <>
+ { Object.entries( fieldsList ).map( ( [ label, fields ], i ) => (
+
+
+ { Object.keys( fieldsList ).length > 1 && (
+
+ { label }
+
+ ) }
+ { Object.entries( fields ).map( ( [ key, value ] ) => (
+
+ addConnection( key, attribute )
+ }
+ name={ attribute + '-binding' }
+ value={ key }
+ checked={ key === currentKey }
+ >
+
+ { key }
+
+
+ { value }
+
+
+ ) ) }
+
+ { i !== Object.keys( fieldsList ).length - 1 && (
+
+ ) }
+
+ ) ) }
+ >
+ );
+}
+
+function BlockBindingsAttribute( { attribute, binding } ) {
+ const { source: sourceName, args } = binding || {};
+ const sourceProps =
+ unlock( blocksPrivateApis ).getBlockBindingsSource( sourceName );
+ return (
+
+ { attribute }
+ { !! binding && (
+
+
+ { args?.key || sourceProps?.label || sourceName }
+
+
+ ) }
+
+ );
+}
+
+function ReadOnlyBlockBindingsPanelItems( { bindings } ) {
+ return (
+ <>
+ { Object.entries( bindings ).map( ( [ attribute, binding ] ) => (
+ -
+
+
+ ) ) }
+ >
+ );
+}
+
+function EditableBlockBindingsPanelItems( {
+ attributes,
+ bindings,
+ fieldsList,
+ addConnection,
+ removeConnection,
+} ) {
+ const isMobile = useViewportMatch( 'medium', '<' );
+ return (
+ <>
+ { attributes.map( ( attribute ) => {
+ const binding = bindings[ attribute ];
+ return (
+ !! binding }
+ label={ attribute }
+ onDeselect={ () => {
+ removeConnection( attribute );
+ } }
+ >
+
+
+
+ }
+ >
+
+
+
+ );
+ } ) }
+ >
+ );
+}
export const BlockBindingsPanel = ( { name, metadata } ) => {
+ const registry = useRegistry();
+ const blockContext = useContext( BlockContext );
const { bindings } = metadata || {};
- const { sources } = useSelect( ( select ) => {
- const _sources = unlock(
- select( blocksStore )
- ).getAllBlockBindingsSources();
- return {
- sources: _sources,
- };
- }, [] );
-
- if ( ! bindings ) {
- return null;
- }
+ const bindableAttributes = getBindableAttributes( name );
+ const dropdownMenuProps = useToolsPanelDropdownMenuProps();
- // Don't show not allowed attributes.
- // Don't show the bindings connected to pattern overrides in the inspectors panel.
- // TODO: Explore if this should be abstracted to let other sources decide.
const filteredBindings = { ...bindings };
Object.keys( filteredBindings ).forEach( ( key ) => {
if (
@@ -48,43 +205,137 @@ export const BlockBindingsPanel = ( { name, metadata } ) => {
}
} );
- if ( Object.keys( filteredBindings ).length === 0 ) {
+ const { updateBlockAttributes } = useDispatch( blockEditorStore );
+
+ const { _id } = useSelect( ( select ) => {
+ const { getSelectedBlockClientId } = select( blockEditorStore );
+
+ return {
+ _id: getSelectedBlockClientId(),
+ };
+ }, [] );
+
+ if ( ! bindableAttributes || bindableAttributes.length === 0 ) {
+ return null;
+ }
+
+ const removeAllConnections = () => {
+ const newMetadata = { ...metadata };
+ delete newMetadata.bindings;
+ updateBlockAttributes( _id, {
+ metadata:
+ Object.keys( newMetadata ).length === 0
+ ? undefined
+ : newMetadata,
+ } );
+ };
+
+ const addConnection = ( value, attribute ) => {
+ // Assuming the block expects a flat structure for its metadata attribute
+ const newMetadata = {
+ ...metadata,
+ // Adjust this according to the actual structure expected by your block
+ bindings: {
+ ...metadata?.bindings,
+ [ attribute ]: {
+ source: 'core/post-meta',
+ args: { key: value },
+ },
+ },
+ };
+ // Update the block's attributes with the new metadata
+ updateBlockAttributes( _id, {
+ metadata: newMetadata,
+ } );
+ };
+
+ const removeConnection = ( key ) => {
+ const newMetadata = { ...metadata };
+ if ( ! newMetadata.bindings ) {
+ return;
+ }
+
+ delete newMetadata.bindings[ key ];
+ if ( Object.keys( newMetadata.bindings ).length === 0 ) {
+ delete newMetadata.bindings;
+ }
+ updateBlockAttributes( _id, {
+ metadata:
+ Object.keys( newMetadata ).length === 0
+ ? undefined
+ : newMetadata,
+ } );
+ };
+
+ const fieldsList = {};
+ const { getBlockBindingsSources } = unlock( blocksPrivateApis );
+ const registeredSources = getBlockBindingsSources();
+ Object.values( registeredSources ).forEach(
+ ( { getFieldsList, label, usesContext } ) => {
+ if ( getFieldsList ) {
+ // Populate context.
+ const context = {};
+ if ( usesContext?.length ) {
+ for ( const key of usesContext ) {
+ context[ key ] = blockContext[ key ];
+ }
+ }
+ const sourceList = getFieldsList( {
+ registry,
+ context,
+ } );
+ // Only add source if the list is not empty.
+ if ( sourceList ) {
+ fieldsList[ label ] = { ...sourceList };
+ }
+ }
+ }
+ );
+ // Remove empty sources.
+ Object.entries( fieldsList ).forEach( ( [ key, value ] ) => {
+ if ( ! Object.keys( value ).length ) {
+ delete fieldsList[ key ];
+ }
+ } );
+
+ // Lock the UI when the experiment is not enabled or there are no fields to connect to.
+ const readOnly =
+ ! window.__experimentalBlockBindingsUI ||
+ ! Object.keys( fieldsList ).length;
+
+ if ( readOnly && Object.keys( filteredBindings ).length === 0 ) {
return null;
}
return (
- {
+ removeAllConnections();
+ } }
+ dropdownMenuProps={ dropdownMenuProps }
+ className="block-editor-bindings__panel"
>
-
-
- { Object.keys( filteredBindings ).map( ( key ) => {
- return (
- -
-
- { key }
-
- { sources[
- filteredBindings[ key ].source
- ]
- ? sources[
- filteredBindings[ key ]
- .source
- ].label
- : filteredBindings[ key ]
- .source }
-
-
-
- );
- } ) }
-
-
-
+
+ { readOnly ? (
+
+ ) : (
+
+ ) }
+
+
+ { __( 'Attributes connected to various sources.' ) }
+
+
);
};
diff --git a/packages/block-editor/src/hooks/block-bindings.scss b/packages/block-editor/src/hooks/block-bindings.scss
index fd46674ad11426..73e7c490160d3e 100644
--- a/packages/block-editor/src/hooks/block-bindings.scss
+++ b/packages/block-editor/src/hooks/block-bindings.scss
@@ -1,3 +1,14 @@
-.components-panel__block-bindings-panel .components-item__block-bindings-source {
- color: $gray-700;
+div.block-editor-bindings__panel {
+ grid-template-columns: auto;
+ button:hover .block-editor-bindings__item-explanation {
+ color: inherit;
+ }
+}
+
+.block-editor-bindings__popover {
+ // This won't be needed if `DropdownMenuGroup` component handles the label.
+ .block-editor-bindings__source-label {
+ grid-column: 2;
+ margin: $grid-unit-10 0;
+ }
}
diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js
index b58ede2e9d389d..2f74640ef4f633 100644
--- a/packages/block-editor/src/hooks/index.js
+++ b/packages/block-editor/src/hooks/index.js
@@ -37,7 +37,6 @@ import './grid-visualizer';
createBlockEditFilter(
[
- blockBindingsPanel,
align,
textAlign,
anchor,
@@ -48,6 +47,7 @@ createBlockEditFilter(
layout,
contentLockUI,
blockHooks,
+ blockBindingsPanel,
childLayout,
].filter( Boolean )
);
diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js
index 54b5425bfc44a5..4cac29a8bc5e91 100644
--- a/packages/block-editor/src/hooks/use-bindings-attributes.js
+++ b/packages/block-editor/src/hooks/use-bindings-attributes.js
@@ -91,6 +91,10 @@ export function canBindAttribute( blockName, attributeName ) {
);
}
+export function getBindableAttributes( blockName ) {
+ return BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ];
+}
+
export const withBlockBindingSupport = createHigherOrderComponent(
( BlockEdit ) => ( props ) => {
const registry = useRegistry();
diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss
index e7e4b75d0a30f0..47b87bb50918df 100644
--- a/packages/block-editor/src/style.scss
+++ b/packages/block-editor/src/style.scss
@@ -46,8 +46,8 @@
@import "./components/tool-selector/style.scss";
@import "./components/url-input/style.scss";
@import "./components/url-popover/style.scss";
-@import "./hooks/block-bindings.scss";
@import "./hooks/block-hooks.scss";
+@import "./hooks/block-bindings.scss";
@import "./hooks/border.scss";
@import "./hooks/color.scss";
@import "./hooks/dimensions.scss";
diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js
index 7cce959c78cc80..e109a7e562f00e 100644
--- a/packages/blocks/src/api/registration.js
+++ b/packages/blocks/src/api/registration.js
@@ -775,6 +775,7 @@ export const unregisterBlockVariation = ( blockName, variationName ) => {
* @param {Function} [source.setValues] Function to update multiple values connected to the source.
* @param {Function} [source.getPlaceholder] Function to get the placeholder when the value is undefined.
* @param {Function} [source.canUserEditValue] Function to determine if the user can edit the value.
+ * @param {Function} [source.getFieldsList] Function to get the lists of fields to expose in the connections panel.
*
* @example
* ```js
@@ -800,6 +801,7 @@ export const registerBlockBindingsSource = ( source ) => {
setValues,
getPlaceholder,
canUserEditValue,
+ getFieldsList,
} = source;
const existingSource = unlock(
@@ -899,6 +901,13 @@ export const registerBlockBindingsSource = ( source ) => {
return;
}
+ // Check the `getFieldsList` property is correct.
+ if ( getFieldsList && typeof getFieldsList !== 'function' ) {
+ // eslint-disable-next-line no-console
+ warning( 'Block bindings source getFieldsList 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 cc339e59d27053..874d664f139047 100644
--- a/packages/blocks/src/api/test/registration.js
+++ b/packages/blocks/src/api/test/registration.js
@@ -1656,6 +1656,19 @@ describe( 'blocks', () => {
expect( getBlockBindingsSource( 'core/testing' ) ).toBeUndefined();
} );
+ // Check the `getFieldsList` callback is correct.
+ it( 'should reject invalid getFieldsList callback', () => {
+ registerBlockBindingsSource( {
+ name: 'core/testing',
+ label: 'testing',
+ getFieldsList: 'should be a function',
+ } );
+ expect( console ).toHaveWarnedWith(
+ 'Block bindings source getFieldsList must be a function.'
+ );
+ expect( getBlockBindingsSource( 'core/testing' ) ).toBeUndefined();
+ } );
+
// Check correct sources are registered as expected.
it( 'should register a valid source', () => {
const sourceProperties = {
@@ -1665,6 +1678,9 @@ describe( 'blocks', () => {
setValues: () => 'new values',
getPlaceholder: () => 'placeholder',
canUserEditValue: () => true,
+ getFieldsList: () => {
+ return { field: 'value' };
+ },
};
registerBlockBindingsSource( {
name: 'core/valid-source',
@@ -1687,6 +1703,7 @@ describe( 'blocks', () => {
expect( source.setValues ).toBeUndefined();
expect( source.getPlaceholder ).toBeUndefined();
expect( source.canUserEditValue ).toBeUndefined();
+ expect( source.getFieldsList ).toBeUndefined();
unregisterBlockBindingsSource( 'core/valid-source' );
} );
diff --git a/packages/blocks/src/store/private-actions.js b/packages/blocks/src/store/private-actions.js
index 977270cf1d0c97..6d2f7598ef7838 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,
getPlaceholder: source.getPlaceholder,
canUserEditValue: source.canUserEditValue,
+ getFieldsList: source.getFieldsList,
};
}
diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js
index 2f141fb0cf9927..df6b33314fb492 100644
--- a/packages/blocks/src/store/reducer.js
+++ b/packages/blocks/src/store/reducer.js
@@ -393,6 +393,7 @@ export function blockBindingsSources( state = {}, action ) {
setValues: action.setValues,
getPlaceholder: action.getPlaceholder,
canUserEditValue: action.canUserEditValue,
+ getFieldsList: action.getFieldsList,
},
};
case 'ADD_BOOTSTRAPPED_BLOCK_BINDINGS_SOURCE':
diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js
index aafc784a21bd4a..298ec4d8ba1535 100644
--- a/packages/editor/src/bindings/post-meta.js
+++ b/packages/editor/src/bindings/post-meta.js
@@ -75,4 +75,24 @@ export default {
return true;
},
+ getFieldsList( { registry, context } ) {
+ const metaFields = registry
+ .select( coreDataStore )
+ .getEditedEntityRecord(
+ 'postType',
+ context?.postType,
+ context?.postId
+ ).meta;
+
+ if ( ! metaFields || ! Object.keys( metaFields ).length ) {
+ return null;
+ }
+
+ // Remove footnotes from the list of fields
+ return Object.fromEntries(
+ Object.entries( metaFields ).filter(
+ ( [ key ] ) => key !== 'footnotes'
+ )
+ );
+ },
};
diff --git a/test/e2e/specs/editor/various/block-bindings.spec.js b/test/e2e/specs/editor/various/block-bindings.spec.js
index 1499e255482c63..d0e1f079ce8439 100644
--- a/test/e2e/specs/editor/various/block-bindings.spec.js
+++ b/test/e2e/specs/editor/various/block-bindings.spec.js
@@ -1390,6 +1390,70 @@ test.describe( 'Block bindings', () => {
'false'
);
} );
+ test( 'should show a selector for content', async ( {
+ editor,
+ page,
+ } ) => {
+ // Activate the block bindings UI experiment.
+ await page.evaluate( () => {
+ window.__experimentalBlockBindingsUI = true;
+ } );
+
+ await editor.insertBlock( {
+ name: 'core/paragraph',
+ } );
+ await page
+ .getByRole( 'tabpanel', {
+ name: 'Settings',
+ } )
+ .getByLabel( 'Attributes options' )
+ .click();
+ const contentAttribute = page.getByRole( 'menuitemcheckbox', {
+ name: 'Show content',
+ } );
+ await expect( contentAttribute ).toBeVisible();
+ } );
+ test( 'should use a selector to update the content', async ( {
+ editor,
+ page,
+ } ) => {
+ // Activate the block bindings UI experiment.
+ await page.evaluate( () => {
+ window.__experimentalBlockBindingsUI = true;
+ } );
+
+ await editor.insertBlock( {
+ name: 'core/paragraph',
+ attributes: {
+ content: 'fallback value',
+ metadata: {
+ bindings: {
+ content: {
+ source: 'core/post-meta',
+ args: { key: 'undefined_field' },
+ },
+ },
+ },
+ },
+ } );
+ await page
+ .getByRole( 'tabpanel', {
+ name: 'Settings',
+ } )
+ .getByRole( 'button', { name: 'content' } )
+ .click();
+
+ await page
+ .getByRole( 'menuitemradio' )
+ .filter( { hasText: 'text_custom_field' } )
+ .click();
+ const paragraphBlock = editor.canvas.getByRole( 'document', {
+ name: 'Block: Paragraph',
+ } );
+ await expect( paragraphBlock ).toHaveText(
+ 'Value of the text_custom_field'
+ );
+ } );
} );
test.describe( 'Heading', () => {
@@ -1471,6 +1535,29 @@ test.describe( 'Block bindings', () => {
await expect( newEmptyParagraph ).toHaveText( '' );
await expect( newEmptyParagraph ).toBeEditable();
} );
+ test( 'should show a selector for content', async ( {
+ editor,
+ page,
+ } ) => {
+ // Activate the block bindings UI experiment.
+ await page.evaluate( () => {
+ window.__experimentalBlockBindingsUI = true;
+ } );
+
+ await editor.insertBlock( {
+ name: 'core/heading',
+ } );
+ await page
+ .getByRole( 'tabpanel', {
+ name: 'Settings',
+ } )
+ .getByLabel( 'Attributes options' )
+ .click();
+ const contentAttribute = page.getByRole( 'menuitemcheckbox', {
+ name: 'Show content',
+ } );
+ await expect( contentAttribute ).toBeVisible();
+ } );
} );
test.describe( 'Button', () => {
@@ -1648,6 +1735,56 @@ test.describe( 'Block bindings', () => {
await expect( newEmptyButton ).toHaveText( '' );
await expect( newEmptyButton ).toBeEditable();
} );
+ test( 'should show a selector for url, text, linkTarget and rel', async ( {
+ editor,
+ page,
+ } ) => {
+ // Activate the block bindings UI experiment.
+ await page.evaluate( () => {
+ window.__experimentalBlockBindingsUI = true;
+ } );
+
+ await editor.insertBlock( {
+ name: 'core/buttons',
+ innerBlocks: [
+ {
+ name: 'core/button',
+ },
+ ],
+ } );
+ await editor.canvas
+ .getByRole( 'document', {
+ name: 'Block: Button',
+ exact: true,
+ } )
+ .getByRole( 'textbox' )
+ .click();
+ await page
+ .getByRole( 'tabpanel', {
+ name: 'Settings',
+ } )
+ .getByLabel( 'Attributes options' )
+ .click();
+ const urlAttribute = page.getByRole( 'menuitemcheckbox', {
+ name: 'Show url',
+ } );
+ await expect( urlAttribute ).toBeVisible();
+ const textAttribute = page.getByRole( 'menuitemcheckbox', {
+ name: 'Show text',
+ } );
+ await expect( textAttribute ).toBeVisible();
+ const linkTargetAttribute = page.getByRole(
+ 'menuitemcheckbox',
+ {
+ name: 'Show linkTarget',
+ }
+ );
+ await expect( linkTargetAttribute ).toBeVisible();
+ const relAttribute = page.getByRole( 'menuitemcheckbox', {
+ name: 'Show rel',
+ } );
+ await expect( relAttribute ).toBeVisible();
+ } );
} );
test.describe( 'Image', () => {
@@ -1933,6 +2070,41 @@ test.describe( 'Block bindings', () => {
'default title value'
);
} );
+ test( 'should show a selector for url, id, title and alt', async ( {
+ editor,
+ page,
+ } ) => {
+ // Activate the block bindings UI experiment.
+ await page.evaluate( () => {
+ window.__experimentalBlockBindingsUI = true;
+ } );
+
+ await editor.insertBlock( {
+ name: 'core/image',
+ } );
+ await page
+ .getByRole( 'tabpanel', {
+ name: 'Settings',
+ } )
+ .getByLabel( 'Attributes options' )
+ .click();
+ const urlAttribute = page.getByRole( 'menuitemcheckbox', {
+ name: 'Show url',
+ } );
+ await expect( urlAttribute ).toBeVisible();
+ const idAttribute = page.getByRole( 'menuitemcheckbox', {
+ name: 'Show id',
+ } );
+ await expect( idAttribute ).toBeVisible();
+ const titleAttribute = page.getByRole( 'menuitemcheckbox', {
+ name: 'Show title',
+ } );
+ await expect( titleAttribute ).toBeVisible();
+ const altAttribute = page.getByRole( 'menuitemcheckbox', {
+ name: 'Show alt',
+ } );
+ await expect( altAttribute ).toBeVisible();
+ } );
} );
test.describe( 'Edit custom fields', () => {
@@ -2195,10 +2367,12 @@ test.describe( 'Block bindings', () => {
},
} );
- const bindingLabel = page.locator(
- '.components-item__block-bindings-source'
- );
- await expect( bindingLabel ).toHaveText( 'Server Source' );
+ const bindingsPanel = page
+ .getByRole( 'tabpanel', {
+ name: 'Settings',
+ } )
+ .locator( '.block-editor-bindings__panel' );
+ await expect( bindingsPanel ).toContainText( 'Server Source' );
} );
} );
} );