@react-spectrum/menu 3.22.8 → 3.22.9

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.
Files changed (71) hide show
  1. package/dist/Menu.main.js +1 -1
  2. package/dist/Menu.main.js.map +1 -1
  3. package/dist/Menu.mjs +2 -2
  4. package/dist/Menu.module.js +2 -2
  5. package/dist/Menu.module.js.map +1 -1
  6. package/dist/Popover.main.js +196 -0
  7. package/dist/Popover.main.js.map +1 -0
  8. package/dist/Popover.mjs +191 -0
  9. package/dist/Popover.module.js +191 -0
  10. package/dist/Popover.module.js.map +1 -0
  11. package/dist/SubmenuTrigger.main.js +2 -2
  12. package/dist/SubmenuTrigger.main.js.map +1 -1
  13. package/dist/SubmenuTrigger.mjs +2 -2
  14. package/dist/SubmenuTrigger.module.js +2 -2
  15. package/dist/SubmenuTrigger.module.js.map +1 -1
  16. package/dist/Underlay.main.js +54 -0
  17. package/dist/Underlay.main.js.map +1 -0
  18. package/dist/Underlay.mjs +49 -0
  19. package/dist/Underlay.module.js +49 -0
  20. package/dist/Underlay.module.js.map +1 -0
  21. package/dist/calculatePosition.main.js +392 -0
  22. package/dist/calculatePosition.main.js.map +1 -0
  23. package/dist/calculatePosition.mjs +386 -0
  24. package/dist/calculatePosition.module.js +386 -0
  25. package/dist/calculatePosition.module.js.map +1 -0
  26. package/dist/menu.2259a533.css +210 -0
  27. package/dist/menu.2259a533.css.map +1 -0
  28. package/dist/menu.359a0c0a.css +131 -0
  29. package/dist/menu.359a0c0a.css.map +1 -0
  30. package/dist/menu.57b3a408.css +12 -0
  31. package/dist/menu.57b3a408.css.map +1 -0
  32. package/dist/overlays_css.main.js +35 -0
  33. package/dist/overlays_css.main.js.map +1 -0
  34. package/dist/overlays_css.mjs +37 -0
  35. package/dist/overlays_css.module.js +37 -0
  36. package/dist/overlays_css.module.js.map +1 -0
  37. package/dist/popover_vars_css.main.js +83 -0
  38. package/dist/popover_vars_css.main.js.map +1 -0
  39. package/dist/popover_vars_css.mjs +85 -0
  40. package/dist/popover_vars_css.module.js +85 -0
  41. package/dist/popover_vars_css.module.js.map +1 -0
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/underlay_vars_css.main.js +50 -0
  44. package/dist/underlay_vars_css.main.js.map +1 -0
  45. package/dist/underlay_vars_css.mjs +52 -0
  46. package/dist/underlay_vars_css.module.js +52 -0
  47. package/dist/underlay_vars_css.module.js.map +1 -0
  48. package/dist/useCloseOnScroll.main.js +49 -0
  49. package/dist/useCloseOnScroll.main.js.map +1 -0
  50. package/dist/useCloseOnScroll.mjs +44 -0
  51. package/dist/useCloseOnScroll.module.js +44 -0
  52. package/dist/useCloseOnScroll.module.js.map +1 -0
  53. package/dist/useOverlayPosition.main.js +225 -0
  54. package/dist/useOverlayPosition.main.js.map +1 -0
  55. package/dist/useOverlayPosition.mjs +220 -0
  56. package/dist/useOverlayPosition.module.js +220 -0
  57. package/dist/useOverlayPosition.module.js.map +1 -0
  58. package/dist/usePopover.main.js +73 -0
  59. package/dist/usePopover.main.js.map +1 -0
  60. package/dist/usePopover.mjs +68 -0
  61. package/dist/usePopover.module.js +68 -0
  62. package/dist/usePopover.module.js.map +1 -0
  63. package/package.json +19 -19
  64. package/src/Menu.tsx +2 -1
  65. package/src/Popover.tsx +240 -0
  66. package/src/SubmenuTrigger.tsx +1 -1
  67. package/src/Underlay.tsx +43 -0
  68. package/src/calculatePosition.ts +627 -0
  69. package/src/useCloseOnScroll.ts +64 -0
  70. package/src/useOverlayPosition.ts +327 -0
  71. package/src/usePopover.ts +136 -0
