Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "patch",
Comment thread
mainframev marked this conversation as resolved.
"comment": "feat: add autoSize to headless usePositioning",
"packageName": "@fluentui/react-headless-components-preview",
"email": "vgenaev@gmail.com"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "minor",
"comment": "feat: expose normalizeAutosize",
"packageName": "@fluentui/react-positioning",
"email": "vgenaev@gmail.com"
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ global.ResizeObserver = class ResizeObserver {
// no-op for jsdom
}
};

// JSDOM provides `CSS.escape` but not `CSS.supports`, so feature detection has nothing to call.
// Provide it, defaulting to "unsupported" — which is what this environment genuinely is, and what
// the missing method already resolved to. Tests exercising a feature-gated path opt in with
// `jest.spyOn(CSS, 'supports')`.
if (typeof CSS === 'undefined') {
global.CSS = { supports: () => false };
} else if (typeof CSS.supports !== 'function') {
CSS.supports = () => false;
}

// JSDOM does not implement native <dialog> APIs yet.
// Provide a minimal test shim so components using showModal/show/close can run in Jest.
if (typeof HTMLDialogElement !== 'undefined') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,30 +174,43 @@ The headless Popover does **not** add `aria-live` to the surface. Consumers rend

## Positioning

Placement is handled entirely by the `usePositioning` hook, which writes native CSS anchor-positioning properties onto the surface element. No JS layout loop.
Placement is handled entirely by the `usePositioning` hook, which writes native CSS anchor-positioning properties onto the surface element. _Placement_ involves no JS layout loop — the browser resolves it, including flipping. _Sizing_ is the one exception: `autoSize` reads the anchor's rect to derive a maximum size. It never reads the surface's own rect, so there is still no measure/apply feedback loop.

### Options (all optional)

| Option | Type | Default | Effect |
| ------------------- | ----------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `position` | `'above' \| 'below' \| 'before' \| 'after'` | `'above'` | Which side of the anchor the surface sits on. Physical `top` / `bottom` / `left` / `right` are normalized. |
| `align` | `'start' \| 'center' \| 'end' \| 'top' \| 'bottom'` | `'center'` | Cross-axis alignment. `top` → `start`, `bottom` → `end` (v9 aliases). |
| `offset` | `number \| { mainAxis?: number; crossAxis?: number }` | `0` | Logical-margin offset from the anchor. |
| `fallbackPositions` | `PositioningShorthandValue[]` | `[]` | Custom fallback chain. Each entry is converted to a `<position-area>` value inline in `position-try-fallbacks`. |
| `coverTarget` | `boolean` | `false` | Overlap the anchor instead of sitting beside it. |
| `pinned` | `boolean` | `false` | Disable fallback flipping; surface stays at the requested placement even if it overflows. |
| `matchTargetSize` | `'width'` | — | Sets the surface's `width` to `anchor-size(width)`. |
| `strategy` | `'fixed' \| 'absolute'` | `'absolute'` | CSS `position` property value on the surface. Matches v9's default. Use `'fixed'` when the surface needs to escape transformed / `contain: layout` ancestors for anchoring purposes. |
| `target` | `HTMLElement \| RefObject` | — | Custom anchor element. When set, `anchor-name` is written on this element instead of the trigger. |
| `positioningRef` | `Ref<PositioningImperativeRef>` | — | `{ setTarget(el): void; updatePosition(): void }`. `updatePosition` is a no-op — native positioning self-updates. |
| Option | Type | Default | Effect |
| ------------------- | --------------------------------------------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `position` | `'above' \| 'below' \| 'before' \| 'after'` | `'above'` | Which side of the anchor the surface sits on. Physical `top` / `bottom` / `left` / `right` are normalized. |
| `align` | `'start' \| 'center' \| 'end' \| 'top' \| 'bottom'` | `'center'` | Cross-axis alignment. `top` → `start`, `bottom` → `end` (v9 aliases). |
| `offset` | `number \| { mainAxis?: number; crossAxis?: number }` | `0` | Logical-margin offset from the anchor. |
| `fallbackPositions` | `PositioningShorthandValue[]` | `[]` | Custom fallback chain. Each entry is converted to a `<position-area>` value inline in `position-try-fallbacks`. |
| `coverTarget` | `boolean` | `false` | Overlap the anchor instead of sitting beside it. |
| `pinned` | `boolean` | `false` | Disable fallback flipping; surface stays at the requested placement even if it overflows. |
| `matchTargetSize` | `'width'` | — | Sets the surface's `width` to `anchor-size(width)`. |
| `autoSize` | `boolean \| 'height' \| 'width' \| 'always' \| 'height-always' \| 'width-always'` | — | Constrains the surface to the space available around the anchor. `true`/`'always'` constrain both axes, `'height'` the block axis, `'width'` the inline axis; the `-always` values are obsolete aliases kept for parity with `@fluentui/react-positioning`. Writes `max-block-size`/`max-inline-size` plus `overflow-block`/`overflow-inline: auto`. |
| `strategy` | `'fixed' \| 'absolute'` | `'absolute'` | CSS `position` property value on the surface. Matches v9's default. Use `'fixed'` when the surface needs to escape transformed / `contain: layout` ancestors for anchoring purposes. |
| `target` | `HTMLElement \| RefObject` | — | Custom anchor element. When set, `anchor-name` is written on this element instead of the trigger. |
| `positioningRef` | `Ref<PositioningImperativeRef>` | — | `{ setTarget(el): void; updatePosition(): void }`. `updatePosition` is a no-op — native positioning self-updates. |

