Skip to content

Conversation

@ggdouglas
Copy link
Contributor

@ggdouglas ggdouglas commented Sep 18, 2025

Summary

Introduce PopoverNext as a modern, Floating UI–based replacement for Popover. The goal is near drop-in API compatibility to ease adoption, while migrating from Popper "modifiers" to Floating UI "middleware". This change also helps to unblock React 19 support as part of #7115.

  • Why: Popper v2 via react-popper isn’t actively maintained, and the ecosystem has moved to Floating UI. React 19 support is blocked by the legacy stack.
  • What: New PopoverNext component, docs, tests, and examples. Existing Popover component marked deprecated.
  • How:
    • Ported the existing Popover from a class-based to a functional component, preserving existing behavior where possible.
    • Maintained feature parity while switching the positioning engine to Floating UI.
    • Exposed middleware as options instead of modifiers in new PopoverNext API.
Screenshot 2025-09-18 at 13 42 19@2x

Migration

  • Prop compatibility
    • Preserved where feasible: children, renderTarget, content, isOpen/defaultIsOpen, onInteraction, onClose, usePortal, hasBackdrop, interactionKind, minimal, matchTargetWidth, etc.
  • Auto behavior
    • The "auto" placement options are not supported in PopoverNext.
    • Instead, for automatic placement behavior, omit placement entirely. When placement is undefined, PopoverNext uses Floating UI's automatic placement by default (via autoPlacement). See the docs: Floating UI autoPlacement.
  • Defaults and behavior notes
    • Boundary defaults differ:
      • Popover: boundary="clippingParents"
      • PopoverNext: boundary="clippingAncestors"
      • To match legacy behavior precisely, set boundary="clippingParents" on PopoverNext.
    • Dismiss and accessibility are preserved:
      • Classes.POPOVER_DISMISS, Classes.POPOVER_DISMISS_OVERRIDE, captureDismiss
      • aria-haspopup/aria-expanded, backdrop + focus behavior via Overlay2
  • Modifiers → Middleware mapping
    • flipflip
    • preventOverflowshift (use boundary/rootBoundary)
    • offsetoffset (mainAxis, crossAxis)
    • arrowarrow
  • Practical migration steps
    • Switch imports to PopoverNext.
    • Use the same props; modify placement, boundary, etc. where needed
    • Translate any Popper modifiers to Floating UI middleware (migration utilities forthcoming)

Follow-ups

  • Migrate internal usages (Tooltip, menus, selects) to PopoverNext.
  • Provide codemods and a migration wiki page as adoption ramps.
  • Remove legacy Popover in a future major version

References

@svc-palantir-github
Copy link

Write docs for PopoverNext and add examples

Build artifact links for this commit: documentation | landing | table | demo

This is an automated comment from the deploy-preview CircleCI job.

@ggdouglas ggdouglas force-pushed the gdouglas/popover-next branch from 5706459 to 7b8f84d Compare September 18, 2025 01:52
@svc-palantir-github
Copy link

Write docs for PopoverNext and add examples

Build artifact links for this commit: documentation | landing | table | demo

This is an automated comment from the deploy-preview CircleCI job.

@ggdouglas ggdouglas force-pushed the gdouglas/popover-next branch from 7b8f84d to e7e0cfe Compare September 18, 2025 17:12
@svc-palantir-github
Copy link

Write docs for PopoverNext and add examples

Build artifact links for this commit: documentation | landing | table | demo

This is an automated comment from the deploy-preview CircleCI job.

@ggdouglas ggdouglas force-pushed the gdouglas/popover-next branch from e7e0cfe to 8db1447 Compare September 19, 2025 20:15
@ggdouglas ggdouglas marked this pull request as ready for review September 19, 2025 20:15
@svc-palantir-github
Copy link

Write docs for PopoverNext and add examples

Build artifact links for this commit: documentation | landing | table | demo

This is an automated comment from the deploy-preview CircleCI job.

Comment on lines +149 to +158
@### Middleware

Middleware allow us to customize Floating UI's positioning behavior. **PopoverNext** configures several of Floating UI's built-in middleware to handle things such as flipping, preventing overflow from a boundary element, and positioning the arrow.

