From 4712500d0dc640c37949bce0b86eb346e61f5ac9 Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Wed, 19 Aug 2026 14:27:07 +1000 Subject: [PATCH 1/3] Add transform to Row for Columns block --- packages/block-library/CHANGELOG.md | 4 + .../src/columns/test/transforms.js | 142 ++++++++++++++++++ .../block-library/src/columns/transforms.js | 76 ++++++++++ 3 files changed, 222 insertions(+) diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index 1eb15bc84b4641..b4fe46cb42ef56 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -6,6 +6,10 @@ - Heading: Declare the heading level and paragraph keyboard shortcuts on the block's variations and transforms, rather than in a `BlockKeyboardShortcuts` component that every editor had to render. The `BlockKeyboardShortcuts` private export has been removed ([#81588](https://github.com/WordPress/gutenberg/pull/81588)). +### Enhancements + +- Columns: Add a transform to the Row variation that preserves column widths through flex child sizing controls. + ### Bug Fixes - Icon: Apply only padding to the inner SVG in the editor, so margin is no longer applied twice compared to the front end ([#81292](https://github.com/WordPress/gutenberg/pull/81292)). diff --git a/packages/block-library/src/columns/test/transforms.js b/packages/block-library/src/columns/test/transforms.js index 1517ee559c07a8..b791dd620a140a 100644 --- a/packages/block-library/src/columns/test/transforms.js +++ b/packages/block-library/src/columns/test/transforms.js @@ -101,6 +101,148 @@ describe( 'transforms', () => { } ); } ); + it( 'transforms Columns to the Row variation of Group with one child for each column', () => { + const block = createBlock( + 'core/columns', + { + align: 'wide', + isStackedOnMobile: true, + verticalAlignment: 'center', + }, + [ + createBlock( 'core/column', {}, [ + createBlock( 'core/paragraph', { content: 'One' } ), + ] ), + createBlock( 'core/column', {}, [ + createBlock( 'core/paragraph', { content: 'Two' } ), + createBlock( 'core/paragraph', { content: 'Three' } ), + ] ), + createBlock( 'core/column' ), + ] + ); + + const transformedBlocks = switchToBlockType( + block, + 'core/group', + 'group-row' + ); + + expect( transformedBlocks[ 0 ] ).toMatchObject( { + name: 'core/group', + attributes: { + align: 'wide', + layout: { + type: 'flex', + flexWrap: 'nowrap', + verticalAlignment: 'center', + }, + }, + } ); + expect( transformedBlocks[ 0 ].attributes ).not.toHaveProperty( + 'isStackedOnMobile' + ); + expect( transformedBlocks[ 0 ].attributes ).not.toHaveProperty( + 'verticalAlignment' + ); + expect( transformedBlocks[ 0 ].innerBlocks ).toHaveLength( 3 ); + expect( transformedBlocks[ 0 ].innerBlocks[ 0 ] ).toMatchObject( { + name: 'core/paragraph', + attributes: { content: 'One' }, + } ); + expect( transformedBlocks[ 0 ].innerBlocks[ 1 ] ).toMatchObject( { + name: 'core/group', + attributes: { layout: { type: 'constrained' } }, + innerBlocks: [ + expect.objectContaining( { + name: 'core/paragraph', + attributes: { content: 'Two' }, + } ), + expect.objectContaining( { + name: 'core/paragraph', + attributes: { content: 'Three' }, + } ), + ], + } ); + expect( transformedBlocks[ 0 ].innerBlocks[ 2 ] ).toMatchObject( { + name: 'core/group', + attributes: { layout: { type: 'constrained' } }, + innerBlocks: [], + } ); + } ); + + it( 'migrates Column widths to Row child sizing controls', () => { + const block = createBlock( 'core/columns', {}, [ + createBlock( 'core/column', { width: '320px' }, [ + createBlock( 'core/paragraph', { + content: 'One', + style: { + color: { text: '#123456' }, + layout: { columnSpan: 2 }, + }, + } ), + ] ), + createBlock( 'core/column', { width: '50%' }, [ + createBlock( 'core/paragraph', { content: 'Two' } ), + ] ), + createBlock( 'core/column', {}, [ + createBlock( 'core/paragraph', { content: 'Three' } ), + ] ), + ] ); + + const transformedBlocks = switchToBlockType( + block, + 'core/group', + 'group-row' + ); + + expect( + transformedBlocks[ 0 ].innerBlocks.map( + ( innerBlock ) => innerBlock.attributes.style.layout + ) + ).toEqual( [ + { + columnSpan: 2, + selfStretch: 'fixed', + flexSize: '320px', + }, + { selfStretch: 'fixed', flexSize: '50%' }, + { selfStretch: 'fill' }, + ] ); + expect( + transformedBlocks[ 0 ].innerBlocks[ 0 ].attributes.style.color + ).toEqual( { text: '#123456' } ); + } ); + + it( 'uses Grow sizing when Column widths are unavailable', () => { + const block = createBlock( 'core/columns', {}, [ + createBlock( 'core/column', {}, [ + createBlock( 'core/paragraph', { content: 'One' } ), + ] ), + createBlock( 'core/column', { width: 'invalid' }, [ + createBlock( 'core/paragraph', { content: 'Two' } ), + ] ), + createBlock( 'core/column', {}, [ + createBlock( 'core/paragraph', { content: 'Three' } ), + ] ), + ] ); + + const transformedBlocks = switchToBlockType( + block, + 'core/group', + 'group-row' + ); + + expect( + transformedBlocks[ 0 ].innerBlocks.map( + ( innerBlock ) => innerBlock.attributes.style.layout + ) + ).toEqual( [ + { selfStretch: 'fill' }, + { selfStretch: 'fill' }, + { selfStretch: 'fill' }, + ] ); + } ); + it( 'transforms Grid variation of Group to Columns using the explicit grid column count', () => { const block = createBlock( 'core/group', diff --git a/packages/block-library/src/columns/transforms.js b/packages/block-library/src/columns/transforms.js index f55ebb2758edd4..ed045022db092e 100644 --- a/packages/block-library/src/columns/transforms.js +++ b/packages/block-library/src/columns/transforms.js @@ -1,9 +1,14 @@ import { + cloneSanitizedBlock, createBlock, createBlocksFromInnerBlocksTemplate, } from '@wordpress/blocks'; const MAXIMUM_SELECTED_BLOCKS = 6; +const ROW_VERTICAL_ALIGNMENTS = [ 'top', 'center', 'bottom', 'stretch' ]; + +const getObjectValue = ( value ) => + value && typeof value === 'object' && ! Array.isArray( value ) ? value : {}; const getColumnBlocksFromGrid = ( innerBlocks, columnCount ) => { const columnWidth = +( 100 / columnCount ).toFixed( 2 ); @@ -37,6 +42,50 @@ const getGridInnerBlocks = ( innerBlocks ) => return columnInnerBlocks; } ); +const getRowInnerBlocks = ( innerBlocks ) => { + return innerBlocks.map( ( column ) => { + const columnInnerBlocks = Array.isArray( column?.innerBlocks ) + ? column.innerBlocks + : []; + const width = column?.attributes?.width; + let columnWidth; + if ( Number.isFinite( width ) ) { + columnWidth = `${ width }%`; + } else if ( typeof width === 'string' && /\d/.test( width ) ) { + columnWidth = width; + } + const childLayout = columnWidth + ? { selfStretch: 'fixed', flexSize: columnWidth } + : { selfStretch: 'fill' }; + + if ( columnInnerBlocks.length === 1 ) { + const innerBlock = columnInnerBlocks[ 0 ]; + const style = getObjectValue( innerBlock.attributes?.style ); + const layout = getObjectValue( style.layout ); + const updatedLayout = { ...layout, ...childLayout }; + if ( ! columnWidth ) { + delete updatedLayout.flexSize; + } + + return cloneSanitizedBlock( innerBlock, { + style: { + ...style, + layout: updatedLayout, + }, + } ); + } + + return createBlock( + 'core/group', + { + layout: { type: 'constrained' }, + style: { layout: childLayout }, + }, + columnInnerBlocks + ); + } ); +}; + const transforms = { from: [ { @@ -161,6 +210,33 @@ const transforms = { }, ], to: [ + { + type: 'block', + blocks: [ 'core/group' ], + variationName: 'group-row', + transform: ( attributes, innerBlocks ) => { + const { verticalAlignment } = attributes; + const rowVerticalAlignment = ROW_VERTICAL_ALIGNMENTS.includes( + verticalAlignment + ) + ? verticalAlignment + : 'stretch'; + return createBlock( + 'core/group', + { + ...attributes, + isStackedOnMobile: undefined, + verticalAlignment: undefined, + layout: { + type: 'flex', + flexWrap: 'nowrap', + verticalAlignment: rowVerticalAlignment, + }, + }, + getRowInnerBlocks( innerBlocks ) + ); + }, + }, { type: 'block', blocks: [ 'core/group' ], From ae840770b6bc91a850382f55a64acaca271f4c15 Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Wed, 19 Aug 2026 14:40:46 +1000 Subject: [PATCH 2/3] deal with equally sized cols --- .../src/columns/test/transforms.js | 10 +++---- .../block-library/src/columns/transforms.js | 29 ++++++++++++++----- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/packages/block-library/src/columns/test/transforms.js b/packages/block-library/src/columns/test/transforms.js index b791dd620a140a..40cb659017606e 100644 --- a/packages/block-library/src/columns/test/transforms.js +++ b/packages/block-library/src/columns/test/transforms.js @@ -213,12 +213,12 @@ describe( 'transforms', () => { ).toEqual( { text: '#123456' } ); } ); - it( 'uses Grow sizing when Column widths are unavailable', () => { + it( 'sets equal fixed widths when no Column widths are set', () => { const block = createBlock( 'core/columns', {}, [ createBlock( 'core/column', {}, [ createBlock( 'core/paragraph', { content: 'One' } ), ] ), - createBlock( 'core/column', { width: 'invalid' }, [ + createBlock( 'core/column', {}, [ createBlock( 'core/paragraph', { content: 'Two' } ), ] ), createBlock( 'core/column', {}, [ @@ -237,9 +237,9 @@ describe( 'transforms', () => { ( innerBlock ) => innerBlock.attributes.style.layout ) ).toEqual( [ - { selfStretch: 'fill' }, - { selfStretch: 'fill' }, - { selfStretch: 'fill' }, + { selfStretch: 'fixed', flexSize: '33.33%' }, + { selfStretch: 'fixed', flexSize: '33.33%' }, + { selfStretch: 'fixed', flexSize: '33.33%' }, ] ); } ); diff --git a/packages/block-library/src/columns/transforms.js b/packages/block-library/src/columns/transforms.js index ed045022db092e..5e8b649a82b275 100644 --- a/packages/block-library/src/columns/transforms.js +++ b/packages/block-library/src/columns/transforms.js @@ -43,17 +43,30 @@ const getGridInnerBlocks = ( innerBlocks ) => } ); const getRowInnerBlocks = ( innerBlocks ) => { - return innerBlocks.map( ( column ) => { - const columnInnerBlocks = Array.isArray( column?.innerBlocks ) - ? column.innerBlocks - : []; + const columnWidths = innerBlocks.map( ( column ) => { const width = column?.attributes?.width; - let columnWidth; if ( Number.isFinite( width ) ) { - columnWidth = `${ width }%`; - } else if ( typeof width === 'string' && /\d/.test( width ) ) { - columnWidth = width; + return `${ width }%`; } + if ( typeof width === 'string' && /\d/.test( width ) ) { + return width; + } + return undefined; + } ); + const allColumnWidthsUnavailable = columnWidths.every( + ( columnWidth ) => ! columnWidth + ); + const equalColumnWidth = innerBlocks.length + ? `${ +( 100 / innerBlocks.length ).toFixed( 2 ) }%` + : undefined; + + return innerBlocks.map( ( column, index ) => { + const columnInnerBlocks = Array.isArray( column?.innerBlocks ) + ? column.innerBlocks + : []; + const columnWidth = + columnWidths[ index ] || + ( allColumnWidthsUnavailable ? equalColumnWidth : undefined ); const childLayout = columnWidth ? { selfStretch: 'fixed', flexSize: columnWidth } : { selfStretch: 'fill' }; From 0b19e93b9cab404ba7cefe7cc9f0d00832505e07 Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Wed, 19 Aug 2026 16:40:33 +1000 Subject: [PATCH 3/3] Allow transforming from Row to Column --- packages/block-library/CHANGELOG.md | 2 +- .../src/columns/test/transforms.js | 120 ++++++++++++++++++ .../block-library/src/columns/transforms.js | 73 +++++++++-- 3 files changed, 185 insertions(+), 10 deletions(-) diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index b4fe46cb42ef56..3daf625de5b5b3 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -8,7 +8,7 @@ ### Enhancements -- Columns: Add a transform to the Row variation that preserves column widths through flex child sizing controls. +- Columns: Add transforms between Columns and the Row variation that preserve column widths through flex child sizing controls. ### Bug Fixes diff --git a/packages/block-library/src/columns/test/transforms.js b/packages/block-library/src/columns/test/transforms.js index 40cb659017606e..062ebb9322ae50 100644 --- a/packages/block-library/src/columns/test/transforms.js +++ b/packages/block-library/src/columns/test/transforms.js @@ -243,6 +243,126 @@ describe( 'transforms', () => { ] ); } ); + it( 'transforms the Row variation of Group to Columns with one Column per Row child', () => { + const block = createBlock( + 'core/group', + { + align: 'wide', + layout: { + type: 'flex', + flexWrap: 'nowrap', + verticalAlignment: 'center', + }, + }, + [ + createBlock( 'core/paragraph', { content: 'One' } ), + createBlock( + 'core/group', + { layout: { type: 'constrained' } }, + [ + createBlock( 'core/paragraph', { content: 'Two' } ), + createBlock( 'core/paragraph', { content: 'Three' } ), + ] + ), + ] + ); + + const transformedBlocks = switchToBlockType( block, 'core/columns' ); + + expect( transformedBlocks[ 0 ] ).toMatchObject( { + name: 'core/columns', + attributes: { + align: 'wide', + verticalAlignment: 'center', + }, + } ); + expect( transformedBlocks[ 0 ].attributes ).not.toHaveProperty( + 'layout' + ); + expect( transformedBlocks[ 0 ].innerBlocks ).toHaveLength( 2 ); + expect( transformedBlocks[ 0 ].innerBlocks[ 0 ] ).toMatchObject( { + name: 'core/column', + innerBlocks: [ + { + name: 'core/paragraph', + attributes: { content: 'One' }, + }, + ], + } ); + expect( transformedBlocks[ 0 ].innerBlocks[ 1 ] ).toMatchObject( { + name: 'core/column', + innerBlocks: [ + { + name: 'core/group', + attributes: { layout: { type: 'constrained' } }, + innerBlocks: [ + { attributes: { content: 'Two' } }, + { attributes: { content: 'Three' } }, + ], + }, + ], + } ); + } ); + + it( 'migrates fixed Row child sizes to Column widths', () => { + const block = createBlock( + 'core/group', + { layout: { type: 'flex', flexWrap: 'nowrap' } }, + [ + createBlock( 'core/paragraph', { + content: 'One', + style: { + color: { text: '#123456' }, + layout: { + columnSpan: 2, + selfStretch: 'fixed', + flexSize: '320px', + }, + }, + } ), + createBlock( 'core/paragraph', { + content: 'Two', + style: { + layout: { + selfStretch: 'fixedNoShrink', + flexSize: '50%', + }, + }, + } ), + createBlock( 'core/paragraph', { + content: 'Three', + style: { + layout: { + selfStretch: 'fill', + flexSize: '25%', + }, + }, + } ), + ] + ); + + const transformedBlocks = switchToBlockType( block, 'core/columns' ); + + expect( + transformedBlocks[ 0 ].innerBlocks.map( + ( column ) => column.attributes.width + ) + ).toEqual( [ '320px', '50%', undefined ] ); + expect( + transformedBlocks[ 0 ].innerBlocks[ 0 ].innerBlocks[ 0 ].attributes + .style + ).toEqual( { + color: { text: '#123456' }, + layout: { columnSpan: 2 }, + } ); + expect( + transformedBlocks[ 0 ].innerBlocks[ 1 ].innerBlocks[ 0 ].attributes + ).not.toHaveProperty( 'style' ); + expect( + transformedBlocks[ 0 ].innerBlocks[ 2 ].innerBlocks[ 0 ].attributes + ).not.toHaveProperty( 'style' ); + } ); + it( 'transforms Grid variation of Group to Columns using the explicit grid column count', () => { const block = createBlock( 'core/group', diff --git a/packages/block-library/src/columns/transforms.js b/packages/block-library/src/columns/transforms.js index 5e8b649a82b275..19545e149eabcb 100644 --- a/packages/block-library/src/columns/transforms.js +++ b/packages/block-library/src/columns/transforms.js @@ -5,11 +5,23 @@ import { } from '@wordpress/blocks'; const MAXIMUM_SELECTED_BLOCKS = 6; -const ROW_VERTICAL_ALIGNMENTS = [ 'top', 'center', 'bottom', 'stretch' ]; +const COLUMN_VERTICAL_ALIGNMENTS = [ 'top', 'center', 'bottom' ]; +const ROW_VERTICAL_ALIGNMENTS = [ ...COLUMN_VERTICAL_ALIGNMENTS, 'stretch' ]; +const FLEX_SIZE_LAYOUT_VALUES = [ 'fixed', 'fixedNoShrink' ]; const getObjectValue = ( value ) => value && typeof value === 'object' && ! Array.isArray( value ) ? value : {}; +const getColumnWidth = ( width ) => { + if ( Number.isFinite( width ) ) { + return `${ width }%`; + } + if ( typeof width === 'string' && /\d/.test( width ) ) { + return width; + } + return undefined; +}; + const getColumnBlocksFromGrid = ( innerBlocks, columnCount ) => { const columnWidth = +( 100 / columnCount ).toFixed( 2 ); const innerBlocksTemplate = Array.from( @@ -27,6 +39,35 @@ const getColumnBlocksFromGrid = ( innerBlocks, columnCount ) => { return createBlocksFromInnerBlocksTemplate( innerBlocksTemplate ); }; +const getColumnBlocksFromRow = ( innerBlocks ) => + innerBlocks.map( ( innerBlock ) => { + const style = getObjectValue( innerBlock?.attributes?.style ); + const { selfStretch, flexSize, ...remainingLayout } = getObjectValue( + style.layout + ); + const columnWidth = FLEX_SIZE_LAYOUT_VALUES.includes( selfStretch ) + ? getColumnWidth( flexSize ) + : undefined; + + const updatedStyle = { ...style }; + if ( Object.keys( remainingLayout ).length ) { + updatedStyle.layout = remainingLayout; + } else { + delete updatedStyle.layout; + } + const columnInnerBlock = cloneSanitizedBlock( innerBlock, { + style: Object.keys( updatedStyle ).length + ? updatedStyle + : undefined, + } ); + + return createBlock( + 'core/column', + columnWidth ? { width: columnWidth } : {}, + [ columnInnerBlock ] + ); + } ); + const getGridInnerBlocks = ( innerBlocks ) => innerBlocks.flatMap( ( column ) => { const columnInnerBlocks = column.innerBlocks || []; @@ -44,14 +85,7 @@ const getGridInnerBlocks = ( innerBlocks ) => const getRowInnerBlocks = ( innerBlocks ) => { const columnWidths = innerBlocks.map( ( column ) => { - const width = column?.attributes?.width; - if ( Number.isFinite( width ) ) { - return `${ width }%`; - } - if ( typeof width === 'string' && /\d/.test( width ) ) { - return width; - } - return undefined; + return getColumnWidth( column?.attributes?.width ); } ); const allColumnWidthsUnavailable = columnWidths.every( ( columnWidth ) => ! columnWidth @@ -120,6 +154,27 @@ const transforms = { Number.isInteger( layout?.columnCount ) && layout.columnCount > 0, }, + { + type: 'block', + blocks: [ 'core/group' ], + priority: 1, + transform: ( attributes, innerBlocks ) => { + const { layout, ...rest } = attributes; + const verticalAlignment = COLUMN_VERTICAL_ALIGNMENTS.includes( + layout?.verticalAlignment + ) + ? layout.verticalAlignment + : undefined; + + return createBlock( + 'core/columns', + { ...rest, verticalAlignment }, + getColumnBlocksFromRow( innerBlocks ) + ); + }, + isMatch: ( { layout } ) => + layout?.type === 'flex' && layout?.orientation !== 'vertical', + }, { type: 'block', isMultiBlock: true,