-
+ >
);
}
diff --git a/packages/block-editor/src/components/content-only-controls/styles.scss b/packages/block-editor/src/components/content-only-controls/styles.scss
index 71d76cdc25a8aa..1bfadb7382dfeb 100644
--- a/packages/block-editor/src/components/content-only-controls/styles.scss
+++ b/packages/block-editor/src/components/content-only-controls/styles.scss
@@ -33,3 +33,12 @@
.block-editor-content-only-controls__drill-down-button {
width: 100%;
}
+
+.block-editor-content-only-controls__fields-container {
+ padding: 0 $grid-unit-20;
+}
+
+.block-editor-content-only-controls__fields-header {
+ padding: $grid-unit-10 0;
+ margin-bottom: $grid-unit-05;
+}
diff --git a/packages/block-library/src/audio/index.js b/packages/block-library/src/audio/index.js
index d00aba880fbce5..1d051918a621c7 100644
--- a/packages/block-library/src/audio/index.js
+++ b/packages/block-library/src/audio/index.js
@@ -39,8 +39,9 @@ export const settings = {
if ( window.__experimentalContentOnlyPatternInsertion ) {
settings[ fieldsKey ] = [
{
+ id: 'audio',
label: __( 'Audio' ),
- type: 'Media',
+ type: 'media',
shownByDefault: true,
mapping: {
id: 'id',
@@ -52,12 +53,10 @@ if ( window.__experimentalContentOnlyPatternInsertion ) {
},
},
{
+ id: 'caption',
label: __( 'Caption' ),
- type: 'RichText',
+ type: 'richtext',
shownByDefault: false,
- mapping: {
- value: 'caption',
- },
},
];
}
diff --git a/packages/block-library/src/button/index.js b/packages/block-library/src/button/index.js
index 9425b0797d1527..090e8ff2d0ceb5 100644
--- a/packages/block-library/src/button/index.js
+++ b/packages/block-library/src/button/index.js
@@ -41,16 +41,15 @@ export const settings = {
if ( window.__experimentalContentOnlyPatternInsertion ) {
settings[ fieldsKey ] = [
{
+ id: 'text',
label: __( 'Content' ),
- type: 'RichText',
+ type: 'richtext',
shownByDefault: true,
- mapping: {
- value: 'text',
- },
},
{
+ id: 'link',
label: __( 'Link' ),
- type: 'Link',
+ type: 'link',
shownByDefault: false,
mapping: {
href: 'url',
diff --git a/packages/block-library/src/code/index.js b/packages/block-library/src/code/index.js
index 072d6510a8aec9..9ab8fea1ba3cfe 100644
--- a/packages/block-library/src/code/index.js
+++ b/packages/block-library/src/code/index.js
@@ -46,12 +46,10 @@ export const settings = {
if ( window.__experimentalContentOnlyPatternInsertion ) {
settings[ fieldsKey ] = [
{
+ id: 'content',
label: __( 'Code' ),
- type: 'RichText',
+ type: 'richtext',
shownByDefault: true,
- mapping: {
- value: 'content',
- },
},
];
}
diff --git a/packages/block-library/src/cover/index.js b/packages/block-library/src/cover/index.js
index b5bce0823628f9..2445a7adc5a3a5 100644
--- a/packages/block-library/src/cover/index.js
+++ b/packages/block-library/src/cover/index.js
@@ -59,8 +59,9 @@ export const settings = {
if ( window.__experimentalContentOnlyPatternInsertion ) {
settings[ fieldsKey ] = [
{
+ id: 'background',
label: __( 'Background' ),
- type: 'Media',
+ type: 'media',
shownByDefault: true,
mapping: {
type: 'backgroundType',
diff --git a/packages/block-library/src/details/index.js b/packages/block-library/src/details/index.js
index d19580bbc13453..9fc72c714543c2 100644
--- a/packages/block-library/src/details/index.js
+++ b/packages/block-library/src/details/index.js
@@ -68,12 +68,10 @@ export const settings = {
if ( window.__experimentalContentOnlyPatternInsertion ) {
settings[ fieldsKey ] = [
{
+ id: 'summary',
label: __( 'Summary' ),
- type: 'RichText',
+ type: 'richtext',
shownByDefault: true,
- mapping: {
- value: 'summary',
- },
},
];
}
diff --git a/packages/block-library/src/file/index.js b/packages/block-library/src/file/index.js
index 4d05bf27d9fb73..4b007dde8685cd 100644
--- a/packages/block-library/src/file/index.js
+++ b/packages/block-library/src/file/index.js
@@ -39,8 +39,9 @@ export const settings = {
if ( window.__experimentalContentOnlyPatternInsertion ) {
settings[ fieldsKey ] = [
{
+ id: 'file',
label: __( 'File' ),
- type: 'Media',
+ type: 'media',
shownByDefault: true,
mapping: {
id: 'id',
@@ -52,20 +53,16 @@ if ( window.__experimentalContentOnlyPatternInsertion ) {
},
},
{
+ id: 'fileName',
label: __( 'Filename' ),
- type: 'RichText',
+ type: 'richtext',
shownByDefault: false,
- mapping: {
- value: 'fileName',
- },
},
{
+ id: 'downloadButtonText',
label: __( 'Button Text' ),
- type: 'RichText',
+ type: 'richtext',
shownByDefault: false,
- mapping: {
- value: 'downloadButtonText',
- },
},
];
}
diff --git a/packages/block-library/src/heading/index.js b/packages/block-library/src/heading/index.js
index e4121a1e998719..3d6cde988ce78e 100644
--- a/packages/block-library/src/heading/index.js
+++ b/packages/block-library/src/heading/index.js
@@ -76,12 +76,10 @@ export const settings = {
if ( window.__experimentalContentOnlyPatternInsertion ) {
settings[ fieldsKey ] = [
{
+ id: 'content',
label: __( 'Content' ),
- type: 'RichText',
+ type: 'richtext',
shownByDefault: true,
- mapping: {
- value: 'content',
- },
},
];
}
diff --git a/packages/block-library/src/image/index.js b/packages/block-library/src/image/index.js
index 11689cb7ea6a4a..ab524f6a3ab935 100644
--- a/packages/block-library/src/image/index.js
+++ b/packages/block-library/src/image/index.js
@@ -69,8 +69,9 @@ export const settings = {
if ( window.__experimentalContentOnlyPatternInsertion ) {
settings[ fieldsKey ] = [
{
+ id: 'image',
label: __( 'Image' ),
- type: 'Media',
+ type: 'media',
shownByDefault: true,
mapping: {
id: 'id',
@@ -84,8 +85,9 @@ if ( window.__experimentalContentOnlyPatternInsertion ) {
},
},
{
+ id: 'link',
label: __( 'Link' ),
- type: 'Link',
+ type: 'link',
shownByDefault: false,
mapping: {
href: 'href',
@@ -95,20 +97,16 @@ if ( window.__experimentalContentOnlyPatternInsertion ) {
},
},
{
+ id: 'caption',
label: __( 'Caption' ),
- type: 'RichText',
+ type: 'richtext',
shownByDefault: false,
- mapping: {
- value: 'caption',
- },
},
{
+ id: 'alt',
label: __( 'Alt text' ),
- type: 'PlainText',
+ type: 'text',
shownByDefault: false,
- mapping: {
- value: 'alt',
- },
},
];
}
diff --git a/packages/block-library/src/list-item/index.js b/packages/block-library/src/list-item/index.js
index 2be2a1f822c5f7..019ccb0a9f9dd0 100644
--- a/packages/block-library/src/list-item/index.js
+++ b/packages/block-library/src/list-item/index.js
@@ -39,12 +39,10 @@ export const settings = {
if ( window.__experimentalContentOnlyPatternInsertion ) {
settings[ fieldsKey ] = [
{
+ id: 'content',
label: __( 'Content' ),
- type: 'RichText',
+ type: 'richtext',
shownByDefault: true,
- mapping: {
- value: 'content',
- },
},
];
}
diff --git a/packages/block-library/src/media-text/index.js b/packages/block-library/src/media-text/index.js
index 66aca5edcba5c9..d3771c78692cd1 100644
--- a/packages/block-library/src/media-text/index.js
+++ b/packages/block-library/src/media-text/index.js
@@ -57,8 +57,9 @@ export const settings = {
if ( window.__experimentalContentOnlyPatternInsertion ) {
settings[ fieldsKey ] = [
{
+ id: 'media',
label: __( 'Media' ),
- type: 'Media',
+ type: 'media',
shownByDefault: true,
mapping: {
id: 'mediaId',
@@ -71,8 +72,9 @@ if ( window.__experimentalContentOnlyPatternInsertion ) {
},
},
{
+ id: 'link',
label: __( 'Link' ),
- type: 'Link',
+ type: 'link',
shownByDefault: false,
mapping: {
href: 'href',
diff --git a/packages/block-library/src/more/index.js b/packages/block-library/src/more/index.js
index 1adbe3bf807bd7..5901dc2b4e462c 100644
--- a/packages/block-library/src/more/index.js
+++ b/packages/block-library/src/more/index.js
@@ -43,12 +43,10 @@ export const settings = {
if ( window.__experimentalContentOnlyPatternInsertion ) {
settings[ fieldsKey ] = [
{
+ id: 'customText',
label: __( 'Content' ),
- type: 'RichText',
+ type: 'richtext',
shownByDefault: true,
- mapping: {
- value: 'customText',
- },
},
];
}
diff --git a/packages/block-library/src/navigation-link/index.js b/packages/block-library/src/navigation-link/index.js
index 4807bb50b2f67a..89d49d4d05cc02 100644
--- a/packages/block-library/src/navigation-link/index.js
+++ b/packages/block-library/src/navigation-link/index.js
@@ -96,16 +96,15 @@ export const settings = {
if ( window.__experimentalContentOnlyPatternInsertion ) {
settings[ fieldsKey ] = [
{
+ id: 'label',
label: __( 'Label' ),
- type: 'RichText',
+ type: 'richtext',
shownByDefault: true,
- mapping: {
- value: 'label',
- },
},
{
+ id: 'link',
label: __( 'Link' ),
- type: 'Link',
+ type: 'link',
shownByDefault: false,
mapping: {
href: 'url',
diff --git a/packages/block-library/src/navigation-submenu/index.js b/packages/block-library/src/navigation-submenu/index.js
index fa0b625e174570..d6762303d5037a 100644
--- a/packages/block-library/src/navigation-submenu/index.js
+++ b/packages/block-library/src/navigation-submenu/index.js
@@ -55,16 +55,15 @@ export const settings = {
if ( window.__experimentalContentOnlyPatternInsertion ) {
settings[ fieldsKey ] = [
{
+ id: 'label',
label: __( 'Label' ),
- type: 'RichText',
+ type: 'richtext',
shownByDefault: true,
- mapping: {
- value: 'label',
- },
},
{
+ id: 'link',
label: __( 'Link' ),
- type: 'Link',
+ type: 'link',
shownByDefault: false,
mapping: {
href: 'url',
diff --git a/packages/block-library/src/paragraph/index.js b/packages/block-library/src/paragraph/index.js
index 23be29edd4591c..ba14f994f5194a 100644
--- a/packages/block-library/src/paragraph/index.js
+++ b/packages/block-library/src/paragraph/index.js
@@ -65,12 +65,10 @@ export const settings = {
if ( window.__experimentalContentOnlyPatternInsertion ) {
settings[ fieldsKey ] = [
{
+ id: 'content',
label: __( 'Content' ),
- type: 'RichText',
+ type: 'richtext',
shownByDefault: true,
- mapping: {
- value: 'content',
- },
},
];
}
diff --git a/packages/block-library/src/preformatted/index.js b/packages/block-library/src/preformatted/index.js
index ad250232e774c6..5860d00b57ae21 100644
--- a/packages/block-library/src/preformatted/index.js
+++ b/packages/block-library/src/preformatted/index.js
@@ -46,12 +46,10 @@ export const settings = {
if ( window.__experimentalContentOnlyPatternInsertion ) {
settings[ fieldsKey ] = [
{
+ id: 'content',
label: __( 'Content' ),
- type: 'RichText',
+ type: 'richtext',
shownByDefault: true,
- mapping: {
- value: 'content',
- },
},
];
}
diff --git a/packages/block-library/src/pullquote/index.js b/packages/block-library/src/pullquote/index.js
index 697fdbdf2f7793..2bcb196b0220bd 100644
--- a/packages/block-library/src/pullquote/index.js
+++ b/packages/block-library/src/pullquote/index.js
@@ -43,20 +43,16 @@ export const settings = {
if ( window.__experimentalContentOnlyPatternInsertion ) {
settings[ fieldsKey ] = [
{
+ id: 'value',
label: __( 'Content' ),
- type: 'RichText',
+ type: 'richtext',
shownByDefault: true,
- mapping: {
- value: 'value',
- },
},
{
+ id: 'citation',
label: __( 'Citation' ),
- type: 'RichText',
+ type: 'richtext',
shownByDefault: false,
- mapping: {
- value: 'citation',
- },
},
];
}
diff --git a/packages/block-library/src/search/index.js b/packages/block-library/src/search/index.js
index 96a738570fcf39..1dde7dd8703fc5 100644
--- a/packages/block-library/src/search/index.js
+++ b/packages/block-library/src/search/index.js
@@ -33,28 +33,22 @@ export const settings = {
if ( window.__experimentalContentOnlyPatternInsertion ) {
settings[ fieldsKey ] = [
{
+ id: 'label',
label: __( 'Label' ),
- type: 'RichText',
+ type: 'richtext',
shownByDefault: true,
- mapping: {
- value: 'label',
- },
},
{
+ id: 'buttonText',
label: __( 'Button text' ),
- type: 'RichText',
+ type: 'richtext',
shownByDefault: false,
- mapping: {
- value: 'buttonText',
- },
},
{
+ id: 'placeholder',
label: __( 'Placeholder' ),
- type: 'RichText',
+ type: 'richtext',
shownByDefault: false,
- mapping: {
- value: 'placeholder',
- },
},
];
}
diff --git a/packages/block-library/src/social-link/index.js b/packages/block-library/src/social-link/index.js
index 28151b78ca74a5..11da16da0a0d25 100644
--- a/packages/block-library/src/social-link/index.js
+++ b/packages/block-library/src/social-link/index.js
@@ -29,8 +29,9 @@ export const settings = {
if ( window.__experimentalContentOnlyPatternInsertion ) {
settings[ fieldsKey ] = [
{
+ id: 'link',
label: __( 'Link' ),
- type: 'Link',
+ type: 'link',
shownByDefault: true,
mapping: {
href: 'url',
@@ -38,12 +39,10 @@ if ( window.__experimentalContentOnlyPatternInsertion ) {
},
},
{
+ id: 'label',
label: __( 'Label' ),
- type: 'RichText',
+ type: 'richtext',
shownByDefault: false,
- mapping: {
- value: 'label',
- },
},
];
}
diff --git a/packages/block-library/src/verse/index.js b/packages/block-library/src/verse/index.js
index ea8faa5cad7d8e..5288cf821413b3 100644
--- a/packages/block-library/src/verse/index.js
+++ b/packages/block-library/src/verse/index.js
@@ -48,12 +48,10 @@ export const settings = {
if ( window.__experimentalContentOnlyPatternInsertion ) {
settings[ fieldsKey ] = [
{
+ id: 'content',
label: __( 'Content' ),
- type: 'RichText',
+ type: 'richtext',
shownByDefault: true,
- mapping: {
- value: 'content',
- },
},
];
}
diff --git a/packages/block-library/src/video/index.js b/packages/block-library/src/video/index.js
index bdfea021016192..e29989051d50d7 100644
--- a/packages/block-library/src/video/index.js
+++ b/packages/block-library/src/video/index.js
@@ -40,8 +40,9 @@ export const settings = {
if ( window.__experimentalContentOnlyPatternInsertion ) {
settings[ fieldsKey ] = [
{
+ id: 'video',
label: __( 'Video' ),
- type: 'Media',
+ type: 'media',
shownByDefault: true,
mapping: {
id: 'id',
@@ -55,12 +56,10 @@ if ( window.__experimentalContentOnlyPatternInsertion ) {
},
},
{
+ id: 'caption',
label: __( 'Caption' ),
- type: 'RichText',
+ type: 'richtext',
shownByDefault: false,
- mapping: {
- value: 'caption',
- },
},
];
}
From e8c1ca477e0d9db28d078cd04f65c4e27765b0d6 Mon Sep 17 00:00:00 2001
From: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
Date: Tue, 18 Nov 2025 16:48:12 +1100
Subject: [PATCH 02/12] Fix media with Cover, Media and Text, and Image blocks
---
.../components/content-only-controls/index.js | 62 +++++++++++++--
.../content-only-controls/link/index.js | 34 ++++----
.../content-only-controls/media/index.js | 77 ++++++++++++++-----
.../content-only-controls/rich-text/index.js | 12 ++-
.../block-library/src/media-text/index.js | 1 +
5 files changed, 138 insertions(+), 48 deletions(-)
diff --git a/packages/block-editor/src/components/content-only-controls/index.js b/packages/block-editor/src/components/content-only-controls/index.js
index 47f039b1156830..2d66b2f1fb19e6 100644
--- a/packages/block-editor/src/components/content-only-controls/index.js
+++ b/packages/block-editor/src/components/content-only-controls/index.js
@@ -74,14 +74,29 @@ function BlockFields( { clientId } ) {
// Build DataForm fields with proper structure
const dataFormFields = useMemo( () => {
- return blockTypeFields?.map( ( fieldDef ) => {
+ if ( ! blockTypeFields?.length ) {
+ return [];
+ }
+
+ return blockTypeFields.map( ( fieldDef ) => {
const ControlComponent = CONTROLS[ fieldDef.type ];
+ const defaultValues = {};
+ if ( fieldDef.mapping && blockType?.attributes ) {
+ Object.entries( fieldDef.mapping ).forEach(
+ ( [ key, attrKey ] ) => {
+ defaultValues[ key ] =
+ blockType.attributes[ attrKey ]?.defaultValue ??
+ undefined;
+ }
+ );
+ }
+
const field = {
id: fieldDef.id,
label: fieldDef.label,
type: fieldDef.type, // Use the field's type; DataForm will use built-in or custom Edit
- config: fieldDef.args || {},
+ config: { ...fieldDef.args, defaultValues },
hideLabelFromVision: fieldDef.id === 'content',
// getValue and setValue handle the mapping to block attributes
getValue: ( { item } ) => {
@@ -104,10 +119,13 @@ function BlockFields( { clientId } ) {
const updates = {};
Object.entries( fieldDef.mapping ).forEach(
( [ key, attrKey ] ) => {
- updates[ attrKey ] =
- value[ key ] !== undefined
- ? value[ key ]
- : item[ attrKey ];
+ // If key is explicitly in value, use it (even if undefined to allow clearing)
+ // Otherwise, preserve the old value
+ if ( key in value ) {
+ updates[ attrKey ] = value[ key ];
+ } else {
+ updates[ attrKey ] = item[ attrKey ];
+ }
}
);
return updates;
@@ -120,11 +138,20 @@ function BlockFields( { clientId } ) {
// Only add custom Edit component if one exists for this type
if ( ControlComponent ) {
field.Edit = ControlComponent;
+ // Pass clientId and updateBlockAttributes to custom Edit components
+ field.clientId = clientId;
+ field.updateBlockAttributes = updateBlockAttributes;
+ field.fieldDef = fieldDef;
}
return field;
} );
- }, [ blockTypeFields ] );
+ }, [
+ blockTypeFields,
+ blockType?.attributes,
+ clientId,
+ updateBlockAttributes,
+ ] );
// Build form config showing only visible fields
const form = useMemo(
@@ -171,7 +198,26 @@ function BlockFields( { clientId } ) {
fields={ dataFormFields }
form={ form }
onChange={ ( changes ) => {
- updateBlockAttributes( clientId, changes );
+ // Map field values to block attributes using field.setValue
+ const mappedChanges = {};
+ Object.entries( changes ).forEach(
+ ( [ fieldId, fieldValue ] ) => {
+ const field = dataFormFields.find(
+ ( f ) => f.id === fieldId
+ );
+ if ( field && field.setValue ) {
+ const updates = field.setValue( {
+ item: attributes,
+ value: fieldValue,
+ } );
+ Object.assign( mappedChanges, updates );
+ } else {
+ // For fields without setValue, use the value directly
+ mappedChanges[ fieldId ] = fieldValue;
+ }
+ }
+ );
+ updateBlockAttributes( clientId, mappedChanges );
} }
/>
diff --git a/packages/block-editor/src/components/content-only-controls/link/index.js b/packages/block-editor/src/components/content-only-controls/link/index.js
index d3efa0ddb52380..4150a845d235c2 100644
--- a/packages/block-editor/src/components/content-only-controls/link/index.js
+++ b/packages/block-editor/src/components/content-only-controls/link/index.js
@@ -67,12 +67,19 @@ export function getUpdatedLinkAttributes( {
};
}
-export default function Link( { data, field, onChange } ) {
+export default function Link( { data, field } ) {
const [ isLinkControlOpen, setIsLinkControlOpen ] = useState( false );
const { popoverProps } = useInspectorPopoverPlacement( {
isControl: true,
} );
+ // For custom Edit components, we need to call updateBlockAttributes directly
+ const { clientId, updateBlockAttributes } = field;
+ const updateAttributes = ( newValue ) => {
+ const mappedChanges = field.setValue( { item: data, value: newValue } );
+ updateBlockAttributes( clientId, mappedChanges );
+ };
+
const value = field.getValue( { item: data } );
const href = value?.href || value?.url;
const rel = value?.rel || '';
@@ -140,24 +147,17 @@ export default function Link( { data, field, onChange } ) {
...newValues,
} );
- onChange(
- field.setValue( {
- item: data,
- value: {
- ...value,
- href: updatedAttrs.url,
- url: updatedAttrs.url,
- rel: updatedAttrs.rel,
- target: updatedAttrs.linkTarget,
- linkTarget: updatedAttrs.linkTarget,
- },
- } )
- );
+ updateAttributes( {
+ ...value,
+ href: updatedAttrs.url,
+ url: updatedAttrs.url,
+ rel: updatedAttrs.rel,
+ target: updatedAttrs.linkTarget,
+ linkTarget: updatedAttrs.linkTarget,
+ } );
} }
onRemove={ () => {
- onChange(
- field.setValue( { item: data, value: {} } )
- );
+ updateAttributes( {} );
} }
/>
diff --git a/packages/block-editor/src/components/content-only-controls/media/index.js b/packages/block-editor/src/components/content-only-controls/media/index.js
index 5353c164e9ef32..ae3790717569b4 100644
--- a/packages/block-editor/src/components/content-only-controls/media/index.js
+++ b/packages/block-editor/src/components/content-only-controls/media/index.js
@@ -83,12 +83,27 @@ function MediaThumbnail( { data, field, attachment } ) {
return ;
}
-export default function Media( { data, field, onChange, config } ) {
+export default function Media( { data, field } ) {
const { popoverProps } = useInspectorPopoverPlacement( {
isControl: true,
} );
const value = field.getValue( { item: data } );
- const { allowedTypes = [], multiple = false } = config || {};
+ const config = field.config || {};
+ const { allowedTypes = [], multiple = false } = config;
+
+ // For custom Edit components, we need to call updateBlockAttributes directly
+ const { clientId, updateBlockAttributes } = field;
+ const updateAttributes = ( newFieldValue ) => {
+ const mappedChanges = field.setValue( {
+ item: data,
+ value: newFieldValue,
+ } );
+ updateBlockAttributes( clientId, mappedChanges );
+ };
+
+ // Check if featured image is supported by checking if it's in the value
+ // Cover block uses 'featuredImage' as the field property name
+ const hasFeaturedImageSupport = 'featuredImage' in value;
const id = value?.id;
const src = value?.src || value?.url;
@@ -138,30 +153,52 @@ export default function Media( { data, field, onChange, config } ) {
multiple={ multiple }
popoverProps={ popoverProps }
onReset={ () => {
- onChange( field.setValue( { item: data, value: {} } ) );
- } }
- useFeaturedImage={ !! value?.useFeaturedImage }
- onToggleFeaturedImage={ () => {
- onChange(
- field.setValue( {
- item: data,
- value: {
- ...value,
- useFeaturedImage: ! value?.useFeaturedImage,
- },
- } )
- );
+ // Reset to empty/cleared values
+ const resetValue = {
+ id: undefined,
+ src: undefined,
+ url: undefined,
+ caption: '',
+ alt: '',
+ };
+ // Merge with existing value to preserve other field properties
+ updateAttributes( { ...value, ...resetValue } );
} }
+ { ...( hasFeaturedImageSupport && {
+ useFeaturedImage: !! value?.featuredImage,
+ onToggleFeaturedImage: () => {
+ updateAttributes( {
+ ...value,
+ featuredImage: ! value?.featuredImage,
+ } );
+ },
+ } ) }
onSelect={ ( selectedMedia ) => {
if ( selectedMedia.id && selectedMedia.url ) {
+ // Determine mediaType from MIME type, not from object type
+ let mediaType = 'image'; // default
+ if ( selectedMedia.mime_type ) {
+ if (
+ selectedMedia.mime_type.startsWith( 'video/' )
+ ) {
+ mediaType = 'video';
+ } else if (
+ selectedMedia.mime_type.startsWith( 'audio/' )
+ ) {
+ mediaType = 'audio';
+ }
+ }
+
const newValue = {
id: selectedMedia.id,
src: selectedMedia.url,
url: selectedMedia.url,
+ type: mediaType,
};
- if ( selectedMedia.type ) {
- newValue.type = selectedMedia.type;
+ // Capture mediaLink
+ if ( selectedMedia.link ) {
+ newValue.link = selectedMedia.link;
}
if ( ! value?.caption && selectedMedia.caption ) {
@@ -174,9 +211,9 @@ export default function Media( { data, field, onChange, config } ) {
newValue.poster = selectedMedia.poster;
}
- onChange(
- field.setValue( { item: data, value: newValue } )
- );
+ // Merge with existing value to preserve other field properties
+ const finalValue = { ...value, ...newValue };
+ updateAttributes( finalValue );
}
} }
renderToggle={ ( buttonProps ) => (
diff --git a/packages/block-editor/src/components/content-only-controls/rich-text/index.js b/packages/block-editor/src/components/content-only-controls/rich-text/index.js
index 21558ba2b75da9..26a67c191fbc18 100644
--- a/packages/block-editor/src/components/content-only-controls/rich-text/index.js
+++ b/packages/block-editor/src/components/content-only-controls/rich-text/index.js
@@ -22,12 +22,18 @@ import { keyboardShortcutContext, inputEventContext } from '../../rich-text';
export default function RichTextControl( {
data,
field,
- onChange,
hideLabelFromVision,
- config,
} ) {
const registry = useRegistry();
const attrValue = field.getValue( { item: data } );
+ const config = field.config || {};
+
+ // For custom Edit components, we need to call updateBlockAttributes directly
+ const { clientId, updateBlockAttributes } = field;
+ const updateAttributes = ( html ) => {
+ const mappedChanges = field.setValue( { item: data, value: html } );
+ updateBlockAttributes( clientId, mappedChanges );
+ };
const [ selection, setSelection ] = useState( {
start: undefined,
end: undefined,
@@ -98,7 +104,7 @@ export default function RichTextControl( {
} = useRichText( {
value: attrValue,
onChange( html, { __unstableFormats, __unstableText } ) {
- onChange( field.setValue( { item: data, value: html } ) );
+ updateAttributes( html );
Object.values( changeHandlers ).forEach( ( changeHandler ) => {
changeHandler( __unstableFormats, __unstableText );
} );
diff --git a/packages/block-library/src/media-text/index.js b/packages/block-library/src/media-text/index.js
index d3771c78692cd1..23e406c5bb7fe6 100644
--- a/packages/block-library/src/media-text/index.js
+++ b/packages/block-library/src/media-text/index.js
@@ -65,6 +65,7 @@ if ( window.__experimentalContentOnlyPatternInsertion ) {
id: 'mediaId',
type: 'mediaType',
src: 'mediaUrl',
+ link: 'mediaLink',
},
args: {
allowedTypes: [ 'image', 'video' ],
From c13fd67c660d2fb19242e3094df42be6db1f5412 Mon Sep 17 00:00:00 2001
From: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
Date: Tue, 18 Nov 2025 16:50:10 +1100
Subject: [PATCH 03/12] Toggle off featured image when doing other things
---
.../src/components/content-only-controls/media/index.js | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/packages/block-editor/src/components/content-only-controls/media/index.js b/packages/block-editor/src/components/content-only-controls/media/index.js
index ae3790717569b4..653155c00cb82c 100644
--- a/packages/block-editor/src/components/content-only-controls/media/index.js
+++ b/packages/block-editor/src/components/content-only-controls/media/index.js
@@ -161,6 +161,10 @@ export default function Media( { data, field } ) {
caption: '',
alt: '',
};
+ // Turn off featured image when resetting
+ if ( hasFeaturedImageSupport ) {
+ resetValue.featuredImage = false;
+ }
// Merge with existing value to preserve other field properties
updateAttributes( { ...value, ...resetValue } );
} }
@@ -211,6 +215,11 @@ export default function Media( { data, field } ) {
newValue.poster = selectedMedia.poster;
}
+ // Turn off featured image when manually selecting media
+ if ( hasFeaturedImageSupport ) {
+ newValue.featuredImage = false;
+ }
+
// Merge with existing value to preserve other field properties
const finalValue = { ...value, ...newValue };
updateAttributes( finalValue );
From db2641af9f04fffe08ce5b38e3c8057cee2d2c4f Mon Sep 17 00:00:00 2001
From: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
Date: Wed, 19 Nov 2025 12:20:14 +1100
Subject: [PATCH 04/12] Fix linting issue... hopefully
---
packages/block-editor/tsconfig.json | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/block-editor/tsconfig.json b/packages/block-editor/tsconfig.json
index 1567e62fc16f6b..1a0cd9d3e2e7cf 100644
--- a/packages/block-editor/tsconfig.json
+++ b/packages/block-editor/tsconfig.json
@@ -9,6 +9,7 @@
{ "path": "../components" },
{ "path": "../compose" },
{ "path": "../data" },
+ { "path": "../dataviews" },
{ "path": "../date" },
{ "path": "../deprecated" },
{ "path": "../dom" },
From 1d591a128d8b842c0eae70ee5d800c04f5c0ab30 Mon Sep 17 00:00:00 2001
From: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
Date: Wed, 19 Nov 2025 14:28:03 +1100
Subject: [PATCH 05/12] Fix the mapping
---
.../components/content-only-controls/index.js | 119 +++++++++++++++++-
.../content-only-controls/link/index.js | 55 ++++++--
.../content-only-controls/media/index.js | 73 +++++++----
3 files changed, 206 insertions(+), 41 deletions(-)
diff --git a/packages/block-editor/src/components/content-only-controls/index.js b/packages/block-editor/src/components/content-only-controls/index.js
index 2d66b2f1fb19e6..53793e349547b8 100644
--- a/packages/block-editor/src/components/content-only-controls/index.js
+++ b/packages/block-editor/src/components/content-only-controls/index.js
@@ -38,6 +38,88 @@ const CONTROLS = {
link: Link,
};
+/**
+ * Normalize a media value to a canonical structure.
+ * Ensures all expected properties exist, even if not in the mapping.
+ *
+ * @param {Object} value - The mapped value from the block attributes
+ * @return {Object} Normalized media value with all properties
+ */
+function normalizeMediaValue( value ) {
+ return {
+ id: value?.id ?? null,
+ src: value?.src ?? '',
+ url: value?.url ?? value?.src ?? '', // url falls back to src
+ caption: value?.caption ?? '',
+ alt: value?.alt ?? '',
+ type: value?.type ?? 'image',
+ poster: value?.poster ?? '',
+ featuredImage: value?.featuredImage ?? false,
+ link: value?.link ?? '',
+ };
+}
+
+/**
+ * Denormalize a media value from canonical structure back to mapped keys.
+ * Only includes properties that are present in the field's mapping.
+ *
+ * @param {Object} value - The normalized media value
+ * @param {Object} fieldDef - The field definition containing the mapping
+ * @return {Object} Value with only mapped properties
+ */
+function denormalizeMediaValue( value, fieldDef ) {
+ if ( ! fieldDef.mapping ) {
+ return value;
+ }
+
+ const result = {};
+ Object.entries( fieldDef.mapping ).forEach( ( [ key ] ) => {
+ if ( key in value ) {
+ result[ key ] = value[ key ];
+ }
+ } );
+ return result;
+}
+
+/**
+ * Normalize a link value to a canonical structure.
+ * Ensures all expected properties exist, even if not in the mapping.
+ *
+ * @param {Object} value - The mapped value from the block attributes
+ * @return {Object} Normalized link value with all properties
+ */
+function normalizeLinkValue( value ) {
+ return {
+ href: value?.href ?? value?.url ?? '',
+ url: value?.url ?? value?.href ?? '', // url falls back to href
+ rel: value?.rel ?? '',
+ target: value?.target ?? value?.linkTarget ?? '',
+ linkTarget: value?.linkTarget ?? value?.target ?? '',
+ };
+}
+
+/**
+ * Denormalize a link value from canonical structure back to mapped keys.
+ * Only includes properties that are present in the field's mapping.
+ *
+ * @param {Object} value - The normalized link value
+ * @param {Object} fieldDef - The field definition containing the mapping
+ * @return {Object} Value with only mapped properties
+ */
+function denormalizeLinkValue( value, fieldDef ) {
+ if ( ! fieldDef.mapping ) {
+ return value;
+ }
+
+ const result = {};
+ Object.entries( fieldDef.mapping ).forEach( ( [ key ] ) => {
+ if ( key in value ) {
+ result[ key ] = value[ key ];
+ }
+ } );
+ return result;
+}
+
function BlockFields( { clientId } ) {
const { attributes, blockType } = useSelect(
( select ) => {
@@ -101,28 +183,53 @@ function BlockFields( { clientId } ) {
// getValue and setValue handle the mapping to block attributes
getValue: ( { item } ) => {
if ( fieldDef.mapping ) {
- // For complex mappings, return an object with all mapped properties
- const value = {};
+ // Extract mapped properties from the block attributes
+ const mappedValue = {};
Object.entries( fieldDef.mapping ).forEach(
( [ key, attrKey ] ) => {
- value[ key ] = item[ attrKey ];
+ mappedValue[ key ] = item[ attrKey ];
}
);
- return value;
+
+ // Normalize to canonical structure based on field type
+ if ( fieldDef.type === 'media' ) {
+ return normalizeMediaValue( mappedValue, fieldDef );
+ }
+ if ( fieldDef.type === 'link' ) {
+ return normalizeLinkValue( mappedValue, fieldDef );
+ }
+
+ // For other types, return as-is
+ return mappedValue;
}
// For simple id-based fields, use the id as the attribute key
return item[ fieldDef.id ];
},
setValue: ( { item, value } ) => {
if ( fieldDef.mapping ) {
+ // Denormalize from canonical structure back to mapped keys
+ let denormalizedValue = value;
+ if ( fieldDef.type === 'media' ) {
+ denormalizedValue = denormalizeMediaValue(
+ value,
+ fieldDef
+ );
+ } else if ( fieldDef.type === 'link' ) {
+ denormalizedValue = denormalizeLinkValue(
+ value,
+ fieldDef
+ );
+ }
+
// Build an object with all mapped attributes
const updates = {};
Object.entries( fieldDef.mapping ).forEach(
( [ key, attrKey ] ) => {
// If key is explicitly in value, use it (even if undefined to allow clearing)
// Otherwise, preserve the old value
- if ( key in value ) {
- updates[ attrKey ] = value[ key ];
+ if ( key in denormalizedValue ) {
+ updates[ attrKey ] =
+ denormalizedValue[ key ];
} else {
updates[ attrKey ] = item[ attrKey ];
}
diff --git a/packages/block-editor/src/components/content-only-controls/link/index.js b/packages/block-editor/src/components/content-only-controls/link/index.js
index 4150a845d235c2..76a4755a564a12 100644
--- a/packages/block-editor/src/components/content-only-controls/link/index.js
+++ b/packages/block-editor/src/components/content-only-controls/link/index.js
@@ -74,7 +74,7 @@ export default function Link( { data, field } ) {
} );
// For custom Edit components, we need to call updateBlockAttributes directly
- const { clientId, updateBlockAttributes } = field;
+ const { clientId, updateBlockAttributes, fieldDef } = field;
const updateAttributes = ( newValue ) => {
const mappedChanges = field.setValue( { item: data, value: newValue } );
updateBlockAttributes( clientId, mappedChanges );
@@ -147,17 +147,52 @@ export default function Link( { data, field } ) {
...newValues,
} );
- updateAttributes( {
- ...value,
- href: updatedAttrs.url,
- url: updatedAttrs.url,
- rel: updatedAttrs.rel,
- target: updatedAttrs.linkTarget,
- linkTarget: updatedAttrs.linkTarget,
- } );
+ // Build update object dynamically based on what's in the mapping
+ const updateValue = { ...value };
+
+ if ( fieldDef?.mapping ) {
+ Object.keys( fieldDef.mapping ).forEach(
+ ( key ) => {
+ if ( key === 'href' || key === 'url' ) {
+ updateValue[ key ] =
+ updatedAttrs.url;
+ } else if ( key === 'rel' ) {
+ updateValue[ key ] =
+ updatedAttrs.rel;
+ } else if (
+ key === 'target' ||
+ key === 'linkTarget'
+ ) {
+ updateValue[ key ] =
+ updatedAttrs.linkTarget;
+ }
+ }
+ );
+ }
+
+ updateAttributes( updateValue );
} }
onRemove={ () => {
- updateAttributes( {} );
+ // Remove all link-related properties based on what's in the mapping
+ const removeValue = {};
+
+ if ( fieldDef?.mapping ) {
+ Object.keys( fieldDef.mapping ).forEach(
+ ( key ) => {
+ if (
+ key === 'href' ||
+ key === 'url' ||
+ key === 'rel' ||
+ key === 'target' ||
+ key === 'linkTarget'
+ ) {
+ removeValue[ key ] = undefined;
+ }
+ }
+ );
+ }
+
+ updateAttributes( removeValue );
} }
/>
diff --git a/packages/block-editor/src/components/content-only-controls/media/index.js b/packages/block-editor/src/components/content-only-controls/media/index.js
index 653155c00cb82c..a47762a7a8fc45 100644
--- a/packages/block-editor/src/components/content-only-controls/media/index.js
+++ b/packages/block-editor/src/components/content-only-controls/media/index.js
@@ -92,7 +92,7 @@ export default function Media( { data, field } ) {
const { allowedTypes = [], multiple = false } = config;
// For custom Edit components, we need to call updateBlockAttributes directly
- const { clientId, updateBlockAttributes } = field;
+ const { clientId, updateBlockAttributes, fieldDef } = field;
const updateAttributes = ( newFieldValue ) => {
const mappedChanges = field.setValue( {
item: data,
@@ -101,9 +101,9 @@ export default function Media( { data, field } ) {
updateBlockAttributes( clientId, mappedChanges );
};
- // Check if featured image is supported by checking if it's in the value
- // Cover block uses 'featuredImage' as the field property name
- const hasFeaturedImageSupport = 'featuredImage' in value;
+ // Check if featured image is supported by checking if it's in the mapping
+ const hasFeaturedImageSupport =
+ fieldDef?.mapping && 'featuredImage' in fieldDef.mapping;
const id = value?.id;
const src = value?.src || value?.url;
@@ -153,7 +153,7 @@ export default function Media( { data, field } ) {
multiple={ multiple }
popoverProps={ popoverProps }
onReset={ () => {
- // Reset to empty/cleared values
+ // Build reset value dynamically based on mapping
const resetValue = {
id: undefined,
src: undefined,
@@ -161,10 +161,12 @@ export default function Media( { data, field } ) {
caption: '',
alt: '',
};
- // Turn off featured image when resetting
+
+ // Turn off featured image when resetting (only if it's in the mapping)
if ( hasFeaturedImageSupport ) {
resetValue.featuredImage = false;
}
+
// Merge with existing value to preserve other field properties
updateAttributes( { ...value, ...resetValue } );
} }
@@ -193,26 +195,47 @@ export default function Media( { data, field } ) {
}
}
- const newValue = {
- id: selectedMedia.id,
- src: selectedMedia.url,
- url: selectedMedia.url,
- type: mediaType,
- };
-
- // Capture mediaLink
- if ( selectedMedia.link ) {
- newValue.link = selectedMedia.link;
- }
+ // Build new value dynamically based on what's in the mapping
+ const newValue = {};
- if ( ! value?.caption && selectedMedia.caption ) {
- newValue.caption = selectedMedia.caption;
- }
- if ( ! value?.alt && selectedMedia.alt ) {
- newValue.alt = selectedMedia.alt;
- }
- if ( selectedMedia.poster ) {
- newValue.poster = selectedMedia.poster;
+ // Iterate over mapping keys and set values for supported properties
+ if ( fieldDef?.mapping ) {
+ Object.keys( fieldDef.mapping ).forEach(
+ ( key ) => {
+ if ( key === 'id' ) {
+ newValue[ key ] = selectedMedia.id;
+ } else if (
+ key === 'src' ||
+ key === 'url'
+ ) {
+ newValue[ key ] = selectedMedia.url;
+ } else if ( key === 'type' ) {
+ newValue[ key ] = mediaType;
+ } else if (
+ key === 'link' &&
+ selectedMedia.link
+ ) {
+ newValue[ key ] = selectedMedia.link;
+ } else if (
+ key === 'caption' &&
+ ! value?.caption &&
+ selectedMedia.caption
+ ) {
+ newValue[ key ] = selectedMedia.caption;
+ } else if (
+ key === 'alt' &&
+ ! value?.alt &&
+ selectedMedia.alt
+ ) {
+ newValue[ key ] = selectedMedia.alt;
+ } else if (
+ key === 'poster' &&
+ selectedMedia.poster
+ ) {
+ newValue[ key ] = selectedMedia.poster;
+ }
+ }
+ );
}
// Turn off featured image when manually selecting media
From e32127c362aaa27125cb883523f1a522ff8e36ca Mon Sep 17 00:00:00 2001
From: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
Date: Wed, 19 Nov 2025 14:36:17 +1100
Subject: [PATCH 06/12] Update dropdown label
---
.../components/content-only-controls/fields-dropdown-menu.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/block-editor/src/components/content-only-controls/fields-dropdown-menu.js b/packages/block-editor/src/components/content-only-controls/fields-dropdown-menu.js
index 828cf404d2d630..f307276c19c0fb 100644
--- a/packages/block-editor/src/components/content-only-controls/fields-dropdown-menu.js
+++ b/packages/block-editor/src/components/content-only-controls/fields-dropdown-menu.js
@@ -17,7 +17,7 @@ export default function FieldsDropdownMenu( {
return (
{ ( { onClose } ) => (
From a14a4aac76212f4a94a267e64465874676652ca4 Mon Sep 17 00:00:00 2001
From: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
Date: Wed, 19 Nov 2025 15:07:22 +1100
Subject: [PATCH 07/12] Try making things a tiny bit more consistent
---
.../components/content-only-controls/index.js | 33 ++++++++++++++++---
.../content-only-controls/link/index.js | 6 ++--
.../content-only-controls/media/index.js | 9 ++---
.../content-only-controls/rich-text/index.js | 25 +++++++-------
4 files changed, 45 insertions(+), 28 deletions(-)
diff --git a/packages/block-editor/src/components/content-only-controls/index.js b/packages/block-editor/src/components/content-only-controls/index.js
index 53793e349547b8..16ed48d78d22f1 100644
--- a/packages/block-editor/src/components/content-only-controls/index.js
+++ b/packages/block-editor/src/components/content-only-controls/index.js
@@ -38,6 +38,27 @@ const CONTROLS = {
link: Link,
};
+/**
+ * Creates a configured control component that wraps a custom control
+ * and passes configuration as props.
+ *
+ * @param {Object} config - The control configuration
+ * @param {string} config.control - The control type (key in CONTROLS map)
+ * @return {Function} A wrapped control component
+ */
+function createConfiguredControl( config ) {
+ const { control, ...controlConfig } = config;
+ const ControlComponent = CONTROLS[ control ];
+
+ if ( ! ControlComponent ) {
+ throw new Error( `Control type "${ control }" not found` );
+ }
+
+ return function ConfiguredControl( props ) {
+ return ;
+ };
+}
+
/**
* Normalize a media value to a canonical structure.
* Ensures all expected properties exist, even if not in the mapping.
@@ -244,11 +265,13 @@ function BlockFields( { clientId } ) {
// Only add custom Edit component if one exists for this type
if ( ControlComponent ) {
- field.Edit = ControlComponent;
- // Pass clientId and updateBlockAttributes to custom Edit components
- field.clientId = clientId;
- field.updateBlockAttributes = updateBlockAttributes;
- field.fieldDef = fieldDef;
+ // Use EditConfig pattern: Edit is an object with control type and config props
+ field.Edit = createConfiguredControl( {
+ control: fieldDef.type,
+ clientId,
+ updateBlockAttributes,
+ fieldDef,
+ } );
}
return field;
diff --git a/packages/block-editor/src/components/content-only-controls/link/index.js b/packages/block-editor/src/components/content-only-controls/link/index.js
index 76a4755a564a12..15e57bf2bce88e 100644
--- a/packages/block-editor/src/components/content-only-controls/link/index.js
+++ b/packages/block-editor/src/components/content-only-controls/link/index.js
@@ -67,14 +67,12 @@ export function getUpdatedLinkAttributes( {
};
}
-export default function Link( { data, field } ) {
+export default function Link( { data, field, config = {} } ) {
const [ isLinkControlOpen, setIsLinkControlOpen ] = useState( false );
const { popoverProps } = useInspectorPopoverPlacement( {
isControl: true,
} );
-
- // For custom Edit components, we need to call updateBlockAttributes directly
- const { clientId, updateBlockAttributes, fieldDef } = field;
+ const { clientId, updateBlockAttributes, fieldDef } = config;
const updateAttributes = ( newValue ) => {
const mappedChanges = field.setValue( { item: data, value: newValue } );
updateBlockAttributes( clientId, mappedChanges );
diff --git a/packages/block-editor/src/components/content-only-controls/media/index.js b/packages/block-editor/src/components/content-only-controls/media/index.js
index a47762a7a8fc45..62f41fe4d34aa9 100644
--- a/packages/block-editor/src/components/content-only-controls/media/index.js
+++ b/packages/block-editor/src/components/content-only-controls/media/index.js
@@ -83,16 +83,13 @@ function MediaThumbnail( { data, field, attachment } ) {
return ;
}
-export default function Media( { data, field } ) {
+export default function Media( { data, field, config = {} } ) {
const { popoverProps } = useInspectorPopoverPlacement( {
isControl: true,
} );
const value = field.getValue( { item: data } );
- const config = field.config || {};
- const { allowedTypes = [], multiple = false } = config;
-
- // For custom Edit components, we need to call updateBlockAttributes directly
- const { clientId, updateBlockAttributes, fieldDef } = field;
+ const { allowedTypes = [], multiple = false } = field.config || {};
+ const { clientId, updateBlockAttributes, fieldDef } = config;
const updateAttributes = ( newFieldValue ) => {
const mappedChanges = field.setValue( {
item: data,
diff --git a/packages/block-editor/src/components/content-only-controls/rich-text/index.js b/packages/block-editor/src/components/content-only-controls/rich-text/index.js
index 26a67c191fbc18..3e89e577051cfb 100644
--- a/packages/block-editor/src/components/content-only-controls/rich-text/index.js
+++ b/packages/block-editor/src/components/content-only-controls/rich-text/index.js
@@ -23,13 +23,12 @@ export default function RichTextControl( {
data,
field,
hideLabelFromVision,
+ config = {},
} ) {
const registry = useRegistry();
const attrValue = field.getValue( { item: data } );
- const config = field.config || {};
-
- // For custom Edit components, we need to call updateBlockAttributes directly
- const { clientId, updateBlockAttributes } = field;
+ const fieldConfig = field.config || {};
+ const { clientId, updateBlockAttributes } = config;
const updateAttributes = ( html ) => {
const mappedChanges = field.setValue( { item: data, value: html } );
updateBlockAttributes( clientId, mappedChanges );
@@ -44,8 +43,8 @@ export default function RichTextControl( {
const keyboardShortcuts = useRef( new Set() );
const adjustedAllowedFormats = getAllowedFormats( {
- allowedFormats: config?.allowedFormats,
- disableFormats: config?.disableFormats,
+ allowedFormats: fieldConfig?.allowedFormats,
+ disableFormats: fieldConfig?.disableFormats,
} );
const {
@@ -58,7 +57,7 @@ export default function RichTextControl( {
clientId: undefined,
identifier: field.id,
allowedFormats: adjustedAllowedFormats,
- withoutInteractiveFormatting: config?.withoutInteractiveFormatting,
+ withoutInteractiveFormatting: fieldConfig?.withoutInteractiveFormatting,
disableNoneEssentialFormatting: true,
} );
@@ -113,9 +112,9 @@ export default function RichTextControl( {
selectionEnd: selection.end,
onSelectionChange: ( start, end ) => setSelection( { start, end } ),
__unstableIsSelected: isSelected,
- preserveWhiteSpace: !! config?.preserveWhiteSpace,
- placeholder: config?.placeholder,
- __unstableDisableFormats: config?.disableFormats,
+ preserveWhiteSpace: !! fieldConfig?.preserveWhiteSpace,
+ placeholder: fieldConfig?.placeholder,
+ __unstableDisableFormats: fieldConfig?.disableFormats,
__unstableDependencies: dependencies,
__unstableAfterParse: addEditorOnlyFormats,
__unstableBeforeSerialize: removeEditorOnlyFormats,
@@ -149,7 +148,7 @@ export default function RichTextControl( {
Date: Wed, 19 Nov 2025 15:22:50 +1100
Subject: [PATCH 08/12] Revert tiny clientId change
---
.../src/components/content-only-controls/rich-text/index.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/block-editor/src/components/content-only-controls/rich-text/index.js b/packages/block-editor/src/components/content-only-controls/rich-text/index.js
index 3e89e577051cfb..9c5dec389e845d 100644
--- a/packages/block-editor/src/components/content-only-controls/rich-text/index.js
+++ b/packages/block-editor/src/components/content-only-controls/rich-text/index.js
@@ -54,7 +54,7 @@ export default function RichTextControl( {
changeHandlers,
dependencies,
} = useFormatTypes( {
- clientId: undefined,
+ clientId,
identifier: field.id,
allowedFormats: adjustedAllowedFormats,
withoutInteractiveFormatting: fieldConfig?.withoutInteractiveFormatting,
From 468eadc9b1f9d3096f815bda70d0ada56ea41ec8 Mon Sep 17 00:00:00 2001
From: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
Date: Thu, 20 Nov 2025 11:19:52 +1100
Subject: [PATCH 09/12] Tidy up properties
---
.../components/content-only-controls/index.js | 19 +++++-----
.../content-only-controls/link/index.js | 14 +++----
.../content-only-controls/media/index.js | 38 +++++++++++--------
3 files changed, 39 insertions(+), 32 deletions(-)
diff --git a/packages/block-editor/src/components/content-only-controls/index.js b/packages/block-editor/src/components/content-only-controls/index.js
index 16ed48d78d22f1..1af10e5cb2b793 100644
--- a/packages/block-editor/src/components/content-only-controls/index.js
+++ b/packages/block-editor/src/components/content-only-controls/index.js
@@ -61,16 +61,16 @@ function createConfiguredControl( config ) {
/**
* Normalize a media value to a canonical structure.
- * Ensures all expected properties exist, even if not in the mapping.
+ * Uses standard Gutenberg attribute names for media (e.g., "url" for images).
+ * Accepts either name as input but normalizes to the canonical form.
*
* @param {Object} value - The mapped value from the block attributes
- * @return {Object} Normalized media value with all properties
+ * @return {Object} Normalized media value with canonical properties
*/
function normalizeMediaValue( value ) {
return {
id: value?.id ?? null,
- src: value?.src ?? '',
- url: value?.url ?? value?.src ?? '', // url falls back to src
+ url: value?.url ?? value?.src ?? '', // accepts src as fallback for HTML native elements
caption: value?.caption ?? '',
alt: value?.alt ?? '',
type: value?.type ?? 'image',
@@ -104,18 +104,17 @@ function denormalizeMediaValue( value, fieldDef ) {
/**
* Normalize a link value to a canonical structure.
- * Ensures all expected properties exist, even if not in the mapping.
+ * Uses standard Gutenberg attribute names: "url" and "linkTarget".
+ * Accepts either name as input but normalizes to the canonical form.
*
* @param {Object} value - The mapped value from the block attributes
- * @return {Object} Normalized link value with all properties
+ * @return {Object} Normalized link value with canonical properties
*/
function normalizeLinkValue( value ) {
return {
- href: value?.href ?? value?.url ?? '',
- url: value?.url ?? value?.href ?? '', // url falls back to href
+ url: value?.url ?? value?.href ?? '', // accepts href as fallback
rel: value?.rel ?? '',
- target: value?.target ?? value?.linkTarget ?? '',
- linkTarget: value?.linkTarget ?? value?.target ?? '',
+ linkTarget: value?.linkTarget ?? value?.target ?? '', // accepts target as fallback
};
}
diff --git a/packages/block-editor/src/components/content-only-controls/link/index.js b/packages/block-editor/src/components/content-only-controls/link/index.js
index 15e57bf2bce88e..b7af677f3e4d64 100644
--- a/packages/block-editor/src/components/content-only-controls/link/index.js
+++ b/packages/block-editor/src/components/content-only-controls/link/index.js
@@ -79,9 +79,9 @@ export default function Link( { data, field, config = {} } ) {
};
const value = field.getValue( { item: data } );
- const href = value?.href || value?.url;
+ const url = value?.url;
const rel = value?.rel || '';
- const target = value?.target || value?.linkTarget;
+ const target = value?.linkTarget;
const opensInNewTab = target === NEW_TAB_TARGET;
const nofollow = rel === NOFOLLOW_REL;
@@ -89,8 +89,8 @@ export default function Link( { data, field, config = {} } ) {
// Memoize link value to avoid overriding the LinkControl's internal state.
// This is a temporary fix. See https://github.com/WordPress/gutenberg/issues/51256.
const linkValue = useMemo(
- () => ( { url: href, opensInNewTab, nofollow } ),
- [ href, opensInNewTab, nofollow ]
+ () => ( { url, opensInNewTab, nofollow } ),
+ [ url, opensInNewTab, nofollow ]
);
return (
@@ -108,15 +108,15 @@ export default function Link( { data, field, config = {} } ) {
templateColumns="24px 1fr"
className="block-editor-content-only-controls__link-row"
>
- { href && (
+ { url && (
<>
- { href }
+ { url }
>
) }
- { ! href && (
+ { ! url && (
<>
);
}
@@ -103,7 +103,7 @@ export default function Media( { data, field, config = {} } ) {
fieldDef?.mapping && 'featuredImage' in fieldDef.mapping;
const id = value?.id;
- const src = value?.src || value?.url;
+ const url = value?.url;
const attachment = useSelect(
( select ) => {
@@ -146,18 +146,26 @@ export default function Media( { data, field, config = {} } ) {
className="block-editor-content-only-controls__media-replace-flow"
allowedTypes={ allowedTypes }
mediaId={ id }
- mediaURL={ src }
+ mediaURL={ url }
multiple={ multiple }
popoverProps={ popoverProps }
onReset={ () => {
// Build reset value dynamically based on mapping
- const resetValue = {
- id: undefined,
- src: undefined,
- url: undefined,
- caption: '',
- alt: '',
- };
+ const resetValue = {};
+
+ if ( fieldDef?.mapping ) {
+ Object.keys( fieldDef.mapping ).forEach( ( key ) => {
+ if (
+ key === 'id' ||
+ key === 'src' ||
+ key === 'url'
+ ) {
+ resetValue[ key ] = undefined;
+ } else if ( key === 'caption' || key === 'alt' ) {
+ resetValue[ key ] = '';
+ }
+ } );
+ }
// Turn off featured image when resetting (only if it's in the mapping)
if ( hasFeaturedImageSupport ) {
@@ -257,7 +265,7 @@ export default function Media( { data, field, config = {} } ) {
templateColumns="24px 1fr"
className="block-editor-content-only-controls__media-row"
>
- { src && (
+ { url && (
<>
>
) }
- { ! src && (
+ { ! url && (
<>
Date: Thu, 20 Nov 2025 11:49:49 +1100
Subject: [PATCH 10/12] Try to make normalize a little more consistent with the
denormalize functions
---
.../components/content-only-controls/index.js | 75 +++++++++++++------
packages/block-library/src/audio/index.js | 2 +-
packages/block-library/src/button/index.js | 4 +-
packages/block-library/src/cover/index.js | 2 +-
packages/block-library/src/file/index.js | 2 +-
packages/block-library/src/image/index.js | 6 +-
.../block-library/src/media-text/index.js | 6 +-
packages/block-library/src/video/index.js | 2 +-
8 files changed, 66 insertions(+), 33 deletions(-)
diff --git a/packages/block-editor/src/components/content-only-controls/index.js b/packages/block-editor/src/components/content-only-controls/index.js
index 1af10e5cb2b793..b78eeed5b1d6a1 100644
--- a/packages/block-editor/src/components/content-only-controls/index.js
+++ b/packages/block-editor/src/components/content-only-controls/index.js
@@ -61,23 +61,39 @@ function createConfiguredControl( config ) {
/**
* Normalize a media value to a canonical structure.
- * Uses standard Gutenberg attribute names for media (e.g., "url" for images).
- * Accepts either name as input but normalizes to the canonical form.
+ * Only includes properties that are present in the field's mapping (if provided).
*
- * @param {Object} value - The mapped value from the block attributes
+ * @param {Object} value - The mapped value from the block attributes (with canonical keys)
+ * @param {Object} fieldDef - Optional field definition containing the mapping
* @return {Object} Normalized media value with canonical properties
*/
-function normalizeMediaValue( value ) {
- return {
- id: value?.id ?? null,
- url: value?.url ?? value?.src ?? '', // accepts src as fallback for HTML native elements
- caption: value?.caption ?? '',
- alt: value?.alt ?? '',
- type: value?.type ?? 'image',
- poster: value?.poster ?? '',
- featuredImage: value?.featuredImage ?? false,
- link: value?.link ?? '',
+function normalizeMediaValue( value, fieldDef ) {
+ const defaults = {
+ id: null,
+ url: '',
+ caption: '',
+ alt: '',
+ type: 'image',
+ poster: '',
+ featuredImage: false,
+ link: '',
};
+
+ const result = {};
+
+ // If there's a mapping, only include properties that are in it
+ if ( fieldDef?.mapping ) {
+ Object.keys( fieldDef.mapping ).forEach( ( key ) => {
+ result[ key ] = value?.[ key ] ?? defaults[ key ] ?? '';
+ } );
+ return result;
+ }
+
+ // Without mapping, include all default properties
+ Object.keys( defaults ).forEach( ( key ) => {
+ result[ key ] = value?.[ key ] ?? defaults[ key ];
+ } );
+ return result;
}
/**
@@ -104,18 +120,35 @@ function denormalizeMediaValue( value, fieldDef ) {
/**
* Normalize a link value to a canonical structure.
- * Uses standard Gutenberg attribute names: "url" and "linkTarget".
- * Accepts either name as input but normalizes to the canonical form.
+ * Only includes properties that are present in the field's mapping (if provided).
*
- * @param {Object} value - The mapped value from the block attributes
+ * @param {Object} value - The mapped value from the block attributes (with canonical keys)
+ * @param {Object} fieldDef - Optional field definition containing the mapping
* @return {Object} Normalized link value with canonical properties
*/
-function normalizeLinkValue( value ) {
- return {
- url: value?.url ?? value?.href ?? '', // accepts href as fallback
- rel: value?.rel ?? '',
- linkTarget: value?.linkTarget ?? value?.target ?? '', // accepts target as fallback
+function normalizeLinkValue( value, fieldDef ) {
+ const defaults = {
+ url: '',
+ rel: '',
+ linkTarget: '',
+ destination: '',
};
+
+ const result = {};
+
+ // If there's a mapping, only include properties that are in it
+ if ( fieldDef?.mapping ) {
+ Object.keys( fieldDef.mapping ).forEach( ( key ) => {
+ result[ key ] = value?.[ key ] ?? defaults[ key ] ?? '';
+ } );
+ return result;
+ }
+
+ // Without mapping, include all default properties
+ Object.keys( defaults ).forEach( ( key ) => {
+ result[ key ] = value?.[ key ] ?? defaults[ key ];
+ } );
+ return result;
}
/**
diff --git a/packages/block-library/src/audio/index.js b/packages/block-library/src/audio/index.js
index 1d051918a621c7..6f719b60a9675a 100644
--- a/packages/block-library/src/audio/index.js
+++ b/packages/block-library/src/audio/index.js
@@ -45,7 +45,7 @@ if ( window.__experimentalContentOnlyPatternInsertion ) {
shownByDefault: true,
mapping: {
id: 'id',
- src: 'src',
+ url: 'src',
},
args: {
allowedTypes: [ 'audio' ],
diff --git a/packages/block-library/src/button/index.js b/packages/block-library/src/button/index.js
index 090e8ff2d0ceb5..0358f6cad36460 100644
--- a/packages/block-library/src/button/index.js
+++ b/packages/block-library/src/button/index.js
@@ -52,9 +52,9 @@ if ( window.__experimentalContentOnlyPatternInsertion ) {
type: 'link',
shownByDefault: false,
mapping: {
- href: 'url',
+ url: 'url',
rel: 'rel',
- target: 'linkTarget',
+ linkTarget: 'linkTarget',
},
},
];
diff --git a/packages/block-library/src/cover/index.js b/packages/block-library/src/cover/index.js
index 2445a7adc5a3a5..8a7018651f4c5d 100644
--- a/packages/block-library/src/cover/index.js
+++ b/packages/block-library/src/cover/index.js
@@ -66,7 +66,7 @@ if ( window.__experimentalContentOnlyPatternInsertion ) {
mapping: {
type: 'backgroundType',
id: 'id',
- src: 'url',
+ url: 'url',
alt: 'alt',
featuredImage: 'useFeaturedImage',
},
diff --git a/packages/block-library/src/file/index.js b/packages/block-library/src/file/index.js
index 4b007dde8685cd..bea2253ca9098a 100644
--- a/packages/block-library/src/file/index.js
+++ b/packages/block-library/src/file/index.js
@@ -45,7 +45,7 @@ if ( window.__experimentalContentOnlyPatternInsertion ) {
shownByDefault: true,
mapping: {
id: 'id',
- src: 'href',
+ url: 'href',
},
args: {
allowedTypes: [],
diff --git a/packages/block-library/src/image/index.js b/packages/block-library/src/image/index.js
index ab524f6a3ab935..3211746553f9ff 100644
--- a/packages/block-library/src/image/index.js
+++ b/packages/block-library/src/image/index.js
@@ -75,7 +75,7 @@ if ( window.__experimentalContentOnlyPatternInsertion ) {
shownByDefault: true,
mapping: {
id: 'id',
- src: 'url',
+ url: 'url',
caption: 'caption',
alt: 'alt',
},
@@ -90,9 +90,9 @@ if ( window.__experimentalContentOnlyPatternInsertion ) {
type: 'link',
shownByDefault: false,
mapping: {
- href: 'href',
+ url: 'href',
rel: 'rel',
- target: 'linkTarget',
+ linkTarget: 'linkTarget',
destination: 'linkDestination',
},
},
diff --git a/packages/block-library/src/media-text/index.js b/packages/block-library/src/media-text/index.js
index 23e406c5bb7fe6..7c29c7dea81794 100644
--- a/packages/block-library/src/media-text/index.js
+++ b/packages/block-library/src/media-text/index.js
@@ -64,7 +64,7 @@ if ( window.__experimentalContentOnlyPatternInsertion ) {
mapping: {
id: 'mediaId',
type: 'mediaType',
- src: 'mediaUrl',
+ url: 'mediaUrl',
link: 'mediaLink',
},
args: {
@@ -78,9 +78,9 @@ if ( window.__experimentalContentOnlyPatternInsertion ) {
type: 'link',
shownByDefault: false,
mapping: {
- href: 'href',
+ url: 'href',
rel: 'rel',
- target: 'linkTarget',
+ linkTarget: 'linkTarget',
},
},
];
diff --git a/packages/block-library/src/video/index.js b/packages/block-library/src/video/index.js
index e29989051d50d7..a43900a7737c47 100644
--- a/packages/block-library/src/video/index.js
+++ b/packages/block-library/src/video/index.js
@@ -46,7 +46,7 @@ if ( window.__experimentalContentOnlyPatternInsertion ) {
shownByDefault: true,
mapping: {
id: 'id',
- src: 'src',
+ url: 'src',
caption: 'caption',
poster: 'poster',
},
From 2a822b2705c1bbe9b19ea4f8b14b9cab250232d4 Mon Sep 17 00:00:00 2001
From: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
Date: Thu, 20 Nov 2025 11:52:01 +1100
Subject: [PATCH 11/12] Ensure there's padding for the bottom field
---
.../src/components/content-only-controls/styles.scss | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/block-editor/src/components/content-only-controls/styles.scss b/packages/block-editor/src/components/content-only-controls/styles.scss
index 1bfadb7382dfeb..105dc3c0c11f1e 100644
--- a/packages/block-editor/src/components/content-only-controls/styles.scss
+++ b/packages/block-editor/src/components/content-only-controls/styles.scss
@@ -6,7 +6,7 @@
.block-editor-content-only-controls__screen {
&.components-navigator-screen {
- padding: $grid-unit-10 0 0 0;
+ padding: $grid-unit-10 0 $grid-unit-20 0;
}
// Add border for the entire content controls and remove the similar border
From 79242824dba8d1ab6a0e196e146c5376fae86620 Mon Sep 17 00:00:00 2001
From: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
Date: Thu, 20 Nov 2025 11:54:15 +1100
Subject: [PATCH 12/12] Update popover positioning for the dropdown menus
---
.../content-only-controls/fields-dropdown-menu.js | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/packages/block-editor/src/components/content-only-controls/fields-dropdown-menu.js b/packages/block-editor/src/components/content-only-controls/fields-dropdown-menu.js
index f307276c19c0fb..918a633aa039fa 100644
--- a/packages/block-editor/src/components/content-only-controls/fields-dropdown-menu.js
+++ b/packages/block-editor/src/components/content-only-controls/fields-dropdown-menu.js
@@ -5,11 +5,18 @@ import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components';
import { moreVertical, check } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
+/**
+ * Internal dependencies
+ */
+import { useInspectorPopoverPlacement } from './use-inspector-popover-placement';
+
export default function FieldsDropdownMenu( {
fields,
visibleFields,
onToggleField,
} ) {
+ const { popoverProps } = useInspectorPopoverPlacement();
+
if ( ! fields || fields.length === 0 ) {
return null;
}
@@ -18,7 +25,7 @@ export default function FieldsDropdownMenu( {
{ ( { onClose } ) => (