DataViews: Migrate modals from @wordpress/components Modal to @wordpress/ui Dialog#76837
Closed
ciampo wants to merge 33 commits into
Closed
DataViews: Migrate modals from @wordpress/components Modal to @wordpress/ui Dialog#76837ciampo wants to merge 33 commits into
ciampo wants to merge 33 commits into
Conversation
50a77c2 to
a8b8a61
Compare
|
Size Change: +212 kB (+2.7%) Total Size: 8.08 MB 📦 View Changed
ℹ️ View Unchanged
|
|
Flaky tests detected in c3630f0. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/25064258044
|
This was referenced Mar 26, 2026
a8b8a61 to
1aa3fc8
Compare
641b86b to
cec69a8
Compare
ciampo
commented
Apr 8, 2026
64dc918 to
d482b19
Compare
d410b6f to
a2c9043
Compare
cb17dc4 to
94800e6
Compare
- Wrap the DataForm in `Drawer.Content` so long forms scroll properly while the `PostCardPanel` header and `Drawer.Footer` stay pinned (sticky default). - Use `closeModal()` directly since the prop is required by the only caller. - Document that `swipeDirection="right"` is intentionally physical (matches the previous Modal-based implementation in both LTR and RTL).
- Correct the `modalSize: 'fill'` deprecation `since` to `15.0.0` (next major for @wordpress/dataviews) and update the matching test assertion. The previous `'7.8'` value was copied over from an unrelated package and would have shown an inaccurate version in the runtime warning. - Drop the loose `as` cast in `mapModalSize` now that the function's return type is satisfied by `?? 'medium'` directly. The cast widened the type unnecessarily and would have hidden future breakage. - Wrap the action-modal body in `Dialog.Content` so long forms scroll and Header/Footer stay sticky (`Dialog.Content` is the official scroll container). - Rename the portal class to `dataviews-action-modal__portal` for BEM consistency with the popup's `dataviews-action-modal` block, and update the per-portal `--wp-ui-dialog-z-index` override accordingly. - Rewrite the `wp-ui-legacy-compat.scss` header comment as a self-contained explanation. The previous wording referenced an unpublished migration plan; the rules themselves are unchanged.
Extract the `useMapFocusOnMount` helper out of `dataviews-item-actions` into `hooks/use-map-focus-on-mount` and reuse it from the DataForm panel modal. `PanelModal` previously dropped to Base UI's smart default after the Dialog migration. The smart default is fine in many cases, but for a field-edit popup we want the first input focused (matching the legacy `Modal.focusOnMount: 'firstInputElement'` behaviour). Reusing the same helper keeps both layouts in sync and avoids duplicating the input-selector heuristic. Also wraps the `PanelModal` body in `Dialog.Content` so long forms scroll while the title and footer stay sticky, matching the action-modal treatment.
…ions
Base UI's `Dialog`/`Drawer` only fire the entry transition when the
underlying store sees `open` go from `false → true` after mount. When the
component is conditionally rendered AND mounted with `open={ true }`, the
internal `mounted`/`transitionStatus` start in their final values and the
animation never plays.
Move to the canonical pattern across the three DataViews/Edit Site call
sites: keep `Dialog.Root` / `Drawer.Root` always mounted in the React tree
and toggle `open` based on state.
- `ActionModal` now accepts a nullable `action` prop. Parents always render
it; `renderedAction` is retained internally so the popup contents survive
the exit animation, and `onOpenChangeComplete` clears it once the dialog
has finished closing.
- `PanelModal`'s heavy hooks move into a new `ModalPopup` child. The parent
always renders `Dialog.Root` and gates `ModalPopup` behind `renderPopup`
so the per-open form state still resets when the dialog closes.
- `QuickEditModal` is always rendered by `PostList` with an `open` prop;
side effects (`useSelect`, etc.) are gated on `renderPopup` to avoid
spurious entity-record fetches when Quick Edit isn't active.
The popup now unmounts asynchronously after Base UI's animation tracking
resolves, so two `dataform` tests are updated to use `waitFor` for the
"dialog removed from document" assertion.
…ckEditModal `Dialog.Portal`/`Drawer.Portal` already mount and unmount the popup DOM based on Base UI's internal `mounted` state, so the consumer-side `renderPopup` gate is only useful as a perf/UX hedge — not for correctness. Drop it where the trade-offs are acceptable: - `PanelModal`: `ModalPopup` now stays mounted and resets its in-progress edits via a `useEffect` that fires when `isOpen` flips to `false`. The per-render hooks (form validity etc.) are cheap; their JSX output only commits when Base UI's portal mounts the popup. - `QuickEditModal`: `useSelect` is gated on the `open` prop directly, so we still skip `getEditedEntityRecord` resolution for non-quick-edit selections. The cost is a brief visual blank in the form area at the start of the exit animation as `record`/`hasFinishedResolution` reset before the drawer finishes sliding out. `ActionModal` keeps its `renderedAction`/`onOpenChangeComplete` pair — the popup contents reference `renderedAction.RenderModal` directly, so losing the retained value during the exit animation would crash.
8d9e90d to
490e0a2
Compare
Refactor the internal `ActionModal` component so each instance owns one
specific action for its lifetime, rather than sharing a single
`Dialog.Root` across multiple actions and swapping which one to render.
Parents now render one `<ActionModal>` per modal action and toggle a
controlled `open` prop, instead of toggling between `null` and the
active action on a single shared instance. This removes the need for
`renderedAction` state, the setter-during-render trick, and the
`onOpenChangeComplete` callback that cleared it; the popup contents
stay rendered through the exit animation naturally because the
`action` prop never changes for that instance.
The component's prop shape changes from
`{ action: Action | null, items, closeModal }` to
`{ action: Action, items, open, onOpenChange }`. This is fully
internal — `ActionModal` is not re-exported from the package — so no
public API changes.
Refactor `PanelModal` so that the per-session state (in-progress `changes`, validity refs, content ref) lives in a dedicated `PanelModalSession` component remounted via `key`-bump from `onOpenChangeComplete` after the exit animation finishes. Previously the session component was always mounted and reset its `changes` state synchronously via a `useEffect( … on isOpen )` — which caused the form contents to flash to their reset state during the exit animation. The new approach keeps `changes` intact through the exit animation (so the form looks right while it's animating out) while still preserving the existing "Cancel/close always wipes the draft" semantic by force-remounting the session once the dialog has finished closing.
Extract the editable form portion of `QuickEditModal` into a new `QuickEditSession` component rendered as a child of `Drawer.Popup`, and remove the explicit `open` gate on the `useSelect` resolver. Base UI's `Drawer.Portal` returns `null` once the exit animation completes, so anything rendered inside `Drawer.Popup` is naturally mounted on open and unmounted on close-complete. By moving the entity-record subscription and the local edits state into the session, those concerns are now tied to the drawer's on-screen lifecycle: they engage when the drawer opens, stay alive through the exit animation (so the form keeps rendering while the drawer slides out, instead of blanking out as the resolver flips to its "closed" branch), and tear down cleanly once the drawer is fully closed. The perf benefit of skipping entity-record fetches while the drawer isn't on screen is preserved — it's just delegated to React's mount lifecycle rather than a manual `open`-prop gate.
…call sites
`ActionModal` now renders only `Dialog.Popup` and accepts a `closeModal`
callback. Each call site wraps it in a `Dialog.Root` paired with a
`Dialog.Trigger` (rendered via `Menu.Item`, plain `Button`, or
`Composite.Item`), so the trigger and the popup share the dialog's
context and the open state lives in a small per-action wrapper instead
of a parent-owned `activeModalAction` map.
This eliminates the imperative `setActiveModalAction(action)` plumbing
in `CompactItemActions`, `PrimaryActions`, `ListItem`, and
`PrimaryActionGridCell`, and replaces it with two new internal helpers
(`ModalActionMenuItem`, `ModalActionInlineButton`) that own a leaf-level
`useState`. Bulk-action triggers move from a custom `ActionTrigger`
component to a direct `Dialog.Trigger render={<Button />}` so Base UI's
ARIA wiring (`aria-haspopup="dialog"`, `aria-expanded`,
`aria-controls`) flows through automatically.
`closeModal` stays on `RenderModalProps` because the public contract
allows consumers to call it from async code; the wrapper component owns
the `useState` so the imperative path remains a one-liner
(`() => setOpen(false)`) without any direct Base UI store access.
Tests updated to render `<ActionModal>` inside a controlled
`<Dialog.Root>` instead of injecting `open`/`onOpenChange` props on
`<ActionModal>` directly.
Replace the Cancel/Apply `@wordpress/components` Buttons in
`PanelModalSession` with `Dialog.Action`, so closing flows through the
dialog primitive rather than an imperative `onClose` callback.
- Cancel becomes a propless `<Dialog.Action variant="outline">`. It
closes via Base UI's `Dialog.Close`; the existing `setTouched(true)`
side effect runs through the parent's `onOpenChange` handler.
- Apply becomes `<Dialog.Action onClick={() => onChange(changes)}>`.
`onChange` runs synchronously before Base UI fires the close, so the
draft commits before the dialog dismisses; `setTouched` follows via
`onOpenChange` exactly as for Cancel.
- `PanelModalSession` drops its `onClose` prop entirely — both buttons
now close through the Dialog primitive.
- `PanelModal` keeps its controlled `isOpen` / `setTouched` /
`sessionKey` state so `SummaryButton` (a custom div-based trigger)
can still set `aria-expanded` and the existing key-bump preserves
"Cancel/close wipes the draft" semantics across reopenings.
Drops `@wordpress/components` `Button` and the legacy
`__next40pxDefaultSize` flag from this file.
Restructure the Quick Edit drawer to match the canonical
`Drawer.Header` → (no Drawer.Description) → `Drawer.Footer` shape from
the @wordpress/ui Storybook, and route close/save through Drawer
primitives instead of imperative callbacks.
- `<Drawer.Header>` now wraps the visually-hidden `<Drawer.Title>`,
`<PostCardPanel hideActions />`, and `<Drawer.CloseIcon />`. The
drawer now gets the primitive's pinned-top header positioning and
scroll-edge separator behavior; before this change those wins were
forfeited because PostCardPanel was a `Drawer.Popup`-direct sibling.
- Drop the `onClose={ closeModal }` prop on `PostCardPanel` — close is
now a single, idiomatic affordance via `<Drawer.CloseIcon />` instead
of a duplicate ad-hoc close button rendered by PostCardPanel.
- Cancel button → `<Drawer.Action variant="outline">`. Drops legacy
`__next40pxDefaultSize` and `variant="secondary"` props (the latter is
a `@wordpress/components` Button variant; `@wordpress/ui` Button uses
`outline` for the equivalent).
- Done button → `<Drawer.Action onClick={ onSave }>`. Drawer closes
synchronously through Base UI's `Drawer.Close`, then the
`editEntityRecord` / `saveEditedEntityRecord` calls run in the
background — more responsive UX. Errors continue to surface via the
existing core-data notice path.
- `QuickEditSession` no longer needs `closeModal` as a prop; both
buttons close through the Drawer primitive.
The Quick Edit drawer keeps controlled `open` / `closeModal` props on
its top-level `<QuickEditModal>` because the trigger lives in the
post-list view (a row-level action driving a single shared drawer over
an array of `postId`s) and doesn't fit `Drawer.Trigger`'s
sibling-of-popup model.
Note for review: `<PostCardPanel>` inside `<Drawer.Header>` is the
"D1a" route from the audit plan — PostCardPanel's VStack sits as a
flex-row child alongside the close icon, leveraging the header's
shared-padding / scroll-edge separator. Worth a quick visual
verification; a fallback ("D1b") leaves PostCardPanel as a Popup-direct
sibling and only puts the title + close icon inside the header.
Replaces the legacy `@wordpress/components` `Modal` wrapper around `duplicateAction.RenderModal` with `<Dialog.Root>` + `<Dialog.Popup>` 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.
Mirrors the recent dataviews `ActionModal` refactor: the `<Modal>` wrapper becomes `<Dialog.Popup>` (with `Dialog.Header` / `Dialog.Title` / `Dialog.CloseIcon` / `Dialog.Content`), and `Dialog.Root` / `Dialog.Trigger` are hoisted to a per-action `ModalActionMenuItem` so each menu item owns its own open state. Drops the parent-level `activeModalAction` state from `PostActions` and rescopes the legacy z-index to the dialog portal class, matching the dataviews compat sheet. Together with the page-templates migration this hosts every consumer of `action.RenderModal` inside a real `Dialog` ancestor, unblocking the follow-up where each `RenderModal`'s Cancel button can adopt `Dialog.Action`.
Now that every consumer of `action.RenderModal` (DataViews item / bulk
actions, edit-site Page Templates, editor PostActions) hosts the
`RenderModal` inside a real `<Dialog.Root>`, internal `RenderModal`s can
swap their `<Button variant="tertiary" onClick={ closeModal }>` Cancel
button for the idiomatic `<Dialog.Action variant="outline">`. The
primary submit/destructive button stays a plain `<Button>` because it
runs async work, surfaces `isBusy`, and decides when to close based on
success/error.
Migrated `RenderModal`s:
- `@wordpress/fields`: `delete-post`, `permanently-delete-post`,
`trash-post`, `reset-post`, `rename-post`, `reorder-page`,
`duplicate-post`.
- `@wordpress/user-taxonomies`: `delete`.
- `@wordpress/user-post-types`: `delete`.
- `@wordpress/editor`: `set-as-homepage`, `set-as-posts-page`.
No public API changes — `RenderModalProps.closeModal` keeps its
plain-callback contract; the inner `Dialog.Action` simply closes via
the surrounding `Dialog.Root`'s `onOpenChange`, which the consumer's
state setter already wires to `closeModal`.
Bring the editor's duplicated ActionModal helpers to parity with the canonical dataviews implementation: - Propagate `action.disabled` to the underlying `Menu.Item` (was being silently dropped). - Honour `action.modalSize` (incl. the deprecated `'fill'` value) by duplicating the small `mapModalSize` helper from dataviews. - Honour `action.modalFocusOnMount` by duplicating `useMapFocusOnMount` from dataviews so `'firstInputElement'` actually targets the first input instead of falling back to the popup default. - Document, in a single comment block, that these helpers are duplicated from dataviews and should eventually move to a shared module. Also reframes the `modalHeader`-as-function support and the `hideModalHeader` alert semantics as the bug fixes they really are in the changelog (both behaviours used to silently mismatch dataviews).
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 `<duplicateAction.RenderModal>`
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.
Short-term layout fix for two issues with rendering `PostCardPanel` inside `Drawer.Header`: 1. PostCardPanel renders its own `<h2>`, which competed with the `<h2>` produced by `Drawer.Title` and resulted in two top-level headings inside the popup. 2. PostCardPanel is laid out as a vertical stack, which doesn't compose with `Drawer.Header`'s flex-row title-and-close-icon shape. Move `PostCardPanel` into `Drawer.Content` (just above the form) and leave `Drawer.Header` minimal: a visually-hidden `Drawer.Title` rendered as a `<span>` (so it doesn't introduce a second `<h2>`) plus `Drawer.CloseIcon`. Base UI still wires `aria-labelledby` automatically, so the drawer keeps an explicit accessible name. Tracked as a follow-up: integrate `PostCardPanel` properly with `Drawer.Header` so it can act as the visible drawer title.
Phase B of the Modal→Dialog migration replaced the Cancel button in
every internal `Action.RenderModal` body with `Dialog.Action`, which
requires a `@wordpress/ui` `Dialog.Root` ancestor at render time.
Every in-tree consumer of `<action.RenderModal>` (DataViews
item-actions, editor `PostActions`, edit-site page-templates) wraps
its content in `Dialog.Root`, so all in-tree call paths are safe.
External consumers that import an action and render its `RenderModal`
outside a `Dialog.Root` will crash with "Dialog parts must be placed
within `<Dialog.Root>`".
Make this contract refinement loud in the diff so it gets explicitly
discussed in PR review:
- Update the `RenderModalProps.RenderModal` JSDoc in
`@wordpress/dataviews` types to spell out the new host requirement.
- Add a short, identical header comment above every migrated
`RenderModal:` declaration (11 files across @wordpress/fields,
@wordpress/user-taxonomies, @wordpress/user-post-types,
@wordpress/editor).
- Add explicit `Breaking Changes` entries in
@wordpress/dataviews, @wordpress/fields, and @wordpress/editor
CHANGELOGs describing the contract narrowing and the upgrade path
("wrap in Dialog.Root or render through an in-tree consumer").
No behaviour change in this commit — purely documentation. The PR
description (see #76837) carries a "heads-up for reviewers" section
calling out the trade-off so it can be discussed and, if needed,
reverted in favour of host-agnostic `<Button onClick={ closeModal }>`
Cancel buttons.
…nder
`ButtonTrigger` and `MenuItemTrigger` (dataviews) and the editor's
mirrored `DropdownMenuItemTrigger` now `forwardRef` and spread unknown
props onto their underlying `Button` / `Menu.Item`. This lets the
modal-action wrappers (`ModalActionInlineButton` / `ModalActionMenuItem`)
reuse them via the render-prop pattern:
<Dialog.Trigger render={ <ButtonTrigger ... /> } />
<MenuItemTrigger ... render={ <Dialog.Trigger /> } />
instead of inlining their own copy of the trigger markup. No behaviour
change — just removes the structural duplication that R1#8 of the
self-review flagged. The triggers now have a single source of truth
for action-derived attributes (`disabled={ action.disabled }`,
`accessibleWhenDisabled`, `size`, label rendering), so future drift
between the modal and non-modal usages is structurally prevented.
Side effect: `ActionTriggerProps.onClick` is now optional, since
render-prop composition supplies the click handler from the wrapping
primitive. Documented in JSDoc.
This was referenced May 6, 2026
Contributor
Author
|
Closing this in favour of 5 smaller, self-contained drafts that split the work along host boundaries: #78028 (DataViews |
ciampo
added a commit
that referenced
this pull request
May 7, 2026
Self-review follow-ups for #78028: - CHANGELOG: retarget the four `[#76837]` links to `[#78028]` across `packages/dataviews`, `packages/edit-site`, and `packages/fields` so the entries point at the active PR (CI `Check CHANGELOG diff` fails otherwise). - CHANGELOG: reword the dataviews Breaking-Changes entry — drop the inaccurate `AlertDialog` mention (the migration imports `Dialog` only), and call out the actual mechanism for destructive actions (`Dialog.Popup` with `role="alertdialog"` + `disablePointerDismissal`). - `useMapFocusOnMount`: enumerate all five legacy `modalFocusOnMount` values (`false`, `'firstInputElement'`, `'firstContentElement'`, `'firstElement'`, `true`) explicitly in the JSDoc, even where three of them converge on the same Base UI smart default — so grepping for any of the legacy strings lands here.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds on #76487. Depends on:
stopPropagation()to Escape handler #76861,AlertDialog#76937,What?
Migrate three overlay surfaces away from
@wordpress/componentsModal:@wordpress/uiDialog(alertdialogsemantics for destructive actions).@wordpress/uiDialog.@wordpress/uiDrawer(replaces ~90 lines of custom drawer-emulation CSS).Also migrates the two remaining direct
RenderModalconsumers (Page Templates' duplicate dialog and the duplicatedActionModalin@wordpress/editor) toDialogso internalRenderModals can adoptDialog.Actionfor Cancel.Action.RenderModalhost-contract refinementPhase B of this PR replaces the Cancel button inside every internal
Action.RenderModalwith@wordpress/uiDialog.Action.Dialog.Actionrequires aDialog.Rootancestor — Base UI throws "Dialog parts must be placed within<Dialog.Root>" otherwise. Practical implications:<action.RenderModal>(DataViews item-actions, editorPostActions, edit-site page-templates) now wraps the content inDialog.Root. ✓deleteTaxonomyAction,deletePostTypeAction, thefields/*actions, the editor'sset-as-*actions) and render itsRenderModaloutside aDialog.Rootwill crash on first render of the popup body.The pre-migration implicit contract was "render
RenderModalinside any modal-like host" (<Modal>and<Dialog>both qualified). The new contract is narrower: "render inside aDialog.Rootancestor". This is a deliberate trade-off — the alternative (keep<Button onClick={ closeModal }>in everyRenderModalbody) avoids the public-API ratchet but loses idiomatic Cancel-via-Dialog-primitives. I'd appreciate explicit reviewer input on whether this trade-off is acceptable, or whether Phase B should be reverted in favour of host-agnostic<Button>Cancel buttons.Made loud in the diff via:
Breaking Changesentries in@wordpress/dataviews,@wordpress/fields, and@wordpress/editorCHANGELOGs.RenderModal:declaration (11 files) calling out the requirement and pointing toRenderModalPropsJSDoc.RenderModalProps.RenderModalinpackages/dataviews/src/types/dataviews.ts.Why?
DialogandDrawerare the recommended replacements forModal, and migrating real consumers validates the new APIs.How?
Tests land first to spec the migration, then the migration itself, then per-instance z-index scoping (via
portalfrom #77452) and the Quick EditDrawerswap.Implementation details
Dialog.Root+Dialog.Popup+Dialog.Header/Dialog.Content/Dialog.Footer. Destructive actions (hideModalHeader: true) usedisablePointerDismissalandrole="alertdialog"onDialog.Popup, with a visually-hiddenDialog.Titlefor the accessible name.Dialog.Root/Dialog.Triggerlive at the call site (per-actionModalActionMenuItem/ModalActionInlineButton) so each modal action owns its own open state.dataviews-action-modal__portalcarries the legacy--wp-ui-dialog-z-indexoverride. A smallwp-ui-legacy-compat.scssalso seeds:rootdefaults for--wp-ui-dialog-z-index/--wp-ui-tooltip-z-indexso@wordpress/uioverlays coexist with legacy@wordpress/componentsoverlays during the transition. The editor'seditor-action-modal__portalfollows the same pattern.modalSize: union extended with'stretch'and'full';'fill'deprecated (since: 15.0.0) and aliased to'stretch'.useMapFocusOnMounthelper translates the legacyfocusOnMountvalues onto Base UI'sinitialFocus. With Base UI's smart default,'firstElement'and'firstContentElement'now behave identically (both skip the close icon and focus the first content tabbable) — documented in the CHANGELOG.Drawer.Root(swipeDirection="right", physical in both LTR and RTL) +Drawer.Popup+Drawer.Header(withDrawer.Title+Drawer.CloseIcon) +Drawer.Content+Drawer.Footer. The~90 linesof drawer-emulation CSS inpost-list/style.scssare gone.Dialog.Root/Drawer.Rootwithopencontrolled by parent state, and per-session state bound to the popup's mount lifecycle so it auto-resets between sessions and stays rendered through the exit animation. Action modals render one<ActionModal>per modal action with a stableactionprop.PanelModalextracts a<PanelModalSession>whosekeyis bumped fromonOpenChangeCompleteto preserve the existing "Cancel/close always wipes the draft" semantic.QuickEditModalextracts a<QuickEditSession>as a child ofDrawer.Popupso Base UI's portal manages its mount/unmount.Dialog.Action/Drawer.Action: Cancel buttons inPanelModal,QuickEditModal, and every internalRenderModal(fields/*,user-taxonomies/delete,user-post-types/delete,editor/post-actions/set-as-*) move from<Button variant="tertiary" onClick={ closeModal }>to<Dialog.Action variant="outline">/<Drawer.Action variant="outline">. Async submit/destructive buttons stay as plain<Button>so they can surfaceisBusyand decide when to close based on success/error. See the heads-up note above for the host-contract implication..components-modal__frame/[role="document"]overrides forduplicateTemplatePartandduplicatePattern(replaced bymodalSize: 'small'on the action definitions); removed.dataforms-layouts-panel__modal-footermargin and the staledataforms-layouts-panel__modalclassName.Testing Instructions
Dialog.Trigger; Cancel viaDialog.Action, Escape, and backdrop all close.Testing Instructions for Keyboard
Open an action modal via keyboard, Tab cycles within it, Escape returns focus to the trigger, and focus on mount lands inside the content (not on the close icon).
TODO / Follow-ups
modal: { type: 'dialog' | 'confirm' }action API to replaceRenderModal/hideModalHeader(separate PR; also wherearia-describedbyfor alert dialogs gets wired in).routes/post-list/quick-edit-modal.tsx(the new admin-shell location).:root--wp-ui-*-z-indexdefaults fromwp-ui-legacy-compat.scssinto a shell-level adapter once the long-term overlay-stacking plan lands.swipeDirectioncould expose a logical inline-end value upstream so RTL flips automatically.saveEditedEntityRecorddoesn't dispatch its own snackbar, and Quick Edit doesn't passthrowOnError, so users get no feedback on success or failure (single or bulk). Follow-up should pass{ throwOnError: true }, runPromise.allSettled, and emit one aggregate snackbar per save batch — same pattern aspermanently-delete-post'sRenderModal. Fits behind the synchronous-close UX (notice fires asynchronously after the drawer dismisses).mapModalSize/useMapFocusOnMount/ActionModalshape out of@wordpress/dataviewsand@wordpress/editorinto a shared module once a neutral home exists. The two are kept in lockstep manually right now (see comment block at the top of the editor'spost-actions/index.js).Use of AI Tools
Cursor + Claude Opus 4.7