From 6b4c982bb5d122874278ff29bf6b6b4e0a30a33d Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Fri, 1 May 2026 15:59:18 +0200 Subject: [PATCH 1/2] Page Templates: migrate registered-template duplicate Modal to Dialog Replaces the legacy `@wordpress/components` `Modal` wrapper around `duplicateAction.RenderModal` with `` + `` so this consumer hosts the migrated `RenderModal` inside a real `Dialog` ancestor (a prerequisite for the action's `Cancel` button to move to `Dialog.Action`). The dialog uses a separate "is open" state so the cached template clears on `onOpenChangeComplete` rather than mid-animation, keeping the popup contents rendered through the exit transition. --- packages/edit-site/CHANGELOG.md | 4 ++ .../src/components/page-templates/index.js | 45 ++++++++++++++----- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/packages/edit-site/CHANGELOG.md b/packages/edit-site/CHANGELOG.md index 27e12aef878f8d..f5c98f6aea0285 100644 --- a/packages/edit-site/CHANGELOG.md +++ b/packages/edit-site/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Code Quality + +- Migrate the registered-template duplicate dialog in Page Templates from `@wordpress/components` `Modal` to `@wordpress/ui` `Dialog`. ([#76837](https://github.com/WordPress/gutenberg/pull/76837)) + ## 6.45.0 (2026-04-29) ## 6.44.0 (2026-04-15) diff --git a/packages/edit-site/src/components/page-templates/index.js b/packages/edit-site/src/components/page-templates/index.js index e019bf290b39dd..8f038a97d3fe16 100644 --- a/packages/edit-site/src/components/page-templates/index.js +++ b/packages/edit-site/src/components/page-templates/index.js @@ -16,7 +16,8 @@ import { addQueryArgs } from '@wordpress/url'; import { useSelect, useDispatch } from '@wordpress/data'; import { useEvent } from '@wordpress/compose'; import { useView } from '@wordpress/views'; -import { Modal } from '@wordpress/components'; +// eslint-disable-next-line @wordpress/use-recommended-components +import { Dialog } from '@wordpress/ui'; import { store as noticesStore } from '@wordpress/notices'; /** @@ -54,6 +55,7 @@ export default function PageTemplates() { const [ selection, setSelection ] = useState( [ postId ] ); const [ selectedRegisteredTemplate, setSelectedRegisteredTemplate ] = useState( false ); + const [ isDuplicateOpen, setIsDuplicateOpen ] = useState( false ); const defaultView = DEFAULT_VIEW; const activeViewOverrides = useMemo( () => getActiveViewOverridesForTab( activeView ), @@ -342,6 +344,7 @@ export default function PageTemplates() { onClickItem={ ( item ) => { if ( typeof item.id === 'string' ) { setSelectedRegisteredTemplate( item ); + setIsDuplicateOpen( true ); } else { history.navigate( `/${ item.type }/${ item.id }?canvas=edit` @@ -359,17 +362,37 @@ export default function PageTemplates() { : false } /> - { selectedRegisteredTemplate && duplicateAction && ( - setSelectedRegisteredTemplate() } - size="small" + { duplicateAction && ( + { + if ( ! isOpen ) { + // Clear the cached template only after the exit + // animation finishes — keeps the modal contents + // rendered while the dialog slides out so the + // inner DataForm doesn't flash empty. + setSelectedRegisteredTemplate( false ); + } + } } > - setSelectedRegisteredTemplate() } - /> - + + + { __( 'Duplicate' ) } + + + + { selectedRegisteredTemplate && ( + + setIsDuplicateOpen( false ) + } + /> + ) } + + + ) } ); From 4f7d353cb642fcde7a7b3ed32bcd55f34a9efe27 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Sun, 3 May 2026 15:22:56 +0200 Subject: [PATCH 2/2] Edit-site page-templates: harden duplicate dialog session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two small safeguards against the mid-animation re-open race: - Defensive setter inside `onOpenChangeComplete` so a late "closed" callback (firing after the user has already re-opened the dialog with a different template) is ignored instead of nulling the active selection mid-session. - `key={ selectedRegisteredTemplate.id }` on `` so switching templates while the dialog is still closing remounts the inner component with a fresh `useState` initializer — otherwise the previous template's "(Copy)" title persists into the new session. Same-template re-open intentionally keeps state (no key change), which matches the user's likely intent of returning to in-progress edits. --- .../src/components/page-templates/index.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/edit-site/src/components/page-templates/index.js b/packages/edit-site/src/components/page-templates/index.js index 8f038a97d3fe16..a48885806e4514 100644 --- a/packages/edit-site/src/components/page-templates/index.js +++ b/packages/edit-site/src/components/page-templates/index.js @@ -371,8 +371,14 @@ export default function PageTemplates() { // Clear the cached template only after the exit // animation finishes — keeps the modal contents // rendered while the dialog slides out so the - // inner DataForm doesn't flash empty. - setSelectedRegisteredTemplate( false ); + // inner DataForm doesn't flash empty. The + // functional setter guards against a stale + // "complete" callback firing after the user has + // already re-opened the dialog with a different + // template (mid-animation re-open race). + setSelectedRegisteredTemplate( ( prev ) => + isDuplicateOpen ? prev : false + ); } } } > @@ -383,7 +389,14 @@ export default function PageTemplates() { { selectedRegisteredTemplate && ( + // Keying on the template id ensures the + // RenderModal remounts with a fresh internal + // `useState` initializer if the user clicks a + // different row while the dialog is still + // closing — otherwise the previous template's + // title would persist into the new session. setIsDuplicateOpen( false )