@react-aria/overlays 3.27.3 → 3.29.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.
Files changed (39) hide show
  1. package/dist/Overlay.main.js.map +1 -1
  2. package/dist/Overlay.module.js.map +1 -1
  3. package/dist/PortalProvider.main.js.map +1 -1
  4. package/dist/PortalProvider.module.js.map +1 -1
  5. package/dist/ariaHideOutside.main.js +34 -16
  6. package/dist/ariaHideOutside.main.js.map +1 -1
  7. package/dist/ariaHideOutside.mjs +34 -16
  8. package/dist/ariaHideOutside.module.js +34 -16
  9. package/dist/ariaHideOutside.module.js.map +1 -1
  10. package/dist/calculatePosition.main.js +15 -2
  11. package/dist/calculatePosition.main.js.map +1 -1
  12. package/dist/calculatePosition.mjs +15 -2
  13. package/dist/calculatePosition.module.js +15 -2
  14. package/dist/calculatePosition.module.js.map +1 -1
  15. package/dist/types.d.ts +21 -1
  16. package/dist/types.d.ts.map +1 -1
  17. package/dist/useModalOverlay.main.js +3 -1
  18. package/dist/useModalOverlay.main.js.map +1 -1
  19. package/dist/useModalOverlay.mjs +3 -1
  20. package/dist/useModalOverlay.module.js +3 -1
  21. package/dist/useModalOverlay.module.js.map +1 -1
  22. package/dist/useOverlayPosition.main.js +15 -4
  23. package/dist/useOverlayPosition.main.js.map +1 -1
  24. package/dist/useOverlayPosition.mjs +15 -4
  25. package/dist/useOverlayPosition.module.js +15 -4
  26. package/dist/useOverlayPosition.module.js.map +1 -1
  27. package/dist/usePopover.main.js +9 -4
  28. package/dist/usePopover.main.js.map +1 -1
  29. package/dist/usePopover.mjs +10 -5
  30. package/dist/usePopover.module.js +10 -5
  31. package/dist/usePopover.module.js.map +1 -1
  32. package/package.json +12 -12
  33. package/src/Overlay.tsx +2 -1
  34. package/src/PortalProvider.tsx +1 -1
  35. package/src/ariaHideOutside.ts +38 -13
  36. package/src/calculatePosition.ts +22 -2
  37. package/src/useModalOverlay.ts +1 -1
  38. package/src/useOverlayPosition.ts +25 -3
  39. package/src/usePopover.ts +12 -6
