diff --git a/packages/@react-aria/overlays/package.json b/packages/@react-aria/overlays/package.json index f8e3f63d7fe..8a6d64993e9 100644 --- a/packages/@react-aria/overlays/package.json +++ b/packages/@react-aria/overlays/package.json @@ -38,6 +38,9 @@ "@react-types/shared": "^3.31.0", "@swc/helpers": "^0.5.0" }, + "devDependencies": { + "@react-stately/flags": "^3.1.2" + }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" diff --git a/packages/@react-aria/overlays/src/useOverlay.ts b/packages/@react-aria/overlays/src/useOverlay.ts index 8fdcfa39410..dfa03668eb0 100644 --- a/packages/@react-aria/overlays/src/useOverlay.ts +++ b/packages/@react-aria/overlays/src/useOverlay.ts @@ -11,6 +11,7 @@ */ import {DOMAttributes, RefObject} from '@react-types/shared'; +import {getEventTarget} from '@react-aria/utils'; import {isElementInChildOfActiveScope} from '@react-aria/focus'; import {useEffect} from 'react'; import {useFocusWithin, useInteractOutside} from '@react-aria/interactions'; @@ -91,7 +92,7 @@ export function useOverlay(props: AriaOverlayProps, ref: RefObject { - if (!shouldCloseOnInteractOutside || shouldCloseOnInteractOutside(e.target as Element)) { + if (!shouldCloseOnInteractOutside || shouldCloseOnInteractOutside(getEventTarget(e))) { if (visibleOverlays[visibleOverlays.length - 1] === ref) { e.stopPropagation(); e.preventDefault(); @@ -100,7 +101,7 @@ export function useOverlay(props: AriaOverlayProps, ref: RefObject { - if (!shouldCloseOnInteractOutside || shouldCloseOnInteractOutside(e.target as Element)) { + if (!shouldCloseOnInteractOutside || shouldCloseOnInteractOutside(getEventTarget(e))) { if (visibleOverlays[visibleOverlays.length - 1] === ref) { e.stopPropagation(); e.preventDefault(); diff --git a/packages/@react-aria/overlays/test/useOverlay.test.js b/packages/@react-aria/overlays/test/useOverlay.test.js index 2f686f0f287..98735bc19ad 100644 --- a/packages/@react-aria/overlays/test/useOverlay.test.js +++ b/packages/@react-aria/overlays/test/useOverlay.test.js @@ -10,17 +10,30 @@ * governing permissions and limitations under the License. */ -import {fireEvent, installMouseEvent, installPointerEvent, render} from '@react-spectrum/test-utils-internal'; +import { + createShadowRoot, + fireEvent, + installMouseEvent, + installPointerEvent, + render +} from '@react-spectrum/test-utils-internal'; +import {enableShadowDOM} from '@react-stately/flags'; import {mergeProps} from '@react-aria/utils'; import React, {useRef} from 'react'; +import ReactDOM from 'react-dom'; import {useOverlay} from '../'; function Example(props) { let ref = useRef(); let {overlayProps, underlayProps} = useOverlay(props, ref); return ( -
-
+
+
{props.children}
@@ -56,7 +69,7 @@ describe('useOverlay', function () { expect(document.activeElement).toBe(input); }); - it('should hide the overlay when clicking outside if isDismissble is true', function () { + it('should hide the overlay when clicking outside if isDismissable is true', function () { let onClose = jest.fn(); render(); pressStart(document.body); @@ -140,3 +153,78 @@ describe('useOverlay', function () { }); }); }); + +describe('useOverlay with shadow dom', () => { + beforeAll(() => { + enableShadowDOM(); + }); + + describe.each` + type | prepare | actions + ${'Mouse Events'} | ${installMouseEvent} | ${[(el) => fireEvent.mouseDown(el, {button: 0}), (el) => fireEvent.mouseUp(el, {button: 0})]} + ${'Pointer Events'} | ${installPointerEvent} | ${[(el) => fireEvent.pointerDown(el, {button: 0, pointerId: 1}), (el) => {fireEvent.pointerUp(el, {button: 0, pointerId: 1}); fireEvent.click(el, {button: 0, pointerId: 1});}]} + ${'Touch Events'} | ${() => {}} | ${[(el) => fireEvent.touchStart(el, {changedTouches: [{identifier: 1}]}), (el) => fireEvent.touchEnd(el, {changedTouches: [{identifier: 1}]})]} + `('$type', ({actions: [pressStart, pressEnd], prepare}) => { + prepare(); + + it('should not close the overlay when clicking outside if shouldCloseOnInteractOutside returns true', function () { + const {shadowRoot, cleanup} = createShadowRoot(); + + let onClose = jest.fn(); + let underlay; + + const WrapperComponent = () => + ReactDOM.createPortal( + { + return target === underlay; + }} />, + shadowRoot + ); + + const {unmount} = render(); + + underlay = shadowRoot.querySelector("[data-testid='underlay']"); + + pressStart(underlay); + pressEnd(underlay); + expect(onClose).toHaveBeenCalled(); + + // Cleanup + unmount(); + cleanup(); + }); + + it('should not close the overlay when clicking outside if shouldCloseOnInteractOutside returns false', function () { + const {shadowRoot, cleanup} = createShadowRoot(); + + let onClose = jest.fn(); + let underlay; + + const WrapperComponent = () => + ReactDOM.createPortal( + target !== underlay} />, + shadowRoot + ); + + const {unmount} = render(); + + underlay = shadowRoot.querySelector("[data-testid='underlay']"); + + pressStart(underlay); + pressEnd(underlay); + expect(onClose).not.toHaveBeenCalled(); + + // Cleanup + unmount(); + cleanup(); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 009a250feb7..f6727454e65 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5912,6 +5912,7 @@ __metadata: "@react-aria/ssr": "npm:^3.9.10" "@react-aria/utils": "npm:^3.30.0" "@react-aria/visually-hidden": "npm:^3.8.26" + "@react-stately/flags": "npm:^3.1.2" "@react-stately/overlays": "npm:^3.6.18" "@react-types/button": "npm:^3.13.0" "@react-types/overlays": "npm:^3.9.0"