-
Notifications
You must be signed in to change notification settings - Fork 4.3k
/
Copy pathutils.ts
320 lines (305 loc) · 10 KB
/
utils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
/**
* External dependencies
*/
// eslint-disable-next-line no-restricted-imports
import type { MotionProps } from 'framer-motion';
import type { ReferenceType } from '@floating-ui/react-dom';
/**
* Internal dependencies
*/
import type {
PopoverProps,
PopoverAnchorRefReference,
PopoverAnchorRefTopBottom,
} from './types';
const POSITION_TO_PLACEMENT: Record<
NonNullable< PopoverProps[ 'position' ] >,
NonNullable< PopoverProps[ 'placement' ] >
> = {
bottom: 'bottom',
top: 'top',
'middle left': 'left',
'middle right': 'right',
'bottom left': 'bottom-end',
'bottom center': 'bottom',
'bottom right': 'bottom-start',
'top left': 'top-end',
'top center': 'top',
'top right': 'top-start',
'middle left left': 'left',
'middle left right': 'left',
'middle left bottom': 'left-end',
'middle left top': 'left-start',
'middle right left': 'right',
'middle right right': 'right',
'middle right bottom': 'right-end',
'middle right top': 'right-start',
'bottom left left': 'bottom-end',
'bottom left right': 'bottom-end',
'bottom left bottom': 'bottom-end',
'bottom left top': 'bottom-end',
'bottom center left': 'bottom',
'bottom center right': 'bottom',
'bottom center bottom': 'bottom',
'bottom center top': 'bottom',
'bottom right left': 'bottom-start',
'bottom right right': 'bottom-start',
'bottom right bottom': 'bottom-start',
'bottom right top': 'bottom-start',
'top left left': 'top-end',
'top left right': 'top-end',
'top left bottom': 'top-end',
'top left top': 'top-end',
'top center left': 'top',
'top center right': 'top',
'top center bottom': 'top',
'top center top': 'top',
'top right left': 'top-start',
'top right right': 'top-start',
'top right bottom': 'top-start',
'top right top': 'top-start',
// `middle`/`middle center [corner?]` positions are associated to a fallback
// `bottom` placement because there aren't any corresponding placement values.
middle: 'bottom',
'middle center': 'bottom',
'middle center bottom': 'bottom',
'middle center left': 'bottom',
'middle center right': 'bottom',
'middle center top': 'bottom',
};
/**
* Converts the `Popover`'s legacy "position" prop to the new "placement" prop
* (used by `floating-ui`).
*
* @param position The legacy position
* @return The corresponding placement
*/
export const positionToPlacement = (
position: NonNullable< PopoverProps[ 'position' ] >
): NonNullable< PopoverProps[ 'placement' ] > =>
POSITION_TO_PLACEMENT[ position ] ?? 'bottom';
/**
* @typedef AnimationOrigin
* @type {Object}
* @property {number} originX A number between 0 and 1 (in CSS logical properties jargon, 0 is "start", 0.5 is "center", and 1 is "end")
* @property {number} originY A number between 0 and 1 (0 is top, 0.5 is center, and 1 is bottom)
*/
const PLACEMENT_TO_ANIMATION_ORIGIN: Record<
NonNullable< PopoverProps[ 'placement' ] >,
{ originX: number; originY: number }
> = {
top: { originX: 0.5, originY: 1 }, // open from bottom, center
'top-start': { originX: 0, originY: 1 }, // open from bottom, left
'top-end': { originX: 1, originY: 1 }, // open from bottom, right
right: { originX: 0, originY: 0.5 }, // open from middle, left
'right-start': { originX: 0, originY: 0 }, // open from top, left
'right-end': { originX: 0, originY: 1 }, // open from bottom, left
bottom: { originX: 0.5, originY: 0 }, // open from top, center
'bottom-start': { originX: 0, originY: 0 }, // open from top, left
'bottom-end': { originX: 1, originY: 0 }, // open from top, right
left: { originX: 1, originY: 0.5 }, // open from middle, right
'left-start': { originX: 1, originY: 0 }, // open from top, right
'left-end': { originX: 1, originY: 1 }, // open from bottom, right
overlay: { originX: 0.5, originY: 0.5 }, // open from center, center
};
/**
* Given the floating-ui `placement`, compute the framer-motion props for the
* popover's entry animation.
*
* @param placement A placement string from floating ui
* @return The object containing the motion props
*/
export const placementToMotionAnimationProps = (
placement: NonNullable< PopoverProps[ 'placement' ] >
): MotionProps => {
const translateProp =
placement.startsWith( 'top' ) || placement.startsWith( 'bottom' )
? 'translateY'
: 'translateX';
const translateDirection =
placement.startsWith( 'top' ) || placement.startsWith( 'left' )
? 1
: -1;
return {
style: PLACEMENT_TO_ANIMATION_ORIGIN[ placement ],
initial: {
opacity: 0,
scale: 0,
[ translateProp ]: `${ 2 * translateDirection }em`,
},
animate: { opacity: 1, scale: 1, [ translateProp ]: 0 },
transition: { duration: 0.1, ease: [ 0, 0, 0.2, 1 ] },
};
};
/**
* Returns the offset of a document's frame element.
*
* @param document The iframe's owner document.
*
* @return The offset of the document's frame element, or undefined if the
* document has no frame element.
*/
export const getFrameOffset = (
document?: Document
): { x: number; y: number } | undefined => {
const frameElement = document?.defaultView?.frameElement;
if ( ! frameElement ) {
return;
}
const iframeRect = frameElement.getBoundingClientRect();
return { x: iframeRect.left, y: iframeRect.top };
};
export const getFrameScale = (
document?: Document
): {
x: number;
y: number;
} => {
const frameElement = document?.defaultView?.frameElement as HTMLElement;
if ( ! frameElement ) {
return { x: 1, y: 1 };
}
const rect = frameElement.getBoundingClientRect();
return {
x: rect.width / frameElement.offsetWidth,
y: rect.height / frameElement.offsetHeight,
};
};
export const getReferenceOwnerDocument = ( {
anchor,
anchorRef,
anchorRect,
getAnchorRect,
fallbackReferenceElement,
fallbackDocument,
}: Pick<
PopoverProps,
'anchorRef' | 'anchorRect' | 'getAnchorRect' | 'anchor'
> & {
fallbackReferenceElement: Element | null;
fallbackDocument: Document;
} ): Document => {
// In floating-ui's terms:
// - "reference" refers to the popover's anchor element.
// - "floating" refers the floating popover's element.
// A floating element can also be positioned relative to a virtual element,
// instead of a real one. A virtual element is represented by an object
// with the `getBoundingClientRect()` function (like real elements).
// See https://floating-ui.com/docs/virtual-elements for more info.
let resultingReferenceOwnerDoc;
if ( anchor ) {
resultingReferenceOwnerDoc = anchor.ownerDocument;
} else if ( ( anchorRef as PopoverAnchorRefTopBottom | undefined )?.top ) {
resultingReferenceOwnerDoc = ( anchorRef as PopoverAnchorRefTopBottom )
?.top.ownerDocument;
} else if ( ( anchorRef as Range | undefined )?.startContainer ) {
resultingReferenceOwnerDoc = ( anchorRef as Range ).startContainer
.ownerDocument;
} else if (
( anchorRef as PopoverAnchorRefReference | undefined )?.current
) {
resultingReferenceOwnerDoc = (
( anchorRef as PopoverAnchorRefReference ).current as Element
).ownerDocument;
} else if ( anchorRef as Element | undefined ) {
// This one should be deprecated.
resultingReferenceOwnerDoc = ( anchorRef as Element ).ownerDocument;
} else if ( anchorRect && anchorRect?.ownerDocument ) {
resultingReferenceOwnerDoc = anchorRect.ownerDocument;
} else if ( getAnchorRect ) {
resultingReferenceOwnerDoc = getAnchorRect(
fallbackReferenceElement
)?.ownerDocument;
}
return resultingReferenceOwnerDoc ?? fallbackDocument;
};
export const getReferenceElement = ( {
anchor,
anchorRef,
anchorRect,
getAnchorRect,
fallbackReferenceElement,
scale,
}: Pick<
PopoverProps,
'anchorRef' | 'anchorRect' | 'getAnchorRect' | 'anchor'
> & {
fallbackReferenceElement: Element | null;
scale: { x: number; y: number };
} ): ReferenceType | null => {
let referenceElement = null;
if ( anchor ) {
referenceElement = anchor;
} else if ( ( anchorRef as PopoverAnchorRefTopBottom | undefined )?.top ) {
// Create a virtual element for the ref. The expectation is that
// if anchorRef.top is defined, then anchorRef.bottom is defined too.
// Seems to be used by the block toolbar, when multiple blocks are selected
// (top and bottom blocks are used to calculate the resulting rect).
referenceElement = {
getBoundingClientRect() {
const topRect = (
anchorRef as PopoverAnchorRefTopBottom
).top.getBoundingClientRect();
const bottomRect = (
anchorRef as PopoverAnchorRefTopBottom
).bottom.getBoundingClientRect();
return new window.DOMRect(
topRect.x,
topRect.y,
topRect.width,
bottomRect.bottom - topRect.top
);
},
};
} else if (
( anchorRef as PopoverAnchorRefReference | undefined )?.current
) {
// Standard React ref.
referenceElement = ( anchorRef as PopoverAnchorRefReference ).current;
} else if ( anchorRef as Element | undefined ) {
// If `anchorRef` holds directly the element's value (no `current` key)
// This is a weird scenario and should be deprecated.
referenceElement = anchorRef as Element;
} else if ( anchorRect ) {
// Create a virtual element for the ref.
referenceElement = {
getBoundingClientRect() {
return anchorRect;
},
};
} else if ( getAnchorRect ) {
// Create a virtual element for the ref.
referenceElement = {
getBoundingClientRect() {
const rect = getAnchorRect( fallbackReferenceElement );
return new window.DOMRect(
rect.x ?? rect.left,
rect.y ?? rect.top,
rect.width ?? rect.right - rect.left,
rect.height ?? rect.bottom - rect.top
);
},
};
} else if ( fallbackReferenceElement ) {
// If no explicit ref is passed via props, fall back to
// anchoring to the popover's parent node.
referenceElement = fallbackReferenceElement.parentElement;
}
if ( referenceElement && ( scale.x !== 1 || scale.y !== 1 ) ) {
// If the popover is inside an iframe, the coordinates of the
// reference element need to be scaled to match the iframe's scale.
const rect = referenceElement.getBoundingClientRect();
referenceElement = {
getBoundingClientRect() {
return new window.DOMRect(
rect.x * scale.x,
rect.y * scale.y,
rect.width * scale.x,
rect.height * scale.y
);
},
};
}
// Convert any `undefined` value to `null`.
return referenceElement ?? null;
};