@react-aria/overlays 3.12.1 → 3.14.0
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/dist/import.mjs +1457 -0
- package/dist/main.js +122 -54
- package/dist/main.js.map +1 -1
- package/dist/module.js +123 -55
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +14 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +17 -12
- package/src/ariaHideOutside.ts +55 -30
- package/src/calculatePosition.ts +111 -25
- package/src/index.ts +1 -0
- package/src/useOverlayPosition.ts +41 -17
- package/src/useOverlayTrigger.ts +3 -3
- package/src/usePopover.ts +2 -12
package/package.json
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-aria/overlays",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.14.0",
|
|
4
4
|
"description": "Spectrum UI components in React",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "dist/main.js",
|
|
7
7
|
"module": "dist/module.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
"types": "./dist/types.d.ts",
|
|
10
|
+
"import": "./dist/import.mjs",
|
|
11
|
+
"require": "./dist/main.js"
|
|
12
|
+
},
|
|
8
13
|
"types": "dist/types.d.ts",
|
|
9
14
|
"source": "src/index.ts",
|
|
10
15
|
"files": [
|
|
@@ -17,16 +22,16 @@
|
|
|
17
22
|
"url": "https://github.com/adobe/react-spectrum"
|
|
18
23
|
},
|
|
19
24
|
"dependencies": {
|
|
20
|
-
"@react-aria/focus": "^3.
|
|
21
|
-
"@react-aria/i18n": "^3.
|
|
22
|
-
"@react-aria/interactions": "^3.
|
|
23
|
-
"@react-aria/ssr": "^3.
|
|
24
|
-
"@react-aria/utils": "^3.
|
|
25
|
-
"@react-aria/visually-hidden": "^3.
|
|
26
|
-
"@react-stately/overlays": "^3.
|
|
27
|
-
"@react-types/button": "^3.7.
|
|
28
|
-
"@react-types/overlays": "^3.
|
|
29
|
-
"@react-types/shared": "^3.
|
|
25
|
+
"@react-aria/focus": "^3.12.0",
|
|
26
|
+
"@react-aria/i18n": "^3.7.1",
|
|
27
|
+
"@react-aria/interactions": "^3.15.0",
|
|
28
|
+
"@react-aria/ssr": "^3.6.0",
|
|
29
|
+
"@react-aria/utils": "^3.16.0",
|
|
30
|
+
"@react-aria/visually-hidden": "^3.8.0",
|
|
31
|
+
"@react-stately/overlays": "^3.5.1",
|
|
32
|
+
"@react-types/button": "^3.7.2",
|
|
33
|
+
"@react-types/overlays": "^3.7.1",
|
|
34
|
+
"@react-types/shared": "^3.18.0",
|
|
30
35
|
"@swc/helpers": "^0.4.14"
|
|
31
36
|
},
|
|
32
37
|
"peerDependencies": {
|
|
@@ -36,5 +41,5 @@
|
|
|
36
41
|
"publishConfig": {
|
|
37
42
|
"access": "public"
|
|
38
43
|
},
|
|
39
|
-
"gitHead": "
|
|
44
|
+
"gitHead": "9d1ba9bd8ebcd63bf3495ade16d349bcb71795ce"
|
|
40
45
|
}
|
package/src/ariaHideOutside.ts
CHANGED
|
@@ -26,36 +26,55 @@ let observerStack = [];
|
|
|
26
26
|
export function ariaHideOutside(targets: Element[], root = document.body) {
|
|
27
27
|
let visibleNodes = new Set<Element>(targets);
|
|
28
28
|
let hiddenNodes = new Set<Element>();
|
|
29
|
-
let walker = document.createTreeWalker(
|
|
30
|
-
root,
|
|
31
|
-
NodeFilter.SHOW_ELEMENT,
|
|
32
|
-
{
|
|
33
|
-
acceptNode(node) {
|
|
34
|
-
// If this node is a live announcer, add it to the set of nodes to keep visible.
|
|
35
|
-
if (((node instanceof HTMLElement || node instanceof SVGElement) && node.dataset.liveAnnouncer === 'true')) {
|
|
36
|
-
visibleNodes.add(node);
|
|
37
|
-
}
|
|
38
29
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
30
|
+
let walk = (root: Element) => {
|
|
31
|
+
// Keep live announcer and top layer elements (e.g. toasts) visible.
|
|
32
|
+
for (let element of root.querySelectorAll('[data-live-announcer], [data-react-aria-top-layer]')) {
|
|
33
|
+
visibleNodes.add(element);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let acceptNode = (node: Element) => {
|
|
37
|
+
// Skip this node and its children if it is one of the target nodes, or a live announcer.
|
|
38
|
+
// Also skip children of already hidden nodes, as aria-hidden is recursive. An exception is
|
|
39
|
+
// made for elements with role="row" since VoiceOver on iOS has issues hiding elements with role="row".
|
|
40
|
+
// For that case we want to hide the cells inside as well (https://bugs.webkit.org/show_bug.cgi?id=222623).
|
|
41
|
+
if (
|
|
42
|
+
visibleNodes.has(node) ||
|
|
43
|
+
(hiddenNodes.has(node.parentElement) && node.parentElement.getAttribute('role') !== 'row')
|
|
44
|
+
) {
|
|
45
|
+
return NodeFilter.FILTER_REJECT;
|
|
46
|
+
}
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
// Skip this node but continue to children if one of the targets is inside the node.
|
|
49
|
+
for (let target of visibleNodes) {
|
|
50
|
+
if (node.contains(target)) {
|
|
52
51
|
return NodeFilter.FILTER_SKIP;
|
|
53
52
|
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
let walker = document.createTreeWalker(
|
|
59
|
+
root,
|
|
60
|
+
NodeFilter.SHOW_ELEMENT,
|
|
61
|
+
{acceptNode}
|
|
62
|
+
);
|
|
54
63
|
|
|
55
|
-
|
|
64
|
+
// TreeWalker does not include the root.
|
|
65
|
+
let acceptRoot = acceptNode(root);
|
|
66
|
+
if (acceptRoot === NodeFilter.FILTER_ACCEPT) {
|
|
67
|
+
hide(root);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (acceptRoot !== NodeFilter.FILTER_REJECT) {
|
|
71
|
+
let node = walker.nextNode() as Element;
|
|
72
|
+
while (node != null) {
|
|
73
|
+
hide(node);
|
|
74
|
+
node = walker.nextNode() as Element;
|
|
56
75
|
}
|
|
57
76
|
}
|
|
58
|
-
|
|
77
|
+
};
|
|
59
78
|
|
|
60
79
|
let hide = (node: Element) => {
|
|
61
80
|
let refCount = refCountMap.get(node) ?? 0;
|
|
@@ -80,11 +99,7 @@ export function ariaHideOutside(targets: Element[], root = document.body) {
|
|
|
80
99
|
observerStack[observerStack.length - 1].disconnect();
|
|
81
100
|
}
|
|
82
101
|
|
|
83
|
-
|
|
84
|
-
while (node != null) {
|
|
85
|
-
hide(node);
|
|
86
|
-
node = walker.nextNode() as Element;
|
|
87
|
-
}
|
|
102
|
+
walk(root);
|
|
88
103
|
|
|
89
104
|
let observer = new MutationObserver(changes => {
|
|
90
105
|
for (let change of changes) {
|
|
@@ -95,11 +110,21 @@ export function ariaHideOutside(targets: Element[], root = document.body) {
|
|
|
95
110
|
// If the parent element of the added nodes is not within one of the targets,
|
|
96
111
|
// and not already inside a hidden node, hide all of the new children.
|
|
97
112
|
if (![...visibleNodes, ...hiddenNodes].some(node => node.contains(change.target))) {
|
|
113
|
+
for (let node of change.removedNodes) {
|
|
114
|
+
if (node instanceof Element) {
|
|
115
|
+
visibleNodes.delete(node);
|
|
116
|
+
hiddenNodes.delete(node);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
98
120
|
for (let node of change.addedNodes) {
|
|
99
|
-
if (
|
|
121
|
+
if (
|
|
122
|
+
(node instanceof HTMLElement || node instanceof SVGElement) &&
|
|
123
|
+
(node.dataset.liveAnnouncer === 'true' || node.dataset.reactAriaTopLayer === 'true')
|
|
124
|
+
) {
|
|
100
125
|
visibleNodes.add(node);
|
|
101
126
|
} else if (node instanceof Element) {
|
|
102
|
-
|
|
127
|
+
walk(node);
|
|
103
128
|
}
|
|
104
129
|
}
|
|
105
130
|
}
|
package/src/calculatePosition.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {Axis, Placement, PlacementAxis, SizeAxis} from '@react-types/overlays';
|
|
14
|
+
import {clamp} from '@react-aria/utils';
|
|
14
15
|
|
|
15
16
|
interface Position {
|
|
16
17
|
top?: number,
|
|
@@ -22,6 +23,8 @@ interface Position {
|
|
|
22
23
|
interface Dimensions {
|
|
23
24
|
width: number,
|
|
24
25
|
height: number,
|
|
26
|
+
totalWidth: number,
|
|
27
|
+
totalHeight: number,
|
|
25
28
|
top: number,
|
|
26
29
|
left: number,
|
|
27
30
|
scroll: Position
|
|
@@ -44,6 +47,7 @@ interface Offset {
|
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
interface PositionOpts {
|
|
50
|
+
arrowSize: number,
|
|
47
51
|
placement: Placement,
|
|
48
52
|
targetNode: Element,
|
|
49
53
|
overlayNode: Element,
|
|
@@ -53,7 +57,8 @@ interface PositionOpts {
|
|
|
53
57
|
boundaryElement: Element,
|
|
54
58
|
offset: number,
|
|
55
59
|
crossOffset: number,
|
|
56
|
-
maxHeight?: number
|
|
60
|
+
maxHeight?: number,
|
|
61
|
+
arrowBoundaryOffset?: number
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
export interface PositionResult {
|
|
@@ -88,19 +93,26 @@ const AXIS_SIZE = {
|
|
|
88
93
|
left: 'width'
|
|
89
94
|
};
|
|
90
95
|
|
|
96
|
+
const TOTAL_SIZE = {
|
|
97
|
+
width: 'totalWidth',
|
|
98
|
+
height: 'totalHeight'
|
|
99
|
+
};
|
|
100
|
+
|
|
91
101
|
const PARSED_PLACEMENT_CACHE = {};
|
|
92
102
|
|
|
93
103
|
// @ts-ignore
|
|
94
104
|
let visualViewport = typeof window !== 'undefined' && window.visualViewport;
|
|
95
105
|
|
|
96
106
|
function getContainerDimensions(containerNode: Element): Dimensions {
|
|
97
|
-
let width = 0, height = 0, top = 0, left = 0;
|
|
107
|
+
let width = 0, height = 0, totalWidth = 0, totalHeight = 0, top = 0, left = 0;
|
|
98
108
|
let scroll: Position = {};
|
|
99
109
|
|
|
100
110
|
if (containerNode.tagName === 'BODY') {
|
|
101
111
|
let documentElement = document.documentElement;
|
|
102
|
-
|
|
103
|
-
|
|
112
|
+
totalWidth = documentElement.clientWidth;
|
|
113
|
+
totalHeight = documentElement.clientHeight;
|
|
114
|
+
width = visualViewport?.width ?? totalWidth;
|
|
115
|
+
height = visualViewport?.height ?? totalHeight;
|
|
104
116
|
|
|
105
117
|
scroll.top = documentElement.scrollTop || containerNode.scrollTop;
|
|
106
118
|
scroll.left = documentElement.scrollLeft || containerNode.scrollLeft;
|
|
@@ -108,9 +120,11 @@ function getContainerDimensions(containerNode: Element): Dimensions {
|
|
|
108
120
|
({width, height, top, left} = getOffset(containerNode));
|
|
109
121
|
scroll.top = containerNode.scrollTop;
|
|
110
122
|
scroll.left = containerNode.scrollLeft;
|
|
123
|
+
totalWidth = width;
|
|
124
|
+
totalHeight = height;
|
|
111
125
|
}
|
|
112
126
|
|
|
113
|
-
return {width, height, scroll, top, left};
|
|
127
|
+
return {width, height, totalWidth, totalHeight, scroll, top, left};
|
|
114
128
|
}
|
|
115
129
|
|
|
116
130
|
function getScroll(node: Element): Offset {
|
|
@@ -181,7 +195,9 @@ function computePosition(
|
|
|
181
195
|
offset: number,
|
|
182
196
|
crossOffset: number,
|
|
183
197
|
containerOffsetWithBoundary: Offset,
|
|
184
|
-
isContainerPositioned: boolean
|
|
198
|
+
isContainerPositioned: boolean,
|
|
199
|
+
arrowSize: number,
|
|
200
|
+
arrowBoundaryOffset: number
|
|
185
201
|
) {
|
|
186
202
|
let {placement, crossPlacement, axis, crossAxis, size, crossSize} = placementInfo;
|
|
187
203
|
let position: Position = {};
|
|
@@ -202,13 +218,11 @@ function computePosition(
|
|
|
202
218
|
// add the crossOffset from props
|
|
203
219
|
position[crossAxis] += crossOffset;
|
|
204
220
|
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
// clamp it into the range of the min/max positions
|
|
211
|
-
position[crossAxis] = Math.min(Math.max(minViablePosition, position[crossAxis]), maxViablePosition);
|
|
221
|
+
// overlay top overlapping arrow with button bottom
|
|
222
|
+
const minPosition = childOffset[crossAxis] - overlaySize[crossSize] + arrowSize + arrowBoundaryOffset;
|
|
223
|
+
// overlay bottom overlapping arrow with button top
|
|
224
|
+
const maxPosition = childOffset[crossAxis] + childOffset[crossSize] - arrowSize - arrowBoundaryOffset;
|
|
225
|
+
position[crossAxis] = clamp(position[crossAxis], minPosition, maxPosition);
|
|
212
226
|
|
|
213
227
|
// 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.
|
|
214
228
|
if (placement === axis) {
|
|
@@ -216,7 +230,7 @@ function computePosition(
|
|
|
216
230
|
// height, as `bottom` will be relative to this height. But if the container is static,
|
|
217
231
|
// then it can only be the `document.body`, and `bottom` will be relative to _its_
|
|
218
232
|
// container, which should be as large as boundaryDimensions.
|
|
219
|
-
const containerHeight = (isContainerPositioned ? containerOffsetWithBoundary[size] : boundaryDimensions[size]);
|
|
233
|
+
const containerHeight = (isContainerPositioned ? containerOffsetWithBoundary[size] : boundaryDimensions[TOTAL_SIZE[size]]);
|
|
220
234
|
position[FLIPPED_DIRECTION[axis]] = Math.floor(containerHeight - childOffset[axis] + offset);
|
|
221
235
|
} else {
|
|
222
236
|
position[axis] = Math.floor(childOffset[axis] + childOffset[size] + offset);
|
|
@@ -277,11 +291,13 @@ export function calculatePositionInternal(
|
|
|
277
291
|
offset: number,
|
|
278
292
|
crossOffset: number,
|
|
279
293
|
isContainerPositioned: boolean,
|
|
280
|
-
userSetMaxHeight
|
|
294
|
+
userSetMaxHeight: number | undefined,
|
|
295
|
+
arrowSize: number,
|
|
296
|
+
arrowBoundaryOffset: number
|
|
281
297
|
): PositionResult {
|
|
282
298
|
let placementInfo = parsePlacement(placementInput);
|
|
283
299
|
let {size, crossAxis, crossSize, placement, crossPlacement} = placementInfo;
|
|
284
|
-
let position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, offset, crossOffset, containerOffsetWithBoundary, isContainerPositioned);
|
|
300
|
+
let position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, offset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);
|
|
285
301
|
let normalizedOffset = offset;
|
|
286
302
|
let space = getAvailableSpace(
|
|
287
303
|
boundaryDimensions,
|
|
@@ -295,7 +311,7 @@ export function calculatePositionInternal(
|
|
|
295
311
|
// Check if the scroll size of the overlay is greater than the available space to determine if we need to flip
|
|
296
312
|
if (flip && scrollSize[size] > space) {
|
|
297
313
|
let flippedPlacementInfo = parsePlacement(`${FLIPPED_DIRECTION[placement]} ${crossPlacement}` as Placement);
|
|
298
|
-
let flippedPosition = computePosition(childOffset, boundaryDimensions, overlaySize, flippedPlacementInfo, offset, crossOffset, containerOffsetWithBoundary, isContainerPositioned);
|
|
314
|
+
let flippedPosition = computePosition(childOffset, boundaryDimensions, overlaySize, flippedPlacementInfo, offset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);
|
|
299
315
|
let flippedSpace = getAvailableSpace(
|
|
300
316
|
boundaryDimensions,
|
|
301
317
|
containerOffsetWithBoundary,
|
|
@@ -331,12 +347,27 @@ export function calculatePositionInternal(
|
|
|
331
347
|
|
|
332
348
|
overlaySize.height = Math.min(overlaySize.height, maxHeight);
|
|
333
349
|
|
|
334
|
-
position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, normalizedOffset, crossOffset, containerOffsetWithBoundary, isContainerPositioned);
|
|
350
|
+
position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, normalizedOffset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);
|
|
335
351
|
delta = getDelta(crossAxis, position[crossAxis], overlaySize[crossSize], boundaryDimensions, padding);
|
|
336
352
|
position[crossAxis] += delta;
|
|
337
353
|
|
|
338
354
|
let arrowPosition: Position = {};
|
|
339
|
-
|
|
355
|
+
|
|
356
|
+
// All values are transformed so that 0 is at the top/left of the overlay depending on the orientation
|
|
357
|
+
// Prefer the arrow being in the center of the trigger/overlay anchor element
|
|
358
|
+
let preferredArrowPosition = childOffset[crossAxis] + .5 * childOffset[crossSize] - overlaySize[crossAxis];
|
|
359
|
+
|
|
360
|
+
// Min/Max position limits for the arrow with respect to the overlay
|
|
361
|
+
const arrowMinPosition = arrowSize / 2 + arrowBoundaryOffset;
|
|
362
|
+
const arrowMaxPosition = overlaySize[crossSize] - (arrowSize / 2) - arrowBoundaryOffset;
|
|
363
|
+
|
|
364
|
+
// Min/Max position limits for the arrow with respect to the trigger/overlay anchor element
|
|
365
|
+
const arrowOverlappingChildMinEdge = childOffset[crossAxis] - overlaySize[crossAxis] + (arrowSize / 2);
|
|
366
|
+
const arrowOverlappingChildMaxEdge = childOffset[crossAxis] + childOffset[crossSize] - overlaySize[crossAxis] - (arrowSize / 2);
|
|
367
|
+
|
|
368
|
+
// Clamp the arrow positioning so that it always is within the bounds of the anchor and the overlay
|
|
369
|
+
const arrowPositionOverlappingChild = clamp(preferredArrowPosition, arrowOverlappingChildMinEdge, arrowOverlappingChildMaxEdge);
|
|
370
|
+
arrowPosition[crossAxis] = clamp(arrowPositionOverlappingChild, arrowMinPosition, arrowMaxPosition);
|
|
340
371
|
|
|
341
372
|
return {
|
|
342
373
|
position,
|
|
@@ -361,16 +392,18 @@ export function calculatePosition(opts: PositionOpts): PositionResult {
|
|
|
361
392
|
boundaryElement,
|
|
362
393
|
offset,
|
|
363
394
|
crossOffset,
|
|
364
|
-
maxHeight
|
|
395
|
+
maxHeight,
|
|
396
|
+
arrowSize,
|
|
397
|
+
arrowBoundaryOffset = 0
|
|
365
398
|
} = opts;
|
|
366
399
|
|
|
367
|
-
let container =
|
|
368
|
-
let
|
|
400
|
+
let container = overlayNode instanceof HTMLElement ? getContainingBlock(overlayNode) : document.documentElement;
|
|
401
|
+
let isViewportContainer = container === document.documentElement;
|
|
369
402
|
const containerPositionStyle = window.getComputedStyle(container).position;
|
|
370
403
|
let isContainerPositioned = !!containerPositionStyle && containerPositionStyle !== 'static';
|
|
371
|
-
let childOffset: Offset =
|
|
404
|
+
let childOffset: Offset = isViewportContainer ? getOffset(targetNode) : getPosition(targetNode, container);
|
|
372
405
|
|
|
373
|
-
if (!
|
|
406
|
+
if (!isViewportContainer) {
|
|
374
407
|
let {marginTop, marginLeft} = window.getComputedStyle(targetNode);
|
|
375
408
|
childOffset.top += parseInt(marginTop, 10) || 0;
|
|
376
409
|
childOffset.left += parseInt(marginLeft, 10) || 0;
|
|
@@ -398,7 +431,9 @@ export function calculatePosition(opts: PositionOpts): PositionResult {
|
|
|
398
431
|
offset,
|
|
399
432
|
crossOffset,
|
|
400
433
|
isContainerPositioned,
|
|
401
|
-
maxHeight
|
|
434
|
+
maxHeight,
|
|
435
|
+
arrowSize,
|
|
436
|
+
arrowBoundaryOffset
|
|
402
437
|
);
|
|
403
438
|
}
|
|
404
439
|
|
|
@@ -433,3 +468,54 @@ function getPosition(node: Element, parent: Element): Offset {
|
|
|
433
468
|
offset.left -= parseInt(style.marginLeft, 10) || 0;
|
|
434
469
|
return offset;
|
|
435
470
|
}
|
|
471
|
+
|
|
472
|
+
// Returns the containing block of an element, which is the element that
|
|
473
|
+
// this element will be positioned relative to.
|
|
474
|
+
// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block
|
|
475
|
+
function getContainingBlock(node: HTMLElement): Element {
|
|
476
|
+
// The offsetParent of an element in most cases equals the containing block.
|
|
477
|
+
// https://w3c.github.io/csswg-drafts/cssom-view/#dom-htmlelement-offsetparent
|
|
478
|
+
let offsetParent = node.offsetParent;
|
|
479
|
+
|
|
480
|
+
// The offsetParent algorithm terminates at the document body,
|
|
481
|
+
// even if the body is not a containing block. Double check that
|
|
482
|
+
// and use the documentElement if so.
|
|
483
|
+
if (
|
|
484
|
+
offsetParent &&
|
|
485
|
+
offsetParent === document.body &&
|
|
486
|
+
window.getComputedStyle(offsetParent).position === 'static' &&
|
|
487
|
+
!isContainingBlock(offsetParent)
|
|
488
|
+
) {
|
|
489
|
+
offsetParent = document.documentElement;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// TODO(later): handle table elements?
|
|
493
|
+
|
|
494
|
+
// The offsetParent can be null if the element has position: fixed, or a few other cases.
|
|
495
|
+
// We have to walk up the tree manually in this case because fixed positioned elements
|
|
496
|
+
// are still positioned relative to their containing block, which is not always the viewport.
|
|
497
|
+
if (offsetParent == null) {
|
|
498
|
+
offsetParent = node.parentElement;
|
|
499
|
+
while (offsetParent && !isContainingBlock(offsetParent)) {
|
|
500
|
+
offsetParent = offsetParent.parentElement;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Fall back to the viewport.
|
|
505
|
+
return offsetParent || document.documentElement;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
|
|
509
|
+
function isContainingBlock(node: Element): boolean {
|
|
510
|
+
let style = window.getComputedStyle(node);
|
|
511
|
+
return (
|
|
512
|
+
style.transform !== 'none' ||
|
|
513
|
+
/transform|perspective/.test(style.willChange) ||
|
|
514
|
+
style.filter !== 'none' ||
|
|
515
|
+
style.contain === 'paint' ||
|
|
516
|
+
// @ts-ignore
|
|
517
|
+
('backdropFilter' in style && style.backdropFilter !== 'none') ||
|
|
518
|
+
// @ts-ignore
|
|
519
|
+
('WebkitBackdropFilter' in style && style.WebkitBackdropFilter !== 'none')
|
|
520
|
+
);
|
|
521
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -28,3 +28,4 @@ export type {DismissButtonProps} from './DismissButton';
|
|
|
28
28
|
export type {AriaPopoverProps, PopoverAria} from './usePopover';
|
|
29
29
|
export type {AriaModalOverlayProps, ModalOverlayAria} from './useModalOverlay';
|
|
30
30
|
export type {OverlayProps} from './Overlay';
|
|
31
|
+
export type {Placement, PlacementAxis, PositionProps} from '@react-types/overlays';
|
|
@@ -19,6 +19,11 @@ import {useLayoutEffect, useResizeObserver} from '@react-aria/utils';
|
|
|
19
19
|
import {useLocale} from '@react-aria/i18n';
|
|
20
20
|
|
|
21
21
|
export interface AriaPositionProps extends PositionProps {
|
|
22
|
+
/**
|
|
23
|
+
* Cross size of the overlay arrow in pixels.
|
|
24
|
+
* @default 0
|
|
25
|
+
*/
|
|
26
|
+
arrowSize?: number,
|
|
22
27
|
/**
|
|
23
28
|
* Element that that serves as the positioning boundary.
|
|
24
29
|
* @default document.body
|
|
@@ -48,7 +53,12 @@ export interface AriaPositionProps extends PositionProps {
|
|
|
48
53
|
* The maxHeight specified for the overlay element.
|
|
49
54
|
* By default, it will take all space up to the current viewport height.
|
|
50
55
|
*/
|
|
51
|
-
maxHeight?: number
|
|
56
|
+
maxHeight?: number,
|
|
57
|
+
/**
|
|
58
|
+
* The minimum distance the arrow's edge should be from the edge of the overlay element.
|
|
59
|
+
* @default 0
|
|
60
|
+
*/
|
|
61
|
+
arrowBoundaryOffset?: number
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
export interface PositionAria {
|
|
@@ -72,6 +82,7 @@ let visualViewport = typeof window !== 'undefined' && window.visualViewport;
|
|
|
72
82
|
export function useOverlayPosition(props: AriaPositionProps): PositionAria {
|
|
73
83
|
let {direction} = useLocale();
|
|
74
84
|
let {
|
|
85
|
+
arrowSize = 0,
|
|
75
86
|
targetRef,
|
|
76
87
|
overlayRef,
|
|
77
88
|
scrollRef = overlayRef,
|
|
@@ -84,7 +95,8 @@ export function useOverlayPosition(props: AriaPositionProps): PositionAria {
|
|
|
84
95
|
shouldUpdatePosition = true,
|
|
85
96
|
isOpen = true,
|
|
86
97
|
onClose,
|
|
87
|
-
maxHeight
|
|
98
|
+
maxHeight,
|
|
99
|
+
arrowBoundaryOffset = 0
|
|
88
100
|
} = props;
|
|
89
101
|
let [position, setPosition] = useState<PositionResult>({
|
|
90
102
|
position: {},
|
|
@@ -107,7 +119,9 @@ export function useOverlayPosition(props: AriaPositionProps): PositionAria {
|
|
|
107
119
|
crossOffset,
|
|
108
120
|
isOpen,
|
|
109
121
|
direction,
|
|
110
|
-
maxHeight
|
|
122
|
+
maxHeight,
|
|
123
|
+
arrowBoundaryOffset,
|
|
124
|
+
arrowSize
|
|
111
125
|
];
|
|
112
126
|
|
|
113
127
|
let updatePosition = useCallback(() => {
|
|
@@ -115,20 +129,28 @@ export function useOverlayPosition(props: AriaPositionProps): PositionAria {
|
|
|
115
129
|
return;
|
|
116
130
|
}
|
|
117
131
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
);
|
|
132
|
+
let position = calculatePosition({
|
|
133
|
+
placement: translateRTL(placement, direction),
|
|
134
|
+
overlayNode: overlayRef.current,
|
|
135
|
+
targetNode: targetRef.current,
|
|
136
|
+
scrollNode: scrollRef.current,
|
|
137
|
+
padding: containerPadding,
|
|
138
|
+
shouldFlip,
|
|
139
|
+
boundaryElement,
|
|
140
|
+
offset,
|
|
141
|
+
crossOffset,
|
|
142
|
+
maxHeight,
|
|
143
|
+
arrowSize,
|
|
144
|
+
arrowBoundaryOffset
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Modify overlay styles directly so positioning happens immediately without the need of a second render
|
|
148
|
+
// This is so we don't have to delay autoFocus scrolling or delay applying preventScroll for popovers
|
|
149
|
+
Object.keys(position.position).forEach(key => (overlayRef.current as HTMLElement).style[key] = position.position[key] + 'px');
|
|
150
|
+
(overlayRef.current as HTMLElement).style.maxHeight = position.maxHeight != null ? position.maxHeight + 'px' : undefined;
|
|
151
|
+
|
|
152
|
+
// Trigger a set state for a second render anyway for arrow positioning
|
|
153
|
+
setPosition(position);
|
|
132
154
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
133
155
|
}, deps);
|
|
134
156
|
|
|
@@ -162,9 +184,11 @@ export function useOverlayPosition(props: AriaPositionProps): PositionAria {
|
|
|
162
184
|
};
|
|
163
185
|
|
|
164
186
|
visualViewport?.addEventListener('resize', onResize);
|
|
187
|
+
visualViewport?.addEventListener('scroll', onResize);
|
|
165
188
|
|
|
166
189
|
return () => {
|
|
167
190
|
visualViewport?.removeEventListener('resize', onResize);
|
|
191
|
+
visualViewport?.removeEventListener('scroll', onResize);
|
|
168
192
|
};
|
|
169
193
|
}, [updatePosition]);
|
|
170
194
|
|
package/src/useOverlayTrigger.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {AriaButtonProps} from '@react-types/button';
|
|
14
|
-
import {
|
|
14
|
+
import {DOMProps} from '@react-types/shared';
|
|
15
15
|
import {onCloseMap} from './useCloseOnScroll';
|
|
16
16
|
import {OverlayTriggerState} from '@react-stately/overlays';
|
|
17
17
|
import {RefObject, useEffect} from 'react';
|
|
@@ -27,14 +27,14 @@ export interface OverlayTriggerAria {
|
|
|
27
27
|
triggerProps: AriaButtonProps,
|
|
28
28
|
|
|
29
29
|
/** Props for the overlay container element. */
|
|
30
|
-
overlayProps:
|
|
30
|
+
overlayProps: DOMProps
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Handles the behavior and accessibility for an overlay trigger, e.g. a button
|
|
35
35
|
* that opens a popover, menu, or other overlay that is positioned relative to the trigger.
|
|
36
36
|
*/
|
|
37
|
-
export function useOverlayTrigger(props: OverlayTriggerProps, state: OverlayTriggerState, ref
|
|
37
|
+
export function useOverlayTrigger(props: OverlayTriggerProps, state: OverlayTriggerState, ref?: RefObject<Element>): OverlayTriggerAria {
|
|
38
38
|
let {type} = props;
|
|
39
39
|
let {isOpen} = state;
|
|
40
40
|
|
package/src/usePopover.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {DOMAttributes} from '@react-types/shared';
|
|
|
16
16
|
import {mergeProps, useLayoutEffect} from '@react-aria/utils';
|
|
17
17
|
import {OverlayTriggerState} from '@react-stately/overlays';
|
|
18
18
|
import {PlacementAxis} from '@react-types/overlays';
|
|
19
|
-
import {RefObject
|
|
19
|
+
import {RefObject} from 'react';
|
|
20
20
|
import {useOverlay} from './useOverlay';
|
|
21
21
|
import {usePreventScroll} from './usePreventScroll';
|
|
22
22
|
|
|
@@ -92,18 +92,8 @@ export function usePopover(props: AriaPopoverProps, state: OverlayTriggerState):
|
|
|
92
92
|
onClose: null
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
-
// Delay preventing scroll until popover is positioned to avoid extra scroll padding.
|
|
96
|
-
// This requires a layout effect so that positioning has been committed to the DOM
|
|
97
|
-
// by the time usePreventScroll measures the element.
|
|
98
|
-
let [isPositioned, setPositioned] = useState(false);
|
|
99
|
-
useLayoutEffect(() => {
|
|
100
|
-
if (!isNonModal && placement) {
|
|
101
|
-
setPositioned(true);
|
|
102
|
-
}
|
|
103
|
-
}, [isNonModal, placement]);
|
|
104
|
-
|
|
105
95
|
usePreventScroll({
|
|
106
|
-
isDisabled: isNonModal
|
|
96
|
+
isDisabled: isNonModal
|
|
107
97
|
});
|
|
108
98
|
|
|
109
99
|
useLayoutEffect(() => {
|