diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js index e1ebf5fda6b8ee..3bf78226f22648 100644 --- a/packages/block-editor/src/hooks/use-bindings-attributes.js +++ b/packages/block-editor/src/hooks/use-bindings-attributes.js @@ -6,12 +6,14 @@ import { createHigherOrderComponent } from '@wordpress/compose'; import { useRegistry, useSelect } from '@wordpress/data'; import { useCallback, useMemo, useContext } from '@wordpress/element'; import { addFilter } from '@wordpress/hooks'; +import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies */ import isURLLike from '../components/link-control/is-url-like'; import { unlock } from '../lock-unlock'; +import { Warning, useBlockProps } from '../components'; import BlockContext from '../components/block-context'; /** @typedef {import('@wordpress/compose').WPHigherOrderComponent} WPHigherOrderComponent */ @@ -121,9 +123,9 @@ export const withBlockBindingSupport = createHigherOrderComponent( // used purposely here to ensure `boundAttributes` is updated whenever // there are attribute updates. // `source.getValues` may also call a selector via `registry.select`. - const boundAttributes = useSelect( () => { + const { boundAttributes, invalidBinding } = useSelect( () => { if ( ! blockBindings ) { - return; + return {}; } const attributes = {}; @@ -135,7 +137,15 @@ export const withBlockBindingSupport = createHigherOrderComponent( ) ) { const { source: sourceName, args: sourceArgs } = binding; const source = sources[ sourceName ]; - if ( ! source || ! canBindAttribute( name, attributeName ) ) { + if ( ! source ) { + return { + invalidBinding: { + source: sourceName, + attribute: attributeName, + }, + }; + } + if ( ! canBindAttribute( name, attributeName ) ) { continue; } @@ -190,7 +200,7 @@ export const withBlockBindingSupport = createHigherOrderComponent( } } - return attributes; + return { boundAttributes: attributes }; }, [ blockBindings, name, clientId, blockContext, registry, sources ] ); const { setAttributes } = props; @@ -286,6 +296,24 @@ export const withBlockBindingSupport = createHigherOrderComponent( ] ); + // Throw a warning if the block is connected to an invalid source. + if ( invalidBinding ) { + return ( +
+ + { sprintf( + /* translators: %1$s: block attribute, %2$s: invalid block bindings source. */ + __( + 'Attribute "%1$s" is connected to unrecognized "%2$s" source.' + ), + invalidBinding.attribute, + invalidBinding.source + ) } + +
+ ); + } + return ( <> { ); } ); - test( 'should lock the appropriate controls when source is not defined', async ( { + test( 'should throw a warning when source is not defined', async ( { editor, - page, } ) => { await editor.insertBlock( { name: 'core/paragraph', @@ -177,32 +176,10 @@ test.describe( 'Block bindings', () => { }, }, } ); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - await paragraphBlock.click(); - - // Alignment controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Align text' } ) - ).toBeVisible(); - - // Format controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Bold', - } ) - ).toBeHidden(); - - // Paragraph is not editable. - await expect( paragraphBlock ).toHaveAttribute( - 'contenteditable', - 'false' + const warningMessage = editor.canvas.locator( + '.block-editor-warning__message' ); + await expect( warningMessage ).toBeVisible(); } ); } ); @@ -275,52 +252,6 @@ test.describe( 'Block bindings', () => { 'false' ); } ); - - test( 'should lock the appropriate controls when source is not defined', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/heading', - attributes: { - content: 'heading default content', - metadata: { - bindings: { - content: { - source: 'plugin/undefined-source', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const headingBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Heading', - } ); - await headingBlock.click(); - - // Alignment controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Align text' } ) - ).toBeVisible(); - - // Format controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Bold', - } ) - ).toBeHidden(); - - // Heading is not editable. - await expect( headingBlock ).toHaveAttribute( - 'contenteditable', - 'false' - ); - } ); } ); test.describe( 'Button', () => { @@ -416,68 +347,6 @@ test.describe( 'Block bindings', () => { ).toBeVisible(); } ); - test( 'should lock text controls when text is bound to an undefined source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - text: 'button default text', - url: '#default-url', - metadata: { - bindings: { - text: { - source: 'plugin/undefined-source', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - }, - ], - } ); - const buttonBlock = editor.canvas - .getByRole( 'document', { - name: 'Block: Button', - exact: true, - } ) - .getByRole( 'textbox' ); - await buttonBlock.click(); - - // Alignment controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Align text' } ) - ).toBeVisible(); - - // Format controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Bold', - } ) - ).toBeHidden(); - - // Button is not editable. - await expect( buttonBlock ).toHaveAttribute( - 'contenteditable', - 'false' - ); - - // Link controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Unlink' } ) - ).toBeVisible(); - } ); - test( 'should lock url controls when url is bound to a registered source', async ( { editor, page, @@ -538,66 +407,6 @@ test.describe( 'Block bindings', () => { ).toBeHidden(); } ); - test( 'should lock url controls when url is bound to an undefined source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - text: 'button default text', - url: '#default-url', - metadata: { - bindings: { - url: { - source: 'plugin/undefined-source', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - }, - ], - } ); - const buttonBlock = editor.canvas - .getByRole( 'document', { - name: 'Block: Button', - exact: true, - } ) - .getByRole( 'textbox' ); - await buttonBlock.click(); - - // Format controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Bold', - } ) - ).toBeVisible(); - - // Button is editable. - await expect( buttonBlock ).toHaveAttribute( - 'contenteditable', - 'true' - ); - - // Link controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Link' } ) - ).toBeHidden(); - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Unlink' } ) - ).toBeHidden(); - } ); - test( 'should lock url and text controls when both are bound', async ( { editor, page, @@ -712,34 +521,6 @@ test.describe( 'Block bindings', () => { ).toBeHidden(); } ); - test( 'should NOT show the upload form when url is bound to an undefined source', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - url: { - source: 'plugin/undefined-source', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Image', - } ); - await imageBlock.click(); - await expect( - imageBlock.getByRole( 'button', { name: 'Upload' } ) - ).toBeHidden(); - } ); - test( 'should lock url controls when url is bound to a registered source', async ( { editor, page, @@ -809,75 +590,6 @@ test.describe( 'Block bindings', () => { expect( titleValue ).toBe( 'default title value' ); } ); - test( 'should lock url controls when url is bound to an undefined source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - url: { - source: 'plugin/undefined-source', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Image', - } ); - await imageBlock.click(); - - // Replace controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Replace', - } ) - ).toBeHidden(); - - // Image placeholder doesn't show the upload button. - await expect( - imageBlock.getByRole( 'button', { name: 'Upload' } ) - ).toBeHidden(); - - // Alt textarea is enabled and with the original value. - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - ).toBeEnabled(); - const altValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - .inputValue(); - expect( altValue ).toBe( 'default alt value' ); - - // Title input is enabled and with the original value. - await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByRole( 'button', { name: 'Advanced' } ) - .click(); - - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - ).toBeEnabled(); - const titleValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - .inputValue(); - expect( titleValue ).toBe( 'default title value' ); - } ); - test( 'should disable alt textarea when alt is bound to a registered source', async ( { editor, page, @@ -941,69 +653,6 @@ test.describe( 'Block bindings', () => { expect( titleValue ).toBe( 'default title value' ); } ); - test( 'should disable alt textarea when alt is bound to an undefined source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - alt: { - source: 'plguin/undefined-source', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Image', - } ); - await imageBlock.click(); - - // Replace controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Replace', - } ) - ).toBeVisible(); - - // Alt textarea is disabled and with the custom field value. - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - ).toHaveAttribute( 'readonly' ); - const altValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - .inputValue(); - expect( altValue ).toBe( 'default alt value' ); - - // Title input is enabled and with the original value. - await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByRole( 'button', { name: 'Advanced' } ) - .click(); - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - ).toBeEnabled(); - const titleValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - .inputValue(); - expect( titleValue ).toBe( 'default title value' ); - } ); - test( 'should disable title input when title is bound to a registered source', async ( { editor, page, @@ -1067,69 +716,6 @@ test.describe( 'Block bindings', () => { expect( titleValue ).toBe( 'text_custom_field' ); } ); - test( 'should disable title input when title is bound to an undefined source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - title: { - source: 'plugin/undefined-source', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Image', - } ); - await imageBlock.click(); - - // Replace controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Replace', - } ) - ).toBeVisible(); - - // Alt textarea is enabled and with the original value. - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - ).toBeEnabled(); - const altValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - .inputValue(); - expect( altValue ).toBe( 'default alt value' ); - - // Title input is disabled and with the custom field value. - await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByRole( 'button', { name: 'Advanced' } ) - .click(); - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - ).toHaveAttribute( 'readonly' ); - const titleValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - .inputValue(); - expect( titleValue ).toBe( 'default title value' ); - } ); - test( 'Multiple bindings should lock the appropriate controls', async ( { editor, page,