@react-aria/overlays 3.19.0 → 3.21.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 +135 -55
- package/dist/main.js +133 -53
- package/dist/main.js.map +1 -1
- package/dist/module.js +135 -55
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/DismissButton.tsx +3 -2
- package/src/calculatePosition.ts +85 -24
- package/src/useOverlay.ts +1 -1
- package/src/useOverlayPosition.ts +31 -4
- package/src/usePopover.ts +13 -4
- package/src/usePreventScroll.ts +56 -41
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":";;;;;AEoBA,kCAAmC,SAAQ,aAAa;IACtD;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;OAEG;IACH,SAAS,EAAE,UAAU,OAAO,CAAC,CAAC;IAC9B;;OAEG;IACH,UAAU,EAAE,UAAU,OAAO,CAAC,CAAC;IAC/B;;;OAGG;IACH,SAAS,CAAC,EAAE,UAAU,OAAO,CAAC,CAAC;IAC/B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC7B;AAED;IACE,+CAA+C;IAC/C,YAAY,EAAE,aAAa,CAAC;IAC5B,8CAA8C;IAC9C,UAAU,EAAE,aAAa,CAAC;IAC1B,oEAAoE;IACpE,SAAS,EAAE,aAAa,CAAC;IACzB,2CAA2C;IAC3C,cAAc,IAAI,IAAI,CAAA;CACvB;AAKD;;;GAGG;AACH,mCAAmC,KAAK,EAAE,iBAAiB,GAAG,YAAY,
|
|
1
|
+
{"mappings":"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","sources":["packages/@react-aria/overlays/src/packages/@react-aria/overlays/src/calculatePosition.ts","packages/@react-aria/overlays/src/packages/@react-aria/overlays/src/useCloseOnScroll.ts","packages/@react-aria/overlays/src/packages/@react-aria/overlays/src/useOverlayPosition.ts","packages/@react-aria/overlays/src/packages/@react-aria/overlays/src/useOverlay.ts","packages/@react-aria/overlays/src/packages/@react-aria/overlays/src/useOverlayTrigger.ts","packages/@react-aria/overlays/src/packages/@react-aria/overlays/src/usePreventScroll.ts","packages/@react-aria/overlays/src/packages/@react-aria/overlays/src/useModal.tsx","packages/@react-aria/overlays/src/packages/@react-aria/overlays/src/DismissButton.tsx","packages/@react-aria/overlays/src/packages/@react-aria/overlays/src/ariaHideOutside.ts","packages/@react-aria/overlays/src/packages/@react-aria/overlays/src/usePopover.ts","packages/@react-aria/overlays/src/packages/@react-aria/overlays/src/Overlay.tsx","packages/@react-aria/overlays/src/packages/@react-aria/overlays/src/useModalOverlay.ts","packages/@react-aria/overlays/src/packages/@react-aria/overlays/src/index.ts","packages/@react-aria/overlays/src/index.ts"],"sourcesContent":[null,null,null,null,null,null,null,null,null,null,null,null,null,"/*\n * Copyright 2020 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\nexport {useOverlayPosition} from './useOverlayPosition';\nexport {useOverlay} from './useOverlay';\nexport {useOverlayTrigger} from './useOverlayTrigger';\nexport {usePreventScroll} from './usePreventScroll';\nexport {ModalProvider, useModalProvider, OverlayProvider, OverlayContainer, useModal} from './useModal';\nexport {DismissButton} from './DismissButton';\nexport {ariaHideOutside} from './ariaHideOutside';\nexport {usePopover} from './usePopover';\nexport {useModalOverlay} from './useModalOverlay';\nexport {Overlay, useOverlayFocusContain} from './Overlay';\n\nexport type {AriaPositionProps, PositionAria} from './useOverlayPosition';\nexport type {AriaOverlayProps, OverlayAria} from './useOverlay';\nexport type {OverlayTriggerAria, OverlayTriggerProps} from './useOverlayTrigger';\nexport type {AriaModalOptions, ModalAria, ModalProviderAria, ModalProviderProps, OverlayContainerProps} from './useModal';\nexport type {DismissButtonProps} from './DismissButton';\nexport type {AriaPopoverProps, PopoverAria} from './usePopover';\nexport type {AriaModalOverlayProps, ModalOverlayAria} from './useModalOverlay';\nexport type {OverlayProps} from './Overlay';\nexport type {Placement, PlacementAxis, PositionProps} from '@react-types/overlays';\n"],"names":[],"version":3,"file":"types.d.ts.map"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-aria/overlays",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.21.0",
|
|
4
4
|
"description": "Spectrum UI components in React",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "dist/main.js",
|
|
@@ -22,12 +22,12 @@
|
|
|
22
22
|
"url": "https://github.com/adobe/react-spectrum"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@react-aria/focus": "^3.
|
|
26
|
-
"@react-aria/i18n": "^3.
|
|
27
|
-
"@react-aria/interactions": "^3.
|
|
28
|
-
"@react-aria/ssr": "^3.9.
|
|
29
|
-
"@react-aria/utils": "^3.
|
|
30
|
-
"@react-aria/visually-hidden": "^3.8.
|
|
25
|
+
"@react-aria/focus": "^3.16.1",
|
|
26
|
+
"@react-aria/i18n": "^3.10.1",
|
|
27
|
+
"@react-aria/interactions": "^3.21.0",
|
|
28
|
+
"@react-aria/ssr": "^3.9.1",
|
|
29
|
+
"@react-aria/utils": "^3.23.1",
|
|
30
|
+
"@react-aria/visually-hidden": "^3.8.9",
|
|
31
31
|
"@react-stately/overlays": "^3.6.4",
|
|
32
32
|
"@react-types/button": "^3.9.1",
|
|
33
33
|
"@react-types/overlays": "^3.8.4",
|
|
@@ -41,5 +41,5 @@
|
|
|
41
41
|
"publishConfig": {
|
|
42
42
|
"access": "public"
|
|
43
43
|
},
|
|
44
|
-
"gitHead": "
|
|
44
|
+
"gitHead": "f040ff62678e6a31375b96c05396df0bae660350"
|
|
45
45
|
}
|
package/src/DismissButton.tsx
CHANGED
|
@@ -30,7 +30,7 @@ export interface DismissButtonProps extends AriaLabelingProps, DOMProps {
|
|
|
30
30
|
*/
|
|
31
31
|
export function DismissButton(props: DismissButtonProps) {
|
|
32
32
|
let {onDismiss, ...otherProps} = props;
|
|
33
|
-
let stringFormatter = useLocalizedStringFormatter(intlMessages);
|
|
33
|
+
let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/overlays');
|
|
34
34
|
|
|
35
35
|
let labels = useLabels(otherProps, stringFormatter.format('dismiss'));
|
|
36
36
|
|
|
@@ -45,7 +45,8 @@ export function DismissButton(props: DismissButtonProps) {
|
|
|
45
45
|
<button
|
|
46
46
|
{...labels}
|
|
47
47
|
tabIndex={-1}
|
|
48
|
-
onClick={onClick}
|
|
48
|
+
onClick={onClick}
|
|
49
|
+
style={{width: 1, height: 1}} />
|
|
49
50
|
</VisuallyHidden>
|
|
50
51
|
);
|
|
51
52
|
}
|
package/src/calculatePosition.ts
CHANGED
|
@@ -11,7 +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
|
+
import {clamp, isWebKit} from '@react-aria/utils';
|
|
15
15
|
|
|
16
16
|
interface Position {
|
|
17
17
|
top?: number,
|
|
@@ -61,6 +61,8 @@ interface PositionOpts {
|
|
|
61
61
|
arrowBoundaryOffset?: number
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
type HeightGrowthDirection = 'top' | 'bottom';
|
|
65
|
+
|
|
64
66
|
export interface PositionResult {
|
|
65
67
|
position?: Position,
|
|
66
68
|
arrowOffsetLeft?: number,
|
|
@@ -106,6 +108,7 @@ let visualViewport = typeof document !== 'undefined' && window.visualViewport;
|
|
|
106
108
|
function getContainerDimensions(containerNode: Element): Dimensions {
|
|
107
109
|
let width = 0, height = 0, totalWidth = 0, totalHeight = 0, top = 0, left = 0;
|
|
108
110
|
let scroll: Position = {};
|
|
111
|
+
let isPinchZoomedIn = visualViewport?.scale > 1;
|
|
109
112
|
|
|
110
113
|
if (containerNode.tagName === 'BODY') {
|
|
111
114
|
let documentElement = document.documentElement;
|
|
@@ -113,9 +116,16 @@ function getContainerDimensions(containerNode: Element): Dimensions {
|
|
|
113
116
|
totalHeight = documentElement.clientHeight;
|
|
114
117
|
width = visualViewport?.width ?? totalWidth;
|
|
115
118
|
height = visualViewport?.height ?? totalHeight;
|
|
116
|
-
|
|
117
119
|
scroll.top = documentElement.scrollTop || containerNode.scrollTop;
|
|
118
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
|
+
}
|
|
119
129
|
} else {
|
|
120
130
|
({width, height, top, left} = getOffset(containerNode));
|
|
121
131
|
scroll.top = containerNode.scrollTop;
|
|
@@ -124,6 +134,17 @@ function getContainerDimensions(containerNode: Element): Dimensions {
|
|
|
124
134
|
totalHeight = height;
|
|
125
135
|
}
|
|
126
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;
|
|
145
|
+
left = visualViewport.pageLeft;
|
|
146
|
+
}
|
|
147
|
+
|
|
127
148
|
return {width, height, totalWidth, totalHeight, scroll, top, left};
|
|
128
149
|
}
|
|
129
150
|
|
|
@@ -136,6 +157,7 @@ function getScroll(node: Element): Offset {
|
|
|
136
157
|
};
|
|
137
158
|
}
|
|
138
159
|
|
|
160
|
+
// Determines the amount of space required when moving the overlay to ensure it remains in the boundary
|
|
139
161
|
function getDelta(
|
|
140
162
|
axis: Axis,
|
|
141
163
|
offset: number,
|
|
@@ -149,17 +171,25 @@ function getDelta(
|
|
|
149
171
|
// is portaled somewhere other than the body and has an ancestor with
|
|
150
172
|
// position: relative/absolute, it will be different.
|
|
151
173
|
containerDimensions: Dimensions,
|
|
152
|
-
padding: number
|
|
174
|
+
padding: number,
|
|
175
|
+
containerOffsetWithBoundary: Offset
|
|
153
176
|
) {
|
|
154
177
|
let containerScroll = containerDimensions.scroll[axis];
|
|
155
|
-
|
|
156
|
-
let
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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);
|
|
163
193
|
} else {
|
|
164
194
|
return 0;
|
|
165
195
|
}
|
|
@@ -222,7 +252,7 @@ function computePosition(
|
|
|
222
252
|
}/* else {
|
|
223
253
|
the overlay top should match the button top
|
|
224
254
|
} */
|
|
225
|
-
|
|
255
|
+
|
|
226
256
|
position[crossAxis] += crossOffset;
|
|
227
257
|
|
|
228
258
|
// overlay top overlapping arrow with button bottom
|
|
@@ -242,7 +272,6 @@ function computePosition(
|
|
|
242
272
|
} else {
|
|
243
273
|
position[axis] = Math.floor(childOffset[axis] + childOffset[size] + offset);
|
|
244
274
|
}
|
|
245
|
-
|
|
246
275
|
return position;
|
|
247
276
|
}
|
|
248
277
|
|
|
@@ -250,23 +279,30 @@ function getMaxHeight(
|
|
|
250
279
|
position: Position,
|
|
251
280
|
boundaryDimensions: Dimensions,
|
|
252
281
|
containerOffsetWithBoundary: Offset,
|
|
253
|
-
|
|
282
|
+
isContainerPositioned: boolean,
|
|
254
283
|
margins: Position,
|
|
255
|
-
padding: number
|
|
284
|
+
padding: number,
|
|
285
|
+
overlayHeight: number,
|
|
286
|
+
heightGrowthDirection: HeightGrowthDirection
|
|
256
287
|
) {
|
|
257
|
-
|
|
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 - overlayHeight);
|
|
292
|
+
let maxHeight = heightGrowthDirection !== 'top' ?
|
|
258
293
|
// We want the distance between the top of the overlay to the bottom of the boundary
|
|
259
|
-
|
|
294
|
+
Math.max(0,
|
|
260
295
|
(boundaryDimensions.height + boundaryDimensions.top + boundaryDimensions.scroll.top) // this is the bottom of the boundary
|
|
261
|
-
-
|
|
296
|
+
- overlayTop // this is the top of the overlay
|
|
262
297
|
- (margins.top + margins.bottom + padding) // save additional space for margin and padding
|
|
263
298
|
)
|
|
264
|
-
// We want the distance between the
|
|
299
|
+
// We want the distance between the bottom of the overlay to the top of the boundary
|
|
265
300
|
: Math.max(0,
|
|
266
|
-
(
|
|
301
|
+
(overlayTop + overlayHeight) // this is the bottom of the overlay
|
|
267
302
|
- (boundaryDimensions.top + boundaryDimensions.scroll.top) // this is the top of the boundary
|
|
268
303
|
- (margins.top + margins.bottom + padding) // save additional space for margin and padding
|
|
269
304
|
);
|
|
305
|
+
return Math.min(boundaryDimensions.height - (padding * 2), maxHeight);
|
|
270
306
|
}
|
|
271
307
|
|
|
272
308
|
function getAvailableSpace(
|
|
@@ -337,16 +373,34 @@ export function calculatePositionInternal(
|
|
|
337
373
|
}
|
|
338
374
|
}
|
|
339
375
|
|
|
340
|
-
|
|
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);
|
|
341
393
|
position[crossAxis] += delta;
|
|
342
394
|
|
|
343
395
|
let maxHeight = getMaxHeight(
|
|
344
396
|
position,
|
|
345
397
|
boundaryDimensions,
|
|
346
398
|
containerOffsetWithBoundary,
|
|
347
|
-
|
|
399
|
+
isContainerPositioned,
|
|
348
400
|
margins,
|
|
349
|
-
padding
|
|
401
|
+
padding,
|
|
402
|
+
overlaySize.height,
|
|
403
|
+
heightGrowthDirection
|
|
350
404
|
);
|
|
351
405
|
|
|
352
406
|
if (userSetMaxHeight && userSetMaxHeight < maxHeight) {
|
|
@@ -356,7 +410,7 @@ export function calculatePositionInternal(
|
|
|
356
410
|
overlaySize.height = Math.min(overlaySize.height, maxHeight);
|
|
357
411
|
|
|
358
412
|
position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, normalizedOffset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset);
|
|
359
|
-
delta = getDelta(crossAxis, position[crossAxis], overlaySize[crossSize], boundaryDimensions, containerDimensions, padding);
|
|
413
|
+
delta = getDelta(crossAxis, position[crossAxis], overlaySize[crossSize], boundaryDimensions, containerDimensions, padding, containerOffsetWithBoundary);
|
|
360
414
|
position[crossAxis] += delta;
|
|
361
415
|
|
|
362
416
|
let arrowPosition: Position = {};
|
|
@@ -425,7 +479,14 @@ export function calculatePosition(opts: PositionOpts): PositionResult {
|
|
|
425
479
|
let scrollSize = getScroll(scrollNode);
|
|
426
480
|
let boundaryDimensions = getContainerDimensions(boundaryElement);
|
|
427
481
|
let containerDimensions = getContainerDimensions(container);
|
|
482
|
+
// If the container is the HTML element wrapping the body element, the retrieved scrollTop/scrollLeft will be equal to the
|
|
483
|
+
// 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
|
|
484
|
+
// by the container scroll since they are essentially the same containing element and thus in the same coordinate system
|
|
428
485
|
let containerOffsetWithBoundary: Offset = boundaryElement.tagName === 'BODY' ? getOffset(container) : getPosition(container, boundaryElement);
|
|
486
|
+
if (container.tagName === 'HTML' && boundaryElement.tagName === 'BODY') {
|
|
487
|
+
containerDimensions.scroll.top = 0;
|
|
488
|
+
containerDimensions.scroll.left = 0;
|
|
489
|
+
}
|
|
429
490
|
|
|
430
491
|
return calculatePositionInternal(
|
|
431
492
|
placement,
|
package/src/useOverlay.ts
CHANGED
|
@@ -84,7 +84,7 @@ export function useOverlay(props: AriaOverlayProps, ref: RefObject<Element>): Ov
|
|
|
84
84
|
};
|
|
85
85
|
}, [isOpen, ref]);
|
|
86
86
|
|
|
87
|
-
// Only hide the overlay when it is the topmost visible overlay in the stack
|
|
87
|
+
// Only hide the overlay when it is the topmost visible overlay in the stack
|
|
88
88
|
let onHide = () => {
|
|
89
89
|
if (visibleOverlays[visibleOverlays.length - 1] === ref && onClose) {
|
|
90
90
|
onClose();
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import {calculatePosition, PositionResult} from './calculatePosition';
|
|
14
14
|
import {DOMAttributes} from '@react-types/shared';
|
|
15
15
|
import {Placement, PlacementAxis, PositionProps} from '@react-types/overlays';
|
|
16
|
-
import {RefObject, useCallback, useRef, useState} from 'react';
|
|
16
|
+
import {RefObject, useCallback, useEffect, useRef, useState} from 'react';
|
|
17
17
|
import {useCloseOnScroll} from './useCloseOnScroll';
|
|
18
18
|
import {useLayoutEffect, useResizeObserver} from '@react-aria/utils';
|
|
19
19
|
import {useLocale} from '@react-aria/i18n';
|
|
@@ -124,11 +124,31 @@ export function useOverlayPosition(props: AriaPositionProps): PositionAria {
|
|
|
124
124
|
arrowSize
|
|
125
125
|
];
|
|
126
126
|
|
|
127
|
+
// Note, the position freezing breaks if body sizes itself dynamicly with the visual viewport but that might
|
|
128
|
+
// just be a non-realistic use case
|
|
129
|
+
// Upon opening a overlay, record the current visual viewport scale so we can freeze the overlay styles
|
|
130
|
+
let lastScale = useRef(visualViewport?.scale);
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (isOpen) {
|
|
133
|
+
lastScale.current = visualViewport?.scale;
|
|
134
|
+
}
|
|
135
|
+
}, [isOpen]);
|
|
136
|
+
|
|
127
137
|
let updatePosition = useCallback(() => {
|
|
128
138
|
if (shouldUpdatePosition === false || !isOpen || !overlayRef.current || !targetRef.current || !scrollRef.current || !boundaryElement) {
|
|
129
139
|
return;
|
|
130
140
|
}
|
|
131
141
|
|
|
142
|
+
if (visualViewport?.scale !== lastScale.current) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Always reset the overlay's previous max height if not defined by the user so that we can compensate for
|
|
147
|
+
// RAC collections populating after a second render and properly set a correct max height + positioning when it populates.
|
|
148
|
+
if (!maxHeight && overlayRef.current) {
|
|
149
|
+
(overlayRef.current as HTMLElement).style.maxHeight = 'none';
|
|
150
|
+
}
|
|
151
|
+
|
|
132
152
|
let position = calculatePosition({
|
|
133
153
|
placement: translateRTL(placement, direction),
|
|
134
154
|
overlayNode: overlayRef.current,
|
|
@@ -183,12 +203,19 @@ export function useOverlayPosition(props: AriaPositionProps): PositionAria {
|
|
|
183
203
|
updatePosition();
|
|
184
204
|
};
|
|
185
205
|
|
|
186
|
-
|
|
187
|
-
|
|
206
|
+
// Only reposition the overlay if a scroll event happens immediately as a result of resize (aka the virtual keyboard has appears)
|
|
207
|
+
// We don't want to reposition the overlay if the user has pinch zoomed in and is scrolling the viewport around.
|
|
208
|
+
let onScroll = () => {
|
|
209
|
+
if (isResizing.current) {
|
|
210
|
+
onResize();
|
|
211
|
+
}
|
|
212
|
+
};
|
|
188
213
|
|
|
214
|
+
visualViewport?.addEventListener('resize', onResize);
|
|
215
|
+
visualViewport?.addEventListener('scroll', onScroll);
|
|
189
216
|
return () => {
|
|
190
217
|
visualViewport?.removeEventListener('resize', onResize);
|
|
191
|
-
visualViewport?.removeEventListener('scroll',
|
|
218
|
+
visualViewport?.removeEventListener('scroll', onScroll);
|
|
192
219
|
};
|
|
193
220
|
}, [updatePosition]);
|
|
194
221
|
|
package/src/usePopover.ts
CHANGED
|
@@ -46,7 +46,14 @@ export interface AriaPopoverProps extends Omit<AriaPositionProps, 'isOpen' | 'on
|
|
|
46
46
|
*
|
|
47
47
|
* @default false
|
|
48
48
|
*/
|
|
49
|
-
isKeyboardDismissDisabled?: boolean
|
|
49
|
+
isKeyboardDismissDisabled?: boolean,
|
|
50
|
+
/**
|
|
51
|
+
* When user interacts with the argument element outside of the popover ref,
|
|
52
|
+
* return true if onClose should be called. This gives you a chance to filter
|
|
53
|
+
* out interaction with elements that should not dismiss the popover.
|
|
54
|
+
* By default, onClose will always be called on interaction outside the popover ref.
|
|
55
|
+
*/
|
|
56
|
+
shouldCloseOnInteractOutside?: (element: Element) => boolean
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
export interface PopoverAria {
|
|
@@ -70,6 +77,7 @@ export function usePopover(props: AriaPopoverProps, state: OverlayTriggerState):
|
|
|
70
77
|
popoverRef,
|
|
71
78
|
isNonModal,
|
|
72
79
|
isKeyboardDismissDisabled,
|
|
80
|
+
shouldCloseOnInteractOutside,
|
|
73
81
|
...otherProps
|
|
74
82
|
} = props;
|
|
75
83
|
|
|
@@ -79,7 +87,8 @@ export function usePopover(props: AriaPopoverProps, state: OverlayTriggerState):
|
|
|
79
87
|
onClose: state.close,
|
|
80
88
|
shouldCloseOnBlur: true,
|
|
81
89
|
isDismissable: !isNonModal,
|
|
82
|
-
isKeyboardDismissDisabled
|
|
90
|
+
isKeyboardDismissDisabled,
|
|
91
|
+
shouldCloseOnInteractOutside
|
|
83
92
|
},
|
|
84
93
|
popoverRef
|
|
85
94
|
);
|
|
@@ -89,11 +98,11 @@ export function usePopover(props: AriaPopoverProps, state: OverlayTriggerState):
|
|
|
89
98
|
targetRef: triggerRef,
|
|
90
99
|
overlayRef: popoverRef,
|
|
91
100
|
isOpen: state.isOpen,
|
|
92
|
-
onClose: null
|
|
101
|
+
onClose: isNonModal ? state.close : null
|
|
93
102
|
});
|
|
94
103
|
|
|
95
104
|
usePreventScroll({
|
|
96
|
-
isDisabled: isNonModal
|
|
105
|
+
isDisabled: isNonModal || !state.isOpen
|
|
97
106
|
});
|
|
98
107
|
|
|
99
108
|
useLayoutEffect(() => {
|
package/src/usePreventScroll.ts
CHANGED
|
@@ -92,8 +92,9 @@ function preventScrollStandard() {
|
|
|
92
92
|
//
|
|
93
93
|
// 1. Prevent default on `touchmove` events that are not in a scrollable element. This prevents touch scrolling
|
|
94
94
|
// on the window.
|
|
95
|
-
// 2.
|
|
96
|
-
// top or bottom.
|
|
95
|
+
// 2. Set `overscroll-behavior: contain` on nested scrollable regions so they do not scroll the page when at
|
|
96
|
+
// the top or bottom. Work around a bug where this does not work when the element does not actually overflow
|
|
97
|
+
// by preventing default in a `touchmove` event.
|
|
97
98
|
// 3. Prevent default on `touchend` events on input elements and handle focusing the element ourselves.
|
|
98
99
|
// 4. When focusing an input, apply a transform to trick Safari into thinking the input is at the top
|
|
99
100
|
// of the page, which prevents it from scrolling the page. After the input is focused, scroll the element
|
|
@@ -105,15 +106,20 @@ function preventScrollStandard() {
|
|
|
105
106
|
// to navigate to an input with the next/previous buttons that's outside a modal.
|
|
106
107
|
function preventScrollMobileSafari() {
|
|
107
108
|
let scrollable: Element;
|
|
108
|
-
let
|
|
109
|
+
let restoreScrollableStyles;
|
|
109
110
|
let onTouchStart = (e: TouchEvent) => {
|
|
110
111
|
// Store the nearest scrollable parent element from the element that the user touched.
|
|
111
|
-
scrollable = getScrollParent(e.target as Element);
|
|
112
|
+
scrollable = getScrollParent(e.target as Element, true);
|
|
112
113
|
if (scrollable === document.documentElement && scrollable === document.body) {
|
|
113
114
|
return;
|
|
114
115
|
}
|
|
115
116
|
|
|
116
|
-
|
|
117
|
+
// Prevent scrolling up when at the top and scrolling down when at the bottom
|
|
118
|
+
// of a nested scrollable area, otherwise mobile Safari will start scrolling
|
|
119
|
+
// the window instead.
|
|
120
|
+
if (scrollable instanceof HTMLElement && window.getComputedStyle(scrollable).overscrollBehavior === 'auto') {
|
|
121
|
+
restoreScrollableStyles = setStyle(scrollable, 'overscrollBehavior', 'contain');
|
|
122
|
+
}
|
|
117
123
|
};
|
|
118
124
|
|
|
119
125
|
let onTouchMove = (e: TouchEvent) => {
|
|
@@ -123,23 +129,15 @@ function preventScrollMobileSafari() {
|
|
|
123
129
|
return;
|
|
124
130
|
}
|
|
125
131
|
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
// the
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (bottom === 0) {
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if ((scrollTop <= 0 && y > lastY) || (scrollTop >= bottom && y < lastY)) {
|
|
132
|
+
// overscroll-behavior should prevent scroll chaining, but currently does not
|
|
133
|
+
// if the element doesn't actually overflow. https://bugs.webkit.org/show_bug.cgi?id=243452
|
|
134
|
+
// This checks that both the width and height do not overflow, otherwise we might
|
|
135
|
+
// block horizontal scrolling too. In that case, adding `touch-action: pan-x` to
|
|
136
|
+
// the element will prevent vertical page scrolling. We can't add that automatically
|
|
137
|
+
// because it must be set before the touchstart event.
|
|
138
|
+
if (scrollable.scrollHeight === scrollable.clientHeight && scrollable.scrollWidth === scrollable.clientWidth) {
|
|
139
139
|
e.preventDefault();
|
|
140
140
|
}
|
|
141
|
-
|
|
142
|
-
lastY = y;
|
|
143
141
|
};
|
|
144
142
|
|
|
145
143
|
let onTouchEnd = (e: TouchEvent) => {
|
|
@@ -148,6 +146,7 @@ function preventScrollMobileSafari() {
|
|
|
148
146
|
// Apply this change if we're not already focused on the target element
|
|
149
147
|
if (willOpenKeyboard(target) && target !== document.activeElement) {
|
|
150
148
|
e.preventDefault();
|
|
149
|
+
setupStyles();
|
|
151
150
|
|
|
152
151
|
// Apply a transform to trick Safari into thinking the input is at the top of the page
|
|
153
152
|
// so it doesn't try to scroll it into view. When tapping on an input, this needs to
|
|
@@ -158,11 +157,17 @@ function preventScrollMobileSafari() {
|
|
|
158
157
|
target.style.transform = '';
|
|
159
158
|
});
|
|
160
159
|
}
|
|
160
|
+
|
|
161
|
+
if (restoreScrollableStyles) {
|
|
162
|
+
restoreScrollableStyles();
|
|
163
|
+
}
|
|
161
164
|
};
|
|
162
165
|
|
|
163
166
|
let onFocus = (e: FocusEvent) => {
|
|
164
167
|
let target = e.target as HTMLElement;
|
|
165
168
|
if (willOpenKeyboard(target)) {
|
|
169
|
+
setupStyles();
|
|
170
|
+
|
|
166
171
|
// Transform also needs to be applied in the focus event in cases where focus moves
|
|
167
172
|
// other than tapping on an input directly, e.g. the next/previous buttons in the
|
|
168
173
|
// software keyboard. In these cases, it seems applying the transform in the focus event
|
|
@@ -190,40 +195,50 @@ function preventScrollMobileSafari() {
|
|
|
190
195
|
}
|
|
191
196
|
};
|
|
192
197
|
|
|
193
|
-
let
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
+
let restoreStyles = null;
|
|
199
|
+
let setupStyles = () => {
|
|
200
|
+
if (restoreStyles) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
198
203
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
+
let onWindowScroll = () => {
|
|
205
|
+
// Last resort. If the window scrolled, scroll it back to the top.
|
|
206
|
+
// It should always be at the top because the body will have a negative margin (see below).
|
|
207
|
+
window.scrollTo(0, 0);
|
|
208
|
+
};
|
|
204
209
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
+
// Record the original scroll position so we can restore it.
|
|
211
|
+
// Then apply a negative margin to the body to offset it by the scroll position. This will
|
|
212
|
+
// enable us to scroll the window to the top, which is required for the rest of this to work.
|
|
213
|
+
let scrollX = window.pageXOffset;
|
|
214
|
+
let scrollY = window.pageYOffset;
|
|
215
|
+
|
|
216
|
+
restoreStyles = chain(
|
|
217
|
+
addEvent(window, 'scroll', onWindowScroll),
|
|
218
|
+
setStyle(document.documentElement, 'paddingRight', `${window.innerWidth - document.documentElement.clientWidth}px`),
|
|
219
|
+
setStyle(document.documentElement, 'overflow', 'hidden'),
|
|
220
|
+
setStyle(document.body, 'marginTop', `-${scrollY}px`),
|
|
221
|
+
() => {
|
|
222
|
+
window.scrollTo(scrollX, scrollY);
|
|
223
|
+
}
|
|
224
|
+
);
|
|
210
225
|
|
|
211
|
-
|
|
212
|
-
|
|
226
|
+
// Scroll to the top. The negative margin on the body will make this appear the same.
|
|
227
|
+
window.scrollTo(0, 0);
|
|
228
|
+
};
|
|
213
229
|
|
|
214
230
|
let removeEvents = chain(
|
|
215
231
|
addEvent(document, 'touchstart', onTouchStart, {passive: false, capture: true}),
|
|
216
232
|
addEvent(document, 'touchmove', onTouchMove, {passive: false, capture: true}),
|
|
217
233
|
addEvent(document, 'touchend', onTouchEnd, {passive: false, capture: true}),
|
|
218
|
-
addEvent(document, 'focus', onFocus, true)
|
|
219
|
-
addEvent(window, 'scroll', onWindowScroll)
|
|
234
|
+
addEvent(document, 'focus', onFocus, true)
|
|
220
235
|
);
|
|
221
236
|
|
|
222
237
|
return () => {
|
|
223
238
|
// Restore styles and scroll the page back to where it was.
|
|
224
|
-
|
|
239
|
+
restoreScrollableStyles?.();
|
|
240
|
+
restoreStyles?.();
|
|
225
241
|
removeEvents();
|
|
226
|
-
window.scrollTo(scrollX, scrollY);
|
|
227
242
|
};
|
|
228
243
|
}
|
|
229
244
|
|