@react-aria/overlays 3.31.0 → 3.31.2
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/ariaHideOutside.main.js +55 -4
- package/dist/ariaHideOutside.main.js.map +1 -1
- package/dist/ariaHideOutside.mjs +56 -5
- package/dist/ariaHideOutside.module.js +56 -5
- package/dist/ariaHideOutside.module.js.map +1 -1
- package/dist/calculatePosition.main.js +29 -5
- package/dist/calculatePosition.main.js.map +1 -1
- package/dist/calculatePosition.mjs +30 -6
- package/dist/calculatePosition.module.js +30 -6
- package/dist/calculatePosition.module.js.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/useCloseOnScroll.main.js +5 -3
- package/dist/useCloseOnScroll.main.js.map +1 -1
- package/dist/useCloseOnScroll.mjs +5 -3
- package/dist/useCloseOnScroll.module.js +5 -3
- package/dist/useCloseOnScroll.module.js.map +1 -1
- package/dist/useOverlay.main.js +11 -5
- package/dist/useOverlay.main.js.map +1 -1
- package/dist/useOverlay.mjs +12 -6
- package/dist/useOverlay.module.js +12 -6
- package/dist/useOverlay.module.js.map +1 -1
- package/dist/useOverlayPosition.main.js +7 -6
- package/dist/useOverlayPosition.main.js.map +1 -1
- package/dist/useOverlayPosition.mjs +7 -6
- package/dist/useOverlayPosition.module.js +7 -6
- package/dist/useOverlayPosition.module.js.map +1 -1
- package/dist/usePreventScroll.main.js +11 -4
- package/dist/usePreventScroll.main.js.map +1 -1
- package/dist/usePreventScroll.mjs +12 -5
- package/dist/usePreventScroll.module.js +12 -5
- package/dist/usePreventScroll.module.js.map +1 -1
- package/package.json +12 -11
- package/src/ariaHideOutside.ts +88 -4
- package/src/calculatePosition.ts +29 -7
- package/src/useCloseOnScroll.ts +4 -3
- package/src/useOverlay.ts +14 -6
- package/src/useOverlayPosition.ts +6 -5
- package/src/usePreventScroll.ts +21 -7
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, isWebKit} from '@react-aria/utils';
|
|
14
|
+
import {clamp, isWebKit, nodeContains} from '@react-aria/utils';
|
|
15
15
|
|
|
16
16
|
interface Position {
|
|
17
17
|
top?: number,
|
|
@@ -309,7 +309,6 @@ function getMaxHeight(
|
|
|
309
309
|
top: Math.max(boundaryDimensions.top + boundaryToContainerTransformOffset, (visualViewport?.offsetTop ?? boundaryDimensions.top) + boundaryToContainerTransformOffset),
|
|
310
310
|
bottom: Math.min((boundaryDimensions.top + boundaryDimensions.height + boundaryToContainerTransformOffset), (visualViewport?.offsetTop ?? 0) + (visualViewport?.height ?? 0))
|
|
311
311
|
};
|
|
312
|
-
|
|
313
312
|
let maxHeight = heightGrowthDirection !== 'top' ?
|
|
314
313
|
// We want the distance between the top of the overlay to the bottom of the boundary
|
|
315
314
|
Math.max(0,
|
|
@@ -558,12 +557,35 @@ export function calculatePosition(opts: PositionOpts): PositionResult {
|
|
|
558
557
|
// Otherwise this returns the height/width of a arbitrary boundary element, and its top/left with respect to the viewport (NOTE THIS MEANS IT DOESNT INCLUDE SCROLL)
|
|
559
558
|
let boundaryDimensions = getContainerDimensions(boundaryElement, visualViewport);
|
|
560
559
|
let containerDimensions = getContainerDimensions(container, visualViewport);
|
|
561
|
-
// If the container is the HTML element wrapping the body element, the retrieved scrollTop/scrollLeft will be equal to the
|
|
562
|
-
// 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
|
|
563
|
-
// by the container scroll since they are essentially the same containing element and thus in the same coordinate system
|
|
564
|
-
let containerOffsetWithBoundary: Offset = getPosition(boundaryElement, container, false);
|
|
565
560
|
|
|
566
|
-
|
|
561
|
+
// There are several difference cases of how to calculate the containerOffsetWithBoundary:
|
|
562
|
+
// - boundaryElement is body or HTML and the container is an arbitrary element in the boundary (aka submenu with parent menu as container in v3)
|
|
563
|
+
// - boundaryElement and container are both body or HTML element (aka standard popover case)
|
|
564
|
+
// - boundaryElement is customized by the user. Container can also be arbitrary (either body/HTML or some other element)
|
|
565
|
+
// containerOffsetWithBoundary should always return a value that is the boundary's coordinate offset with respect to the container coord system (container is 0, 0)
|
|
566
|
+
let containerOffsetWithBoundary: Offset;
|
|
567
|
+
if ((boundaryElement.tagName === 'BODY' || boundaryElement.tagName === 'HTML') && !isViewportContainer) {
|
|
568
|
+
// Use getRect instead of getOffset because boundaryDimensions for BODY/HTML is in viewport coordinate space,
|
|
569
|
+
// not document coordinate space
|
|
570
|
+
let containerRect = getRect(container, false);
|
|
571
|
+
// the offset should be negative because if container is at viewport position x,y, then viewport top (aka 0)
|
|
572
|
+
// is at position -x,y in container-relative coordinates
|
|
573
|
+
containerOffsetWithBoundary = {
|
|
574
|
+
top: -(containerRect.top - boundaryDimensions.top),
|
|
575
|
+
left: -(containerRect.left - boundaryDimensions.left),
|
|
576
|
+
width: 0,
|
|
577
|
+
height: 0
|
|
578
|
+
};
|
|
579
|
+
} else if ((boundaryElement.tagName === 'BODY' || boundaryElement.tagName === 'HTML') && isViewportContainer) {
|
|
580
|
+
// both are the same viewport container, no offset needed
|
|
581
|
+
containerOffsetWithBoundary = {top: 0, left: 0, width: 0, height: 0};
|
|
582
|
+
} else {
|
|
583
|
+
// This returns the boundary's coordinate with respect to the container. This case captures cases such as when you provide a custom boundary
|
|
584
|
+
// like in ScrollingBoundaryContainerExample in Popover.stories.
|
|
585
|
+
containerOffsetWithBoundary = getPosition(boundaryElement, container, false);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
let isContainerDescendentOfBoundary = nodeContains(boundaryElement, container);
|
|
567
589
|
return calculatePositionInternal(
|
|
568
590
|
placement,
|
|
569
591
|
childOffset,
|
package/src/useCloseOnScroll.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import {getEventTarget, nodeContains} from '@react-aria/utils';
|
|
13
14
|
import {RefObject} from '@react-types/shared';
|
|
14
15
|
import {useEffect} from 'react';
|
|
15
16
|
|
|
@@ -37,16 +38,16 @@ export function useCloseOnScroll(opts: CloseOnScrollOptions): void {
|
|
|
37
38
|
|
|
38
39
|
let onScroll = (e: Event) => {
|
|
39
40
|
// Ignore if scrolling an scrollable region outside the trigger's tree.
|
|
40
|
-
let target = e
|
|
41
|
+
let target = getEventTarget(e);
|
|
41
42
|
// window is not a Node and doesn't have contain, but window contains everything
|
|
42
|
-
if (!triggerRef.current || ((target instanceof Node) && !target
|
|
43
|
+
if (!triggerRef.current || ((target instanceof Node) && !nodeContains(target, triggerRef.current))) {
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
// Ignore scroll events on any input or textarea as the cursor position can cause it to scroll
|
|
47
48
|
// such as in a combobox. Clicking the dropdown button places focus on the input, and if the
|
|
48
49
|
// text inside the input extends beyond the 'end', then it will scroll so the cursor is visible at the end.
|
|
49
|
-
if (
|
|
50
|
+
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
|
|
50
51
|
return;
|
|
51
52
|
}
|
|
52
53
|
|
package/src/useOverlay.ts
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {DOMAttributes, RefObject} from '@react-types/shared';
|
|
14
|
+
import {getEventTarget} from '@react-aria/utils';
|
|
14
15
|
import {isElementInChildOfActiveScope} from '@react-aria/focus';
|
|
15
|
-
import {useEffect} from 'react';
|
|
16
|
+
import {useEffect, useRef} from 'react';
|
|
16
17
|
import {useFocusWithin, useInteractOutside} from '@react-aria/interactions';
|
|
17
18
|
|
|
18
19
|
export interface AriaOverlayProps {
|
|
@@ -70,6 +71,8 @@ export function useOverlay(props: AriaOverlayProps, ref: RefObject<Element | nul
|
|
|
70
71
|
shouldCloseOnInteractOutside
|
|
71
72
|
} = props;
|
|
72
73
|
|
|
74
|
+
let lastVisibleOverlay = useRef<RefObject<Element | null>>(undefined);
|
|
75
|
+
|
|
73
76
|
// Add the overlay ref to the stack of visible overlays on mount, and remove on unmount.
|
|
74
77
|
useEffect(() => {
|
|
75
78
|
if (isOpen && !visibleOverlays.includes(ref)) {
|
|
@@ -91,8 +94,10 @@ export function useOverlay(props: AriaOverlayProps, ref: RefObject<Element | nul
|
|
|
91
94
|
};
|
|
92
95
|
|
|
93
96
|
let onInteractOutsideStart = (e: PointerEvent) => {
|
|
94
|
-
|
|
95
|
-
|
|
97
|
+
const topMostOverlay = visibleOverlays[visibleOverlays.length - 1];
|
|
98
|
+
lastVisibleOverlay.current = topMostOverlay;
|
|
99
|
+
if (!shouldCloseOnInteractOutside || shouldCloseOnInteractOutside(getEventTarget(e) as Element)) {
|
|
100
|
+
if (topMostOverlay === ref) {
|
|
96
101
|
e.stopPropagation();
|
|
97
102
|
e.preventDefault();
|
|
98
103
|
}
|
|
@@ -100,13 +105,16 @@ export function useOverlay(props: AriaOverlayProps, ref: RefObject<Element | nul
|
|
|
100
105
|
};
|
|
101
106
|
|
|
102
107
|
let onInteractOutside = (e: PointerEvent) => {
|
|
103
|
-
if (!shouldCloseOnInteractOutside || shouldCloseOnInteractOutside(e
|
|
108
|
+
if (!shouldCloseOnInteractOutside || shouldCloseOnInteractOutside(getEventTarget(e) as Element)) {
|
|
104
109
|
if (visibleOverlays[visibleOverlays.length - 1] === ref) {
|
|
105
110
|
e.stopPropagation();
|
|
106
111
|
e.preventDefault();
|
|
107
112
|
}
|
|
108
|
-
|
|
113
|
+
if (lastVisibleOverlay.current === ref) {
|
|
114
|
+
onHide();
|
|
115
|
+
}
|
|
109
116
|
}
|
|
117
|
+
lastVisibleOverlay.current = undefined;
|
|
110
118
|
};
|
|
111
119
|
|
|
112
120
|
// Handle the escape key
|
|
@@ -145,7 +153,7 @@ export function useOverlay(props: AriaOverlayProps, ref: RefObject<Element | nul
|
|
|
145
153
|
|
|
146
154
|
let onPointerDownUnderlay = e => {
|
|
147
155
|
// fixes a firefox issue that starts text selection https://bugzilla.mozilla.org/show_bug.cgi?id=1675846
|
|
148
|
-
if (e
|
|
156
|
+
if (getEventTarget(e) === e.currentTarget) {
|
|
149
157
|
e.preventDefault();
|
|
150
158
|
}
|
|
151
159
|
};
|
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
|
|
13
13
|
import {calculatePosition, getRect, PositionResult} from './calculatePosition';
|
|
14
14
|
import {DOMAttributes, RefObject} from '@react-types/shared';
|
|
15
|
+
import {getActiveElement, isFocusWithin, useLayoutEffect, useResizeObserver} from '@react-aria/utils';
|
|
15
16
|
import {Placement, PlacementAxis, PositionProps} from '@react-types/overlays';
|
|
16
17
|
import {useCallback, useEffect, useRef, useState} from 'react';
|
|
17
18
|
import {useCloseOnScroll} from './useCloseOnScroll';
|
|
18
|
-
import {useLayoutEffect, useResizeObserver} from '@react-aria/utils';
|
|
19
19
|
import {useLocale} from '@react-aria/i18n';
|
|
20
20
|
|
|
21
21
|
export interface AriaPositionProps extends PositionProps {
|
|
@@ -154,8 +154,8 @@ export function useOverlayPosition(props: AriaPositionProps): PositionAria {
|
|
|
154
154
|
// so it can be restored after repositioning. This way if the overlay height
|
|
155
155
|
// changes, the focused element appears to stay in the same position.
|
|
156
156
|
let anchor: ScrollAnchor | null = null;
|
|
157
|
-
if (scrollRef.current && scrollRef.current
|
|
158
|
-
let anchorRect =
|
|
157
|
+
if (scrollRef.current && isFocusWithin(scrollRef.current)) {
|
|
158
|
+
let anchorRect = getActiveElement()?.getBoundingClientRect();
|
|
159
159
|
let scrollRect = scrollRef.current.getBoundingClientRect();
|
|
160
160
|
// Anchor from the top if the offset is in the top half of the scrollable element,
|
|
161
161
|
// otherwise anchor from the bottom.
|
|
@@ -208,8 +208,9 @@ export function useOverlayPosition(props: AriaPositionProps): PositionAria {
|
|
|
208
208
|
overlay.style.maxHeight = position.maxHeight != null ? position.maxHeight + 'px' : '';
|
|
209
209
|
|
|
210
210
|
// Restore scroll position relative to anchor element.
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
let activeElement = getActiveElement();
|
|
212
|
+
if (anchor && activeElement && scrollRef.current) {
|
|
213
|
+
let anchorRect = activeElement.getBoundingClientRect();
|
|
213
214
|
let scrollRect = scrollRef.current.getBoundingClientRect();
|
|
214
215
|
let newOffset = anchorRect[anchor.type] - scrollRect[anchor.type];
|
|
215
216
|
scrollRef.current.scrollTop += newOffset - anchor.offset;
|
package/src/usePreventScroll.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {chain, getScrollParent, isIOS, isScrollable, useLayoutEffect, willOpenKeyboard} from '@react-aria/utils';
|
|
13
|
+
import {chain, getActiveElement, getEventTarget, getScrollParent, isIOS, isScrollable, useLayoutEffect, willOpenKeyboard} from '@react-aria/utils';
|
|
14
14
|
|
|
15
15
|
interface PreventScrollOptions {
|
|
16
16
|
/** Whether the scroll lock is disabled. */
|
|
@@ -88,27 +88,39 @@ function preventScrollStandard() {
|
|
|
88
88
|
// by preventing default in a `touchmove` event. This is best effort: we can't prevent default when pinch
|
|
89
89
|
// zooming or when an element contains text selection, which may allow scrolling in some cases.
|
|
90
90
|
// 3. Prevent default on `touchend` events on input elements and handle focusing the element ourselves.
|
|
91
|
-
// 4. When focus moves to an input, create an off screen input and focus that temporarily. This prevents
|
|
91
|
+
// 4. When focus moves to an input, create an off screen input and focus that temporarily. This prevents
|
|
92
92
|
// Safari from scrolling the page. After a small delay, focus the real input and scroll it into view
|
|
93
93
|
// ourselves, without scrolling the whole page.
|
|
94
94
|
function preventScrollMobileSafari() {
|
|
95
|
+
// Set overflow hidden so scrollIntoViewport() (useSelectableCollection) sees isScrollPrevented and
|
|
96
|
+
// scrolls only scroll parents instead of calling native scrollIntoView() which moves the window.
|
|
97
|
+
let restoreOverflow = setStyle(document.documentElement, 'overflow', 'hidden');
|
|
98
|
+
|
|
95
99
|
let scrollable: Element;
|
|
96
100
|
let allowTouchMove = false;
|
|
97
101
|
let onTouchStart = (e: TouchEvent) => {
|
|
98
102
|
// Store the nearest scrollable parent element from the element that the user touched.
|
|
99
|
-
let target = e
|
|
103
|
+
let target = getEventTarget(e) as Element;
|
|
100
104
|
scrollable = isScrollable(target) ? target : getScrollParent(target, true);
|
|
101
105
|
allowTouchMove = false;
|
|
102
|
-
|
|
106
|
+
|
|
103
107
|
// If the target is selected, don't preventDefault in touchmove to allow user to adjust selection.
|
|
104
108
|
let selection = target.ownerDocument.defaultView!.getSelection();
|
|
105
109
|
if (selection && !selection.isCollapsed && selection.containsNode(target, true)) {
|
|
106
110
|
allowTouchMove = true;
|
|
107
111
|
}
|
|
108
112
|
|
|
113
|
+
// If this is a range input, allow touch move to allow user to adjust the slider value
|
|
114
|
+
if (e.composedPath().some((el) =>
|
|
115
|
+
el instanceof HTMLInputElement &&
|
|
116
|
+
el.type === 'range'
|
|
117
|
+
)) {
|
|
118
|
+
allowTouchMove = true;
|
|
119
|
+
}
|
|
120
|
+
|
|
109
121
|
// If this is a focused input element with a selected range, allow user to drag the selection handles.
|
|
110
122
|
if (
|
|
111
|
-
'selectionStart' in target &&
|
|
123
|
+
'selectionStart' in target &&
|
|
112
124
|
'selectionEnd' in target &&
|
|
113
125
|
(target.selectionStart as number) < (target.selectionEnd as number) &&
|
|
114
126
|
target.ownerDocument.activeElement === target
|
|
@@ -154,7 +166,7 @@ function preventScrollMobileSafari() {
|
|
|
154
166
|
};
|
|
155
167
|
|
|
156
168
|
let onBlur = (e: FocusEvent) => {
|
|
157
|
-
let target = e
|
|
169
|
+
let target = getEventTarget(e) as HTMLElement;
|
|
158
170
|
let relatedTarget = e.relatedTarget as HTMLElement | null;
|
|
159
171
|
if (relatedTarget && willOpenKeyboard(relatedTarget)) {
|
|
160
172
|
// Focus without scrolling the whole page, and then scroll into view manually.
|
|
@@ -175,7 +187,8 @@ function preventScrollMobileSafari() {
|
|
|
175
187
|
let focus = HTMLElement.prototype.focus;
|
|
176
188
|
HTMLElement.prototype.focus = function (opts) {
|
|
177
189
|
// Track whether the keyboard was already visible before.
|
|
178
|
-
let
|
|
190
|
+
let activeElement = getActiveElement();
|
|
191
|
+
let wasKeyboardVisible = activeElement != null && willOpenKeyboard(activeElement);
|
|
179
192
|
|
|
180
193
|
// Focus the element without scrolling the page.
|
|
181
194
|
focus.call(this, {...opts, preventScroll: true});
|
|
@@ -192,6 +205,7 @@ function preventScrollMobileSafari() {
|
|
|
192
205
|
);
|
|
193
206
|
|
|
194
207
|
return () => {
|
|
208
|
+
restoreOverflow();
|
|
195
209
|
removeEvents();
|
|
196
210
|
style.remove();
|
|
197
211
|
HTMLElement.prototype.focus = focus;
|