Skip to content

Commit c16a425

Browse files
committed
fixup! refactor(react-headless-components-preview): rebuild usePositioning on spec-pure CSS anchor positioning
1 parent 8db0ab2 commit c16a425

22 files changed

Lines changed: 256 additions & 492 deletions

packages/react-components/react-headless-components-preview/library/docs/popover-spec.md

Lines changed: 16 additions & 16 deletions
Large diffs are not rendered by default.

packages/react-components/react-headless-components-preview/library/etc/react-headless-components-preview.api.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,6 @@ export type PositioningProps = {
586586
};
587587
fallbackPositions?: PositioningShorthandValue[];
588588
coverTarget?: boolean;
589-
autoSize?: boolean | 'width' | 'height';
590589
target?: HTMLElement | React_2.RefObject<HTMLElement | null> | null;
591590
strategy?: 'absolute' | 'fixed';
592591
matchTargetSize?: 'width';

packages/react-components/react-headless-components-preview/library/src/hooks/usePositioning/types.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -66,17 +66,15 @@ export type PositioningProps = {
6666
fallbackPositions?: PositioningShorthandValue[];
6767
/** Position on top of the target */
6868
coverTarget?: boolean;
69-
/** Auto-size to available space */
70-
autoSize?: boolean | 'width' | 'height';
7169
/**
7270
* Custom anchor element for the positioned surface. When provided, the
7371
* anchor-name is written to this element instead of the target assigned via
7472
* the returned `targetRef`. Accepts a DOM element or a RefObject.
7573
*/
7674
target?: HTMLElement | React.RefObject<HTMLElement | null> | null;
7775
/**
78-
* CSS `position` value used on the positioned surface.
79-
* @default 'fixed'
76+
* CSS `position` value used on the positioned surface. Matches v9's default.
77+
* @default 'absolute'
8078
*/
8179
strategy?: 'absolute' | 'fixed';
8280
/**
@@ -98,18 +96,32 @@ export type PositioningProps = {
9896
*/
9997
positioningRef?: React.Ref<PositioningImperativeRef>;
10098
/**
101-
* Element that clips the surface when `autoSize` is set. Instead of
102-
* constraining the surface to the viewport, `max-width` / `max-height`
103-
* are computed from this element's rect. Accepts a DOM element or ref.
99+
* Element the surface must stay within. When set, the hook writes logical
100+
* `max-inline-size` and `max-block-size` on the surface so it can never
101+
* extend past the boundary's opposite edge on either axis. The surface's
102+
* start edge stays anchored where CSS anchor positioning placed it; only
103+
* the far edge is clamped.
104+
*
105+
* Known limitations:
106+
* - **No boundary-driven flip.** Native CSS `position-try-fallbacks`
107+
* evaluates overflow against the viewport only (the surface lives in
108+
* the top layer via the HTML Popover API). A primary placement that
109+
* wouldn't fit the custom boundary but does fit the viewport won't
110+
* flip — surface content wraps due to the clamp instead.
111+
* - **Clamp, not shift.** Differs from v9's `shift({ altBoundary: true })`
112+
* which translates the surface whole. Here the surface is reshaped.
113+
* - **Ref updates.** A `useRef` whose `.current` mutates without a render
114+
* won't trigger re-subscription. Prefer a `useState<HTMLElement | null>`
115+
* setter-ref pattern.
104116
*/
105117
overflowBoundary?: HTMLElement | React.RefObject<HTMLElement | null> | null;
106118
/**
107-
* Padding (in pixels) applied inward from the `overflowBoundary` rect
108-
* before `autoSize`'s `max-width` / `max-height` math runs. Accepts a
109-
* single number for uniform padding, or a logical-side object
110-
* (`{ top, end, bottom, start }`) for per-side control.
119+
* Padding (in pixels) subtracted from each side of the `overflowBoundary`
120+
* rect before the clamp is computed — breathing room between the surface
121+
* and the boundary edges. Accepts a single number for uniform padding or
122+
* a logical-side object (`{ top, end, bottom, start }`, RTL-aware).
111123
*
112-
* Has no effect when `overflowBoundary` or `autoSize` is unset.
124+
* Has no effect when `overflowBoundary` is unset.
113125
*/
114126
overflowBoundaryPadding?: PositioningBoundaryPadding;
115127
};

