@react-aria/overlays 3.11.0 → 3.12.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/main.js +75 -15
- package/dist/main.js.map +1 -1
- package/dist/module.js +76 -16
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +14 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +12 -12
- package/src/Overlay.tsx +6 -4
- package/src/ariaHideOutside.ts +28 -0
- package/src/useCloseOnScroll.ts +1 -1
- package/src/useOverlay.ts +8 -0
- package/src/useOverlayPosition.ts +10 -2
- package/src/usePopover.ts +47 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-aria/overlays",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.12.0",
|
|
4
4
|
"description": "Spectrum UI components in React",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "dist/main.js",
|
|
@@ -18,16 +18,16 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@babel/runtime": "^7.6.2",
|
|
21
|
-
"@react-aria/focus": "^3.
|
|
22
|
-
"@react-aria/i18n": "^3.6.
|
|
23
|
-
"@react-aria/interactions": "^3.
|
|
24
|
-
"@react-aria/ssr": "^3.
|
|
25
|
-
"@react-aria/utils": "^3.14.
|
|
26
|
-
"@react-aria/visually-hidden": "^3.
|
|
27
|
-
"@react-stately/overlays": "^3.4.
|
|
28
|
-
"@react-types/button": "^3.
|
|
29
|
-
"@react-types/overlays": "^3.6.
|
|
30
|
-
"@react-types/shared": "^3.
|
|
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"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0",
|
|
@@ -36,5 +36,5 @@
|
|
|
36
36
|
"publishConfig": {
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "2954307ddbefe149241685440c81f80ece6b2c83"
|
|
40
40
|
}
|
package/src/Overlay.tsx
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {FocusScope} from '@react-aria/focus';
|
|
14
|
-
import React, {ReactNode, useContext, useState} from 'react';
|
|
14
|
+
import React, {ReactNode, useContext, useMemo, useState} from 'react';
|
|
15
15
|
import ReactDOM from 'react-dom';
|
|
16
16
|
import {useIsSSR} from '@react-aria/ssr';
|
|
17
17
|
import {useLayoutEffect} from '@react-aria/utils';
|
|
@@ -26,7 +26,7 @@ export interface OverlayProps {
|
|
|
26
26
|
children: ReactNode
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const OverlayContext = React.createContext(null);
|
|
29
|
+
export const OverlayContext = React.createContext(null);
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* A container which renders an overlay such as a popover or modal in a portal,
|
|
@@ -36,13 +36,14 @@ export function Overlay(props: OverlayProps) {
|
|
|
36
36
|
let isSSR = useIsSSR();
|
|
37
37
|
let {portalContainer = isSSR ? null : document.body} = props;
|
|
38
38
|
let [contain, setContain] = useState(false);
|
|
39
|
+
let contextValue = useMemo(() => ({contain, setContain}), [contain, setContain]);
|
|
39
40
|
|
|
40
41
|
if (!portalContainer) {
|
|
41
42
|
return null;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
let contents = (
|
|
45
|
-
<OverlayContext.Provider value={
|
|
46
|
+
<OverlayContext.Provider value={contextValue}>
|
|
46
47
|
<FocusScope restoreFocus contain={contain}>
|
|
47
48
|
{props.children}
|
|
48
49
|
</FocusScope>
|
|
@@ -54,7 +55,8 @@ export function Overlay(props: OverlayProps) {
|
|
|
54
55
|
|
|
55
56
|
/** @private */
|
|
56
57
|
export function useOverlayFocusContain() {
|
|
57
|
-
let
|
|
58
|
+
let ctx = useContext(OverlayContext);
|
|
59
|
+
let setContain = ctx?.setContain;
|
|
58
60
|
useLayoutEffect(() => {
|
|
59
61
|
setContain?.(true);
|
|
60
62
|
}, [setContain]);
|
package/src/ariaHideOutside.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// Keeps a ref count of all hidden elements. Added to when hiding an element, and
|
|
14
14
|
// subtracted from when showing it again. When it reaches zero, aria-hidden is removed.
|
|
15
15
|
let refCountMap = new WeakMap<Element, number>();
|
|
16
|
+
let observerStack = [];
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Hides all elements in the DOM outside the given targets from screen readers using aria-hidden,
|
|
@@ -73,6 +74,12 @@ export function ariaHideOutside(targets: Element[], root = document.body) {
|
|
|
73
74
|
refCountMap.set(node, refCount + 1);
|
|
74
75
|
};
|
|
75
76
|
|
|
77
|
+
// If there is already a MutationObserver listening from a previous call,
|
|
78
|
+
// disconnect it so the new on takes over.
|
|
79
|
+
if (observerStack.length) {
|
|
80
|
+
observerStack[observerStack.length - 1].disconnect();
|
|
81
|
+
}
|
|
82
|
+
|
|
76
83
|
let node = walker.nextNode() as Element;
|
|
77
84
|
while (node != null) {
|
|
78
85
|
hide(node);
|
|
@@ -101,6 +108,17 @@ export function ariaHideOutside(targets: Element[], root = document.body) {
|
|
|
101
108
|
|
|
102
109
|
observer.observe(root, {childList: true, subtree: true});
|
|
103
110
|
|
|
111
|
+
let observerWrapper = {
|
|
112
|
+
observe() {
|
|
113
|
+
observer.observe(root, {childList: true, subtree: true});
|
|
114
|
+
},
|
|
115
|
+
disconnect() {
|
|
116
|
+
observer.disconnect();
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
observerStack.push(observerWrapper);
|
|
121
|
+
|
|
104
122
|
return () => {
|
|
105
123
|
observer.disconnect();
|
|
106
124
|
|
|
@@ -113,5 +131,15 @@ export function ariaHideOutside(targets: Element[], root = document.body) {
|
|
|
113
131
|
refCountMap.set(node, count - 1);
|
|
114
132
|
}
|
|
115
133
|
}
|
|
134
|
+
|
|
135
|
+
// Remove this observer from the stack, and start the previous one.
|
|
136
|
+
if (observerWrapper === observerStack[observerStack.length - 1]) {
|
|
137
|
+
observerStack.pop();
|
|
138
|
+
if (observerStack.length) {
|
|
139
|
+
observerStack[observerStack.length - 1].observe();
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
observerStack.splice(observerStack.indexOf(observerWrapper), 1);
|
|
143
|
+
}
|
|
116
144
|
};
|
|
117
145
|
}
|
package/src/useCloseOnScroll.ts
CHANGED
package/src/useOverlay.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {DOMAttributes} from '@react-types/shared';
|
|
14
|
+
import {isElementInChildOfActiveScope} from '@react-aria/focus';
|
|
14
15
|
import {RefObject, SyntheticEvent, useEffect} from 'react';
|
|
15
16
|
import {useFocusWithin, useInteractOutside} from '@react-aria/interactions';
|
|
16
17
|
|
|
@@ -124,6 +125,13 @@ export function useOverlay(props: AriaOverlayProps, ref: RefObject<Element>): Ov
|
|
|
124
125
|
let {focusWithinProps} = useFocusWithin({
|
|
125
126
|
isDisabled: !shouldCloseOnBlur,
|
|
126
127
|
onBlurWithin: (e) => {
|
|
128
|
+
// If focus is moving into a child focus scope (e.g. menu inside a dialog),
|
|
129
|
+
// do not close the outer overlay. At this point, the active scope should
|
|
130
|
+
// still be the outer overlay, since blur events run before focus.
|
|
131
|
+
if (e.relatedTarget && isElementInChildOfActiveScope(e.relatedTarget)) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
127
135
|
if (!shouldCloseOnInteractOutside || shouldCloseOnInteractOutside(e.relatedTarget as Element)) {
|
|
128
136
|
onClose();
|
|
129
137
|
}
|
|
@@ -15,7 +15,7 @@ import {DOMAttributes} from '@react-types/shared';
|
|
|
15
15
|
import {Placement, PlacementAxis, PositionProps} from '@react-types/overlays';
|
|
16
16
|
import {RefObject, useCallback, useRef, useState} from 'react';
|
|
17
17
|
import {useCloseOnScroll} from './useCloseOnScroll';
|
|
18
|
-
import {useLayoutEffect} from '@react-aria/utils';
|
|
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 {
|
|
@@ -129,14 +129,22 @@ export function useOverlayPosition(props: AriaPositionProps): PositionAria {
|
|
|
129
129
|
maxHeight
|
|
130
130
|
})
|
|
131
131
|
);
|
|
132
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
132
133
|
}, deps);
|
|
133
134
|
|
|
134
135
|
// Update position when anything changes
|
|
136
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
135
137
|
useLayoutEffect(updatePosition, deps);
|
|
136
138
|
|
|
137
139
|
// Update position on window resize
|
|
138
140
|
useResize(updatePosition);
|
|
139
141
|
|
|
142
|
+
// Update position when the overlay changes size (might need to flip).
|
|
143
|
+
useResizeObserver({
|
|
144
|
+
ref: overlayRef,
|
|
145
|
+
onResize: updatePosition
|
|
146
|
+
});
|
|
147
|
+
|
|
140
148
|
// Reposition the overlay and do not close on scroll while the visual viewport is resizing.
|
|
141
149
|
// This will ensure that overlays adjust their positioning when the iOS virtual keyboard appears.
|
|
142
150
|
let isResizing = useRef(false);
|
|
@@ -171,7 +179,7 @@ export function useOverlayPosition(props: AriaPositionProps): PositionAria {
|
|
|
171
179
|
useCloseOnScroll({
|
|
172
180
|
triggerRef: targetRef,
|
|
173
181
|
isOpen,
|
|
174
|
-
onClose: onClose
|
|
182
|
+
onClose: onClose && close
|
|
175
183
|
});
|
|
176
184
|
|
|
177
185
|
return {
|
package/src/usePopover.ts
CHANGED
|
@@ -11,15 +11,16 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {ariaHideOutside} from './ariaHideOutside';
|
|
14
|
+
import {AriaPositionProps, useOverlayPosition} from './useOverlayPosition';
|
|
14
15
|
import {DOMAttributes} from '@react-types/shared';
|
|
15
|
-
import {mergeProps} from '@react-aria/utils';
|
|
16
|
+
import {mergeProps, useLayoutEffect} from '@react-aria/utils';
|
|
16
17
|
import {OverlayTriggerState} from '@react-stately/overlays';
|
|
17
|
-
import {
|
|
18
|
-
import {RefObject,
|
|
18
|
+
import {PlacementAxis} from '@react-types/overlays';
|
|
19
|
+
import {RefObject, useState} from 'react';
|
|
19
20
|
import {useOverlay} from './useOverlay';
|
|
20
|
-
import {
|
|
21
|
+
import {usePreventScroll} from './usePreventScroll';
|
|
21
22
|
|
|
22
|
-
export interface AriaPopoverProps extends Omit<
|
|
23
|
+
export interface AriaPopoverProps extends Omit<AriaPositionProps, 'isOpen' | 'onClose' | 'targetRef' | 'overlayRef'> {
|
|
23
24
|
/**
|
|
24
25
|
* The ref for the element which the popover positions itself with respect to.
|
|
25
26
|
*/
|
|
@@ -36,14 +37,27 @@ export interface AriaPopoverProps extends Omit<PositionProps, 'isOpen'> {
|
|
|
36
37
|
* reader experience. Only use with components such as combobox, which are designed
|
|
37
38
|
* to handle this situation carefully.
|
|
38
39
|
*/
|
|
39
|
-
isNonModal?: boolean
|
|
40
|
+
isNonModal?: boolean,
|
|
41
|
+
/**
|
|
42
|
+
* Whether pressing the escape key to close the popover should be disabled.
|
|
43
|
+
*
|
|
44
|
+
* Most popovers should not use this option. When set to true, an alternative
|
|
45
|
+
* way to close the popover with a keyboard must be provided.
|
|
46
|
+
*
|
|
47
|
+
* @default false
|
|
48
|
+
*/
|
|
49
|
+
isKeyboardDismissDisabled?: boolean
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
export interface PopoverAria {
|
|
43
53
|
/** Props for the popover element. */
|
|
44
54
|
popoverProps: DOMAttributes,
|
|
45
55
|
/** Props for the popover tip arrow if any. */
|
|
46
|
-
arrowProps: DOMAttributes
|
|
56
|
+
arrowProps: DOMAttributes,
|
|
57
|
+
/** Props to apply to the underlay element, if any. */
|
|
58
|
+
underlayProps: DOMAttributes,
|
|
59
|
+
/** Placement of the popover with respect to the trigger. */
|
|
60
|
+
placement: PlacementAxis
|
|
47
61
|
}
|
|
48
62
|
|
|
49
63
|
/**
|
|
@@ -55,34 +69,53 @@ export function usePopover(props: AriaPopoverProps, state: OverlayTriggerState):
|
|
|
55
69
|
triggerRef,
|
|
56
70
|
popoverRef,
|
|
57
71
|
isNonModal,
|
|
72
|
+
isKeyboardDismissDisabled,
|
|
58
73
|
...otherProps
|
|
59
74
|
} = props;
|
|
60
75
|
|
|
61
|
-
let {overlayProps} = useOverlay(
|
|
76
|
+
let {overlayProps, underlayProps} = useOverlay(
|
|
62
77
|
{
|
|
63
78
|
isOpen: state.isOpen,
|
|
64
79
|
onClose: state.close,
|
|
65
80
|
shouldCloseOnBlur: true,
|
|
66
|
-
isDismissable:
|
|
81
|
+
isDismissable: !isNonModal,
|
|
82
|
+
isKeyboardDismissDisabled
|
|
67
83
|
},
|
|
68
84
|
popoverRef
|
|
69
85
|
);
|
|
70
86
|
|
|
71
|
-
let {overlayProps: positionProps, arrowProps} = useOverlayPosition({
|
|
87
|
+
let {overlayProps: positionProps, arrowProps, placement} = useOverlayPosition({
|
|
72
88
|
...otherProps,
|
|
73
89
|
targetRef: triggerRef,
|
|
74
90
|
overlayRef: popoverRef,
|
|
75
|
-
isOpen: state.isOpen
|
|
91
|
+
isOpen: state.isOpen,
|
|
92
|
+
onClose: null
|
|
93
|
+
});
|
|
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
|
+
usePreventScroll({
|
|
106
|
+
isDisabled: isNonModal || !isPositioned
|
|
76
107
|
});
|
|
77
108
|
|
|
78
|
-
|
|
79
|
-
if (state.isOpen && !isNonModal) {
|
|
109
|
+
useLayoutEffect(() => {
|
|
110
|
+
if (state.isOpen && !isNonModal && popoverRef.current) {
|
|
80
111
|
return ariaHideOutside([popoverRef.current]);
|
|
81
112
|
}
|
|
82
113
|
}, [isNonModal, state.isOpen, popoverRef]);
|
|
83
114
|
|
|
84
115
|
return {
|
|
85
116
|
popoverProps: mergeProps(overlayProps, positionProps),
|
|
86
|
-
arrowProps
|
|
117
|
+
arrowProps,
|
|
118
|
+
underlayProps,
|
|
119
|
+
placement
|
|
87
120
|
};
|
|
88
121
|
}
|