@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.
Files changed (38) hide show
  1. package/dist/ariaHideOutside.main.js +55 -4
  2. package/dist/ariaHideOutside.main.js.map +1 -1
  3. package/dist/ariaHideOutside.mjs +56 -5
  4. package/dist/ariaHideOutside.module.js +56 -5
  5. package/dist/ariaHideOutside.module.js.map +1 -1
  6. package/dist/calculatePosition.main.js +29 -5
  7. package/dist/calculatePosition.main.js.map +1 -1
  8. package/dist/calculatePosition.mjs +30 -6
  9. package/dist/calculatePosition.module.js +30 -6
  10. package/dist/calculatePosition.module.js.map +1 -1
  11. package/dist/types.d.ts.map +1 -1
  12. package/dist/useCloseOnScroll.main.js +5 -3
  13. package/dist/useCloseOnScroll.main.js.map +1 -1
  14. package/dist/useCloseOnScroll.mjs +5 -3
  15. package/dist/useCloseOnScroll.module.js +5 -3
  16. package/dist/useCloseOnScroll.module.js.map +1 -1
  17. package/dist/useOverlay.main.js +11 -5
  18. package/dist/useOverlay.main.js.map +1 -1
  19. package/dist/useOverlay.mjs +12 -6
  20. package/dist/useOverlay.module.js +12 -6
  21. package/dist/useOverlay.module.js.map +1 -1
  22. package/dist/useOverlayPosition.main.js +7 -6
  23. package/dist/useOverlayPosition.main.js.map +1 -1
  24. package/dist/useOverlayPosition.mjs +7 -6
  25. package/dist/useOverlayPosition.module.js +7 -6
  26. package/dist/useOverlayPosition.module.js.map +1 -1
  27. package/dist/usePreventScroll.main.js +11 -4
  28. package/dist/usePreventScroll.main.js.map +1 -1
  29. package/dist/usePreventScroll.mjs +12 -5
  30. package/dist/usePreventScroll.module.js +12 -5
  31. package/dist/usePreventScroll.module.js.map +1 -1
  32. package/package.json +12 -11
  33. package/src/ariaHideOutside.ts +88 -4
  34. package/src/calculatePosition.ts +29 -7
  35. package/src/useCloseOnScroll.ts +4 -3
  36. package/src/useOverlay.ts +14 -6
  37. package/src/useOverlayPosition.ts +6 -5
  38. package/src/usePreventScroll.ts +21 -7
@@ -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
- let isContainerDescendentOfBoundary = boundaryElement.contains(container);
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,
@@ -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.target;
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.contains(triggerRef.current))) {
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 (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
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
- if (!shouldCloseOnInteractOutside || shouldCloseOnInteractOutside(e.target as Element)) {
95
- if (visibleOverlays[visibleOverlays.length - 1] === ref) {
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.target as Element)) {
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
- onHide();
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.target === e.currentTarget) {
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.contains(document.activeElement)) {
158
- let anchorRect = document.activeElement?.getBoundingClientRect();
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
- if (anchor && document.activeElement && scrollRef.current) {
212
- let anchorRect = document.activeElement.getBoundingClientRect();
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;
@@ -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.target as Element;
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.target as HTMLElement;
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 wasKeyboardVisible = document.activeElement != null && willOpenKeyboard(document.activeElement);
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;