From d3a95861935d0a8ec92d9f0685343028769a447d Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Tue, 18 Feb 2025 16:39:10 +0100 Subject: [PATCH 01/28] Cover: Fix placeholder color options keyboard accessibility (#68662) * Render the color options as buttons. * Make circular option picker get a group role and optional label when rendered as buttons. * Adjust tests. * Adjust more tests. * Abstract logic to compute the common props. * Update snapshot. * Fix OptionGroup ARIA role. * Rename. * Adjust tests. * Restore group ARIA role on OptionGroup. * Change Background color label to Overlay color. Co-authored-by: afercia Co-authored-by: ciampo Co-authored-by: Mamaduka Co-authored-by: carolinan Co-authored-by: Mayank-Tripathi32 --- .../test/__snapshots__/control.js.snap | 2 +- .../block-library/src/cover/edit/index.js | 2 + packages/block-library/src/cover/test/edit.js | 12 +++--- .../src/border-box-control/test/index.tsx | 2 +- .../src/border-control/test/index.js | 2 +- .../src/circular-option-picker/README.md | 13 ++++++ .../circular-option-picker.tsx | 2 +- .../src/circular-option-picker/index.tsx | 1 + .../stories/index.story.tsx | 2 +- .../src/circular-option-picker/test/index.tsx | 1 + .../src/circular-option-picker/types.ts | 21 +++++----- .../src/circular-option-picker/utils.tsx | 27 +++++++++++++ .../components/src/color-palette/index.tsx | 40 +++++-------------- .../src/color-palette/test/index.tsx | 2 +- .../src/duotone-picker/duotone-picker.tsx | 38 +++++------------- .../components/src/gradient-picker/index.tsx | 38 +++++------------- test/e2e/specs/editor/blocks/buttons.spec.js | 4 +- test/e2e/specs/editor/blocks/cover.spec.js | 10 ++--- test/e2e/specs/editor/blocks/heading.spec.js | 2 +- .../specs/editor/various/list-view.spec.js | 6 +-- 20 files changed, 109 insertions(+), 118 deletions(-) create mode 100644 packages/components/src/circular-option-picker/utils.tsx diff --git a/packages/block-editor/src/components/color-palette/test/__snapshots__/control.js.snap b/packages/block-editor/src/components/color-palette/test/__snapshots__/control.js.snap index 3d082a14a92bff..eb665ced62079f 100644 --- a/packages/block-editor/src/components/color-palette/test/__snapshots__/control.js.snap +++ b/packages/block-editor/src/components/color-palette/test/__snapshots__/control.js.snap @@ -203,7 +203,7 @@ exports[`ColorPaletteControl matches the snapshot 1`] = ` class="components-circular-option-picker" >
diff --git a/packages/block-library/src/cover/edit/index.js b/packages/block-library/src/cover/edit/index.js index 1eafe99e283eb4..b09093e312211d 100644 --- a/packages/block-library/src/cover/edit/index.js +++ b/packages/block-library/src/cover/edit/index.js @@ -514,6 +514,8 @@ function CoverEdit( { value={ overlayColor.color } onChange={ onSetOverlayColor } clearable={ false } + asButtons + aria-label={ __( 'Overlay color' ) } />
diff --git a/packages/block-library/src/cover/test/edit.js b/packages/block-library/src/cover/test/edit.js index 0a18d2cf3f9f8e..16695f53f67466 100644 --- a/packages/block-library/src/cover/test/edit.js +++ b/packages/block-library/src/cover/test/edit.js @@ -47,7 +47,7 @@ async function setup( attributes, useCoreBlocks, customSettings ) { async function createAndSelectBlock() { await userEvent.click( - screen.getByRole( 'option', { + screen.getByRole( 'button', { name: 'Black', } ) ); @@ -72,7 +72,7 @@ describe( 'Cover block', () => { test( 'can set overlay color using color picker on block placeholder', async () => { const { container } = await setup(); - const colorPicker = screen.getByRole( 'option', { + const colorPicker = screen.getByRole( 'button', { name: 'Black', } ); await userEvent.click( colorPicker ); @@ -96,7 +96,7 @@ describe( 'Cover block', () => { await setup(); await userEvent.click( - screen.getByRole( 'option', { + screen.getByRole( 'button', { name: 'Black', } ) ); @@ -389,7 +389,7 @@ describe( 'Cover block', () => { describe( 'isDark settings', () => { test( 'should toggle is-light class if background changed from light to dark', async () => { await setup(); - const colorPicker = screen.getByRole( 'option', { + const colorPicker = screen.getByRole( 'button', { name: 'White', } ); await userEvent.click( colorPicker ); @@ -413,7 +413,7 @@ describe( 'Cover block', () => { } ); test( 'should remove is-light class if overlay color is removed', async () => { await setup(); - const colorPicker = screen.getByRole( 'option', { + const colorPicker = screen.getByRole( 'button', { name: 'White', } ); await userEvent.click( colorPicker ); @@ -426,7 +426,7 @@ describe( 'Cover block', () => { } ) ); await userEvent.click( screen.getByText( 'Overlay' ) ); - // The default color is black, so clicking the black color option will remove the background color, + // The default color is black, so clicking the black color button will remove the background color, // which should remove the isDark setting and assign the is-light class. const popupColorPicker = screen.getByRole( 'option', { name: 'White', diff --git a/packages/components/src/border-box-control/test/index.tsx b/packages/components/src/border-box-control/test/index.tsx index fb536656453f4d..74501f7fccc7ac 100644 --- a/packages/components/src/border-box-control/test/index.tsx +++ b/packages/components/src/border-box-control/test/index.tsx @@ -202,7 +202,7 @@ describe( 'BorderBoxControl', () => { await waitFor( () => expect( screen.getByRole( 'button', { - name: 'Custom color picker.', + name: 'Custom color picker', } ) ).toBeVisible() ); diff --git a/packages/components/src/border-control/test/index.js b/packages/components/src/border-control/test/index.js index ff9007be28f1a2..c3e3987ed13517 100644 --- a/packages/components/src/border-control/test/index.js +++ b/packages/components/src/border-control/test/index.js @@ -138,7 +138,7 @@ describe( 'BorderControl', () => { const customColorPicker = getButton( /Custom color picker/ ); const circularOptionPicker = screen.getByRole( 'listbox', { - name: 'Custom color picker.', + name: 'Custom color picker', } ); const colorSwatchButtons = within( circularOptionPicker ).getAllByRole( 'option' ); diff --git a/packages/components/src/circular-option-picker/README.md b/packages/components/src/circular-option-picker/README.md index b6db6f06daf456..8a4d5ac3cf5ca3 100644 --- a/packages/components/src/circular-option-picker/README.md +++ b/packages/components/src/circular-option-picker/README.md @@ -93,6 +93,19 @@ Prevents keyboard interaction from wrapping around. Only used when `asButtons` i - Required: No - Default: `true` +### `aria-labelledby`: `string` + +The ID reference list of one or more elements that label the wrapper element. + +- Required: No + +### `aria-label`: `string` + +The label for the wrapper element. Not used if an 'aria-labelledby' is provided. + +- Required: No +- Default: `Custom color picker` + ## Subcomponents ### `CircularOptionPicker.ButtonAction` diff --git a/packages/components/src/circular-option-picker/circular-option-picker.tsx b/packages/components/src/circular-option-picker/circular-option-picker.tsx index 8b6be8cd2215f0..c4309ecf4dda3d 100644 --- a/packages/components/src/circular-option-picker/circular-option-picker.tsx +++ b/packages/components/src/circular-option-picker/circular-option-picker.tsx @@ -132,7 +132,7 @@ function ButtonsCircularOptionPicker( ); return ( -
+
{ options } { children } diff --git a/packages/components/src/circular-option-picker/index.tsx b/packages/components/src/circular-option-picker/index.tsx index ef975c21ee6545..ef379994b476f5 100644 --- a/packages/components/src/circular-option-picker/index.tsx +++ b/packages/components/src/circular-option-picker/index.tsx @@ -9,5 +9,6 @@ export { ButtonAction, DropdownLinkAction, } from './circular-option-picker-actions'; +export { getComputeCircularOptionPickerCommonProps } from './utils'; export default CircularOptionPicker; diff --git a/packages/components/src/circular-option-picker/stories/index.story.tsx b/packages/components/src/circular-option-picker/stories/index.story.tsx index 9d45c9bb92f7d0..6b564929fd8eb9 100644 --- a/packages/components/src/circular-option-picker/stories/index.story.tsx +++ b/packages/components/src/circular-option-picker/stories/index.story.tsx @@ -131,7 +131,7 @@ WithLoopingDisabled.parameters = { docs: { source: { code: `} />`, diff --git a/packages/components/src/circular-option-picker/test/index.tsx b/packages/components/src/circular-option-picker/test/index.tsx index a6e9f2c45a05ce..7d58ed3920f9bd 100644 --- a/packages/components/src/circular-option-picker/test/index.tsx +++ b/packages/components/src/circular-option-picker/test/index.tsx @@ -57,6 +57,7 @@ describe( 'CircularOptionPicker', () => { expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument(); expect( screen.queryByRole( 'option' ) ).not.toBeInTheDocument(); + expect( screen.getByRole( 'group' ) ).toBeInTheDocument(); expect( screen.getByRole( 'button' ) ).toBeInTheDocument(); } ); } ); diff --git a/packages/components/src/circular-option-picker/types.ts b/packages/components/src/circular-option-picker/types.ts index 411782aed575b1..54fae3ab2e798a 100644 --- a/packages/components/src/circular-option-picker/types.ts +++ b/packages/components/src/circular-option-picker/types.ts @@ -40,6 +40,16 @@ type CommonCircularOptionPickerProps = { * The child elements. */ children?: ReactNode; + /** + * The ID reference list of one or more elements that label the wrapper + * element. + */ + 'aria-labelledby'?: string; + /** + * The label for the wrapper element. Defaults to 'Custom color picker'. Not + * used if an 'aria-labelledby' is provided. + */ + 'aria-label'?: string; }; type WithBaseId = { @@ -59,16 +69,7 @@ type FullListboxCircularOptionPickerProps = CommonCircularOptionPickerProps & { * @default true */ loop?: boolean; -} & ( - | { - 'aria-label': string; - 'aria-labelledby'?: never; - } - | { - 'aria-label'?: never; - 'aria-labelledby': string; - } - ); +}; export type ListboxCircularOptionPickerProps = WithBaseId & Omit< FullListboxCircularOptionPickerProps, 'asButtons' >; diff --git a/packages/components/src/circular-option-picker/utils.tsx b/packages/components/src/circular-option-picker/utils.tsx new file mode 100644 index 00000000000000..fcb3b2bcac3695 --- /dev/null +++ b/packages/components/src/circular-option-picker/utils.tsx @@ -0,0 +1,27 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Computes the common props for the CircularOptionPicker. + */ +export function getComputeCircularOptionPickerCommonProps( + asButtons?: boolean, + loop?: boolean, + ariaLabel?: string, + ariaLabelledby?: string +) { + const metaProps = asButtons + ? { asButtons: true } + : { asButtons: false, loop }; + + const labelProps = { + 'aria-labelledby': ariaLabelledby, + 'aria-label': ariaLabelledby + ? undefined + : ariaLabel || __( 'Custom color picker' ), + }; + + return { metaProps, labelProps }; +} diff --git a/packages/components/src/color-palette/index.tsx b/packages/components/src/color-palette/index.tsx index de4e4f4206fe3a..eb981e8b9acc70 100644 --- a/packages/components/src/color-palette/index.tsx +++ b/packages/components/src/color-palette/index.tsx @@ -19,7 +19,9 @@ import { useCallback, useMemo, useState, forwardRef } from '@wordpress/element'; */ import Dropdown from '../dropdown'; import { ColorPicker } from '../color-picker'; -import CircularOptionPicker from '../circular-option-picker'; +import CircularOptionPicker, { + getComputeCircularOptionPickerCommonProps, +} from '../circular-option-picker'; import { VStack } from '../v-stack'; import { Truncate } from '../truncate'; import { ColorHeading } from './styles'; @@ -233,7 +235,7 @@ function UnforwardedColorPalette( buttonLabelName, displayValue ) - : __( 'Custom color picker.' ); + : __( 'Custom color picker' ); const paletteCommonProps = { clearColor, @@ -251,33 +253,12 @@ function UnforwardedColorPalette( ); - let metaProps: - | { asButtons: false; loop?: boolean; 'aria-label': string } - | { asButtons: false; loop?: boolean; 'aria-labelledby': string } - | { asButtons: true }; - - if ( asButtons ) { - metaProps = { asButtons: true }; - } else { - const _metaProps: { asButtons: false; loop?: boolean } = { - asButtons: false, - loop, - }; - - if ( ariaLabel ) { - metaProps = { ..._metaProps, 'aria-label': ariaLabel }; - } else if ( ariaLabelledby ) { - metaProps = { - ..._metaProps, - 'aria-labelledby': ariaLabelledby, - }; - } else { - metaProps = { - ..._metaProps, - 'aria-label': __( 'Custom color picker.' ), - }; - } - } + const { metaProps, labelProps } = getComputeCircularOptionPickerCommonProps( + asButtons, + loop, + ariaLabel, + ariaLabelledby + ); return ( @@ -335,6 +316,7 @@ function UnforwardedColorPalette( { ( colors.length > 0 || actions ) && ( { expect( screen.queryByText( colorCode ) ).not.toBeInTheDocument(); expect( screen.getByRole( 'button', { - name: /^Custom color picker.$/, + name: /^Custom color picker$/, } ) ).toBeInTheDocument(); } ); diff --git a/packages/components/src/duotone-picker/duotone-picker.tsx b/packages/components/src/duotone-picker/duotone-picker.tsx index 8764b401c38296..a21d12b73a65c4 100644 --- a/packages/components/src/duotone-picker/duotone-picker.tsx +++ b/packages/components/src/duotone-picker/duotone-picker.tsx @@ -13,7 +13,9 @@ import { __, sprintf } from '@wordpress/i18n'; * Internal dependencies */ import ColorListPicker from './color-list-picker'; -import CircularOptionPicker from '../circular-option-picker'; +import CircularOptionPicker, { + getComputeCircularOptionPickerCommonProps, +} from '../circular-option-picker'; import { VStack } from '../v-stack'; import CustomDuotoneBar from './custom-duotone-bar'; @@ -127,33 +129,12 @@ function DuotonePicker( { ); } ); - let metaProps: - | { asButtons: false; loop?: boolean; 'aria-label': string } - | { asButtons: false; loop?: boolean; 'aria-labelledby': string } - | { asButtons: true }; - - if ( asButtons ) { - metaProps = { asButtons: true }; - } else { - const _metaProps: { asButtons: false; loop?: boolean } = { - asButtons: false, - loop, - }; - - if ( ariaLabel ) { - metaProps = { ..._metaProps, 'aria-label': ariaLabel }; - } else if ( ariaLabelledby ) { - metaProps = { - ..._metaProps, - 'aria-labelledby': ariaLabelledby, - }; - } else { - metaProps = { - ..._metaProps, - 'aria-label': __( 'Custom color picker.' ), - }; - } - } + const { metaProps, labelProps } = getComputeCircularOptionPickerCommonProps( + asButtons, + loop, + ariaLabel, + ariaLabelledby + ); const options = unsetable ? [ unsetOption, ...duotoneOptions ] @@ -163,6 +144,7 @@ function DuotonePicker( { ) { ); - let metaProps: - | { asButtons: false; loop?: boolean; 'aria-label': string } - | { asButtons: false; loop?: boolean; 'aria-labelledby': string } - | { asButtons: true }; - - if ( asButtons ) { - metaProps = { asButtons: true }; - } else { - const _metaProps: { asButtons: false; loop?: boolean } = { - asButtons: false, - loop, - }; - - if ( ariaLabel ) { - metaProps = { ..._metaProps, 'aria-label': ariaLabel }; - } else if ( ariaLabelledby ) { - metaProps = { - ..._metaProps, - 'aria-labelledby': ariaLabelledby, - }; - } else { - metaProps = { - ..._metaProps, - 'aria-label': __( 'Custom color picker.' ), - }; - } - } + const { metaProps, labelProps } = getComputeCircularOptionPickerCommonProps( + asButtons, + loop, + ariaLabel, + ariaLabelledby + ); return ( diff --git a/test/e2e/specs/editor/blocks/buttons.spec.js b/test/e2e/specs/editor/blocks/buttons.spec.js index 554bd8947f0bf5..7830a934529aa4 100644 --- a/test/e2e/specs/editor/blocks/buttons.spec.js +++ b/test/e2e/specs/editor/blocks/buttons.spec.js @@ -324,13 +324,13 @@ test.describe( 'Buttons', () => { await page.click( 'role=region[name="Editor settings"i] >> role=button[name="Text"i]' ); - await page.click( 'role=button[name="Custom color picker."i]' ); + await page.click( 'role=button[name="Custom color picker"i]' ); await page.fill( 'role=textbox[name="Hex color"i]', 'ff0000' ); await page.click( 'role=region[name="Editor settings"i] >> role=button[name="Background"i]' ); - await page.click( 'role=button[name="Custom color picker."i]' ); + await page.click( 'role=button[name="Custom color picker"i]' ); await page.fill( 'role=textbox[name="Hex color"i]', '00ff00' ); // Check the content. diff --git a/test/e2e/specs/editor/blocks/cover.spec.js b/test/e2e/specs/editor/blocks/cover.spec.js index 87c244a7306dc6..bee2548c2305d2 100644 --- a/test/e2e/specs/editor/blocks/cover.spec.js +++ b/test/e2e/specs/editor/blocks/cover.spec.js @@ -33,7 +33,7 @@ test.describe( 'Cover', () => { } ); // Locate the Black color swatch. - const blackColorSwatch = coverBlock.getByRole( 'option', { + const blackColorSwatch = coverBlock.getByRole( 'button', { name: 'Black', } ); await expect( blackColorSwatch ).toBeVisible(); @@ -105,7 +105,7 @@ test.describe( 'Cover', () => { // Choose a color swatch to transform the placeholder block into // a functioning block. await coverBlock - .getByRole( 'option', { + .getByRole( 'button', { name: 'Black', } ) .click(); @@ -128,7 +128,7 @@ test.describe( 'Cover', () => { name: 'Block: Cover', } ); await coverBlock - .getByRole( 'option', { + .getByRole( 'button', { name: 'Black', } ) .click(); @@ -240,7 +240,7 @@ test.describe( 'Cover', () => { // Choose a color swatch to transform the placeholder block into // a functioning block. await coverBlock - .getByRole( 'option', { + .getByRole( 'button', { name: 'Black', } ) .click(); @@ -266,7 +266,7 @@ test.describe( 'Cover', () => { // Choose a color swatch to transform the placeholder block into // a functioning block. await secondCoverBlock - .getByRole( 'option', { + .getByRole( 'button', { name: 'Black', } ) .click(); diff --git a/test/e2e/specs/editor/blocks/heading.spec.js b/test/e2e/specs/editor/blocks/heading.spec.js index 906095cad9d080..6ff7e11bb334e0 100644 --- a/test/e2e/specs/editor/blocks/heading.spec.js +++ b/test/e2e/specs/editor/blocks/heading.spec.js @@ -184,7 +184,7 @@ test.describe( 'Heading', () => { await textColor.click(); await page - .getByRole( 'button', { name: /Custom color picker./i } ) + .getByRole( 'button', { name: /Custom color picker/i } ) .click(); await page diff --git a/test/e2e/specs/editor/various/list-view.spec.js b/test/e2e/specs/editor/various/list-view.spec.js index 988683c8d11aa3..98dfe5e304f802 100644 --- a/test/e2e/specs/editor/various/list-view.spec.js +++ b/test/e2e/specs/editor/various/list-view.spec.js @@ -162,10 +162,10 @@ test.describe( 'List View', () => { // make the inner blocks appear. await editor.canvas .getByRole( 'document', { name: 'Block: Cover' } ) - .getByRole( 'listbox', { - name: 'Custom color picker.', + .getByRole( 'group', { + name: 'Overlay color', } ) - .getByRole( 'option' ) + .getByRole( 'button' ) .first() .click(); From 1ede5106fd4b872de8780852f1b9152f218f979d Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Tue, 18 Feb 2025 17:20:23 +0100 Subject: [PATCH 02/28] Fix CSS classes for the post editor iframe body. (#68481) * Fix CSS classes for the post editor iframe body. * Remove canvas loader CSS animation for testing purposes. * Restore canvas loader CSS animation previously removed for testing purposes. * Add test theme and post editor tests. * Add tests for the site editor. Co-authored-by: afercia Co-authored-by: carolinan Co-authored-by: Mamaduka Co-authored-by: ellatrix Co-authored-by: richtabor Co-authored-by: colinduwe --- .../src/components/iframe/index.js | 29 +++---- .../block-editor-dark-background.spec.js | 52 +++++++++++++ .../site-editor-dark-background.spec.js | 75 +++++++++++++++++++ .../darktheme/block-templates/index.html | 11 +++ .../darktheme/block-templates/singular.html | 2 + .../gutenberg-test-themes/darktheme/index.php | 9 +++ .../gutenberg-test-themes/darktheme/style.css | 15 ++++ .../darktheme/theme.json | 10 +++ 8 files changed, 190 insertions(+), 13 deletions(-) create mode 100644 test/e2e/specs/editor/various/block-editor-dark-background.spec.js create mode 100644 test/e2e/specs/site-editor/site-editor-dark-background.spec.js create mode 100644 test/gutenberg-test-themes/darktheme/block-templates/index.html create mode 100644 test/gutenberg-test-themes/darktheme/block-templates/singular.html create mode 100644 test/gutenberg-test-themes/darktheme/index.php create mode 100644 test/gutenberg-test-themes/darktheme/style.css create mode 100644 test/gutenberg-test-themes/darktheme/theme.json diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 3ae01525a80109..8411430dfbdadd 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -131,8 +131,23 @@ function Iframe( { function preventFileDropDefault( event ) { event.preventDefault(); } + + const { ownerDocument } = node; + + // Ideally ALL classes that are added through get_body_class should + // be added in the editor too, which we'll somehow have to get from + // the server in the future (which will run the PHP filters). + setBodyClasses( + Array.from( ownerDocument.body.classList ).filter( + ( name ) => + name.startsWith( 'admin-color-' ) || + name.startsWith( 'post-type-' ) || + name === 'wp-embed-responsive' + ) + ); + function onLoad() { - const { contentDocument, ownerDocument } = node; + const { contentDocument } = node; const { documentElement } = contentDocument; iFrameDocument = contentDocument; @@ -140,18 +155,6 @@ function Iframe( { clearerRef( documentElement ); - // Ideally ALL classes that are added through get_body_class should - // be added in the editor too, which we'll somehow have to get from - // the server in the future (which will run the PHP filters). - setBodyClasses( - Array.from( ownerDocument.body.classList ).filter( - ( name ) => - name.startsWith( 'admin-color-' ) || - name.startsWith( 'post-type-' ) || - name === 'wp-embed-responsive' - ) - ); - contentDocument.dir = ownerDocument.dir; for ( const compatStyle of getCompatibilityStyles() ) { diff --git a/test/e2e/specs/editor/various/block-editor-dark-background.spec.js b/test/e2e/specs/editor/various/block-editor-dark-background.spec.js new file mode 100644 index 00000000000000..dc333f31c7ed9d --- /dev/null +++ b/test/e2e/specs/editor/various/block-editor-dark-background.spec.js @@ -0,0 +1,52 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Block editor with dark background theme', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'darktheme' ); + } ); + + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyone' ); + } ); + + test.describe( 'Block editor iframe body', () => { + test( 'Should have the is-dark-theme CSS class', async ( { + editor, + } ) => { + const canvasBody = editor.canvas.locator( 'body' ); + + await expect( canvasBody ).toHaveClass( /is-dark-theme/ ); + } ); + } ); +} ); + +test.describe( 'Block editor with light background theme', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyfour' ); + } ); + + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyone' ); + } ); + + test.describe( 'Block editor iframe body', () => { + test( 'Should not have the is-dark-theme CSS class', async ( { + editor, + } ) => { + const canvasBody = editor.canvas.locator( 'body' ); + + await expect( canvasBody ).not.toHaveClass( /is-dark-theme/ ); + } ); + } ); +} ); diff --git a/test/e2e/specs/site-editor/site-editor-dark-background.spec.js b/test/e2e/specs/site-editor/site-editor-dark-background.spec.js new file mode 100644 index 00000000000000..1bf49c196bdfc6 --- /dev/null +++ b/test/e2e/specs/site-editor/site-editor-dark-background.spec.js @@ -0,0 +1,75 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Site editor with dark background theme', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'darktheme' ); + } ); + + test.beforeEach( async ( { admin } ) => { + await admin.visitSiteEditor(); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyone' ); + } ); + + test.describe( 'Site editor iframe body', () => { + test( 'Should have the is-dark-theme CSS class', async ( { + editor, + } ) => { + const canvasBody = editor.canvas.locator( 'body' ); + + await expect( canvasBody ).toHaveClass( /is-dark-theme/ ); + } ); + } ); +} ); + +test.describe( 'Site editor with light background theme and theme variations', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyfour' ); + } ); + + test.beforeEach( async ( { admin } ) => { + await admin.visitSiteEditor(); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyone' ); + } ); + + test.describe( 'Site editor iframe body', () => { + test( 'Should not have the is-dark-theme CSS class', async ( { + editor, + } ) => { + const canvasBody = editor.canvas.locator( 'body' ); + + await expect( canvasBody ).not.toHaveClass( /is-dark-theme/ ); + } ); + + test( 'Should add and remove the is-dark-theme CSS class with dark and light theme variation', async ( { + page, + editor, + } ) => { + // Click "Styles" + await page.getByRole( 'button', { name: 'Styles' } ).click(); + + // Click "Browse styles" + await page.getByRole( 'button', { name: 'Browse styles' } ).click(); + + const canvasBody = editor.canvas.locator( 'body' ); + + // Activate "Maelstrom" Theme Variation. + await page.getByRole( 'button', { name: 'Maelstrom' } ).click(); + + await expect( canvasBody ).toHaveClass( /is-dark-theme/ ); + + // Activate "Ember" Theme Variation. + await page.getByRole( 'button', { name: 'Ember' } ).click(); + + await expect( canvasBody ).not.toHaveClass( /is-dark-theme/ ); + } ); + } ); +} ); diff --git a/test/gutenberg-test-themes/darktheme/block-templates/index.html b/test/gutenberg-test-themes/darktheme/block-templates/index.html new file mode 100644 index 00000000000000..0283daeb54c6f4 --- /dev/null +++ b/test/gutenberg-test-themes/darktheme/block-templates/index.html @@ -0,0 +1,11 @@ + +
+ + + + +
+ + +

My awesome paragraph

+ diff --git a/test/gutenberg-test-themes/darktheme/block-templates/singular.html b/test/gutenberg-test-themes/darktheme/block-templates/singular.html new file mode 100644 index 00000000000000..cd05d5fe917fea --- /dev/null +++ b/test/gutenberg-test-themes/darktheme/block-templates/singular.html @@ -0,0 +1,2 @@ + + diff --git a/test/gutenberg-test-themes/darktheme/index.php b/test/gutenberg-test-themes/darktheme/index.php new file mode 100644 index 00000000000000..0c6530acc1aaff --- /dev/null +++ b/test/gutenberg-test-themes/darktheme/index.php @@ -0,0 +1,9 @@ + Date: Wed, 19 Feb 2025 00:03:53 +0530 Subject: [PATCH 03/28] Docs: Add Documentation for Adding Block Variations Using get_block_type_variations Hook (#68434) * Update doc to add PHP strategy to register block variations * Update example code * Add callout with link to relevant blog post --------- Co-authored-by: JuanMa --- .../block-api/block-variations.md | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/reference-guides/block-api/block-variations.md b/docs/reference-guides/block-api/block-variations.md index 8a0c6b1dd5bd6c..8c223e54eea299 100644 --- a/docs/reference-guides/block-api/block-variations.md +++ b/docs/reference-guides/block-api/block-variations.md @@ -60,6 +60,44 @@ wp.blocks.registerBlockVariation( 'core/embed', { } ); ``` +## Registering block variations in PHP + +Block variations can also be registered from PHP using the `get_block_type_variations` filter hook. This approach is particularly useful when you need to dynamically generate variations based on registered post types, taxonomies, or other WordPress data. + +Here's an example of how to register a custom variation for the `core/image` block: + +```php +function my_custom_image_variation( $variations, $block_type ) { + // Only modify variations for the image block + if ( 'core/image' !== $block_type->name ) { + return $variations; + } + + // Add a custom variation + $variations[] = array( + 'name' => 'wide-image', + 'title' => __( 'Wide image', 'textdomain' ), + 'description' => __( 'A wide image', 'textdomain' ), + 'scope' => array( 'inserter' ), + 'isDefault' => false, + 'attributes' => array( + 'align' => 'wide', // Identifies the link type as custom + ), + ); + + return $variations; +} +add_filter( 'get_block_type_variations', 'my_custom_image_variation', 10, 2 ); +``` + +The `get_block_type_variations` filter is called when variations are requested for a block type. It receives two parameters: +- `$variations`: An array of currently registered variations for the block type +- `$block_type`: The full block type object + +Note that variations registered through PHP will be merged with any variations registered through JavaScript using `registerBlockVariation()`. + +
Check the How to register block variations with PHP blog post for more info about this
+ ## Removing a block variation Block variations can also be easily removed. To do so, use `wp.blocks.unregisterBlockVariation()`. This function accepts the name of the block and the `name` of the variation that should be unregistered. From 90ee09a5d28b68f1e2cd5291709de0da06f7418e Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Wed, 19 Feb 2025 08:34:46 +0100 Subject: [PATCH 04/28] Make navigation block wavy underline more visible on dark backgrounds. (#69004) Co-authored-by: afercia Co-authored-by: Mamaduka Co-authored-by: Rishit30G Co-authored-by: hanneslsm --- .../src/navigation-link/editor.scss | 27 +++---------------- 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/packages/block-library/src/navigation-link/editor.scss b/packages/block-library/src/navigation-link/editor.scss index b27c4520921fd4..5034c6f909ecdc 100644 --- a/packages/block-library/src/navigation-link/editor.scss +++ b/packages/block-library/src/navigation-link/editor.scss @@ -82,30 +82,9 @@ // Draw a wavy underline. .wp-block-navigation-link__placeholder-text { span { - $blur: 10%; - $width: 6%; - $stop1: 30%; - $stop2: 64%; - - --wp-underline-color: var(--wp-admin-theme-color); - - background-image: - linear-gradient(45deg, transparent ($stop1 - $blur), var(--wp-underline-color) $stop1, var(--wp-underline-color) ($stop1 + $width), transparent ($stop1 + $width + $blur)), - linear-gradient(135deg, transparent ($stop2 - $blur), var(--wp-underline-color) $stop2, var(--wp-underline-color) ($stop2 + $width), transparent ($stop2 + $width + $blur)); - background-position: 0 100%; - background-size: 6px 3px; - background-repeat: repeat-x; - - // Since applied to a span, it doesn't change the footprint of the item, - // but it does vertically shift the underline to better align. - padding-bottom: 0.1em; - } - - &.is-invalid, - &.is-draft { - span { - --wp-underline-color: #{$alert-red}; - } + text-decoration: wavy underline; + text-decoration-skip-ink: none; + text-underline-offset: 0.25rem; } } From f017b64dcf58dff5d89a9f00efdd7be956f7a33d Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Wed, 19 Feb 2025 11:11:17 +0000 Subject: [PATCH 05/28] Bump plugin version to 20.3.0 --- gutenberg.php | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index 1ef51115ec9323..58146500737685 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -5,7 +5,7 @@ * Description: Printing since 1440. This is the development plugin for the block editor, site editor, and other future WordPress core functionality. * Requires at least: 6.6 * Requires PHP: 7.2 - * Version: 20.3.0-rc.2 + * Version: 20.3.0 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/package-lock.json b/package-lock.json index 1c0795c90bcc92..a8bbafbfc2eb1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "20.3.0-rc.2", + "version": "20.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "20.3.0-rc.2", + "version": "20.3.0", "hasInstallScript": true, "license": "GPL-2.0-or-later", "workspaces": [ diff --git a/package.json b/package.json index 2984502a07a5da..0737c415f71c10 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "20.3.0-rc.2", + "version": "20.3.0", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", From 4ad117cdba18f81e8c9756b38d14e02d0ee4cad1 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Wed, 19 Feb 2025 21:04:09 +0900 Subject: [PATCH 06/28] Site Editor: show pattern category step in navigation for mobile (#69206) Co-authored-by: t-hamano Co-authored-by: stokesman --- .../components/site-editor-routes/patterns.js | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/edit-site/src/components/site-editor-routes/patterns.js b/packages/edit-site/src/components/site-editor-routes/patterns.js index db97c4b5c080fd..785528f09afb04 100644 --- a/packages/edit-site/src/components/site-editor-routes/patterns.js +++ b/packages/edit-site/src/components/site-editor-routes/patterns.js @@ -1,8 +1,27 @@ +/** + * WordPress dependencies + */ +import { privateApis as routerPrivateApis } from '@wordpress/router'; + /** * Internal dependencies */ import SidebarNavigationScreenPatterns from '../sidebar-navigation-screen-patterns'; import PagePatterns from '../page-patterns'; +import { unlock } from '../../lock-unlock'; + +const { useLocation } = unlock( routerPrivateApis ); + +function MobilePatternsView() { + const { query = {} } = useLocation(); + const { categoryId } = query; + + return !! categoryId ? ( + + ) : ( + + ); +} export const patternsRoute = { name: 'patterns', @@ -10,6 +29,6 @@ export const patternsRoute = { areas: { sidebar: , content: , - mobile: , + mobile: , }, }; From 099b3a3029ba970d2c1cf6af2ec462983b292a6f Mon Sep 17 00:00:00 2001 From: Shail Mehta Date: Wed, 19 Feb 2025 17:35:40 +0530 Subject: [PATCH 07/28] Added Missing Global Documentation in Query Total Block (#69233) * Added Missing Global Documentation * Fix PHPCS Issue Co-authored-by: shail-mehta Co-authored-by: t-hamano --- packages/block-library/src/query-total/index.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/block-library/src/query-total/index.php b/packages/block-library/src/query-total/index.php index ff2ac486727b92..ba99b324601331 100644 --- a/packages/block-library/src/query-total/index.php +++ b/packages/block-library/src/query-total/index.php @@ -10,6 +10,8 @@ * * @since 6.8.0 * + * @global WP_Query $wp_query WordPress Query object. + * * @param array $attributes Block attributes. * @param string $content Block default content. * @param WP_Block $block Block instance. From 3639ac4843c4cd7f4464ca5e945489f0f5b2e670 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Wed, 19 Feb 2025 12:06:43 +0000 Subject: [PATCH 08/28] Update Changelog for 20.3.0 --- changelog.txt | 186 +------------------------------------------------- 1 file changed, 1 insertion(+), 185 deletions(-) diff --git a/changelog.txt b/changelog.txt index 3a8e2bafd95671..9f6e6f652c2b02 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,6 @@ == Changelog == -= 20.3.0-rc.2 = += 20.3.0 = ## Changelog @@ -184,190 +184,6 @@ The following contributors merged PRs in this release: @adamsilverstein @afercia @akasunil @benazeer-ben @carolinan @DAreRodz @desrosj @ellatrix @grgar @Gulamdastgir-Momin @himanshupathak95 @Infinite-Null @joemcgill @Juzar10 @Mamaduka @Mayank-Tripathi32 @ockham @peterwilsoncc @Rishit30G @SainathPoojary @shail-mehta @shimotmk @singhakanshu00 @srtfisher @swissspidy @t-hamano @torounit @yogeshbhutkar -= 20.3.0-rc.1 = - - -## Changelog - -### Enhancements - -- Disable hover animation on preview frame for classic themes. ([68976](https://github.com/WordPress/gutenberg/pull/68976)) - -#### Block Library -- Added discord in social links. ([68848](https://github.com/WordPress/gutenberg/pull/68848)) -- Featured Image block: Use resolution tool component. ([68471](https://github.com/WordPress/gutenberg/pull/68471)) -- Query block: Add option to ignore sticky posts behavior. ([69057](https://github.com/WordPress/gutenberg/pull/69057)) -- RSS: Border & Spacing support. ([66411](https://github.com/WordPress/gutenberg/pull/66411)) - -#### Global Styles -- Disable Clear button if there's no shadow. ([69092](https://github.com/WordPress/gutenberg/pull/69092)) -- Duotone Settings: Add `reset` button and improve toggle rendering in FiltersPanel. ([68672](https://github.com/WordPress/gutenberg/pull/68672)) -- Shadow Panel: Add reset button. ([68981](https://github.com/WordPress/gutenberg/pull/68981)) - -#### Post Editor -- Editor: Add loading state to the 'PageAttributesParent' component. ([69062](https://github.com/WordPress/gutenberg/pull/69062)) -- Editor: Add loading state to the 'PostAuthorCombobox' component. ([68991](https://github.com/WordPress/gutenberg/pull/68991)) -- Editor: Display error message when loading current post fails. ([68999](https://github.com/WordPress/gutenberg/pull/68999)) - -#### Block Editor -- Quick Inserter: Restore pattern search and insertion. ([69028](https://github.com/WordPress/gutenberg/pull/69028)) -- Update keyboard shortcuts to use `primaryShift+backspace` for block deletion. ([69074](https://github.com/WordPress/gutenberg/pull/69074)) - -#### Design Tools -- Archives: Add Color Support. ([68685](https://github.com/WordPress/gutenberg/pull/68685)) -- Categories: Add Color Support. ([68686](https://github.com/WordPress/gutenberg/pull/68686)) - -#### Components -- ComboboxControl: Add an `isLoading` prop to show a loading spinner. ([68990](https://github.com/WordPress/gutenberg/pull/68990)) - - -### Bug Fixes - -- Exclude Iterator helpers from polyfills. ([69070](https://github.com/WordPress/gutenberg/pull/69070)) -- Fix Dependabot ignore statements. ([69144](https://github.com/WordPress/gutenberg/pull/69144)) - -#### Block Editor -- Block popover: Fix scrolling over. ([68075](https://github.com/WordPress/gutenberg/pull/68075)) -- Display root appender when default block is disabled. ([68951](https://github.com/WordPress/gutenberg/pull/68951)) -- Fix regression for root appender logic. ([68994](https://github.com/WordPress/gutenberg/pull/68994)) -- Inserter: Remove block default icon from no results message. ([68693](https://github.com/WordPress/gutenberg/pull/68693)) -- Rename `aspect` property to `ratio` to carry the `defaultAspect` in `AspectRatioDropdown`. ([69085](https://github.com/WordPress/gutenberg/pull/69085)) -- Writing Flow: Restore early return for no block selection in tab nav hook. ([69079](https://github.com/WordPress/gutenberg/pull/69079)) - -#### Site Editor -- Edit Site: Fix Fields package private APIs error. ([68964](https://github.com/WordPress/gutenberg/pull/68964)) -- Fix: Site Editor should display a 404 message. ([69009](https://github.com/WordPress/gutenberg/pull/69009)) -- Use the same editor component for all routes. ([69093](https://github.com/WordPress/gutenberg/pull/69093)) - -#### Global Styles -- Fix: Additional CSS button not working after back navigation. ([68954](https://github.com/WordPress/gutenberg/pull/68954)) -- Fix: Missing 'No blocks found.' message for block search in editor. ([69036](https://github.com/WordPress/gutenberg/pull/69036)) - -#### Block Library -- Query Block: Fix 'parents' argument validation. ([68983](https://github.com/WordPress/gutenberg/pull/68983)) -- Social Links: Fix appender size in non-iframe editor. ([68215](https://github.com/WordPress/gutenberg/pull/68215)) - -#### REST API -- Add support for the 'ignore_sticky_posts' argument. ([68970](https://github.com/WordPress/gutenberg/pull/68970)) -- Fix: Prevent Errors in Header Processing and Encode URLs Properly. ([67780](https://github.com/WordPress/gutenberg/pull/67780)) - -#### Icons -- Fix the background, arrowUpLeft, keyboardReturn and square icons. ([69076](https://github.com/WordPress/gutenberg/pull/69076)) - -#### Font Library -- Refactor font variant components to use useId for checkbox IDs. ([69050](https://github.com/WordPress/gutenberg/pull/69050)) - -#### Block Directory -- Remove block icon from InstalledBlocksPrePublishPanel. ([69046](https://github.com/WordPress/gutenberg/pull/69046)) - -#### DataViews -- Fixed: Empty Pattern Overlap in Pattern Title in Dataviews Table Layout. ([68997](https://github.com/WordPress/gutenberg/pull/68997)) - -#### Media -- Add optional chain to sizes indexing of media details in edit-site. ([68995](https://github.com/WordPress/gutenberg/pull/68995)) - -#### CSS & Styling -- Enabled Full height in Additional CSS. ([68993](https://github.com/WordPress/gutenberg/pull/68993)) - -#### Block hooks -- Fix truncation of post content. ([68926](https://github.com/WordPress/gutenberg/pull/68926)) - -#### Interactivity API -- iAPI Router: Fix CSS rule order in some constructed style sheets. ([68923](https://github.com/WordPress/gutenberg/pull/68923)) - - -### Accessibility - -#### Components -- Font Size Picker: Remove Custom option from FontSizePickerSelect dropdown. ([69038](https://github.com/WordPress/gutenberg/pull/69038)) - -#### Global Styles -- Add missing list role to the list of blocks in the global Styles. ([69027](https://github.com/WordPress/gutenberg/pull/69027)) - -#### Block API -- Block support: Preserve aria-label value in comment delimiter. ([69002](https://github.com/WordPress/gutenberg/pull/69002)) - -#### Block Editor -- [Block Editor]: A11y - Add and Update missing reduce-motion mixing. ([68417](https://github.com/WordPress/gutenberg/pull/68417)) - - -### Performance - -#### Post Editor -- Editor: Don't use selector shortcuts for the taxonomy queries. ([68998](https://github.com/WordPress/gutenberg/pull/68998)) -- Editor: Optimize 'PostAuthorCheck' component data selection. ([69105](https://github.com/WordPress/gutenberg/pull/69105)) - -#### Style Book -- Improve StyleBook resize responsiveness for Classic Theme. ([68980](https://github.com/WordPress/gutenberg/pull/68980)) - - -### Documentation - -- Added Missing Global Documentation. ([69104](https://github.com/WordPress/gutenberg/pull/69104)) -- Changed Inline Document Order. ([68992](https://github.com/WordPress/gutenberg/pull/68992)) -- wp-env: Add lifecycleScripts to the schema. ([68724](https://github.com/WordPress/gutenberg/pull/68724)) - - -### Code Quality - -- Core Data: Add type for term entity. ([69151](https://github.com/WordPress/gutenberg/pull/69151)) -- iAPI Router: Add missing changelog entry for #68923. ([68945](https://github.com/WordPress/gutenberg/pull/68945)) - -#### Block Library -- E2E: Add regression test for spacer block in themes without spacing units. ([68913](https://github.com/WordPress/gutenberg/pull/68913)) -- Navigation Link Block: Use stable variable for underline color styling. ([68953](https://github.com/WordPress/gutenberg/pull/68953)) -- Regenerate block fixtures. ([68982](https://github.com/WordPress/gutenberg/pull/68982)) -- Social Links: Remove redundant reduce-motion mixin. ([69000](https://github.com/WordPress/gutenberg/pull/69000)) - -#### Site Editor -- Fast follow: Redirections of deprecated site editor URLs. ([68971](https://github.com/WordPress/gutenberg/pull/68971)) -- Quality: Remove unused props and styles from SidebarNavigationScreen. ([68972](https://github.com/WordPress/gutenberg/pull/68972)) - -#### Block Editor -- Inserter: Remove unused no-results-icon styles. ([69018](https://github.com/WordPress/gutenberg/pull/69018)) - - -### Tools - -- PR Template: Suggest linking the issue. ([68924](https://github.com/WordPress/gutenberg/pull/68924)) -- Relocate changelog file for WP#6910 to 6.9 backports. ([69068](https://github.com/WordPress/gutenberg/pull/69068)) - -#### Build Tooling -- Fix installing svn during deploys. ([69047](https://github.com/WordPress/gutenberg/pull/69047)) -- Remove `react-native` dependabot group. ([69118](https://github.com/WordPress/gutenberg/pull/69118)) - -#### Testing -- e2e: Fix "add new" selector. ([69111](https://github.com/WordPress/gutenberg/pull/69111)) - - -### Various - -- Remove react-native dependabot group - Take 2. ([69122](https://github.com/WordPress/gutenberg/pull/69122)) - -#### Plugin -- Code Quality: Delete unused function from PHP Sync Issue generation script. ([68947](https://github.com/WordPress/gutenberg/pull/68947)) -- npm scripts: Use `node -p` instead of `echo`. ([68946](https://github.com/WordPress/gutenberg/pull/68946)) - - -## First-time contributors - -The following PRs were merged by first-time contributors: - -- @benazeer-ben: RSS: Border & Spacing support. ([66411](https://github.com/WordPress/gutenberg/pull/66411)) -- @grgar: Add optional chain to sizes indexing of media details in edit-site. ([68995](https://github.com/WordPress/gutenberg/pull/68995)) -- @Gulamdastgir-Momin: Added discord in social links. ([68848](https://github.com/WordPress/gutenberg/pull/68848)) -- @Juzar10: Fix: Prevent Errors in Header Processing and Encode URLs Properly. ([67780](https://github.com/WordPress/gutenberg/pull/67780)) -- @singhakanshu00: Disable hover animation on preview frame for classic themes. ([68976](https://github.com/WordPress/gutenberg/pull/68976)) -- @srtfisher: wp-env: Add lifecycleScripts to the schema. ([68724](https://github.com/WordPress/gutenberg/pull/68724)) - - -## Contributors - -The following contributors merged PRs in this release: - -@adamsilverstein @afercia @akasunil @benazeer-ben @carolinan @DAreRodz @desrosj @ellatrix @grgar @Gulamdastgir-Momin @himanshupathak95 @Infinite-Null @joemcgill @Juzar10 @Mamaduka @Mayank-Tripathi32 @ockham @peterwilsoncc @Rishit30G @SainathPoojary @shail-mehta @shimotmk @singhakanshu00 @srtfisher @swissspidy @t-hamano @torounit @yogeshbhutkar - - = 20.0.1 = ## Changelog From 8df9b70ce51d1b0892c981b8cb421fa3b26c0106 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Wed, 19 Feb 2025 22:38:59 +0900 Subject: [PATCH 09/28] Notice: Fix text contrast for dark mode (#69226) Co-authored-by: t-hamano Co-authored-by: carolinan Co-authored-by: ciampo --- packages/components/CHANGELOG.md | 4 ++++ packages/components/src/notice/style.scss | 1 + 2 files changed, 5 insertions(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index f425baae726172..6917427ef15716 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -9,6 +9,10 @@ - `NumberControl`: Fix invalid HTML attributes for infinite bounds ([#69033](https://github.com/WordPress/gutenberg/pull/69033)). +### Bug Fixes + +- `Notice`: Fix text contrast for dark mode ([#69226](https://github.com/WordPress/gutenberg/pull/69226)). + ## 29.4.0 (2025-02-12) - `FontSizePicker`: Remove Custom option from dropdown to prevent unexpected context changes during keyboard navigation ([#69038](https://github.com/WordPress/gutenberg/pull/69038)). diff --git a/packages/components/src/notice/style.scss b/packages/components/src/notice/style.scss index a2d6aca530a93c..9cfc2b983a4918 100644 --- a/packages/components/src/notice/style.scss +++ b/packages/components/src/notice/style.scss @@ -6,6 +6,7 @@ border-left: 4px solid $components-color-accent; padding: 8px 12px; align-items: center; + color: $gray-900; &.is-dismissible { position: relative; From 8c0db8e5b1dda64c317bbcec86896c675b49b0f9 Mon Sep 17 00:00:00 2001 From: Bernie Reiter <96308+ockham@users.noreply.github.com> Date: Wed, 19 Feb 2025 14:51:48 +0100 Subject: [PATCH 10/28] Block Hooks: Enable first/last child insertion next to Classic block (#69241)x Allow insertion of a hooked block as the first or last child of the Post Content block (and, less importantly, of the Synced Pattern and Navigation blocks). Co-authored-by: ockham Co-authored-by: gziolo Co-authored-by: t-hamano --- backport-changelog/6.8/8212.md | 1 + lib/compat/wordpress-6.8/blocks.php | 30 +++++ .../specs/editor/plugins/block-hooks.spec.js | 121 +++++++++++++++++- 3 files changed, 149 insertions(+), 3 deletions(-) diff --git a/backport-changelog/6.8/8212.md b/backport-changelog/6.8/8212.md index 30483af9e7b715..2a0019eae28094 100644 --- a/backport-changelog/6.8/8212.md +++ b/backport-changelog/6.8/8212.md @@ -2,3 +2,4 @@ https://github.com/WordPress/wordpress-develop/pull/8212 * https://github.com/WordPress/gutenberg/pull/68926 * https://github.com/WordPress/gutenberg/pull/69142 +* https://github.com/WordPress/gutenberg/pull/69241 diff --git a/lib/compat/wordpress-6.8/blocks.php b/lib/compat/wordpress-6.8/blocks.php index 7fd4237bc81d37..ba6d23b2486b84 100644 --- a/lib/compat/wordpress-6.8/blocks.php +++ b/lib/compat/wordpress-6.8/blocks.php @@ -36,6 +36,25 @@ function apply_block_hooks_to_content_from_post_object( $content, WP_Post $post return apply_block_hooks_to_content( $content, $post, $callback ); } + /* + * If the content was created using the classic editor or using a single Classic block + * (`core/freeform`), it might not contain any block markup at all. + * However, we still might need to inject hooked blocks in the first child or last child + * positions of the parent block. To be able to apply the Block Hooks algorithm, we wrap + * the content in a `core/freeform` wrapper block. + */ + if ( ! has_blocks( $content ) ) { + $original_content = $content; + + $content_wrapped_in_classic_block = get_comment_delimited_block_content( + 'core/freeform', + array(), + $content + ); + + $content = $content_wrapped_in_classic_block; + } + $attributes = array(); // If context is a post object, `ignoredHookedBlocks` information is stored in its post meta. @@ -71,6 +90,17 @@ function apply_block_hooks_to_content_from_post_object( $content, WP_Post $post // Finally, we need to remove the temporary wrapper block. $content = remove_serialized_parent_block( $content ); + // If we wrapped the content in a `core/freeform` block, we also need to remove that. + if ( ! empty( $content_wrapped_in_classic_block ) ) { + /* + * We cannot simply use remove_serialized_parent_block() here, + * as that function assumes that the block wrapper is at the top level. + * However, there might now be a hooked block inserted next to it + * (as first or last child of the parent). + */ + $content = str_replace( $content_wrapped_in_classic_block, $original_content, $content ); + } + return $content; } // We need to apply this filter before `do_blocks` (which is hooked to `the_content` at priority 9). diff --git a/test/e2e/specs/editor/plugins/block-hooks.spec.js b/test/e2e/specs/editor/plugins/block-hooks.spec.js index ec78db6b3a8fee..969f8ebb7091e2 100644 --- a/test/e2e/specs/editor/plugins/block-hooks.spec.js +++ b/test/e2e/specs/editor/plugins/block-hooks.spec.js @@ -3,12 +3,14 @@ */ const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); -const dummyBlockContent = ` +const dummyBlocksContent = `

This is a dummy heading

This is a dummy paragraph.

`; +const dummyClassicContent = + '

This is a dummy heading

This is a dummy paragraph.

'; const getHookedBlockClassName = ( relativePosition, anchorBlock ) => `hooked-block-${ relativePosition }-${ anchorBlock.replace( @@ -34,13 +36,13 @@ test.describe( 'Block Hooks API', () => { createMethod: 'createBlock', }, ].forEach( ( { name, postType, blockType, createMethod } ) => { - test.describe( `Hooked blocks in ${ name }`, () => { + test.describe( `Hooked blocks in ${ name } (blocks)`, () => { let postObject, containerPost; test.beforeAll( async ( { requestUtils } ) => { postObject = await requestUtils[ createMethod ]( { title: name, status: 'publish', - content: dummyBlockContent, + content: dummyBlocksContent, } ); await requestUtils.activatePlugin( @@ -162,6 +164,119 @@ test.describe( 'Block Hooks API', () => { ] ); } ); } ); + + test.describe( `Hooked blocks in ${ name } (classic)`, () => { + let postObject, containerPost; + test.beforeAll( async ( { requestUtils } ) => { + postObject = await requestUtils[ createMethod ]( { + title: name, + status: 'publish', + content: dummyClassicContent, + } ); + + await requestUtils.activatePlugin( + 'gutenberg-test-block-hooks' + ); + + if ( postType !== 'post' ) { + // We need a container post to hold our block instance. + containerPost = await requestUtils.createPost( { + title: `Block Hooks in ${ name }`, + status: 'publish', + content: ``, + meta: { + // Prevent Block Hooks from injecting blocks into the container + // post content so they won't distract from the ones injected + // into the block instance. + _wp_ignored_hooked_blocks: '["core/paragraph"]', + }, + } ); + } else { + containerPost = postObject; + } + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deactivatePlugin( + 'gutenberg-test-block-hooks' + ); + + await requestUtils.deleteAllPosts(); + await requestUtils.deleteAllBlocks(); + } ); + + test( `should insert hooked blocks into ${ name } on frontend`, async ( { + page, + } ) => { + await page.goto( `/?p=${ containerPost.id }` ); + await expect( + page.locator( '.entry-content > *' ) + ).toHaveClass( [ + 'dummy-heading', + 'dummy-paragraph', + getHookedBlockClassName( 'last_child', blockType ), + ] ); + } ); + + test( `should insert hooked blocks into ${ name } in editor and respect changes made there`, async ( { + admin, + editor, + page, + } ) => { + const expectedHookedBlockLastChild = { + name: 'core/paragraph', + attributes: { + className: getHookedBlockClassName( + 'last_child', + blockType + ), + }, + }; + + await admin.editPost( postObject.id ); + await expect + .poll( editor.getBlocks ) + .toMatchObject( [ + { name: 'core/freeform' }, + expectedHookedBlockLastChild, + ] ); + + const hookedBlock = editor.canvas.getByText( + getHookedBlockContent( 'last_child', blockType ) + ); + await editor.selectBlocks( hookedBlock ); + await editor.clickBlockToolbarButton( 'Move up' ); + + // Save updated post. + const saveButton = page + .getByRole( 'region', { name: 'Editor top bar' } ) + .getByRole( 'button', { name: 'Save', exact: true } ); + await saveButton.click(); + await page + .getByRole( 'button', { name: 'Dismiss this notice' } ) + .filter( { hasText: 'updated' } ) + .waitFor(); + + // Reload and verify that the new position of the hooked block has been persisted. + await page.reload(); + await expect + .poll( editor.getBlocks ) + .toMatchObject( [ + expectedHookedBlockLastChild, + { name: 'core/freeform' }, + ] ); + + // Verify that the frontend reflects the changes made in the editor. + await page.goto( `/?p=${ containerPost.id }` ); + await expect( + page.locator( '.entry-content > *' ) + ).toHaveClass( [ + getHookedBlockClassName( 'last_child', blockType ), + 'dummy-heading', + 'dummy-paragraph', + ] ); + } ); + } ); } ); test.describe( 'Hooked blocks in Navigation Menu', () => { From 67a07a721c77eeebf47df43729fcb182ae0ee0cd Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Wed, 19 Feb 2025 18:46:47 +0000 Subject: [PATCH 11/28] Update Changelog for 20.0.2 --- changelog.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/changelog.txt b/changelog.txt index 9f6e6f652c2b02..68e13890ff280e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,13 @@ == Changelog == += 20.0.2 = + +### Bug Fixes + +#### Block Editor +- Fix 'isBlockVisibleInTheInserter' selector helper performance ([68898](https://github.com/WordPress/gutenberg/pull/68898)) + + = 20.3.0 = ## Changelog From 1756bacc6ee76b68f602c4ed2e6bc640b95baec5 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Thu, 20 Feb 2025 19:36:44 +0800 Subject: [PATCH 12/28] Core Data: Allow 'null' as raw attribute value (#69257) Co-authored-by: Mamaduka Co-authored-by: tyxla Co-authored-by: TimothyBJacobs --- packages/core-data/src/selectors.ts | 5 ++- packages/core-data/src/test/selectors.js | 42 ++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index c31ebc04254640..64ff340afd0c27 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -465,7 +465,10 @@ export const getRawEntityRecord = createSelector( // Because edits are the "raw" attribute values, // we return those from record selectors to make rendering, // comparisons, and joins with edits easier. - accumulator[ _key ] = record[ _key ]?.raw ?? record[ _key ]; + accumulator[ _key ] = + record[ _key ]?.raw !== undefined + ? record[ _key ]?.raw + : record[ _key ]; } else { accumulator[ _key ] = record[ _key ]; } diff --git a/packages/core-data/src/test/selectors.js b/packages/core-data/src/test/selectors.js index 4b5e8417ad2028..0330865e959570 100644 --- a/packages/core-data/src/test/selectors.js +++ b/packages/core-data/src/test/selectors.js @@ -332,6 +332,48 @@ describe( 'getRawEntityRecord', () => { }, } ); } ); + it( 'should allow `null` as raw value', () => { + const state = deepFreeze( { + entities: { + config: [ + { + kind: 'someKind', + name: 'someName', + rawAttributes: [ 'title' ], + }, + ], + records: { + someKind: { + someName: { + queriedData: { + items: { + default: { + post: { + title: { + raw: null, + rendered: 'Placeholder', + }, + }, + }, + }, + itemIsComplete: { + default: { + post: true, + }, + }, + queries: {}, + }, + }, + }, + }, + }, + } ); + expect( + getRawEntityRecord( state, 'someKind', 'someName', 'post' ) + ).toEqual( { + title: null, + } ); + } ); } ); describe( 'getEntityRecords', () => { From 0f7193c79886abbb0c0132e58897cdd596698a2f Mon Sep 17 00:00:00 2001 From: Ankit Kumar Shah Date: Thu, 20 Feb 2025 18:12:00 +0530 Subject: [PATCH 13/28] Enhance 404 message styling (#69234) * Enhance 404 message styling with Notice component * Improve 404 error message * Remove color style from `.edit-site-layout__area__404` * Update 404 notice implementation with reusable component * Fixed DOM nesting validation error by replacing p with div Co-authored-by: Infinite-Null Co-authored-by: t-hamano Co-authored-by: joedolson Co-authored-by: carolinan --- .../src/components/layout/style.scss | 4 ---- .../sidebar-navigation-screen/index.js | 4 ++-- .../components/site-editor-routes/notfound.js | 19 +++++++++++++++---- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/edit-site/src/components/layout/style.scss b/packages/edit-site/src/components/layout/style.scss index 16813e8ac04943..caf7dd78da4b34 100644 --- a/packages/edit-site/src/components/layout/style.scss +++ b/packages/edit-site/src/components/layout/style.scss @@ -262,10 +262,6 @@ html.canvas-mode-edit-transition::view-transition-group(toggle) { } } -.edit-site-layout__area__404 { - margin: $canvas-padding; -} - .edit-site .components-editor-notices__snackbar { position: fixed; right: 0; diff --git a/packages/edit-site/src/components/sidebar-navigation-screen/index.js b/packages/edit-site/src/components/sidebar-navigation-screen/index.js index 473b28245eaee5..f5f7e380467c25 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen/index.js @@ -122,9 +122,9 @@ export default function SidebarNavigationScreen( {
{ description && ( -

+

{ description } -

+
) } { content }
diff --git a/packages/edit-site/src/components/site-editor-routes/notfound.js b/packages/edit-site/src/components/site-editor-routes/notfound.js index ee15e28e06f25a..2a42eb48f10ebf 100644 --- a/packages/edit-site/src/components/site-editor-routes/notfound.js +++ b/packages/edit-site/src/components/site-editor-routes/notfound.js @@ -6,8 +6,19 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ +import { Notice, __experimentalSpacer as Spacer } from '@wordpress/components'; import SidebarNavigationScreenMain from '../sidebar-navigation-screen-main'; +function NotFoundError() { + return ( + + { __( + 'The requested page could not be found. Please check the URL.' + ) } + + ); +} + export const notFoundRoute = { name: 'notfound', path: '*', @@ -15,13 +26,13 @@ export const notFoundRoute = { sidebar: , mobile: ( } /> ), content: ( -

- { __( '404 (Not Found)' ) } -

+ + + ), }, }; From c300edfebb48f79f6f0f6643ce04dd73303c5fcb Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Thu, 20 Feb 2025 12:44:27 -0800 Subject: [PATCH 14/28] Introduce `withSyncEvent` action wrapper utility and proxy `event` object whenever it is not used (#68097) * Implement withSyncEvent action wrapper utility. * Prepare Interactivity API infrastructure for awareness of action prior to evaluating it. * Proxy event object when withSyncEvent() is not used. * Ensure generator functions using withSyncEvent() are wrapped correctly to still be recognized as generator functions. * Update Interactivity API documentation to reference withSyncEvent(). * Use withSyncEvent() in all built-in actions that require it. * Minor fixes for withSyncEvent docs. * Clarify documentation. Co-authored-by: Weston Ruter * Enhance withSyncEvent implementation and ensure the sync flag is maintained when proxying functions via withScope. * Add doc block for wrapEventAsync(). * Use more specific types for event proxy handler. * Amend callback in withSyncEvent instead of wrapping it. * Revert "Prepare Interactivity API infrastructure for awareness of action prior to evaluating it." This reverts commit dba93ec4f7bb617d1f12ba1a06acfa39ff33d4b3. * Update evaluate() to no longer invoke functions (except where needed for BC) and move responsibility to the caller. * Export withSyncEvent * Fix evaluate to return scoped function and always reset scope. * Update custom directives for e2e tests to account for evaluate behavior change. * Update release version number in documentation. --------- Co-authored-by: Weston Ruter Co-authored-by: Luis Herranz --- .../interactivity-api/api-reference.md | 46 ++++++- packages/block-library/src/image/view.js | 15 ++- packages/block-library/src/navigation/view.js | 11 +- packages/block-library/src/query/view.js | 11 +- packages/block-library/src/search/view.js | 11 +- .../interactive-blocks/directive-each/view.js | 7 +- .../interactive-blocks/directive-init/view.js | 6 +- .../directive-on-document/view.js | 6 +- .../directive-on-window/view.js | 6 +- .../directive-priorities/view.js | 26 +++- .../interactive-blocks/directive-run/view.js | 8 +- .../directive-watch/view.js | 6 +- .../get-server-context/view.js | 11 +- .../get-server-state/view.js | 11 +- .../router-navigate/view.js | 6 +- .../interactive-blocks/router-regions/view.js | 6 +- .../interactive-blocks/tovdom-islands/view.js | 6 +- packages/interactivity-router/README.md | 7 +- packages/interactivity/src/directives.tsx | 119 ++++++++++++++++-- packages/interactivity/src/hooks.tsx | 25 +++- packages/interactivity/src/index.ts | 1 + packages/interactivity/src/utils.ts | 52 ++++++-- 22 files changed, 334 insertions(+), 69 deletions(-) diff --git a/docs/reference-guides/interactivity-api/api-reference.md b/docs/reference-guides/interactivity-api/api-reference.md index bbbb565684c578..bf2c1370ebcde2 100644 --- a/docs/reference-guides/interactivity-api/api-reference.md +++ b/docs/reference-guides/interactivity-api/api-reference.md @@ -873,6 +873,8 @@ const { state } = store( 'myPlugin', { } ); ``` +You may want to add multiple such `yield` points in your action if it is doing a lot of work. + As mentioned above with [`wp-on`](#wp-on), [`wp-on-window`](#wp-on-window), and [`wp-on-document`](#wp-on-document), an async action should be used whenever the `async` versions of the aforementioned directives cannot be used due to the action requiring synchronous access to the `event` object. Synchronous access is required whenever the action needs to call `event.preventDefault()`, `event.stopPropagation()`, or `event.stopImmediatePropagation()`. To ensure that the action code does not contribute to a long task, you may manually yield to the main thread after calling the synchronous event API. For example: ```js @@ -885,16 +887,17 @@ function splitTask() { store( 'myPlugin', { actions: { - handleClick: function* ( event ) { + handleClick: withSyncEvent( function* ( event ) { event.preventDefault(); yield splitTask(); doTheWork(); - }, + } ), }, } ); ``` -You may want to add multiple such `yield` points in your action if it is doing a lot of work. +You may notice the use of the [`withSyncEvent()`](#withsyncevent) utility function in this example. This is necessary due to an ongoing effort to handle store actions asynchronously by default, unless they require synchronous event access (which this example does due to the call to `event.preventDefault()`). Otherwise a deprecation warning will be triggered, and in a future release the behavior will change accordingly. + #### Side Effects @@ -1253,6 +1256,43 @@ store( 'mySliderPlugin', { } ); ``` +### withSyncEvent() + +Actions that require synchronous access to the `event` object need to use the `withSyncEvent()` function to annotate their handler callback. This is necessary due to an ongoing effort to handle store actions asynchronously by default, unless they require synchronous event access. Therefore, as of Gutenberg 20.4 / WordPress 6.8 all actions that require synchronous event access need to use the `withSyncEvent()` function. Otherwise a deprecation warning will be triggered, and in a future release the behavior will change accordingly. + +Only very specific event methods and properties require synchronous access, so it is advised to only use `withSyncEvent()` when necessary. The following event methods and properties require synchronous access: + +* `event.currentTarget` +* `event.preventDefault()` +* `event.stopImmediatePropagation()` +* `event.stopPropagation()` + +Here is an example, where one action requires synchronous event access while the other actions do not: + +```js +// store +import { store, withSyncEvent } from '@wordpress/interactivity'; + +store( 'myPlugin', { + actions: { + // `event.preventDefault()` requires synchronous event access. + preventNavigation: withSyncEvent( ( event ) => { + event.preventDefault(); + } ), + + // `event.target` does not require synchronous event access. + logTarget: ( event ) => { + console.log( 'event target => ', event.target ); + }, + + // Not using `event` at all does not require synchronous event access. + logSomething: () => { + console.log( 'something' ); + }, + }, +} ); +``` + ## Server functions The Interactivity API comes with handy functions that allow you to initialize and reference configuration options on the server. This is necessary to feed the initial data that the Server Directive Processing will use to modify the HTML markup before it's send to the browser. It is also a great way to leverage many of WordPress's APIs, like nonces, AJAX, and translations. diff --git a/packages/block-library/src/image/view.js b/packages/block-library/src/image/view.js index 3c9a729538813e..71a492a570b2ae 100644 --- a/packages/block-library/src/image/view.js +++ b/packages/block-library/src/image/view.js @@ -1,7 +1,12 @@ /** * WordPress dependencies */ -import { store, getContext, getElement } from '@wordpress/interactivity'; +import { + store, + getContext, + getElement, + withSyncEvent, +} from '@wordpress/interactivity'; /** * Tracks whether user is touching screen; used to differentiate behavior for @@ -128,7 +133,7 @@ const { state, actions, callbacks } = store( }, 450 ); } }, - handleKeydown( event ) { + handleKeydown: withSyncEvent( ( event ) => { if ( state.overlayEnabled ) { // Focuses the close button when the user presses the tab key. if ( event.key === 'Tab' ) { @@ -141,8 +146,8 @@ const { state, actions, callbacks } = store( actions.hideLightbox(); } } - }, - handleTouchMove( event ) { + } ), + handleTouchMove: withSyncEvent( ( event ) => { // On mobile devices, prevents triggering the scroll event because // otherwise the page jumps around when it resets the scroll position. // This also means that closing the lightbox requires that a user @@ -152,7 +157,7 @@ const { state, actions, callbacks } = store( if ( state.overlayEnabled ) { event.preventDefault(); } - }, + } ), handleTouchStart() { isTouching = true; }, diff --git a/packages/block-library/src/navigation/view.js b/packages/block-library/src/navigation/view.js index 9da7ab70d84f33..fd1fe33537b2f5 100644 --- a/packages/block-library/src/navigation/view.js +++ b/packages/block-library/src/navigation/view.js @@ -1,7 +1,12 @@ /** * WordPress dependencies */ -import { store, getContext, getElement } from '@wordpress/interactivity'; +import { + store, + getContext, + getElement, + withSyncEvent, +} from '@wordpress/interactivity'; const focusableSelectors = [ 'a[href]', @@ -106,7 +111,7 @@ const { state, actions } = store( actions.openMenu( 'click' ); } }, - handleMenuKeydown( event ) { + handleMenuKeydown: withSyncEvent( ( event ) => { const { type, firstFocusableElement, lastFocusableElement } = getContext(); if ( state.menuOpenedBy.click ) { @@ -137,7 +142,7 @@ const { state, actions } = store( } } } - }, + } ), handleMenuFocusout( event ) { const { modal, type } = getContext(); // If focus is outside modal, and in the document, close menu diff --git a/packages/block-library/src/query/view.js b/packages/block-library/src/query/view.js index e23294a24e02e3..fff12b16eac65b 100644 --- a/packages/block-library/src/query/view.js +++ b/packages/block-library/src/query/view.js @@ -1,7 +1,12 @@ /** * WordPress dependencies */ -import { store, getContext, getElement } from '@wordpress/interactivity'; +import { + store, + getContext, + getElement, + withSyncEvent, +} from '@wordpress/interactivity'; const isValidLink = ( ref ) => ref && @@ -22,7 +27,7 @@ store( 'core/query', { actions: { - *navigate( event ) { + navigate: withSyncEvent( function* ( event ) { const ctx = getContext(); const { ref } = getElement(); const queryRef = ref.closest( @@ -42,7 +47,7 @@ store( const firstAnchor = `.wp-block-post-template a[href]`; queryRef.querySelector( firstAnchor )?.focus(); } - }, + } ), *prefetch() { const { ref } = getElement(); if ( isValidLink( ref ) ) { diff --git a/packages/block-library/src/search/view.js b/packages/block-library/src/search/view.js index 0e4c462a2e3213..617e179b1dc88a 100644 --- a/packages/block-library/src/search/view.js +++ b/packages/block-library/src/search/view.js @@ -1,7 +1,12 @@ /** * WordPress dependencies */ -import { store, getContext, getElement } from '@wordpress/interactivity'; +import { + store, + getContext, + getElement, + withSyncEvent, +} from '@wordpress/interactivity'; const { actions } = store( 'core/search', @@ -31,7 +36,7 @@ const { actions } = store( }, }, actions: { - openSearchInput( event ) { + openSearchInput: withSyncEvent( ( event ) => { const ctx = getContext(); const { ref } = getElement(); if ( ! ctx.isSearchInputVisible ) { @@ -39,7 +44,7 @@ const { actions } = store( ctx.isSearchInputVisible = true; ref.parentElement.querySelector( 'input' ).focus(); } - }, + } ), closeSearchInput() { const ctx = getContext(); ctx.isSearchInputVisible = false; diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-each/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-each/view.js index 7577810b6bb876..98fd1fdb5593d3 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-each/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-each/view.js @@ -240,7 +240,12 @@ directive( 'priority-2-init', ( { directives: { 'priority-2-init': init }, evaluate } ) => { init.forEach( ( entry ) => { - useInit( () => evaluate( entry ) ); + useInit( () => { + const result = evaluate( entry ); + if ( typeof result === 'function' ) { + result(); + } + } ); } ); }, { priority: 2 } diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-init/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-init/view.js index a8c70a4a907207..c27fe8d534d863 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-init/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-init/view.js @@ -13,7 +13,11 @@ directive( 'show-mock', ( { directives: { 'show-mock': showMock }, element, evaluate } ) => { const entry = showMock.find( ( { suffix } ) => suffix === null ); - if ( ! evaluate( entry ) ) { + const result = evaluate( entry ); + if ( ! result ) { + return null; + } + if ( typeof result === 'function' && ! result() ) { return null; } return element; diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-on-document/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-on-document/view.js index b9689ac978f85f..f7918f3c6bf53b 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-on-document/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-on-document/view.js @@ -13,7 +13,11 @@ directive( 'show-mock', ( { directives: { 'show-mock': showMock }, element, evaluate } ) => { const entry = showMock.find( ( { suffix } ) => suffix === null ); - if ( ! evaluate( entry ) ) { + const result = evaluate( entry ); + if ( ! result ) { + return null; + } + if ( typeof result === 'function' && ! result() ) { return null; } return element; diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-on-window/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-on-window/view.js index ef72e266e10759..0c29b09e5a70c9 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-on-window/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-on-window/view.js @@ -13,7 +13,11 @@ directive( 'show-mock', ( { directives: { 'show-mock': showMock }, element, evaluate } ) => { const entry = showMock.find( ( { suffix } ) => suffix === null ); - if ( ! evaluate( entry ) ) { + const result = evaluate( entry ); + if ( ! result ) { + return null; + } + if ( typeof result === 'function' && ! result() ) { return null; } return element; diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-priorities/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-priorities/view.js index 77f2f25c5f9a41..dd4cad1c32ed65 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-priorities/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-priorities/view.js @@ -58,10 +58,13 @@ directive( */ directive( 'test-attribute', ( { evaluate, element } ) => { executionProof( 'attribute' ); - const attributeValue = evaluate( { + let attributeValue = evaluate( { namespace, value: 'context.attribute', } ); + if ( typeof attributeValue === 'function' ) { + attributeValue = attributeValue(); + } useEffect( () => { element.ref.current.setAttribute( 'data-attribute', attributeValue ); }, [] ); @@ -76,7 +79,10 @@ directive( 'test-text', ( { evaluate, element } ) => { executionProof( 'text' ); - const textValue = evaluate( { namespace, value: 'context.text' } ); + let textValue = evaluate( { namespace, value: 'context.text' } ); + if ( typeof textValue === 'function' ) { + textValue = textValue(); + } element.props.children = h( 'p', { 'data-testid': 'text' }, textValue ); }, { priority: 12 } @@ -92,10 +98,22 @@ directive( ( { evaluate, element } ) => { executionProof( 'children' ); const updateAttribute = () => { - evaluate( { namespace, value: 'actions.updateAttribute' } ); + const result = evaluate( { + namespace, + value: 'actions.updateAttribute', + } ); + if ( typeof result === 'function' ) { + result(); + } }; const updateText = () => { - evaluate( { namespace, value: 'actions.updateText' } ); + const result = evaluate( { + namespace, + value: 'actions.updateText', + } ); + if ( typeof result === 'function' ) { + result(); + } }; element.props.children = h( 'div', diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-run/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-run/view.js index 125ac392042306..3b623baa43a099 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-run/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-run/view.js @@ -22,9 +22,11 @@ directive( evaluate, } ) => { const entry = showChildren.find( ( { suffix } ) => suffix === null ); - return evaluate( entry ) - ? element - : cloneElement( element, { children: null } ); + let result = evaluate( entry ); + if ( typeof result === 'function' ) { + result = result(); + } + return result ? element : cloneElement( element, { children: null } ); }, { priority: 9 } ); diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-watch/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-watch/view.js index ad035811a0bcd7..bb533ef9a208ac 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-watch/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-watch/view.js @@ -13,7 +13,11 @@ directive( 'show-mock', ( { directives: { 'show-mock': showMock }, element, evaluate } ) => { const entry = showMock.find( ( { suffix } ) => suffix === null ); - if ( ! evaluate( entry ) ) { + const result = evaluate( entry ); + if ( ! result ) { + return null; + } + if ( typeof result === 'function' && ! result() ) { return null; } return element; diff --git a/packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.js b/packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.js index 83f016e2eac16a..d9eb2005cef885 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.js @@ -1,17 +1,22 @@ /** * WordPress dependencies */ -import { store, getContext, getServerContext } from '@wordpress/interactivity'; +import { + store, + getContext, + getServerContext, + withSyncEvent, +} from '@wordpress/interactivity'; store( 'test/get-server-context', { actions: { - *navigate( e ) { + navigate: withSyncEvent( function* ( e ) { e.preventDefault(); const { actions } = yield import( '@wordpress/interactivity-router' ); yield actions.navigate( e.target.href ); - }, + } ), attemptModification() { try { getServerContext().prop = 'updated from client'; diff --git a/packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.js b/packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.js index db2992ec4a5863..23cd0c328aee60 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.js @@ -1,17 +1,22 @@ /** * WordPress dependencies */ -import { store, getServerState, getContext } from '@wordpress/interactivity'; +import { + store, + getServerState, + getContext, + withSyncEvent, +} from '@wordpress/interactivity'; const { state } = store( 'test/get-server-state', { actions: { - *navigate( e ) { + navigate: withSyncEvent( function* ( e ) { e.preventDefault(); const { actions } = yield import( '@wordpress/interactivity-router' ); yield actions.navigate( e.target.href ); - }, + } ), attemptModification() { try { getServerState().prop = 'updated from client'; diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/view.js b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/view.js index bd1d6e11647799..266a989ada7397 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/view.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { store } from '@wordpress/interactivity'; +import { store, withSyncEvent } from '@wordpress/interactivity'; const { state } = store( 'router', { state: { @@ -18,7 +18,7 @@ const { state } = store( 'router', { }, }, actions: { - *navigate( e ) { + navigate: withSyncEvent( function* ( e ) { e.preventDefault(); state.navigations.count += 1; @@ -38,7 +38,7 @@ const { state } = store( 'router', { if ( state.navigations.pending === 0 ) { state.status = 'idle'; } - }, + } ), toggleTimeout() { state.timeout = state.timeout === 10000 ? 0 : 10000; }, diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-regions/view.js b/packages/e2e-tests/plugins/interactive-blocks/router-regions/view.js index f3468eb88aff01..a3a35d792755c2 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/router-regions/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/router-regions/view.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { store, getContext } from '@wordpress/interactivity'; +import { store, getContext, withSyncEvent } from '@wordpress/interactivity'; const { state } = store( 'router-regions', { state: { @@ -17,13 +17,13 @@ const { state } = store( 'router-regions', { }, actions: { router: { - *navigate( e ) { + navigate: withSyncEvent( function* ( e ) { e.preventDefault(); const { actions } = yield import( '@wordpress/interactivity-router' ); yield actions.navigate( e.target.href ); - }, + } ), back() { history.back(); }, diff --git a/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/view.js b/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/view.js index 8016e931624a16..b4fc12a91a4f20 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/view.js @@ -14,7 +14,11 @@ directive( ( { directives: { 'show-mock': showMock }, element, evaluate } ) => { const entry = showMock.find( ( { suffix } ) => suffix === null ); - if ( ! evaluate( entry ) ) { + let result = evaluate( entry ); + if ( typeof result === 'function' ) { + result = result(); + } + if ( ! result ) { element.props.children = h( 'template', null, diff --git a/packages/interactivity-router/README.md b/packages/interactivity-router/README.md index b79e6b310e2399..3491ad3b459a47 100644 --- a/packages/interactivity-router/README.md +++ b/packages/interactivity-router/README.md @@ -17,12 +17,13 @@ The package is intended to be imported dynamically in the `view.js` files of int ```js /* view.js */ -import { store } from '@wordpress/interactivity'; +import { store, withSyncEvent } from '@wordpress/interactivity'; // This is how you would typically use the navigate() action in your block. store( 'my-namespace/myblock', { actions: { - *goToPage( e ) { + // The withSyncEvent() utility needs to be used because preventDefault() requires synchronous event access. + goToPage: withSyncEvent( function* ( e ) { e.preventDefault(); // We import the package dynamically to reduce the initial JS bundle size. @@ -31,7 +32,7 @@ store( 'my-namespace/myblock', { '@wordpress/interactivity-router' ); yield actions.navigate( e.target.href ); - }, + } ), }, } ); ``` diff --git a/packages/interactivity/src/directives.tsx b/packages/interactivity/src/directives.tsx index bddd017b1c99db..4568eba013c3bd 100644 --- a/packages/interactivity/src/directives.tsx +++ b/packages/interactivity/src/directives.tsx @@ -50,6 +50,54 @@ function deepClone< T >( source: T ): T { return source; } +/** + * Wraps event object to warn about access of synchronous properties and methods. + * + * For all store actions attached to an event listener the event object is proxied via this function, unless the action + * uses the `withSyncEvent()` utility to indicate that it requires synchronous access to the event object. + * + * At the moment, the proxied event only emits warnings when synchronous properties or methods are being accessed. In + * the future this will be changed and result in an error. The current temporary behavior allows implementers to update + * their relevant actions to use `withSyncEvent()`. + * + * For additional context, see https://github.com/WordPress/gutenberg/issues/64944. + * + * @param event Event object. + * @return Proxied event object. + */ +function wrapEventAsync( event: Event ) { + const handler = { + get( target: Event, prop: string | symbol, receiver: any ) { + const value = target[ prop ]; + switch ( prop ) { + case 'currentTarget': + warn( + `Accessing the synchronous event.${ prop } property in a store action without wrapping it in withSyncEvent() is deprecated and will stop working in WordPress 6.9. Please wrap the store action in withSyncEvent().` + ); + break; + case 'preventDefault': + case 'stopImmediatePropagation': + case 'stopPropagation': + warn( + `Using the synchronous event.${ prop }() function in a store action without wrapping it in withSyncEvent() is deprecated and will stop working in WordPress 6.9. Please wrap the store action in withSyncEvent().` + ); + break; + } + if ( value instanceof Function ) { + return function ( this: any, ...args: any[] ) { + return value.apply( + this === receiver ? target : this, + args + ); + }; + } + return value; + }, + }; + + return new Proxy( event, handler ); +} + const newRule = /(?:([\u0080-\uFFFF\w-%@]+) *:? *([^{;]+?);|([^;}{]*?) *{)|(}\s*)/g; const ruleClean = /\/\*[^]*?\*\/| +/g; @@ -102,7 +150,15 @@ const getGlobalEventDirective = ( .forEach( ( entry ) => { const eventName = entry.suffix.split( '--', 1 )[ 0 ]; useInit( () => { - const cb = ( event: Event ) => evaluate( entry, event ); + const cb = ( event: Event ) => { + const result = evaluate( entry ); + if ( typeof result === 'function' ) { + if ( ! result?.sync ) { + event = wrapEventAsync( event ); + } + result( event ); + } + }; const globalVar = type === 'window' ? window : document; globalVar.addEventListener( eventName, cb ); return () => globalVar.removeEventListener( eventName, cb ); @@ -128,7 +184,10 @@ const getGlobalAsyncEventDirective = ( useInit( () => { const cb = async ( event: Event ) => { await splitTask(); - evaluate( entry, event ); + const result = evaluate( entry ); + if ( typeof result === 'function' ) { + result( event ); + } }; const globalVar = type === 'window' ? window : document; globalVar.addEventListener( eventName, cb, { @@ -206,7 +265,10 @@ export default () => { start = performance.now(); } } - const result = evaluate( entry ); + let result = evaluate( entry ); + if ( typeof result === 'function' ) { + result = result(); + } if ( globalThis.IS_GUTENBERG_PLUGIN ) { if ( globalThis.SCRIPT_DEBUG ) { performance.measure( @@ -239,7 +301,10 @@ export default () => { start = performance.now(); } } - const result = evaluate( entry ); + let result = evaluate( entry ); + if ( typeof result === 'function' ) { + result = result(); + } if ( globalThis.IS_GUTENBERG_PLUGIN ) { if ( globalThis.SCRIPT_DEBUG ) { performance.measure( @@ -286,7 +351,13 @@ export default () => { start = performance.now(); } } - evaluate( entry, event ); + const result = evaluate( entry ); + if ( typeof result === 'function' ) { + if ( ! result?.sync ) { + event = wrapEventAsync( event ); + } + result( event ); + } if ( globalThis.IS_GUTENBERG_PLUGIN ) { if ( globalThis.SCRIPT_DEBUG ) { performance.measure( @@ -332,7 +403,10 @@ export default () => { } entries.forEach( async ( entry ) => { await splitTask(); - evaluate( entry, event ); + const result = evaluate( entry ); + if ( typeof result === 'function' ) { + result( event ); + } } ); }; } ); @@ -360,7 +434,10 @@ export default () => { .filter( isNonDefaultDirectiveSuffix ) .forEach( ( entry ) => { const className = entry.suffix; - const result = evaluate( entry ); + let result = evaluate( entry ); + if ( typeof result === 'function' ) { + result = result(); + } const currentClass = element.props.class || ''; const classFinder = new RegExp( `(^|\\s)${ className }(\\s|$)`, @@ -400,7 +477,10 @@ export default () => { directive( 'style', ( { directives: { style }, element, evaluate } ) => { style.filter( isNonDefaultDirectiveSuffix ).forEach( ( entry ) => { const styleProp = entry.suffix; - const result = evaluate( entry ); + let result = evaluate( entry ); + if ( typeof result === 'function' ) { + result = result(); + } element.props.style = element.props.style || {}; if ( typeof element.props.style === 'string' ) { element.props.style = cssStringToObject( element.props.style ); @@ -434,7 +514,10 @@ export default () => { directive( 'bind', ( { directives: { bind }, element, evaluate } ) => { bind.filter( isNonDefaultDirectiveSuffix ).forEach( ( entry ) => { const attribute = entry.suffix; - const result = evaluate( entry ); + let result = evaluate( entry ); + if ( typeof result === 'function' ) { + result = result(); + } element.props[ attribute ] = result; /* @@ -535,7 +618,10 @@ export default () => { } try { - const result = evaluate( entry ); + let result = evaluate( entry ); + if ( typeof result === 'function' ) { + result = result(); + } element.props.children = typeof result === 'object' ? null : result.toString(); } catch ( e ) { @@ -545,7 +631,13 @@ export default () => { // data-wp-run directive( 'run', ( { directives: { run }, evaluate } ) => { - run.forEach( ( entry ) => evaluate( entry ) ); + run.forEach( ( entry ) => { + let result = evaluate( entry ); + if ( typeof result === 'function' ) { + result = result(); + } + return result; + } ); } ); // data-wp-each--[item] @@ -567,7 +659,10 @@ export default () => { const [ entry ] = each; const { namespace } = entry; - const iterable = evaluate( entry ); + let iterable = evaluate( entry ); + if ( typeof iterable === 'function' ) { + iterable = iterable(); + } if ( typeof iterable?.[ Symbol.iterator ] !== 'function' ) { return; diff --git a/packages/interactivity/src/hooks.tsx b/packages/interactivity/src/hooks.tsx index 7899e3eafd2281..3d75fb03aa728c 100644 --- a/packages/interactivity/src/hooks.tsx +++ b/packages/interactivity/src/hooks.tsx @@ -231,6 +231,7 @@ const resolve = ( path: string, namespace: string ) => { // Generate the evaluate function. export const getEvaluate: GetEvaluate = ( { scope } ) => + // TODO: When removing the temporarily remaining `value( ...args )` call below, remove the `...args` parameter too. ( entry, ...args ) => { let { value: path, namespace } = entry; if ( typeof path !== 'string' ) { @@ -241,7 +242,29 @@ export const getEvaluate: GetEvaluate = path[ 0 ] === '!' && !! ( path = path.slice( 1 ) ); setScope( scope ); const value = resolve( path, namespace ); - const result = typeof value === 'function' ? value( ...args ) : value; + // Functions are returned without invoking them. + if ( typeof value === 'function' ) { + // Except if they have a negation operator present, for backward compatibility. + // This pattern is strongly discouraged and deprecated, and it will be removed in a near future release. + // TODO: Remove this condition to effectively ignore negation operator when provided with a function. + if ( hasNegationOperator ) { + warn( + 'Using a function with a negation operator is deprecated and will stop working in WordPress 6.9. Please use derived state instead.' + ); + const functionResult = ! value( ...args ); + resetScope(); + return functionResult; + } + // Reset scope before return and wrap the function so it will still run within the correct scope. + resetScope(); + return ( ...functionArgs: any[] ) => { + setScope( scope ); + const functionResult = value( ...functionArgs ); + resetScope(); + return functionResult; + }; + } + const result = value; resetScope(); return hasNegationOperator ? ! result : result; }; diff --git a/packages/interactivity/src/index.ts b/packages/interactivity/src/index.ts index 9d013e4e744ed5..b7d68fd2007055 100644 --- a/packages/interactivity/src/index.ts +++ b/packages/interactivity/src/index.ts @@ -27,6 +27,7 @@ export { useCallback, useMemo, splitTask, + withSyncEvent, } from './utils'; export { useState, useRef } from 'preact/hooks'; diff --git a/packages/interactivity/src/utils.ts b/packages/interactivity/src/utils.ts index d894d37a7b84bc..7069088a8836be 100644 --- a/packages/interactivity/src/utils.ts +++ b/packages/interactivity/src/utils.ts @@ -30,6 +30,10 @@ declare global { } } +interface SyncAwareFunction extends Function { + sync?: boolean; +} + /** * Executes a callback function after the next frame is rendered. * @@ -135,11 +139,14 @@ export function withScope< ? Promise< Return > : never; export function withScope< Func extends Function >( func: Func ): Func; +export function withScope< Func extends SyncAwareFunction >( func: Func ): Func; export function withScope( func: ( ...args: unknown[] ) => unknown ) { const scope = getScope(); const ns = getNamespace(); + + let wrapped: Function; if ( func?.constructor?.name === 'GeneratorFunction' ) { - return async ( ...args: Parameters< typeof func > ) => { + wrapped = async ( ...args: Parameters< typeof func > ) => { const gen = func( ...args ) as Generator; let value: any; let it: any; @@ -171,17 +178,28 @@ export function withScope( func: ( ...args: unknown[] ) => unknown ) { return value; }; + } else { + wrapped = ( ...args: Parameters< typeof func > ) => { + setNamespace( ns ); + setScope( scope ); + try { + return func( ...args ); + } finally { + resetNamespace(); + resetScope(); + } + }; } - return ( ...args: Parameters< typeof func > ) => { - setNamespace( ns ); - setScope( scope ); - try { - return func( ...args ); - } finally { - resetNamespace(); - resetScope(); - } - }; + + // If function was annotated via `withSyncEvent()`, maintain the annotation. + const syncAware = func as SyncAwareFunction; + if ( syncAware.sync ) { + const syncAwareWrapped = wrapped as SyncAwareFunction; + syncAwareWrapped.sync = true; + return syncAwareWrapped; + } + + return wrapped; } /** @@ -374,3 +392,15 @@ export const isPlainObject = ( typeof candidate === 'object' && candidate.constructor === Object ); + +/** + * Indicates that the passed `callback` requires synchronous access to the event object. + * + * @param callback The event callback. + * @return Altered event callback. + */ +export function withSyncEvent( callback: Function ): SyncAwareFunction { + const syncAware = callback as SyncAwareFunction; + syncAware.sync = true; + return syncAware; +} From afd4d376fe08fdd1825bc9ee6017952c1e744e37 Mon Sep 17 00:00:00 2001 From: Shail Mehta Date: Fri, 21 Feb 2025 15:03:24 +0530 Subject: [PATCH 15/28] Post Comments Count: Add Border Support (#68223) * Post Comments Count: Add Border Support Co-authored-by: shail-mehta Co-authored-by: carolinan --- .../block-library/src/post-comments-count/block.json | 9 ++++++++- .../block-library/src/post-comments-count/style.scss | 4 ++++ packages/block-library/src/style.scss | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 packages/block-library/src/post-comments-count/style.scss diff --git a/packages/block-library/src/post-comments-count/block.json b/packages/block-library/src/post-comments-count/block.json index 796e6e3830236c..4b195a8c928e6f 100644 --- a/packages/block-library/src/post-comments-count/block.json +++ b/packages/block-library/src/post-comments-count/block.json @@ -39,8 +39,15 @@ "fontSize": true } }, + "__experimentalBorder": { + "radius": true, + "color": true, + "width": true, + "style": true + }, "interactivity": { "clientNavigation": true } - } + }, + "style": "wp-block-post-comments-count" } diff --git a/packages/block-library/src/post-comments-count/style.scss b/packages/block-library/src/post-comments-count/style.scss new file mode 100644 index 00000000000000..c596ec200ab1fc --- /dev/null +++ b/packages/block-library/src/post-comments-count/style.scss @@ -0,0 +1,4 @@ +.wp-block-post-comments-count { + // This block has customizable padding, border-box makes that more predictable. + box-sizing: border-box; +} diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss index c61049c23151b9..3c8b8e623cc4d9 100644 --- a/packages/block-library/src/style.scss +++ b/packages/block-library/src/style.scss @@ -36,6 +36,7 @@ @import "./post-author/style.scss"; @import "./post-author-biography/style.scss"; @import "./post-comments-form/style.scss"; +@import "./post-comments-count/style.scss"; @import "./post-content/style.scss"; @import "./post-comments-link/style.scss"; @import "./post-date/style.scss"; From 563c89102c50c4972791a403d071fa1cac1119c5 Mon Sep 17 00:00:00 2001 From: Kelly Mears Date: Fri, 21 Feb 2025 08:45:05 -0500 Subject: [PATCH 16/28] docs: correction in plugin-sidebar-0.md (#69268) Code samples correctly identify `PluginSidebar` as requiring the `wp-editor` dependency, but the how-to text still references `wp-edit-post`. Co-authored-by: kellymears Co-authored-by: t-hamano --- docs/how-to-guides/plugin-sidebar-0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how-to-guides/plugin-sidebar-0.md b/docs/how-to-guides/plugin-sidebar-0.md index 76ef54bb9d3082..495aa0a1d6b8bd 100644 --- a/docs/how-to-guides/plugin-sidebar-0.md +++ b/docs/how-to-guides/plugin-sidebar-0.md @@ -42,7 +42,7 @@ Add the following code to a JavaScript file called `plugin-sidebar.js` and save } )( window.wp, window.React ); ``` -For this code to work, those utilities need to be available in the browser, so you must specify `wp-plugins`, `wp-edit-post`, and `react` as dependencies of your script. +For this code to work, those utilities need to be available in the browser, so you must specify `wp-plugins`, `wp-editor`, and `react` as dependencies of your script. Here is the PHP code to register your script and specify the dependencies: From b6bb01ab60a3c1d027ef910651bb57fd5b5fc777 Mon Sep 17 00:00:00 2001 From: Huub <50170696+huubl@users.noreply.github.com> Date: Fri, 21 Feb 2025 18:50:21 +0100 Subject: [PATCH 17/28] Allow :focus-visible pseudo-selector to be set in theme.json (#68521) * Allow :focus-visible pseudo-selector to be set in theme.json Co-authored-by: huubl Co-authored-by: carolinan Co-authored-by: audrasjb Co-authored-by: joedolson Co-authored-by: sabernhardt Co-authored-by: afercia Co-authored-by: Bovelett --- backport-changelog/6.8/8261.md | 3 +++ .../themes/global-settings-and-styles.md | 2 +- lib/class-wp-theme-json-gutenberg.php | 13 +++++++++---- schemas/json/theme.json | 4 ++++ 4 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 backport-changelog/6.8/8261.md diff --git a/backport-changelog/6.8/8261.md b/backport-changelog/6.8/8261.md new file mode 100644 index 00000000000000..d125d122cb3a4d --- /dev/null +++ b/backport-changelog/6.8/8261.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/8261 + +* https://github.com/WordPress/gutenberg/pull/68521 diff --git a/docs/how-to-guides/themes/global-settings-and-styles.md b/docs/how-to-guides/themes/global-settings-and-styles.md index 359b36b4ad205b..e0b7653321413d 100644 --- a/docs/how-to-guides/themes/global-settings-and-styles.md +++ b/docs/how-to-guides/themes/global-settings-and-styles.md @@ -1034,7 +1034,7 @@ h3 { {% end %} ##### Element pseudo selectors -Pseudo selectors `:hover`, `:focus`, `:visited`, `:active`, `:link`, `:any-link` are supported by Gutenberg. +Pseudo selectors `:hover`, `:focus`, `:focus-visible`, `:visited`, `:active`, `:link`, `:any-link` are supported by Gutenberg. ```json "elements": { diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index e3186d2d370325..0679dd4283acf3 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -563,7 +563,7 @@ class WP_Theme_JSON_Gutenberg { /** * Defines which pseudo selectors are enabled for which elements. * - * The order of the selectors should be: link, any-link, visited, hover, focus, active. + * The order of the selectors should be: link, any-link, visited, hover, focus, focus-visible, active. * This is to ensure the user action (hover, focus and active) styles have a higher * specificity than the visited styles, which in turn have a higher specificity than * the unvisited styles. @@ -573,10 +573,11 @@ class WP_Theme_JSON_Gutenberg { * * @since 6.1.0 * @since 6.2.0 Added support for `:link` and `:any-link`. + * @since 6.8.0 Added support for `:focus-visible`. */ const VALID_ELEMENT_PSEUDO_SELECTORS = array( - 'link' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':active' ), - 'button' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':active' ), + 'link' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':focus-visible', ':active' ), + 'button' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':focus-visible', ':active' ), ); /** @@ -2931,7 +2932,11 @@ static function ( $split_selector ) use ( $clean_style_variation_selector ) { array_filter( $element_pseudo_allowed, static function ( $pseudo_selector ) use ( $selector ) { - return str_contains( $selector, $pseudo_selector ); + /* + * Check if the pseudo selector is in the current selector, + * ensuring it is not followed by a dash (e.g., :focus should not match :focus-visible). + */ + return preg_match( '/' . preg_quote( $pseudo_selector, '/' ) . '(?!-)/', $selector ) === 1; } ) ); diff --git a/schemas/json/theme.json b/schemas/json/theme.json index 4eec377e3a94b9..158b72e648cc2d 100644 --- a/schemas/json/theme.json +++ b/schemas/json/theme.json @@ -1713,6 +1713,9 @@ ":focus": { "$ref": "#/definitions/stylesPropertiesComplete" }, + ":focus-visible": { + "$ref": "#/definitions/stylesPropertiesComplete" + }, ":hover": { "$ref": "#/definitions/stylesPropertiesComplete" }, @@ -1729,6 +1732,7 @@ ":active", ":any-link", ":focus", + ":focus-visible", ":hover", ":link", ":visited" From 7afecbf57094533c77642bd6dae4754216ca8e84 Mon Sep 17 00:00:00 2001 From: JuanMa Date: Sat, 22 Feb 2025 03:16:47 +0100 Subject: [PATCH 18/28] Update javascript-in-the-block-editor.md - fix link (#62431) * Update javascript-in-the-block-editor.md - fix link * Fix Links in remaining two files * Revert Changes in javascript-in-the-block-editor.md file * Added Suggested Changes Co-authored-by: juanmaguitar Co-authored-by: shail-mehta Co-authored-by: t-hamano --------- Co-authored-by: shail-mehta --- docs/getting-started/README.md | 2 +- .../fundamentals/javascript-in-the-block-editor.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index f1b5999ae31472..f37316e3dcc521 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -29,7 +29,7 @@ The WordPress project, and Gutenberg in particular, iterates quickly. Staying up ## Additional resources -For more resources on block development and extending the Block Editor, review the additional sections here in the Block Editor Handbook. Further practical examples are also available in the [block-development-examples](https://github.com/wptrainingteam/block-development-examples) GitHub repository. +For more resources on block development and extending the Block Editor, review the additional sections here in the Block Editor Handbook. Further practical examples are also available in the [block-development-examples](https://github.com/WordPress/block-development-examples) GitHub repository. If you are looking for more educational content, check out [Learn WordPress](https://learn.wordpress.org/), where you can find [tutorials](https://learn.wordpress.org/tutorials/), [courses](https://learn.wordpress.org/courses/), and [online workshops](https://learn.wordpress.org/online-workshops/). Here is a selection of current offerings: diff --git a/docs/getting-started/fundamentals/javascript-in-the-block-editor.md b/docs/getting-started/fundamentals/javascript-in-the-block-editor.md index 4cd7c0b36fe86a..717626af5437e8 100644 --- a/docs/getting-started/fundamentals/javascript-in-the-block-editor.md +++ b/docs/getting-started/fundamentals/javascript-in-the-block-editor.md @@ -75,7 +75,7 @@ wp.blocks.registerBlockVariation( For scripts that need to run in the Block Editor, make sure you use the [`enqueue_block_editor_assets`](https://developer.wordpress.org/reference/hooks/enqueue_block_editor_assets/) hook coupled with the standard [`wp_enqueue_script`](https://developer.wordpress.org/reference/functions/wp_enqueue_script/) function. -Refer to [Enqueueing assets in the Editor](/docs/how-to-guides/enqueueing-assets-in-the-editor.md) for more information. You can also visit the [block-development-example](https://github.com/wptrainingteam/block-theme-examples/blob/master/example-block-variation/functions.php) GitHub repository for more practical examples. +Refer to [Enqueueing assets in the Editor](/docs/how-to-guides/enqueueing-assets-in-the-editor.md) for more information.
Open your browser's dev tools and try running wp.data.select('core/editor').getBlocks() in the console when editing a post or when using the Site Editor. This command will return all available blocks. From c3151e8f6209d60e24a21505cf08d8698655e841 Mon Sep 17 00:00:00 2001 From: Eshaan Dabasiya <76681468+im3dabasia@users.noreply.github.com> Date: Sun, 23 Feb 2025 12:16:09 +0530 Subject: [PATCH 19/28] Dataviews: Fix alignment issue of "Title" column header (#68840) * fix: Alignment issue of first header in dataviews * fix: Remove span parent of title field * fix: Remove inline css and remove extra css code Co-authored-by: im3dabasia Co-authored-by: t-hamano Co-authored-by: hbhalodia Co-authored-by: ecgan --- .../src/dataviews-layouts/table/index.tsx | 42 +++++++------------ .../src/dataviews-layouts/table/style.scss | 4 +- 2 files changed, 18 insertions(+), 28 deletions(-) diff --git a/packages/dataviews/src/dataviews-layouts/table/index.tsx b/packages/dataviews/src/dataviews-layouts/table/index.tsx index 8e69e7353ce921..b3789df8fbb68a 100644 --- a/packages/dataviews/src/dataviews-layouts/table/index.tsx +++ b/packages/dataviews/src/dataviews-layouts/table/index.tsx @@ -140,12 +140,7 @@ function TableRow< Item >( { } } > { hasBulkActions && ( - +
( { { hasBulkActions && ( ( { ) } { hasPrimaryColumn && ( - - { titleField && ( - - ) } - + { titleField && ( + + ) } ) } { columns.map( ( column, index ) => { diff --git a/packages/dataviews/src/dataviews-layouts/table/style.scss b/packages/dataviews/src/dataviews-layouts/table/style.scss index 5a4ac01b566f74..02ccbe4fed7bdd 100644 --- a/packages/dataviews/src/dataviews-layouts/table/style.scss +++ b/packages/dataviews/src/dataviews-layouts/table/style.scss @@ -24,6 +24,7 @@ &.dataviews-view-table__checkbox-column { padding-right: 0; + width: 1%; } } tr { @@ -37,8 +38,7 @@ th:first-child { padding-left: $grid-unit-60; - .dataviews-view-table-header-button, - .dataviews-view-table-header { + .dataviews-view-table-header-button { margin-left: - #{$grid-unit-10}; } } From cca7615f3bb1727b1de920c4514a4734ca33f240 Mon Sep 17 00:00:00 2001 From: Rishit Gupta Date: Sun, 23 Feb 2025 13:30:13 +0530 Subject: [PATCH 20/28] Video Block: Disable autoplay when video is not muted (#69232) Co-authored-by: Rishit30G Co-authored-by: Mamaduka Co-authored-by: hanneslsm Co-authored-by: yogeshbhutkar Co-authored-by: Infinite-Null Co-authored-by: carolinan --- .../block-library/src/video/edit-common-settings.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/video/edit-common-settings.js b/packages/block-library/src/video/edit-common-settings.js index 96d59d7e2f7269..312586259efe14 100644 --- a/packages/block-library/src/video/edit-common-settings.js +++ b/packages/block-library/src/video/edit-common-settings.js @@ -22,6 +22,7 @@ const VideoSettings = ( { setAttributes, attributes } ) => { const autoPlayHelpText = __( 'Autoplay may cause usability issues for some users.' ); + const getAutoplayHelp = Platform.select( { web: useCallback( ( checked ) => { return checked ? autoPlayHelpText : null; @@ -32,7 +33,11 @@ const VideoSettings = ( { setAttributes, attributes } ) => { const toggleFactory = useMemo( () => { const toggleAttribute = ( attribute ) => { return ( newValue ) => { - setAttributes( { [ attribute ]: newValue } ); + setAttributes( { + [ attribute ]: newValue, + // Set muted when autoplay changes + ...( attribute === 'autoplay' && { muted: newValue } ), + } ); }; }; @@ -56,7 +61,7 @@ const VideoSettings = ( { setAttributes, attributes } ) => { isShownByDefault hasValue={ () => !! autoplay } onDeselect={ () => { - setAttributes( { autoplay: false } ); + setAttributes( { autoplay: false, muted: false } ); } } > { label={ __( 'Muted' ) } onChange={ toggleFactory.muted } checked={ !! muted } + disabled={ autoplay } + help={ + autoplay ? __( 'Muted because of Autoplay.' ) : null + } /> Date: Mon, 24 Feb 2025 06:04:32 +0100 Subject: [PATCH 21/28] Post formats: Make title and description human readable (#69275) * Post formats: Make title and description human readable Co-authored-by: carolinan Co-authored-by: t-hamano Co-authored-by: Mamaduka --- .../wordpress-6.8/block-template-utils.php | 29 +++++++++++++++++++ lib/load.php | 1 + 2 files changed, 30 insertions(+) create mode 100644 lib/compat/wordpress-6.8/block-template-utils.php diff --git a/lib/compat/wordpress-6.8/block-template-utils.php b/lib/compat/wordpress-6.8/block-template-utils.php new file mode 100644 index 00000000000000..6e792f5be886eb --- /dev/null +++ b/lib/compat/wordpress-6.8/block-template-utils.php @@ -0,0 +1,29 @@ + $post_format_name ) { + $default_template_types[ 'taxonomy-post_format-post-format-' . $post_format_slug ] = array( + 'title' => sprintf( + /* translators: %s: Post format name. */ + _x( 'Post Format: %s', 'Template name' ), + $post_format_name + ), + 'description' => sprintf( + /* translators: %s: Post format name. */ + __( 'Displays the %s post format archive.' ), + $post_format_name + ), + ); + } + return $default_template_types; +} + +add_filter( 'default_template_types', 'gutenberg_post_format_template_title_description', 10, 1 ); diff --git a/lib/load.php b/lib/load.php index 789c9f9980e9a3..769066111fb95a 100644 --- a/lib/load.php +++ b/lib/load.php @@ -104,6 +104,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.8/post.php'; require __DIR__ . '/compat/wordpress-6.8/site-editor.php'; require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-rest-user-controller.php'; +require __DIR__ . '/compat/wordpress-6.8/block-template-utils.php'; // Experimental features. require __DIR__ . '/experimental/block-editor-settings-mobile.php'; From 74a2ba4e81805ad6e9867f1051e434e6b2e470f2 Mon Sep 17 00:00:00 2001 From: Eshaan Dabasiya <76681468+im3dabasia@users.noreply.github.com> Date: Mon, 24 Feb 2025 18:58:22 +0530 Subject: [PATCH 22/28] TextControl: Fix text direction for URL and email fields in block editor for RTL languages (#68561) * fix: add ltr for email and url in the textcontrol component * doc: Add changelog * doc: Add changelog in unreleased section * doc: Update changelog Co-authored-by: im3dabasia Co-authored-by: t-hamano --- packages/block-library/src/form/edit.js | 2 ++ packages/block-library/src/navigation-link/edit.js | 1 + packages/block-library/src/navigation-submenu/edit.js | 1 + packages/components/CHANGELOG.md | 5 +++++ packages/components/src/text-control/style.scss | 6 ++++++ 5 files changed, 15 insertions(+) diff --git a/packages/block-library/src/form/edit.js b/packages/block-library/src/form/edit.js index a7260afabedf39..227f5ea05014ff 100644 --- a/packages/block-library/src/form/edit.js +++ b/packages/block-library/src/form/edit.js @@ -123,6 +123,7 @@ const Edit = ( { attributes, setAttributes, clientId } ) => { help={ __( 'The email address where form submissions will be sent. Separate multiple email addresses with a comma.' ) } + type="email" /> ) } @@ -159,6 +160,7 @@ const Edit = ( { attributes, setAttributes, clientId } ) => { help={ __( 'The URL where the form should be submitted.' ) } + type="url" /> ) } diff --git a/packages/block-library/src/navigation-link/edit.js b/packages/block-library/src/navigation-link/edit.js index 11a281a44a76d7..e3018c274ef787 100644 --- a/packages/block-library/src/navigation-link/edit.js +++ b/packages/block-library/src/navigation-link/edit.js @@ -201,6 +201,7 @@ function Controls( { attributes, setAttributes, setIsLabelFieldFocused } ) { ); } } autoComplete="off" + type="url" /> diff --git a/packages/block-library/src/navigation-submenu/edit.js b/packages/block-library/src/navigation-submenu/edit.js index b5f40ffb676770..00ee02c50920f9 100644 --- a/packages/block-library/src/navigation-submenu/edit.js +++ b/packages/block-library/src/navigation-submenu/edit.js @@ -431,6 +431,7 @@ export default function NavigationSubmenuEdit( { } } label={ __( 'Link' ) } autoComplete="off" + type="url" /> diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 6917427ef15716..5cdf14f89937f1 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,7 +2,12 @@ ## Unreleased +### Bug Fixes + +- `TextControl`: Ensures email and url inputs have consistent LTR alignment in RTL languages ([#68561](https://github.com/WordPress/gutenberg/pull/68561)). + ### Enhancement + - `BorderControlDropdown`, `BorderControl`: Reset button is always visible. ([#69066](https://github.com/WordPress/gutenberg/pull/69066)). ### Internal diff --git a/packages/components/src/text-control/style.scss b/packages/components/src/text-control/style.scss index 5ae2a90b827736..3e59c47532a617 100644 --- a/packages/components/src/text-control/style.scss +++ b/packages/components/src/text-control/style.scss @@ -28,3 +28,9 @@ padding-right: $grid-unit-15; } } + +.components-text-control__input[type="email"], +.components-text-control__input[type="url"] { + /* rtl:ignore */ + direction: ltr; +} From d181ce44648e6af799264ccae0eb48d0237579ba Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Mon, 24 Feb 2025 22:12:55 +0400 Subject: [PATCH 23/28] Editor: Use a stable array ref as fallback value in 'BlockVisibility' (#69288) Co-authored-by: Mamaduka Co-authored-by: t-hamano --- .../src/components/preferences-modal/block-visibility.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/components/preferences-modal/block-visibility.js b/packages/editor/src/components/preferences-modal/block-visibility.js index 8726b114d97480..fc6deab6b5fb58 100644 --- a/packages/editor/src/components/preferences-modal/block-visibility.js +++ b/packages/editor/src/components/preferences-modal/block-visibility.js @@ -14,6 +14,7 @@ import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; const { BlockManager } = unlock( blockEditorPrivateApis ); +const EMPTY_ARRAY = []; export default function BlockVisibility() { const { showBlockTypes, hideBlockTypes } = unlock( @@ -31,7 +32,7 @@ export default function BlockVisibility() { select( editorStore ).getEditorSettings().allowedBlockTypes, hiddenBlockTypes: select( preferencesStore ).get( 'core', 'hiddenBlockTypes' ) ?? - [], + EMPTY_ARRAY, }; }, [] ); From 77da6c24e9cf5d4b06658a471d3daa5cbdd2e475 Mon Sep 17 00:00:00 2001 From: Shail Mehta Date: Tue, 25 Feb 2025 08:51:02 +0530 Subject: [PATCH 24/28] Added php.net/date links (#69280) * Added php.net/date links * Added php.net/date links Co-authored-by: shail-mehta Co-authored-by: t-hamano --- packages/date/README.md | 10 +++++----- packages/date/src/index.js | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/date/README.md b/packages/date/README.md index 4f0a64c24aa713..a7543df01cd631 100644 --- a/packages/date/README.md +++ b/packages/date/README.md @@ -27,7 +27,7 @@ _Related_ _Parameters_ -- _dateFormat_ `string`: PHP-style formatting string. See php.net/date. +- _dateFormat_ `string`: PHP-style formatting string. See [php.net/date](https://www.php.net/manual/en/function.date.php). - _dateValue_ `Moment | Date | string | undefined`: Date object or string, parsable by moment.js. - _timezone_ `string | number | undefined`: Timezone to output result in or a UTC offset. Defaults to timezone from site. @@ -48,7 +48,7 @@ _Related_ _Parameters_ -- _dateFormat_ `string`: PHP-style formatting string. See php.net/date. +- _dateFormat_ `string`: PHP-style formatting string. See [php.net/date](https://www.php.net/manual/en/function.date.php). - _dateValue_ `Moment | Date | string | undefined`: Date object or string, parsable by moment.js. - _timezone_ `string | number | boolean | undefined=`: Timezone to output result in or a UTC offset. Defaults to timezone from site. Notice: `boolean` is effectively deprecated, but still supported for backward compatibility reasons. @@ -62,7 +62,7 @@ Formats a date. Does not alter the date's timezone. _Parameters_ -- _dateFormat_ `string`: PHP-style formatting string. See php.net/date. +- _dateFormat_ `string`: PHP-style formatting string. See [php.net/date](https://www.php.net/manual/en/function.date.php). - _dateValue_ `Moment | Date | string | undefined`: Date object or string, parsable by moment.js. _Returns_ @@ -95,7 +95,7 @@ Formats a date (like `date()` in PHP), in the UTC timezone. _Parameters_ -- _dateFormat_ `string`: PHP-style formatting string. See php.net/date. +- _dateFormat_ `string`: PHP-style formatting string. See [php.net/date](https://www.php.net/manual/en/function.date.php). - _dateValue_ `Moment | Date | string | undefined`: Date object or string, parsable by moment.js. _Returns_ @@ -108,7 +108,7 @@ Formats a date (like `wp_date()` in PHP), translating it into site's locale and _Parameters_ -- _dateFormat_ `string`: PHP-style formatting string. See php.net/date. +- _dateFormat_ `string`: PHP-style formatting string. See [php.net/date](https://www.php.net/manual/en/function.date.php). - _dateValue_ `Moment | Date | string | undefined`: Date object or string, parsable by moment.js. _Returns_ diff --git a/packages/date/src/index.js b/packages/date/src/index.js index 8d8f53fd8bc10f..5af8b5f4780e89 100644 --- a/packages/date/src/index.js +++ b/packages/date/src/index.js @@ -445,7 +445,7 @@ const formatMap = { * Formats a date. Does not alter the date's timezone. * * @param {string} dateFormat PHP-style formatting string. - * See php.net/date. + * See [php.net/date](https://www.php.net/manual/en/function.date.php). * @param {Moment | Date | string | undefined} dateValue Date object or string, * parsable by moment.js. * @@ -487,7 +487,7 @@ export function format( dateFormat, dateValue = new Date() ) { * Formats a date (like `date()` in PHP). * * @param {string} dateFormat PHP-style formatting string. - * See php.net/date. + * See [php.net/date](https://www.php.net/manual/en/function.date.php). * @param {Moment | Date | string | undefined} dateValue Date object or string, parsable * by moment.js. * @param {string | number | undefined} timezone Timezone to output result in or a @@ -508,7 +508,7 @@ export function date( dateFormat, dateValue = new Date(), timezone ) { * Formats a date (like `date()` in PHP), in the UTC timezone. * * @param {string} dateFormat PHP-style formatting string. - * See php.net/date. + * See [php.net/date](https://www.php.net/manual/en/function.date.php). * @param {Moment | Date | string | undefined} dateValue Date object or string, * parsable by moment.js. * @@ -526,7 +526,7 @@ export function gmdate( dateFormat, dateValue = new Date() ) { * behaves like `gmdateI18n`. * * @param {string} dateFormat PHP-style formatting string. - * See php.net/date. + * See [php.net/date](https://www.php.net/manual/en/function.date.php). * @param {Moment | Date | string | undefined} dateValue Date object or string, parsable by * moment.js. * @param {string | number | boolean | undefined=} timezone Timezone to output result in or a @@ -559,7 +559,7 @@ export function dateI18n( dateFormat, dateValue = new Date(), timezone ) { * and using the UTC timezone. * * @param {string} dateFormat PHP-style formatting string. - * See php.net/date. + * See [php.net/date](https://www.php.net/manual/en/function.date.php). * @param {Moment | Date | string | undefined} dateValue Date object or string, * parsable by moment.js. * From 3e6ddf90261a6e25b9b5610dbbe0255a576c9624 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Tue, 25 Feb 2025 13:17:31 +0900 Subject: [PATCH 25/28] Block support: Add server-side processing for ariaLabel (#69096) * Block support: Add server-side processing for ariaLabel * Add backport changelog * Add unit test Co-authored-by: t-hamano Co-authored-by: carolinan Co-authored-by: Mamaduka Co-authored-by: aaronrobertshaw Co-authored-by: joemcgill Co-authored-by: fabiankaegy --- backport-changelog/6.8/8274.md | 3 + lib/block-supports/aria-label.php | 63 ++++++++++++++++ lib/load.php | 1 + phpunit/block-supports/aria-label-test.php | 85 ++++++++++++++++++++++ 4 files changed, 152 insertions(+) create mode 100644 backport-changelog/6.8/8274.md create mode 100644 lib/block-supports/aria-label.php create mode 100644 phpunit/block-supports/aria-label-test.php diff --git a/backport-changelog/6.8/8274.md b/backport-changelog/6.8/8274.md new file mode 100644 index 00000000000000..f841a6f010d6d3 --- /dev/null +++ b/backport-changelog/6.8/8274.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/8274 + +* https://github.com/WordPress/gutenberg/pull/69096 diff --git a/lib/block-supports/aria-label.php b/lib/block-supports/aria-label.php new file mode 100644 index 00000000000000..062ff027bf5947 --- /dev/null +++ b/lib/block-supports/aria-label.php @@ -0,0 +1,63 @@ +attributes ) { + $block_type->attributes = array(); + } + + if ( ! array_key_exists( 'ariaLabel', $block_type->attributes ) ) { + $block_type->attributes['ariaLabel'] = array( + 'type' => 'string', + ); + } +} + +/** + * Add the aria-label to the output. + * + * @param WP_Block_Type $block_type Block Type. + * @param array $block_attributes Block attributes. + * + * @return array Block aria-label. + */ +function gutenberg_apply_aria_label_support( $block_type, $block_attributes ) { + if ( ! $block_attributes ) { + return array(); + } + + $has_aria_label_support = block_has_support( $block_type, array( 'ariaLabel' ), false ); + if ( ! $has_aria_label_support ) { + return array(); + } + + $has_aria_label = array_key_exists( 'ariaLabel', $block_attributes ); + if ( ! $has_aria_label ) { + return array(); + } + return array( 'aria-label' => $block_attributes['ariaLabel'] ); +} + +// Register the block support. +WP_Block_Supports::get_instance()->register( + 'aria-label', + array( + 'register_attribute' => 'gutenberg_register_aria_label_support', + 'apply' => 'gutenberg_apply_aria_label_support', + ) +); diff --git a/lib/load.php b/lib/load.php index 769066111fb95a..25b5189010909d 100644 --- a/lib/load.php +++ b/lib/load.php @@ -178,6 +178,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/block-supports/shadow.php'; require __DIR__ . '/block-supports/background.php'; require __DIR__ . '/block-supports/block-style-variations.php'; +require __DIR__ . '/block-supports/aria-label.php'; // Data views. require_once __DIR__ . '/experimental/data-views.php'; diff --git a/phpunit/block-supports/aria-label-test.php b/phpunit/block-supports/aria-label-test.php new file mode 100644 index 00000000000000..52b14edd5b67ca --- /dev/null +++ b/phpunit/block-supports/aria-label-test.php @@ -0,0 +1,85 @@ +test_block_name = null; + } + + public function tear_down() { + unregister_block_type( $this->test_block_name ); + $this->test_block_name = null; + parent::tear_down(); + } + + /** + * Registers a new block for testing aria-label support. + * + * @param string $block_name Name for the test block. + * @param array $supports Array defining block support configuration. + * + * @return WP_Block_Type The block type for the newly registered test block. + */ + private function register_aria_label_block_with_support( $block_name, $supports = array() ) { + $this->test_block_name = $block_name; + register_block_type( + $this->test_block_name, + array( + 'api_version' => 3, + 'supports' => $supports, + ) + ); + $registry = WP_Block_Type_Registry::get_instance(); + + return $registry->get_registered( $this->test_block_name ); + } + + /** + * Tests that position block support works as expected. + * + * @dataProvider data_aria_label_block_support + * + * @param boolean|array $support Aria label block support configuration. + * @param string $value Aria label value for attribute object. + * @param array $expected Expected aria label block support styles. + */ + public function test_gutenberg_apply_aria_label_support( $support, $value, $expected ) { + $block_type = self::register_aria_label_block_with_support( + 'test/aria-label-block', + array( 'ariaLabel' => $support ) + ); + $block_attrs = array( 'ariaLabel' => $value ); + $actual = gutenberg_apply_aria_label_support( $block_type, $block_attrs ); + + $this->assertSame( $expected, $actual ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_aria_label_block_support() { + return array( + 'aria-label attribute is applied' => array( + 'support' => true, + 'value' => 'Label', + 'expected' => array( 'aria-label' => 'Label' ), + ), + 'aria-label attribute is not applied if block does not support it' => array( + 'support' => false, + 'value' => 'Label', + 'expected' => array(), + ), + ); + } +} From e532b746c40101e2468383ecbfa92cc5ee244069 Mon Sep 17 00:00:00 2001 From: tomoki shimomura Date: Tue, 25 Feb 2025 15:38:43 +0900 Subject: [PATCH 26/28] Format Library: Prevent the text and color picker from overlapping (#69169) Co-authored-by: shimotmk Co-authored-by: Mamaduka --- packages/format-library/src/text-color/inline.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/format-library/src/text-color/inline.js b/packages/format-library/src/text-color/inline.js index bc1e0eef07b0c1..a14240ed9f489d 100644 --- a/packages/format-library/src/text-color/inline.js +++ b/packages/format-library/src/text-color/inline.js @@ -142,6 +142,8 @@ function ColorPicker( { name, property, value, onChange } ) { setColors( value, name, colors, { [ property ]: color } ) ); } } + // Prevent the text and color picker from overlapping. + __experimentalIsRenderedInSidebar /> ); } From 9a495f826ed344359df89b79f26b5759e36451fe Mon Sep 17 00:00:00 2001 From: Ankit Kumar Shah Date: Tue, 25 Feb 2025 13:55:50 +0530 Subject: [PATCH 27/28] Test: Add e2e test for 404 page (#69272) * Add e2e test for 404 page * Improve navigation test * Remove unnecessery `visitSiteEditor()` Co-authored-by: Infinite-Null Co-authored-by: t-hamano Co-authored-by: Mamaduka --- test/e2e/specs/site-editor/navigation.spec.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/e2e/specs/site-editor/navigation.spec.js b/test/e2e/specs/site-editor/navigation.spec.js index 18eb6c9904b449..2a886c2048f886 100644 --- a/test/e2e/specs/site-editor/navigation.spec.js +++ b/test/e2e/specs/site-editor/navigation.spec.js @@ -106,6 +106,23 @@ test.describe( 'Site editor navigation', () => { // We should have our editor canvas button back await expect( editorCanvasButton ).toBeVisible(); } ); + + test( 'Should show 404 page when navigating to non-existent template', async ( { + admin, + page, + } ) => { + // Navigate to a non-existent template. + await admin.visitAdminPage( 'site-editor.php', 'p=/template-foo-bar' ); + + // Verify the 404 error notice is displayed with the correct message. + await expect( + page.locator( + '.edit-site-layout__area .components-notice__content' + ) + ).toHaveText( + 'The requested page could not be found. Please check the URL.' + ); + } ); } ); class EditorNavigationUtils { From db7153125f6dcbd7135ac12af3adc6e56f96109f Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Tue, 25 Feb 2025 14:48:30 +0100 Subject: [PATCH 28/28] Swap fullscreen mode snackbar notice message. (#69305) Co-authored-by: afercia Co-authored-by: carolinan --- packages/edit-post/src/store/actions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/edit-post/src/store/actions.js b/packages/edit-post/src/store/actions.js index 85702ae1622851..959c63083a9d33 100644 --- a/packages/edit-post/src/store/actions.js +++ b/packages/edit-post/src/store/actions.js @@ -530,8 +530,8 @@ export const toggleFullscreenMode = .dispatch( noticesStore ) .createInfoNotice( isFullscreen - ? __( 'Fullscreen mode activated.' ) - : __( 'Fullscreen mode deactivated.' ), + ? __( 'Fullscreen mode deactivated.' ) + : __( 'Fullscreen mode activated.' ), { id: 'core/edit-post/toggle-fullscreen-mode/notice', type: 'snackbar',