@@ -1 +1 @@
1
- {"mappings":";;;;;;AAAA;;;;;;;;;;CAUC;;;;;AAoEM,SAAS,0CAAW,KAAuB,EAAE,KAA0B;IAC5E,IAAI,cACF,UAAU,cACV,UAAU,YACV,QAAQ,cACR,UAAU,6BACV,yBAAyB,gCACzB,4BAA4B,EAC5B,GAAG,YACJ,GAAG;IAEJ,IAAI,YAAY,UAAU,CAAC,UAAU,KAAK;IAE1C,IAAI,gBAAC,YAAY,iBAAE,aAAa,EAAC,GAAG,CAAA,GAAA,yCAAS,EAC3C;QACE,QAAQ,MAAM,MAAM;QACpB,SAAS,MAAM,KAAK;QACpB,mBAAmB;QACnB,eAAe,CAAC,cAAc;mCAC9B;sCACA;IACF,GACA,qBAAA,sBAAA,WAAY;IAGd,IAAI,EAAC,cAAc,aAAa,cAAE,UAAU,aAAE,SAAS,EAAC,GAAG,CAAA,GAAA,yCAAiB,EAAE;QAC5E,GAAG,UAAU;QACb,WAAW;QACX,YAAY;QACZ,QAAQ,MAAM,MAAM;QACpB,SAAS,cAAc,CAAC,YAAY,MAAM,KAAK,GAAG;IACpD;IAEA,CAAA,GAAA,yCAAe,EAAE;QACf,YAAY,cAAc,CAAC,MAAM,MAAM;IACzC;IAEA,CAAA,GAAA,sBAAc,EAAE;QACd,IAAI,MAAM,MAAM,IAAI,WAAW,OAAO,EAAE;gBAEjB,mBAEK;YAH1B,IAAI,YACF,OAAO,CAAA,GAAA,yCAAU,EAAE,CAAA,oBAAA,qBAAA,+BAAA,SAAU,OAAO,cAAjB,+BAAA,oBAAqB,WAAW,OAAO;iBAE1D,OAAO,CAAA,GAAA,yCAAc,EAAE;gBAAC,CAAA,qBAAA,qBAAA,+BAAA,SAAU,OAAO,cAAjB,gCAAA,qBAAqB,WAAW,OAAO;aAAC;QAEpE;IACF,GAAG;QAAC;QAAY,MAAM,MAAM;QAAE;QAAY;KAAS;IAEnD,OAAO;QACL,cAAc,CAAA,GAAA,iBAAS,EAAE,cAAc;oBACvC;uBACA;mBACA;IACF;AACF","sources":["packages/@react-aria/overlays/src/usePopover.ts"],"sourcesContent":["/*\n * Copyright 2022 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 */\n\nimport {ariaHideOutside, keepVisible} from './ariaHideOutside';\nimport {AriaPositionProps, useOverlayPosition} from './useOverlayPosition';\nimport {DOMAttributes, RefObject} from '@react-types/shared';\nimport {mergeProps, useLayoutEffect} from '@react-aria/utils';\nimport {OverlayTriggerState} from '@react-stately/overlays';\nimport {PlacementAxis} from '@react-types/overlays';\nimport {useOverlay} from './useOverlay';\nimport {usePreventScroll} from './usePreventScroll';\n\nexport interface AriaPopoverProps extends Omit<AriaPositionProps, 'isOpen' | 'onClose' | 'targetRef' | 'overlayRef'> {\n /**\n * The ref for the element which the popover positions itself with respect to.\n */\n triggerRef: RefObject<Element | null>,\n /**\n * The ref for the popover element.\n */\n popoverRef: RefObject<Element | null>,\n /**\n * An optional ref for a group of popovers, e.g. submenus.\n * When provided, this element is used to detect outside interactions\n * and hiding elements from assistive technologies instead of the popoverRef.\n */\n groupRef?: RefObject<Element | null>,\n /**\n * Whether the popover is non-modal, i.e. elements outside the popover may be\n * interacted with by assistive technologies.\n *\n * Most popovers should not use this option as it may negatively impact the screen\n * reader experience. Only use with components such as combobox, which are designed\n * to handle this situation carefully.\n */\n isNonModal?: boolean,\n /**\n * Whether pressing the escape key to close the popover should be disabled.\n *\n * Most popovers should not use this option. When set to true, an alternative\n * way to close the popover with a keyboard must be provided.\n *\n * @default false\n */\n isKeyboardDismissDisabled?: boolean,\n /**\n * When user interacts with the argument element outside of the popover ref,\n * return true if onClose should be called. This gives you a chance to filter\n * out interaction with elements that should not dismiss the popover.\n * By default, onClose will always be called on interaction outside the popover ref.\n */\n shouldCloseOnInteractOutside?: (element: Element) => boolean\n}\n\nexport interface PopoverAria {\n /** Props for the popover element. */\n popoverProps: DOMAttributes,\n /** Props for the popover tip arrow if any. */\n arrowProps: DOMAttributes,\n /** Props to apply to the underlay element, if any. */\n underlayProps: DOMAttributes,\n /** Placement of the popover with respect to the trigger. */\n placement: PlacementAxis | null\n}\n\n/**\n * Provides the behavior and accessibility implementation for a popover component.\n * A popover is an overlay element positioned relative to a trigger.\n */\nexport function usePopover(props: AriaPopoverProps, state: OverlayTriggerState): PopoverAria {\n let {\n triggerRef,\n popoverRef,\n groupRef,\n isNonModal,\n isKeyboardDismissDisabled,\n shouldCloseOnInteractOutside,\n ...otherProps\n } = props;\n\n let isSubmenu = otherProps['trigger'] === 'SubmenuTrigger';\n\n let {overlayProps, underlayProps} = useOverlay(\n {\n isOpen: state.isOpen,\n onClose: state.close,\n shouldCloseOnBlur: true,\n isDismissable: !isNonModal || isSubmenu,\n isKeyboardDismissDisabled,\n shouldCloseOnInteractOutside\n },\n groupRef ?? popoverRef\n );\n\n let {overlayProps: positionProps, arrowProps, placement} = useOverlayPosition({\n ...otherProps,\n targetRef: triggerRef,\n overlayRef: popoverRef,\n isOpen: state.isOpen,\n onClose: isNonModal && !isSubmenu ? state.close : null\n });\n\n usePreventScroll({\n isDisabled: isNonModal || !state.isOpen\n });\n\n useLayoutEffect(() => {\n if (state.isOpen && popoverRef.current) {\n if (isNonModal) {\n return keepVisible(groupRef?.current ?? popoverRef.current);\n } else {\n return ariaHideOutside([groupRef?.current ?? popoverRef.current]);\n }\n }\n }, [isNonModal, state.isOpen, popoverRef, groupRef]);\n\n return {\n popoverProps: mergeProps(overlayProps, positionProps),\n arrowProps,\n underlayProps,\n placement\n };\n}\n"],"names":[],"version":3,"file":"usePopover.module.js.map"}
1
+ {"mappings":";;;;;;;AAAA;;;;;;;;;;CAUC;;;;;;AAyEM,SAAS,0CAAW,KAAuB,EAAE,KAA0B;IAC5E,IAAI,cACF,UAAU,cACV,UAAU,YACV,QAAQ,cACR,UAAU,6BACV,yBAAyB,gCACzB,4BAA4B,EAC5B,GAAG,YACJ,GAAG;IAEJ,IAAI,YAAY,UAAU,CAAC,UAAU,KAAK;IAE1C,IAAI,gBAAC,YAAY,iBAAE,aAAa,EAAC,GAAG,CAAA,GAAA,yCAAS,EAC3C;QACE,QAAQ,MAAM,MAAM;QACpB,SAAS,MAAM,KAAK;QACpB,mBAAmB;QACnB,eAAe,CAAC,cAAc;mCAC9B;sCACA;IACF,GACA,qBAAA,sBAAA,WAAY;IAGd,IAAI,EAAC,cAAc,aAAa,cAAE,UAAU,aAAE,SAAS,EAAE,oBAAoB,MAAM,EAAC,GAAG,CAAA,GAAA,yCAAiB,EAAE;QACxG,GAAG,UAAU;QACb,WAAW;QACX,YAAY;QACZ,QAAQ,MAAM,MAAM;QACpB,SAAS,cAAc,CAAC,YAAY,MAAM,KAAK,GAAG;IACpD;IAEA,CAAA,GAAA,yCAAe,EAAE;QACf,YAAY,cAAc,CAAC,MAAM,MAAM;IACzC;IAEA,CAAA,GAAA,gBAAQ,EAAE;QACR,IAAI,MAAM,MAAM,IAAI,WAAW,OAAO,EAAE;gBAEjB,mBAEK;YAH1B,IAAI,YACF,OAAO,CAAA,GAAA,yCAAU,EAAE,CAAA,oBAAA,qBAAA,+BAAA,SAAU,OAAO,cAAjB,+BAAA,oBAAqB,WAAW,OAAO;iBAE1D,OAAO,CAAA,GAAA,yCAAc,EAAE;gBAAC,CAAA,qBAAA,qBAAA,+BAAA,SAAU,OAAO,cAAjB,gCAAA,qBAAqB,WAAW,OAAO;aAAC,EAAE;gBAAC,gBAAgB;YAAI;QAE3F;IACF,GAAG;QAAC;QAAY,MAAM,MAAM;QAAE;QAAY;KAAS;IAEnD,OAAO;QACL,cAAc,CAAA,GAAA,iBAAS,EAAE,cAAc;oBACvC;uBACA;mBACA;QACA,oBAAoB;IACtB;AACF","sources":["packages/@react-aria/overlays/src/usePopover.ts"],"sourcesContent":["/*\n * Copyright 2022 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 */\n\nimport {ariaHideOutside, keepVisible} from './ariaHideOutside';\nimport {AriaPositionProps, useOverlayPosition} from './useOverlayPosition';\nimport {DOMAttributes, RefObject} from '@react-types/shared';\nimport {mergeProps} from '@react-aria/utils';\nimport {OverlayTriggerState} from '@react-stately/overlays';\nimport {PlacementAxis} from '@react-types/overlays';\nimport {useEffect} from 'react';\nimport {useOverlay} from './useOverlay';\nimport {usePreventScroll} from './usePreventScroll';\n\nexport interface AriaPopoverProps extends Omit<AriaPositionProps, 'isOpen' | 'onClose' | 'targetRef' | 'overlayRef'> {\n /**\n * The ref for the element which the popover positions itself with respect to.\n */\n triggerRef: RefObject<Element | null>,\n /**\n * The ref for the popover element.\n */\n popoverRef: RefObject<Element | null>,\n /** A ref for the popover arrow element. */\n arrowRef?: RefObject<Element | null>,\n /**\n * An optional ref for a group of popovers, e.g. submenus.\n * When provided, this element is used to detect outside interactions\n * and hiding elements from assistive technologies instead of the popoverRef.\n */\n groupRef?: RefObject<Element | null>,\n /**\n * Whether the popover is non-modal, i.e. elements outside the popover may be\n * interacted with by assistive technologies.\n *\n * Most popovers should not use this option as it may negatively impact the screen\n * reader experience. Only use with components such as combobox, which are designed\n * to handle this situation carefully.\n */\n isNonModal?: boolean,\n /**\n * Whether pressing the escape key to close the popover should be disabled.\n *\n * Most popovers should not use this option. When set to true, an alternative\n * way to close the popover with a keyboard must be provided.\n *\n * @default false\n */\n isKeyboardDismissDisabled?: boolean,\n /**\n * When user interacts with the argument element outside of the popover ref,\n * return true if onClose should be called. This gives you a chance to filter\n * out interaction with elements that should not dismiss the popover.\n * By default, onClose will always be called on interaction outside the popover ref.\n */\n shouldCloseOnInteractOutside?: (element: Element) => boolean\n}\n\nexport interface PopoverAria {\n /** Props for the popover element. */\n popoverProps: DOMAttributes,\n /** Props for the popover tip arrow if any. */\n arrowProps: DOMAttributes,\n /** Props to apply to the underlay element, if any. */\n underlayProps: DOMAttributes,\n /** Placement of the popover with respect to the trigger. */\n placement: PlacementAxis | null,\n /** The origin of the target in the overlay's coordinate system. Useful for animations. */\n triggerAnchorPoint: {x: number, y: number} | null\n}\n\n/**\n * Provides the behavior and accessibility implementation for a popover component.\n * A popover is an overlay element positioned relative to a trigger.\n */\nexport function usePopover(props: AriaPopoverProps, state: OverlayTriggerState): PopoverAria {\n let {\n triggerRef,\n popoverRef,\n groupRef,\n isNonModal,\n isKeyboardDismissDisabled,\n shouldCloseOnInteractOutside,\n ...otherProps\n } = props;\n\n let isSubmenu = otherProps['trigger'] === 'SubmenuTrigger';\n\n let {overlayProps, underlayProps} = useOverlay(\n {\n isOpen: state.isOpen,\n onClose: state.close,\n shouldCloseOnBlur: true,\n isDismissable: !isNonModal || isSubmenu,\n isKeyboardDismissDisabled,\n shouldCloseOnInteractOutside\n },\n groupRef ?? popoverRef\n );\n\n let {overlayProps: positionProps, arrowProps, placement, triggerAnchorPoint: origin} = useOverlayPosition({\n ...otherProps,\n targetRef: triggerRef,\n overlayRef: popoverRef,\n isOpen: state.isOpen,\n onClose: isNonModal && !isSubmenu ? state.close : null\n });\n\n usePreventScroll({\n isDisabled: isNonModal || !state.isOpen\n });\n\n useEffect(() => {\n if (state.isOpen && popoverRef.current) {\n if (isNonModal) {\n return keepVisible(groupRef?.current ?? popoverRef.current);\n } else {\n return ariaHideOutside([groupRef?.current ?? popoverRef.current], {shouldUseInert: true});\n }\n }\n }, [isNonModal, state.isOpen, popoverRef, groupRef]);\n\n return {\n popoverProps: mergeProps(overlayProps, positionProps),\n arrowProps,\n underlayProps,\n placement,\n triggerAnchorPoint: origin\n };\n}\n"],"names":[],"version":3,"file":"usePopover.module.js.map"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-aria/overlays",
3
- "version": "3.27.3",
3
+ "version": "3.29.0",
4
4
  "description": "Spectrum UI components in React",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/main.js",
@@ -26,16 +26,16 @@
26
26
  "url": "https://github.com/adobe/react-spectrum"
27
27
  },
