diff --git a/backport-changelog/7.1/12172.md b/backport-changelog/7.1/12172.md new file mode 100644 index 00000000000000..433e663fbcc736 --- /dev/null +++ b/backport-changelog/7.1/12172.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/12172 + +* https://github.com/WordPress/gutenberg/pull/79073 \ No newline at end of file diff --git a/docs/reference-guides/block-api/block-supports.md b/docs/reference-guides/block-api/block-supports.md index cc73de631cec93..eda5b0102f4b67 100644 --- a/docs/reference-guides/block-api/block-supports.md +++ b/docs/reference-guides/block-api/block-supports.md @@ -834,7 +834,7 @@ For the `flow` layout type only, determines display of the "Inner blocks use con - Type: `boolean` - Default value: `false` -For the `flex` layout type only, determines display of sizing controls (Fit/Fill/Fixed) on all child blocks of the flex block. +For the `flex` layout type only, determines display of sizing controls (Fit/Grow/Max width/Fixed) on all child blocks of the flex block. ### layout.allowVerticalAlignment diff --git a/lib/block-supports/layout.php b/lib/block-supports/layout.php index 4d0d79e77b14a9..4ed461a68cd791 100644 --- a/lib/block-supports/layout.php +++ b/lib/block-supports/layout.php @@ -317,13 +317,45 @@ function gutenberg_get_child_layout_style_rules( $selector, $child_layout, $pare return array_key_exists( $property, $viewport_overrides ); }; - $self_stretch = $child_layout['selfStretch'] ?? null; + $self_stretch = $child_layout['selfStretch'] ?? null; + $base_self_stretch = $base_child_layout['selfStretch'] ?? null; + + /* + * These are the serialized `selfStretch` values. `max` used to be called + * "Fixed" in the UI, but was renamed and replaced by `fixedNoShrink`. + */ + $flex_child_layout_values = array( + 'fit' => 'fit', + 'grow' => 'fill', + 'max' => 'fixed', + 'fixed' => 'fixedNoShrink', + ); + $flex_size_values = array( + $flex_child_layout_values['max'], + $flex_child_layout_values['fixed'], + ); if ( null === $viewport_overrides || $has_viewport_property_override( 'selfStretch' ) || $has_viewport_property_override( 'flexSize' ) ) { - if ( 'fixed' === $self_stretch && isset( $child_layout['flexSize'] ) ) { + if ( + null !== $viewport_overrides && + ( $flex_child_layout_values['fit'] === $self_stretch || $flex_child_layout_values['grow'] === $self_stretch ) && + in_array( $base_self_stretch, $flex_size_values, true ) && + isset( $base_child_layout['flexSize'] ) + ) { + $child_layout_declarations['flex-basis'] = 'unset'; + if ( $flex_child_layout_values['fixed'] === $base_self_stretch ) { + $child_layout_declarations['flex-shrink'] = 'unset'; + } + } + if ( in_array( $self_stretch, $flex_size_values, true ) && isset( $child_layout['flexSize'] ) ) { $child_layout_declarations['flex-basis'] = $child_layout['flexSize']; + if ( $flex_child_layout_values['fixed'] === $self_stretch ) { + $child_layout_declarations['flex-shrink'] = '0'; + } elseif ( null !== $viewport_overrides && $flex_child_layout_values['fixed'] === $base_self_stretch ) { + $child_layout_declarations['flex-shrink'] = 'unset'; + } $child_layout_declarations['box-sizing'] = 'border-box'; - } elseif ( 'fill' === $self_stretch ) { + } elseif ( $flex_child_layout_values['grow'] === $self_stretch ) { $child_layout_declarations['flex-grow'] = '1'; } } diff --git a/packages/block-editor/src/components/child-layout-control/index.js b/packages/block-editor/src/components/child-layout-control/index.js index 51541dc7854c06..2e86da314c6244 100644 --- a/packages/block-editor/src/components/child-layout-control/index.js +++ b/packages/block-editor/src/components/child-layout-control/index.js @@ -23,15 +23,51 @@ import { useGetNumberOfBlocksBeforeCell } from '../grid/use-get-number-of-blocks import { store as blockEditorStore } from '../../store'; import { useSettings } from '../use-settings'; -function helpText( selfStretch, parentLayout ) { - const { orientation = 'horizontal' } = parentLayout; +// These are the serialized `selfStretch` values. `max` used to be called +// "Fixed" in the UI, but was renamed and replaced by `fixedNoShrink`. +const FLEX_CHILD_LAYOUT_VALUES = { + fit: 'fit', + grow: 'fill', + max: 'fixed', + fixed: 'fixedNoShrink', +}; - if ( selfStretch === 'fill' ) { +const FLEX_SIZE_VALUES = [ + FLEX_CHILD_LAYOUT_VALUES.max, + FLEX_CHILD_LAYOUT_VALUES.fixed, +]; + +function isFlexSizeValue( value ) { + return FLEX_SIZE_VALUES.includes( value ); +} + +function maxSizeLabel( parentLayout ) { + const { orientation = 'horizontal' } = parentLayout ?? {}; + return orientation === 'horizontal' + ? _x( 'Max', 'Block with maximum width in flex layout' ) + : _x( 'Max', 'Block with maximum height in flex layout' ); +} + +function helpText( flexControlValue, parentLayout ) { + const { orientation = 'horizontal' } = parentLayout ?? {}; + + if ( flexControlValue === FLEX_CHILD_LAYOUT_VALUES.grow ) { return __( 'Stretch to fill available space.' ); } - if ( selfStretch === 'fixed' && orientation === 'horizontal' ) { + if ( + flexControlValue === FLEX_CHILD_LAYOUT_VALUES.max && + orientation === 'horizontal' + ) { + return __( 'Specify a maximum width.' ); + } else if ( flexControlValue === FLEX_CHILD_LAYOUT_VALUES.max ) { + return __( 'Specify a maximum height.' ); + } + if ( + flexControlValue === FLEX_CHILD_LAYOUT_VALUES.fixed && + orientation === 'horizontal' + ) { return __( 'Specify a fixed width.' ); - } else if ( selfStretch === 'fixed' ) { + } else if ( flexControlValue === FLEX_CHILD_LAYOUT_VALUES.fixed ) { return __( 'Specify a fixed height.' ); } return __( 'Fit contents.' ); @@ -99,6 +135,8 @@ function FlexControls( { } ) { const { selfStretch, flexSize } = childLayout; const { orientation = 'horizontal' } = parentLayout ?? {}; + const flexControlValue = selfStretch || FLEX_CHILD_LAYOUT_VALUES.fit; + const hasFlexSizeValue = isFlexSizeValue( flexControlValue ); const hasFlexValue = () => !! selfStretch; const flexResetLabel = orientation === 'horizontal' ? __( 'Width' ) : __( 'Height' ); @@ -121,13 +159,13 @@ function FlexControls( { }; useEffect( () => { - if ( selfStretch === 'fixed' && ! flexSize ) { + if ( isFlexSizeValue( selfStretch ) && ! flexSize ) { onChange( { ...childLayout, - selfStretch: 'fit', + selfStretch: FLEX_CHILD_LAYOUT_VALUES.fit, } ); } - }, [] ); + }, [] ); // eslint-disable-line react-hooks/exhaustive-deps return ( { - const newFlexSize = value !== 'fixed' ? null : flexSize; + const newFlexSize = isFlexSizeValue( value ) + ? flexSize + : null; onChange( { selfStretch: value, flexSize: newFlexSize, @@ -154,37 +194,42 @@ function FlexControls( { isBlock > + - { selfStretch === 'fixed' && ( + { hasFlexSizeValue && ( { onChange( { - selfStretch, + selfStretch: flexControlValue, flexSize: value, } ); } } @@ -199,7 +244,7 @@ function FlexControls( { } export function childLayoutOrientation( parentLayout ) { - const { orientation = 'horizontal' } = parentLayout; + const { orientation = 'horizontal' } = parentLayout ?? {}; return orientation === 'horizontal' ? __( 'Width' ) : __( 'Height' ); } diff --git a/packages/block-editor/src/components/child-layout-control/test/index.js b/packages/block-editor/src/components/child-layout-control/test/index.js index dccecd52052d1d..1a13cf2d1a740c 100644 --- a/packages/block-editor/src/components/child-layout-control/test/index.js +++ b/packages/block-editor/src/components/child-layout-control/test/index.js @@ -14,6 +14,10 @@ import { __experimentalToolsPanel as ToolsPanel } from '@wordpress/components'; */ import ChildLayoutControl from '../'; +jest.mock( '../../use-settings', () => ( { + useSettings: () => [ undefined ], +} ) ); + jest.mock( '@wordpress/data/src/components/use-select', () => jest.fn( ( mapSelect ) => { if ( typeof mapSelect === 'function' ) { @@ -123,4 +127,64 @@ describe( 'ChildLayoutControl', () => { columnSpan: 1, } ); } ); + + it( 'shows legacy fixed flex sizing as max', () => { + renderControl( { + parentLayout: { + type: 'flex', + orientation: 'horizontal', + }, + value: { + selfStretch: 'fixed', + flexSize: '320px', + }, + } ); + + expect( screen.getByRole( 'radio', { name: 'Max' } ) ).toBeChecked(); + } ); + + it( 'sets fixedNoShrink when selecting fixed flex sizing', async () => { + const user = userEvent.setup(); + const onChange = jest.fn(); + + renderControl( { + parentLayout: { + type: 'flex', + orientation: 'horizontal', + }, + value: {}, + onChange, + } ); + + await user.click( screen.getByRole( 'radio', { name: 'Fixed' } ) ); + + expect( onChange ).toHaveBeenCalledWith( { + selfStretch: 'fixedNoShrink', + flexSize: undefined, + } ); + } ); + + it( 'sets legacy fixed when selecting max sizing', async () => { + const user = userEvent.setup(); + const onChange = jest.fn(); + + renderControl( { + parentLayout: { + type: 'flex', + orientation: 'horizontal', + }, + value: { + selfStretch: 'fixedNoShrink', + flexSize: '320px', + }, + onChange, + } ); + + await user.click( screen.getByRole( 'radio', { name: 'Max' } ) ); + + expect( onChange ).toHaveBeenCalledWith( { + selfStretch: 'fixed', + flexSize: '320px', + } ); + } ); } ); diff --git a/packages/block-editor/src/hooks/layout-child.js b/packages/block-editor/src/hooks/layout-child.js index e7dd46194417b1..6e6f8d2cb70d1c 100644 --- a/packages/block-editor/src/hooks/layout-child.js +++ b/packages/block-editor/src/hooks/layout-child.js @@ -28,6 +28,24 @@ const RESPONSIVE_BREAKPOINTS = { tablet: '@media (480px < width <= 782px)', }; +// These are the serialized `selfStretch` values. `max` used to be called +// "Fixed" in the UI, but was renamed and replaced by `fixedNoShrink`. +const FLEX_CHILD_LAYOUT_VALUES = { + fit: 'fit', + grow: 'fill', + max: 'fixed', + fixed: 'fixedNoShrink', +}; + +const FLEX_SIZE_VALUES = [ + FLEX_CHILD_LAYOUT_VALUES.max, + FLEX_CHILD_LAYOUT_VALUES.fixed, +]; + +function isFlexSizeValue( value ) { + return FLEX_SIZE_VALUES.includes( value ); +} + function serializeRule( { selector, declarations } ) { return `${ selector } { ${ Object.entries( declarations ) @@ -60,6 +78,7 @@ export function getChildLayoutStyleRules( { columnSpan, rowSpan, } = effectiveLayout; + const baseSelfStretch = layout.selfStretch; const { columnCount, minimumColumnWidth } = parentLayout; const rules = []; @@ -69,10 +88,30 @@ export function getChildLayoutStyleRules( { hasViewportOverride( 'selfStretch' ) || hasViewportOverride( 'flexSize' ) ) { - if ( selfStretch === 'fixed' && flexSize ) { + if ( + hasViewportOverrides && + ( selfStretch === FLEX_CHILD_LAYOUT_VALUES.fit || + selfStretch === FLEX_CHILD_LAYOUT_VALUES.grow ) && + isFlexSizeValue( baseSelfStretch ) && + layout.flexSize + ) { + declarations[ 'flex-basis' ] = 'unset'; + if ( baseSelfStretch === FLEX_CHILD_LAYOUT_VALUES.fixed ) { + declarations[ 'flex-shrink' ] = 'unset'; + } + } + if ( isFlexSizeValue( selfStretch ) && flexSize ) { declarations[ 'flex-basis' ] = flexSize; + if ( selfStretch === FLEX_CHILD_LAYOUT_VALUES.fixed ) { + declarations[ 'flex-shrink' ] = '0'; + } else if ( + hasViewportOverrides && + baseSelfStretch === FLEX_CHILD_LAYOUT_VALUES.fixed + ) { + declarations[ 'flex-shrink' ] = 'unset'; + } declarations[ 'box-sizing' ] = 'border-box'; - } else if ( selfStretch === 'fill' ) { + } else if ( selfStretch === FLEX_CHILD_LAYOUT_VALUES.grow ) { declarations[ 'flex-grow' ] = '1'; } } diff --git a/packages/block-editor/src/hooks/test/layout-child.js b/packages/block-editor/src/hooks/test/layout-child.js new file mode 100644 index 00000000000000..1bd28f3196840d --- /dev/null +++ b/packages/block-editor/src/hooks/test/layout-child.js @@ -0,0 +1,165 @@ +/** + * Internal dependencies + */ +import { getChildLayoutStyleRules } from '../layout-child'; + +describe( 'layout child', () => { + describe( 'getChildLayoutStyleRules()', () => { + it( 'preserves legacy fixed sizing as shrinkable max width', () => { + expect( + getChildLayoutStyleRules( { + selector: '.wp-container-content-test', + layout: { + selfStretch: 'fixed', + flexSize: '320px', + }, + } ) + ).toEqual( [ + { + selector: '.wp-container-content-test', + declarations: { + 'flex-basis': '320px', + 'box-sizing': 'border-box', + }, + }, + ] ); + } ); + + it( 'adds flex-shrink for fixedNoShrink sizing', () => { + expect( + getChildLayoutStyleRules( { + selector: '.wp-container-content-test', + layout: { + selfStretch: 'fixedNoShrink', + flexSize: '320px', + }, + } ) + ).toEqual( [ + { + selector: '.wp-container-content-test', + declarations: { + 'flex-basis': '320px', + 'flex-shrink': '0', + 'box-sizing': 'border-box', + }, + }, + ] ); + } ); + + it( 'allows viewport overrides to switch fixedNoShrink to max', () => { + expect( + getChildLayoutStyleRules( { + selector: '.wp-container-content-test', + layout: { + selfStretch: 'fixedNoShrink', + flexSize: '320px', + }, + viewportOverrides: { + selfStretch: 'fixed', + }, + } ) + ).toEqual( [ + { + selector: '.wp-container-content-test', + declarations: { + 'flex-basis': '320px', + 'flex-shrink': 'unset', + 'box-sizing': 'border-box', + }, + }, + ] ); + } ); + + it( 'allows viewport overrides to switch fixedNoShrink to fit', () => { + expect( + getChildLayoutStyleRules( { + selector: '.wp-container-content-test', + layout: { + selfStretch: 'fixedNoShrink', + flexSize: '320px', + }, + viewportOverrides: { + selfStretch: 'fit', + }, + } ) + ).toEqual( [ + { + selector: '.wp-container-content-test', + declarations: { + 'flex-basis': 'unset', + 'flex-shrink': 'unset', + }, + }, + ] ); + } ); + + it( 'allows viewport overrides to switch fixed to fit', () => { + expect( + getChildLayoutStyleRules( { + selector: '.wp-container-content-test', + layout: { + selfStretch: 'fixed', + flexSize: '320px', + }, + viewportOverrides: { + selfStretch: 'fit', + }, + } ) + ).toEqual( [ + { + selector: '.wp-container-content-test', + declarations: { + 'flex-basis': 'unset', + }, + }, + ] ); + } ); + + it( 'allows viewport overrides to switch fixedNoShrink to grow', () => { + expect( + getChildLayoutStyleRules( { + selector: '.wp-container-content-test', + layout: { + selfStretch: 'fixedNoShrink', + flexSize: '320px', + }, + viewportOverrides: { + selfStretch: 'fill', + }, + } ) + ).toEqual( [ + { + selector: '.wp-container-content-test', + declarations: { + 'flex-basis': 'unset', + 'flex-shrink': 'unset', + 'flex-grow': '1', + }, + }, + ] ); + } ); + + it( 'allows viewport overrides to switch fixed to grow', () => { + expect( + getChildLayoutStyleRules( { + selector: '.wp-container-content-test', + layout: { + selfStretch: 'fixed', + flexSize: '320px', + }, + viewportOverrides: { + selfStretch: 'fill', + }, + } ) + ).toEqual( [ + { + selector: '.wp-container-content-test', + declarations: { + 'flex-basis': 'unset', + 'flex-grow': '1', + }, + }, + ] ); + } ); + } ); +} ); diff --git a/phpunit/block-supports/layout-test.php b/phpunit/block-supports/layout-test.php index bb2dd790c60ac0..fe96b7e2c55e50 100644 --- a/phpunit/block-supports/layout-test.php +++ b/phpunit/block-supports/layout-test.php @@ -421,6 +421,162 @@ public function data_gutenberg_get_layout_style() { ); } + /** + * Check that gutenberg_get_child_layout_style_rules() renders flex child sizing styles. + * + * @dataProvider data_gutenberg_get_child_layout_style_rules + * + * @covers ::gutenberg_get_child_layout_style_rules + * + * @param array $child_layout Child layout values. + * @param array|null $viewport_overrides Optional child viewport layout overrides. + * @param array $expected_output The expected output. + */ + public function test_gutenberg_get_child_layout_style_rules( $child_layout, $viewport_overrides, $expected_output ) { + $actual_output = gutenberg_get_child_layout_style_rules( + '.wp-container-content-test', + $child_layout, + array(), + $viewport_overrides + ); + + $this->assertSame( $expected_output, $actual_output ); + } + + /** + * Data provider for test_gutenberg_get_child_layout_style_rules(). + * + * @return array + */ + public function data_gutenberg_get_child_layout_style_rules() { + return array( + 'legacy fixed sizing remains shrinkable' => array( + 'child_layout' => array( + 'selfStretch' => 'fixed', + 'flexSize' => '320px', + ), + 'viewport_overrides' => null, + 'expected_output' => array( + array( + 'selector' => '.wp-container-content-test', + 'declarations' => array( + 'flex-basis' => '320px', + 'box-sizing' => 'border-box', + ), + ), + ), + ), + 'fixed sizing can opt out of shrinking' => array( + 'child_layout' => array( + 'selfStretch' => 'fixedNoShrink', + 'flexSize' => '320px', + ), + 'viewport_overrides' => null, + 'expected_output' => array( + array( + 'selector' => '.wp-container-content-test', + 'declarations' => array( + 'flex-basis' => '320px', + 'flex-shrink' => '0', + 'box-sizing' => 'border-box', + ), + ), + ), + ), + 'viewport overrides can switch fixedNoShrink to max' => array( + 'child_layout' => array( + 'selfStretch' => 'fixedNoShrink', + 'flexSize' => '320px', + ), + 'viewport_overrides' => array( + 'selfStretch' => 'fixed', + ), + 'expected_output' => array( + array( + 'selector' => '.wp-container-content-test', + 'declarations' => array( + 'flex-basis' => '320px', + 'flex-shrink' => 'unset', + 'box-sizing' => 'border-box', + ), + ), + ), + ), + 'viewport overrides can switch fixedNoShrink to fit' => array( + 'child_layout' => array( + 'selfStretch' => 'fixedNoShrink', + 'flexSize' => '320px', + ), + 'viewport_overrides' => array( + 'selfStretch' => 'fit', + ), + 'expected_output' => array( + array( + 'selector' => '.wp-container-content-test', + 'declarations' => array( + 'flex-basis' => 'unset', + 'flex-shrink' => 'unset', + ), + ), + ), + ), + 'viewport overrides can switch fixed to fit' => array( + 'child_layout' => array( + 'selfStretch' => 'fixed', + 'flexSize' => '320px', + ), + 'viewport_overrides' => array( + 'selfStretch' => 'fit', + ), + 'expected_output' => array( + array( + 'selector' => '.wp-container-content-test', + 'declarations' => array( + 'flex-basis' => 'unset', + ), + ), + ), + ), + 'viewport overrides can switch fixedNoShrink to grow' => array( + 'child_layout' => array( + 'selfStretch' => 'fixedNoShrink', + 'flexSize' => '320px', + ), + 'viewport_overrides' => array( + 'selfStretch' => 'fill', + ), + 'expected_output' => array( + array( + 'selector' => '.wp-container-content-test', + 'declarations' => array( + 'flex-basis' => 'unset', + 'flex-shrink' => 'unset', + 'flex-grow' => '1', + ), + ), + ), + ), + 'viewport overrides can switch fixed to grow' => array( + 'child_layout' => array( + 'selfStretch' => 'fixed', + 'flexSize' => '320px', + ), + 'viewport_overrides' => array( + 'selfStretch' => 'fill', + ), + 'expected_output' => array( + array( + 'selector' => '.wp-container-content-test', + 'declarations' => array( + 'flex-basis' => 'unset', + 'flex-grow' => '1', + ), + ), + ), + ), + ); + } + /** * Check that gutenberg_render_layout_support_flag() renders the correct classnames on the wrapper. * diff --git a/schemas/json/block.json b/schemas/json/block.json index 94bddfa1d8546b..c23938e249dba2 100644 --- a/schemas/json/block.json +++ b/schemas/json/block.json @@ -495,7 +495,7 @@ "default": true }, "allowSizingOnChildren": { - "description": "For the `flex` layout type only, determines display of sizing controls (Fit/Fill/Fixed) on all child blocks of the flex block.", + "description": "For the `flex` layout type only, determines display of sizing controls (Fit/Grow/Max/Fixed) on all child blocks of the flex block.", "type": "boolean", "default": false },