You may override the default middleware with the `middleware` prop, which is an object with key-value pairs representing the middleware name and its options object, respectively. See the [Floating UI middleware docs page](https://floating-ui.com/docs/middleware) for more info.

<div class="@ns-callout @ns-intent-warning @ns-icon-warning-sign @ns-callout-has-body-content">
<h5 class="@ns-heading">Auto placement requires autoPlacement or flip middleware</h5>

Be careful when disabling the "autoPlacement" or "flip" middleware, since automatic placement relies on them. If you _do_ decide to disable these middleware, be sure to also specify a placement which is specific (not automatic).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to support this API? It might be best to keep the simplest possible API for now until we know we need more, and even then maybe make it more semantic than just exposing all of Floating UI's API. That way, if we ever have to switch underlying libraries again, we might be able to get away without an API break next time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get the concern about keeping the API simple and future-proof, but I'm thinking we should probably keep the middleware prop for now.

The main thing is that we're going to need this flexibility pretty much immediately. Looking at the current codebase, submenus and tooltips already rely on custom modifiers for things like offset, flip, and preventOverflow.

const SUBMENU_POPOVER_MODIFIERS: PopoverProps["modifiers"] = {
// 20px padding - scrollbar width + a bit
flip: { enabled: true, options: { padding: 20, rootBoundary: "viewport" } },
// shift popover up 5px so MenuItems align
offset: { enabled: true, options: { offset: [-5, 0] } },
preventOverflow: { enabled: true, options: { padding: 20, rootBoundary: "viewport" } },
};

modifiers={{
arrow: {
enabled: !this.props.minimal,
},
offset: {
options: {
offset: [0, TOOLTIP_ARROW_SVG_SIZE / 2],
},
},
}}

Without exposing middleware, we'd either have to recreate all these specific APIs (which feels like more work for less flexibility) or continually add on to functionality during the migration.

I also think the middleware concept is actually pretty well-designed already. Floating UI's API is well-documented, and it's tough to anticipate all the positioning edge cases we might need to handle. A more semantic API would probably just end up being a thin wrapper around middleware anyway, and then we'd be maintaining our own abstraction layer.

On the future-proofing front, if we do need to switch libraries again someday, the middleware concept seems pretty universal. Popper had modifiers, Floating UI has middleware, so there's probably a pattern here. We could potentially create a compatibility layer that maps between different systems if needed.

What's your take? Are there specific middleware use cases that concern you, or do you have a suggestion on a clean wrapper we could use here?

@### Minimal style

You can create a minimal popover by setting `minimal={true}`.
This removes the arrow from the popover and makes the transitions more subtle.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we've talked about this one before, but do we want to split these two into separate props now that we're redoing the API anyway?

Copy link
Contributor Author

@ggdouglas ggdouglas Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, there are a few things to split out here:

  • Presence of the arrow
  • Offset of the popup element from the target
  • Animation style

I've split minimal into two separate props, arrow and animation. The arrow prop controls the display of the arrow and applies the correct amount of offset to accommodate the arrow. The animation prop solely controls the style of the animation ("scale" vs. "minimal"). 86cd4c0

}

function cssPropertyToString(value: string | number | undefined): string {
return value != null && !isNaN(Number(value)) ? `${value}px` : "";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if you set left: (the empty string case)? Should we set the property at all in that case?

@svc-palantir-github
Copy link

[CR] Edit comments/markdown

Build artifact links for this commit: documentation | landing | table | demo

This is an automated comment from the deploy-preview CircleCI job.

@svc-palantir-github
Copy link

[CR] Remove redundant `interactionKind` default in `PopoverTarget`

Build artifact links for this commit: documentation | landing | table | demo

This is an automated comment from the deploy-preview CircleCI job.

@svc-palantir-github
Copy link

[CR] Redeclare new Placement type within Blueprint

Build artifact links for this commit: documentation | landing | table | demo

This is an automated comment from the deploy-preview CircleCI job.

Copy link
Contributor

@mm-wang mm-wang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed for syntax and understanding, mostly tried to get context and an example of how we structure these components. I'd love to hear more about how I can use and test this.


export const PopoverNext = forwardRef<PopoverNextRef, PopoverNextProps>((props, ref) => {
const {
boundary = "clippingAncestors",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: Thought it was odd that Floating UI didn't have an enum for this, but it looks like it's literally the string: https://floating-ui.com/docs/detectoverflow#boundary.

popoverClassName,
);

const defaultAutoFocus = isHoverInteractionKind ? false : undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: What is the difference between false and undefined here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic was originally defined in the original Popover component here:

const defaultAutoFocus = this.isHoverInteractionKind() ? false : undefined;

My understanding of this is that for hover interactions, we want to explicitly disable auto-focus (false) because we don't want hover to steal focus from the user's current input. For click interactions, we want to use the default behavior which likely enables auto-focus when appropriate.

@svc-palantir-github
Copy link

[CR] Simplify middleware placement logic

Build artifact links for this commit: documentation | landing | table | demo

This is an automated comment from the deploy-preview CircleCI job.

Replace createRef with useRef for targetRef to maintain ref persistence
across re-renders
@svc-palantir-github
Copy link

[CR] Use `useRef` instead of `createRef` for targetRef in PopoverNext

Build artifact links for this commit: documentation | landing | table | demo

This is an automated comment from the deploy-preview CircleCI job.

@svc-palantir-github
Copy link

Split `minimal` prop into new `arrow` and `animation` props

Build artifact links for this commit: documentation | landing | table | demo

This is an automated comment from the deploy-preview CircleCI job.

@ggdouglas ggdouglas force-pushed the gdouglas/popover-next branch from 3ab8cbf to 86cd4c0 Compare October 10, 2025 20:41
@svc-palantir-github
Copy link

Split `minimal` prop into new `arrow` and `animation` props

Build artifact links for this commit: documentation | landing | table | demo

This is an automated comment from the deploy-preview CircleCI job.

@svc-palantir-github
Copy link

Set `enforceFocus` to false in PopoverNext example

Build artifact links for this commit: documentation | landing | table | demo

This is an automated comment from the deploy-preview CircleCI job.

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.

5 participants