28
28
  "dependencies": {
29
- "@react-aria/focus": "^3.20.5",
30
- "@react-aria/i18n": "^3.12.10",
31
- "@react-aria/interactions": "^3.25.3",
32
- "@react-aria/ssr": "^3.9.9",
33
- "@react-aria/utils": "^3.29.1",
34
- "@react-aria/visually-hidden": "^3.8.25",
35
- "@react-stately/overlays": "^3.6.17",
36
- "@react-types/button": "^3.12.2",
37
- "@react-types/overlays": "^3.8.16",
38
- "@react-types/shared": "^3.30.0",
29
+ "@react-aria/focus": "^3.21.1",
30
+ "@react-aria/i18n": "^3.12.12",
31
+ "@react-aria/interactions": "^3.25.5",
32
+ "@react-aria/ssr": "^3.9.10",
33
+ "@react-aria/utils": "^3.30.1",
34
+ "@react-aria/visually-hidden": "^3.8.27",
35
+ "@react-stately/overlays": "^3.6.19",
36
+ "@react-types/button": "^3.14.0",
37
+ "@react-types/overlays": "^3.9.1",
38
+ "@react-types/shared": "^3.32.0",
39
39
  "@swc/helpers": "^0.5.0"
40
40
  },
41
41
  "peerDependencies": {
@@ -45,5 +45,5 @@
45
45
  "publishConfig": {
46
46
  "access": "public"
47
47
  },
48
- "gitHead": "a063122082d2b372e4846b58c85ae69ec73887ff"
48
+ "gitHead": "2c58ed3ddd9be9100af9b1d0cfd137fcdc95ba2d"
49
49
  }
