Skip to content

feat(popover): add PopoverOverlay - #10800

Draft
huyanxius wants to merge 9 commits into
adobe:mainfrom
huyanxius:feat/popover-overlay
Draft

huyanxius wants to merge 9 commits into
adobe:mainfrom
huyanxius:feat/popover-overlay

Conversation

@huyanxius

@huyanxius huyanxius commented Oct 17, 2026 •

Copy link
Copy Markdown

Closes #5231

Why

Issue #5231 describes positioned outside controls remaining clickable above the default transparent underlay. Applications also need a supported way to style that underlay. This follows the maintainer-proposed PopoverOverlay wrapper API, while keeping positioning and dismissal on Popover.

Summary

Add an optional PopoverOverlay wrapper so applications can style the existing popover backdrop, including its stacking order and independent entry/exit animations. An ordinary Popover keeps its default backdrop.

The wrapper shares the trigger state and passes props/ref to Popover's existing underlay. Popover retains ownership of positioning, focus, dismissal, and submenu grouping. The backdrop stays outside the existing group container, and the overlay remains mounted until both backdrop and content exit animations finish. Non-modal popovers and submenus do not gain extra backdrops.

The latest commit, 08b7ce68, also retains the CustomOverlay example's final popover exit appearance while the longer backdrop exit finishes, preventing the content from flashing back. The documentation explains animation-fill-mode: forwards for unequal keyframe durations.

This change was AI-assisted. The assistant was given AGENTS.md and the contribution/testing guidance. The contributor's own review and ownership remain required before marking this draft ready.

Scope and maintainer feedback

This draft contains the wrapper implementation, its seven existing Popover export surfaces, unit/SSR/browser coverage, one Storybook example, and Popover documentation. It does not change dependencies or add a new overlay interaction hook.

Please review the wrapper API and trigger-versus-standalone state ownership, the sibling-underlay DOM arrangement, and the rule that both backdrop and content exit animations must finish before unmounting.

✅ Pull Request Checklist:

  • Included link to corresponding React Spectrum GitHub Issue.
  • Added/updated unit tests and storybook for this change.
  • Filled out test instructions.
  • Updated documentation.
  • Looked at the Accessibility Practices for this feature - Aria Practices
  • I understand every change in this PR and can explain why it's there.
  • If AI-assisted, I followed the AI contribution guidance and pointed my assistant at AGENTS.md.

📝 Test Instructions:

Current head: 08b7ce68a760ab32a8afde702bf15fc5f1b60396.

Exact-head upstream CircleCI passed its unit and SSR jobs for the default React version and React 16/17/18, aggregate lint (including repository TypeScript), package build, API/type comparison, documentation checks, and both Storybook builds.

The browser job passed 441 tests, with 78 skipped across Chromium, Firefox, and WebKit. All 8 Popover browser cases passed in each browser: independent entry animations; both exit completion orders and focus restoration; CSS transitions; reopening during exit; elevated outside-control hit testing; and skipped entry/exit animations. These are automated browser results, not manual acceptance.

The former local TypeScript and browser-startup gaps are superseded by the successful upstream jobs. The CI Storybook URL currently returns BlobNotFound. Supplemental desktop Chrome checks therefore used an isolated Parcel page that imports the unchanged component sources and the unchanged CustomOverlay story/CSS from this exact head, with React 19.3.0. This was not a run of the full Storybook manager.

Supplemental interaction results:

  • Natural 300 ms content / 600 ms backdrop exits: both stayed mounted after the short exit; content opacity remained 0, then both unmounted after the longer exit. Close, Escape, and outside-pointer dismissal restored trigger focus.
  • Reverse 200 ms backdrop / 600 ms content exits: the transparent backdrop stayed mounted with the still-animating content until both finished.
  • A real pointer press at the elevated Outside action button's position dismissed the popover without clicking the button; the click counter stayed unchanged. Clicking the button after dismissal incremented it.
  • An ordinary Popover retained one default underlay, with no custom backdrop, and Escape restored trigger focus.
  • A submenu inside a Modal shared exactly one custom backdrop and one popover group. Choosing its Email action closed the menus and restored the Menu trigger; clicking the menu backdrop also retained the Modal. Closing the Modal restored its own trigger.

