From c37229ee2a7fc424eae38520d53c0944980dbfb1 Mon Sep 17 00:00:00 2001 From: Ritesh Kumar Date: Mon, 10 Feb 2025 04:56:39 +0530 Subject: [PATCH 1/7] feat: Add Shadow DOM support for useOverlay and improve event handling --- packages/@react-aria/overlays/package.json | 1 + .../@react-aria/overlays/src/useOverlay.ts | 5 +- .../overlays/test/useOverlay.test.js | 146 +++++++++++++++--- yarn.lock | 1 + 4 files changed, 128 insertions(+), 25 deletions(-) diff --git a/packages/@react-aria/overlays/package.json b/packages/@react-aria/overlays/package.json index a91532d0f2c..584fea5f882 100644 --- a/packages/@react-aria/overlays/package.json +++ b/packages/@react-aria/overlays/package.json @@ -28,6 +28,7 @@ "@react-aria/ssr": "^3.9.7", "@react-aria/utils": "^3.27.0", "@react-aria/visually-hidden": "^3.8.19", + "@react-stately/flags": "^3.0.5", "@react-stately/overlays": "^3.6.13", "@react-types/button": "^3.10.2", "@react-types/overlays": "^3.8.12", diff --git a/packages/@react-aria/overlays/src/useOverlay.ts b/packages/@react-aria/overlays/src/useOverlay.ts index 2ebeeabad4a..429fbfbc6b5 100644 --- a/packages/@react-aria/overlays/src/useOverlay.ts +++ b/packages/@react-aria/overlays/src/useOverlay.ts @@ -13,6 +13,7 @@ import {DOMAttributes, RefObject} from '@react-types/shared'; import {isElementInChildOfActiveScope} from '@react-aria/focus'; import {useEffect} from 'react'; +import {getEventTarget} from "@react-aria/utils" import {useFocusWithin, useInteractOutside} from '@react-aria/interactions'; export interface AriaOverlayProps { @@ -92,7 +93,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(); @@ -101,7 +102,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 050de17b0dd..19f14459e10 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}
@@ -29,19 +42,10 @@ function Example(props) { describe('useOverlay', function () { 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}) - ]} - ${'Touch Events'} | ${() => {}} | ${[ - (el) => fireEvent.touchStart(el, {changedTouches: [{identifier: 1}]}), - (el) => fireEvent.touchEnd(el, {changedTouches: [{identifier: 1}]}) - ]} + 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})]} + ${'Touch Events'} | ${() => {}} | ${[(el) => fireEvent.touchStart(el, {changedTouches: [{identifier: 1}]}), (el) => fireEvent.touchEnd(el, {changedTouches: [{identifier: 1}]})]} `('$type', ({actions: [pressStart, pressEnd], prepare}) => { prepare(); @@ -56,7 +60,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); @@ -66,7 +70,13 @@ describe('useOverlay', function () { it('should hide the overlay when clicking outside if shouldCloseOnInteractOutside returns true', function () { let onClose = jest.fn(); - render( target === document.body} />); + render( + target === document.body} /> + ); pressStart(document.body); pressEnd(document.body); expect(onClose).toHaveBeenCalledTimes(1); @@ -74,7 +84,13 @@ describe('useOverlay', function () { it('should not hide the overlay when clicking outside if shouldCloseOnInteractOutside returns false', function () { let onClose = jest.fn(); - render( target !== document.body} />); + render( + target !== document.body} /> + ); pressStart(document.body); pressEnd(document.body); expect(onClose).toHaveBeenCalledTimes(0); @@ -92,7 +108,9 @@ describe('useOverlay', function () { let onCloseFirst = jest.fn(); let onCloseSecond = jest.fn(); render(); - let second = render(); + let second = render( + + ); pressStart(document.body); pressEnd(document.body); @@ -117,7 +135,9 @@ describe('useOverlay', function () { it('should still hide the overlay when pressing the escape key if isDismissable is false', function () { let onClose = jest.fn(); - let res = render(); + let res = render( + + ); let el = res.getByTestId('test'); fireEvent.keyDown(el, {key: 'Escape'}); expect(onClose).toHaveBeenCalledTimes(1); @@ -127,10 +147,90 @@ describe('useOverlay', function () { installPointerEvent(); it('should prevent default on pointer down on the underlay', function () { let underlayRef = React.createRef(); - render(); - let isPrevented = fireEvent.pointerDown(underlayRef.current, {button: 0, pointerId: 1}); + render( + + ); + let isPrevented = fireEvent.pointerDown(underlayRef.current, { + button: 0, + pointerId: 1 + }); fireEvent.pointerUp(document.body); expect(isPrevented).toBeFalsy(); // meaning the event had preventDefault called }); }); }); + +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})]} + ${'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, shadowHost} = 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(); + document.body.removeChild(shadowHost); + }); + + it('should not close the overlay when clicking outside if shouldCloseOnInteractOutside returns false', function () { + const {shadowRoot, shadowHost} = 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(); + document.body.removeChild(shadowHost); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index f451f575ae5..b3bd3a4841f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6407,6 +6407,7 @@ __metadata: "@react-aria/ssr": "npm:^3.9.7" "@react-aria/utils": "npm:^3.27.0" "@react-aria/visually-hidden": "npm:^3.8.19" + "@react-stately/flags": "npm:^3.0.5" "@react-stately/overlays": "npm:^3.6.13" "@react-types/button": "npm:^3.10.2" "@react-types/overlays": "npm:^3.8.12" From f09384a991bd207a41d7845a38a804c346afb295 Mon Sep 17 00:00:00 2001 From: Ritesh Kumar Date: Mon, 10 Feb 2025 05:04:01 +0530 Subject: [PATCH 2/7] chore: Organize imports in useOverlay --- packages/@react-aria/overlays/src/useOverlay.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-aria/overlays/src/useOverlay.ts b/packages/@react-aria/overlays/src/useOverlay.ts index 429fbfbc6b5..04a9afe9dae 100644 --- a/packages/@react-aria/overlays/src/useOverlay.ts +++ b/packages/@react-aria/overlays/src/useOverlay.ts @@ -11,9 +11,9 @@ */ import {DOMAttributes, RefObject} from '@react-types/shared'; +import {getEventTarget} from '@react-aria/utils'; import {isElementInChildOfActiveScope} from '@react-aria/focus'; import {useEffect} from 'react'; -import {getEventTarget} from "@react-aria/utils" import {useFocusWithin, useInteractOutside} from '@react-aria/interactions'; export interface AriaOverlayProps { From 6c1a00e8a72b5affe1b4a2a7c1f2a153a0328296 Mon Sep 17 00:00:00 2001 From: Ritesh Kumar Date: Wed, 26 Feb 2025 10:55:51 +0530 Subject: [PATCH 3/7] chore: Move @react-stately/flags to devDependencies in overlays package --- packages/@react-aria/overlays/package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/@react-aria/overlays/package.json b/packages/@react-aria/overlays/package.json index 584fea5f882..22a9781202d 100644 --- a/packages/@react-aria/overlays/package.json +++ b/packages/@react-aria/overlays/package.json @@ -28,13 +28,15 @@ "@react-aria/ssr": "^3.9.7", "@react-aria/utils": "^3.27.0", "@react-aria/visually-hidden": "^3.8.19", - "@react-stately/flags": "^3.0.5", "@react-stately/overlays": "^3.6.13", "@react-types/button": "^3.10.2", "@react-types/overlays": "^3.8.12", "@react-types/shared": "^3.27.0", "@swc/helpers": "^0.5.0" }, + "devDependencies": { + "@react-stately/flags": "^3.0.5" + }, "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" From da35a422a9df3d1fbc85cd78afec84f6d71fe0a0 Mon Sep 17 00:00:00 2001 From: Ritesh Kumar Date: Wed, 26 Feb 2025 10:59:17 +0530 Subject: [PATCH 4/7] Fix formatting --- .../overlays/test/useOverlay.test.js | 50 +++++++------------ 1 file changed, 19 insertions(+), 31 deletions(-) diff --git a/packages/@react-aria/overlays/test/useOverlay.test.js b/packages/@react-aria/overlays/test/useOverlay.test.js index 19f14459e10..2cd9fd7d812 100644 --- a/packages/@react-aria/overlays/test/useOverlay.test.js +++ b/packages/@react-aria/overlays/test/useOverlay.test.js @@ -42,10 +42,19 @@ function Example(props) { describe('useOverlay', function () { 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})]} - ${'Touch Events'} | ${() => {}} | ${[(el) => fireEvent.touchStart(el, {changedTouches: [{identifier: 1}]}), (el) => fireEvent.touchEnd(el, {changedTouches: [{identifier: 1}]})]} + 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}) + ]} + ${'Touch Events'} | ${() => {}} | ${[ + (el) => fireEvent.touchStart(el, {changedTouches: [{identifier: 1}]}), + (el) => fireEvent.touchEnd(el, {changedTouches: [{identifier: 1}]}) + ]} `('$type', ({actions: [pressStart, pressEnd], prepare}) => { prepare(); @@ -70,13 +79,7 @@ describe('useOverlay', function () { it('should hide the overlay when clicking outside if shouldCloseOnInteractOutside returns true', function () { let onClose = jest.fn(); - render( - target === document.body} /> - ); + render( target === document.body} />); pressStart(document.body); pressEnd(document.body); expect(onClose).toHaveBeenCalledTimes(1); @@ -84,13 +87,7 @@ describe('useOverlay', function () { it('should not hide the overlay when clicking outside if shouldCloseOnInteractOutside returns false', function () { let onClose = jest.fn(); - render( - target !== document.body} /> - ); + render( target !== document.body} />); pressStart(document.body); pressEnd(document.body); expect(onClose).toHaveBeenCalledTimes(0); @@ -108,9 +105,7 @@ describe('useOverlay', function () { let onCloseFirst = jest.fn(); let onCloseSecond = jest.fn(); render(); - let second = render( - - ); + let second = render(); pressStart(document.body); pressEnd(document.body); @@ -135,9 +130,7 @@ describe('useOverlay', function () { it('should still hide the overlay when pressing the escape key if isDismissable is false', function () { let onClose = jest.fn(); - let res = render( - - ); + let res = render(); let el = res.getByTestId('test'); fireEvent.keyDown(el, {key: 'Escape'}); expect(onClose).toHaveBeenCalledTimes(1); @@ -147,13 +140,8 @@ describe('useOverlay', function () { installPointerEvent(); it('should prevent default on pointer down on the underlay', function () { let underlayRef = React.createRef(); - render( - - ); - let isPrevented = fireEvent.pointerDown(underlayRef.current, { - button: 0, - pointerId: 1 - }); + render(); + let isPrevented = fireEvent.pointerDown(underlayRef.current, {button: 0, pointerId: 1}); fireEvent.pointerUp(document.body); expect(isPrevented).toBeFalsy(); // meaning the event had preventDefault called }); From 96a336437daf58b65f3b66faf483fe40d59c36ef Mon Sep 17 00:00:00 2001 From: Ritesh Kumar Date: Wed, 26 Feb 2025 11:01:32 +0530 Subject: [PATCH 5/7] chore: Improve test formatting for useOverlay events --- .../overlays/test/useOverlay.test.js | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/@react-aria/overlays/test/useOverlay.test.js b/packages/@react-aria/overlays/test/useOverlay.test.js index 2cd9fd7d812..9088d576b9c 100644 --- a/packages/@react-aria/overlays/test/useOverlay.test.js +++ b/packages/@react-aria/overlays/test/useOverlay.test.js @@ -42,19 +42,19 @@ function Example(props) { describe('useOverlay', function () { 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}) - ]} - ${'Touch Events'} | ${() => {}} | ${[ - (el) => fireEvent.touchStart(el, {changedTouches: [{identifier: 1}]}), - (el) => fireEvent.touchEnd(el, {changedTouches: [{identifier: 1}]}) - ]} + 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}) + ]} + ${'Touch Events'} | ${() => {}} | ${[ + (el) => fireEvent.touchStart(el, {changedTouches: [{identifier: 1}]}), + (el) => fireEvent.touchEnd(el, {changedTouches: [{identifier: 1}]}) + ]} `('$type', ({actions: [pressStart, pressEnd], prepare}) => { prepare(); From 42cc5cbd08d4b5f46ca54920cbccb2b7d1909760 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Mon, 4 Aug 2025 19:11:01 +1000 Subject: [PATCH 6/7] fix test --- .../@react-aria/overlays/test/useOverlay.test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/@react-aria/overlays/test/useOverlay.test.js b/packages/@react-aria/overlays/test/useOverlay.test.js index e2cc68b6f69..69803fb5952 100644 --- a/packages/@react-aria/overlays/test/useOverlay.test.js +++ b/packages/@react-aria/overlays/test/useOverlay.test.js @@ -162,13 +162,13 @@ describe('useOverlay with shadow dom', () => { 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})]} + ${'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, shadowHost} = createShadowRoot(); + it.only('should not close the overlay when clicking outside if shouldCloseOnInteractOutside returns true', function () { + const {shadowRoot, cleanup} = createShadowRoot(); let onClose = jest.fn(); let underlay; @@ -195,11 +195,11 @@ describe('useOverlay with shadow dom', () => { // Cleanup unmount(); - document.body.removeChild(shadowHost); + cleanup(); }); it('should not close the overlay when clicking outside if shouldCloseOnInteractOutside returns false', function () { - const {shadowRoot, shadowHost} = createShadowRoot(); + const {shadowRoot, cleanup} = createShadowRoot(); let onClose = jest.fn(); let underlay; @@ -224,7 +224,7 @@ describe('useOverlay with shadow dom', () => { // Cleanup unmount(); - document.body.removeChild(shadowHost); + cleanup(); }); }); }); From bd82fbb672a6e057980953b179722eae282176e4 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Mon, 4 Aug 2025 19:12:02 +1000 Subject: [PATCH 7/7] remove accidental only --- packages/@react-aria/overlays/test/useOverlay.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-aria/overlays/test/useOverlay.test.js b/packages/@react-aria/overlays/test/useOverlay.test.js index 69803fb5952..98735bc19ad 100644 --- a/packages/@react-aria/overlays/test/useOverlay.test.js +++ b/packages/@react-aria/overlays/test/useOverlay.test.js @@ -167,7 +167,7 @@ describe('useOverlay with shadow dom', () => { `('$type', ({actions: [pressStart, pressEnd], prepare}) => { prepare(); - it.only('should not close the overlay when clicking outside if shouldCloseOnInteractOutside returns true', function () { + it('should not close the overlay when clicking outside if shouldCloseOnInteractOutside returns true', function () { const {shadowRoot, cleanup} = createShadowRoot(); let onClose = jest.fn();