From 939c49a70fa5b123c494c22bf507d8a74273cd5b Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Mon, 15 Jan 2024 13:56:14 +0400 Subject: [PATCH 01/18] Try: Measing typing with the top toolbar enabled (#57709) --- .../config/performance-reporter.ts | 3 ++ test/performance/specs/post-editor.spec.js | 34 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/test/performance/config/performance-reporter.ts b/test/performance/config/performance-reporter.ts index 6f9229be866a03..e80f75e92eedd3 100644 --- a/test/performance/config/performance-reporter.ts +++ b/test/performance/config/performance-reporter.ts @@ -27,6 +27,7 @@ export interface WPRawPerformanceResults { firstBlock: number[]; type: number[]; typeWithoutInspector: number[]; + typeWithTopToolbar: number[]; typeContainer: number[]; focus: number[]; inserterOpen: number[]; @@ -51,6 +52,7 @@ export interface WPPerformanceResults { minType?: number; maxType?: number; typeWithoutInspector?: number; + typeWithTopToolbar?: number; typeContainer?: number; minTypeContainer?: number; maxTypeContainer?: number; @@ -97,6 +99,7 @@ export function curateResults( minType: minimum( results.type ), maxType: maximum( results.type ), typeWithoutInspector: average( results.typeWithoutInspector ), + typeWithTopToolbar: average( results.typeWithTopToolbar ), typeContainer: average( results.typeContainer ), minTypeContainer: minimum( results.typeContainer ), maxTypeContainer: maximum( results.typeContainer ), diff --git a/test/performance/specs/post-editor.spec.js b/test/performance/specs/post-editor.spec.js index 105a4bd9a80ca0..e720396afe5dac 100644 --- a/test/performance/specs/post-editor.spec.js +++ b/test/performance/specs/post-editor.spec.js @@ -23,6 +23,7 @@ const results = { firstBlock: [], type: [], typeWithoutInspector: [], + typeWithTopToolbar: [], typeContainer: [], focus: [], listViewOpen: [], @@ -186,6 +187,39 @@ test.describe( 'Post Editor Performance', () => { } ); } ); + test.describe( 'Typing (with top toolbar)', () => { + let draftId = null; + + test( 'Setup the test post', async ( { admin, perfUtils, editor } ) => { + await admin.createNewPost(); + await perfUtils.loadBlocksForLargePost(); + await editor.insertBlock( { name: 'core/paragraph' } ); + draftId = await perfUtils.saveDraft(); + } ); + + test( 'Run the test', async ( { + admin, + perfUtils, + metrics, + editor, + } ) => { + await admin.editPost( draftId ); + await perfUtils.disableAutosave(); + // Enable fixed toolbar. + await editor.setIsFixedToolbar( true ); + const canvas = await perfUtils.getCanvas(); + + const paragraph = canvas.getByRole( 'document', { + name: /Empty block/i, + } ); + + await type( paragraph, metrics, 'typeWithTopToolbar' ); + + // Disabled fixed toolbar. Default state. + await editor.setIsFixedToolbar( false ); + } ); + } ); + test.describe( 'Typing within containers', () => { let draftId = null; From c3d72183a694391a9042417534fff1cfbc5867c2 Mon Sep 17 00:00:00 2001 From: Akira Tachibana Date: Mon, 15 Jan 2024 21:06:09 +0900 Subject: [PATCH 02/18] Remove unused argument from sprintf in pagination.js (#57823) * Remove unused argument from sprintf * Fix for the lint error --- packages/dataviews/src/pagination.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/dataviews/src/pagination.js b/packages/dataviews/src/pagination.js index 2c9cade42d89b7..f69d7c7a12e193 100644 --- a/packages/dataviews/src/pagination.js +++ b/packages/dataviews/src/pagination.js @@ -30,12 +30,8 @@ const Pagination = memo( function Pagination( { { createInterpolateElement( sprintf( - // translators: %1$s: Current page number, %2$s: Total number of pages. - _x( - 'Page of %2$s', - 'paging' - ), - view.page, + // translators: %s: Total number of pages. + _x( 'Page of %s', 'paging' ), totalPages ), { From 075fe63a9e048d8bacabe68cf68d6bea52713cc5 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Mon, 15 Jan 2024 21:40:25 +0900 Subject: [PATCH 03/18] Video Block: add raw transformation from video html (#47159) --- .../block-library/src/video/transforms.js | 32 +++++++++++++++++++ .../api/raw-handling/test/paste-handler.js | 27 ++++++++++++++++ packages/dom/src/phrasing-content.js | 1 + 3 files changed, 60 insertions(+) diff --git a/packages/block-library/src/video/transforms.js b/packages/block-library/src/video/transforms.js index 246032422d643d..6ec1f9096fa86d 100644 --- a/packages/block-library/src/video/transforms.js +++ b/packages/block-library/src/video/transforms.js @@ -63,6 +63,38 @@ const transforms = { }, }, }, + { + type: 'raw', + isMatch: ( node ) => + node.nodeName === 'P' && + node.children.length === 1 && + node.firstChild.nodeName === 'VIDEO', + transform: ( node ) => { + const videoElement = node.firstChild; + const attributes = { + autoplay: videoElement.hasAttribute( 'autoplay' ) + ? true + : undefined, + controls: videoElement.hasAttribute( 'controls' ) + ? undefined + : false, + loop: videoElement.hasAttribute( 'loop' ) + ? true + : undefined, + muted: videoElement.hasAttribute( 'muted' ) + ? true + : undefined, + preload: + videoElement.getAttribute( 'preload' ) || undefined, + playsInline: videoElement.hasAttribute( 'playsinline' ) + ? true + : undefined, + poster: videoElement.getAttribute( 'poster' ) || undefined, + src: videoElement.getAttribute( 'src' ) || undefined, + }; + return createBlock( 'core/video', attributes ); + }, + }, ], }; diff --git a/packages/blocks/src/api/raw-handling/test/paste-handler.js b/packages/blocks/src/api/raw-handling/test/paste-handler.js index 9b3dad39a0a5b8..ffd98fd4dadab7 100644 --- a/packages/blocks/src/api/raw-handling/test/paste-handler.js +++ b/packages/blocks/src/api/raw-handling/test/paste-handler.js @@ -6,6 +6,7 @@ import { pasteHandler } from '@wordpress/blocks'; * Internal dependencies */ import { init as initAndRegisterTableBlock } from '../../../../../block-library/src/table'; +import { init as initAndRegisterVideoBlock } from '../../../../../block-library/src/video'; const tableWithHeaderFooterAndBodyUsingColspan = ` @@ -63,6 +64,7 @@ const tableWithHeaderFooterAndBodyUsingRowspan = ` describe( 'pasteHandler', () => { beforeAll( () => { initAndRegisterTableBlock(); + initAndRegisterVideoBlock(); } ); it( 'can handle a table with thead, tbody and tfoot using colspan', () => { @@ -153,4 +155,29 @@ describe( 'pasteHandler', () => { expect( result.name ).toEqual( 'core/table' ); expect( result.isValid ).toBeTruthy(); } ); + + it( 'can handle a video', () => { + const [ result ] = pasteHandler( { + HTML: '', + tagName: 'p', + preserveWhiteSpace: false, + } ); + + expect( console ).toHaveLogged(); + + delete result.attributes.caption; + expect( result.attributes ).toEqual( { + autoplay: true, + loop: true, + muted: true, + controls: true, + playsInline: true, + preload: 'auto', + poster: 'https://example.com/media.jpg', + src: 'https://example.com/media.mp4', + tracks: [], + } ); + expect( result.name ).toEqual( 'core/video' ); + expect( result.isValid ).toBeTruthy(); + } ); } ); diff --git a/packages/dom/src/phrasing-content.js b/packages/dom/src/phrasing-content.js index 571d081c2843a1..c9ed0981629747 100644 --- a/packages/dom/src/phrasing-content.js +++ b/packages/dom/src/phrasing-content.js @@ -114,6 +114,7 @@ const embeddedContentSchema = { 'src', 'poster', 'preload', + 'playsinline', 'autoplay', 'mediagroup', 'loop', From 32a73d12571b1b3cabbd13391134317428f6df2d Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Mon, 15 Jan 2024 16:56:49 +0400 Subject: [PATCH 04/18] Editor: Use hooks instead of HoCs in 'PostScheduleCheck' (#57833) --- .../src/components/post-schedule/check.js | 24 ++++++-------- .../components/post-schedule/test/check.js | 33 ++++++++++++++----- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/packages/editor/src/components/post-schedule/check.js b/packages/editor/src/components/post-schedule/check.js index 050e5a82e8c455..716d75efc9d407 100644 --- a/packages/editor/src/components/post-schedule/check.js +++ b/packages/editor/src/components/post-schedule/check.js @@ -1,29 +1,25 @@ /** * WordPress dependencies */ -import { compose } from '@wordpress/compose'; -import { withSelect } from '@wordpress/data'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ import { store as editorStore } from '../../store'; -export function PostScheduleCheck( { hasPublishAction, children } ) { +export default function PostScheduleCheck( { children } ) { + const hasPublishAction = useSelect( ( select ) => { + return ( + select( editorStore ).getCurrentPost()._links?.[ + 'wp:action-publish' + ] ?? false + ); + }, [] ); + if ( ! hasPublishAction ) { return null; } return children; } - -export default compose( [ - withSelect( ( select ) => { - const { getCurrentPost, getCurrentPostType } = select( editorStore ); - return { - hasPublishAction: - getCurrentPost()._links?.[ 'wp:action-publish' ] ?? false, - postType: getCurrentPostType(), - }; - } ), -] )( PostScheduleCheck ); diff --git a/packages/editor/src/components/post-schedule/test/check.js b/packages/editor/src/components/post-schedule/test/check.js index 26fc0193ece8d8..b892842e3ef8d7 100644 --- a/packages/editor/src/components/post-schedule/test/check.js +++ b/packages/editor/src/components/post-schedule/test/check.js @@ -3,25 +3,40 @@ */ import { render, screen } from '@testing-library/react'; +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; + /** * Internal dependencies */ -import { PostScheduleCheck } from '../check'; +import PostScheduleCheck from '../check'; + +jest.mock( '@wordpress/data/src/components/use-select', () => jest.fn() ); + +function setupMockSelect( hasPublishAction ) { + useSelect.mockImplementation( ( mapSelect ) => { + return mapSelect( () => ( { + getCurrentPost: () => ( { + _links: { + 'wp:action-publish': hasPublishAction, + }, + } ), + } ) ); + } ); +} describe( 'PostScheduleCheck', () => { it( "should not render anything if the user doesn't have the right capabilities", () => { - render( - - yes - - ); + setupMockSelect( false ); + render( yes ); expect( screen.queryByText( 'yes' ) ).not.toBeInTheDocument(); } ); it( 'should render if the user has the correct capability', () => { - render( - yes - ); + setupMockSelect( true ); + render( yes ); expect( screen.getByText( 'yes' ) ).toBeVisible(); } ); } ); From cb71ab814aef59970f480888a691fd21a9d552bd Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Mon, 15 Jan 2024 17:01:12 +0400 Subject: [PATCH 05/18] Migrate 'block grouping' e2e tests to Playwright (#57684) * Migrate 'block grouping' e2e tests to Playwright * Remove old test files * fix typo Co-authored-by: Bart Kalisz --- .../__snapshots__/block-grouping.test.js.snap | 115 ----- .../editor/various/block-grouping.test.js | 283 ----------- .../editor/various/block-grouping.spec.js | 462 ++++++++++++++++++ 3 files changed, 462 insertions(+), 398 deletions(-) delete mode 100644 packages/e2e-tests/specs/editor/various/__snapshots__/block-grouping.test.js.snap delete mode 100644 packages/e2e-tests/specs/editor/various/block-grouping.test.js create mode 100644 test/e2e/specs/editor/various/block-grouping.spec.js diff --git a/packages/e2e-tests/specs/editor/various/__snapshots__/block-grouping.test.js.snap b/packages/e2e-tests/specs/editor/various/__snapshots__/block-grouping.test.js.snap deleted file mode 100644 index 5ffca68763f42e..00000000000000 --- a/packages/e2e-tests/specs/editor/various/__snapshots__/block-grouping.test.js.snap +++ /dev/null @@ -1,115 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Block Grouping Group creation creates a group from multiple blocks of different types via block transforms 1`] = ` -" -
-

Group Heading

- - - -
- - - -

Some paragraph

-
-" -`; - -exports[`Block Grouping Group creation creates a group from multiple blocks of the same type via block transforms 1`] = ` -" -
-

First Paragraph

- - - -

Second Paragraph

- - - -

Third Paragraph

-
-" -`; - -exports[`Block Grouping Group creation creates a group from multiple blocks of the same type via options toolbar 1`] = ` -" -
-

First Paragraph

- - - -

Second Paragraph

- - - -

Third Paragraph

-
-" -`; - -exports[`Block Grouping Group creation groups and ungroups multiple blocks of different types via options toolbar 1`] = ` -" -
-

Group Heading

- - - -
- - - -

Some paragraph

-
-" -`; - -exports[`Block Grouping Group creation groups and ungroups multiple blocks of different types via options toolbar 2`] = ` -" -

Group Heading

- - - -
- - - -

Some paragraph

-" -`; - -exports[`Block Grouping Preserving selected blocks attributes preserves width alignment settings of selected blocks 1`] = ` -" -
-

Group Heading

- - - -
- - - -
- - - -

Some paragraph

-
-" -`; - -exports[`Block Grouping Registering alternative Blocks to handle Grouping interactions should use registered grouping block for grouping interactions 1`] = ` -" - -

First Paragraph

- - - -

Second Paragraph

- - - -

Third Paragraph

- -" -`; diff --git a/packages/e2e-tests/specs/editor/various/block-grouping.test.js b/packages/e2e-tests/specs/editor/various/block-grouping.test.js deleted file mode 100644 index f67273a550d1c2..00000000000000 --- a/packages/e2e-tests/specs/editor/various/block-grouping.test.js +++ /dev/null @@ -1,283 +0,0 @@ -/** - * WordPress dependencies - */ -import { - insertBlock, - createNewPost, - clickBlockToolbarButton, - clickMenuItem, - pressKeyWithModifier, - getEditedPostContent, - transformBlockTo, - getAllBlocks, - getAvailableBlockTransforms, - activatePlugin, - deactivatePlugin, - createReusableBlock, - canvas, -} from '@wordpress/e2e-test-utils'; - -async function insertBlocksOfSameType() { - await insertBlock( 'Paragraph' ); - await page.keyboard.type( 'First Paragraph' ); - - await insertBlock( 'Paragraph' ); - await page.keyboard.type( 'Second Paragraph' ); - - await insertBlock( 'Paragraph' ); - await page.keyboard.type( 'Third Paragraph' ); -} - -async function insertBlocksOfMultipleTypes() { - await insertBlock( 'Heading' ); - await page.keyboard.type( 'Group Heading' ); - - await insertBlock( 'Image' ); - - await insertBlock( 'Paragraph' ); - await page.keyboard.type( 'Some paragraph' ); -} - -describe( 'Block Grouping', () => { - beforeEach( async () => { - // Posts are auto-removed at the end of each test run. - await createNewPost(); - } ); - - describe( 'Group creation', () => { - it( 'creates a group from multiple blocks of the same type via block transforms', async () => { - // Creating test blocks. - await insertBlocksOfSameType(); - - // Multiselect via keyboard. - await pressKeyWithModifier( 'primary', 'a' ); - await pressKeyWithModifier( 'primary', 'a' ); - - await transformBlockTo( 'Group' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'creates a group from multiple blocks of different types via block transforms', async () => { - // Creating test blocks. - await insertBlocksOfMultipleTypes(); - - // Multiselect via keyboard. - await pressKeyWithModifier( 'primary', 'a' ); - await pressKeyWithModifier( 'primary', 'a' ); - - await transformBlockTo( 'Group' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'creates a group from multiple blocks of the same type via options toolbar', async () => { - // Creating test blocks. - await insertBlocksOfSameType(); - - // Multiselect via keyboard. - await pressKeyWithModifier( 'primary', 'a' ); - await pressKeyWithModifier( 'primary', 'a' ); - - await clickBlockToolbarButton( 'Options' ); - await clickMenuItem( 'Group' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'groups and ungroups multiple blocks of different types via options toolbar', async () => { - // Creating test blocks. - await insertBlocksOfMultipleTypes(); - await pressKeyWithModifier( 'primary', 'a' ); - await pressKeyWithModifier( 'primary', 'a' ); - - // Group. - await clickBlockToolbarButton( 'Options' ); - await clickMenuItem( 'Group' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - - // UnGroup. - await clickBlockToolbarButton( 'Options' ); - await clickMenuItem( 'Ungroup' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'does not allow ungrouping a group block that has no children', async () => { - await insertBlock( 'Group' ); - await clickBlockToolbarButton( 'Options' ); - const ungroupButtons = await page.$x( - '//button/span[text()="Ungroup"]' - ); - expect( ungroupButtons ).toHaveLength( 0 ); - } ); - it( 'should group and ungroup a controlled block properly', async () => { - const getParagraphText = async () => { - const paragraphInReusableSelector = - '.block-editor-block-list__block[data-type="core/block"] p'; - await canvas().waitForSelector( paragraphInReusableSelector ); - return canvas().$eval( - paragraphInReusableSelector, - ( element ) => element.innerText - ); - }; - - const paragraphText = 'hi'; - await createReusableBlock( paragraphText, 'Block' ); - // Group - await clickBlockToolbarButton( 'Options' ); - await clickMenuItem( 'Group' ); - - let group = await canvas().$$( '[data-type="core/group"]' ); - expect( group ).toHaveLength( 1 ); - // Make sure the paragraph in reusable block exists. - expect( await getParagraphText() ).toMatch( paragraphText ); - - await clickBlockToolbarButton( 'Options' ); - await clickMenuItem( 'Ungroup' ); - group = await canvas().$$( '[data-type="core/group"]' ); - expect( group ).toHaveLength( 0 ); - // Make sure the paragraph in reusable block exists. - expect( await getParagraphText() ).toEqual( paragraphText ); - } ); - it( 'should group another Group block via options toolbar', async () => { - await insertBlock( 'Paragraph' ); - await page.keyboard.type( '1' ); - await clickBlockToolbarButton( 'Options' ); - await clickMenuItem( 'Group' ); - await clickBlockToolbarButton( 'Options' ); - await clickMenuItem( 'Group' ); - expect( await getEditedPostContent() ).toMatchInlineSnapshot( ` - " -
-
-

1

-
-
- " - ` ); - } ); - } ); - - describe( 'Grouping Block availability', () => { - beforeEach( async () => { - // Disable the Group block. - await page.evaluate( () => { - const { dispatch } = wp.data; - dispatch( 'core/edit-post' ).hideBlockTypes( [ 'core/group' ] ); - } ); - - // Create a Group. - await insertBlocksOfMultipleTypes(); - await pressKeyWithModifier( 'primary', 'a' ); - await pressKeyWithModifier( 'primary', 'a' ); - } ); - - afterAll( async () => { - // Re-enable the Group block. - await page.evaluate( () => { - const { dispatch } = wp.data; - dispatch( 'core/edit-post' ).showBlockTypes( [ 'core/group' ] ); - } ); - } ); - - it( 'does not show group transform if Grouping block is disabled', async () => { - const availableTransforms = await getAvailableBlockTransforms(); - - expect( availableTransforms ).not.toContain( 'Group' ); - } ); - - it( 'does not show group option in the options toolbar if Grouping block is disabled', async () => { - await clickBlockToolbarButton( 'Options' ); - - const blockOptionsDropdownHTML = await page.evaluate( - () => - document.querySelector( - '.block-editor-block-settings-menu__popover' - ).innerHTML - ); - - expect( blockOptionsDropdownHTML ).not.toContain( 'Group' ); - } ); - } ); - - describe( 'Preserving selected blocks attributes', () => { - it( 'preserves width alignment settings of selected blocks', async () => { - await insertBlock( 'Heading' ); - await page.keyboard.type( 'Group Heading' ); - - // Full width image. - await insertBlock( 'Image' ); - await clickBlockToolbarButton( 'Align' ); - const fullButton = await page.waitForXPath( - `//button[contains(@class,'components-dropdown-menu__menu-item')]//span[contains(text(), 'Full width')]` - ); - await fullButton.evaluate( ( element ) => - element.scrollIntoView() - ); - await fullButton.click(); - - // Wide width image. - await insertBlock( 'Image' ); - await clickBlockToolbarButton( 'Align' ); - const wideButton = await page.waitForXPath( - `//button[contains(@class,'components-dropdown-menu__menu-item')]//span[contains(text(), 'Wide width')]` - ); - await wideButton.evaluate( ( element ) => - element.scrollIntoView() - ); - await wideButton.click(); - - await insertBlock( 'Paragraph' ); - await page.keyboard.type( 'Some paragraph' ); - - await pressKeyWithModifier( 'primary', 'a' ); - await pressKeyWithModifier( 'primary', 'a' ); - - await transformBlockTo( 'Group' ); - - const allBlocks = await getAllBlocks(); - - // We expect Group block align setting to match that - // of the widest of it's "child" innerBlocks - expect( allBlocks[ 0 ].attributes.align ).toBe( 'full' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - } ); - - describe( 'Registering alternative Blocks to handle Grouping interactions', () => { - beforeAll( async () => { - await activatePlugin( 'gutenberg-test-custom-grouping-block' ); - } ); - - afterAll( async () => { - await deactivatePlugin( 'gutenberg-test-custom-grouping-block' ); - } ); - - it( 'should use registered grouping block for grouping interactions', async () => { - // Set custom Block as the Block to use for Grouping. - await page.evaluate( () => { - window.wp.blocks.setGroupingBlockName( - 'test/alternative-group-block' - ); - } ); - - // Creating test blocks. - await insertBlocksOfSameType(); - - // Multiselect via keyboard. - await pressKeyWithModifier( 'primary', 'a' ); - await pressKeyWithModifier( 'primary', 'a' ); - - // Group - this will use whichever Block is registered as the Grouping Block - // as opposed to "transformTo()" which uses whatever is passed to it. To - // ensure this test is meaningful we must rely on what is registered. - await clickBlockToolbarButton( 'Options' ); - await clickMenuItem( 'Group' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - } ); -} ); diff --git a/test/e2e/specs/editor/various/block-grouping.spec.js b/test/e2e/specs/editor/various/block-grouping.spec.js new file mode 100644 index 00000000000000..ec09edc4160d08 --- /dev/null +++ b/test/e2e/specs/editor/various/block-grouping.spec.js @@ -0,0 +1,462 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +/** @typedef {import('@playwright/test').Page} Page */ +/** @typedef {import('@wordpress/e2e-test-utils-playwright').Editor} Editor */ + +test.use( { + groupingUtils: async ( { page, editor }, use ) => { + await use( new GroupingUtils( { page, editor } ) ); + }, +} ); + +test.describe( 'Block Grouping', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test.describe( 'Group creation', () => { + test( 'creates a group from multiple blocks of the same type via block transforms', async ( { + editor, + page, + pageUtils, + groupingUtils, + } ) => { + await groupingUtils.insertBlocksOfSameType(); + + // Multiselect via keyboard. + await pageUtils.pressKeys( 'primary+a', { times: 2 } ); + await page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Multiple blocks selected' } ) + .click(); + await page + .getByRole( 'menu', { name: 'Multiple blocks selected' } ) + .getByRole( 'menuitem', { name: 'Group' } ) + .click(); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/group', + innerBlocks: [ + { + name: 'core/paragraph', + attributes: { content: 'First Paragraph' }, + }, + { + name: 'core/paragraph', + attributes: { content: 'Second Paragraph' }, + }, + { + name: 'core/paragraph', + attributes: { content: 'Third Paragraph' }, + }, + ], + }, + ] ); + } ); + + test( 'creates a group from multiple blocks of different types via block transforms', async ( { + editor, + page, + pageUtils, + groupingUtils, + } ) => { + await groupingUtils.insertBlocksOfMultipleTypes(); + + // Multiselect via keyboard. + await pageUtils.pressKeys( 'primary+a', { times: 2 } ); + await page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Multiple blocks selected' } ) + .click(); + await page + .getByRole( 'menu', { name: 'Multiple blocks selected' } ) + .getByRole( 'menuitem', { name: 'Group' } ) + .click(); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/group', + innerBlocks: [ + { + name: 'core/heading', + attributes: { content: 'Group Heading', level: 2 }, + }, + { + name: 'core/image', + }, + { + name: 'core/paragraph', + attributes: { content: 'Some paragraph' }, + }, + ], + }, + ] ); + } ); + + test( 'creates a group from multiple blocks of the same type via options toolbar', async ( { + editor, + pageUtils, + groupingUtils, + } ) => { + await groupingUtils.insertBlocksOfSameType(); + + // Multiselect via keyboard. + await pageUtils.pressKeys( 'primary+a', { times: 2 } ); + await editor.clickBlockOptionsMenuItem( 'Group' ); + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/group', + innerBlocks: [ + { + name: 'core/paragraph', + attributes: { content: 'First Paragraph' }, + }, + { + name: 'core/paragraph', + attributes: { content: 'Second Paragraph' }, + }, + { + name: 'core/paragraph', + attributes: { content: 'Third Paragraph' }, + }, + ], + }, + ] ); + } ); + + test( 'groups and ungroups multiple blocks of different types via options toolbar', async ( { + editor, + pageUtils, + groupingUtils, + } ) => { + await groupingUtils.insertBlocksOfMultipleTypes(); + + // Multiselect via keyboard. + await pageUtils.pressKeys( 'primary+a', { times: 2 } ); + + await editor.clickBlockOptionsMenuItem( 'Group' ); + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/group', + innerBlocks: [ + { + name: 'core/heading', + attributes: { content: 'Group Heading', level: 2 }, + }, + { + name: 'core/image', + }, + { + name: 'core/paragraph', + attributes: { content: 'Some paragraph' }, + }, + ], + }, + ] ); + + await editor.clickBlockOptionsMenuItem( 'Ungroup' ); + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/heading', + attributes: { content: 'Group Heading', level: 2 }, + }, + { + name: 'core/image', + }, + { + name: 'core/paragraph', + attributes: { content: 'Some paragraph' }, + }, + ] ); + } ); + + test( 'does not allow ungrouping a group block that has no children', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { name: 'core/group' } ); + await editor.clickBlockToolbarButton( 'Options' ); + await expect( + page + .getByRole( 'menu', { name: 'Options' } ) + .getByRole( 'menuitem', { name: 'Ungroup' } ) + ).toBeHidden(); + } ); + + test( 'should group and ungroup a controlled block properly', async ( { + editor, + requestUtils, + } ) => { + const { id: ref } = await requestUtils.createBlock( { + title: 'Block', + status: 'publish', + content: `\n

Hey!

\n`, + wp_pattern_category: [], + } ); + + await editor.insertBlock( { + name: 'core/block', + attributes: { ref }, + } ); + await editor.clickBlockOptionsMenuItem( 'Group' ); + await expect( + editor.canvas.getByRole( 'document', { + name: 'Block: Group', + } ) + ).toContainText( 'Hey!' ); + + await editor.clickBlockOptionsMenuItem( 'Ungroup' ); + await expect( + editor.canvas.getByRole( 'document', { + name: 'Block: Pattern', + } ) + ).toContainText( 'Hey!' ); + } ); + + test( 'should group another Group block via options toolbar', async ( { + page, + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: '1' }, + } ); + + const group = page + .getByRole( 'menu', { name: 'Options' } ) + .getByRole( 'menuitem', { name: 'Group', exact: true } ); + + await editor.clickBlockToolbarButton( 'Options' ); + await group.click(); + await editor.clickBlockToolbarButton( 'Options' ); + await group.click(); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/group', + innerBlocks: [ + { + name: 'core/group', + innerBlocks: [ + { + name: 'core/paragraph', + attributes: { content: '1' }, + }, + ], + }, + ], + }, + ] ); + } ); + } ); + + test.describe( 'Grouping Block availability', () => { + test.beforeEach( async ( { page, pageUtils, groupingUtils } ) => { + // Disable the Group block. + await page.evaluate( () => { + window.wp.data + .dispatch( 'core/edit-post' ) + .hideBlockTypes( [ 'core/group' ] ); + } ); + + await groupingUtils.insertBlocksOfMultipleTypes(); + await pageUtils.pressKeys( 'primary+a', { times: 2 } ); + } ); + + test.afterEach( async ( { page } ) => { + // Re-enable the Group block. + await page.evaluate( () => { + window.wp.data + .dispatch( 'core/edit-post' ) + .showBlockTypes( [ 'core/group' ] ); + } ); + } ); + + test( 'does not show group transform if Grouping block is disabled', async ( { + page, + } ) => { + await page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Multiple blocks selected' } ) + .click(); + + await expect( + page + .getByRole( 'menu', { name: 'Multiple blocks selected' } ) + .getByRole( 'menuitem', { name: 'Group' } ) + ).toBeHidden(); + } ); + + test( 'does not show group option in the options toolbar if Grouping block is disabled', async ( { + page, + editor, + } ) => { + await editor.clickBlockToolbarButton( 'Options' ); + + await expect( + page + .getByRole( 'menu', { name: 'Options' } ) + .getByRole( 'menuitem', { name: 'Group' } ) + ).toBeHidden(); + } ); + } ); + + test.describe( 'Preserving selected blocks attributes', () => { + test( 'preserves width alignment settings of selected blocks', async ( { + editor, + page, + pageUtils, + } ) => { + await editor.insertBlock( { + name: 'core/heading', + attributes: { content: 'Heading', level: 2 }, + } ); + const alignOptions = page.getByRole( 'menu', { name: 'Align' } ); + + // Full width image. + await editor.insertBlock( { name: 'core/image' } ); + await editor.clickBlockToolbarButton( 'Align' ); + await alignOptions + .getByRole( 'menuitemradio', { name: 'Full width' } ) + .click(); + + // Wide width image. + await editor.insertBlock( { name: 'core/image' } ); + await editor.clickBlockToolbarButton( 'Align' ); + await alignOptions + .getByRole( 'menuitemradio', { + name: 'Wide width', + } ) + .click(); + + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Some paragraph' }, + } ); + + // Select everythinga and group. + await pageUtils.pressKeys( 'primary+a', { times: 2 } ); + await editor.transformBlockTo( 'core/group' ); + + // We expect Group block align setting to match that + // of the widest of it's "child" innerBlocks + await editor.clickBlockToolbarButton( 'Align' ); + await expect( + alignOptions.getByRole( 'menuitemradio', { checked: true } ) + ).toHaveText( 'Full width' ); + } ); + } ); + + test.describe( 'Registering alternative Blocks to handle Grouping interactions', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activatePlugin( + 'gutenberg-test-custom-grouping-block' + ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deactivatePlugin( + 'gutenberg-test-custom-grouping-block' + ); + } ); + + test( 'should use registered grouping block for grouping interactions', async ( { + editor, + page, + pageUtils, + groupingUtils, + } ) => { + // Set custom Block as the Block to use for Grouping. + await page.evaluate( () => { + window.wp.blocks.setGroupingBlockName( + 'test/alternative-group-block' + ); + } ); + + await groupingUtils.insertBlocksOfSameType(); + await pageUtils.pressKeys( 'primary+a', { times: 2 } ); + + // Group - this will use whichever Block is registered as the Grouping Block + // as opposed to "transformTo()" which uses whatever is passed to it. To + // ensure this test is meaningful we must rely on what is registered. + await editor.clickBlockOptionsMenuItem( 'Group' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'test/alternative-group-block', + innerBlocks: [ + { + name: 'core/paragraph', + attributes: { content: 'First Paragraph' }, + }, + { + name: 'core/paragraph', + attributes: { content: 'Second Paragraph' }, + }, + { + name: 'core/paragraph', + attributes: { content: 'Third Paragraph' }, + }, + ], + }, + ] ); + } ); + } ); +} ); + +class GroupingUtils { + /** @type {import('@playwright/test').Page} */ + #page; + /** @type {Editor} */ + #editor; + + constructor( { page, editor } ) { + this.#page = page; + this.#editor = editor; + } + + async insertBlocksOfSameType() { + await test.step( + 'insert blocks of the same type', + async () => { + await this.#editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'First Paragraph' }, + } ); + await this.#editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Second Paragraph' }, + } ); + await this.#editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Third Paragraph' }, + } ); + }, + { box: true } + ); + } + + async insertBlocksOfMultipleTypes() { + await test.step( + 'insert blocks of multiple types', + async () => { + await this.#editor.insertBlock( { + name: 'core/heading', + attributes: { content: 'Group Heading', level: 2 }, + } ); + await this.#editor.insertBlock( { + name: 'core/image', + } ); + await this.#editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Some paragraph' }, + } ); + }, + { box: true } + ); + } +} From 4782f6488060e70ed819d0ca25d02a80a60cb67a Mon Sep 17 00:00:00 2001 From: David Arenas Date: Mon, 15 Jan 2024 14:44:16 +0100 Subject: [PATCH 06/18] Interactivity API: add `wp-run` directive and `useInit` & `useWatch` hooks (#57805) * Implement tests for wp-run * Implement wp-run * Rename useSignalEffect to useWatch and add useInit * Export useLayoutEffect * Expose cloneElement * Pass the scope to useWatch and useInit * Add tests for hooks inside wp-run * Document useWatch and useInit * Update changelog * Always reset scope inside `withScope` * Expose scoped versions of preact hooks * Add docs for `wp-run` * Removed `runs` from `wp-run` store * Improve `wp-init` documentation * Expose `useState` and `useRef` from preact --- .../directive-run/block.json | 15 ++ .../directive-run/render.php | 54 ++++++++ .../interactive-blocks/directive-run/view.js | 108 +++++++++++++++ packages/interactivity/CHANGELOG.md | 4 + .../interactivity/docs/2-api-reference.md | 59 +++++++- packages/interactivity/src/directives.js | 23 ++-- packages/interactivity/src/index.js | 13 +- packages/interactivity/src/utils.js | 129 +++++++++++++++++- .../specs/interactivity/directive-run.spec.ts | 61 +++++++++ 9 files changed, 452 insertions(+), 14 deletions(-) create mode 100644 packages/e2e-tests/plugins/interactive-blocks/directive-run/block.json create mode 100644 packages/e2e-tests/plugins/interactive-blocks/directive-run/render.php create mode 100644 packages/e2e-tests/plugins/interactive-blocks/directive-run/view.js create mode 100644 test/e2e/specs/interactivity/directive-run.spec.ts diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-run/block.json b/packages/e2e-tests/plugins/interactive-blocks/directive-run/block.json new file mode 100644 index 00000000000000..5242ca2c872ae6 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-run/block.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "test/directive-run", + "title": "E2E Interactivity tests - directive run", + "category": "text", + "icon": "heart", + "description": "", + "supports": { + "interactivity": true + }, + "textdomain": "e2e-interactivity", + "viewScript": "directive-run-view", + "render": "file:./render.php" +} diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-run/render.php b/packages/e2e-tests/plugins/interactive-blocks/directive-run/render.php new file mode 100644 index 00000000000000..a7eaaa984035e9 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-run/render.php @@ -0,0 +1,54 @@ + + +
+
+
+
+
no
+ +
+
+ +
+ + + + + + + +
+
+ Element with wp-run using hooks +
+
+
diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-run/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-run/view.js new file mode 100644 index 00000000000000..b6dd68154d7164 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-run/view.js @@ -0,0 +1,108 @@ +/** + * WordPress dependencies + */ +import { + store, + directive, + navigate, + useInit, + useWatch, + cloneElement, + getElement, +} from '@wordpress/interactivity'; + +// Custom directive to show hide the content elements in which it is placed. +directive( + 'show-children', + ( { directives: { 'show-children': showChildren }, element, evaluate } ) => { + const entry = showChildren.find( + ( { suffix } ) => suffix === 'default' + ); + return evaluate( entry ) + ? element + : cloneElement( element, { children: null } ); + }, + { priority: 9 } +); + +const html = ` +
+
+
+
+
yes
+ +
+
+`; + +const { state } = store( 'directive-run', { + state: { + isOpen: false, + isHydrated: 'no', + isMounted: 'no', + renderCount: 0, + clickCount: 0 + }, + actions: { + toggle() { + state.isOpen = ! state.isOpen; + }, + increment() { + state.clickCount = state.clickCount + 1; + }, + navigate() { + navigate( window.location, { + force: true, + html, + } ); + }, + }, + callbacks: { + updateIsHydrated() { + setTimeout( () => ( state.isHydrated = 'yes' ) ); + }, + updateIsMounted() { + setTimeout( () => ( state.isMounted = 'yes' ) ); + }, + updateRenderCount() { + setTimeout( () => ( state.renderCount = state.renderCount + 1 ) ); + }, + useHooks() { + // Runs only on first render. + useInit( () => { + const { ref } = getElement(); + ref + .closest( '[data-testid="wp-run hooks results"]') + .setAttribute( 'data-init', 'initialized' ); + return () => { + ref + .closest( '[data-testid="wp-run hooks results"]') + .setAttribute( 'data-init', 'cleaned up' ); + }; + } ); + + // Runs whenever a signal consumed inside updates its value. Also + // executes for the first render. + useWatch( () => { + const { ref } = getElement(); + const { clickCount } = state; + ref + .closest( '[data-testid="wp-run hooks results"]') + .setAttribute( 'data-watch', clickCount ); + return () => { + ref + .closest( '[data-testid="wp-run hooks results"]') + .setAttribute( 'data-watch', 'cleaned up' ); + }; + } ); + } + } +} ); diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md index 8c03cfc314efed..ff0c4942abb3f1 100644 --- a/packages/interactivity/CHANGELOG.md +++ b/packages/interactivity/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### New Features + +- Add the `data-wp-run` directive along with the `useInit` and `useWatch` hooks. ([57805](https://github.com/WordPress/gutenberg/pull/57805)) + ### Bug Fix - Fix namespaces when there are nested interactive regions. ([#57029](https://github.com/WordPress/gutenberg/pull/57029)) diff --git a/packages/interactivity/docs/2-api-reference.md b/packages/interactivity/docs/2-api-reference.md index 3c8f179861f525..7980c31b984f8d 100644 --- a/packages/interactivity/docs/2-api-reference.md +++ b/packages/interactivity/docs/2-api-reference.md @@ -22,6 +22,7 @@ DOM elements are connected to data stored in the state and context through direc - [`wp-on`](#wp-on) ![](https://img.shields.io/badge/EVENT_HANDLERS-afd2e3.svg) - [`wp-watch`](#wp-watch) ![](https://img.shields.io/badge/SIDE_EFFECTS-afd2e3.svg) - [`wp-init`](#wp-init) ![](https://img.shields.io/badge/SIDE_EFFECTS-afd2e3.svg) + - [`wp-run`](#wp-run) ![](https://img.shields.io/badge/SIDE_EFFECTS-afd2e3.svg) - [`wp-key`](#wp-key) ![](https://img.shields.io/badge/TEMPLATING-afd2e3.svg) - [Values of directives are references to store properties](#values-of-directives-are-references-to-store-properties) - [The store](#the-store) @@ -471,6 +472,62 @@ store( "myPlugin", { The `wp-init` can return a function. If it does, the returned function will run when the element is removed from the DOM. +#### `wp-run` + +It runs the passed callback **during node's render execution**. + +You can use and compose hooks like `useState`, `useWatch` or `useEffect` inside inside the passed callback and create your own logic, providing more flexibility than previous directives. + +You can attach several `wp-run` to the same DOM element by using the syntax `data-wp-run--[unique-id]`. _The unique id doesn't need to be unique globally, it just needs to be different than the other unique ids of the `wp-run` directives of that DOM element._ + +_Example of `data-wp-run` directive_ + +```html +
+

Hi!

+
+``` + +
+ See store used with the directive above + +```js +import { store, useState, useEffect } from '@wordpress/interactivity'; + +// Unlike `data-wp-init` and `data-wp-watch`, you can use any hooks inside +// `data-wp-run` callbacks. +const useInView = ( ref ) => { + const [ inView, setInView ] = useState( false ); + useEffect( () => { + const observer = new IntersectionObserver( ( [ entry ] ) => { + setInView( entry.isIntersecting ); + } ); + if ( ref ) observer.observe( ref ); + return () => ref && observer.unobserve( ref ); + }, []); + return inView; +}; + +store( 'myPlugin', { + callbacks: { + logInView: () => { + const { ref } = getElement(); + const isInView = useInView( ref ); + useEffect( () => { + if ( isInView ) { + console.log( 'Inside' ); + } else { + console.log( 'Outside' ); + } + }); + } + }, +} ); +``` + +
+
+ #### `wp-key` The `wp-key` directive assigns a unique key to an element to help the Interactivity API identify it when iterating through arrays of elements. This becomes important if your array elements can move (e.g., due to sorting), get inserted, or get deleted. A well-chosen key value helps the Interactivity API infer what exactly has changed in the array, allowing it to make the correct updates to the DOM. @@ -528,7 +585,7 @@ In the example below, we get `state.isPlaying` from `otherPlugin` instead of `my ```html
-
+
diff --git a/packages/interactivity/src/directives.js b/packages/interactivity/src/directives.js index 0793dc0cc5d5ba..e9cc66fa50a6f0 100644 --- a/packages/interactivity/src/directives.js +++ b/packages/interactivity/src/directives.js @@ -14,7 +14,7 @@ import { deepSignal, peek } from 'deepsignal'; * Internal dependencies */ import { createPortal } from './portals'; -import { useSignalEffect } from './utils'; +import { useWatch, useInit } from './utils'; import { directive } from './hooks'; import { SlotProvider, Slot, Fill } from './slots'; import { navigate } from './router'; @@ -75,14 +75,14 @@ export default () => { // data-wp-watch--[name] directive( 'watch', ( { directives: { watch }, evaluate } ) => { watch.forEach( ( entry ) => { - useSignalEffect( () => evaluate( entry ) ); + useWatch( () => evaluate( entry ) ); } ); } ); // data-wp-init--[name] directive( 'init', ( { directives: { init }, evaluate } ) => { init.forEach( ( entry ) => { - useEffect( () => evaluate( entry ), [] ); + useInit( () => evaluate( entry ) ); } ); } ); @@ -118,7 +118,7 @@ export default () => { ? `${ currentClass } ${ name }` : name; - useEffect( () => { + useInit( () => { // This seems necessary because Preact doesn't change the class // names on the hydration, so we have to do it manually. It doesn't // need deps because it only needs to do it the first time. @@ -127,7 +127,7 @@ export default () => { } else { element.ref.current.classList.add( name ); } - }, [] ); + } ); } ); } ); @@ -182,7 +182,7 @@ export default () => { if ( ! result ) delete element.props.style[ key ]; else element.props.style[ key ] = result; - useEffect( () => { + useInit( () => { // This seems necessary because Preact doesn't change the styles on // the hydration, so we have to do it manually. It doesn't need deps // because it only needs to do it the first time. @@ -191,7 +191,7 @@ export default () => { } else { element.ref.current.style[ key ] = result; } - }, [] ); + } ); } ); } ); @@ -217,7 +217,7 @@ export default () => { // This seems necessary because Preact doesn't change the attributes // on the hydration, so we have to do it manually. It doesn't need // deps because it only needs to do it the first time. - useEffect( () => { + useInit( () => { const el = element.ref.current; // We set the value directly to the corresponding @@ -260,7 +260,7 @@ export default () => { } else { el.removeAttribute( attribute ); } - }, [] ); + } ); } ); } ); @@ -390,4 +390,9 @@ export default () => { ), { priority: 4 } ); + + // data-wp-run + directive( 'run', ( { directives: { run }, evaluate } ) => { + run.forEach( ( entry ) => evaluate( entry ) ); + } ); }; diff --git a/packages/interactivity/src/index.js b/packages/interactivity/src/index.js index 6c7b98e8e7a79e..cf0b4c88cac4bf 100644 --- a/packages/interactivity/src/index.js +++ b/packages/interactivity/src/index.js @@ -7,8 +7,17 @@ import { init } from './router'; export { store } from './store'; export { directive, getContext, getElement } from './hooks'; export { navigate, prefetch } from './router'; -export { h as createElement } from 'preact'; -export { useEffect, useContext, useMemo } from 'preact/hooks'; +export { + useWatch, + useInit, + useEffect, + useLayoutEffect, + useCallback, + useMemo, +} from './utils'; + +export { h as createElement, cloneElement } from 'preact'; +export { useContext, useState, useRef } from 'preact/hooks'; export { deepSignal } from 'deepsignal'; document.addEventListener( 'DOMContentLoaded', async () => { diff --git a/packages/interactivity/src/utils.js b/packages/interactivity/src/utils.js index 10b53104fb9c89..021df983cb4f0a 100644 --- a/packages/interactivity/src/utils.js +++ b/packages/interactivity/src/utils.js @@ -1,9 +1,19 @@ /** * External dependencies */ -import { useEffect } from 'preact/hooks'; +import { + useMemo as _useMemo, + useCallback as _useCallback, + useEffect as _useEffect, + useLayoutEffect as _useLayoutEffect, +} from 'preact/hooks'; import { effect } from '@preact/signals'; +/** + * Internal dependencies + */ +import { getScope, setScope, resetScope } from './hooks'; + const afterNextFrame = ( callback ) => { return new Promise( ( resolve ) => { const done = () => { @@ -38,7 +48,7 @@ function createFlusher( compute, notify ) { // implementation comes from this PR, but we added short-cirtuiting to avoid // infinite loops: https://github.com/preactjs/signals/pull/290 export function useSignalEffect( callback ) { - useEffect( () => { + _useEffect( () => { let eff = null; let isExecuting = false; const notify = async () => { @@ -53,6 +63,121 @@ export function useSignalEffect( callback ) { }, [] ); } +/** + * Returns the passed function wrapped with the current scope so it is + * accessible whenever the function runs. This is primarily to make the scope + * available inside hook callbacks. + * + * @param {Function} func The passed function. + * @return {Function} The wrapped function. + */ +const withScope = ( func ) => { + const scope = getScope(); + return ( ...args ) => { + setScope( scope ); + try { + return func( ...args ); + } finally { + resetScope(); + } + }; +}; + +/** + * Accepts a function that contains imperative code which runs whenever any of + * the accessed _reactive_ properties (e.g., values from the global state or the + * context) is modified. + * + * This hook makes the element's scope available so functions like + * `getElement()` and `getContext()` can be used inside the passed callback. + * + * @param {Function} callback The hook callback. + */ +export function useWatch( callback ) { + useSignalEffect( withScope( callback ) ); +} + +/** + * Accepts a function that contains imperative code which runs only after the + * element's first render, mainly useful for intialization logic. + * + * This hook makes the element's scope available so functions like + * `getElement()` and `getContext()` can be used inside the passed callback. + * + * @param {Function} callback The hook callback. + */ +export function useInit( callback ) { + _useEffect( withScope( callback ), [] ); +} + +/** + * Accepts a function that contains imperative, possibly effectful code. The + * effects run after browser paint, without blocking it. + * + * This hook is equivalent to Preact's `useEffect` and makes the element's scope + * available so functions like `getElement()` and `getContext()` can be used + * inside the passed callback. + * + * @param {Function} callback Imperative function that can return a cleanup + * function. + * @param {any[]} inputs If present, effect will only activate if the + * values in the list change (using `===`). + */ +export function useEffect( callback, inputs ) { + _useEffect( withScope( callback ), inputs ); +} + +/** + * Accepts a function that contains imperative, possibly effectful code. Use + * this to read layout from the DOM and synchronously re-render. + * + * This hook is equivalent to Preact's `useLayoutEffect` and makes the element's + * scope available so functions like `getElement()` and `getContext()` can be + * used inside the passed callback. + * + * @param {Function} callback Imperative function that can return a cleanup + * function. + * @param {any[]} inputs If present, effect will only activate if the + * values in the list change (using `===`). + */ +export function useLayoutEffect( callback, inputs ) { + _useLayoutEffect( withScope( callback ), inputs ); +} + +/** + * Returns a memoized version of the callback that only changes if one of the + * inputs has changed (using `===`). + * + * This hook is equivalent to Preact's `useCallback` and makes the element's + * scope available so functions like `getElement()` and `getContext()` can be + * used inside the passed callback. + * + * @param {Function} callback Imperative function that can return a cleanup + * function. + * @param {any[]} inputs If present, effect will only activate if the + * values in the list change (using `===`). + */ +export function useCallback( callback, inputs ) { + _useCallback( withScope( callback ), inputs ); +} + +/** + * Pass a factory function and an array of inputs. `useMemo` will only recompute + * the memoized value when one of the inputs has changed. + * + * This hook is equivalent to Preact's `useMemo` and makes the element's scope + * available so functions like `getElement()` and `getContext()` can be used + * inside the passed factory function. + * + * @param {Function} factory Imperative function that can return a cleanup + * function. + * @param {any[]} inputs If present, effect will only activate if the + * values in the list change (using `===`). + */ +export function useMemo( factory, inputs ) { + _useMemo( withScope( factory ), inputs ); +} + // For wrapperless hydration. // See https://gist.github.com/developit/f4c67a2ede71dc2fab7f357f39cff28c export const createRootFragment = ( parent, replaceNode ) => { diff --git a/test/e2e/specs/interactivity/directive-run.spec.ts b/test/e2e/specs/interactivity/directive-run.spec.ts new file mode 100644 index 00000000000000..0348bdb95c2ab7 --- /dev/null +++ b/test/e2e/specs/interactivity/directive-run.spec.ts @@ -0,0 +1,61 @@ +/** + * Internal dependencies + */ +import { test, expect } from './fixtures'; + +test.describe( 'data-wp-run', () => { + test.beforeAll( async ( { interactivityUtils: utils } ) => { + await utils.activatePlugins(); + await utils.addPostWithBlock( 'test/directive-run' ); + } ); + + test.beforeEach( async ( { interactivityUtils: utils, page } ) => { + await page.goto( utils.getLink( 'test/directive-run' ) ); + } ); + + test.afterAll( async ( { interactivityUtils: utils } ) => { + await utils.deactivatePlugins(); + await utils.deleteAllPosts(); + } ); + + test( 'should execute each element render', async ( { page } ) => { + await expect( page.getByTestId( 'hydrated' ) ).toHaveText( 'yes' ); + await expect( page.getByTestId( 'renderCount' ) ).toHaveText( '1' ); + await page.getByTestId( 'increment' ).click(); + await expect( page.getByTestId( 'renderCount' ) ).toHaveText( '2' ); + await page.getByTestId( 'increment' ).click(); + await expect( page.getByTestId( 'renderCount' ) ).toHaveText( '3' ); + } ); + + test( 'should execute when an element is mounted', async ( { page } ) => { + await expect( page.getByTestId( 'mounted' ) ).toHaveText( 'no' ); + await page.getByTestId( 'toggle' ).click(); + await expect( page.getByTestId( 'mounted' ) ).toHaveText( 'yes' ); + } ); + + test( 'should work with client-side navigation', async ( { page } ) => { + await page.getByTestId( 'increment' ).click(); + await page.getByTestId( 'increment' ).click(); + await expect( page.getByTestId( 'navigated' ) ).toHaveText( 'no' ); + await expect( page.getByTestId( 'renderCount' ) ).toHaveText( '3' ); + await page.getByTestId( 'navigate' ).click(); + await expect( page.getByTestId( 'navigated' ) ).toHaveText( 'yes' ); + await expect( page.getByTestId( 'renderCount' ) ).toHaveText( '4' ); + } ); + + test( 'should allow executing hooks', async ( { page } ) => { + await page.getByTestId( 'toggle' ).click(); + const results = page.getByTestId( 'wp-run hooks results' ); + await expect( results ).toHaveAttribute( 'data-init', 'initialized' ); + + await expect( results ).toHaveAttribute( 'data-watch', '0' ); + await page.getByTestId( 'increment' ).click(); + await expect( results ).toHaveAttribute( 'data-watch', '1' ); + await page.getByTestId( 'increment' ).click(); + await expect( results ).toHaveAttribute( 'data-watch', '2' ); + + await page.getByTestId( 'toggle' ).click(); + await expect( results ).toHaveAttribute( 'data-init', 'cleaned up' ); + await expect( results ).toHaveAttribute( 'data-watch', 'cleaned up' ); + } ); +} ); From 0760faa19ccd219eebd72e1afd1e005d27277b3f Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Mon, 15 Jan 2024 17:59:01 +0400 Subject: [PATCH 07/18] Remove unused Navigation block e2e test fixtures (#57848) --- .../fixtures/menu-items-request-fixture.json | 84 -- .../fixtures/menu-items-response-fixture.json | 1261 ----------------- 2 files changed, 1345 deletions(-) delete mode 100644 packages/e2e-tests/specs/editor/fixtures/menu-items-request-fixture.json delete mode 100644 packages/e2e-tests/specs/editor/fixtures/menu-items-response-fixture.json diff --git a/packages/e2e-tests/specs/editor/fixtures/menu-items-request-fixture.json b/packages/e2e-tests/specs/editor/fixtures/menu-items-request-fixture.json deleted file mode 100644 index 01b844c45ceee4..00000000000000 --- a/packages/e2e-tests/specs/editor/fixtures/menu-items-request-fixture.json +++ /dev/null @@ -1,84 +0,0 @@ -[ - { - "title": "Home", - "url": "http://localhost:8889/", - "menu_order": 1 - }, - { - "title": "About", - "type": "post_type", - "object": "page", - "menu_order": 2 - }, - { - "title": "Our team", - "type": "post_type", - "object": "page", - "menu_order": 3, - "parent": 1 - }, - { - "title": "Shop", - "type": "post_type", - "object": "page", - "menu_order": 4 - }, - { - "title": "Winter apparel", - "type": "post_type", - "object": "page", - "menu_order": 5, - "parent": 3 - }, - { - "title": "Chunky socks", - "type": "post_type", - "object": "page", - "menu_order": 6, - "parent": 4 - }, - { - "title": "Hideous hats", - "type": "post_type", - "object": "page", - "menu_order": 7, - "parent": 4 - }, - { - "title": "Glorious gloves", - "type": "post_type", - "object": "page", - "menu_order": 8, - "parent": 4 - }, - { - "title": "Jazzy Jumpers", - "type": "post_type", - "object": "page", - "menu_order": 9, - "parent": 4 - }, - { - "title": "Shipping", - "type": "post_type", - "object": "page", - "menu_order": 10 - }, - { - "title": "Contact Us", - "type": "post_type", - "object": "page", - "menu_order": 11 - }, - { - "title": "WordPress.org", - "url": "https://wordpress.org", - "menu_order": 12 - }, - { - "title": "Google", - "url": "https://google.com", - "menu_order": 13, - "parent": 11 - } -] diff --git a/packages/e2e-tests/specs/editor/fixtures/menu-items-response-fixture.json b/packages/e2e-tests/specs/editor/fixtures/menu-items-response-fixture.json deleted file mode 100644 index 4de75a5c0bacc6..00000000000000 --- a/packages/e2e-tests/specs/editor/fixtures/menu-items-response-fixture.json +++ /dev/null @@ -1,1261 +0,0 @@ -[ - { - "id": 94, - "title": { - "raw": "Home", - "rendered": "Home" - }, - "status": "publish", - "url": "http://localhost:8889/", - "attr_title": "", - "description": "", - "type": "custom", - "type_label": "Custom Link", - "object": "custom", - "object_id": 94, - "parent": 0, - "menu_order": 1, - "target": "", - "classes": [ "" ], - "xfn": [ "" ], - "meta": [], - "menus": [ 23 ], - "_links": { - "self": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/94" - } - ], - "collection": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items" - } - ], - "about": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/types/nav_menu_item" - } - ], - "wp:term": [ - { - "taxonomy": "nav_menu", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=94" - } - ], - "wp:action-publish": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/94" - } - ], - "wp:action-unfiltered-html": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/94" - } - ], - "wp:action-create-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/94" - } - ], - "wp:action-assign-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/94" - } - ], - "curies": [ - { - "name": "wp", - "href": "https://api.w.org/{rel}", - "templated": true - } - ] - } - }, - { - "id": 95, - "title": { - "raw": "", - "rendered": "Accusamus quo repellat illum magnam quas" - }, - "status": "publish", - "url": "http://localhost:8889/?page_id=41", - "attr_title": "", - "description": "", - "type": "post_type", - "type_label": "Page", - "object": "page", - "object_id": 41, - "parent": 0, - "menu_order": 2, - "target": "", - "classes": [ "" ], - "xfn": [ "" ], - "meta": [], - "menus": [ 23 ], - "_links": { - "self": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/95" - } - ], - "collection": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items" - } - ], - "about": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/types/nav_menu_item" - } - ], - "wp:term": [ - { - "taxonomy": "nav_menu", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=95" - } - ], - "wp:object": [ - { - "post_type": "post_type", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/pages/41" - } - ], - "wp:action-publish": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/95" - } - ], - "wp:action-unfiltered-html": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/95" - } - ], - "wp:action-create-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/95" - } - ], - "wp:action-assign-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/95" - } - ], - "curies": [ - { - "name": "wp", - "href": "https://api.w.org/{rel}", - "templated": true - } - ] - } - }, - { - "id": 96, - "title": { - "raw": "", - "rendered": "Debitis cum consequatur sit doloremque" - }, - "status": "publish", - "url": "http://localhost:8889/?page_id=51", - "attr_title": "", - "description": "", - "type": "post_type", - "type_label": "Page", - "object": "page", - "object_id": 51, - "parent": 95, - "menu_order": 3, - "target": "", - "classes": [ "" ], - "xfn": [ "" ], - "meta": [], - "menus": [ 23 ], - "_links": { - "self": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/96" - } - ], - "collection": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items" - } - ], - "about": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/types/nav_menu_item" - } - ], - "wp:term": [ - { - "taxonomy": "nav_menu", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=96" - } - ], - "wp:object": [ - { - "post_type": "post_type", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/pages/51" - } - ], - "wp:action-publish": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/96" - } - ], - "wp:action-unfiltered-html": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/96" - } - ], - "wp:action-create-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/96" - } - ], - "wp:action-assign-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/96" - } - ], - "curies": [ - { - "name": "wp", - "href": "https://api.w.org/{rel}", - "templated": true - } - ] - } - }, - { - "id": 97, - "title": { - "raw": "", - "rendered": "Est ea vero non nihil officiis in" - }, - "status": "publish", - "url": "http://localhost:8889/?page_id=53", - "attr_title": "", - "description": "", - "type": "post_type", - "type_label": "Page", - "object": "page", - "object_id": 53, - "parent": 0, - "menu_order": 4, - "target": "", - "classes": [ "" ], - "xfn": [ "" ], - "meta": [], - "menus": [ 23 ], - "_links": { - "self": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/97" - } - ], - "collection": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items" - } - ], - "about": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/types/nav_menu_item" - } - ], - "wp:term": [ - { - "taxonomy": "nav_menu", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=97" - } - ], - "wp:object": [ - { - "post_type": "post_type", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/pages/53" - } - ], - "wp:action-publish": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/97" - } - ], - "wp:action-unfiltered-html": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/97" - } - ], - "wp:action-create-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/97" - } - ], - "wp:action-assign-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/97" - } - ], - "curies": [ - { - "name": "wp", - "href": "https://api.w.org/{rel}", - "templated": true - } - ] - } - }, - { - "id": 98, - "title": { - "raw": "", - "rendered": "Fuga odio quis tempora" - }, - "status": "publish", - "url": "http://localhost:8889/?page_id=56", - "attr_title": "", - "description": "", - "type": "post_type", - "type_label": "Page", - "object": "page", - "object_id": 56, - "parent": 97, - "menu_order": 5, - "target": "", - "classes": [ "" ], - "xfn": [ "" ], - "meta": [], - "menus": [ 23 ], - "_links": { - "self": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/98" - } - ], - "collection": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items" - } - ], - "about": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/types/nav_menu_item" - } - ], - "wp:term": [ - { - "taxonomy": "nav_menu", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=98" - } - ], - "wp:object": [ - { - "post_type": "post_type", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/pages/56" - } - ], - "wp:action-publish": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/98" - } - ], - "wp:action-unfiltered-html": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/98" - } - ], - "wp:action-create-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/98" - } - ], - "wp:action-assign-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/98" - } - ], - "curies": [ - { - "name": "wp", - "href": "https://api.w.org/{rel}", - "templated": true - } - ] - } - }, - { - "id": 99, - "title": { - "raw": "", - "rendered": "In consectetur repellendus eveniet maiores aperiam" - }, - "status": "publish", - "url": "http://localhost:8889/?page_id=15", - "attr_title": "", - "description": "", - "type": "post_type", - "type_label": "Page", - "object": "page", - "object_id": 15, - "parent": 98, - "menu_order": 6, - "target": "", - "classes": [ "" ], - "xfn": [ "" ], - "meta": [], - "menus": [ 23 ], - "_links": { - "self": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/99" - } - ], - "collection": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items" - } - ], - "about": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/types/nav_menu_item" - } - ], - "wp:term": [ - { - "taxonomy": "nav_menu", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=99" - } - ], - "wp:object": [ - { - "post_type": "post_type", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/pages/15" - } - ], - "wp:action-publish": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/99" - } - ], - "wp:action-unfiltered-html": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/99" - } - ], - "wp:action-create-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/99" - } - ], - "wp:action-assign-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/99" - } - ], - "curies": [ - { - "name": "wp", - "href": "https://api.w.org/{rel}", - "templated": true - } - ] - } - }, - { - "id": 100, - "title": { - "raw": "", - "rendered": "Mollitia maiores consequatur ea dolorem blanditiis" - }, - "status": "publish", - "url": "http://localhost:8889/?page_id=45", - "attr_title": "", - "description": "", - "type": "post_type", - "type_label": "Page", - "object": "page", - "object_id": 45, - "parent": 99, - "menu_order": 7, - "target": "", - "classes": [ "" ], - "xfn": [ "" ], - "meta": [], - "menus": [ 23 ], - "_links": { - "self": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/100" - } - ], - "collection": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items" - } - ], - "about": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/types/nav_menu_item" - } - ], - "wp:term": [ - { - "taxonomy": "nav_menu", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=100" - } - ], - "wp:object": [ - { - "post_type": "post_type", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/pages/45" - } - ], - "wp:action-publish": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/100" - } - ], - "wp:action-unfiltered-html": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/100" - } - ], - "wp:action-create-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/100" - } - ], - "wp:action-assign-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/100" - } - ], - "curies": [ - { - "name": "wp", - "href": "https://api.w.org/{rel}", - "templated": true - } - ] - } - }, - { - "id": 101, - "title": { - "raw": "", - "rendered": "Necessitatibus nisi qui qui necessitatibus quaerat possimus" - }, - "status": "publish", - "url": "http://localhost:8889/?page_id=27", - "attr_title": "", - "description": "", - "type": "post_type", - "type_label": "Page", - "object": "page", - "object_id": 27, - "parent": 100, - "menu_order": 8, - "target": "", - "classes": [ "" ], - "xfn": [ "" ], - "meta": [], - "menus": [ 23 ], - "_links": { - "self": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/101" - } - ], - "collection": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items" - } - ], - "about": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/types/nav_menu_item" - } - ], - "wp:term": [ - { - "taxonomy": "nav_menu", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=101" - } - ], - "wp:object": [ - { - "post_type": "post_type", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/pages/27" - } - ], - "wp:action-publish": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/101" - } - ], - "wp:action-unfiltered-html": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/101" - } - ], - "wp:action-create-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/101" - } - ], - "wp:action-assign-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/101" - } - ], - "curies": [ - { - "name": "wp", - "href": "https://api.w.org/{rel}", - "templated": true - } - ] - } - }, - { - "id": 102, - "title": { - "raw": "", - "rendered": "Nulla omnis autem dolores eligendi" - }, - "status": "publish", - "url": "http://localhost:8889/?page_id=43", - "attr_title": "", - "description": "", - "type": "post_type", - "type_label": "Page", - "object": "page", - "object_id": 43, - "parent": 0, - "menu_order": 9, - "target": "", - "classes": [ "" ], - "xfn": [ "" ], - "meta": [], - "menus": [ 23 ], - "_links": { - "self": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/102" - } - ], - "collection": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items" - } - ], - "about": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/types/nav_menu_item" - } - ], - "wp:term": [ - { - "taxonomy": "nav_menu", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=102" - } - ], - "wp:object": [ - { - "post_type": "post_type", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/pages/43" - } - ], - "wp:action-publish": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/102" - } - ], - "wp:action-unfiltered-html": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/102" - } - ], - "wp:action-create-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/102" - } - ], - "wp:action-assign-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/102" - } - ], - "curies": [ - { - "name": "wp", - "href": "https://api.w.org/{rel}", - "templated": true - } - ] - } - }, - { - "id": 103, - "title": { - "raw": "", - "rendered": "Sample Page" - }, - "status": "publish", - "url": "http://localhost:8889/?page_id=2", - "attr_title": "", - "description": "", - "type": "post_type", - "type_label": "Page", - "object": "page", - "object_id": 2, - "parent": 0, - "menu_order": 10, - "target": "", - "classes": [ "" ], - "xfn": [ "" ], - "meta": [], - "menus": [ 23 ], - "_links": { - "self": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/103" - } - ], - "collection": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items" - } - ], - "about": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/types/nav_menu_item" - } - ], - "wp:term": [ - { - "taxonomy": "nav_menu", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=103" - } - ], - "wp:object": [ - { - "post_type": "post_type", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/pages/2" - } - ], - "wp:action-publish": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/103" - } - ], - "wp:action-unfiltered-html": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/103" - } - ], - "wp:action-create-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/103" - } - ], - "wp:action-assign-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/103" - } - ], - "curies": [ - { - "name": "wp", - "href": "https://api.w.org/{rel}", - "templated": true - } - ] - } - }, - { - "id": 104, - "title": { - "raw": "", - "rendered": "Beatae qui labore voluptas eveniet officia quia voluptas qui porro sequi et aut est" - }, - "status": "publish", - "url": "http://localhost:8889/?cat=7", - "attr_title": "", - "description": "Ratione nemo ut aut ullam sed assumenda quis est exercitationem", - "type": "taxonomy", - "type_label": "Category", - "object": "category", - "object_id": 7, - "parent": 0, - "menu_order": 11, - "target": "", - "classes": [ "" ], - "xfn": [ "" ], - "meta": [], - "menus": [ 23 ], - "_links": { - "self": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/104" - } - ], - "collection": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items" - } - ], - "about": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/types/nav_menu_item" - } - ], - "wp:term": [ - { - "taxonomy": "nav_menu", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=104" - } - ], - "wp:object": [ - { - "taxonomy": "taxonomy", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/categories/7" - } - ], - "wp:action-publish": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/104" - } - ], - "wp:action-unfiltered-html": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/104" - } - ], - "wp:action-create-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/104" - } - ], - "wp:action-assign-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/104" - } - ], - "curies": [ - { - "name": "wp", - "href": "https://api.w.org/{rel}", - "templated": true - } - ] - } - }, - { - "id": 105, - "title": { - "raw": "", - "rendered": "Et minus itaque velit tempore hic quisquam saepe quas asperiores" - }, - "status": "publish", - "url": "http://localhost:8889/?cat=19", - "attr_title": "", - "description": "Vel fuga enim rerum perspiciatis sapiente mollitia magni ut molestiae labore quae quia quia libero perspiciatis voluptatem quidem deleniti eveniet laboriosam doloribus dolor laborum accusantium modi ducimus itaque rerum cum nostrum", - "type": "taxonomy", - "type_label": "Category", - "object": "category", - "object_id": 19, - "parent": 104, - "menu_order": 12, - "target": "", - "classes": [ "" ], - "xfn": [ "" ], - "meta": [], - "menus": [ 23 ], - "_links": { - "self": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/105" - } - ], - "collection": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items" - } - ], - "about": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/types/nav_menu_item" - } - ], - "wp:term": [ - { - "taxonomy": "nav_menu", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=105" - } - ], - "wp:object": [ - { - "taxonomy": "taxonomy", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/categories/19" - } - ], - "wp:action-publish": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/105" - } - ], - "wp:action-unfiltered-html": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/105" - } - ], - "wp:action-create-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/105" - } - ], - "wp:action-assign-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/105" - } - ], - "curies": [ - { - "name": "wp", - "href": "https://api.w.org/{rel}", - "templated": true - } - ] - } - }, - { - "id": 106, - "title": { - "raw": "", - "rendered": "Et quas a et mollitia et voluptas optio voluptate quia quo unde aut in nostrum iste impedit quisquam id aut" - }, - "status": "publish", - "url": "http://localhost:8889/?cat=6", - "attr_title": "", - "description": "Quas sit labore earum omnis eos sint iste est possimus harum aut soluta sint optio quos distinctio inventore voluptate non ut aliquam ad ut voluptates fugiat numquam magnam modi repellendus modi laudantium et debitis officia est voluptatum quidem unde molestiae animi vero fuga accusamus nam", - "type": "taxonomy", - "type_label": "Category", - "object": "category", - "object_id": 6, - "parent": 105, - "menu_order": 13, - "target": "", - "classes": [ "" ], - "xfn": [ "" ], - "meta": [], - "menus": [ 23 ], - "_links": { - "self": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/106" - } - ], - "collection": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items" - } - ], - "about": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/types/nav_menu_item" - } - ], - "wp:term": [ - { - "taxonomy": "nav_menu", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=106" - } - ], - "wp:object": [ - { - "taxonomy": "taxonomy", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/categories/6" - } - ], - "wp:action-publish": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/106" - } - ], - "wp:action-unfiltered-html": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/106" - } - ], - "wp:action-create-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/106" - } - ], - "wp:action-assign-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/106" - } - ], - "curies": [ - { - "name": "wp", - "href": "https://api.w.org/{rel}", - "templated": true - } - ] - } - }, - { - "id": 107, - "title": { - "raw": "", - "rendered": "Illo quis sit impedit itaque expedita earum deserunt magni doloremque velit eum id error" - }, - "status": "publish", - "url": "http://localhost:8889/?cat=16", - "attr_title": "", - "description": "Doloremque vero sunt officiis iste voluptatibus voluptas molestiae sint asperiores recusandae amet praesentium et explicabo nesciunt similique voluptatum laudantium amet officiis quas distinctio quis enim nihil tempora", - "type": "taxonomy", - "type_label": "Category", - "object": "category", - "object_id": 16, - "parent": 106, - "menu_order": 14, - "target": "", - "classes": [ "" ], - "xfn": [ "" ], - "meta": [], - "menus": [ 23 ], - "_links": { - "self": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/107" - } - ], - "collection": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items" - } - ], - "about": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/types/nav_menu_item" - } - ], - "wp:term": [ - { - "taxonomy": "nav_menu", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=107" - } - ], - "wp:object": [ - { - "taxonomy": "taxonomy", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/categories/16" - } - ], - "wp:action-publish": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/107" - } - ], - "wp:action-unfiltered-html": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/107" - } - ], - "wp:action-create-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/107" - } - ], - "wp:action-assign-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/107" - } - ], - "curies": [ - { - "name": "wp", - "href": "https://api.w.org/{rel}", - "templated": true - } - ] - } - }, - { - "id": 108, - "title": { - "raw": "WordPress.org", - "rendered": "WordPress.org" - }, - "status": "publish", - "url": "https://wordpress.org", - "attr_title": "", - "description": "", - "type": "custom", - "type_label": "Custom Link", - "object": "custom", - "object_id": 108, - "parent": 0, - "menu_order": 15, - "target": "", - "classes": [ "" ], - "xfn": [ "" ], - "meta": [], - "menus": [ 23 ], - "_links": { - "self": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/108" - } - ], - "collection": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items" - } - ], - "about": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/types/nav_menu_item" - } - ], - "wp:term": [ - { - "taxonomy": "nav_menu", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=108" - } - ], - "wp:action-publish": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/108" - } - ], - "wp:action-unfiltered-html": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/108" - } - ], - "wp:action-create-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/108" - } - ], - "wp:action-assign-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/108" - } - ], - "curies": [ - { - "name": "wp", - "href": "https://api.w.org/{rel}", - "templated": true - } - ] - } - }, - { - "id": 109, - "title": { - "raw": "Google", - "rendered": "Google" - }, - "status": "publish", - "url": "https://google.com", - "attr_title": "", - "description": "", - "type": "custom", - "type_label": "Custom Link", - "object": "custom", - "object_id": 109, - "parent": 108, - "menu_order": 16, - "target": "", - "classes": [ "" ], - "xfn": [ "" ], - "meta": [], - "menus": [ 23 ], - "_links": { - "self": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/109" - } - ], - "collection": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items" - } - ], - "about": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/types/nav_menu_item" - } - ], - "wp:term": [ - { - "taxonomy": "nav_menu", - "embeddable": true, - "href": "http://localhost:8889/index.php?rest_route=%2Fwp%2Fv2%2Fmenus&post=109" - } - ], - "wp:action-publish": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/109" - } - ], - "wp:action-unfiltered-html": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/109" - } - ], - "wp:action-create-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/109" - } - ], - "wp:action-assign-menus": [ - { - "href": "http://localhost:8889/index.php?rest_route=/wp/v2/menu-items/109" - } - ], - "curies": [ - { - "name": "wp", - "href": "https://api.w.org/{rel}", - "templated": true - } - ] - } - } -] From e6a34a33be315553f810cf214f720d478e68a36a Mon Sep 17 00:00:00 2001 From: Andrei Draganescu Date: Mon, 15 Jan 2024 16:15:33 +0200 Subject: [PATCH 08/18] removes the template condition and shows the chevron back in edit post header (#57807) --- packages/edit-post/src/components/header/index.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index b76b89ff7673db..050ee8103251f6 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -119,9 +119,7 @@ function Header( { setEntitiesSavedStatesCallback } ) { className={ classnames( 'selected-block-tools-wrapper', { - 'is-collapsed': - isEditingTemplate && - isBlockToolsCollapsed, + 'is-collapsed': isBlockToolsCollapsed, } ) } > @@ -131,7 +129,7 @@ function Header( { setEntitiesSavedStatesCallback } ) { ref={ blockToolbarRef } name="block-toolbar" /> - { isEditingTemplate && hasBlockSelection && ( + { hasBlockSelection && ( - - - - - -
- diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-slots/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-slots/view.js deleted file mode 100644 index a8673fea9eba92..00000000000000 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-slots/view.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * WordPress dependencies - */ -import { store, getContext } from '@wordpress/interactivity'; - -const { state } = store( 'directive-slots', { - state: { - slot: '', - }, - actions: { - changeSlot( event ) { - state.slot = event.target.dataset.slot; - }, - updateSlotText() { - const context = getContext(); - const n = context.text[ 1 ]; - context.text = `[${ n } updated]`; - }, - }, -} ); diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md index ff0c4942abb3f1..db396cfa11de12 100644 --- a/packages/interactivity/CHANGELOG.md +++ b/packages/interactivity/CHANGELOG.md @@ -6,6 +6,10 @@ - Add the `data-wp-run` directive along with the `useInit` and `useWatch` hooks. ([57805](https://github.com/WordPress/gutenberg/pull/57805)) +### Breaking Change + +- Remove `data-wp-slot` and `data-wp-fill`. ([#57854](https://github.com/WordPress/gutenberg/pull/57854)) + ### Bug Fix - Fix namespaces when there are nested interactive regions. ([#57029](https://github.com/WordPress/gutenberg/pull/57029)) diff --git a/packages/interactivity/src/directives.js b/packages/interactivity/src/directives.js index e9cc66fa50a6f0..cddab32079823e 100644 --- a/packages/interactivity/src/directives.js +++ b/packages/interactivity/src/directives.js @@ -16,7 +16,6 @@ import { deepSignal, peek } from 'deepsignal'; import { createPortal } from './portals'; import { useWatch, useInit } from './utils'; import { directive } from './hooks'; -import { SlotProvider, Slot, Fill } from './slots'; import { navigate } from './router'; const isObject = ( item ) => @@ -333,64 +332,6 @@ export default () => { element.props.children = evaluate( entry ); } ); - // data-wp-slot - directive( - 'slot', - ( { directives: { slot }, props: { children }, element } ) => { - const { value } = slot.find( - ( { suffix } ) => suffix === 'default' - ); - const name = typeof value === 'string' ? value : value.name; - const position = value.position || 'children'; - - if ( position === 'before' ) { - return ( - <> - - { children } - - ); - } - if ( position === 'after' ) { - return ( - <> - { children } - - - ); - } - if ( position === 'replace' ) { - return { children }; - } - if ( position === 'children' ) { - element.props.children = ( - { element.props.children } - ); - } - }, - { priority: 4 } - ); - - // data-wp-fill - directive( - 'fill', - ( { directives: { fill }, props: { children }, evaluate } ) => { - const entry = fill.find( ( { suffix } ) => suffix === 'default' ); - const slot = evaluate( entry ); - return { children }; - }, - { priority: 4 } - ); - - // data-wp-slot-provider - directive( - 'slot-provider', - ( { props: { children } } ) => ( - { children } - ), - { priority: 4 } - ); - // data-wp-run directive( 'run', ( { directives: { run }, evaluate } ) => { run.forEach( ( entry ) => evaluate( entry ) ); diff --git a/packages/interactivity/src/slots.js b/packages/interactivity/src/slots.js deleted file mode 100644 index e8bc6ddfa368f5..00000000000000 --- a/packages/interactivity/src/slots.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * External dependencies - */ -import { createContext } from 'preact'; -import { useContext, useEffect } from 'preact/hooks'; -import { signal } from '@preact/signals'; - -const slotsContext = createContext(); - -export const Fill = ( { slot, children } ) => { - const slots = useContext( slotsContext ); - - useEffect( () => { - if ( slot ) { - slots.value = { ...slots.value, [ slot ]: children }; - return () => { - slots.value = { ...slots.value, [ slot ]: null }; - }; - } - }, [ slots, slot, children ] ); - - return !! slot ? null : children; -}; - -export const SlotProvider = ( { children } ) => { - return ( - // TODO: We can change this to use deepsignal once this PR is merged. - // https://github.com/luisherranz/deepsignal/pull/38 - - { children } - - ); -}; - -export const Slot = ( { name, children } ) => { - const slots = useContext( slotsContext ); - return slots.value[ name ] || children; -}; diff --git a/test/e2e/specs/interactivity/directive-slots.spec.ts b/test/e2e/specs/interactivity/directive-slots.spec.ts deleted file mode 100644 index 195af52fdb1bd2..00000000000000 --- a/test/e2e/specs/interactivity/directive-slots.spec.ts +++ /dev/null @@ -1,186 +0,0 @@ -/** - * Internal dependencies - */ -import { test, expect } from './fixtures'; - -test.describe( 'data-wp-slot', () => { - test.beforeAll( async ( { interactivityUtils: utils } ) => { - await utils.activatePlugins(); - await utils.addPostWithBlock( 'test/directive-slots' ); - } ); - - test.beforeEach( async ( { interactivityUtils: utils, page } ) => { - await page.goto( utils.getLink( 'test/directive-slots' ) ); - } ); - - test.afterAll( async ( { interactivityUtils: utils } ) => { - await utils.deactivatePlugins(); - await utils.deleteAllPosts(); - } ); - - test( 'should render the fill in its children by default', async ( { - page, - } ) => { - const slot1 = page.getByTestId( 'slot-1' ); - const slots = page.getByTestId( 'slots' ); - const fillContainer = page.getByTestId( 'fill-container' ); - - await page.getByTestId( 'slot-1-button' ).click(); - - await expect( fillContainer ).toBeEmpty(); - await expect( slot1.getByTestId( 'fill' ) ).toBeVisible(); - await expect( slot1 ).toHaveText( 'fill inside slot 1' ); - await expect( slots.locator( 'css= > *' ) ).toHaveText( [ - 'fill inside slot 1', - '[2]', - '[3]', - '[4]', - '[5]', - ] ); - } ); - - test( 'should render the fill before if specified', async ( { page } ) => { - const slot2 = page.getByTestId( 'slot-2' ); - const slots = page.getByTestId( 'slots' ); - const fillContainer = page.getByTestId( 'fill-container' ); - - await page.getByTestId( 'slot-2-button' ).click(); - - await expect( fillContainer ).toBeEmpty(); - await expect( slot2 ).toHaveText( '[2]' ); - await expect( slots.getByTestId( 'fill' ) ).toBeVisible(); - await expect( slots.locator( 'css= > *' ) ).toHaveText( [ - '[1]', - 'fill inside slots', - '[2]', - '[3]', - '[4]', - '[5]', - ] ); - } ); - - test( 'should render the fill after if specified', async ( { page } ) => { - const slot3 = page.getByTestId( 'slot-3' ); - const slots = page.getByTestId( 'slots' ); - const fillContainer = page.getByTestId( 'fill-container' ); - - await page.getByTestId( 'slot-3-button' ).click(); - - await expect( fillContainer ).toBeEmpty(); - await expect( slot3 ).toHaveText( '[3]' ); - await expect( slots.getByTestId( 'fill' ) ).toBeVisible(); - await expect( slots.locator( 'css= > *' ) ).toHaveText( [ - '[1]', - '[2]', - '[3]', - 'fill inside slots', - '[4]', - '[5]', - ] ); - } ); - - test( 'should render the fill in its children if specified', async ( { - page, - } ) => { - const slot4 = page.getByTestId( 'slot-4' ); - const slots = page.getByTestId( 'slots' ); - const fillContainer = page.getByTestId( 'fill-container' ); - - await page.getByTestId( 'slot-4-button' ).click(); - - await expect( fillContainer ).toBeEmpty(); - await expect( slot4.getByTestId( 'fill' ) ).toBeVisible(); - await expect( slot4 ).toHaveText( 'fill inside slot 4' ); - await expect( slots.locator( 'css= > *' ) ).toHaveText( [ - '[1]', - '[2]', - '[3]', - 'fill inside slot 4', - '[5]', - ] ); - } ); - - test( 'should be replaced by the fill if specified', async ( { page } ) => { - const slot5 = page.getByTestId( 'slot-5' ); - const slots = page.getByTestId( 'slots' ); - const fillContainer = page.getByTestId( 'fill-container' ); - - await page.getByTestId( 'slot-5-button' ).click(); - - await expect( fillContainer ).toBeEmpty(); - await expect( slot5 ).toBeHidden(); - await expect( slots.getByTestId( 'fill' ) ).toBeVisible(); - await expect( slots.locator( 'css= > *' ) ).toHaveText( [ - '[1]', - '[2]', - '[3]', - '[4]', - 'fill inside slots', - ] ); - } ); - - test( 'should keep the fill in its original position if no slot matches', async ( { - page, - } ) => { - const fillContainer = page.getByTestId( 'fill-container' ); - await expect( fillContainer.getByTestId( 'fill' ) ).toBeVisible(); - - await page.getByTestId( 'slot-1-button' ).click(); - - await expect( fillContainer ).toBeEmpty(); - - await page.getByTestId( 'reset' ).click(); - - await expect( fillContainer.getByTestId( 'fill' ) ).toBeVisible(); - } ); - - test( 'should not be re-mounted when adding the fill before', async ( { - page, - } ) => { - const slot2 = page.getByTestId( 'slot-2' ); - const slots = page.getByTestId( 'slots' ); - - await expect( slot2 ).toHaveText( '[2]' ); - - await slot2.click(); - - await expect( slot2 ).toHaveText( '[2 updated]' ); - - await page.getByTestId( 'slot-2-button' ).click(); - - await expect( slots.getByTestId( 'fill' ) ).toBeVisible(); - await expect( slots.locator( 'css= > *' ) ).toHaveText( [ - '[1]', - 'fill inside slots', - '[2 updated]', - '[3]', - '[4]', - '[5]', - ] ); - } ); - - test( 'should not be re-mounted when adding the fill after', async ( { - page, - } ) => { - const slot3 = page.getByTestId( 'slot-3' ); - const slots = page.getByTestId( 'slots' ); - - await expect( slot3 ).toHaveText( '[3]' ); - - await slot3.click(); - - await expect( slot3 ).toHaveText( '[3 updated]' ); - - await page.getByTestId( 'slot-3-button' ).click(); - - await expect( slots.getByTestId( 'fill' ) ).toBeVisible(); - await expect( slots.locator( 'css= > *' ) ).toHaveText( [ - '[1]', - '[2]', - '[3 updated]', - 'fill inside slots', - '[4]', - '[5]', - ] ); - } ); -} ); From 45eb9a5b1e3aa57b163ec8f1651e89881ca22773 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Tue, 16 Jan 2024 10:17:44 +0100 Subject: [PATCH 17/18] Editor styles: memo transform (#57810) --- .../src/utils/transform-styles/index.js | 89 +++++++++++-------- 1 file changed, 52 insertions(+), 37 deletions(-) diff --git a/packages/block-editor/src/utils/transform-styles/index.js b/packages/block-editor/src/utils/transform-styles/index.js index 742e3a9becaef3..808566776f6ea3 100644 --- a/packages/block-editor/src/utils/transform-styles/index.js +++ b/packages/block-editor/src/utils/transform-styles/index.js @@ -5,6 +5,52 @@ import postcss, { CssSyntaxError } from 'postcss'; import wrap from 'postcss-prefixwrap'; import rebaseUrl from 'postcss-urlrebase'; +const transformStylesCache = new WeakMap(); + +function transformStyle( + { css, ignoredSelectors = [], baseURL }, + wrapperSelector = '' +) { + // When there is no wrapper selector or base URL, there is no need + // to transform the CSS. This is most cases because in the default + // iframed editor, no wrapping is needed, and not many styles + // provide a base URL. + if ( ! wrapperSelector && ! baseURL ) { + return css; + } + + try { + return postcss( + [ + wrapperSelector && + wrap( wrapperSelector, { + ignoredSelectors: [ + ...ignoredSelectors, + wrapperSelector, + ], + } ), + baseURL && rebaseUrl( { rootUrl: baseURL } ), + ].filter( Boolean ) + ).process( css, {} ).css; // use sync PostCSS API + } catch ( error ) { + if ( error instanceof CssSyntaxError ) { + // eslint-disable-next-line no-console + console.warn( + 'wp.blockEditor.transformStyles Failed to transform CSS.', + error.message + '\n' + error.showSourceCode( false ) + ); + } else { + // eslint-disable-next-line no-console + console.warn( + 'wp.blockEditor.transformStyles Failed to transform CSS.', + error + ); + } + + return null; + } +} + /** * Applies a series of CSS rule transforms to wrap selectors inside a given class and/or rewrite URLs depending on the parameters passed. * @@ -18,45 +64,14 @@ import rebaseUrl from 'postcss-urlrebase'; * @return {Array} converted rules. */ const transformStyles = ( styles, wrapperSelector = '' ) => { - return styles.map( ( { css, ignoredSelectors = [], baseURL } ) => { - // When there is no wrapper selector or base URL, there is no need - // to transform the CSS. This is most cases because in the default - // iframed editor, no wrapping is needed, and not many styles - // provide a base URL. - if ( ! wrapperSelector && ! baseURL ) { - return css; + return styles.map( ( style ) => { + if ( transformStylesCache.has( style ) ) { + return transformStylesCache.get( style ); } - try { - return postcss( - [ - wrapperSelector && - wrap( wrapperSelector, { - ignoredSelectors: [ - ...ignoredSelectors, - wrapperSelector, - ], - } ), - baseURL && rebaseUrl( { rootUrl: baseURL } ), - ].filter( Boolean ) - ).process( css, {} ).css; // use sync PostCSS API - } catch ( error ) { - if ( error instanceof CssSyntaxError ) { - // eslint-disable-next-line no-console - console.warn( - 'wp.blockEditor.transformStyles Failed to transform CSS.', - error.message + '\n' + error.showSourceCode( false ) - ); - } else { - // eslint-disable-next-line no-console - console.warn( - 'wp.blockEditor.transformStyles Failed to transform CSS.', - error - ); - } - - return null; - } + const transformedStyle = transformStyle( style, wrapperSelector ); + transformStylesCache.set( style, transformedStyle ); + return transformedStyle; } ); }; From 33b6e641d6f3737a8880d22bad77cb4ee53be602 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Tue, 16 Jan 2024 10:21:09 +0100 Subject: [PATCH 18/18] Iframe: calc compat styles once per page load (#57798) --- .../iframe/get-compatibility-styles.js | 120 +++++++++++++++++ .../src/components/iframe/index.js | 5 +- .../iframe/use-compatibility-styles.js | 122 ------------------ 3 files changed, 122 insertions(+), 125 deletions(-) create mode 100644 packages/block-editor/src/components/iframe/get-compatibility-styles.js delete mode 100644 packages/block-editor/src/components/iframe/use-compatibility-styles.js diff --git a/packages/block-editor/src/components/iframe/get-compatibility-styles.js b/packages/block-editor/src/components/iframe/get-compatibility-styles.js new file mode 100644 index 00000000000000..ab80e0e8eb43ea --- /dev/null +++ b/packages/block-editor/src/components/iframe/get-compatibility-styles.js @@ -0,0 +1,120 @@ +let compatibilityStyles = null; + +/** + * Returns a list of stylesheets that target the editor canvas. A stylesheet is + * considered targetting the editor a canvas if it contains the + * `editor-styles-wrapper`, `wp-block`, or `wp-block-*` class selectors. + * + * Ideally, this hook should be removed in the future and styles should be added + * explicitly as editor styles. + */ +export function getCompatibilityStyles() { + if ( compatibilityStyles ) { + return compatibilityStyles; + } + + // Only memoize the result once on load, since these stylesheets should not + // change. + compatibilityStyles = Array.from( document.styleSheets ).reduce( + ( accumulator, styleSheet ) => { + try { + // May fail for external styles. + // eslint-disable-next-line no-unused-expressions + styleSheet.cssRules; + } catch ( e ) { + return accumulator; + } + + const { ownerNode, cssRules } = styleSheet; + + // Stylesheet is added by another stylesheet. See + // https://developer.mozilla.org/en-US/docs/Web/API/StyleSheet/ownerNode#notes. + if ( ownerNode === null ) { + return accumulator; + } + + if ( ! cssRules ) { + return accumulator; + } + + // Don't try to add the reset styles, which were removed as a dependency + // from `edit-blocks` for the iframe since we don't need to reset admin + // styles. + if ( ownerNode.id === 'wp-reset-editor-styles-css' ) { + return accumulator; + } + + // Don't try to add styles without ID. Styles enqueued via the WP dependency system will always have IDs. + if ( ! ownerNode.id ) { + return accumulator; + } + + function matchFromRules( _cssRules ) { + return Array.from( _cssRules ).find( + ( { + selectorText, + conditionText, + cssRules: __cssRules, + } ) => { + // If the rule is conditional then it will not have selector text. + // Recurse into child CSS ruleset to determine selector eligibility. + if ( conditionText ) { + return matchFromRules( __cssRules ); + } + + return ( + selectorText && + ( selectorText.includes( + '.editor-styles-wrapper' + ) || + selectorText.includes( '.wp-block' ) ) + ); + } + ); + } + + if ( matchFromRules( cssRules ) ) { + const isInline = ownerNode.tagName === 'STYLE'; + + if ( isInline ) { + // If the current target is inline, + // it could be a dependency of an existing stylesheet. + // Look for that dependency and add it BEFORE the current target. + const mainStylesCssId = ownerNode.id.replace( + '-inline-css', + '-css' + ); + const mainStylesElement = + document.getElementById( mainStylesCssId ); + if ( mainStylesElement ) { + accumulator.push( mainStylesElement.cloneNode( true ) ); + } + } + + accumulator.push( ownerNode.cloneNode( true ) ); + + if ( ! isInline ) { + // If the current target is not inline, + // we still look for inline styles that could be relevant for the current target. + // If they exist, add them AFTER the current target. + const inlineStylesCssId = ownerNode.id.replace( + '-css', + '-inline-css' + ); + const inlineStylesElement = + document.getElementById( inlineStylesCssId ); + if ( inlineStylesElement ) { + accumulator.push( + inlineStylesElement.cloneNode( true ) + ); + } + } + } + + return accumulator; + }, + [] + ); + + return compatibilityStyles; +} diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 1939f75811c8c5..de482c5f059dc8 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -28,7 +28,7 @@ import { useSelect } from '@wordpress/data'; */ import { useBlockSelectionClearer } from '../block-selection-clearer'; import { useWritingFlow } from '../writing-flow'; -import { useCompatibilityStyles } from './use-compatibility-styles'; +import { getCompatibilityStyles } from './get-compatibility-styles'; import { store as blockEditorStore } from '../../store'; function bubbleEvent( event, Constructor, frame ) { @@ -121,7 +121,6 @@ function Iframe( { const { styles = '', scripts = '' } = resolvedAssets; const [ iframeDocument, setIframeDocument ] = useState(); const [ bodyClasses, setBodyClasses ] = useState( [] ); - const compatStyles = useCompatibilityStyles(); const clearerRef = useBlockSelectionClearer(); const [ before, writingFlowRef, after ] = useWritingFlow(); const [ contentResizeListener, { height: contentHeight } ] = @@ -156,7 +155,7 @@ function Iframe( { contentDocument.dir = ownerDocument.dir; - for ( const compatStyle of compatStyles ) { + for ( const compatStyle of getCompatibilityStyles() ) { if ( contentDocument.getElementById( compatStyle.id ) ) { continue; } diff --git a/packages/block-editor/src/components/iframe/use-compatibility-styles.js b/packages/block-editor/src/components/iframe/use-compatibility-styles.js deleted file mode 100644 index eb738c7ebefdfe..00000000000000 --- a/packages/block-editor/src/components/iframe/use-compatibility-styles.js +++ /dev/null @@ -1,122 +0,0 @@ -/** - * WordPress dependencies - */ -import { useMemo } from '@wordpress/element'; - -/** - * Returns a list of stylesheets that target the editor canvas. A stylesheet is - * considered targetting the editor a canvas if it contains the - * `editor-styles-wrapper`, `wp-block`, or `wp-block-*` class selectors. - * - * Ideally, this hook should be removed in the future and styles should be added - * explicitly as editor styles. - */ -export function useCompatibilityStyles() { - // Only memoize the result once on load, since these stylesheets should not - // change. - return useMemo( () => { - // Search the document for stylesheets targetting the editor canvas. - return Array.from( document.styleSheets ).reduce( - ( accumulator, styleSheet ) => { - try { - // May fail for external styles. - // eslint-disable-next-line no-unused-expressions - styleSheet.cssRules; - } catch ( e ) { - return accumulator; - } - - const { ownerNode, cssRules } = styleSheet; - - // Stylesheet is added by another stylesheet. See - // https://developer.mozilla.org/en-US/docs/Web/API/StyleSheet/ownerNode#notes. - if ( ownerNode === null ) { - return accumulator; - } - - if ( ! cssRules ) { - return accumulator; - } - - // Don't try to add the reset styles, which were removed as a dependency - // from `edit-blocks` for the iframe since we don't need to reset admin - // styles. - if ( ownerNode.id === 'wp-reset-editor-styles-css' ) { - return accumulator; - } - - // Don't try to add styles without ID. Styles enqueued via the WP dependency system will always have IDs. - if ( ! ownerNode.id ) { - return accumulator; - } - - function matchFromRules( _cssRules ) { - return Array.from( _cssRules ).find( - ( { - selectorText, - conditionText, - cssRules: __cssRules, - } ) => { - // If the rule is conditional then it will not have selector text. - // Recurse into child CSS ruleset to determine selector eligibility. - if ( conditionText ) { - return matchFromRules( __cssRules ); - } - - return ( - selectorText && - ( selectorText.includes( - '.editor-styles-wrapper' - ) || - selectorText.includes( '.wp-block' ) ) - ); - } - ); - } - - if ( matchFromRules( cssRules ) ) { - const isInline = ownerNode.tagName === 'STYLE'; - - if ( isInline ) { - // If the current target is inline, - // it could be a dependency of an existing stylesheet. - // Look for that dependency and add it BEFORE the current target. - const mainStylesCssId = ownerNode.id.replace( - '-inline-css', - '-css' - ); - const mainStylesElement = - document.getElementById( mainStylesCssId ); - if ( mainStylesElement ) { - accumulator.push( - mainStylesElement.cloneNode( true ) - ); - } - } - - accumulator.push( ownerNode.cloneNode( true ) ); - - if ( ! isInline ) { - // If the current target is not inline, - // we still look for inline styles that could be relevant for the current target. - // If they exist, add them AFTER the current target. - const inlineStylesCssId = ownerNode.id.replace( - '-css', - '-inline-css' - ); - const inlineStylesElement = - document.getElementById( inlineStylesCssId ); - if ( inlineStylesElement ) { - accumulator.push( - inlineStylesElement.cloneNode( true ) - ); - } - } - } - - return accumulator; - }, - [] - ); - }, [] ); -}