package/src/Overlay.tsx CHANGED
@@ -43,7 +43,8 @@ export interface OverlayProps {
43
43
  isExiting?: boolean
44
44
  }
45
45
 
46
- export const OverlayContext = React.createContext<{contain: boolean, setContain: React.Dispatch<React.SetStateAction<boolean>>} | null>(null);
46
+ export const OverlayContext: React.Context<{contain: boolean, setContain: React.Dispatch<React.SetStateAction<boolean>>} | null> =
47
+ React.createContext<{contain: boolean, setContain: React.Dispatch<React.SetStateAction<boolean>>} | null>(null);
47
48
 
48
49
  /**
49
50
  * A container which renders an overlay such as a popover or modal in a portal,
@@ -21,7 +21,7 @@ export interface PortalProviderProps {
21
21
 
22
22
  export interface PortalProviderContextValue extends Omit<PortalProviderProps, 'children'>{};
23
23
 
24
- export const PortalContext = createContext<PortalProviderContextValue>({});
24
+ export const PortalContext: React.Context<PortalProviderContextValue> = createContext<PortalProviderContextValue>({});
25
25
 
26
26
  /**
27
27
  * Sets the portal container for all overlay elements rendered by its children.
@@ -10,6 +10,14 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
+ import {getOwnerWindow} from '@react-aria/utils';
14
+ const supportsInert = typeof HTMLElement !== 'undefined' && 'inert' in HTMLElement.prototype;
15
+
16
+ interface AriaHideOutsideOptions {
17
+ root?: Element,
18
+ shouldUseInert?: boolean
19
+ }
20
+
13
21
  // Keeps a ref count of all hidden elements. Added to when hiding an element, and
14
22
  // subtracted from when showing it again. When it reaches zero, aria-hidden is removed.
15
23
  let refCountMap = new WeakMap<Element, number>();
@@ -29,10 +37,33 @@ let observerStack: Array<ObserverWrapper> = [];
29
37
  * @param root - Nothing will be hidden above this element.
30
38
  * @returns - A function to restore all hidden elements.
31
39
  */