packages/react-components/react-headless-components-preview/library/src/hooks/usePositioning/useAutoSizeBoundary.ts

Lines changed: 0 additions & 71 deletions
This file was deleted.
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
'use client';
2+
3+
import { useIsomorphicLayoutEffect } from '@fluentui/react-utilities';
4+
import type { LogicalAlignment, Position, PositioningProps, ResolvedBoundaryPadding } from './types';
5+
import { computeAvailableHeight, computeAvailableWidth, resolveBoundaryPadding, resolveElementRef } from './utils';
6+
7+
export type UseBoundaryClampOptions = {
8+
overflowBoundary: PositioningProps['overflowBoundary'];
9+
overflowBoundaryPadding: PositioningProps['overflowBoundaryPadding'];
10+
position: Position;
11+
align: LogicalAlignment;
12+
coverTarget: boolean;
13+
containerEl: HTMLElement | null;
14+
targetEl: HTMLElement | null;
15+
targetDocument: Document | undefined;
16+
};
17+
18+
function shrinkRect(rect: DOMRect, padding: ResolvedBoundaryPadding): DOMRect {
19+
return {
20+
top: rect.top + padding.top,
21+
right: rect.right - padding.right,
22+
bottom: rect.bottom - padding.bottom,
23+
left: rect.left + padding.left,
24+
width: rect.width - padding.left - padding.right,
25+
height: rect.height - padding.top - padding.bottom,
26+
x: rect.left + padding.left,
27+
y: rect.top + padding.top,
28+
toJSON: rect.toJSON,
29+
};
30+
}
31+
32+
export function useBoundaryClamp({
33+
overflowBoundary,
34+
overflowBoundaryPadding,
35+
position,
36+
align,
37+
coverTarget,
38+
containerEl,
39+
targetEl,
40+
targetDocument,
41+
}: UseBoundaryClampOptions): void {
42+
useIsomorphicLayoutEffect(() => {
43+
if (coverTarget || !containerEl || !targetEl) {
44+
return;
45+
}
46+
47+
const boundaryEl = resolveElementRef(overflowBoundary);
48+
const view = targetDocument?.defaultView;
49+
const ResizeObserverCtor = view?.ResizeObserver;
50+
51+
if (!boundaryEl || !view || !ResizeObserverCtor) {
52+
return;
53+
}
54+
55+
const surface = containerEl;
56+
const trigger = targetEl;
57+
58+
const apply = () => {
59+
const direction = view.getComputedStyle(surface).direction === 'rtl' ? 'rtl' : 'ltr';
60+
const padding = resolveBoundaryPadding(overflowBoundaryPadding, direction);
61+
const boundaryRect = shrinkRect(boundaryEl.getBoundingClientRect(), padding);
62+
const triggerRect = trigger.getBoundingClientRect();
63+
64+
const maxBlock = Math.max(0, computeAvailableHeight(position, align, boundaryRect, triggerRect));
65+
const maxInline = Math.max(0, computeAvailableWidth(position, align, boundaryRect, triggerRect));
66+
67+
surface.style.setProperty('box-sizing', 'border-box');
68+
surface.style.setProperty('max-inline-size', `${maxInline}px`);
69+
surface.style.setProperty('max-block-size', `${maxBlock}px`);
70+
};
71+
72+
apply();
73+
74+
const observer = new ResizeObserverCtor(apply);
75+
76+
observer.observe(surface);
77+
observer.observe(trigger);
78+
observer.observe(boundaryEl);
79+
80+
view.addEventListener('scroll', apply, true);
81+
view.addEventListener('resize', apply);
82+
83+
return () => {
84+
observer.disconnect();
85+
view.removeEventListener('scroll', apply, true);
86+
view.removeEventListener('resize', apply);
87+
surface.style.removeProperty('box-sizing');
88+
surface.style.removeProperty('max-inline-size');
89+
surface.style.removeProperty('max-block-size');
90+
};
91+
}, [overflowBoundary, overflowBoundaryPadding, position, align, coverTarget, containerEl, targetEl, targetDocument]);
92+
}

packages/react-components/react-headless-components-preview/library/src/hooks/usePositioning/useOverflowShift.ts

Lines changed: 0 additions & 114 deletions
This file was deleted.

0 commit comments

Comments
 (0)