From 8fb23761f37dde278e0c7766281c5e6ddfc2ca17 Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Mon, 27 Jan 2025 16:32:49 +0100 Subject: [PATCH 1/8] Remove HeaderHint from font size picker. --- .../font-size-picker-select.tsx | 14 +----- .../components/src/font-size-picker/index.tsx | 50 ++----------------- .../components/src/font-size-picker/styles.ts | 5 -- .../src/font-size-picker/test/utils.ts | 38 +------------- .../components/src/font-size-picker/utils.ts | 25 +--------- 5 files changed, 9 insertions(+), 123 deletions(-) diff --git a/packages/components/src/font-size-picker/font-size-picker-select.tsx b/packages/components/src/font-size-picker/font-size-picker-select.tsx index fcc80355ddd19..0d3681f39d5a9 100644 --- a/packages/components/src/font-size-picker/font-size-picker-select.tsx +++ b/packages/components/src/font-size-picker/font-size-picker-select.tsx @@ -7,12 +7,11 @@ import { __, sprintf } from '@wordpress/i18n'; * Internal dependencies */ import CustomSelectControl from '../custom-select-control'; -import { parseQuantityAndUnitFromRawValue } from '../unit-control'; import type { FontSizePickerSelectProps, FontSizePickerSelectOption, } from './types'; -import { getCommonSizeUnit, isSimpleCssValue } from './utils'; +import { isSimpleCssValue } from './utils'; const DEFAULT_OPTION: FontSizePickerSelectOption = { key: 'default', @@ -23,20 +22,11 @@ const DEFAULT_OPTION: FontSizePickerSelectOption = { const FontSizePickerSelect = ( props: FontSizePickerSelectProps ) => { const { __next40pxDefaultSize, fontSizes, value, size, onChange } = props; - const areAllSizesSameUnit = !! getCommonSizeUnit( fontSizes ); - const options: FontSizePickerSelectOption[] = [ DEFAULT_OPTION, ...fontSizes.map( ( fontSize ) => { let hint; - if ( areAllSizesSameUnit ) { - const [ quantity ] = parseQuantityAndUnitFromRawValue( - fontSize.size - ); - if ( quantity !== undefined ) { - hint = String( quantity ); - } - } else if ( isSimpleCssValue( fontSize.size ) ) { + if ( isSimpleCssValue( fontSize.size ) ) { hint = String( fontSize.size ); } return { diff --git a/packages/components/src/font-size-picker/index.tsx b/packages/components/src/font-size-picker/index.tsx index a47812640f1a2..6824ae3b96ded 100644 --- a/packages/components/src/font-size-picker/index.tsx +++ b/packages/components/src/font-size-picker/index.tsx @@ -8,7 +8,7 @@ import type { ForwardedRef } from 'react'; */ import { __ } from '@wordpress/i18n'; import { settings } from '@wordpress/icons'; -import { useState, useMemo, forwardRef } from '@wordpress/element'; +import { useState, forwardRef } from '@wordpress/element'; /** * Internal dependencies @@ -22,19 +22,11 @@ import { useCustomUnits, } from '../unit-control'; import { VisuallyHidden } from '../visually-hidden'; -import { getCommonSizeUnit } from './utils'; import type { FontSizePickerProps } from './types'; -import { - Container, - Header, - HeaderHint, - HeaderLabel, - HeaderToggle, -} from './styles'; +import { Container, Header, HeaderLabel, HeaderToggle } from './styles'; import { Spacer } from '../spacer'; import FontSizePickerSelect from './font-size-picker-select'; import FontSizePickerToggleGroup from './font-size-picker-toggle-group'; -import { T_SHIRT_NAMES } from './constants'; import { maybeWarnDeprecated36pxSize } from '../utils/deprecated-36px-size'; const DEFAULT_UNITS = [ 'px', 'em', 'rem', 'vw', 'vh' ]; @@ -83,29 +75,6 @@ const UnforwardedFontSizePicker = ( : ( 'togglegroup' as const ); } - const headerHint = useMemo( () => { - switch ( currentPickerType ) { - case 'custom': - return __( 'Custom' ); - case 'togglegroup': - if ( selectedFontSize ) { - return ( - selectedFontSize.name || - T_SHIRT_NAMES[ fontSizes.indexOf( selectedFontSize ) ] - ); - } - break; - case 'select': - const commonUnit = getCommonSizeUnit( fontSizes ); - if ( commonUnit ) { - return `(${ commonUnit })`; - } - break; - } - - return ''; - }, [ currentPickerType, selectedFontSize, fontSizes ] ); - if ( fontSizes.length === 0 && disableCustomFontSizes ) { return null; } @@ -135,16 +104,7 @@ const UnforwardedFontSizePicker = ( { __( 'Font size' ) }
- - { __( 'Size' ) } - { headerHint && ( - - { headerHint } - - ) } - + { __( 'Font size' ) } { ! disableCustomFontSizes && ( { test.each( [ @@ -31,39 +31,3 @@ describe( 'isSimpleCssValue', () => { expect( isSimpleCssValue( cssValue ) ).toBe( result ); } ); } ); - -describe( 'getCommonSizeUnit', () => { - it( 'returns null when fontSizes is empty', () => { - expect( getCommonSizeUnit( [] ) ).toBe( null ); - } ); - - it( 'returns px when all sizes are px', () => { - expect( - getCommonSizeUnit( [ - { slug: 'small', size: '10px' }, - { slug: 'medium', size: '20px' }, - { slug: 'large', size: '30px' }, - ] ) - ).toBe( 'px' ); - } ); - - it( 'returns em when all sizes are em', () => { - expect( - getCommonSizeUnit( [ - { slug: 'small', size: '1em' }, - { slug: 'medium', size: '2em' }, - { slug: 'large', size: '3em' }, - ] ) - ).toBe( 'em' ); - } ); - - it( 'returns null when sizes are heterogeneous', () => { - expect( - getCommonSizeUnit( [ - { slug: 'small', size: '10px' }, - { slug: 'medium', size: '2em' }, - { slug: 'large', size: '3rem' }, - ] ) - ).toBe( null ); - } ); -} ); diff --git a/packages/components/src/font-size-picker/utils.ts b/packages/components/src/font-size-picker/utils.ts index 64816f12255ba..4d256496e6300 100644 --- a/packages/components/src/font-size-picker/utils.ts +++ b/packages/components/src/font-size-picker/utils.ts @@ -1,8 +1,7 @@ /** * Internal dependencies */ -import type { FontSizePickerProps, FontSize } from './types'; -import { parseQuantityAndUnitFromRawValue } from '../unit-control'; +import type { FontSizePickerProps } from './types'; /** * Some themes use css vars for their font sizes, so until we @@ -18,25 +17,3 @@ export function isSimpleCssValue( /^[\d\.]+(px|em|rem|vw|vh|%|svw|lvw|dvw|svh|lvh|dvh|vi|svi|lvi|dvi|vb|svb|lvb|dvb|vmin|svmin|lvmin|dvmin|vmax|svmax|lvmax|dvmax)?$/i; return sizeRegex.test( String( value ) ); } - -/** - * If all of the given font sizes have the same unit (e.g. 'px'), return that - * unit. Otherwise return null. - * - * @param fontSizes List of font sizes. - * @return The common unit, or null. - */ -export function getCommonSizeUnit( fontSizes: FontSize[] ) { - const [ firstFontSize, ...otherFontSizes ] = fontSizes; - if ( ! firstFontSize ) { - return null; - } - const [ , firstUnit ] = parseQuantityAndUnitFromRawValue( - firstFontSize.size - ); - const areAllSizesSameUnit = otherFontSizes.every( ( fontSize ) => { - const [ , unit ] = parseQuantityAndUnitFromRawValue( fontSize.size ); - return unit === firstUnit; - } ); - return areAllSizesSameUnit ? firstUnit : null; -} From ea9c3f1c6c77322849c5f0811f69dbc4ab284eeb Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Tue, 28 Jan 2025 14:56:02 +0100 Subject: [PATCH 2/8] Adjust tests. --- packages/components/src/font-size-picker/index.tsx | 2 +- test/e2e/specs/site-editor/style-variations.spec.js | 12 +++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/components/src/font-size-picker/index.tsx b/packages/components/src/font-size-picker/index.tsx index 6824ae3b96ded..9cc577e445121 100644 --- a/packages/components/src/font-size-picker/index.tsx +++ b/packages/components/src/font-size-picker/index.tsx @@ -173,7 +173,7 @@ const UnforwardedFontSizePicker = ( { await page.click( 'role=button[name="Text"i]' ); await expect( - page.locator( 'css=.components-font-size-picker__header__hint' ) - ).toHaveText( 'Medium' ); + page.locator( 'role=radio[name="Medium"i]' ) + ).toHaveAttribute( 'aria-checked', 'true' ); } ); test( 'should apply custom colors and font sizes in a variation', async ( { @@ -132,14 +132,8 @@ test.describe( 'Global styles variations', () => { await page.click( 'role=button[name="Typography"i]' ); await page.click( 'role=button[name="Text"i]' ); - // TODO: to avoid use classnames to locate these elements, - // we could provide accessible attributes to the source code in packages/components/src/font-size-picker/index.js. await expect( - page.locator( 'css=.components-font-size-picker__header__hint' ) - ).toHaveText( 'Custom' ); - - await expect( - page.locator( 'role=spinbutton[name="Custom"i]' ) + page.locator( 'role=spinbutton[name="Custom font size"i]' ) ).toHaveValue( '15' ); } ); From 184f1f4e9c93fd32a861e07d26c0a0d8198eb631 Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Tue, 28 Jan 2025 15:50:24 +0100 Subject: [PATCH 3/8] Simplify fieldset labeling to avoid repetition. --- .../components/src/font-size-picker/index.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/components/src/font-size-picker/index.tsx b/packages/components/src/font-size-picker/index.tsx index 9cc577e445121..aa81e4ef668d3 100644 --- a/packages/components/src/font-size-picker/index.tsx +++ b/packages/components/src/font-size-picker/index.tsx @@ -9,6 +9,7 @@ import type { ForwardedRef } from 'react'; import { __ } from '@wordpress/i18n'; import { settings } from '@wordpress/icons'; import { useState, forwardRef } from '@wordpress/element'; +import { useInstanceId } from '@wordpress/compose'; /** * Internal dependencies @@ -21,7 +22,6 @@ import { parseQuantityAndUnitFromRawValue, useCustomUnits, } from '../unit-control'; -import { VisuallyHidden } from '../visually-hidden'; import type { FontSizePickerProps } from './types'; import { Container, Header, HeaderLabel, HeaderToggle } from './styles'; import { Spacer } from '../spacer'; @@ -50,6 +50,11 @@ const UnforwardedFontSizePicker = ( withReset = true, } = props; + const labelId = useInstanceId( + UnforwardedFontSizePicker, + 'font-size-picker-label' + ); + const units = useCustomUnits( { availableUnits: unitsProp, } ); @@ -100,11 +105,17 @@ const UnforwardedFontSizePicker = ( } ); return ( - - { __( 'Font size' ) } +
- { __( 'Font size' ) } + + { __( 'Font size' ) } + { ! disableCustomFontSizes && ( Date: Mon, 3 Feb 2025 13:40:46 +0100 Subject: [PATCH 4/8] Update custom font size range control label. --- packages/components/src/font-size-picker/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/font-size-picker/index.tsx b/packages/components/src/font-size-picker/index.tsx index aa81e4ef668d3..7688da5553336 100644 --- a/packages/components/src/font-size-picker/index.tsx +++ b/packages/components/src/font-size-picker/index.tsx @@ -216,7 +216,7 @@ const UnforwardedFontSizePicker = ( } __shouldNotWarnDeprecated36pxSize className="components-font-size-picker__custom-input" - label={ __( 'Font size' ) } + label={ __( 'Custom font size' ) } hideLabelFromVision value={ valueQuantity } initialPosition={ fallbackFontSize } From f48a0a9b9697824c694e8dc33d4edb3f83f2601a Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Mon, 3 Feb 2025 14:29:11 +0100 Subject: [PATCH 5/8] Avoid mismatch between visual label and actual labels. --- packages/components/src/font-size-picker/index.tsx | 4 ++-- test/e2e/specs/site-editor/style-variations.spec.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/src/font-size-picker/index.tsx b/packages/components/src/font-size-picker/index.tsx index 7688da5553336..6bc2bd206ebe0 100644 --- a/packages/components/src/font-size-picker/index.tsx +++ b/packages/components/src/font-size-picker/index.tsx @@ -184,7 +184,7 @@ const UnforwardedFontSizePicker = ( { await page.click( 'role=button[name="Text"i]' ); await expect( - page.locator( 'role=spinbutton[name="Custom font size"i]' ) + page.locator( 'role=spinbutton[name="Font size"i]' ) ).toHaveValue( '15' ); } ); From da1a04262a54a3cd77d68cf8fa6826be8f209013 Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Tue, 4 Feb 2025 15:18:08 +0100 Subject: [PATCH 6/8] Adjust unit tests. --- .../src/font-size-picker/test/index.tsx | 116 ++++++------------ 1 file changed, 36 insertions(+), 80 deletions(-) diff --git a/packages/components/src/font-size-picker/test/index.tsx b/packages/components/src/font-size-picker/test/index.tsx index b3612029df362..53bfc79979720 100644 --- a/packages/components/src/font-size-picker/test/index.tsx +++ b/packages/components/src/font-size-picker/test/index.tsx @@ -52,7 +52,9 @@ describe( 'FontSizePicker', () => { await render( ); - const input = screen.getByLabelText( 'Custom' ); + const input = screen.getByRole( 'spinbutton', { + name: 'Font size', + } ); await user.clear( input ); await user.type( input, '80' ); expect( onChange ).toHaveBeenCalledTimes( 3 ); // Once for the clear, then once per keystroke. @@ -79,7 +81,9 @@ describe( 'FontSizePicker', () => { await user.click( screen.getByRole( 'button', { name: 'Set custom size' } ) ); - const input = screen.getByLabelText( 'Custom' ); + const input = screen.getByRole( 'spinbutton', { + name: 'Font size', + } ); await user.type( input, '80' ); expect( onChange ).toHaveBeenCalledTimes( 2 ); // Once per keystroke. expect( onChange ).toHaveBeenCalledWith( expectedValue ); @@ -129,28 +133,14 @@ describe( 'FontSizePicker', () => { const options = screen.getAllByRole( 'option' ); expect( options ).toHaveLength( 7 ); expect( options[ 0 ] ).toHaveAccessibleName( 'Default' ); - expect( options[ 1 ] ).toHaveAccessibleName( 'Tiny 8' ); - expect( options[ 2 ] ).toHaveAccessibleName( 'Small 12' ); - expect( options[ 3 ] ).toHaveAccessibleName( 'Medium 16' ); - expect( options[ 4 ] ).toHaveAccessibleName( 'Large 20' ); - expect( options[ 5 ] ).toHaveAccessibleName( 'Extra Large 30' ); - expect( options[ 6 ] ).toHaveAccessibleName( 'xx-large 40' ); + expect( options[ 1 ] ).toHaveAccessibleName( 'Tiny 8px' ); + expect( options[ 2 ] ).toHaveAccessibleName( 'Small 12px' ); + expect( options[ 3 ] ).toHaveAccessibleName( 'Medium 16px' ); + expect( options[ 4 ] ).toHaveAccessibleName( 'Large 20px' ); + expect( options[ 5 ] ).toHaveAccessibleName( 'Extra Large 30px' ); + expect( options[ 6 ] ).toHaveAccessibleName( 'xx-large 40px' ); } ); - test.each( [ - { value: undefined, expectedLabel: 'Size (px)' }, - { value: '8px', expectedLabel: 'Size (px)' }, - { value: '3px', expectedLabel: 'Size Custom' }, - ] )( - 'displays $expectedLabel as label when value is $value', - async ( { value, expectedLabel } ) => { - await render( - - ); - expect( screen.getByLabelText( expectedLabel ) ).toBeVisible(); - } - ); - test.each( [ { option: 'Default', @@ -158,7 +148,7 @@ describe( 'FontSizePicker', () => { expectedArguments: [ undefined ], }, { - option: 'Tiny 8', + option: 'Tiny 8px', value: undefined, expectedArguments: [ '8px', fontSizes[ 0 ] ], }, @@ -255,23 +245,6 @@ describe( 'FontSizePicker', () => { } ); - test.each( [ - { value: undefined, expectedLabel: 'Size' }, - { value: '8px', expectedLabel: 'Size' }, - { value: '1em', expectedLabel: 'Size' }, - { value: '2rem', expectedLabel: 'Size' }, - { value: 'clamp(1.75rem, 3vw, 2.25rem)', expectedLabel: 'Size' }, - { value: '3px', expectedLabel: 'Size Custom' }, - ] )( - 'displays $expectedLabel as label when value is $value', - async ( { value, expectedLabel } ) => { - await render( - - ); - expect( screen.getByLabelText( expectedLabel ) ).toBeVisible(); - } - ); - test.each( [ { option: 'Default', @@ -372,20 +345,6 @@ describe( 'FontSizePicker', () => { expect( options[ 4 ] ).toHaveAccessibleName( 'Gigantosaurus' ); } ); - test.each( [ - { value: undefined, expectedLabel: 'Size' }, - { value: '12px', expectedLabel: 'Size Small' }, - { value: '40px', expectedLabel: 'Size Gigantosaurus' }, - ] )( - 'displays $expectedLabel as label when value is $value', - async ( { value, expectedLabel } ) => { - await render( - - ); - expect( screen.getByLabelText( expectedLabel ) ).toBeVisible(); - } - ); - it( 'calls onChange when a font size is selected', async () => { const user = userEvent.setup(); const onChange = jest.fn(); @@ -439,25 +398,6 @@ describe( 'FontSizePicker', () => { expect( options[ 3 ] ).toHaveAccessibleName( 'Extra Large' ); } ); - test.each( [ - { value: undefined, expectedLabel: 'Size' }, - { value: '12px', expectedLabel: 'Size Small' }, - { value: '1em', expectedLabel: 'Size Medium' }, - { value: '2rem', expectedLabel: 'Size Large' }, - { - value: 'clamp(1.75rem, 3vw, 2.25rem)', - expectedLabel: 'Size Extra Large', - }, - ] )( - 'displays $expectedLabel as label when value is $value', - async ( { value, expectedLabel } ) => { - await render( - - ); - expect( screen.getByLabelText( expectedLabel ) ).toBeVisible(); - } - ); - test.each( [ { radio: 'Small', expectedArguments: [ '12px', fontSizes[ 0 ] ] }, { radio: 'Medium', expectedArguments: [ '1em', fontSizes[ 1 ] ] }, @@ -524,14 +464,18 @@ describe( 'FontSizePicker', () => { await render( ); - expect( screen.getByLabelText( 'Custom' ) ).toBeVisible(); + expect( + screen.getByRole( 'spinbutton', { name: 'Font size' } ) + ).toBeVisible(); } ); it( 'hides custom input when disableCustomFontSizes is set to `true` with a custom font size', async () => { const { rerender } = await render( ); - expect( screen.getByLabelText( 'Custom' ) ).toBeVisible(); + expect( + screen.getByRole( 'spinbutton', { name: 'Font size' } ) + ).toBeVisible(); rerender( { const { rerender } = await render( ); - expect( screen.getByLabelText( 'Custom' ) ).toBeVisible(); + expect( + screen.getByRole( 'spinbutton', { name: 'Font size' } ) + ).toBeVisible(); rerender( { value={ fontSizes[ 0 ].size } /> ); - expect( screen.getByLabelText( 'Custom' ) ).toBeVisible(); + expect( + screen.getByRole( 'spinbutton', { name: 'Font size' } ) + ).toBeVisible(); } ); it( 'allows custom values by default', async () => { @@ -569,7 +517,10 @@ describe( 'FontSizePicker', () => { await user.click( screen.getByRole( 'button', { name: 'Set custom size' } ) ); - await user.type( screen.getByLabelText( 'Custom' ), '80' ); + await user.type( + screen.getByRole( 'spinbutton', { name: 'Font size' } ), + '80' + ); expect( onChange ).toHaveBeenCalledTimes( 2 ); // Once per keystroke. expect( onChange ).toHaveBeenCalledWith( '80px' ); } ); @@ -585,7 +536,10 @@ describe( 'FontSizePicker', () => { screen.getByRole( 'button', { name: 'Set custom size' } ) ); - await user.type( screen.getByLabelText( 'Custom' ), '80' ); + await user.type( + screen.getByRole( 'spinbutton', { name: 'Font size' } ), + '80' + ); await user.click( screen.getByRole( 'button', { name: 'Use size preset' } ) @@ -632,7 +586,9 @@ describe( 'FontSizePicker', () => { await user.click( screen.getByRole( 'button', { name: 'Set custom size' } ) ); - const sliderInput = screen.getByLabelText( 'Custom Size' ); + const sliderInput = screen.getByRole( 'slider', { + name: 'Font size', + } ); fireEvent.change( sliderInput, { target: { value: 80 }, } ); From 0cc3cd962f4524c2ae2fc099a3288da9e5a3f5cd Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Tue, 4 Feb 2025 15:23:24 +0100 Subject: [PATCH 7/8] Adjust e2e test. --- test/e2e/specs/editor/various/font-size-picker.spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/e2e/specs/editor/various/font-size-picker.spec.js b/test/e2e/specs/editor/various/font-size-picker.spec.js index 597b458d682e8..d3957e7cb65d8 100644 --- a/test/e2e/specs/editor/various/font-size-picker.spec.js +++ b/test/e2e/specs/editor/various/font-size-picker.spec.js @@ -31,7 +31,7 @@ test.describe( 'Font Size Picker', () => { await page.click( 'role=region[name="Editor settings"i] >> role=button[name="Set custom size"i]' ); - await page.click( 'role=spinbutton[name="Custom"i]' ); + await page.click( 'role=spinbutton[name="Font size"i]' ); await page.keyboard.type( '23' ); @@ -54,7 +54,7 @@ test.describe( 'Font Size Picker', () => { await page.click( 'role=region[name="Editor settings"i] >> role=button[name="Set custom size"i]' ); - await page.click( 'role=spinbutton[name="Custom"i]' ); + await page.click( 'role=spinbutton[name="Font size"i]' ); await page.keyboard.type( '23' ); await expect.poll( editor.getEditedPostContent ) @@ -214,7 +214,7 @@ test.describe( 'Font Size Picker', () => { await page.click( 'role=region[name="Editor settings"i] >> role=button[name="Set custom size"i]' ); - await page.click( 'role=spinbutton[name="Custom"i]' ); + await page.click( 'role=spinbutton[name="Font size"i]' ); await pageUtils.pressKeys( 'primary+A' ); await page.keyboard.press( 'Backspace' ); @@ -299,7 +299,7 @@ test.describe( 'Font Size Picker', () => { await page.click( 'role=region[name="Editor settings"i] >> role=button[name="Set custom size"i]' ); - await page.click( 'role=spinbutton[name="Custom"i]' ); + await page.click( 'role=spinbutton[name="Font size"i]' ); await pageUtils.pressKeys( 'primary+A' ); await page.keyboard.press( 'Backspace' ); From d1618214f1fcfd34a7d0850cfd07ae5237e67e58 Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Tue, 4 Feb 2025 15:28:01 +0100 Subject: [PATCH 8/8] Add changelog entry. --- packages/components/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 6917427ef1571..2cff4f26c20b9 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -11,6 +11,7 @@ ### Bug Fixes +- `FontSizePicker`: Remove non translatable additional info from font size picker visual label and improve labeling. ([#69011](https://github.com/WordPress/gutenberg/pull/69011)). - `Notice`: Fix text contrast for dark mode ([#69226](https://github.com/WordPress/gutenberg/pull/69226)). ## 29.4.0 (2025-02-12)