feat(headless & react-positioning): add engine option to allow floating-ui based positioning in headless - #36800
Conversation
📊 Bundle size report
|
|
Pull request demo site: URL |
| @@ -19,9 +19,11 @@ experience to provide. Fluent UI Headless leaves that tradeoff to the consuming | |||
| anchor-positioning polyfill. This keeps the common path lean and requires little application code, | |||
There was a problem hiding this comment.
🕵🏾♀️ visual changes to review in the Visual Change Report
vr-tests-react-components/Avatar Converged 2 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests-react-components/Avatar Converged.size+icon+badge+square.normal.chromium.png | 1 | Changed |
| vr-tests-react-components/Avatar Converged.badgeMask.normal.chromium.png | 3 | Changed |
vr-tests-react-components/Drawer 6 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests-react-components/Drawer.inline drawer separator position start - Dark Mode.chromium.png | 2 | Changed |
| vr-tests-react-components/Drawer.inline drawer separator position start - High Contrast.chromium.png | 3 | Changed |
| vr-tests-react-components/Drawer.inline drawer.chromium.png | 2 | Changed |
| vr-tests-react-components/Drawer.overlay drawer position start - Dark Mode.chromium.png | 2 | Changed |
| vr-tests-react-components/Drawer.overlay drawer position start - High Contrast.chromium.png | 3 | Changed |
| vr-tests-react-components/Drawer.overlay drawer.chromium.png | 2 | Changed |
vr-tests-react-components/Field 10 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests-react-components/Field.Checkbox.default.chromium.png | 8 | Changed |
| vr-tests-react-components/Field.ProgressBar.default.chromium.png | 16 | Changed |
| vr-tests-react-components/Field.Slider.default.chromium.png | 8 | Changed |
| vr-tests-react-components/Field.RadioGroup.default.chromium.png | 8 | Changed |
| vr-tests-react-components/Field.SpinButton.default.chromium.png | 8 | Changed |
| vr-tests-react-components/Field.Switch.default.chromium.png | 8 | Changed |
| vr-tests-react-components/Field.horizontal+longLabel.default.chromium.png | 8 | Changed |
| vr-tests-react-components/Field.validation-error.default.chromium.png | 8 | Changed |
| vr-tests-react-components/Field.Textarea.default.chromium.png | 8 | Changed |
| vr-tests-react-components/Field.validation-warning.default.chromium.png | 8 | Changed |
vr-tests-react-components/Menu Converged - submenuIndicator slotted content 1 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests-react-components/Menu Converged - submenuIndicator slotted content.default.submenus open.chromium.png | 413 | Changed |
vr-tests-react-components/MessageBar 6 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests-react-components/MessageBar.Intents - Dark Mode.default.chromium.png | 36 | Changed |
| vr-tests-react-components/MessageBar.Intents - High Contrast.default.chromium.png | 79 | Changed |
| vr-tests-react-components/MessageBar.Intents.default.chromium.png | 51 | Changed |
| vr-tests-react-components/MessageBar.Multiline Without Actions.default.chromium.png | 51 | Changed |
| vr-tests-react-components/MessageBar.Multiline.default.chromium.png | 51 | Changed |
| vr-tests-react-components/MessageBar.Multiline No Actions.default.chromium.png | 51 | Changed |
vr-tests-react-components/Positioning 2 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests-react-components/Positioning.Positioning end.chromium.png | 609 | Changed |
| vr-tests-react-components/Positioning.Positioning end.updated 2 times.chromium.png | 138 | Changed |
vr-tests-react-components/ProgressBar converged 2 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests-react-components/ProgressBar converged.Indeterminate + thickness - High Contrast.default.chromium.png | 84 | Changed |
| vr-tests-react-components/ProgressBar converged.Indeterminate + thickness.default.chromium.png | 41 | Changed |
vr-tests-react-components/SearchBox Converged 6 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests-react-components/SearchBox Converged.Appearance- filled-lighter.hover.chromium.png | 33 | Changed |
| vr-tests-react-components/SearchBox Converged.Disabled, appearance- filled-lighter.hover.chromium.png | 11 | Changed |
| vr-tests-react-components/SearchBox Converged.Disabled, appearance- underline.focused.chromium.png | 13 | Changed |
| vr-tests-react-components/SearchBox Converged.Size- large.hover.chromium.png | 45 | Changed |
| vr-tests-react-components/SearchBox Converged.Size- small.hover.chromium.png | 18 | Changed |
| vr-tests-react-components/SearchBox Converged.With value.hover.chromium.png | 33 | Changed |
vr-tests-react-components/SwatchPicker Converged 3 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests-react-components/SwatchPicker Converged.size.default.chromium.png | 2 | Changed |
| vr-tests-react-components/SwatchPicker Converged.spacing.default.chromium.png | 2 | Changed |
| vr-tests-react-components/SwatchPicker Converged.shape.default.chromium.png | 2 | Changed |
vr-tests-react-components/TagPicker 1 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests-react-components/TagPicker.disabled - Dark Mode.chromium.png | 658 | Changed |
vr-tests-react-components/Toast 6 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests-react-components/Toast.Full Toast Inverted - High Contrast.Toast visible.chromium.png | 54 | Changed |
| vr-tests-react-components/Toast.Full Toast.Toast visible.chromium.png | 36 | Changed |
| vr-tests-react-components/Toast.Title Only.Toast visible.chromium.png | 36 | Changed |
| vr-tests-react-components/Toast.Title Only Inverted - High Contrast.Toast visible.chromium.png | 54 | Changed |
| vr-tests-react-components/Toast.Without Subtitle Inverted.Toast visible.chromium.png | 25 | Changed |
| vr-tests-react-components/Toast.Without Subtitle Inverted - High Contrast.Toast visible.chromium.png | 54 | Changed |
vr-tests-react-components/Toolbar Converged 4 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests-react-components/Toolbar Converged.Large.default.chromium.png | 5 | Changed |
| vr-tests-react-components/Toolbar Converged.Small.default.chromium.png | 5 | Changed |
| vr-tests-react-components/Toolbar Converged.Transparent.default.chromium.png | 5 | Changed |
| vr-tests-react-components/Toolbar Converged.Vertical.Button Pressed.chromium.png | 5 | Changed |
vr-tests-react-components/Tree 1 screenshots
| Image Name | Diff(in Pixels) | Image Type |
|---|---|---|
| vr-tests-react-components/Tree.layout.default.chromium.png | 115 | Changed |
There were 149 duplicate changes discarded. Check the build logs for more information.
…SS.supports The React 17 integration run uses Cypress 13 / Electron 118, which predates CSS anchor positioning, so position-anchor is dropped and the observer reports the centred placement.
|
I’d suggest documenting the positioning strategy in an RFC before we settle on the API. There are several reasonable approaches with different trade-offs:
The RFC would benefit from comparing these options based on bundle impact, browser behavior, API complexity, parity with |
Agree an RFC is the right place to settle this, happy to write it up with those four options 👍🏻 I'm a bit sceptical about the automatic approach: it moves detection and floating-ui loading into the library and has a first-paint cost (unpositioned surface or a jump until the fallback runs). But we clearly have different favourites here, so let's compare them properly in the RFC and decide there |
… explicit strategy
Previous Behavior
Headless surfaces are positioned with CSS anchor positioning only. Options that have no CSS equivalent (
autoSize,flipBoundary,overflowBoundary,overflowBoundaryPadding,shiftToCoverTarget,arrowPadding,useTransform,onPositioningEnd) are not available.New Behavior
Usage
Per surface, alongside any other positioning option:
App-wide via context; every headless surface below it uses the engine,
withArrowis positioned by it as well:Imperative updates and end-of-positioning events work the same way as in v9:
Without an engine the same props still position via CSS anchors; engine-only options are ignored with a dev warning.
enginewins over the provider.PositioningPropsin headless is widened to the full canonical contract. Engine-only options without an engine log a dev warning and are ignored.strategytofixed(surfaces live in the top layer).usePositioningreturnsarrowRef, wired into Popover/Tooltip.inset: auto) and keepsdata-placementcurrent with the logical placement, so placement-keyed CSS keeps working. This is documented as thePositioningEnginecontract.@floating-ui/*stays out of the headless bundle;verify-bundle-isolationforbids it.Stories:
Concepts/Positioninggets anEngineoverview plus one story per engine-only option, mirroring the v9 positioning docs.Impact on v9
None, the change is additive:
react-positioningpublic API only gains exports (createFloatingUIPositioningEngine,floatingUIPositioningEngine,PositioningEngine,PositioningEngineCreateParams, plus types v9 already used internally:PositioningOptions,PositionManager,OnPositioningEndEvent,OnPositioningEndEventDetail,PositioningPlacement). Nothing removed, renamed or retyped, hence aminorchange file.usePositioning/usePositioningOptionsreturn the same shape. The middleware assembly was moved as-is intoresolvePositioningOptions(same middleware, same order, same subset forwarded toPositioningConfigurationFn, samestrategyfallback expression) and the per-field dependency list is unchanged.createPositionManager, all middleware and the v9 component packages are untouched. The top-layerinsetreset anddata-placementwriting only happen increateFloatingUIPositioningEngine.create(), which v9 never calls.