WIP: feat(react-headless-components-preview): add Popover & positioning#36006
WIP: feat(react-headless-components-preview): add Popover & positioning#36006mainframev wants to merge 9 commits intomicrosoft:masterfrom
Conversation
f26ae3e to
22f36ba
Compare
|
Pull request demo site: URL |
22f36ba to
e9fef9d
Compare
📊 Bundle size report
🤖 This report was generated against 86cf4cbebfc9517e2373ce8077674bbcaf3ce7da |
58218ca to
c32d139
Compare
…n spec-pure CSS anchor positioning
c32d139 to
4774cce
Compare
60d79ff to
30490b7
Compare
`useDialogContextValues copy.ts` was an editor artifact that slipped into the feature commit. Removing it leaves `useDialogContextValues.ts` untouched.
30490b7 to
f4a8375
Compare
…oning on spec-pure CSS anchor positioning
…oning on spec-pure CSS anchor positioning
0da03bc to
c16a425
Compare
…oning on spec-pure CSS anchor positioning
c16a425 to
e42cb0f
Compare
|
|
||
| export type PositioningReturn = { | ||
| targetRef: React.RefCallback<HTMLElement>; | ||
| containerRef: React.RefCallback<HTMLElement>; |
There was a problem hiding this comment.
should this be surfaceRef or is it for consistency with v9?
| import { computeAvailableHeight, computeAvailableWidth, resolveBoundaryPadding, resolveElementRef } from './utils'; | ||
|
|
||
| export type UseBoundaryClampOptions = { | ||
| overflowBoundary: PositioningProps['overflowBoundary']; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
nit, but we can probably type-cast once if it's really needed
| 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'); |
There was a problem hiding this comment.
nit, but we can use the https://github.com/testing-library/jest-dom#tohavestyle to make it a bit cleaner
| 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'); |
There was a problem hiding this comment.
nit, but can be combined to one assertion for all styles
There was a problem hiding this comment.
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', |
There was a problem hiding this comment.
Why don't we use the popovertarget attribute https://developer.mozilla.org/en-US/docs/Web/API/Popover_API#html_attributes?
There was a problem hiding this comment.
current implementation uses showPopover() and I found it more suitable as we need to also show Popover on hover (v9 canonical Popover has that feature), have it controlled and uncontrolled, mouseLeaveDelay for openOnHover etc
|
|
||
| const positioning = usePositioning(resolvePositioningShorthand(props.positioning)); | ||
|
|
||
| useOnClickOutside({ |
There was a problem hiding this comment.
just to confrim, did you check if Popover API has this functionality out-of-the-box?
There was a problem hiding this comment.
it does, but only when popover has popover='auto' / popover attribute, which has native dismiss on Esc and overlay click, great that it works also with nested popovers, but in current implementation it uses manual, which does not have such functionality out of the box, because we need multiple non-nested Popovers to be opened. I think auto will be a great fit to Tooltip
| disabledFocusOnIframe: !closeOnIframeFocus, | ||
| }); | ||
|
|
||
| useOnScrollOutside({ |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
lets cover these utils with unit tests, should be straightforward as they are stateless

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-popoveropen/defaultOpen/onOpenChangeopenOnHover+mouseLeaveDelayopenOnContext(right-click, cursor-anchored)withArrow(+ consumer-owned arrow CSS via[data-placement])trapFocus+aria-modal/role="dialog"useFocusScope)disableAutoFocuscloseOnScroll,closeOnIframeFocusinline(skip top-layer)<Portal>wrapper - DOM placement only, positioning math unchanged. Headless: skips HTML Popover API top-layer promotion, also changes the overflow boundary CSSposition-try-fallbacksflips against (viewport → nearest + scroll port / containing block)mountNode(portal target)positioning.position+positioning.alignpositioning.offset(number or{ mainAxis, crossAxis })positioning.coverTargetpositioning.fallbackPositionspositioning.autoSize(true/'width'/'height')positioning.overflowBoundarypositioning.overflowBoundaryPaddingpositioning.overflowBoundaryRectpositioning.matchTargetSize: 'width'positioning.strategy: 'absolute' | 'fixed'positioning.pinnedpositioning.target(custom anchor)positioning.positioningRef(imperativesetTarget)flipBoundaryarrowPaddingshiftToCoverTargetonPositioningEnddisableUpdateOnResizeuseTransformRelated Issue(s)