@@ -0,0 +1,627 @@
1
+ /*
2
+ * Copyright 2020 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import {Axis, Placement, PlacementAxis, SizeAxis} from '@react-types/overlays';
14
+ import {clamp, isWebKit} from '@react-aria/utils';
15
+
16
+ interface Position {
17
+ top?: number,
18
+ left?: number,
19
+ bottom?: number,
20
+ right?: number
21
+ }
22
+
23
+ interface Dimensions {
24
+ width: number,
25
+ height: number,
26
+ totalWidth: number,
27
+ totalHeight: number,
28
+ top: number,
29
+ left: number,
30
+ scroll: Position
31
+ }
32
+
33
+ interface ParsedPlacement {
34
+ placement: PlacementAxis,
35
+ crossPlacement: PlacementAxis,
36
+ axis: Axis,
37
+ crossAxis: Axis,
38
+ size: SizeAxis,
39
+ crossSize: SizeAxis
40
+ }
41
+
42
+ interface Offset {
43
+ top: number,
44
+ left: number,
45
+ width: number,
46
+ height: number
47
+ }
48
+
49
+ interface PositionOpts {
50
+ arrowSize: number,
51
+ placement: Placement,
52
+ targetNode: Element,
53
+ overlayNode: Element,
54
+ scrollNode: Element,
55
+ padding: number,
56
+ shouldFlip: boolean,
57
+ boundaryElement: Element,
58
+ offset: number,
59
+ crossOffset: number,
60
+ maxHeight?: number,
61
+ arrowBoundaryOffset?: number
62
+ }
63
+
64
+ type HeightGrowthDirection = 'top' | 'bottom';
65
+
66
+ export interface PositionResult {
67
+ position: Position,
68
+ arrowOffsetLeft?: number,
69
+ arrowOffsetTop?: number,
70
+ triggerAnchorPoint: {x: number, y: number},
71
+ maxHeight: number,
72
+ placement: PlacementAxis
73
+ }
74
+
75
+ const AXIS = {
76
+ top: 'top',
77
+ bottom: 'top',
78
+ left: 'left',
79
+ right: 'left'
80
+ };
81
+
82
+ const FLIPPED_DIRECTION = {
83
+ top: 'bottom',
84
+ bottom: 'top',
85
+ left: 'right',
86
+ right: 'left'
87
+ };
88
+
89
+ const CROSS_AXIS = {
90
+ top: 'left',
91
+ left: 'top'
92
+ };
93
+
94
+ const AXIS_SIZE = {
95
+ top: 'height',
96
+ left: 'width'
97
+ };
98
+
99
+ const TOTAL_SIZE = {
100
+ width: 'totalWidth',
101
+ height: 'totalHeight'
102
+ };
103
+
104
+ const PARSED_PLACEMENT_CACHE = {};
105
+
106
+ let visualViewport = typeof document !== 'undefined' ? window.visualViewport : null;
107
+
108
+ function getContainerDimensions(containerNode: Element): Dimensions {
109
+ let width = 0, height = 0, totalWidth = 0, totalHeight = 0, top = 0, left = 0;
110
+ let scroll: Position = {};
111
+ let isPinchZoomedIn = (visualViewport?.scale ?? 1) > 1;
112
+
113
+ if (containerNode.tagName === 'BODY') {
114
+ let documentElement = document.documentElement;
115
+ totalWidth = documentElement.clientWidth;
116
+ totalHeight = documentElement.clientHeight;
117
+ width = visualViewport?.width ?? totalWidth;
118
+ height = visualViewport?.height ?? totalHeight;
119
+ scroll.top = documentElement.scrollTop || containerNode.scrollTop;
120
+ scroll.left = documentElement.scrollLeft || containerNode.scrollLeft;
121
+
122
+ // The goal of the below is to get a top/left value that represents the top/left of the visual viewport with
123
+ // respect to the layout viewport origin. This combined with the scrollTop/scrollLeft will allow us to calculate
124
+ // coordinates/values with respect to the visual viewport or with respect to the layout viewport.
125
+ if (visualViewport) {
126
+ top = visualViewport.offsetTop;
127
+ left = visualViewport.offsetLeft;
128
+ }
129
+ } else {
130
+ ({width, height, top, left} = getOffset(containerNode, false));
131
+ scroll.top = containerNode.scrollTop;
132
+ scroll.left = containerNode.scrollLeft;
133
+ totalWidth = width;
134
+ totalHeight = height;
135
+ }
136
+
137
+ if (isWebKit() && (containerNode.tagName === 'BODY' || containerNode.tagName === 'HTML') && isPinchZoomedIn) {
138
+ // Safari will report a non-zero scrollTop/Left for the non-scrolling body/HTML element when pinch zoomed in unlike other browsers.
139
+ // Set to zero for parity calculations so we get consistent positioning of overlays across all browsers.
140
+ // Also switch to visualViewport.pageTop/pageLeft so that we still accomodate for scroll positioning for body/HTML elements that are actually scrollable
141
+ // before pinch zoom happens
142
+ scroll.top = 0;
143
+ scroll.left = 0;
144
+ top = visualViewport?.pageTop ?? 0;
145
+ left = visualViewport?.pageLeft ?? 0;
146
+ }
147
+
148
+ return {width, height, totalWidth, totalHeight, scroll, top, left};
149
+ }
150
+
151
+ function getScroll(node: Element): Offset {
152
+ return {
153
+ top: node.scrollTop,
154
+ left: node.scrollLeft,
155
+ width: node.scrollWidth,
156
+ height: node.scrollHeight
157
+ };
158
+ }
159
+
160
+ // Determines the amount of space required when moving the overlay to ensure it remains in the boundary
161
+ function getDelta(
162
+ axis: Axis,
163
+ offset: number,
164
+ size: number,
165
+ // The dimensions of the boundary element that the popover is
166
+ // positioned within (most of the time this is the <body>).
167
+ boundaryDimensions: Dimensions,
168
+ // The dimensions of the containing block element that the popover is
169
+ // positioned relative to (e.g. parent with position: relative).
170
+ // Usually this is the same as the boundary element, but if the popover
171
+ // is portaled somewhere other than the body and has an ancestor with
172
+ // position: relative/absolute, it will be different.
173
+ containerDimensions: Dimensions,
174
+ padding: number,
175
+ containerOffsetWithBoundary: Offset
176
+ ) {
177
+ let containerScroll = containerDimensions.scroll[axis] ?? 0;
178
+ // The height/width of the boundary. Matches the axis along which we are adjusting the overlay position
179
+ let boundarySize = boundaryDimensions[AXIS_SIZE[axis]];
180
+ // Calculate the edges of the boundary (accomodating for the boundary padding) and the edges of the overlay.
181
+ // Note that these values are with respect to the visual viewport (aka 0,0 is the top left of the viewport)
182
+ let boundaryStartEdge = boundaryDimensions.scroll[AXIS[axis]] + padding;
183
+ let boundaryEndEdge = boundarySize + boundaryDimensions.scroll[AXIS[axis]] - padding;
184
+ let startEdgeOffset = offset - containerScroll + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]];
185
+ let endEdgeOffset = offset - containerScroll + size + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]];
186
+
187
+ // If any of the overlay edges falls outside of the boundary, shift the overlay the required amount to align one of the overlay's
188
+ // edges with the closest boundary edge.
189
+ if (startEdgeOffset < boundaryStartEdge) {
190
+ return boundaryStartEdge - startEdgeOffset;
191
+ } else if (endEdgeOffset > boundaryEndEdge) {
192
+ return Math.max(boundaryEndEdge - endEdgeOffset, boundaryStartEdge - startEdgeOffset);
193
+ } else {
194
+ return 0;
195
+ }
196
+ }
197
+
198
+ function getMargins(node: Element): Position {
199
+ let style = window.getComputedStyle(node);
200
+ return {
201
+ top: parseInt(style.marginTop, 10) || 0,
202
+ bottom: parseInt(style.marginBottom, 10) || 0,
203
+ left: parseInt(style.marginLeft, 10) || 0,
204
+ right: parseInt(style.marginRight, 10) || 0
205
+ };
206
+ }
207
+
208
+ function parsePlacement(input: Placement): ParsedPlacement {
209
+ if (PARSED_PLACEMENT_CACHE[input]) {
210
+ return PARSED_PLACEMENT_CACHE[input];
211
+ }
212
+
213
+ let [placement, crossPlacement] = input.split(' ');
214
+ let axis: Axis = AXIS[placement] || 'right';
215
+ let crossAxis: Axis = CROSS_AXIS[axis];
216
+
217
+ if (!AXIS[crossPlacement]) {
218
+ crossPlacement = 'center';
219
+ }
220
+
221
+ let size = AXIS_SIZE[axis];
222
+ let crossSize = AXIS_SIZE[crossAxis];
223
+ PARSED_PLACEMENT_CACHE[input] = {placement, crossPlacement, axis, crossAxis, size, crossSize};
224
+ return PARSED_PLACEMENT_CACHE[input];
225
+ }
226
+
227
+ function computePosition(
228
+ childOffset: Offset,
229
+ boundaryDimensions: Dimensions,
230
+ overlaySize: Offset,
231
+ placementInfo: ParsedPlacement,
232
+ offset: number,
233
+ crossOffset: number,
234
+ containerOffsetWithBoundary: Offset,
235
+ isContainerPositioned: boolean,
236
+ arrowSize: number,
237
+ arrowBoundaryOffset: number
238
+ ) {
239
+ let {placement, crossPlacement, axis, crossAxis, size, crossSize} = placementInfo;
240
+ let position: Position = {};
241
+
242
+ // button position
243
+ position[crossAxis] = childOffset[crossAxis] ?? 0;
244
+ if (crossPlacement === 'center') {
245
+ // + (button size / 2) - (overlay size / 2)
246
+ // at this point the overlay center should match the button center
247
+ position[crossAxis]! += ((childOffset[crossSize] ?? 0) - (overlaySize[crossSize] ?? 0)) / 2;
248
+ } else if (crossPlacement !== crossAxis) {
249
+ // + (button size) - (overlay size)
250
+ // at this point the overlay bottom should match the button bottom
251
+ position[crossAxis]! += (childOffset[crossSize] ?? 0) - (overlaySize[crossSize] ?? 0);
252
+ }/* else {
253
+ the overlay top should match the button top
254
+ } */
255
+
256
+ position[crossAxis]! += crossOffset;
257
+
258
+ // overlay top overlapping arrow with button bottom
259
+ const minPosition = childOffset[crossAxis] - overlaySize[crossSize] + arrowSize + arrowBoundaryOffset;
260
+ // overlay bottom overlapping arrow with button top
261
+ const maxPosition = childOffset[crossAxis] + childOffset[crossSize] - arrowSize - arrowBoundaryOffset;
262
+ position[crossAxis] = clamp(position[crossAxis]!, minPosition, maxPosition);
263
+
264
+ // Floor these so the position isn't placed on a partial pixel, only whole pixels. Shouldn't matter if it was floored or ceiled, so chose one.
265
+ if (placement === axis) {
266
+ // If the container is positioned (non-static), then we use the container's actual
267
+ // height, as `bottom` will be relative to this height. But if the container is static,
268
+ // then it can only be the `document.body`, and `bottom` will be relative to _its_
269
+ // container, which should be as large as boundaryDimensions.
270
+ const containerHeight = (isContainerPositioned ? containerOffsetWithBoundary[size] : boundaryDimensions[TOTAL_SIZE[size]]);
271
+ position[FLIPPED_DIRECTION[axis]] = Math.floor(containerHeight - childOffset[axis] + offset);
272
+ } else {
273
+ position[axis] = Math.floor(childOffset[axis] + childOffset[size] + offset);
274
+ }
275
+ return position;
276
+ }
277
+
278
+ function getMaxHeight(
279
+ position: Position,
280
+ boundaryDimensions: Dimensions,
281
+ containerOffsetWithBoundary: Offset,
282
+ isContainerPositioned: boolean,
283
+ margins: Position,
284
+ padding: number,
285
+ overlayHeight: number,
286
+ heightGrowthDirection: HeightGrowthDirection
287
+ ) {
288
+ const containerHeight = (isContainerPositioned ? containerOffsetWithBoundary.height : boundaryDimensions[TOTAL_SIZE.height]);
289
+ // For cases where position is set via "bottom" instead of "top", we need to calculate the true overlay top with respect to the boundary. Reverse calculate this with the same method
290
+ // used in computePosition.
291
+ let overlayTop = position.top != null ? containerOffsetWithBoundary.top + position.top : containerOffsetWithBoundary.top + (containerHeight - (position.bottom ?? 0) - overlayHeight);
292
+ let maxHeight = heightGrowthDirection !== 'top' ?
293
+ // We want the distance between the top of the overlay to the bottom of the boundary
294
+ Math.max(0,
295
+ (boundaryDimensions.height + boundaryDimensions.top + (boundaryDimensions.scroll.top ?? 0)) // this is the bottom of the boundary
296
+ - overlayTop // this is the top of the overlay
297
+ - ((margins.top ?? 0) + (margins.bottom ?? 0) + padding) // save additional space for margin and padding
298
+ )
299
+ // We want the distance between the bottom of the overlay to the top of the boundary
300
+ : Math.max(0,
301
+ (overlayTop + overlayHeight) // this is the bottom of the overlay
302
+ - (boundaryDimensions.top + (boundaryDimensions.scroll.top ?? 0)) // this is the top of the boundary
303
+ - ((margins.top ?? 0) + (margins.bottom ?? 0) + padding) // save additional space for margin and padding
304
+ );
305
+ return Math.min(boundaryDimensions.height - (padding * 2), maxHeight);
306
+ }
307
+
308
+ function getAvailableSpace(
309
+ boundaryDimensions: Dimensions,
310
+ containerOffsetWithBoundary: Offset,
311
+ childOffset: Offset,
312
+ margins: Position,
313
+ padding: number,
314
+ placementInfo: ParsedPlacement
315
+ ) {
316
+ let {placement, axis, size} = placementInfo;
317
+ if (placement === axis) {
318
+ return Math.max(0, childOffset[axis] - boundaryDimensions[axis] - (boundaryDimensions.scroll[axis] ?? 0) + containerOffsetWithBoundary[axis] - (margins[axis] ?? 0) - margins[FLIPPED_DIRECTION[axis]] - padding);
319
+ }
320
+
321
+ return Math.max(0, boundaryDimensions[size] + boundaryDimensions[axis] + boundaryDimensions.scroll[axis] - containerOffsetWithBoundary[axis] - childOffset[axis] - childOffset[size] - (margins[axis] ?? 0) - margins[FLIPPED_DIRECTION[axis]] - padding);
322
+ }
323
+
324
+ export function calculatePositionInternal(
325
+ placementInput: Placement,
326
+ childOffset: Offset,
327
+ overlaySize: Offset,
328
+ scrollSize: Offset,
329
+ margins: Position,
330
+ padding: number,
331
+ flip: boolean,
332
+ boundaryDimensions: Dimensions,
333
+ containerDimensions: Dimensions,
334
+ containerOffsetWithBoundary: Offset,
335
+ offset: number,
336
+ crossOffset: number,
337
+ isContainerPositioned: boolean,
338
+ userSetMaxHeight: number | undefined,
339
+ arrowSize: number,
340
+ arrowBoundaryOffset: number
341
+ ): PositionResult {
342
+ let placementInfo = parsePlacement(placementInput);
343
+ let {size, crossAxis, crossSize, placement, crossPlacement} = placementInfo;
344
+ let position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, offset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);
345
+ let normalizedOffset = offset;
346
+ let space = getAvailableSpace(
347
+ boundaryDimensions,
348
+ containerOffsetWithBoundary,
349
+ childOffset,
350
+ margins,
351
+ padding + offset,
352
+ placementInfo
353
+ );
354
+
355
+ // Check if the scroll size of the overlay is greater than the available space to determine if we need to flip
356
+ if (flip && scrollSize[size] > space) {
357
+ let flippedPlacementInfo = parsePlacement(`${FLIPPED_DIRECTION[placement]} ${crossPlacement}` as Placement);
358
+ let flippedPosition = computePosition(childOffset, boundaryDimensions, overlaySize, flippedPlacementInfo, offset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);
359
+ let flippedSpace = getAvailableSpace(
360
+ boundaryDimensions,
361
+ containerOffsetWithBoundary,
362
+ childOffset,
363
+ margins,
364
+ padding + offset,
365
+ flippedPlacementInfo
366
+ );
367
+
368
+ // If the available space for the flipped position is greater than the original available space, flip.
369
+ if (flippedSpace > space) {
370
+ placementInfo = flippedPlacementInfo;
371
+ position = flippedPosition;
372
+ normalizedOffset = offset;
373
+ }
374
+ }
375
+
376
+ // Determine the direction the height of the overlay can grow so that we can choose how to calculate the max height
377
+ let heightGrowthDirection: HeightGrowthDirection = 'bottom';
378
+ if (placementInfo.axis === 'top') {
379
+ if (placementInfo.placement === 'top') {
380
+ heightGrowthDirection = 'top';
381
+ } else if (placementInfo.placement === 'bottom') {
382
+ heightGrowthDirection = 'bottom';
383
+ }
384
+ } else if (placementInfo.crossAxis === 'top') {
385
+ if (placementInfo.crossPlacement === 'top') {
386
+ heightGrowthDirection = 'bottom';
387
+ } else if (placementInfo.crossPlacement === 'bottom') {
388
+ heightGrowthDirection = 'top';
389
+ }
390
+ }
391
+
392
+ let delta = getDelta(crossAxis, position[crossAxis]!, overlaySize[crossSize], boundaryDimensions, containerDimensions, padding, containerOffsetWithBoundary);
393
+ position[crossAxis]! += delta;
394
+
395
+ let maxHeight = getMaxHeight(
396
+ position,
397
+ boundaryDimensions,
398
+ containerOffsetWithBoundary,
399
+ isContainerPositioned,
400
+ margins,
401
+ padding,
402
+ overlaySize.height,
403
+ heightGrowthDirection
404
+ );
405
+
406
+ if (userSetMaxHeight && userSetMaxHeight < maxHeight) {
407
+ maxHeight = userSetMaxHeight;
408
+ }
409
+
410
+ overlaySize.height = Math.min(overlaySize.height, maxHeight);
411
+
412
+ position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, normalizedOffset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);
413
+ delta = getDelta(crossAxis, position[crossAxis]!, overlaySize[crossSize], boundaryDimensions, containerDimensions, padding, containerOffsetWithBoundary);
414
+ position[crossAxis]! += delta;
415
+
416
+ let arrowPosition: Position = {};
417
+
418
+ // All values are transformed so that 0 is at the top/left of the overlay depending on the orientation
419
+ // Prefer the arrow being in the center of the trigger/overlay anchor element
420
+ // childOffset[crossAxis] + .5 * childOffset[crossSize] = absolute position with respect to the trigger's coordinate system that would place the arrow in the center of the trigger
421
+ // position[crossAxis] - margins[AXIS[crossAxis]] = value use to transform the position to a value with respect to the overlay's coordinate system. A child element's (aka arrow) position absolute's "0"
422
+ // is positioned after the margin of its parent (aka overlay) so we need to subtract it to get the proper coordinate transform
423
+ let origin = childOffset[crossAxis] - position[crossAxis]! - margins[AXIS[crossAxis]];
424
+ let preferredArrowPosition = origin + .5 * childOffset[crossSize];
425
+
426
+ // Min/Max position limits for the arrow with respect to the overlay
427
+ const arrowMinPosition = arrowSize / 2 + arrowBoundaryOffset;
428
+ // overlaySize[crossSize] - margins = true size of the overlay
429
+ const overlayMargin = AXIS[crossAxis] === 'left' ? (margins.left ?? 0) + (margins.right ?? 0) : (margins.top ?? 0) + (margins.bottom ?? 0);
430
+ const arrowMaxPosition = overlaySize[crossSize] - overlayMargin - (arrowSize / 2) - arrowBoundaryOffset;
431
+
432
+ // Min/Max position limits for the arrow with respect to the trigger/overlay anchor element
433
+ // Same margin accomodation done here as well as for the preferredArrowPosition
434
+ const arrowOverlappingChildMinEdge = childOffset[crossAxis] + (arrowSize / 2) - (position[crossAxis] + margins[AXIS[crossAxis]]);
435
+ const arrowOverlappingChildMaxEdge = childOffset[crossAxis] + childOffset[crossSize] - (arrowSize / 2) - (position[crossAxis] + margins[AXIS[crossAxis]]);
436
+
437
+ // Clamp the arrow positioning so that it always is within the bounds of the anchor and the overlay
438
+ const arrowPositionOverlappingChild = clamp(preferredArrowPosition, arrowOverlappingChildMinEdge, arrowOverlappingChildMaxEdge);
439
+ arrowPosition[crossAxis] = clamp(arrowPositionOverlappingChild, arrowMinPosition, arrowMaxPosition);
440
+
441
+ // If there is an arrow, use that as the origin so that animations are smooth.
442
+ // Otherwise use the target edge.
443
+ ({placement, crossPlacement} = placementInfo);
444
+ if (arrowSize) {
445
+ origin = arrowPosition[crossAxis];
446
+ } else if (crossPlacement === 'right') {
447
+ origin += childOffset[crossSize];
448
+ } else if (crossPlacement === 'center') {
449
+ origin += childOffset[crossSize] / 2;
450
+ }
451
+
452
+ let crossOrigin = placement === 'left' || placement === 'top' ? overlaySize[size] : 0;
453
+ let triggerAnchorPoint = {
454
+ x: placement === 'top' || placement === 'bottom' ? origin : crossOrigin,
455
+ y: placement === 'left' || placement === 'right' ? origin : crossOrigin
456
+ };
457
+
458
+ return {
459
+ position,
460
+ maxHeight: maxHeight,
461
+ arrowOffsetLeft: arrowPosition.left,
462
+ arrowOffsetTop: arrowPosition.top,
463
+ placement,
464
+ triggerAnchorPoint
465
+ };
466
+ }
467
+
468
+ /**
469
+ * Determines where to place the overlay with regards to the target and the position of an optional indicator.
470
+ */
471
+ export function calculatePosition(opts: PositionOpts): PositionResult {
472
+ let {
473
+ placement,
474
+ targetNode,
475
+ overlayNode,
476
+ scrollNode,
477
+ padding,
478
+ shouldFlip,
479
+ boundaryElement,
480
+ offset,
481
+ crossOffset,
482
+ maxHeight,
483
+ arrowSize = 0,
484
+ arrowBoundaryOffset = 0
485
+ } = opts;
486
+
487
+ let container = overlayNode instanceof HTMLElement ? getContainingBlock(overlayNode) : document.documentElement;
488
+ let isViewportContainer = container === document.documentElement;
489
+ const containerPositionStyle = window.getComputedStyle(container).position;
490
+ let isContainerPositioned = !!containerPositionStyle && containerPositionStyle !== 'static';
491
+ let childOffset: Offset = isViewportContainer ? getOffset(targetNode, false) : getPosition(targetNode, container, false);
492
+
493
+ if (!isViewportContainer) {
494
+ let {marginTop, marginLeft} = window.getComputedStyle(targetNode);
495
+ childOffset.top += parseInt(marginTop, 10) || 0;
496
+ childOffset.left += parseInt(marginLeft, 10) || 0;
497
+ }
498
+
499
+ let overlaySize: Offset = getOffset(overlayNode, true);
500
+ let margins = getMargins(overlayNode);
501
+ overlaySize.width += (margins.left ?? 0) + (margins.right ?? 0);
502
+ overlaySize.height += (margins.top ?? 0) + (margins.bottom ?? 0);
503
+
504
+ let scrollSize = getScroll(scrollNode);
505
+ let boundaryDimensions = getContainerDimensions(boundaryElement);
506
+ let containerDimensions = getContainerDimensions(container);
507
+ // If the container is the HTML element wrapping the body element, the retrieved scrollTop/scrollLeft will be equal to the
508
+ // body element's scroll. Set the container's scroll values to 0 since the overlay's edge position value in getDelta don't then need to be further offset
509
+ // by the container scroll since they are essentially the same containing element and thus in the same coordinate system
510
+ let containerOffsetWithBoundary: Offset = boundaryElement.tagName === 'BODY' ? getOffset(container, false) : getPosition(container, boundaryElement, false);
511
+ if (container.tagName === 'HTML' && boundaryElement.tagName === 'BODY') {
512
+ containerDimensions.scroll.top = 0;
513
+ containerDimensions.scroll.left = 0;
514
+ }
515
+
516
+ return calculatePositionInternal(
517
+ placement,
518
+ childOffset,
519
+ overlaySize,
520
+ scrollSize,
521
+ margins,
522
+ padding,
523
+ shouldFlip,
524
+ boundaryDimensions,
525
+ containerDimensions,
526
+ containerOffsetWithBoundary,
527
+ offset,
528
+ crossOffset,
529
+ isContainerPositioned,
530
+ maxHeight,
531
+ arrowSize,
532
+ arrowBoundaryOffset
533
+ );
534
+ }
535
+
536
+ export function getRect(node: Element, ignoreScale: boolean) {
537
+ let {top, left, width, height} = node.getBoundingClientRect();
538
+
539
+ // Use offsetWidth and offsetHeight if this is an HTML element, so that
540
+ // the size is not affected by scale transforms.
541
+ if (ignoreScale && node instanceof node.ownerDocument.defaultView!.HTMLElement) {
542
+ width = node.offsetWidth;
543
+ height = node.offsetHeight;
544
+ }
545
+
546
+ return {top, left, width, height};
547
+ }
548
+
549
+ function getOffset(node: Element, ignoreScale: boolean): Offset {
550
+ let {top, left, width, height} = getRect(node, ignoreScale);
551
+ let {scrollTop, scrollLeft, clientTop, clientLeft} = document.documentElement;
552
+ return {
553
+ top: top + scrollTop - clientTop,
554
+ left: left + scrollLeft - clientLeft,
555
+ width,
556
+ height
557
+ };
558
+ }
559
+
560
+ function getPosition(node: Element, parent: Element, ignoreScale: boolean): Offset {
561
+ let style = window.getComputedStyle(node);
562
+ let offset: Offset;
563
+ if (style.position === 'fixed') {
564
+ offset = getRect(node, ignoreScale);
565
+ } else {
566
+ offset = getOffset(node, ignoreScale);
567
+ let parentOffset = getOffset(parent, ignoreScale);
568
+ let parentStyle = window.getComputedStyle(parent);
569
+ parentOffset.top += (parseInt(parentStyle.borderTopWidth, 10) || 0) - parent.scrollTop;
570
+ parentOffset.left += (parseInt(parentStyle.borderLeftWidth, 10) || 0) - parent.scrollLeft;
571
+ offset.top -= parentOffset.top;
572
+ offset.left -= parentOffset.left;
573
+ }
574
+
575
+ offset.top -= parseInt(style.marginTop, 10) || 0;
576
+ offset.left -= parseInt(style.marginLeft, 10) || 0;
577
+ return offset;
578
+ }
579
+
580
+ // Returns the containing block of an element, which is the element that
581
+ // this element will be positioned relative to.
582
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block
583
+ function getContainingBlock(node: HTMLElement): Element {
584
+ // The offsetParent of an element in most cases equals the containing block.
585
+ // https://w3c.github.io/csswg-drafts/cssom-view/#dom-htmlelement-offsetparent
586
+ let offsetParent = node.offsetParent;
587
+
588
+ // The offsetParent algorithm terminates at the document body,
589
+ // even if the body is not a containing block. Double check that
590
+ // and use the documentElement if so.
591
+ if (
592
+ offsetParent &&
593
+ offsetParent === document.body &&
594
+ window.getComputedStyle(offsetParent).position === 'static' &&
595
+ !isContainingBlock(offsetParent)
596
+ ) {
597
+ offsetParent = document.documentElement;
598
+ }
599
+
600
+ // TODO(later): handle table elements?
601
+
602
+ // The offsetParent can be null if the element has position: fixed, or a few other cases.
603
+ // We have to walk up the tree manually in this case because fixed positioned elements
604
+ // are still positioned relative to their containing block, which is not always the viewport.
605
+ if (offsetParent == null) {
606
+ offsetParent = node.parentElement;
607
+ while (offsetParent && !isContainingBlock(offsetParent)) {
608
+ offsetParent = offsetParent.parentElement;
609
+ }
610
+ }
611
+
612
+ // Fall back to the viewport.
613
+ return offsetParent || document.documentElement;
614
+ }
615
+
616
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
617
+ function isContainingBlock(node: Element): boolean {
618
+ let style = window.getComputedStyle(node);
619
+ return (
620
+ style.transform !== 'none' ||
621
+ /transform|perspective/.test(style.willChange) ||
622
+ style.filter !== 'none' ||
623
+ style.contain === 'paint' ||
624
+ ('backdropFilter' in style && style.backdropFilter !== 'none') ||
625
+ ('WebkitBackdropFilter' in style && style.WebkitBackdropFilter !== 'none')
626
+ );
627
+ }
@@ -0,0 +1,64 @@
1
+ /*
2
+ * Copyright 2020 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import {RefObject} from '@react-types/shared';
14
+ import {useEffect} from 'react';
15
+
16
+ // This behavior moved from useOverlayTrigger to useOverlayPosition.
17
+ // For backward compatibility, where useOverlayTrigger handled hiding the popover on close,
18
+ // it sets a close function here mapped from the trigger element. This way we can avoid
19
+ // forcing users to pass an onClose function to useOverlayPosition which could be considered
20
+ // a breaking change.
21
+ export const onCloseMap: WeakMap<Element, () => void> = new WeakMap();
22
+
23
+ interface CloseOnScrollOptions {
24
+ triggerRef: RefObject<Element | null>,
25
+ isOpen?: boolean,
26
+ onClose?: (() => void) | null
27
+ }
28
+
29
+ /** @private */
30
+ export function useCloseOnScroll(opts: CloseOnScrollOptions): void {
31
+ let {triggerRef, isOpen, onClose} = opts;
32
+
33
+ useEffect(() => {
34
+ if (!isOpen || onClose === null) {
35
+ return;
36
+ }
37
+
38
+ let onScroll = (e: Event) => {
39
+ // Ignore if scrolling an scrollable region outside the trigger's tree.
40
+ let target = e.target;
41
+ // window is not a Node and doesn't have contain, but window contains everything
42
+ if (!triggerRef.current || ((target instanceof Node) && !target.contains(triggerRef.current))) {
43
+ return;
44
+ }
45
+
46
+ // Ignore scroll events on any input or textarea as the cursor position can cause it to scroll
47
+ // such as in a combobox. Clicking the dropdown button places focus on the input, and if the
48
+ // text inside the input extends beyond the 'end', then it will scroll so the cursor is visible at the end.
49
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
50
+ return;
51
+ }
52
+
53
+ let onCloseHandler = onClose || onCloseMap.get(triggerRef.current);
54
+ if (onCloseHandler) {
55
+ onCloseHandler();
56
+ }
57
+ };
58
+
59
+ window.addEventListener('scroll', onScroll, true);
60
+ return () => {
61
+ window.removeEventListener('scroll', onScroll, true);
62
+ };
63
+ }, [isOpen, onClose, triggerRef]);
64
+ }