diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 43a9fb1a31f1bf..deafde69b35c37 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -156,12 +156,19 @@ export function RichTextWrapper( for ( const [ attribute, args ] of Object.entries( blockBindings ) ) { - // If any of the attributes with source "rich-text" is part of the bindings, - // has a source with `lockAttributesEditing`, disable it. if ( - blockTypeAttributes?.[ attribute ]?.source === - 'rich-text' && - getBlockBindingsSource( args.source )?.lockAttributesEditing + blockTypeAttributes?.[ attribute ]?.source !== 'rich-text' + ) { + break; + } + + // If the source is not defined, or if its value of `lockAttributesEditing` is `true`, disable it. + const blockBindingsSource = getBlockBindingsSource( + args.source + ); + if ( + ! blockBindingsSource || + blockBindingsSource.lockAttributesEditing ) { shouldDisableEditing = true; break; diff --git a/packages/block-library/src/button/edit.js b/packages/block-library/src/button/edit.js index e01898ca00dec4..ff90cdd1bf64c0 100644 --- a/packages/block-library/src/button/edit.js +++ b/packages/block-library/src/button/edit.js @@ -238,15 +238,15 @@ function ButtonEdit( props ) { return {}; } - const { getBlockBindingsSource } = unlock( + const blockBindingsSource = unlock( select( blockEditorStore ) - ); + ).getBlockBindingsSource( metadata?.bindings?.url?.source ); return { lockUrlControls: !! metadata?.bindings?.url && - getBlockBindingsSource( metadata?.bindings?.url?.source ) - ?.lockAttributesEditing, + ( ! blockBindingsSource || + blockBindingsSource?.lockAttributesEditing ), }; }, [ isSelected ] diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index 61d023e4e580a1..86970b588ff03d 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -341,15 +341,15 @@ export function ImageEdit( { return {}; } - const { getBlockBindingsSource } = unlock( + const blockBindingsSource = unlock( select( blockEditorStore ) - ); + ).getBlockBindingsSource( metadata?.bindings?.url?.source ); return { lockUrlControls: !! metadata?.bindings?.url && - getBlockBindingsSource( metadata?.bindings?.url?.source ) - ?.lockAttributesEditing, + ( ! blockBindingsSource || + blockBindingsSource?.lockAttributesEditing ), }; }, [ isSingleSelected ] diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index f551d8df007a8e..f188f8eaaf3101 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -423,23 +423,32 @@ export default function Image( { } = metadata?.bindings || {}; const hasParentPattern = getBlockParentsByBlockName( clientId, 'core/block' ).length > 0; + const urlBindingSource = getBlockBindingsSource( + urlBinding?.source + ); + const altBindingSource = getBlockBindingsSource( + altBinding?.source + ); + const titleBindingSource = getBlockBindingsSource( + titleBinding?.source + ); return { lockUrlControls: !! urlBinding && - getBlockBindingsSource( urlBinding?.source ) - ?.lockAttributesEditing, + ( ! urlBindingSource || + urlBindingSource?.lockAttributesEditing ), lockHrefControls: // Disable editing the link of the URL if the image is inside a pattern instance. // This is a temporary solution until we support overriding the link on the frontend. hasParentPattern, lockAltControls: !! altBinding && - getBlockBindingsSource( altBinding?.source ) - ?.lockAttributesEditing, + ( ! altBindingSource || + altBindingSource?.lockAttributesEditing ), lockTitleControls: !! titleBinding && - getBlockBindingsSource( titleBinding?.source ) - ?.lockAttributesEditing, + ( ! titleBindingSource || + titleBindingSource?.lockAttributesEditing ), }; }, [ clientId, isSingleSelected, metadata?.bindings ] diff --git a/test/e2e/specs/editor/various/block-bindings.spec.js b/test/e2e/specs/editor/various/block-bindings.spec.js index ccd115e79e8d82..d98a4eda512651 100644 --- a/test/e2e/specs/editor/various/block-bindings.spec.js +++ b/test/e2e/specs/editor/various/block-bindings.spec.js @@ -72,7 +72,7 @@ test.describe( 'Block bindings', () => { ); } ); - test( 'Should lock the appropriate controls', async ( { + test( 'Should lock the appropriate controls with a registered source', async ( { editor, page, } ) => { @@ -117,6 +117,52 @@ test.describe( 'Block bindings', () => { 'false' ); } ); + + test( 'Should lock the appropriate controls when source is not defined', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source: 'plugin/undefined-source', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); + 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' + ); + } ); } ); test.describe( 'Heading', () => { @@ -143,7 +189,7 @@ test.describe( 'Block bindings', () => { await expect( headingBlock ).toHaveText( 'text_custom_field' ); } ); - test( 'Should lock the appropriate controls', async ( { + test( 'Should lock the appropriate controls with a registered source', async ( { editor, page, } ) => { @@ -188,6 +234,52 @@ 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', () => { @@ -221,7 +313,7 @@ test.describe( 'Block bindings', () => { await expect( buttonBlock ).toHaveText( 'text_custom_field' ); } ); - test( 'Should lock text controls when text is bound', async ( { + test( 'Should lock text controls when text is bound to a registered source', async ( { editor, page, } ) => { @@ -283,7 +375,69 @@ test.describe( 'Block bindings', () => { ).toBeVisible(); } ); - test( 'Should lock url controls when url is bound', async ( { + 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, } ) => { @@ -343,6 +497,66 @@ 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, @@ -429,7 +643,7 @@ test.describe( 'Block bindings', () => { ).toBeVisible(); } ); - test( 'Should NOT show the upload form when url is bound', async ( { + test( 'Should NOT show the upload form when url is bound to a registered source', async ( { editor, } ) => { await editor.insertBlock( { @@ -457,7 +671,35 @@ test.describe( 'Block bindings', () => { ).toBeHidden(); } ); - test( 'Should lock url controls when url is bound', async ( { + 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, } ) => { @@ -526,7 +768,76 @@ test.describe( 'Block bindings', () => { expect( titleValue ).toBe( 'default title value' ); } ); - test( 'Should disable alt textarea when alt is bound', async ( { + 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, } ) => { @@ -589,7 +900,70 @@ test.describe( 'Block bindings', () => { expect( titleValue ).toBe( 'default title value' ); } ); - test( 'Should disable title input when title is bound', async ( { + 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' ) + ).toBeDisabled(); + 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, } ) => { @@ -652,6 +1026,69 @@ 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' ) + ).toBeDisabled(); + 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,