@octanejs/base-ui 0.1.14 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/alert-dialog.ts +2 -0
- package/src/button.ts +63 -0
- package/src/csp-provider.ts +37 -0
- package/src/dialog.ts +89 -22
- package/src/direction-provider.ts +32 -0
- package/src/index.ts +7 -0
- package/src/popover.ts +92 -22
- package/src/preview-card.ts +812 -0
- package/src/tooltip.ts +1053 -0
- package/src/unstable-use-media-query.ts +101 -0
- package/src/utils/CSPContext.ts +21 -0
- package/src/utils/FloatingPortalLite.ts +29 -0
- package/src/utils/createChangeEventDetails.ts +1 -0
- package/src/utils/floating/FloatingDelayGroup.ts +280 -0
- package/src/utils/floating/element.ts +36 -2
- package/src/utils/floating/safePolygon.ts +448 -0
- package/src/utils/floating/useClientPoint.ts +276 -0
- package/src/utils/floating/useFocus.ts +248 -0
- package/src/utils/floating/useHoverFloatingInteraction.ts +301 -11
- package/src/utils/floating/useHoverInteractionSharedState.ts +139 -0
- package/src/utils/floating/useHoverReferenceInteraction.ts +507 -14
- package/src/utils/floating/useHoverShared.ts +78 -0
- package/src/utils/getCssDimensions.ts +28 -0
- package/src/utils/platform.ts +10 -0
- package/src/utils/popups/index.ts +6 -0
- package/src/utils/popups/inlineRect.ts +289 -0
- package/src/utils/usePopupAutoResize.ts +250 -0
- package/src/utils/usePopupViewport.ts +375 -0
- package/src/utils/usePreviousValue.ts +25 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/utils/popups/inlineRect.ts (v1.6.0). Floating UI ships an
|
|
2
|
+
// `inline()` middleware; this local version mirrors its line-rect selection while adding trigger
|
|
3
|
+
// identity checks, delayed-open hit-line reuse, and better left/right edge grouping for Preview
|
|
4
|
+
// Card's reusable trigger model. Pure geometry plus a floating-ui middleware — transcribed as-is,
|
|
5
|
+
// with React's ref types collapsed to plain `{ current }` objects and native event handlers.
|
|
6
|
+
import { isElement } from '../dom';
|
|
7
|
+
|
|
8
|
+
export interface InlineRectCoords {
|
|
9
|
+
/** The x position in viewport coordinates. */
|
|
10
|
+
x: number;
|
|
11
|
+
/** The y position in viewport coordinates. */
|
|
12
|
+
y: number;
|
|
13
|
+
/** The line index under the pointer when coordinates were captured. */
|
|
14
|
+
lineIndex?: number | undefined;
|
|
15
|
+
/** The trigger element whose rects produced these coordinates. */
|
|
16
|
+
element: Element;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface RectLike {
|
|
20
|
+
left: number;
|
|
21
|
+
top: number;
|
|
22
|
+
right: number;
|
|
23
|
+
bottom: number;
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ClientRectLike extends RectLike {
|
|
29
|
+
x: number;
|
|
30
|
+
y: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface ClientRectsReference {
|
|
34
|
+
getClientRects(): ArrayLike<RectLike>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createRect(left: number, top: number, right: number, bottom: number): ClientRectLike {
|
|
38
|
+
return {
|
|
39
|
+
left,
|
|
40
|
+
top,
|
|
41
|
+
right,
|
|
42
|
+
bottom,
|
|
43
|
+
x: left,
|
|
44
|
+
y: top,
|
|
45
|
+
width: right - left,
|
|
46
|
+
height: bottom - top,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function copyRect(rect: RectLike): RectLike {
|
|
51
|
+
return {
|
|
52
|
+
left: rect.left,
|
|
53
|
+
top: rect.top,
|
|
54
|
+
right: rect.right,
|
|
55
|
+
bottom: rect.bottom,
|
|
56
|
+
width: rect.width,
|
|
57
|
+
height: rect.height,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getLineRects(rects: ArrayLike<RectLike>) {
|
|
62
|
+
const lines: RectLike[] = [];
|
|
63
|
+
let previousRect: RectLike | undefined;
|
|
64
|
+
let left = Number.POSITIVE_INFINITY;
|
|
65
|
+
let top = Number.POSITIVE_INFINITY;
|
|
66
|
+
let right = Number.NEGATIVE_INFINITY;
|
|
67
|
+
let bottom = Number.NEGATIVE_INFINITY;
|
|
68
|
+
|
|
69
|
+
for (const rect of Array.from(rects).sort((a, b) => a.top - b.top)) {
|
|
70
|
+
left = Math.min(left, rect.left);
|
|
71
|
+
top = Math.min(top, rect.top);
|
|
72
|
+
right = Math.max(right, rect.right);
|
|
73
|
+
bottom = Math.max(bottom, rect.bottom);
|
|
74
|
+
|
|
75
|
+
if (!previousRect || rect.top - previousRect.top > previousRect.height / 2) {
|
|
76
|
+
lines.push(copyRect(rect));
|
|
77
|
+
} else {
|
|
78
|
+
const line = lines[lines.length - 1];
|
|
79
|
+
line.left = Math.min(line.left, rect.left);
|
|
80
|
+
line.right = Math.max(line.right, rect.right);
|
|
81
|
+
line.bottom = Math.max(line.bottom, rect.bottom);
|
|
82
|
+
line.width = line.right - line.left;
|
|
83
|
+
line.height = line.bottom - line.top;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
previousRect = rect;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
lines,
|
|
91
|
+
fallback: createRect(left, top, right, bottom),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function findLineIndex(lines: RectLike[], x: number, y: number) {
|
|
96
|
+
return lines.findIndex(
|
|
97
|
+
(lineRect) =>
|
|
98
|
+
x > lineRect.left - 2 &&
|
|
99
|
+
x < lineRect.right + 2 &&
|
|
100
|
+
y > lineRect.top - 2 &&
|
|
101
|
+
y < lineRect.bottom + 2,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function createClientRect(rect: RectLike) {
|
|
106
|
+
return createRect(rect.left, rect.top, rect.right, rect.bottom);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getInlineRectCoords(
|
|
110
|
+
element: Element,
|
|
111
|
+
clientX: number,
|
|
112
|
+
clientY: number,
|
|
113
|
+
): InlineRectCoords | undefined {
|
|
114
|
+
const { lines } = getLineRects(element.getClientRects());
|
|
115
|
+
|
|
116
|
+
if (lines.length < 2) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const lineIndex = findLineIndex(lines, clientX, clientY);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
x: clientX,
|
|
124
|
+
y: clientY,
|
|
125
|
+
lineIndex: lineIndex === -1 ? undefined : lineIndex,
|
|
126
|
+
element,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getInlineReferenceRect(
|
|
131
|
+
reference: ClientRectsReference,
|
|
132
|
+
placement: string,
|
|
133
|
+
coords: InlineRectCoords | undefined,
|
|
134
|
+
): ClientRectLike | null {
|
|
135
|
+
const { lines, fallback } = getLineRects(reference.getClientRects());
|
|
136
|
+
|
|
137
|
+
if (lines.length < 2) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const x = coords?.x;
|
|
142
|
+
const y = coords?.y;
|
|
143
|
+
const side = placement[0];
|
|
144
|
+
|
|
145
|
+
if (coords?.lineIndex != null && lines[coords.lineIndex]) {
|
|
146
|
+
return createClientRect(lines[coords.lineIndex]);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (x != null && y != null) {
|
|
150
|
+
const lineIndex = findLineIndex(lines, x, y);
|
|
151
|
+
|
|
152
|
+
if (lineIndex !== -1) {
|
|
153
|
+
return createClientRect(lines[lineIndex]);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (lines.length === 2 && lines[0].left > lines[1].right && x != null && y != null) {
|
|
158
|
+
return fallback;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (side === 't' || side === 'b') {
|
|
162
|
+
const firstRect = lines[0];
|
|
163
|
+
const lastRect = lines[lines.length - 1];
|
|
164
|
+
const targetRect = side === 't' ? firstRect : lastRect;
|
|
165
|
+
|
|
166
|
+
return createRect(targetRect.left, firstRect.top, targetRect.right, lastRect.bottom);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const isLeft = side === 'l';
|
|
170
|
+
let left = lines[0].left;
|
|
171
|
+
let right = lines[0].right;
|
|
172
|
+
let edge = isLeft ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
|
|
173
|
+
let targetFirstRect = lines[0];
|
|
174
|
+
let targetLastRect = lines[0];
|
|
175
|
+
|
|
176
|
+
for (const rect of lines) {
|
|
177
|
+
left = Math.min(left, rect.left);
|
|
178
|
+
right = Math.max(right, rect.right);
|
|
179
|
+
|
|
180
|
+
const nextEdge = isLeft ? rect.left : rect.right;
|
|
181
|
+
|
|
182
|
+
if ((isLeft && nextEdge < edge) || (!isLeft && nextEdge > edge)) {
|
|
183
|
+
edge = nextEdge;
|
|
184
|
+
targetFirstRect = rect;
|
|
185
|
+
targetLastRect = rect;
|
|
186
|
+
} else if (nextEdge === edge) {
|
|
187
|
+
targetLastRect = rect;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return createRect(left, targetFirstRect.top, right, targetLastRect.bottom);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function getContextElement(reference: any): Element | undefined {
|
|
195
|
+
if ('contextElement' in reference && reference.contextElement) {
|
|
196
|
+
return reference.contextElement;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return isElement(reference) ? (reference as Element) : undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function getInlineRectTriggerProps(
|
|
203
|
+
coordsRef: { current: InlineRectCoords | undefined },
|
|
204
|
+
isOpen: boolean,
|
|
205
|
+
): Record<string, any> {
|
|
206
|
+
function updateCoords(event: MouseEvent) {
|
|
207
|
+
updateInlineRectCoords(coordsRef, event.currentTarget as Element, event.clientX, event.clientY);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function updateCoordsIfClosed(event: MouseEvent) {
|
|
211
|
+
if (!isOpen) {
|
|
212
|
+
updateCoords(event);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
onFocus() {
|
|
218
|
+
coordsRef.current = undefined;
|
|
219
|
+
},
|
|
220
|
+
onMouseEnter: updateCoordsIfClosed,
|
|
221
|
+
onMouseMove: updateCoordsIfClosed,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function updateInlineRectCoords(
|
|
226
|
+
coordsRef: { current: InlineRectCoords | undefined },
|
|
227
|
+
element: Element,
|
|
228
|
+
clientX: number,
|
|
229
|
+
clientY: number,
|
|
230
|
+
) {
|
|
231
|
+
const nextCoords = getInlineRectCoords(element, clientX, clientY);
|
|
232
|
+
coordsRef.current = nextCoords;
|
|
233
|
+
return nextCoords;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function createInlineMiddleware(coordsRef: { current: InlineRectCoords | undefined }): any {
|
|
237
|
+
return {
|
|
238
|
+
name: 'inline',
|
|
239
|
+
async fn(state: any) {
|
|
240
|
+
const reference = state.elements.reference;
|
|
241
|
+
|
|
242
|
+
if (
|
|
243
|
+
typeof (reference as Partial<ClientRectsReference> | null)?.getClientRects !== 'function'
|
|
244
|
+
) {
|
|
245
|
+
return {};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const contextElement = getContextElement(reference);
|
|
249
|
+
const coords = coordsRef.current;
|
|
250
|
+
const currentCoords =
|
|
251
|
+
coords?.element === reference || coords?.element === contextElement ? coords : undefined;
|
|
252
|
+
const rect = getInlineReferenceRect(
|
|
253
|
+
reference as ClientRectsReference,
|
|
254
|
+
state.placement,
|
|
255
|
+
currentCoords,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
if (!rect || typeof state.platform.getElementRects !== 'function') {
|
|
259
|
+
return {};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const resetRects = await state.platform.getElementRects({
|
|
263
|
+
reference: {
|
|
264
|
+
contextElement,
|
|
265
|
+
getBoundingClientRect() {
|
|
266
|
+
return rect;
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
floating: state.elements.floating,
|
|
270
|
+
strategy: state.strategy,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
if (
|
|
274
|
+
state.rects.reference.x === resetRects.reference.x &&
|
|
275
|
+
state.rects.reference.y === resetRects.reference.y &&
|
|
276
|
+
state.rects.reference.width === resetRects.reference.width &&
|
|
277
|
+
state.rects.reference.height === resetRects.reference.height
|
|
278
|
+
) {
|
|
279
|
+
return {};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
reset: {
|
|
284
|
+
rects: resetRects,
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/utils/usePopupAutoResize.ts (v1.6.0). Lets a popup resize
|
|
2
|
+
// to its content while keeping the change animatable: it measures the new content at `max-content`,
|
|
3
|
+
// pins the popup to its previous size, then animates to the new one on the next frame.
|
|
4
|
+
//
|
|
5
|
+
// octane adaptations: `useIsoLayoutEffect` → `useLayoutEffect`; every composed hook threads an
|
|
6
|
+
// explicit slot.
|
|
7
|
+
//
|
|
8
|
+
// SLOT: plain-`.ts` hook; the trailing arg is the caller's slot.
|
|
9
|
+
import { useLayoutEffect, useMemo, useRef } from 'octane';
|
|
10
|
+
|
|
11
|
+
import { S, splitSlot, subSlot } from '../internal';
|
|
12
|
+
import { NOOP, EMPTY_OBJECT } from './empty';
|
|
13
|
+
import { useAnimationFrame } from './useAnimationFrame';
|
|
14
|
+
import { useAnimationsFinished } from './useAnimationsFinished';
|
|
15
|
+
import { useStableCallback } from './useStableCallback';
|
|
16
|
+
import { getCssDimensions } from './getCssDimensions';
|
|
17
|
+
import type { Dimensions } from './getCssDimensions';
|
|
18
|
+
import type { Side } from './useAnchorPositioning';
|
|
19
|
+
|
|
20
|
+
export interface UsePopupAutoResizeParameters {
|
|
21
|
+
/** Element to resize. */
|
|
22
|
+
popupElement: HTMLElement | null;
|
|
23
|
+
/** Positioner element (parent of the popup). */
|
|
24
|
+
positionerElement: HTMLElement | null;
|
|
25
|
+
/** Whether the popup is mounted. */
|
|
26
|
+
mounted: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Content that may change and trigger a resize. It doesn't have to be the actual content, just
|
|
29
|
+
* a value that changes when a resize is needed.
|
|
30
|
+
*/
|
|
31
|
+
content: unknown;
|
|
32
|
+
/** Fired immediately before measuring the dimensions of the new content. */
|
|
33
|
+
onMeasureLayout?: (() => void) | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Fired after the new dimensions have been measured.
|
|
36
|
+
* @param previousDimensions Dimensions before the change, or `null` on the first measurement.
|
|
37
|
+
* @param newDimensions Newly measured dimensions.
|
|
38
|
+
*/
|
|
39
|
+
onMeasureLayoutComplete?:
|
|
40
|
+
((previousDimensions: Dimensions | null, newDimensions: Dimensions) => void) | undefined;
|
|
41
|
+
side: Side;
|
|
42
|
+
direction: 'ltr' | 'rtl';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function overrideElementStyle(element: HTMLElement, property: string, value: string): () => void {
|
|
46
|
+
const originalValue = element.style.getPropertyValue(property);
|
|
47
|
+
element.style.setProperty(property, value);
|
|
48
|
+
return () => {
|
|
49
|
+
element.style.setProperty(property, originalValue);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function applyElementStyles(element: HTMLElement, styles: Record<string, string>): () => void {
|
|
54
|
+
const restorers: Array<() => void> = [];
|
|
55
|
+
for (const [key, value] of Object.entries(styles)) {
|
|
56
|
+
restorers.push(overrideElementStyle(element, key, value));
|
|
57
|
+
}
|
|
58
|
+
return restorers.length
|
|
59
|
+
? () => {
|
|
60
|
+
restorers.forEach((restore) => restore());
|
|
61
|
+
}
|
|
62
|
+
: NOOP;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function setPopupCssSize(popupElement: HTMLElement, size: Dimensions | 'auto'): void {
|
|
66
|
+
const width = size === 'auto' ? 'auto' : `${size.width}px`;
|
|
67
|
+
const height = size === 'auto' ? 'auto' : `${size.height}px`;
|
|
68
|
+
popupElement.style.setProperty('--popup-width', width);
|
|
69
|
+
popupElement.style.setProperty('--popup-height', height);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function setPositionerCssSize(
|
|
73
|
+
positionerElement: HTMLElement,
|
|
74
|
+
size: Dimensions | 'max-content',
|
|
75
|
+
): void {
|
|
76
|
+
const width = size === 'max-content' ? 'max-content' : `${size.width}px`;
|
|
77
|
+
const height = size === 'max-content' ? 'max-content' : `${size.height}px`;
|
|
78
|
+
positionerElement.style.setProperty('--positioner-width', width);
|
|
79
|
+
positionerElement.style.setProperty('--positioner-height', height);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function usePopupAutoResize(...args: any[]): void {
|
|
83
|
+
const [user, slotArg] = splitSlot(args);
|
|
84
|
+
const slot = slotArg ?? S('usePopupAutoResize');
|
|
85
|
+
const parameters = user[0] as UsePopupAutoResizeParameters;
|
|
86
|
+
|
|
87
|
+
const {
|
|
88
|
+
popupElement,
|
|
89
|
+
positionerElement,
|
|
90
|
+
content,
|
|
91
|
+
mounted,
|
|
92
|
+
onMeasureLayout: onMeasureLayoutParam,
|
|
93
|
+
onMeasureLayoutComplete: onMeasureLayoutCompleteParam,
|
|
94
|
+
side,
|
|
95
|
+
direction,
|
|
96
|
+
} = parameters;
|
|
97
|
+
|
|
98
|
+
const runOnceAnimationsFinish = useAnimationsFinished(
|
|
99
|
+
popupElement,
|
|
100
|
+
true,
|
|
101
|
+
false,
|
|
102
|
+
subSlot(slot, 'anim'),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const animationFrame = useAnimationFrame(subSlot(slot, 'frame'));
|
|
106
|
+
|
|
107
|
+
const committedDimensionsRef = useRef<Dimensions | null>(null, subSlot(slot, 'dims'));
|
|
108
|
+
const isInitialRenderRef = useRef(true, subSlot(slot, 'init'));
|
|
109
|
+
|
|
110
|
+
const restoreAnchoringStylesRef = useRef<() => void>(NOOP, subSlot(slot, 'restore'));
|
|
111
|
+
|
|
112
|
+
const onMeasureLayout = useStableCallback(onMeasureLayoutParam, subSlot(slot, 'oml'));
|
|
113
|
+
const onMeasureLayoutComplete = useStableCallback(
|
|
114
|
+
onMeasureLayoutCompleteParam,
|
|
115
|
+
subSlot(slot, 'omlc'),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const anchoringStyles = useMemo(
|
|
119
|
+
() => {
|
|
120
|
+
// Ensure the popup size transitions correctly when anchored to `bottom` (side=top) or
|
|
121
|
+
// `right` (side=left).
|
|
122
|
+
let isOriginSide = side === 'top';
|
|
123
|
+
let isPhysicalLeft = side === 'left';
|
|
124
|
+
if (direction === 'rtl') {
|
|
125
|
+
isOriginSide = isOriginSide || side === 'inline-end';
|
|
126
|
+
isPhysicalLeft = isPhysicalLeft || side === 'inline-end';
|
|
127
|
+
} else {
|
|
128
|
+
isOriginSide = isOriginSide || side === 'inline-start';
|
|
129
|
+
isPhysicalLeft = isPhysicalLeft || side === 'inline-start';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return isOriginSide
|
|
133
|
+
? ({
|
|
134
|
+
position: 'absolute',
|
|
135
|
+
[side === 'top' ? 'bottom' : 'top']: '0',
|
|
136
|
+
[isPhysicalLeft ? 'right' : 'left']: '0',
|
|
137
|
+
} as Record<string, string>)
|
|
138
|
+
: (EMPTY_OBJECT as unknown as Record<string, string>);
|
|
139
|
+
},
|
|
140
|
+
[side, direction],
|
|
141
|
+
subSlot(slot, 'anchoring'),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
useLayoutEffect(
|
|
145
|
+
() => {
|
|
146
|
+
// Reset the state when the popup is closed.
|
|
147
|
+
if (!mounted) {
|
|
148
|
+
restoreAnchoringStylesRef.current = NOOP;
|
|
149
|
+
isInitialRenderRef.current = true;
|
|
150
|
+
committedDimensionsRef.current = null;
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!popupElement || !positionerElement) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
restoreAnchoringStylesRef.current = applyElementStyles(popupElement, anchoringStyles);
|
|
159
|
+
|
|
160
|
+
// Measure the rendered size to enable transitions.
|
|
161
|
+
setPopupCssSize(popupElement, 'auto');
|
|
162
|
+
|
|
163
|
+
const restorePopupPosition = overrideElementStyle(popupElement, 'position', 'static');
|
|
164
|
+
const restorePopupTransform = overrideElementStyle(popupElement, 'transform', 'none');
|
|
165
|
+
const restorePopupScale = overrideElementStyle(popupElement, 'scale', '1');
|
|
166
|
+
const restorePositionerAvailableSize = applyElementStyles(positionerElement, {
|
|
167
|
+
'--available-width': 'max-content',
|
|
168
|
+
'--available-height': 'max-content',
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
function restoreMeasurementOverrides() {
|
|
172
|
+
restorePopupPosition();
|
|
173
|
+
restorePopupTransform();
|
|
174
|
+
restorePositionerAvailableSize();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function restoreMeasurementOverridesIncludingScale() {
|
|
178
|
+
restoreMeasurementOverrides();
|
|
179
|
+
restorePopupScale();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
onMeasureLayout?.();
|
|
183
|
+
|
|
184
|
+
// Initial render (for each time the popup opens).
|
|
185
|
+
if (isInitialRenderRef.current || committedDimensionsRef.current === null) {
|
|
186
|
+
setPositionerCssSize(positionerElement, 'max-content');
|
|
187
|
+
|
|
188
|
+
const dimensions = getCssDimensions(popupElement);
|
|
189
|
+
|
|
190
|
+
committedDimensionsRef.current = dimensions;
|
|
191
|
+
|
|
192
|
+
setPositionerCssSize(positionerElement, dimensions);
|
|
193
|
+
restoreMeasurementOverridesIncludingScale();
|
|
194
|
+
onMeasureLayoutComplete?.(null, dimensions);
|
|
195
|
+
|
|
196
|
+
isInitialRenderRef.current = false;
|
|
197
|
+
|
|
198
|
+
return () => {
|
|
199
|
+
restoreAnchoringStylesRef.current();
|
|
200
|
+
restoreAnchoringStylesRef.current = NOOP;
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Subsequent renders while open (when `content` changes).
|
|
205
|
+
setPositionerCssSize(positionerElement, 'max-content');
|
|
206
|
+
|
|
207
|
+
const previousDimensions = committedDimensionsRef.current;
|
|
208
|
+
const newDimensions = getCssDimensions(popupElement);
|
|
209
|
+
|
|
210
|
+
// Commit immediately so future content changes have a stable previous size.
|
|
211
|
+
committedDimensionsRef.current = newDimensions;
|
|
212
|
+
|
|
213
|
+
setPopupCssSize(popupElement, previousDimensions);
|
|
214
|
+
restoreMeasurementOverridesIncludingScale();
|
|
215
|
+
onMeasureLayoutComplete?.(previousDimensions, newDimensions);
|
|
216
|
+
|
|
217
|
+
setPositionerCssSize(positionerElement, newDimensions);
|
|
218
|
+
|
|
219
|
+
const abortController = new AbortController();
|
|
220
|
+
|
|
221
|
+
animationFrame.request(() => {
|
|
222
|
+
setPopupCssSize(popupElement, newDimensions);
|
|
223
|
+
|
|
224
|
+
runOnceAnimationsFinish(() => {
|
|
225
|
+
popupElement.style.setProperty('--popup-width', 'auto');
|
|
226
|
+
popupElement.style.setProperty('--popup-height', 'auto');
|
|
227
|
+
}, abortController.signal);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
return () => {
|
|
231
|
+
abortController.abort();
|
|
232
|
+
animationFrame.cancel();
|
|
233
|
+
restoreAnchoringStylesRef.current();
|
|
234
|
+
restoreAnchoringStylesRef.current = NOOP;
|
|
235
|
+
};
|
|
236
|
+
},
|
|
237
|
+
[
|
|
238
|
+
content,
|
|
239
|
+
popupElement,
|
|
240
|
+
positionerElement,
|
|
241
|
+
runOnceAnimationsFinish,
|
|
242
|
+
animationFrame,
|
|
243
|
+
mounted,
|
|
244
|
+
onMeasureLayout,
|
|
245
|
+
onMeasureLayoutComplete,
|
|
246
|
+
anchoringStyles,
|
|
247
|
+
],
|
|
248
|
+
subSlot(slot, 'l:resize'),
|
|
249
|
+
);
|
|
250
|
+
}
|