To reproduce, start yarn start-parcel3, open React Aria Components/Popover/CustomOverlay, and exercise Close/Escape/outside dismissal while observing the unequal exit durations. Also compare an ordinary Popover and nested menus in a Modal. Select/ComboBox were subsequently checked in the isolated exact-source page using installed Chrome 155.0.8059.85 and Playwright 1.62.1:

  • Select ArrowDown/Enter updates the value and restores trigger focus; Escape preserves the selection. ComboBox filtering, virtual focus, Enter selection, Tab/Escape, and a nonblocking outside click passed without a custom backdrop or default underlay.
  • At a 320 × 640 CSS viewport with RTL (ar-AE) and CSS color-scheme dark, and at 640 × 800 with LTR/light, popovers stayed within the viewport and the page had no horizontal overflow. The long Select option wrapped. Forced-colors engine emulation passed the same interactions; this is not an OS high-contrast-theme certification.
  • Browser-emulated Select touch selection/outside dismissal passed. In emulated 2× pinch/page scale, the overlay remained mounted and selection worked after restoring scale; this is not desktop browser UI zoom or a real-device pinch test.

Two limits remain: 200% CSS zoom displaced the popup in both this PR and a control using the unchanged Popover implementation; the positioning source files are identical. That simulation is not counted as a passing native browser-zoom check. ComboBox touch-emulated option tapping was inconclusive: the PR fixture reported an intercepted tap, while the unchanged control returned from the tap without selecting the value. Neither result is claimed as touch acceptance or proof of a defect introduced by this wrapper.

No real-device touch/virtual-keyboard, screen-reader announcement, native desktop browser-zoom, or full Spectrum/OS theme matrix verification is claimed. The RTL, light/dark, viewport, forced-colors, and pinch results above are narrowly scoped to the supplemental page and explicitly identified emulation. Chromatic remains with maintainers. PR Title Lint is currently action_required with no job; it has not passed, even though the existing title matches the workflow's regex.

🧢 Your Project:

Community contribution for #5231.

Popover backdrops need a supported surface for custom styles.

Expose a wrapper that customizes the existing underlay through every Popover export.

Preserve default behavior while coordinating backdrop and content exit animations.
Custom backdrops must preserve existing popover interactions.

Cover state ownership, rendering, dismissal, focus, portals, Select, ComboBox, and nested menus.

Document the expected composition behavior through unit regressions.
The public wrapper must support server-rendered applications.

Add a StrictMode SSR hydration case using the public Popover entry point.

Verify that the dialog and custom backdrop appear after hydration.
Backdrop layering and animations need real browser coverage.

Exercise entry, exit ordering, transitions, reopening, pointer hit testing, and skipped animations.

Capture the browser behavior expected from a custom PopoverOverlay.
Applications need an example of a styled popover backdrop.

Show a custom stacking order with independent backdrop entry and exit animations.

Provide a Storybook composition for manual interaction review.
The backdrop API needs guidance alongside Popover documentation.

Describe styling, sibling portal elements, state ownership, animations, and the exported props.

Help applications customize modal backdrops without changing Popover positioning.
The skipped-exit test can observe DOM before React commits dismissal in Firefox.

Wait up to 1000 ms for each removal and assert that focus returns to the trigger.

Keep the long animation negative control effective without changing runtime behavior.
@huyanxius

Copy link
Copy Markdown
Author

Updated in 1754c8189d7e78e9950cdd473261855602217070: the two removal assertions now wait (1,000 ms each) for React's dismissal commit, then verify trigger focus. This changes only test synchronization; runtime code and the 100-second CSS animation are unchanged.

Reviewed patch validation: Chromium 8/8, oxfmt and oxlint passed. The negative control (removing shouldSkipAnimation) still failed at the 1,000 ms assertion deadline, so a non-skipped animation remains detectable. Firefox could not launch on the cloud validation host; Firefox and the full browser matrix still require the new upstream CI. No browser tests were rerun on this submission Mac.

The CustomOverlay popover could reappear after its exit animation while the longer backdrop animation was still running.

Retain the final keyframe while exiting and document fill mode for CSS keyframe exits with different durations.

Keep the example hidden until both exits finish without changing runtime behavior.
Keep the padded popover inside the viewport and scroll overflowing
content within its own surface. Add an actual-story browser regression
for short, narrow viewports and keyboard dismissal/focus restoration.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ability to style the Popover overlay

1 participant