diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-regions/render.php b/packages/e2e-tests/plugins/interactive-blocks/router-regions/render.php index 3b9f8429c18823..b8fd920d8ea56b 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/router-regions/render.php +++ b/packages/e2e-tests/plugins/interactive-blocks/router-regions/render.php @@ -11,6 +11,7 @@

Region 1

@@ -54,8 +55,11 @@

Region 2

diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-regions/view.js b/packages/e2e-tests/plugins/interactive-blocks/router-regions/view.js index 836bb8366a58bd..80dd0dc263bc8f 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/router-regions/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/router-regions/view.js @@ -64,5 +64,8 @@ const { state } = store( 'router-regions', { init() { state.initCount += 1; }, + nope() { + // This function does nothing. + }, }, } ); diff --git a/packages/interactivity-router/CHANGELOG.md b/packages/interactivity-router/CHANGELOG.md index e2f6b2fa51f08b..8e8a188bfb6af0 100644 --- a/packages/interactivity-router/CHANGELOG.md +++ b/packages/interactivity-router/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Bug Fixes + +- Prevent router regions with data-wp-key from being recreated on navigation. ([#74750](https://github.com/WordPress/gutenberg/pull/74750)) + ## 2.38.0 (2026-01-16) ## 2.36.0 (2025-11-26) diff --git a/packages/interactivity-router/src/index.ts b/packages/interactivity-router/src/index.ts index 424027c5fd7a56..cdaa4675c9c1f8 100644 --- a/packages/interactivity-router/src/index.ts +++ b/packages/interactivity-router/src/index.ts @@ -23,7 +23,7 @@ const { populateServerData, batch, routerRegions, - cloneElement, + h: createElement, navigationSignal, } = privateApis( 'I acknowledge that using private APIs means my theme or plugin will inevitably break in the next version of WordPress.' @@ -117,7 +117,7 @@ const cloneRouterRegionContent = ( vdom: any ) => { : allPriorityLevels; return priorityLevels.length > 0 - ? cloneElement( vdom, { + ? createElement( vdom.type, { ...vdom.props, priorityLevels, } ) diff --git a/test/e2e/specs/interactivity/router-regions.spec.ts b/test/e2e/specs/interactivity/router-regions.spec.ts index 130547518b72b1..021d9e3fc11760 100644 --- a/test/e2e/specs/interactivity/router-regions.spec.ts +++ b/test/e2e/specs/interactivity/router-regions.spec.ts @@ -124,6 +124,7 @@ test.describe( 'Router regions', () => { test( 'should preserve state across pages', async ( { page } ) => { const counter = page.getByTestId( 'state-counter' ); + await expect( counter ).toHaveText( '0' ); await counter.click( { clickCount: 3, delay: 50 } ); @@ -477,4 +478,119 @@ test.describe( 'Router regions', () => { await expect( region7 ).toBeVisible(); await expect( region8 ).toBeVisible(); } ); + + test( 'should be preserved on first navigation without `data-wp-key`', async ( { + page, + } ) => { + const region1 = page.getByTestId( 'region-1' ); + const region1Text = page.getByTestId( 'region-1-text' ); + await expect( region1Text ).toHaveText( 'hydrated' ); + + // Adds a tag to know whether the counter element was replaced. + await region1.evaluate( ( ref ) => { + if ( ref instanceof HTMLElement ) { + ref.dataset.tag = 'region-1'; + } + } ); + + // Navigate to the next page. + await page.getByTestId( 'next' ).click(); + await expect( page ).toHaveTitle( + 'router regions – page 2 – gutenberg' + ); + + // The region element should retain the same attributes when it doesn't change. + await expect( region1 ).toHaveAttribute( 'data-tag', 'region-1' ); + } ); + + test( 'should be preserved on first navigation with `data-wp-key` and other directives ', async ( { + page, + } ) => { + const region2 = page.getByTestId( 'region-2' ); + const validInsideInteractive = page.getByTestId( + 'valid-inside-interactive' + ); + const validInsideRouterRegion = page.getByTestId( + 'valid-inside-router-region' + ); + + // Add tags to know whether the region elements were replaced. + await region2.evaluate( ( el ) => { + el.dataset.tag = 'region-2'; + } ); + await validInsideInteractive.evaluate( ( el ) => { + el.dataset.tag = 'valid-inside-interactive'; + } ); + await validInsideRouterRegion.evaluate( ( el ) => { + el.dataset.tag = 'valid-inside-router-region'; + } ); + + // Navigate to the next page. + await page.getByTestId( 'next' ).click(); + await expect( page ).toHaveTitle( + 'router regions – page 2 – gutenberg' + ); + + // Regions with `data-wp-key` and other directives should not be recreated. + await expect( region2 ).toHaveAttribute( 'data-tag', 'region-2' ); + await expect( validInsideInteractive ).toHaveAttribute( + 'data-tag', + 'valid-inside-interactive' + ); + await expect( validInsideRouterRegion ).toHaveAttribute( + 'data-tag', + 'valid-inside-router-region' + ); + } ); + + test( 'should be preserved on first navigation with `data-wp-key` and `attachTo`', async ( { + page, + interactivityUtils: utils, + } ) => { + await page.goto( + utils.getLink( 'router regions - page 1 - attachTo' ) + ); + + const bodyLocator = page.locator( 'body' ); + const regionsLocator = page.locator( '#regions-with-attach-to' ); + + const region3 = bodyLocator.getByTestId( 'region3' ); + const region4 = regionsLocator.getByTestId( 'region4' ); + const region5 = bodyLocator.getByTestId( 'region5' ); + const region6 = regionsLocator.getByTestId( 'region6' ); + + const regions: Record< string, Locator > = { + region3, + region4, + region5, + region6, + }; + + // The text of this element is used to check a navigation is completed. + const region1Ssr = page.getByTestId( 'region-1-ssr' ); + + await expect( region1Ssr ).toHaveText( 'content from page 1' ); + + // Navigate to "Page attachTo 1". + await page.getByTestId( 'next' ).click(); + await expect( region1Ssr ).toHaveText( 'content from page attachTo1' ); + + // Add tags to know whether the region elements were replaced. + for ( const regionId in regions ) { + const region = regions[ regionId ]; + await region.evaluate( ( el, id ) => { + el.dataset.tag = id; + }, regionId ); + } + + // Navigate to "Page attachTo 2". + await page.getByTestId( 'next' ).click(); + await expect( region1Ssr ).toHaveText( 'content from page attachTo2' ); + + // Regions with `data-wp-key` and other directives should not be recreated. + for ( const regionId in regions ) { + const region = regions[ regionId ]; + await expect( region ).toHaveAttribute( 'data-tag', regionId ); + } + } ); } );