### Rendering

- The hook writes `anchor-name: --popover-anchor-<id>` on the anchor (trigger or custom target) via `useIsomorphicLayoutEffect`.
- On the surface it writes `position: absolute` (or `fixed` if `strategy: 'fixed'`); `inset: auto; margin: 0; position-anchor: --popover-anchor-<id>; position-area: <value>; position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline`. The `inset: auto; margin: 0` reset is required because the UA popover stylesheet sets `inset: 0; margin: auto`, which fights `position-area`.
- For center alignment, the hook also writes `place-self: anchor-center` as a workaround for https://crbug.com/438334710 (Chromium <=130 doesn't reliably apply the implicit anchor-center self-alignment to single-keyword `position-area` values).
- For center alignment, the hook writes `anchor-center` as a workaround for https://crbug.com/438334710 (Chromium <=130 doesn't reliably apply the implicit anchor-center self-alignment to single-keyword `position-area` values). It applies it to the **cross axis only** — `justify-self` for `above`/`below`, `align-self` for `before`/`after`. The `place-self` shorthand would set both axes, which centres the surface on its anchor along the main axis too, overriding `position-area` and dropping the surface on top of the anchor.
- With `autoSize`, the hook additionally writes `max-block-size`/`max-inline-size` and `overflow-block`/`overflow-inline: auto` on the constrained axes. An overflow value the consumer already set inline is never overwritten, and never removed on teardown.
- `data-placement` is set to the requested placement and then live-updated by `usePlacementObserver` (ResizeObserver + scroll listener) to reflect the browser's post-flip decision.

### autoSize limitations

- **Set `box-sizing: border-box` on the surface.** `max-block-size`/`max-inline-size` constrain the content box, and the UA stylesheet for `[popover]` adds a border and padding — so a `content-box` surface overflows the constraint by roughly 10px. Same requirement as `matchTargetSize`.
- **The boundary is the viewport.** There is no `overflowBoundary` equivalent, so a surface inside a scrollable or clipping ancestor may be given more room than it can actually use.
- **The constraint is the roomier of the requested side and its opposite, not the resolved one.** It is derived from the anchor rather than the browser's flip decision, which is what lets the browser still choose the roomier side — the surface is never pre-shrunk onto the side it was asked for. A CSS-only constraint (`max-block-size: 100%`) resolves per placement and so defeats flipping entirely.
- **Custom `fallbackPositions` do not widen the measurement.** Only the requested side and its opposite are measured, so a fallback chain that crosses axes (for example `below` falling back to `after`) is constrained more tightly than it strictly needs to be.
- **Only the main axis is measured.** The cross axis is bounded by the viewport: under `position-area` an auto-sized surface already shrink-to-fits within its area, so a tighter cross-axis value would only affect a surface with an explicit size.
- **No `shift` equivalent.** For `before`/`after` with `align: 'center'` the surface stays centred on its anchor, so near a boundary edge it can still spill symmetrically even when constrained. CSS anchor positioning has no counterpart to Floating UI's `shift`.
- **Horizontal writing modes only.** The properties written are logical, and measuring the roomier of two opposite sides is direction-invariant, so RTL is unaffected. The computation does assume `horizontal-tb` when mapping physical edges onto logical axes, matching the assumption already made by the `data-placement` read-back. In a vertical writing mode the block and inline measurements are transposed.
- **An anchor scrolled out of view takes the surface with it.** The surface stays anchored, so it leaves the viewport alongside its anchor; the constraint is capped at the boundary size but does not pull the surface back into view.

### Arrow

Arrow positioning is **consumer-owned CSS** keyed off `[data-placement]`. The hook doesn't manipulate the arrow element. Consumers writing arrow styles typically target `[data-placement^='above']`, `[data-placement^='below']`, etc., and use anchor queries (`@container anchored()`) for flip-aware styling when supported.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export { Position }
export { PositioningImperativeRef }

// @public (undocumented)
export type PositioningProps = Pick<PositioningProps_2, 'align' | 'coverTarget' | 'fallbackPositions' | 'matchTargetSize' | 'offset' | 'pinned' | 'position' | 'positioningRef' | 'strategy' | 'target'>;
export type PositioningProps = Pick<PositioningProps_2, 'align' | 'autoSize' | 'coverTarget' | 'fallbackPositions' | 'matchTargetSize' | 'offset' | 'pinned' | 'position' | 'positioningRef' | 'strategy' | 'target'>;

// @public (undocumented)
export type PositioningReturn = {
Expand Down
Loading
Loading