Skip to content

feat(react-headless-components-preview): add Popover & positioning#36006

Draft
mainframev wants to merge 9 commits intomicrosoft:masterfrom
mainframev:feat/headless-popover-v2
Draft

feat(react-headless-components-preview): add Popover & positioning#36006
mainframev wants to merge 9 commits intomicrosoft:masterfrom
mainframev:feat/headless-popover-v2

Conversation

@mainframev
Copy link
Copy Markdown
Contributor

@mainframev mainframev commented Apr 20, 2026

Adds a headless Popover (Popover, PopoverTrigger, PopoverSurface) to the @fluentui/react-headless-components-preview package, positioned via the native CSS Anchor Positioning API instead of floating-ui

Feature with v9 @fluentui/react-popover

v9 feature Status
open / defaultOpen / onOpenChange
openOnHover + mouseLeaveDelay
openOnContext (right-click, cursor-anchored)
withArrow (+ consumer-owned arrow CSS via [data-placement]) ⚠️ User provides styles for arrow based on data-placement and data-arrow attributes
trapFocus + aria-modal / role="dialog" ✅ (via useFocusScope)
disableAutoFocus
closeOnScroll, closeOnIframeFocus
inline (skip top-layer) ⚠️ v9: skips the <Portal> wrapper - DOM placement only, positioning math unchanged. Headless: skips HTML Popover API top-layer promotion, also changes the overflow boundary CSS position-try-fallbacks flips against (viewport → nearest + scroll port / containing block)
mountNode (portal target)
Nested popovers (per-instance Escape / dismiss)
positioning.position + positioning.align
positioning.offset (number or { mainAxis, crossAxis })
positioning.coverTarget
positioning.fallbackPositions
positioning.autoSize (true / 'width' / 'height') ❌ Not implemented
positioning.overflowBoundary
positioning.overflowBoundaryPadding ❌ Not implemented
positioning.overflowBoundaryRect ❌ Not implemented
positioning.matchTargetSize: 'width'
positioning.strategy: 'absolute' | 'fixed'
positioning.pinned
positioning.target (custom anchor)
positioning.positioningRef (imperative setTarget)
flipBoundary ❌ Not implemented
arrowPadding ➖ Configurable on user side via styles
shiftToCoverTarget ❌ Not implemented
onPositioningEnd ➖ Native API has no "settle" event
disableUpdateOnResize ➖ Native anchor positioning + targeted usage is always-on
useTransform ➖ Native anchor positioning doesn't use transform, no conceptual equivalent

Related Issue(s)

@github-actions
Copy link
Copy Markdown

Pull request demo site: URL

@mainframev mainframev force-pushed the feat/headless-popover-v2 branch from 22f36ba to e9fef9d Compare April 20, 2026 01:46
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 20, 2026

📊 Bundle size report

Package & Exports Baseline (minified/GZIP) PR Change
react-headless-components-preview
react-headless-components-preview: entire library
70.65 kB
20.862 kB
88.57 kB
26.636 kB
17.92 kB
5.774 kB

🤖 This report was generated against 86cf4cbebfc9517e2373ce8077674bbcaf3ce7da

@mainframev mainframev force-pushed the feat/headless-popover-v2 branch 12 times, most recently from 58218ca to c32d139 Compare April 22, 2026 10:30
@mainframev mainframev changed the title WIP: feat(react-headless-components-preview): add Popover & positioning feat(react-headless-components-preview): add Popover & positioning Apr 22, 2026
@mainframev mainframev marked this pull request as ready for review April 22, 2026 10:42
@mainframev mainframev requested review from a team as code owners April 22, 2026 10:42
@mainframev mainframev requested a review from dmytrokirpa April 22, 2026 10:47
@mainframev mainframev force-pushed the feat/headless-popover-v2 branch from c32d139 to 4774cce Compare April 22, 2026 12:21
@mainframev mainframev requested a review from a team as a code owner April 22, 2026 12:21
@mainframev mainframev force-pushed the feat/headless-popover-v2 branch 2 times, most recently from 60d79ff to 30490b7 Compare April 22, 2026 13:18
`useDialogContextValues copy.ts` was an editor artifact that slipped into the
feature commit. Removing it leaves `useDialogContextValues.ts` untouched.
@mainframev mainframev force-pushed the feat/headless-popover-v2 branch from 30490b7 to f4a8375 Compare April 22, 2026 13:29
@mainframev mainframev marked this pull request as draft April 22, 2026 22:16
@mainframev mainframev force-pushed the feat/headless-popover-v2 branch 2 times, most recently from 0da03bc to c16a425 Compare April 23, 2026 02:33
@mainframev mainframev marked this pull request as ready for review April 23, 2026 09:35
@mainframev mainframev force-pushed the feat/headless-popover-v2 branch from c16a425 to e42cb0f Compare April 23, 2026 12:42
@mainframev mainframev marked this pull request as draft April 23, 2026 13:18

export type PositioningReturn = {
targetRef: React.RefCallback<HTMLElement>;
containerRef: React.RefCallback<HTMLElement>;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should this be surfaceRef or is it for consistency with v9?

import { computeAvailableHeight, computeAvailableWidth, resolveBoundaryPadding, resolveElementRef } from './utils';

export type UseBoundaryClampOptions = {
overflowBoundary: PositioningProps['overflowBoundary'];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

would be great to document the exported props and hooks

function mountHook(options: PositioningProps = {}) {
const resultRef = React.createRef<{ current: PositioningReturn }>();
const Capture = () => {
const result = usePositioning(options);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit, but we can probably type-cast once if it's really needed

Suggested change
const result = usePositioning(options);
const result = usePositioning(options) as unknown as { current: PositioningReturn };

result.current.containerRef(node);

expect(node.style.getPropertyValue('position-anchor')).toMatch(/^--popover-anchor-/);
expect(node.style.getPropertyValue('position-area')).toBe('block-end span-inline-end');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit, but we can use the https://github.com/testing-library/jest-dom#tohavestyle to make it a bit cleaner

Suggested change
expect(node.style.getPropertyValue('position-area')).toBe('block-end span-inline-end');
expect(node).toHaveStyle({ positionArea: 'block-end span-inline-end' });

const node = document.createElement('div');
result.current.containerRef(node);

expect(node.style.position).toBe('absolute');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit, but can be combined to one assertion for all styles

Copy link
Copy Markdown
Contributor

@dmytrokirpa dmytrokirpa Apr 23, 2026

Choose a reason for hiding this comment

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

as discussed offline, we'll need to remove this for now

const childProps = (child?.props ?? {}) as Record<string, unknown>;

const triggerChildProps = {
'aria-expanded': `${open}` as 'true' | 'false',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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


const positioning = usePositioning(resolvePositioningShorthand(props.positioning));

useOnClickOutside({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

just to confrim, did you check if Popover API has this functionality out-of-the-box?

disabledFocusOnIframe: !closeOnIframeFocus,
});

useOnScrollOutside({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the same question as above, pls double check if we can drop this in favor of Popover API

* for the breathing-room `GAP`. Used by `useAutoSizeBoundary` to derive a
* numeric `max-height` when `overflowBoundary` is supplied.
*/
export function computeAvailableHeight(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

lets cover these utils with unit tests, should be straightforward as they are stateless

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.

2 participants