@react-aria/overlays 3.12.0 → 3.13.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/package.json CHANGED
@@ -1,10 +1,15 @@
1
1
  {
2
2
  "name": "@react-aria/overlays",
3
- "version": "3.12.0",
3
+ "version": "3.13.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,17 +22,17 @@
17
22
  "url": "https://github.com/adobe/react-spectrum"
18
23
  },
19
24
  "dependencies": {
20
- "@babel/runtime": "^7.6.2",
21
- "@react-aria/focus": "^3.10.0",
22
- "@react-aria/i18n": "^3.6.2",
23
- "@react-aria/interactions": "^3.13.0",
24
- "@react-aria/ssr": "^3.4.0",
25
- "@react-aria/utils": "^3.14.1",
26
- "@react-aria/visually-hidden": "^3.6.0",
27
- "@react-stately/overlays": "^3.4.3",
28
- "@react-types/button": "^3.7.0",
29
- "@react-types/overlays": "^3.6.5",
30
- "@react-types/shared": "^3.16.0"
25
+ "@react-aria/focus": "^3.11.0",
26
+ "@react-aria/i18n": "^3.7.0",
27
+ "@react-aria/interactions": "^3.14.0",
28
+ "@react-aria/ssr": "^3.5.0",
29
+ "@react-aria/utils": "^3.15.0",
30
+ "@react-aria/visually-hidden": "^3.7.0",
31
+ "@react-stately/overlays": "^3.5.0",
32
+ "@react-types/button": "^3.7.1",
33
+ "@react-types/overlays": "^3.7.0",
34
+ "@react-types/shared": "^3.17.0",
35
+ "@swc/helpers": "^0.4.14"
31
36
  },
32
37
  "peerDependencies": {
33
38
  "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0",
@@ -36,5 +41,5 @@
36
41
  "publishConfig": {
37
42
  "access": "public"
38
43
  },
39
- "gitHead": "2954307ddbefe149241685440c81f80ece6b2c83"
44
+ "gitHead": "a0efee84aa178cb1a202951dfd6d8de02b292307"
40
45
  }
@@ -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
- // Skip this node and its children if it is one of the target nodes, or a live announcer.
40
- // Also skip children of already hidden nodes, as aria-hidden is recursive. An exception is
41
- // made for elements with role="row" since VoiceOver on iOS has issues hiding elements with role="row".
42
- // For that case we want to hide the cells inside as well (https://bugs.webkit.org/show_bug.cgi?id=222623).
43
- if (
44
- visibleNodes.has(node as Element) ||
45
- (hiddenNodes.has(node.parentElement) && node.parentElement.getAttribute('role') !== 'row')
46
- ) {
47
- return NodeFilter.FILTER_REJECT;
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
- // Skip this node but continue to children if one of the targets is inside the node.
51
- if (targets.some(target => node.contains(target))) {
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
- return NodeFilter.FILTER_ACCEPT;
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
- let node = walker.nextNode() as Element;
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 (((node instanceof HTMLElement || node instanceof SVGElement) && node.dataset.liveAnnouncer === 'true')) {
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
- hide(node);
127
+ walk(node);
103
128
  }
104
129
  }
105
130
  }
@@ -115,20 +115,26 @@ export function useOverlayPosition(props: AriaPositionProps): PositionAria {
115
115
  return;
116
116
  }
117
117
 
118
- setPosition(
119
- calculatePosition({
120
- placement: translateRTL(placement, direction),
121
- overlayNode: overlayRef.current,
122
- targetNode: targetRef.current,
123
- scrollNode: scrollRef.current,
124
- padding: containerPadding,
125
- shouldFlip,
126
- boundaryElement,
127
- offset,
128
- crossOffset,
129
- maxHeight
130
- })
131
- );
118
+ let position = calculatePosition({
119
+ placement: translateRTL(placement, direction),
120
+ overlayNode: overlayRef.current,
121
+ targetNode: targetRef.current,
122
+ scrollNode: scrollRef.current,
123
+ padding: containerPadding,
124
+ shouldFlip,
125
+ boundaryElement,
126
+ offset,
127
+ crossOffset,
128
+ maxHeight
129
+ });
130
+
131
+ // Modify overlay styles directly so positioning happens immediately without the need of a second render
132
+ // This is so we don't have to delay autoFocus scrolling or delay applying preventScroll for popovers
133
+ Object.keys(position.position).forEach(key => (overlayRef.current as HTMLElement).style[key] = position.position[key] + 'px');
134
+ (overlayRef.current as HTMLElement).style.maxHeight = position.maxHeight != null ? position.maxHeight + 'px' : undefined;
135
+
136
+ // Trigger a set state for a second render anyway for arrow positioning
137
+ setPosition(position);
132
138
  // eslint-disable-next-line react-hooks/exhaustive-deps
133
139
  }, deps);
134
140
 
@@ -34,7 +34,7 @@ export interface OverlayTriggerAria {
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: RefObject<Element>): OverlayTriggerAria {
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, useState} from 'react';
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 || !isPositioned
96
+ isDisabled: isNonModal
107
97
  });
108
98
 
109
99
  useLayoutEffect(() => {
@@ -33,6 +33,10 @@ const nonTextInputTypes = new Set([
33
33
  'reset'
34
34
  ]);
35
35
 
36
+ // The number of active usePreventScroll calls. Used to determine whether to revert back to the original page style/scroll position
37
+ let preventScrollCount = 0;
38
+ let restore;
39
+
36
40
  /**
37
41
  * Prevents scrolling on the document body on mount, and
38
42
  * restores it on unmount. Also ensures that content does not
@@ -46,11 +50,21 @@ export function usePreventScroll(options: PreventScrollOptions = {}) {
46
50
  return;
47
51
  }
48
52
 
49
- if (isIOS()) {
50
- return preventScrollMobileSafari();
51
- } else {
52
- return preventScrollStandard();
53
+ preventScrollCount++;
54
+ if (preventScrollCount === 1) {
55
+ if (isIOS()) {
56
+ restore = preventScrollMobileSafari();
57
+ } else {
58
+ restore = preventScrollStandard();
59
+ }
53
60
  }
61
+
62
+ return () => {
63
+ preventScrollCount--;
64
+ if (preventScrollCount === 0) {
65
+ restore();
66
+ }
67
+ };
54
68
  }, [isDisabled]);
55
69
  }
56
70
 
@@ -183,6 +197,7 @@ function preventScrollMobileSafari() {
183
197
  // enable us to scroll the window to the top, which is required for the rest of this to work.
184
198
  let scrollX = window.pageXOffset;
185
199
  let scrollY = window.pageYOffset;
200
+
186
201
  let restoreStyles = chain(
187
202
  setStyle(document.documentElement, 'paddingRight', `${window.innerWidth - document.documentElement.clientWidth}px`),
188
203
  setStyle(document.documentElement, 'overflow', 'hidden'),
@@ -212,6 +227,7 @@ function preventScrollMobileSafari() {
212
227
  function setStyle(element: HTMLElement, style: string, value: string) {
213
228
  let cur = element.style[style];
214
229
  element.style[style] = value;
230
+
215
231
  return () => {
216
232
  element.style[style] = cur;
217
233
  };