-
Notifications
You must be signed in to change notification settings - Fork 2.2k
feat: Add PopoverNext component built on Floating UI #7573
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
Write docs for PopoverNext and add examplesBuild artifact links for this commit: documentation | landing | table | demoThis is an automated comment from the deploy-preview CircleCI job. |
5706459 to
7b8f84d
Compare
Write docs for PopoverNext and add examplesBuild artifact links for this commit: documentation | landing | table | demoThis is an automated comment from the deploy-preview CircleCI job. |
7b8f84d to
e7e0cfe
Compare
Write docs for PopoverNext and add examplesBuild artifact links for this commit: documentation | landing | table | demoThis is an automated comment from the deploy-preview CircleCI job. |
e7e0cfe to
8db1447
Compare
Write docs for PopoverNext and add examplesBuild artifact links for this commit: documentation | landing | table | demoThis is an automated comment from the deploy-preview CircleCI job. |
| @### 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). |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
blueprint/packages/core/src/components/menu/menuItem.tsx
Lines 308 to 314 in 031a998
| 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" } }, | |
| }; |
blueprint/packages/core/src/components/tooltip/tooltip.tsx
Lines 126 to 135 in 031a998
| 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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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` : ""; |
There was a problem hiding this comment.
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?
[CR] Edit comments/markdownBuild artifact links for this commit: documentation | landing | table | demoThis is an automated comment from the deploy-preview CircleCI job. |
[CR] Remove redundant `interactionKind` default in `PopoverTarget`Build artifact links for this commit: documentation | landing | table | demoThis is an automated comment from the deploy-preview CircleCI job. |
[CR] Redeclare new Placement type within BlueprintBuild artifact links for this commit: documentation | landing | table | demoThis is an automated comment from the deploy-preview CircleCI job. |
mm-wang
left a comment
There was a problem hiding this 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", |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
[CR] Simplify middleware placement logicBuild artifact links for this commit: documentation | landing | table | demoThis is an automated comment from the deploy-preview CircleCI job. |
Replace createRef with useRef for targetRef to maintain ref persistence across re-renders
[CR] Use `useRef` instead of `createRef` for targetRef in PopoverNextBuild artifact links for this commit: documentation | landing | table | demoThis is an automated comment from the deploy-preview CircleCI job. |
Split `minimal` prop into new `arrow` and `animation` propsBuild artifact links for this commit: documentation | landing | table | demoThis is an automated comment from the deploy-preview CircleCI job. |
3ab8cbf to
86cd4c0
Compare
Split `minimal` prop into new `arrow` and `animation` propsBuild artifact links for this commit: documentation | landing | table | demoThis is an automated comment from the deploy-preview CircleCI job. |
Set `enforceFocus` to false in PopoverNext exampleBuild artifact links for this commit: documentation | landing | table | demoThis is an automated comment from the deploy-preview CircleCI job. |
Summary
Introduce
PopoverNextas a modern, Floating UI–based replacement forPopover. 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.react-popperisn’t actively maintained, and the ecosystem has moved to Floating UI. React 19 support is blocked by the legacy stack.PopoverNextcomponent, docs, tests, and examples. ExistingPopovercomponent marked deprecated.middlewareas options instead ofmodifiersin newPopoverNextAPI.Migration
children,renderTarget,content,isOpen/defaultIsOpen,onInteraction,onClose,usePortal,hasBackdrop,interactionKind,minimal,matchTargetWidth, etc."auto"placement options are not supported inPopoverNext.placemententirely. Whenplacementis undefined,PopoverNextuses Floating UI's automatic placement by default (viaautoPlacement). See the docs: Floating UI autoPlacement.Popover:boundary="clippingParents"PopoverNext:boundary="clippingAncestors"boundary="clippingParents"onPopoverNext.Classes.POPOVER_DISMISS,Classes.POPOVER_DISMISS_OVERRIDE,captureDismissaria-haspopup/aria-expanded, backdrop + focus behavior viaOverlay2flip→flippreventOverflow→shift(useboundary/rootBoundary)offset→offset(mainAxis,crossAxis)arrow→arrowPopoverNext.placement,boundary, etc. where neededFollow-ups
Tooltip, menus, selects) toPopoverNext.Popoverin a future major versionReferences