explore: engine option to support floating-ui based positioning in headless - #36675
Draft
Victor Genaev (mainframev) wants to merge 1 commit into
Draft
explore: engine option to support floating-ui based positioning in headless#36675Victor Genaev (mainframev) wants to merge 1 commit into
Victor Genaev (mainframev) wants to merge 1 commit into
Conversation
Victor Genaev (mainframev)
requested a review
from Dmytro Kirpa (dmytrokirpa)
September 1, 2026 13:47
📊 Bundle size report🤖 This report was generated against 78ea7c4de683945b759a5e3431fb4e7d9ff451c9 |
|
Pull request demo site: URL |
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.

Previous Behavior
Headless components position surfaces exclusively with native CSS anchor positioning. That covers
position,align,offset,coverTarget,matchTargetSize,pinnedandfallbackPositions, but it structurally cannot express several behaviours the canonical positioning contract offers — it flips between discrete fallbacks rather than sliding, it resolves collisions against the viewport or containing block rather than an element you choose, and it needs a real DOM node to anchor to.Options such as
autoSize,flipBoundaryandshiftare correctly rejected by the type after #36623, but there was no supported path to obtain them.PolyfillsAndFallbacks.mdxtold consumers to write their own measurement-based positioner.New Behavior
positioningaccepts anengine:No adapter.
usePositioningfrom@fluentui/react-positioningsatisfies thePositioningEnginecontract as-is — asserted by a compiling type binding inpositioningEngine.test.tsx. This PR adds no wrapper, no subpath and no invented contract.Nothing is bundled unless you supply it. No default entry point references a JavaScript positioner.
@floating-ui/*is now in the package'sverify-bundle-isolationforbidden list, so the guarantee is CI-enforced rather than documented. I confirmed the check actually catches a leak before relying on it.The engine is passed uncalled, and the component invokes it with options it has already merged. This is the load-bearing decision:
useMenuderivesposition: 'after',align: 'top'and a six-entry fallback chain fromisSubmenu, which lives in menu context and is not observable at the call site. Any shape where the consumer calls the positioner inverts that ownership and needs a back-channel to undo it —@fluentui/react-positioningexposes no channel to push options into an already-invoked positioner (checked againstPositioningImperativeRef,PositioningConfigurationProvider, and the options argument). Invoking it from the component means submenus and context menus keep their derived placement with zero consumer configuration.Two concerns stay with headless regardless of engine, because they belong to the surface rather than the positioner:
[popover]:popover-openappliesinset: 0; margin: auto, and a JS positioner writes onlyleft/top, leavingright/bottomat0and stretching the surface across the viewport. The reset is deliberately partial:insetends up0px auto auto 0px, sincetop/leftare the positioner's translate origin.data-placement, mapped from physical to logical vocabulary via the publiconPositioningEndoption. Consumer styling keyed on placement — arrows especially — keeps working across engines.Typing.
positioning={{ flipBoundary: el }}is still a compile error without an engine, and legal with one. Nonevermapping is needed: the engine branch requiresengine, andPositioningPropsis a weak type, so both the fresh-literal and through-a-variable cases are already rejected. Four@ts-expect-errorassertions cover this and fail in both directions.Notes for reviewers
engine={o => usePositioning(o)}is not and fails as a hook-order error. I built a dev-time guard naming the cause, then removed it — the failure is already loud and immediate, and it cost a conditional hook plus a lint suppression to improve a message. The constraint is documented onPositioningEngineand in the positioning docs.preserve-manual-memoizationis disabled at file scope inusePositioning.ts. Dynamic hook dispatch can't be traced by the compiler, verified by substituting a static call. Memoization is explicit and correct; only the automatic optimisation is lost.PositioningEngine.stories.tsxopts out of the barrel-import rule. That rule steers v9 stories to@fluentui/react-components, which would tell headless consumers to pull the entire suite for one hook.docs/architecture/layers.mdrule 3 also states stories may depend on anything.FlipBoundary.cy.tsxcovers the simpler claim that an engine-only option takes effect.Verification
type-check · lint · 1043 unit tests · 219 Cypress tests · bundle isolation · React 17/18/19 integration targets · API reports regenerated — all passing.
Related Issue(s)
Follows #36623 (narrowed the positioning prop, introduced the headless-local resolver this extends).
Related: #36662.