32
- export function ariaHideOutside(targets: Element[], root = document.body) {
40
+ export function ariaHideOutside(targets: Element[], options?: AriaHideOutsideOptions | Element) {
41
+ let windowObj = getOwnerWindow(targets?.[0]);
42
+ let opts = options instanceof windowObj.Element ? {root: options} : options;
43
+ let root = opts?.root ?? document.body;
44
+ let shouldUseInert = opts?.shouldUseInert && supportsInert;
33
45
  let visibleNodes = new Set<Element>(targets);
34
46
  let hiddenNodes = new Set<Element>();
35
47
 
48
+ let getHidden = (element: Element) => {
49
+ return shouldUseInert && element instanceof windowObj.HTMLElement ? element.inert : element.getAttribute('aria-hidden') === 'true';
50
+ };
51
+
52
+ let setHidden = (element: Element, hidden: boolean) => {
53
+ if (shouldUseInert && element instanceof windowObj.HTMLElement) {
54
+ element.inert = hidden;
55
+ } else if (hidden) {
56
+ element.setAttribute('aria-hidden', 'true');
57
+ } else {
58
+ element.removeAttribute('aria-hidden');
59
+ if (element instanceof windowObj.HTMLElement) {
60
+ // We only ever call setHidden with hidden = false when the nodeCount is 1 aka
61
+ // we are trying to make the element visible to screen readers again, so remove inert as well
62
+ element.inert = false;
63
+ }
64
+ }
65
+ };
66
+
36
67
  let walk = (root: Element) => {
37
68
  // Keep live announcer and top layer elements (e.g. toasts) visible.
38
69
  for (let element of root.querySelectorAll('[data-live-announcer], [data-react-aria-top-layer]')) {
@@ -45,6 +76,7 @@ export function ariaHideOutside(targets: Element[], root = document.body) {
45
76
  // made for elements with role="row" since VoiceOver on iOS has issues hiding elements with role="row".
46
77
  // For that case we want to hide the cells inside as well (https://bugs.webkit.org/show_bug.cgi?id=222623).
47
78
  if (
79
+ hiddenNodes.has(node) ||
48
80
  visibleNodes.has(node) ||
49
81
  (node.parentElement && hiddenNodes.has(node.parentElement) && node.parentElement.getAttribute('role') !== 'row')
50
82
  ) {
@@ -87,12 +119,12 @@ export function ariaHideOutside(targets: Element[], root = document.body) {
87
119
 
88
120
  // If already aria-hidden, and the ref count is zero, then this element
89
121
  // was already hidden and there's nothing for us to do.
90
- if (node.getAttribute('aria-hidden') === 'true' && refCount === 0) {
122
+ if (getHidden(node) && refCount === 0) {
91
123
  return;
92
124
  }
93
125
 
94
126
  if (refCount === 0) {
95
- node.setAttribute('aria-hidden', 'true');
127
+ setHidden(node, true);
96
128
  }
97
129
 
98
130
  hiddenNodes.add(node);
@@ -109,20 +141,13 @@ export function ariaHideOutside(targets: Element[], root = document.body) {
109
141
 
110
142
  let observer = new MutationObserver(changes => {
111
143
  for (let change of changes) {
112
- if (change.type !== 'childList' || change.addedNodes.length === 0) {
144
+ if (change.type !== 'childList') {
113
145
  continue;
114
146
  }
115
147
 
116
148
  // If the parent element of the added nodes is not within one of the targets,
117
149
  // and not already inside a hidden node, hide all of the new children.
118
150
  if (![...visibleNodes, ...hiddenNodes].some(node => node.contains(change.target))) {
119
- for (let node of change.removedNodes) {
120
- if (node instanceof Element) {
121
- visibleNodes.delete(node);
122
- hiddenNodes.delete(node);
123
- }
124
- }
125
-
126
151
  for (let node of change.addedNodes) {
127
152
  if (
128
153
  (node instanceof HTMLElement || node instanceof SVGElement) &&
@@ -161,7 +186,7 @@ export function ariaHideOutside(targets: Element[], root = document.body) {
161
186
  continue;
162
187
  }
163
188
  if (count === 1) {
164
- node.removeAttribute('aria-hidden');
189
+ setHidden(node, false);
165
190
  refCountMap.delete(node);
166
191
  } else {
167
192
  refCountMap.set(node, count - 1);
@@ -180,7 +205,7 @@ export function ariaHideOutside(targets: Element[], root = document.body) {
180
205
  };
181
206
  }
182
207
 
183
- export function keepVisible(element: Element) {
208
+ export function keepVisible(element: Element): (() => void) | undefined {
184
209
  let observer = observerStack[observerStack.length - 1];
185
210
  if (observer && !observer.visibleNodes.has(element)) {
186
211
  observer.visibleNodes.add(element);
@@ -67,6 +67,7 @@ export interface PositionResult {
67
67
  position: Position,
68
68
  arrowOffsetLeft?: number,
69
69
  arrowOffsetTop?: number,
70
+ triggerAnchorPoint: {x: number, y: number},
70
71
  maxHeight: number,
71
72
  placement: PlacementAxis
72
73
  }
@@ -419,7 +420,8 @@ export function calculatePositionInternal(
419
420
  // childOffset[crossAxis] + .5 * childOffset[crossSize] = absolute position with respect to the trigger's coordinate system that would place the arrow in the center of the trigger
420
421
  // position[crossAxis] - margins[AXIS[crossAxis]] = value use to transform the position to a value with respect to the overlay's coordinate system. A child element's (aka arrow) position absolute's "0"
421
422
  // is positioned after the margin of its parent (aka overlay) so we need to subtract it to get the proper coordinate transform
422
- let preferredArrowPosition = childOffset[crossAxis] + .5 * childOffset[crossSize] - position[crossAxis]! - margins[AXIS[crossAxis]];
423
+ let origin = childOffset[crossAxis] - position[crossAxis]! - margins[AXIS[crossAxis]];
424
+ let preferredArrowPosition = origin + .5 * childOffset[crossSize];
423
425
 
424
426
  // Min/Max position limits for the arrow with respect to the overlay
425
427
  const arrowMinPosition = arrowSize / 2 + arrowBoundaryOffset;
@@ -436,12 +438,30 @@ export function calculatePositionInternal(
436
438
  const arrowPositionOverlappingChild = clamp(preferredArrowPosition, arrowOverlappingChildMinEdge, arrowOverlappingChildMaxEdge);
437
439
  arrowPosition[crossAxis] = clamp(arrowPositionOverlappingChild, arrowMinPosition, arrowMaxPosition);
438
440
 
441
+ // If there is an arrow, use that as the origin so that animations are smooth.
442
+ // Otherwise use the target edge.
443
+ ({placement, crossPlacement} = placementInfo);
444
+ if (arrowSize) {
445
+ origin = arrowPosition[crossAxis];
446
+ } else if (crossPlacement === 'right') {
447
+ origin += childOffset[crossSize];
448
+ } else if (crossPlacement === 'center') {
449
+ origin += childOffset[crossSize] / 2;
450
+ }
451
+
452
+ let crossOrigin = placement === 'left' || placement === 'top' ? overlaySize[size] : 0;
453
+ let triggerAnchorPoint = {
454
+ x: placement === 'top' || placement === 'bottom' ? origin : crossOrigin,
455
+ y: placement === 'left' || placement === 'right' ? origin : crossOrigin
456
+ };
457
+
439
458
  return {
440
459
  position,
441
460
  maxHeight: maxHeight,
442
461
  arrowOffsetLeft: arrowPosition.left,
443
462
  arrowOffsetTop: arrowPosition.top,
444
- placement: placementInfo.placement
463
+ placement,
464
+ triggerAnchorPoint
445
465
  };
446
466
  }
447
467
 
@@ -58,7 +58,7 @@ export function useModalOverlay(props: AriaModalOverlayProps, state: OverlayTrig
58
58
 
59
59
  useEffect(() => {
60
60
  if (state.isOpen && ref.current) {
61
- return ariaHideOutside([ref.current]);
61
+ return ariaHideOutside([ref.current], {shouldUseInert: true});
62
62
  }
63
63
  }, [state.isOpen, ref]);
64
64
 
@@ -37,6 +37,10 @@ export interface AriaPositionProps extends PositionProps {
37
37
  * The ref for the overlay element.
38
38
  */
39
39
  overlayRef: RefObject<Element | null>,
40
+ /**
41
+ * The ref for the arrow element.
42
+ */
43
+ arrowRef?: RefObject<Element | null>,
40
44
  /**
41
45
  * A ref for the scrollable region within the overlay.
42
46
  * @default overlayRef
@@ -68,6 +72,8 @@ export interface PositionAria {
68
72
  arrowProps: DOMAttributes,
69
73
  /** Placement of the overlay with respect to the overlay trigger. */
70
74
  placement: PlacementAxis | null,
75
+ /** The origin of the target in the overlay's coordinate system. Useful for animations. */
76
+ triggerAnchorPoint: {x: number, y: number} | null,
71
77
  /** Updates the position of the overlay. */
72
78
  updatePosition(): void
73
79
  }
@@ -86,9 +92,10 @@ let visualViewport = typeof document !== 'undefined' ? window.visualViewport : n
86
92
  export function useOverlayPosition(props: AriaPositionProps): PositionAria {
87
93
  let {direction} = useLocale();
88
94
  let {
89
- arrowSize = 0,
95
+ arrowSize,
90
96
  targetRef,
91
97
  overlayRef,
98
+ arrowRef,
92
99
  scrollRef = overlayRef,
93
100
  placement = 'bottom' as Placement,
94
101
  containerPadding = 12,
@@ -109,6 +116,7 @@ export function useOverlayPosition(props: AriaPositionProps): PositionAria {
109
116
  placement,
110
117
  overlayRef.current,
111
118
  targetRef.current,
119
+ arrowRef?.current,
112
120
  scrollRef.current,
113
121
  containerPadding,
114
122
  shouldFlip,
@@ -141,6 +149,17 @@ export function useOverlayPosition(props: AriaPositionProps): PositionAria {
141
149
  return;
142
150
  }
143
151
 
152
+ // Delay updating the position until children are finished rendering (e.g. collections).
153
+ if (overlayRef.current.querySelector('[data-react-aria-incomplete]')) {
154
+ return;
155
+ }
156
+
157
+ // Don't update while the overlay is animating.
158
+ // Things like scale animations can mess up positioning by affecting the overlay's computed size.
159
+ if (overlayRef.current.getAnimations?.().length > 0) {
160
+ return;
161
+ }
162
+
144
163
  // Determine a scroll anchor based on the focused element.
145
164
  // This stores the offset of the anchor element from the scroll container
146
165
  // so it can be restored after repositioning. This way if the overlay height
@@ -181,7 +200,7 @@ export function useOverlayPosition(props: AriaPositionProps): PositionAria {
181
200
  offset,
182
201
  crossOffset,
183
202
  maxHeight,
184
- arrowSize,
203
+ arrowSize: arrowSize ?? arrowRef?.current?.getBoundingClientRect().width ?? 0,
185
204
  arrowBoundaryOffset
186
205
  });
187
206
 
@@ -280,13 +299,16 @@ export function useOverlayPosition(props: AriaPositionProps): PositionAria {
280
299
  return {
281
300
  overlayProps: {
282
301
  style: {
283
- position: 'absolute',
302
+ position: position ? 'absolute' : 'fixed',
303
+ top: !position ? 0 : undefined,
304
+ left: !position ? 0 : undefined,
284
305
  zIndex: 100000, // should match the z-index in ModalTrigger
285
306
  ...position?.position,
286
307
  maxHeight: position?.maxHeight ?? '100vh'
287
308
  }
288
309
  },
289
310
  placement: position?.placement ?? null,
311
+ triggerAnchorPoint: position?.triggerAnchorPoint ?? null,
290
312
  arrowProps: {
291
313
  'aria-hidden': 'true',
292
314
  role: 'presentation',
package/src/usePopover.ts CHANGED
@@ -13,9 +13,10 @@
13
13
  import {ariaHideOutside, keepVisible} from './ariaHideOutside';
14
14
  import {AriaPositionProps, useOverlayPosition} from './useOverlayPosition';
15
15
  import {DOMAttributes, RefObject} from '@react-types/shared';
16
- import {mergeProps, useLayoutEffect} from '@react-aria/utils';
16
+ import {mergeProps} from '@react-aria/utils';
17
17
  import {OverlayTriggerState} from '@react-stately/overlays';
18
18
  import {PlacementAxis} from '@react-types/overlays';
19
+ import {useEffect} from 'react';
19
20
  import {useOverlay} from './useOverlay';
20
21
  import {usePreventScroll} from './usePreventScroll';
21
22
 
@@ -28,6 +29,8 @@ export interface AriaPopoverProps extends Omit<AriaPositionProps, 'isOpen' | 'on
28
29
  * The ref for the popover element.
29
30
  */
30
31
  popoverRef: RefObject<Element | null>,
32
+ /** A ref for the popover arrow element. */
33
+ arrowRef?: RefObject<Element | null>,
31
34
  /**
32
35
  * An optional ref for a group of popovers, e.g. submenus.
33
36
  * When provided, this element is used to detect outside interactions
@@ -69,7 +72,9 @@ export interface PopoverAria {
69
72
  /** Props to apply to the underlay element, if any. */
70
73
  underlayProps: DOMAttributes,
71
74
  /** Placement of the popover with respect to the trigger. */
72
- placement: PlacementAxis | null
75
+ placement: PlacementAxis | null,
76
+ /** The origin of the target in the overlay's coordinate system. Useful for animations. */
77
+ triggerAnchorPoint: {x: number, y: number} | null
73
78
  }
74
79
 
75
80
  /**
@@ -101,7 +106,7 @@ export function usePopover(props: AriaPopoverProps, state: OverlayTriggerState):
101
106
  groupRef ?? popoverRef
102
107
  );
103
108
 
104
- let {overlayProps: positionProps, arrowProps, placement} = useOverlayPosition({
109
+ let {overlayProps: positionProps, arrowProps, placement, triggerAnchorPoint: origin} = useOverlayPosition({
105
110
  ...otherProps,
106
111
  targetRef: triggerRef,
107
112
  overlayRef: popoverRef,
@@ -113,12 +118,12 @@ export function usePopover(props: AriaPopoverProps, state: OverlayTriggerState):
113
118
  isDisabled: isNonModal || !state.isOpen
114
119
  });
115
120
 
116
- useLayoutEffect(() => {
121
+ useEffect(() => {
117
122
  if (state.isOpen && popoverRef.current) {
118
123
  if (isNonModal) {
119
124
  return keepVisible(groupRef?.current ?? popoverRef.current);
120
125
  } else {
121
- return ariaHideOutside([groupRef?.current ?? popoverRef.current]);
126
+ return ariaHideOutside([groupRef?.current ?? popoverRef.current], {shouldUseInert: true});
122
127
  }
123
128
  }
124
129
  }, [isNonModal, state.isOpen, popoverRef, groupRef]);
@@ -127,6 +132,7 @@ export function usePopover(props: AriaPopoverProps, state: OverlayTriggerState):
127
132
  popoverProps: mergeProps(overlayProps, positionProps),
128
133
  arrowProps,
129
134
  underlayProps,
130
- placement
135
+ placement,
136
+ triggerAnchorPoint: origin
131
137
  };
132
138
  }