UI: Add Drawer primitive#76690
Merged
Merged
Conversation
|
Size Change: 0 B Total Size: 7.76 MB ℹ️ View Unchanged
|
9c6faa6 to
669168f
Compare
|
Flaky tests detected in e8b89e0. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/24790327883
|
80739c1 to
7c2cdf2
Compare
608ee11 to
850d01d
Compare
ciampo
added a commit
that referenced
this pull request
Apr 9, 2026
- Add Dialog.Description sub-component wrapping Base UI's Dialog.Description with the Text component (variant="body-md") - Add DialogModalContext so Popup can conditionally render the backdrop only when modal === true (previously always rendered) - Extract --viewport-inset CSS variable for consistent viewport padding - Add onOpenChangeComplete to RootProps - Move component description from Storybook params to Root JSDoc Split from #76690. Made-with: Cursor
Scope will-change and transform transitions to data-open and data-swiping so compositing is not retained while idle. Add safe-area padding on left and right drawer content to match top and bottom handling. Made-with: Cursor
Add createOverlayModalContext and createOverlayTitleValidation utilities and wire Drawer, Dialog, and Popover context modules through them to remove duplication while preserving dev-only title checks and error messages. Made-with: Cursor
Use args-forwarding render functions where needed, remove redundant default args, and disable conflicting controls on stories with internal state. Made-with: Cursor
6c849a2 to
93d19bf
Compare
Render `Drawer.Header` / `Drawer.Footer` and `Dialog.Header` / `Dialog.Footer` as native `<header>` / `<footer>` landmarks by default so assistive technology can jump straight to them, useful when a surface contains many interactive elements before the action buttons. Consumers can still opt out via `render`. Made-with: Cursor
Match the non-underscored convention used by sibling custom properties (`--backdrop-opacity`, `--popup-size`). No behavior change. Made-with: Cursor
Switch the `.popup[data-swipe-direction="…"] > .content` rules to a descendant combinator. There is no expectation of nested `Drawer.Content` and the looser selector keeps the safe-area padding working through any intermediate wrappers. Made-with: Cursor
The Drawer backdrop runs a longer duration and different easing than Dialog's because it needs to match the popup's slide-in/out animation so both surfaces transition together. Add a comment explaining the intent so it is not mistaken for drift. Made-with: Cursor
Replace the custom `data-wp-ui-drawer-backdrop` / `data-wp-ui-dialog-backdrop` attributes with `data-testid` so they are clearly test-only hooks rather than looking like generic styling or behavior attributes. Update the corresponding tests to query the backdrops via `queryAllByTestId`. Made-with: Cursor
- Drawer: add a Header render/className parity test alongside the
existing Footer one so regressions in the shared `useRender` pattern
are caught.
- Drawer: assert `data-swipe-direction` on the popup while rerendering
across swipe directions so a wiring break cannot pass silently.
- Drawer: add `initialFocus={ false }` and custom-callback focus tests,
matching Dialog/Popover, to document the Drawer's own viewport
wrapper on top of the shared hook.
- Drawer: add an Escape-to-close smoke test that also asserts focus
restoration to the trigger, documenting the keyboard contract.
- Dialog, Popover: add a class-name-merge regression test for the
shared `useRender` behavior applied to Title/Description.
Made-with: Cursor
Drawer is still awaiting merge, so the entry was sitting under the already-released 0.11.0 section by mistake.
Call out the new <header>/<footer> defaults and the corresponding ref type widening so existing Dialog consumers can spot the change.
The RootProps JSDoc documented 'left' as the default, but Drawer.Root was falling through to Base UI's default of 'down', so an unconfigured drawer rendered as a bottom sheet. Forward the documented default explicitly and add a regression test that asserts the dialog element carries data-swipe-direction="left" when the prop is omitted.
- Let initial validation settle before asserting `errors.toHaveLength(0)` in the "title removed after mount" test, mirroring Dialog. - Add the "title added back" recovery case that Dialog and Popover already cover, so all three overlays exercise the shared `createOverlayTitleValidation` cleanup path.
The regex check against /title/ matched the user-supplied class `custom-title` itself, so it passed even if the module-scoped class stopped being emitted. In addition, CSS module imports are stubbed to an empty object in the Jest environment, so there is no internal hashed class to match on to begin with. Reduce the assertion to the actual regression target — the user-provided className must still reach the DOM — and document why the internal class cannot be checked here.
Drawer.Popup used to list the size modifier class before the consumer `className`; Dialog.Popup does the opposite. Align Drawer to Dialog. The DOM class-attribute order does not affect the CSS cascade, but keeping the two primitives consistent makes the pattern easier to reason about across overlay components.
The previous wording implied landmark navigation is universally available. In practice several screen readers suppress banner and contentinfo landmarks when they are nested inside a role="dialog", so the guarantee is audience-specific. Word the benefit more conservatively.
The popup itself is anchored to a physical edge regardless of writing direction (`left: 0` / `right: 0`, with stylelint overrides), but the content's safe-area padding was expressed in logical properties (`padding-inline-start` / `padding-inline-end`) while still reading physical `env(safe-area-inset-left/right)` values. In RTL the two axes swap, so a left-anchored drawer received its notch-aware padding on the detached edge. Switch the overrides to physical `padding-left` / `padding-right` so the padding always matches the side the drawer is anchored to, and add a comment explaining why.
The deferred title-validation runs inside a setTimeout, so its state updates happen during the manual 50 ms settle waits sprinkled across the validation tests. Without an act wrapper, React Testing Library occasionally logs 'not wrapped in act(...)' warnings from those updates. Apply the same pattern Tabs adopted in #77319 so all three overlay primitives behave consistently.
The existing "With Custom z-index" story admitted it couldn't actually demo the feature because a global CSS rule was required. Now that `Dialog.Popup` accepts a `portal` prop (from #77452), the override can be scoped to a single instance by setting `--wp-ui-dialog-z-index` on the `Dialog.Portal` wrapper — the variable cascades from there to both the backdrop and the popup. Update the doc to present both the global and per-instance options, and convert the story to a live demo of the per-instance approach.
Both components already demonstrate the popup-level `--wp-ui-*-z-index` override via `style`. Extend the story docs to also mention the `:root` / `body` global override and the per-instance portal override (`portal` prop + `Portal` with `style` / `className`), matching the Dialog story.
These overlays did not demonstrate the `--wp-ui-*-z-index` escape hatch. Add "With Custom z-index" stories for each that show the per-instance override via the `portal` prop (`style` on the component's `Portal`), and mention the global `:root` / `body` override in the doc. This mirrors the approach taken in the Dialog, Popover, and Select stories. `AlertDialog` reuses `Dialog`'s stylesheet, so it also uses `--wp-ui-dialog-z-index`. Also register `Tooltip.Portal` in the Storybook subcomponents list so it shows up alongside `Popup`.
Fixes CI lint failure introduced in 62d129b.
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.

Closes #75291
What?
Adds a
Drawerprimitive to@wordpress/ui: a WPDS-styled compound component wrapping Base UI's Drawer, for side panels and bottom sheets with consistent a11y, motion, and focus behavior.Compound API:
Root,Trigger,Popup,Portal,Header,Title,Description,CloseIcon,Action,Footer.Why?
Gutenberg needs a shared drawer primitive to avoid custom per-feature implementations, and to keep accessibility, motion, and interaction patterns aligned with
DialogandPopover.Testing
npm run storybook:dev→ Design System / Components / Drawer.npm run test:unit -- packages/ui/src/drawer/test/index.test.tsxnpm run test:unit -- packages/ui/src/dialog/test/index.test.tsxnpm run test:unit -- packages/ui/src/popover/test/index.test.tsxKeyboard checks
Supported props
swipeDirection:left | right | up | downleft.sizeonDrawer.Popup:small | medium | large | stretch | automediumforleft/right,autoforup/down.portal,initialFocus,finalFocusonDrawer.Popupportalaccepts aDrawer.Portalelement for customcontainer/portal options.Drawer.ActionmirrorsDialog.Action(disabled ?? loadingprecedence).Implementation notes
Drawernow follows the same portal composition pattern asDialog/Popover(Popup+Portal+renderPortalWithChildren).Drawer.Title/Drawer.Descriptionforward props to Base UI render elements and rely on Base UI merging forclassName, consistent withDialog/Popover.Drawer/Dialog/Popovervia extracted overlay utilities (same runtime behavior and error messages).DrawerandDialogvia a common overlay modal context utility.Drawer.Header/Drawer.Footer(and theirDialogcounterparts) default to native<header>/<footer>landmarks so assistive technology can jump to them directly; consumers can still opt out viarender.ThemeProviderwraps_Drawer.Popupdirectly (inside_Drawer.Viewport) so thedisplay: contentsworkaround selector applies, matching sibling overlays.[data-open]/[data-swiping]) to avoid keeping layers around while idle.padding-left/padding-rightto match the popup's physical anchoring (left: 0/right: 0), so the inset stays on the correct side in RTL.Dialog's — its duration and easing match the popup's slide-in/out (and scale with--drawer-swipe-strength) so both surfaces move together; documented inline.data-testid="{drawer,dialog}-backdrop"for test targeting (no ad-hocdata-wp-ui-*-backdropattributes).Dialog,AlertDialog,Drawer,Popover,Select,Tooltip) now has a "With Custom z-index" story that documents both the global (:root/body) and per-instance (portalprop +Portalwithstyle/className) ways to override--wp-ui-*-z-index; theDialog,AlertDialog,Drawer, andTooltipstories live-demo the per-instance approach.Test coverage
Unit tests cover: ref forwarding (including
<header>/<footer>landmark defaults),Drawer.HeaderandDrawer.Footer(children/className/render), custom portal rendering, modal/backdrop behavior (queried viadata-testid),aria-describedbywiring forDrawer.Description,Drawer.TitleclassName merging, deprioritized close-icon focus,initialFocus={ false }and custom-callback focus handling, Escape-to-close with focus restoration to the trigger,data-swipe-directionwiring across swipe directions, size resolution,Drawer.Actionloading/disabled precedence, and dev-mode title validation.A className-merge regression test is also added to
DialogandPopoverto cover the shareduseRenderbehavior.Not in scope for v1
Drawer.Provider,Drawer.IndentBackground,Drawer.IndentDrawer.SwipeAreasnapPoints,snapPoint,defaultSnapPoint,onSnapPointChange,snapToSequentialPoints)--nested-drawers)handle,triggerId,defaultTriggerId,actionsRef,Drawer.createHandle)Drawer.Contentas a dedicated public scroll-container abstractionScreen recording
Kapture.2026-04-17.at.13.44.06.mp4
Use of AI Tools
This PR was authored with assistance from an AI coding agent in Cursor. All changes were reviewed by the author.