From 7a6dfcc50cd7938c8ec9a28cc2dca24ed3ba33bd Mon Sep 17 00:00:00 2001 From: John Pangalos Date: Thu, 7 Aug 2025 11:46:47 +0200 Subject: [PATCH 1/6] test: adds ShadowDOM / UNSAFE_PortalProvider tests These tests specifically target issue #8675 where menu items in popovers close immediately instead when using ShadowDOM with UNSAFE_PortalProvider. New test suites added: - FocusScope: Shadow DOM boundary containment issues - usePopover: Shadow DOM popover interactions and focus management - useFocusWithin: Focus within detection across shadow boundaries - useInteractOutside: Interact outside detection with portals I generated these tests with AI then reviewed / updated them. --- .../@react-aria/focus/test/FocusScope.test.js | 411 +++++++++++++++++ .../interactions/test/useFocusWithin.test.js | 356 ++++++++++++++- .../test/useInteractOutside.test.js | 403 ++++++++++++++++- .../overlays/test/usePopover.test.tsx | 419 +++++++++++++++++- 4 files changed, 1585 insertions(+), 4 deletions(-) diff --git a/packages/@react-aria/focus/test/FocusScope.test.js b/packages/@react-aria/focus/test/FocusScope.test.js index ef0b104c084..f36876d2f37 100644 --- a/packages/@react-aria/focus/test/FocusScope.test.js +++ b/packages/@react-aria/focus/test/FocusScope.test.js @@ -20,6 +20,7 @@ import {Provider} from '@react-spectrum/provider'; import React, {useEffect, useState} from 'react'; import ReactDOM from 'react-dom'; import {Example as StorybookExample} from '../stories/FocusScope.stories'; +import {UNSAFE_PortalProvider} from '@react-aria/overlays'; import {useEvent} from '@react-aria/utils'; import userEvent from '@testing-library/user-event'; @@ -2150,6 +2151,416 @@ describe('FocusScope with Shadow DOM', function () { unmount(); document.body.removeChild(shadowHost); }); + + describe('Shadow DOM boundary containment issues (issue #8675)', function () { + it('should properly detect element containment across shadow DOM boundaries with UNSAFE_PortalProvider', async function () { + const {shadowRoot} = createShadowRoot(); + + // Create a menu-like structure that reproduces the issue with UNSAFE_PortalProvider + function MenuInPopoverWithPortalProvider() { + const [isOpen, setIsOpen] = React.useState(true); + + return ( + shadowRoot}> + +
+ {isOpen && ( + +
+ + +
+
+ )} +
+
+
+ ); + } + + const {unmount} = render(); + + const menuItem1 = shadowRoot.querySelector('[data-testid="menu-item-1"]'); + const menuItem2 = shadowRoot.querySelector('[data-testid="menu-item-2"]'); + const menu = shadowRoot.querySelector('[data-testid="menu"]'); + + // Focus the first menu item + act(() => { menuItem1.focus(); }); + expect(shadowRoot.activeElement).toBe(menuItem1); + + // Tab to second menu item should work + await user.tab(); + expect(shadowRoot.activeElement).toBe(menuItem2); + + // Tab should wrap back to first item due to focus containment + await user.tab(); + expect(shadowRoot.activeElement).toBe(menuItem1); + + // Menu should still be visible (not closed unexpectedly) + expect(menu).toBeInTheDocument(); + + // Cleanup + unmount(); + document.body.removeChild(shadowRoot.host); + }); + + it('should handle focus events correctly in shadow DOM with nested FocusScopes and UNSAFE_PortalProvider', async function () { + const {shadowRoot} = createShadowRoot(); + let menuItemClickHandled = false; + + function NestedScopeMenuWithPortalProvider() { + const handleMenuItemClick = () => { + menuItemClickHandled = true; + }; + + return ( + shadowRoot}> + +
+ + +
+ +
+
+
+
+
+ ); + } + + const {unmount} = render(); + + const trigger = shadowRoot.querySelector('[data-testid="trigger"]'); + const menuItem = shadowRoot.querySelector('[data-testid="menu-item"]'); + + // Focus the trigger first + act(() => { trigger.focus(); }); + expect(shadowRoot.activeElement).toBe(trigger); + + // Tab to menu item + await user.tab(); + expect(shadowRoot.activeElement).toBe(menuItem); + + // Click the menu item - this should fire the onClick handler + await user.click(menuItem); + expect(menuItemClickHandled).toBe(true); + + // Cleanup + unmount(); + document.body.removeChild(shadowRoot.host); + }); + + + it('should handle focus manager operations across shadow DOM boundaries', async function () { + const {shadowRoot} = createShadowRoot(); + + function FocusManagerTest() { + const focusManager = useFocusManager(); + + return ReactDOM.createPortal( + +
+ + + +
+
, + shadowRoot + ); + } + + const {unmount} = render(); + + const firstButton = shadowRoot.querySelector('[data-testid="first"]'); + const secondButton = shadowRoot.querySelector('[data-testid="second"]'); + const thirdButton = shadowRoot.querySelector('[data-testid="third"]'); + + // Focus first button + act(() => { firstButton.focus(); }); + expect(shadowRoot.activeElement).toBe(firstButton); + + // Click first button to trigger focusNext + await user.click(firstButton); + expect(shadowRoot.activeElement).toBe(secondButton); + + // Click second button to trigger focusPrevious + await user.click(secondButton); + expect(shadowRoot.activeElement).toBe(firstButton); + + // Move to third button and test focusFirst + act(() => { thirdButton.focus(); }); + await user.click(thirdButton); + expect(shadowRoot.activeElement).toBe(firstButton); + + // Cleanup + unmount(); + document.body.removeChild(shadowRoot.host); + }); + + it('should correctly handle portaled elements within shadow DOM scopes', async function () { + const {shadowRoot} = createShadowRoot(); + const portalTarget = document.createElement('div'); + shadowRoot.appendChild(portalTarget); + + function PortalInShadowDOM() { + return ReactDOM.createPortal( + +
+ + {ReactDOM.createPortal( + , + portalTarget + )} +
+
, + shadowRoot + ); + } + + const {unmount} = render(); + + const mainButton = shadowRoot.querySelector('[data-testid="main-button"]'); + const portaledButton = shadowRoot.querySelector('[data-testid="portaled-button"]'); + + // Focus main button + act(() => { mainButton.focus(); }); + expect(shadowRoot.activeElement).toBe(mainButton); + + // Focus portaled button + act(() => { portaledButton.focus(); }); + expect(shadowRoot.activeElement).toBe(portaledButton); + + // Tab navigation should work between main and portaled elements + await user.tab(); + // The exact behavior may vary, but focus should remain within the shadow DOM + expect(shadowRoot.activeElement).toBeTruthy(); + + // Cleanup + unmount(); + document.body.removeChild(shadowRoot.host); + }); + + it('should reproduce the specific issue #8675: Menu items in popover close immediately with UNSAFE_PortalProvider', async function () { + const {shadowRoot} = createShadowRoot(); + let actionExecuted = false; + let menuClosed = false; + + // This reproduces the exact scenario described in the issue + function WebComponentWithReactApp() { + const [isPopoverOpen, setIsPopoverOpen] = React.useState(true); + + const handleMenuAction = (key) => { + actionExecuted = true; + // In the original issue, this never executes because the popover closes first + console.log('Menu action executed:', key); + }; + + return ( + shadowRoot}> +
+ {isPopoverOpen && ( + +
+ +
+ + +
+
+
+
+ )} + +
+
+ ); + } + + const {unmount} = render(); + + const saveMenuItem = shadowRoot.querySelector('[data-testid="menu-item-save"]'); + const exportMenuItem = shadowRoot.querySelector('[data-testid="menu-item-export"]'); + const menuContainer = shadowRoot.querySelector('[data-testid="menu-container"]'); + const popoverOverlay = shadowRoot.querySelector('[data-testid="popover-overlay"]'); + + // Verify the menu is initially visible + expect(menuContainer).toBeInTheDocument(); + expect(popoverOverlay).toBeInTheDocument(); + + // Focus the first menu item + act(() => { saveMenuItem.focus(); }); + expect(shadowRoot.activeElement).toBe(saveMenuItem); + + // Click the menu item - this should execute the onAction handler, NOT close the menu + await user.click(saveMenuItem); + + // The action should have been executed (this would fail in the buggy version) + expect(actionExecuted).toBe(true); + + // The menu should still be open (this would fail in the buggy version where it closes immediately) + expect(menuClosed).toBe(false); + expect(menuContainer).toBeInTheDocument(); + + // Test focus containment within the menu + act(() => { saveMenuItem.focus(); }); + await user.tab(); + expect(shadowRoot.activeElement).toBe(exportMenuItem); + + await user.tab(); + // Focus should wrap back to first item due to containment + expect(shadowRoot.activeElement).toBe(saveMenuItem); + + // Cleanup + unmount(); + document.body.removeChild(shadowRoot.host); + }); + + it('should handle web component scenario with multiple nested portals and UNSAFE_PortalProvider', async function () { + const {shadowRoot} = createShadowRoot(); + + // Create nested portal containers within the shadow DOM + const modalPortal = document.createElement('div'); + modalPortal.setAttribute('data-testid', 'modal-portal'); + shadowRoot.appendChild(modalPortal); + + const tooltipPortal = document.createElement('div'); + tooltipPortal.setAttribute('data-testid', 'tooltip-portal'); + shadowRoot.appendChild(tooltipPortal); + + function ComplexWebComponent() { + const [showModal, setShowModal] = React.useState(true); + const [showTooltip, setShowTooltip] = React.useState(true); + + return ( + shadowRoot}> +
+ + + {/* Modal with its own focus scope */} + {showModal && ReactDOM.createPortal( + +
+ + + +
+
, + modalPortal + )} + + {/* Tooltip with nested focus scope */} + {showTooltip && ReactDOM.createPortal( + +
+ +
+
, + tooltipPortal + )} +
+
+ ); + } + + const {unmount} = render(); + + const modalButton1 = shadowRoot.querySelector('[data-testid="modal-button-1"]'); + const modalButton2 = shadowRoot.querySelector('[data-testid="modal-button-2"]'); + const tooltipAction = shadowRoot.querySelector('[data-testid="tooltip-action"]'); + + // Due to autoFocus, the first modal button should be focused + act(() => { jest.runAllTimers(); }); + expect(shadowRoot.activeElement).toBe(modalButton1); + + // Tab navigation should work within the modal + await user.tab(); + expect(shadowRoot.activeElement).toBe(modalButton2); + + // Focus should be contained within the modal due to the contain prop + await user.tab(); + // Should cycle to the close button + expect(shadowRoot.activeElement.getAttribute('data-testid')).toBe('close-modal'); + + await user.tab(); + // Should wrap back to first modal button + expect(shadowRoot.activeElement).toBe(modalButton1); + + // The tooltip button should be focusable when we explicitly focus it + act(() => { tooltipAction.focus(); }); + // But due to modal containment, focus should be restored back to modal + act(() => { jest.runAllTimers(); }); + expect(shadowRoot.activeElement).toBe(modalButton1); + + // Cleanup + unmount(); + document.body.removeChild(shadowRoot.host); + }); + }); }); describe('Unmounting cleanup', () => { diff --git a/packages/@react-aria/interactions/test/useFocusWithin.test.js b/packages/@react-aria/interactions/test/useFocusWithin.test.js index a5cd33a45b0..fd5cef60f4a 100644 --- a/packages/@react-aria/interactions/test/useFocusWithin.test.js +++ b/packages/@react-aria/interactions/test/useFocusWithin.test.js @@ -10,9 +10,13 @@ * governing permissions and limitations under the License. */ -import {act, render, waitFor} from '@react-spectrum/test-utils-internal'; +import {act, createShadowRoot, pointerMap, render, waitFor} from '@react-spectrum/test-utils-internal'; +import {enableShadowDOM} from '@react-stately/flags'; import React, {useState} from 'react'; +import ReactDOM from 'react-dom'; +import {UNSAFE_PortalProvider} from '@react-aria/overlays'; import {useFocusWithin} from '../'; +import userEvent from '@testing-library/user-event'; function Example(props) { let {focusWithinProps} = useFocusWithin(props); @@ -195,3 +199,353 @@ describe('useFocusWithin', function () { ]); }); }); + +describe('useFocusWithin with Shadow DOM and UNSAFE_PortalProvider', () => { + let user; + + beforeAll(() => { + enableShadowDOM(); + user = userEvent.setup({delay: null, pointerMap}); + }); + + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + act(() => {jest.runAllTimers();}); + }); + + it('should handle focus within events in shadow DOM with UNSAFE_PortalProvider', async () => { + const {shadowRoot} = createShadowRoot(); + let focusWithinTriggered = false; + let blurWithinTriggered = false; + let focusChangeEvents = []; + + function ShadowFocusWithinExample() { + const handleFocusWithin = () => { + focusWithinTriggered = true; + }; + + const handleBlurWithin = () => { + blurWithinTriggered = true; + }; + + const handleFocusWithinChange = (isFocused) => { + focusChangeEvents.push(isFocused); + }; + + return ( + shadowRoot}> +
+ + + + + +
+
+ ); + } + + const {unmount} = render(); + + const innerButton = shadowRoot.querySelector('[data-testid="inner-button"]'); + const innerInput = shadowRoot.querySelector('[data-testid="inner-input"]'); + const outerButton = shadowRoot.querySelector('[data-testid="outer-button"]'); + + // Focus within the example container + act(() => { innerButton.focus(); }); + expect(shadowRoot.activeElement).toBe(innerButton); + expect(focusWithinTriggered).toBe(true); + expect(focusChangeEvents).toContain(true); + + // Move focus within the container (should not trigger blur) + act(() => { innerInput.focus(); }); + expect(shadowRoot.activeElement).toBe(innerInput); + expect(blurWithinTriggered).toBe(false); + + // Move focus outside the container + act(() => { outerButton.focus(); }); + expect(shadowRoot.activeElement).toBe(outerButton); + expect(blurWithinTriggered).toBe(true); + expect(focusChangeEvents).toContain(false); + + // Cleanup + unmount(); + document.body.removeChild(shadowRoot.host); + }); + + it('should handle focus within detection across shadow DOM boundaries (issue #8675)', async () => { + const {shadowRoot} = createShadowRoot(); + let focusWithinEvents = []; + + function MenuWithFocusWithinExample() { + const handleFocusWithinChange = (isFocused) => { + focusWithinEvents.push({type: 'focusWithinChange', isFocused}); + }; + + return ( + shadowRoot}> +
+ + +
+ + +
+
+
+
+ ); + } + + const {unmount} = render(); + + const menuItem1 = shadowRoot.querySelector('[data-testid="menu-item-1"]'); + const menuItem2 = shadowRoot.querySelector('[data-testid="menu-item-2"]'); + const menuTrigger = shadowRoot.querySelector('[data-testid="menu-trigger"]'); + + // Focus enters the menu + act(() => { menuItem1.focus(); }); + expect(shadowRoot.activeElement).toBe(menuItem1); + expect(focusWithinEvents).toContainEqual({type: 'focusWithinChange', isFocused: true}); + + // Click menu item (this should not cause focus within to be lost) + await user.click(menuItem1); + + // Focus should remain within the menu area + expect(focusWithinEvents.filter(e => e.isFocused === false)).toHaveLength(0); + + // Move focus within menu + act(() => { menuItem2.focus(); }); + expect(shadowRoot.activeElement).toBe(menuItem2); + + // Only when focus moves completely outside should focus within be false + act(() => { menuTrigger.focus(); }); + expect(shadowRoot.activeElement).toBe(menuTrigger); + expect(focusWithinEvents).toContainEqual({type: 'focusWithinChange', isFocused: false}); + + // Cleanup + unmount(); + document.body.removeChild(shadowRoot.host); + }); + + it('should handle nested focus within containers in shadow DOM with portals', async () => { + const {shadowRoot} = createShadowRoot(); + let outerFocusEvents = []; + let innerFocusEvents = []; + + function NestedFocusWithinExample() { + return ( + shadowRoot}> + outerFocusEvents.push(isFocused)} + data-testid="outer-container" + > + + innerFocusEvents.push(isFocused)} + data-testid="inner-container" + > + + + + + + + ); + } + + const {unmount} = render(); + + const outerButton = shadowRoot.querySelector('[data-testid="outer-button"]'); + const innerButton1 = shadowRoot.querySelector('[data-testid="inner-button-1"]'); + const innerButton2 = shadowRoot.querySelector('[data-testid="inner-button-2"]'); + const outerButton2 = shadowRoot.querySelector('[data-testid="outer-button-2"]'); + + // Focus enters outer container + act(() => { outerButton.focus(); }); + expect(outerFocusEvents).toContain(true); + expect(innerFocusEvents).toHaveLength(0); + + // Focus enters inner container + act(() => { innerButton1.focus(); }); + expect(innerFocusEvents).toContain(true); + expect(outerFocusEvents.filter(e => e === false)).toHaveLength(0); // Outer should still be focused + + // Move within inner container + act(() => { innerButton2.focus(); }); + expect(innerFocusEvents.filter(e => e === false)).toHaveLength(0); + + // Move to outer container (leaves inner) + act(() => { outerButton2.focus(); }); + expect(innerFocusEvents).toContain(false); + expect(outerFocusEvents.filter(e => e === false)).toHaveLength(0); + + // Cleanup + unmount(); + document.body.removeChild(shadowRoot.host); + }); + + it('should handle focus within with complex portal hierarchies in shadow DOM', async () => { + const {shadowRoot} = createShadowRoot(); + const modalPortal = document.createElement('div'); + modalPortal.setAttribute('data-testid', 'modal-portal'); + shadowRoot.appendChild(modalPortal); + + let modalFocusEvents = []; + let popoverFocusEvents = []; + + function ComplexPortalExample() { + return ( + shadowRoot}> +
+ + + {/* Modal with focus within */} + {ReactDOM.createPortal( + modalFocusEvents.push(isFocused)} + data-testid="modal" + > +
+ + + + {/* Nested popover within modal */} + popoverFocusEvents.push(isFocused)} + data-testid="popover" + > +
+ + +
+
+
+
, + modalPortal + )} +
+
+ ); + } + + const {unmount} = render(); + + const modalButton1 = shadowRoot.querySelector('[data-testid="modal-button-1"]'); + const popoverItem1 = shadowRoot.querySelector('[data-testid="popover-item-1"]'); + const popoverItem2 = shadowRoot.querySelector('[data-testid="popover-item-2"]'); + const mainButton = shadowRoot.querySelector('[data-testid="main-button"]'); + + // Focus enters modal + act(() => { modalButton1.focus(); }); + expect(modalFocusEvents).toContain(true); + + // Focus enters popover within modal + act(() => { popoverItem1.focus(); }); + expect(popoverFocusEvents).toContain(true); + expect(modalFocusEvents.filter(e => e === false)).toHaveLength(0); // Modal should still have focus within + + // Move within popover + act(() => { popoverItem2.focus(); }); + expect(popoverFocusEvents.filter(e => e === false)).toHaveLength(0); + + // Move back to modal (leaves popover) + act(() => { modalButton1.focus(); }); + expect(popoverFocusEvents).toContain(false); + expect(modalFocusEvents.filter(e => e === false)).toHaveLength(0); + + // Move completely outside (leaves modal) + act(() => { mainButton.focus(); }); + expect(modalFocusEvents).toContain(false); + + // Cleanup + unmount(); + document.body.removeChild(shadowRoot.host); + }); + + it('should correctly handle focus within when elements are dynamically added/removed in shadow DOM', async () => { + const {shadowRoot} = createShadowRoot(); + let focusWithinEvents = []; + + function DynamicFocusWithinExample() { + const [showItems, setShowItems] = React.useState(true); + + return ( + shadowRoot}> + focusWithinEvents.push(isFocused)} + data-testid="dynamic-container" + > + + {showItems && ( +
+ + +
+ )} +
+
+ ); + } + + const {unmount} = render(); + + const toggleButton = shadowRoot.querySelector('[data-testid="toggle-button"]'); + const dynamicItem1 = shadowRoot.querySelector('[data-testid="dynamic-item-1"]'); + + // Focus within the container + act(() => { dynamicItem1.focus(); }); + expect(focusWithinEvents).toContain(true); + + // Click toggle to remove items while focused on one + await user.click(toggleButton); + + // Focus should now be on the toggle button, still within container + expect(shadowRoot.activeElement).toBe(toggleButton); + expect(focusWithinEvents.filter(e => e === false)).toHaveLength(0); + + // Toggle back to show items + await user.click(toggleButton); + + // Focus should still be within the container + const newDynamicItem1 = shadowRoot.querySelector('[data-testid="dynamic-item-1"]'); + act(() => { newDynamicItem1.focus(); }); + expect(focusWithinEvents.filter(e => e === false)).toHaveLength(0); + + // Cleanup + unmount(); + document.body.removeChild(shadowRoot.host); + }); +}); diff --git a/packages/@react-aria/interactions/test/useInteractOutside.test.js b/packages/@react-aria/interactions/test/useInteractOutside.test.js index cdc2aa07a40..2a671aea2c7 100644 --- a/packages/@react-aria/interactions/test/useInteractOutside.test.js +++ b/packages/@react-aria/interactions/test/useInteractOutside.test.js @@ -10,10 +10,13 @@ * governing permissions and limitations under the License. */ -import {fireEvent, installPointerEvent, render, waitFor} from '@react-spectrum/test-utils-internal'; +import {act, createShadowRoot, fireEvent, installPointerEvent, pointerMap, render, waitFor} from '@react-spectrum/test-utils-internal'; +import {enableShadowDOM} from '@react-stately/flags'; import React, {useEffect, useRef} from 'react'; import ReactDOM, {createPortal} from 'react-dom'; +import {UNSAFE_PortalProvider} from '@react-aria/overlays'; import {useInteractOutside} from '../'; +import userEvent from '@testing-library/user-event'; function Example(props) { let ref = useRef(); @@ -593,3 +596,401 @@ describe('useInteractOutside shadow DOM extended tests', function () { cleanup(); }); }); + +describe('useInteractOutside with Shadow DOM and UNSAFE_PortalProvider', () => { + let user; + + beforeAll(() => { + enableShadowDOM(); + user = userEvent.setup({delay: null, pointerMap}); + }); + + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + act(() => {jest.runAllTimers();}); + }); + + it('should handle interact outside events with UNSAFE_PortalProvider in shadow DOM', async () => { + const {shadowRoot} = createShadowRoot(); + let interactOutsideTriggered = false; + + function ShadowInteractOutsideExample() { + const ref = useRef(); + useInteractOutside({ + ref, + onInteractOutside: () => { + interactOutsideTriggered = true; + } + }); + + return ( + shadowRoot}> +
+
+ + +
+ +
+
+ ); + } + + const {unmount} = render(); + + const target = shadowRoot.querySelector('[data-testid="target"]'); + const innerButton = shadowRoot.querySelector('[data-testid="inner-button"]'); + const outsideButton = shadowRoot.querySelector('[data-testid="outside-button"]'); + + // Click inside the target - should NOT trigger interact outside + await user.click(innerButton); + expect(interactOutsideTriggered).toBe(false); + + // Click the target itself - should NOT trigger interact outside + await user.click(target); + expect(interactOutsideTriggered).toBe(false); + + // Click outside the target within shadow DOM - should trigger interact outside + await user.click(outsideButton); + expect(interactOutsideTriggered).toBe(true); + + // Cleanup + unmount(); + document.body.removeChild(shadowRoot.host); + }); + + it('should correctly identify interactions across shadow DOM boundaries (issue #8675)', async () => { + const {shadowRoot} = createShadowRoot(); + let popoverClosed = false; + + function MenuPopoverExample() { + const popoverRef = useRef(); + useInteractOutside({ + ref: popoverRef, + onInteractOutside: () => { + popoverClosed = true; + } + }); + + return ( + shadowRoot}> +
+ +
+
+ + +
+
+
+
+ ); + } + + const {unmount} = render(); + + const menuItem1 = shadowRoot.querySelector('[data-testid="menu-item-1"]'); + const menuTrigger = shadowRoot.querySelector('[data-testid="menu-trigger"]'); + const menuPopover = shadowRoot.querySelector('[data-testid="menu-popover"]'); + + // Click menu item - should NOT close popover (this is the bug being tested) + await user.click(menuItem1); + expect(popoverClosed).toBe(false); + + // Click on the popover itself - should NOT close popover + await user.click(menuPopover); + expect(popoverClosed).toBe(false); + + // Click outside the popover - SHOULD close popover + await user.click(menuTrigger); + expect(popoverClosed).toBe(true); + + // Cleanup + unmount(); + document.body.removeChild(shadowRoot.host); + }); + + it('should handle nested portal scenarios with interact outside in shadow DOM', async () => { + const {shadowRoot} = createShadowRoot(); + const modalPortal = document.createElement('div'); + modalPortal.setAttribute('data-testid', 'modal-portal'); + shadowRoot.appendChild(modalPortal); + + let modalInteractOutside = false; + let popoverInteractOutside = false; + + function NestedPortalsExample() { + const modalRef = useRef(); + const popoverRef = useRef(); + + useInteractOutside({ + ref: modalRef, + onInteractOutside: () => { + modalInteractOutside = true; + } + }); + + useInteractOutside({ + ref: popoverRef, + onInteractOutside: () => { + popoverInteractOutside = true; + } + }); + + return ( + shadowRoot}> +
+ + + {/* Modal */} + {ReactDOM.createPortal( +
+
+ + + {/* Popover within modal */} +
+ +
+
+
, + modalPortal + )} +
+
+ ); + } + + const {unmount} = render(); + + const mainButton = shadowRoot.querySelector('[data-testid="main-button"]'); + const modalButton = shadowRoot.querySelector('[data-testid="modal-button"]'); + const popoverButton = shadowRoot.querySelector('[data-testid="popover-button"]'); + + // Click popover button - should NOT trigger either interact outside + await user.click(popoverButton); + expect(popoverInteractOutside).toBe(false); + expect(modalInteractOutside).toBe(false); + + // Click modal button - should trigger popover interact outside but NOT modal + await user.click(modalButton); + expect(popoverInteractOutside).toBe(true); + expect(modalInteractOutside).toBe(false); + + // Reset and click completely outside + popoverInteractOutside = false; + modalInteractOutside = false; + + await user.click(mainButton); + expect(modalInteractOutside).toBe(true); + + // Cleanup + unmount(); + document.body.removeChild(shadowRoot.host); + }); + + it('should handle pointer events correctly in shadow DOM with portal provider', async () => { + installPointerEvent(); + + const {shadowRoot} = createShadowRoot(); + let interactOutsideCount = 0; + + function PointerEventsExample() { + const ref = useRef(); + useInteractOutside({ + ref, + onInteractOutside: () => { + interactOutsideCount++; + } + }); + + return ( + shadowRoot}> +
+
+ +
+ +
+
+ ); + } + + const {unmount} = render(); + + const targetButton = shadowRoot.querySelector('[data-testid="target-button"]'); + const outsideButton = shadowRoot.querySelector('[data-testid="outside-button"]'); + + // Simulate pointer events on target - should NOT trigger interact outside + fireEvent(targetButton, pointerEvent('pointerdown')); + fireEvent(targetButton, pointerEvent('pointerup')); + fireEvent.click(targetButton); + expect(interactOutsideCount).toBe(0); + + // Simulate pointer events outside - should trigger interact outside + fireEvent(outsideButton, pointerEvent('pointerdown')); + fireEvent(outsideButton, pointerEvent('pointerup')); + fireEvent.click(outsideButton); + expect(interactOutsideCount).toBe(1); + + // Cleanup + unmount(); + document.body.removeChild(shadowRoot.host); + }); + + it('should handle interact outside with dynamic content in shadow DOM', async () => { + const {shadowRoot} = createShadowRoot(); + let interactOutsideCount = 0; + + function DynamicContentExample() { + const ref = useRef(); + const [showContent, setShowContent] = React.useState(true); + + useInteractOutside({ + ref, + onInteractOutside: () => { + interactOutsideCount++; + } + }); + + return ( + shadowRoot}> +
+
+ + {showContent && ( +
+ +
+ )} +
+ +
+
+ ); + } + + const {unmount} = render(); + + const toggleButton = shadowRoot.querySelector('[data-testid="toggle-button"]'); + const dynamicButton = shadowRoot.querySelector('[data-testid="dynamic-button"]'); + const outsideButton = shadowRoot.querySelector('[data-testid="outside-button"]'); + + // Click dynamic content - should NOT trigger interact outside + await user.click(dynamicButton); + expect(interactOutsideCount).toBe(0); + + // Toggle to remove content, then click outside - should trigger interact outside + await user.click(toggleButton); + await user.click(outsideButton); + expect(interactOutsideCount).toBe(1); + + // Toggle content back and click it - should still NOT trigger interact outside + await user.click(toggleButton); + const newDynamicButton = shadowRoot.querySelector('[data-testid="dynamic-button"]'); + await user.click(newDynamicButton); + expect(interactOutsideCount).toBe(1); // Should remain 1 + + // Cleanup + unmount(); + document.body.removeChild(shadowRoot.host); + }); + + it('should handle interact outside across mixed shadow DOM and regular DOM boundaries', async () => { + const {shadowRoot} = createShadowRoot(); + let interactOutsideTriggered = false; + + // Create a regular DOM button outside the shadow DOM + const regularDOMButton = document.createElement('button'); + regularDOMButton.textContent = 'Regular DOM Button'; + regularDOMButton.setAttribute('data-testid', 'regular-dom-button'); + document.body.appendChild(regularDOMButton); + + function MixedDOMExample() { + const ref = useRef(); + useInteractOutside({ + ref, + onInteractOutside: () => { + interactOutsideTriggered = true; + } + }); + + return ( + shadowRoot}> +
+
+ +
+ +
+
+ ); + } + + const {unmount} = render(); + + const shadowButton = shadowRoot.querySelector('[data-testid="shadow-button"]'); + const shadowOutside = shadowRoot.querySelector('[data-testid="shadow-outside"]'); + + // Click inside shadow target - should NOT trigger + await user.click(shadowButton); + expect(interactOutsideTriggered).toBe(false); + + // Click outside in shadow DOM - should trigger + await user.click(shadowOutside); + expect(interactOutsideTriggered).toBe(true); + + // Reset and test regular DOM interaction + interactOutsideTriggered = false; + await user.click(regularDOMButton); + expect(interactOutsideTriggered).toBe(true); + + // Cleanup + document.body.removeChild(regularDOMButton); + unmount(); + document.body.removeChild(shadowRoot.host); + }); +}); + +function pointerEvent(type, opts) { + let evt = new Event(type, {bubbles: true, cancelable: true}); + Object.assign(evt, opts); + return evt; +} diff --git a/packages/@react-aria/overlays/test/usePopover.test.tsx b/packages/@react-aria/overlays/test/usePopover.test.tsx index 1b65f9edf23..a47a3c2f20e 100644 --- a/packages/@react-aria/overlays/test/usePopover.test.tsx +++ b/packages/@react-aria/overlays/test/usePopover.test.tsx @@ -10,10 +10,13 @@ * governing permissions and limitations under the License. */ -import {fireEvent, render} from '@react-spectrum/test-utils-internal'; +import {act, createShadowRoot, fireEvent, pointerMap, render} from '@react-spectrum/test-utils-internal'; +import {enableShadowDOM} from '@react-stately/flags'; import {type OverlayTriggerProps, useOverlayTriggerState} from '@react-stately/overlays'; import React, {useRef} from 'react'; -import {useOverlayTrigger, usePopover} from '../'; +import ReactDOM from 'react-dom'; +import {UNSAFE_PortalProvider, useOverlayTrigger, usePopover} from '../'; +import userEvent from '@testing-library/user-event'; function Example(props: OverlayTriggerProps) { const triggerRef = useRef(null); @@ -39,3 +42,415 @@ describe('usePopover', () => { expect(onOpenChange).not.toHaveBeenCalled(); }); }); + +describe('usePopover with Shadow DOM and UNSAFE_PortalProvider', () => { + let user; + + beforeAll(() => { + enableShadowDOM(); + user = userEvent.setup({delay: null, pointerMap}); + }); + + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + act(() => {jest.runAllTimers();}); + }); + + it('should handle popover interactions within shadow DOM with UNSAFE_PortalProvider', async () => { + const {shadowRoot} = createShadowRoot(); + let triggerClicked = false; + let popoverInteracted = false; + + function ShadowPopoverExample() { + const triggerRef = useRef(null); + const popoverRef = useRef(null); + const state = useOverlayTriggerState({ + defaultOpen: false, + onOpenChange: (isOpen) => { + // Track state changes + } + }); + + useOverlayTrigger({type: 'listbox'}, state, triggerRef); + const {popoverProps} = usePopover({ + triggerRef, + popoverRef, + placement: 'bottom start' + }, state); + + return ( + shadowRoot}> +
+ + {state.isOpen && ( +
+ + +
+ )} +
+
+ ); + } + + const {unmount} = render(); + + const trigger = shadowRoot.querySelector('[data-testid="popover-trigger"]'); + + // Click trigger to open popover + await user.click(trigger); + expect(triggerClicked).toBe(true); + + // Verify popover opened in shadow DOM + const popoverContent = shadowRoot.querySelector('[data-testid="popover-content"]'); + expect(popoverContent).toBeInTheDocument(); + + // Interact with popover content + const popoverAction = shadowRoot.querySelector('[data-testid="popover-action"]'); + await user.click(popoverAction); + expect(popoverInteracted).toBe(true); + + // Popover should still be open after interaction + expect(shadowRoot.querySelector('[data-testid="popover-content"]')).toBeInTheDocument(); + + // Close popover + const closeButton = shadowRoot.querySelector('[data-testid="close-popover"]'); + await user.click(closeButton); + + // Wait for any cleanup + act(() => {jest.runAllTimers();}); + + // Cleanup + unmount(); + document.body.removeChild(shadowRoot.host); + }); + + it('should handle focus management in shadow DOM popover with nested interactive elements', async () => { + const {shadowRoot} = createShadowRoot(); + + function FocusTestPopover() { + const triggerRef = useRef(null); + const popoverRef = useRef(null); + const state = useOverlayTriggerState({defaultOpen: true}); + + useOverlayTrigger({type: 'dialog'}, state, triggerRef); + const {popoverProps} = usePopover({ + triggerRef, + popoverRef + }, state); + + return ( + shadowRoot}> +
+ + {state.isOpen && ( +
+
+ + + +
+
+ )} +
+
+ ); + } + + const {unmount} = render(); + + const menuItem1 = shadowRoot.querySelector('[data-testid="menu-item-1"]'); + const menuItem2 = shadowRoot.querySelector('[data-testid="menu-item-2"]'); + const menuItem3 = shadowRoot.querySelector('[data-testid="menu-item-3"]'); + + // Focus first menu item + act(() => { menuItem1.focus(); }); + expect(shadowRoot.activeElement).toBe(menuItem1); + + // Tab through menu items + await user.tab(); + expect(shadowRoot.activeElement).toBe(menuItem2); + + await user.tab(); + expect(shadowRoot.activeElement).toBe(menuItem3); + + // Cleanup + unmount(); + document.body.removeChild(shadowRoot.host); + }); + + it('should properly handle click events on popover content within shadow DOM (issue #8675)', async () => { + const {shadowRoot} = createShadowRoot(); + let menuActionExecuted = false; + let popoverClosedUnexpectedly = false; + + function MenuPopoverExample() { + const triggerRef = useRef(null); + const popoverRef = useRef(null); + const [isOpen, setIsOpen] = React.useState(true); + + const state = useOverlayTriggerState({ + isOpen, + onOpenChange: (open) => { + setIsOpen(open); + if (!open) { + popoverClosedUnexpectedly = true; + } + } + }); + + useOverlayTrigger({type: 'listbox'}, state, triggerRef); + const {popoverProps} = usePopover({ + triggerRef, + popoverRef + }, state); + + const handleMenuAction = (action) => { + menuActionExecuted = true; + // In the buggy version, this wouldn't execute because popover closes first + console.log('Menu action:', action); + }; + + return ( + shadowRoot}> +
+ + {state.isOpen && ( +
+
+ + +
+
+ )} +
+
+ ); + } + + const {unmount} = render(); + + const saveItem = shadowRoot.querySelector('[data-testid="save-item"]'); + const menuPopover = shadowRoot.querySelector('[data-testid="menu-popover"]'); + + // Verify popover is initially open + expect(menuPopover).toBeInTheDocument(); + + // Focus the menu item + act(() => { saveItem.focus(); }); + expect(shadowRoot.activeElement).toBe(saveItem); + + // Click the menu item - this should execute the action, NOT close the popover + await user.click(saveItem); + + // The action should have been executed (this fails in the buggy version) + expect(menuActionExecuted).toBe(true); + + // The popover should NOT have closed unexpectedly (this fails in the buggy version) + expect(popoverClosedUnexpectedly).toBe(false); + + // Menu should still be visible + expect(shadowRoot.querySelector('[data-testid="menu-popover"]')).toBeInTheDocument(); + + // Cleanup + unmount(); + document.body.removeChild(shadowRoot.host); + }); + + it('should handle multiple overlapping popovers in shadow DOM with portal provider', async () => { + const {shadowRoot} = createShadowRoot(); + + function MultiplePopoversExample() { + const trigger1Ref = useRef(null); + const popover1Ref = useRef(null); + const trigger2Ref = useRef(null); + const popover2Ref = useRef(null); + + const state1 = useOverlayTriggerState({defaultOpen: true}); + const state2 = useOverlayTriggerState({defaultOpen: true}); + + useOverlayTrigger({type: 'dialog'}, state1, trigger1Ref); + useOverlayTrigger({type: 'dialog'}, state2, trigger2Ref); + + const {popoverProps: popover1Props} = usePopover({ + triggerRef: trigger1Ref, + popoverRef: popover1Ref + }, state1); + + const {popoverProps: popover2Props} = usePopover({ + triggerRef: trigger2Ref, + popoverRef: popover2Ref + }, state2); + + return ( + shadowRoot}> +
+ + + + {state1.isOpen && ( +
+ +
+ )} + + {state2.isOpen && ( +
+ +
+ )} +
+
+ ); + } + + const {unmount} = render(); + + const popover1 = shadowRoot.querySelector('[data-testid="popover-1"]'); + const popover2 = shadowRoot.querySelector('[data-testid="popover-2"]'); + const popover1Action = shadowRoot.querySelector('[data-testid="popover-1-action"]'); + const popover2Action = shadowRoot.querySelector('[data-testid="popover-2-action"]'); + + // Both popovers should be present + expect(popover1).toBeInTheDocument(); + expect(popover2).toBeInTheDocument(); + + // Should be able to interact with both popovers + await user.click(popover1Action); + await user.click(popover2Action); + + // Both should still be present after interactions + expect(shadowRoot.querySelector('[data-testid="popover-1"]')).toBeInTheDocument(); + expect(shadowRoot.querySelector('[data-testid="popover-2"]')).toBeInTheDocument(); + + // Cleanup + unmount(); + document.body.removeChild(shadowRoot.host); + }); + + it('should handle popover positioning and containment in shadow DOM', async () => { + const {shadowRoot} = createShadowRoot(); + + function PositionedPopoverExample() { + const triggerRef = useRef(null); + const popoverRef = useRef(null); + const state = useOverlayTriggerState({defaultOpen: true}); + + useOverlayTrigger({type: 'listbox'}, state, triggerRef); + const {popoverProps} = usePopover({ + triggerRef, + popoverRef, + placement: 'bottom start', + containerPadding: 12 + }, state); + + return ( + shadowRoot}> +
+ + {state.isOpen && ( +
+
+

This is a positioned popover

+ +
+
+ )} +
+
+ ); + } + + const {unmount} = render(); + + const trigger = shadowRoot.querySelector('[data-testid="positioned-trigger"]'); + const popover = shadowRoot.querySelector('[data-testid="positioned-popover"]'); + const actionButton = shadowRoot.querySelector('[data-testid="action-button"]'); + + // Verify popover exists and is positioned + expect(popover).toBeInTheDocument(); + expect(trigger).toBeInTheDocument(); + + // Verify we can interact with popover content + await user.click(actionButton); + + // Popover should still be present after interaction + expect(shadowRoot.querySelector('[data-testid="positioned-popover"]')).toBeInTheDocument(); + + // Cleanup + unmount(); + document.body.removeChild(shadowRoot.host); + }); +}); From 322c7cf53fa9ac773f049020c4a8fb919ea59e15 Mon Sep 17 00:00:00 2001 From: John Pangalos Date: Fri, 15 Aug 2025 20:30:25 +0200 Subject: [PATCH 2/6] Add patch and fixing some tests. After applying the patches I mentioned in my issue I've noticed that many of the AI generated tests were either broken or just not testing anything interesting. Luckily there are some that are. I'll have to update the rest of the tests as they aren't passing at the moment. --- packages/@react-aria/focus/src/FocusScope.tsx | 9 +- .../@react-aria/focus/test/FocusScope.test.js | 1835 +++++++++-------- .../interactions/src/useFocusWithin.ts | 6 +- .../interactions/src/useInteractOutside.ts | 4 +- .../test/useInteractOutside.test.js | 634 +++--- .../overlays/src/ariaHideOutside.ts | 13 +- .../utils/src/shadowdom/DOMFunctions.ts | 2 +- .../react-aria-components/src/Popover.tsx | 4 +- 8 files changed, 1308 insertions(+), 1199 deletions(-) diff --git a/packages/@react-aria/focus/src/FocusScope.tsx b/packages/@react-aria/focus/src/FocusScope.tsx index 6dad540400a..0f3334993b4 100644 --- a/packages/@react-aria/focus/src/FocusScope.tsx +++ b/packages/@react-aria/focus/src/FocusScope.tsx @@ -19,6 +19,7 @@ import { isChrome, isFocusable, isTabbable, + nodeContains, ShadowTreeWalker, useLayoutEffect } from '@react-aria/utils'; @@ -440,7 +441,7 @@ function isElementInScope(element?: Element | null, scope?: Element[] | null) { if (!scope) { return false; } - return scope.some(node => node.contains(element)); + return scope.some(node => nodeContains(node, element)); } function isElementInChildScope(element: Element, scope: ScopeRef = null) { @@ -771,7 +772,7 @@ export function getFocusableTreeWalker(root: Element, opts?: FocusManagerOptions { acceptNode(node) { // Skip nodes inside the starting node. - if (opts?.from?.contains(node)) { + if (opts?.from && nodeContains(opts.from, node)) { return NodeFilter.FILTER_REJECT; } @@ -822,7 +823,7 @@ export function createFocusManager(ref: RefObject, defaultOption let {from, tabbable = defaultOptions.tabbable, wrap = defaultOptions.wrap, accept = defaultOptions.accept} = opts; let node = from || getActiveElement(getOwnerDocument(root)); let walker = getFocusableTreeWalker(root, {tabbable, accept}); - if (root.contains(node)) { + if (nodeContains(root, node)) { walker.currentNode = node!; } let nextNode = walker.nextNode() as FocusableElement; @@ -843,7 +844,7 @@ export function createFocusManager(ref: RefObject, defaultOption let {from, tabbable = defaultOptions.tabbable, wrap = defaultOptions.wrap, accept = defaultOptions.accept} = opts; let node = from || getActiveElement(getOwnerDocument(root)); let walker = getFocusableTreeWalker(root, {tabbable, accept}); - if (root.contains(node)) { + if (nodeContains(root, node)) { walker.currentNode = node!; } else { let next = last(walker); diff --git a/packages/@react-aria/focus/test/FocusScope.test.js b/packages/@react-aria/focus/test/FocusScope.test.js index f36876d2f37..5e91ef2b3a2 100644 --- a/packages/@react-aria/focus/test/FocusScope.test.js +++ b/packages/@react-aria/focus/test/FocusScope.test.js @@ -10,25 +10,32 @@ * governing permissions and limitations under the License. */ -import {act, createShadowRoot, fireEvent, pointerMap, render, waitFor} from '@react-spectrum/test-utils-internal'; -import {defaultTheme} from '@adobe/react-spectrum'; -import {DialogContainer} from '@react-spectrum/dialog'; -import {enableShadowDOM} from '@react-stately/flags'; -import {FocusScope, useFocusManager} from '../'; -import {focusScopeTree} from '../src/FocusScope'; -import {Provider} from '@react-spectrum/provider'; -import React, {useEffect, useState} from 'react'; -import ReactDOM from 'react-dom'; -import {Example as StorybookExample} from '../stories/FocusScope.stories'; -import {UNSAFE_PortalProvider} from '@react-aria/overlays'; -import {useEvent} from '@react-aria/utils'; -import userEvent from '@testing-library/user-event'; - -describe('FocusScope', function () { +import { + act, + createShadowRoot, + fireEvent, + pointerMap, + render, + waitFor, +} from "@react-spectrum/test-utils-internal"; +import { defaultTheme } from "@adobe/react-spectrum"; +import { DialogContainer } from "@react-spectrum/dialog"; +import { enableShadowDOM } from "@react-stately/flags"; +import { FocusScope, useFocusManager } from "../"; +import { focusScopeTree } from "../src/FocusScope"; +import { Provider } from "@react-spectrum/provider"; +import React, { useEffect, useState } from "react"; +import ReactDOM from "react-dom"; +import { Example as StorybookExample } from "../stories/FocusScope.stories"; +import { UNSAFE_PortalProvider } from "@react-aria/overlays"; +import { useEvent } from "@react-aria/utils"; +import userEvent from "@testing-library/user-event"; + +describe("FocusScope", function () { let user; beforeAll(() => { - user = userEvent.setup({delay: null, pointerMap}); + user = userEvent.setup({ delay: null, pointerMap }); }); beforeEach(() => { @@ -36,24 +43,28 @@ describe('FocusScope', function () { }); afterEach(() => { // make sure to clean up any raf's that may be running to restore focus on unmount - act(() => {jest.runAllTimers();}); + act(() => { + jest.runAllTimers(); + }); }); - describe('focus containment', function () { - it('should contain focus within the scope', async function () { - let {getByTestId} = render( + describe("focus containment", function () { + it("should contain focus within the scope", async function () { + let { getByTestId } = render( - + , ); - let input1 = getByTestId('input1'); - let input2 = getByTestId('input2'); - let input3 = getByTestId('input3'); + let input1 = getByTestId("input1"); + let input2 = getByTestId("input2"); + let input3 = getByTestId("input3"); - act(() => {input1.focus();}); + act(() => { + input1.focus(); + }); expect(document.activeElement).toBe(input1); await user.tab(); @@ -65,18 +76,18 @@ describe('FocusScope', function () { await user.tab(); expect(document.activeElement).toBe(input1); - await user.tab({shift: true}); + await user.tab({ shift: true }); expect(document.activeElement).toBe(input3); - await user.tab({shift: true}); + await user.tab({ shift: true }); expect(document.activeElement).toBe(input2); - await user.tab({shift: true}); + await user.tab({ shift: true }); expect(document.activeElement).toBe(input1); }); - it('should work with nested elements', async function () { - let {getByTestId} = render( + it("should work with nested elements", async function () { + let { getByTestId } = render(
@@ -85,14 +96,16 @@ describe('FocusScope', function () {
-
+ , ); - let input1 = getByTestId('input1'); - let input2 = getByTestId('input2'); - let input3 = getByTestId('input3'); + let input1 = getByTestId("input1"); + let input2 = getByTestId("input2"); + let input3 = getByTestId("input3"); - act(() => {input1.focus();}); + act(() => { + input1.focus(); + }); expect(document.activeElement).toBe(input1); await user.tab(); @@ -104,37 +117,39 @@ describe('FocusScope', function () { await user.tab(); expect(document.activeElement).toBe(input1); - await user.tab({shift: true}); + await user.tab({ shift: true }); expect(document.activeElement).toBe(input3); - await user.tab({shift: true}); + await user.tab({ shift: true }); expect(document.activeElement).toBe(input2); - await user.tab({shift: true}); + await user.tab({ shift: true }); expect(document.activeElement).toBe(input1); }); - it('should skip non-tabbable elements', async function () { - let {getByTestId} = render( + it("should skip non-tabbable elements", async function () { + let { getByTestId } = render(
- - - + + +
- + , ); - let input1 = getByTestId('input1'); - let input2 = getByTestId('input2'); - let input3 = getByTestId('input3'); + let input1 = getByTestId("input1"); + let input2 = getByTestId("input2"); + let input3 = getByTestId("input3"); - act(() => {input1.focus();}); + act(() => { + input1.focus(); + }); expect(document.activeElement).toBe(input1); await user.tab(); @@ -146,18 +161,18 @@ describe('FocusScope', function () { await user.tab(); expect(document.activeElement).toBe(input1); - await user.tab({shift: true}); + await user.tab({ shift: true }); expect(document.activeElement).toBe(input3); - await user.tab({shift: true}); + await user.tab({ shift: true }); expect(document.activeElement).toBe(input2); - await user.tab({shift: true}); + await user.tab({ shift: true }); expect(document.activeElement).toBe(input1); }); - it('should only skip content editable which are false', async function () { - let {getByTestId} = render( + it("should only skip content editable which are false", async function () { + let { getByTestId } = render( @@ -165,15 +180,17 @@ describe('FocusScope', function () { - + , ); - let input1 = getByTestId('input1'); - let input2 = getByTestId('input2'); - let input3 = getByTestId('input3'); - let input4 = getByTestId('input4'); + let input1 = getByTestId("input1"); + let input2 = getByTestId("input2"); + let input3 = getByTestId("input3"); + let input4 = getByTestId("input4"); - act(() => {input1.focus();}); + act(() => { + input1.focus(); + }); expect(document.activeElement).toBe(input1); await user.tab(); @@ -185,62 +202,66 @@ describe('FocusScope', function () { await user.tab(); expect(document.activeElement).toBe(input4); - await user.tab({shift: true}); + await user.tab({ shift: true }); expect(document.activeElement).toBe(input3); - await user.tab({shift: true}); + await user.tab({ shift: true }); expect(document.activeElement).toBe(input2); - await user.tab({shift: true}); + await user.tab({ shift: true }); expect(document.activeElement).toBe(input1); }); - it('should do nothing if a modifier key is pressed', function () { - let {getByTestId} = render( + it("should do nothing if a modifier key is pressed", function () { + let { getByTestId } = render( - + , ); - let input1 = getByTestId('input1'); + let input1 = getByTestId("input1"); - act(() => {input1.focus();}); + act(() => { + input1.focus(); + }); expect(document.activeElement).toBe(input1); - fireEvent.keyDown(document.activeElement, {key: 'Tab', altKey: true}); + fireEvent.keyDown(document.activeElement, { key: "Tab", altKey: true }); expect(document.activeElement).toBe(input1); }); - it('should work with multiple focus scopes', async function () { - let {getByTestId} = render( + it("should work with multiple focus scopes", async function () { + let { getByTestId } = render(
- - - + + + - - - + + + -
+
, ); - let input1 = getByTestId('input1'); - let input2 = getByTestId('input2'); - let input3 = getByTestId('input3'); - let input4 = getByTestId('input4'); + let input1 = getByTestId("input1"); + let input2 = getByTestId("input2"); + let input3 = getByTestId("input3"); + let input4 = getByTestId("input4"); - act(() => {input1.focus();}); + act(() => { + input1.focus(); + }); expect(document.activeElement).toBe(input1); await user.tab(); @@ -252,21 +273,23 @@ describe('FocusScope', function () { await user.tab(); expect(document.activeElement).toBe(input1); - await user.tab({shift: true}); + await user.tab({ shift: true }); expect(document.activeElement).toBe(input3); - await user.tab({shift: true}); + await user.tab({ shift: true }); expect(document.activeElement).toBe(input2); - await user.tab({shift: true}); + await user.tab({ shift: true }); expect(document.activeElement).toBe(input1); - act(() => {input4.focus();}); + act(() => { + input4.focus(); + }); expect(document.activeElement).toBe(input1); }); - it('should restore focus to the last focused element in the scope when re-entering the browser', async function () { - let {getByTestId} = render( + it("should restore focus to the last focused element in the scope when re-entering the browser", async function () { + let { getByTestId } = render(
@@ -274,75 +297,95 @@ describe('FocusScope', function () { -
+
, ); - let input1 = getByTestId('input1'); - let input2 = getByTestId('input2'); - let outside = getByTestId('outside'); + let input1 = getByTestId("input1"); + let input2 = getByTestId("input2"); + let outside = getByTestId("outside"); - act(() => {input1.focus();}); + act(() => { + input1.focus(); + }); fireEvent.focusIn(input1); // jsdom doesn't fire this automatically expect(document.activeElement).toBe(input1); await user.tab(); fireEvent.focusIn(input2); - act(() => {jest.runAllTimers();}); + act(() => { + jest.runAllTimers(); + }); expect(document.activeElement).toBe(input2); - act(() => {input2.blur();}); - act(() => {jest.runAllTimers();}); + act(() => { + input2.blur(); + }); + act(() => { + jest.runAllTimers(); + }); expect(document.activeElement).toBe(input2); - act(() => {outside.focus();}); + act(() => { + outside.focus(); + }); fireEvent.focusIn(outside); expect(document.activeElement).toBe(input2); }); - it('should restore focus to the last focused element in the scope on focus out', async function () { - let {getByTestId} = render( + it("should restore focus to the last focused element in the scope on focus out", async function () { + let { getByTestId } = render(
-
+ , ); - let input1 = getByTestId('input1'); - let input2 = getByTestId('input2'); + let input1 = getByTestId("input1"); + let input2 = getByTestId("input2"); - act(() => {input1.focus();}); + act(() => { + input1.focus(); + }); fireEvent.focusIn(input1); // jsdom doesn't fire this automatically expect(document.activeElement).toBe(input1); await user.tab(); fireEvent.focusIn(input2); - act(() => {jest.runAllTimers();}); + act(() => { + jest.runAllTimers(); + }); expect(document.activeElement).toBe(input2); - act(() => {input2.blur();}); - act(() => {jest.runAllTimers();}); + act(() => { + input2.blur(); + }); + act(() => { + jest.runAllTimers(); + }); expect(document.activeElement).toBe(input2); fireEvent.focusOut(input2); expect(document.activeElement).toBe(input2); }); // This test setup is a bit contrived to just purely simulate the blur/focus events that would happen in a case like this - it('focus properly moves into child iframe on click', function () { - let {getByTestId} = render( + it("focus properly moves into child iframe on click", function () { + let { getByTestId } = render(
-
+ , ); - let input1 = getByTestId('input1'); - let input2 = getByTestId('input2'); + let input1 = getByTestId("input1"); + let input2 = getByTestId("input2"); - act(() => {input1.focus();}); + act(() => { + input1.focus(); + }); fireEvent.focusIn(input1); // jsdom doesn't fire this automatically expect(document.activeElement).toBe(input1); @@ -350,311 +393,343 @@ describe('FocusScope', function () { // set document.activeElement to input2 input2.focus(); // if onBlur didn't fallback to checking document.activeElement, this would reset focus to input1 - fireEvent.blur(input1, {relatedTarget: null}); + fireEvent.blur(input1, { relatedTarget: null }); }); expect(document.activeElement).toBe(input2); }); }); - describe('focus restoration', function () { - it('should restore focus to the previously focused node on unmount', function () { - function Test({show}) { + describe("focus restoration", function () { + it("should restore focus to the previously focused node on unmount", function () { + function Test({ show }) { return (
- {show && + {show && ( - } + )}
); } - let {getByTestId, rerender} = render(); + let { getByTestId, rerender } = render(); - let outside = getByTestId('outside'); - act(() => {outside.focus();}); + let outside = getByTestId("outside"); + act(() => { + outside.focus(); + }); rerender(); - let input1 = getByTestId('input1'); + let input1 = getByTestId("input1"); expect(document.activeElement).toBe(input1); rerender(); - act(() => {jest.runAllTimers();}); + act(() => { + jest.runAllTimers(); + }); expect(document.activeElement).toBe(outside); }); - it('should restore focus to the previously focused node after a child with autoFocus unmounts', function () { - function Test({show}) { + it("should restore focus to the previously focused node after a child with autoFocus unmounts", function () { + function Test({ show }) { return (
- {show && + {show && ( - } + )}
); } - let {getByTestId, rerender} = render(); + let { getByTestId, rerender } = render(); - let outside = getByTestId('outside'); - act(() => {outside.focus();}); + let outside = getByTestId("outside"); + act(() => { + outside.focus(); + }); rerender(); - let input2 = getByTestId('input2'); + let input2 = getByTestId("input2"); expect(document.activeElement).toBe(input2); rerender(); - act(() => {jest.runAllTimers();}); + act(() => { + jest.runAllTimers(); + }); expect(document.activeElement).toBe(outside); }); - it('should move focus after the previously focused node when tabbing away from a scope with autoFocus', async function () { - function Test({show}) { + it("should move focus after the previously focused node when tabbing away from a scope with autoFocus", async function () { + function Test({ show }) { return (
- {show && + {show && ( - } + )}
); } - let {getByTestId, rerender} = render(); + let { getByTestId, rerender } = render(); - let outside = getByTestId('outside'); - act(() => {outside.focus();}); + let outside = getByTestId("outside"); + act(() => { + outside.focus(); + }); rerender(); - let input3 = getByTestId('input3'); + let input3 = getByTestId("input3"); expect(document.activeElement).toBe(input3); await user.tab(); - expect(document.activeElement).toBe(getByTestId('after')); + expect(document.activeElement).toBe(getByTestId("after")); }); - it('should move focus before the previously focused node when tabbing away from a scope with Shift+Tab', async function () { - function Test({show}) { + it("should move focus before the previously focused node when tabbing away from a scope with Shift+Tab", async function () { + function Test({ show }) { return (
- {show && + {show && ( - } + )}
); } - let {getByTestId, rerender} = render(); + let { getByTestId, rerender } = render(); - let outside = getByTestId('outside'); - act(() => {outside.focus();}); + let outside = getByTestId("outside"); + act(() => { + outside.focus(); + }); rerender(); - let input1 = getByTestId('input1'); + let input1 = getByTestId("input1"); expect(document.activeElement).toBe(input1); - await user.tab({shift: true}); - expect(document.activeElement).toBe(getByTestId('before')); + await user.tab({ shift: true }); + expect(document.activeElement).toBe(getByTestId("before")); }); - it('should restore focus to the previously focused node after children change', function () { - function Test({show, showChild}) { + it("should restore focus to the previously focused node after children change", function () { + function Test({ show, showChild }) { return (
- {show && + {show && ( {showChild && } - } + )}
); } - let {getByTestId, rerender} = render(); + let { getByTestId, rerender } = render(); - let outside = getByTestId('outside'); - act(() => {outside.focus();}); + let outside = getByTestId("outside"); + act(() => { + outside.focus(); + }); rerender(); rerender(); - let dynamic = getByTestId('dynamic'); - act(() => {dynamic.focus();}); + let dynamic = getByTestId("dynamic"); + act(() => { + dynamic.focus(); + }); expect(document.activeElement).toBe(dynamic); rerender(); - act(() => {jest.runAllTimers();}); + act(() => { + jest.runAllTimers(); + }); expect(document.activeElement).toBe(outside); }); - it('should move focus to the element after the previously focused node on Tab', async function () { - function Test({show}) { + it("should move focus to the element after the previously focused node on Tab", async function () { + function Test({ show }) { return (
); } - let {getByTestId, rerender} = render(); + let { getByTestId, rerender } = render(); - let trigger = getByTestId('trigger'); - act(() => {trigger.focus();}); + let trigger = getByTestId("trigger"); + act(() => { + trigger.focus(); + }); rerender(); - let input1 = getByTestId('input1'); + let input1 = getByTestId("input1"); expect(document.activeElement).toBe(input1); - let input3 = getByTestId('input3'); - act(() => {input3.focus();}); + let input3 = getByTestId("input3"); + act(() => { + input3.focus(); + }); await user.tab(); - expect(document.activeElement).toBe(getByTestId('after')); + expect(document.activeElement).toBe(getByTestId("after")); }); - it('should move focus to the previous element after the previously focused node on Shift+Tab', async function () { - function Test({show}) { + it("should move focus to the previous element after the previously focused node on Shift+Tab", async function () { + function Test({ show }) { return (
); } - let {getByTestId, rerender} = render(); + let { getByTestId, rerender } = render(); - let trigger = getByTestId('trigger'); - act(() => {trigger.focus();}); + let trigger = getByTestId("trigger"); + act(() => { + trigger.focus(); + }); rerender(); - let input1 = getByTestId('input1'); + let input1 = getByTestId("input1"); expect(document.activeElement).toBe(input1); - await user.tab({shift: true}); - expect(document.activeElement).toBe(getByTestId('before')); + await user.tab({ shift: true }); + expect(document.activeElement).toBe(getByTestId("before")); }); - it('should skip over elements within the scope when moving focus to the next element', async function () { - function Test({show}) { + it("should skip over elements within the scope when moving focus to the next element", async function () { + function Test({ show }) { return (
); } - let {getByTestId, rerender} = render(); + let { getByTestId, rerender } = render(); - let trigger = getByTestId('trigger'); - act(() => {trigger.focus();}); + let trigger = getByTestId("trigger"); + act(() => { + trigger.focus(); + }); rerender(); - let input1 = getByTestId('input1'); + let input1 = getByTestId("input1"); expect(document.activeElement).toBe(input1); - let input3 = getByTestId('input3'); - act(() => {input3.focus();}); + let input3 = getByTestId("input3"); + act(() => { + input3.focus(); + }); await user.tab(); - expect(document.activeElement).toBe(getByTestId('after')); + expect(document.activeElement).toBe(getByTestId("after")); }); - it('should not handle tabbing if the focus scope does not restore focus', async function () { - function Test({show}) { + it("should not handle tabbing if the focus scope does not restore focus", async function () { + function Test({ show }) { return (
); } - let {getByTestId, rerender} = render(); + let { getByTestId, rerender } = render(); - let trigger = getByTestId('trigger'); - act(() => {trigger.focus();}); + let trigger = getByTestId("trigger"); + act(() => { + trigger.focus(); + }); rerender(); - let input1 = getByTestId('input1'); + let input1 = getByTestId("input1"); expect(document.activeElement).toBe(input1); - let input3 = getByTestId('input3'); - act(() => {input3.focus();}); + let input3 = getByTestId("input3"); + act(() => { + input3.focus(); + }); await user.tab(); - expect(document.activeElement).toBe(getByTestId('after')); + expect(document.activeElement).toBe(getByTestId("after")); }); it.each` @@ -663,58 +738,86 @@ describe('FocusScope', function () { ${true} | ${false} ${false} | ${true} ${true} | ${true} - `('contain=$contain, isPortaled=$isPortaled should restore focus to previous nodeToRestore when the nodeToRestore for the unmounting scope in no longer in the DOM', - async function ({contain, isPortaled}) { - expect(focusScopeTree.size).toBe(1); - let {getAllByText, getAllByRole} = render(); - expect(focusScopeTree.size).toBe(1); - act(() => {getAllByText('Open dialog')[0].focus();}); - await user.click(document.activeElement); - act(() => {jest.runAllTimers();}); - expect(document.activeElement).toBe(getAllByRole('textbox')[2]); - act(() => {getAllByText('Open dialog')[1].focus();}); - await user.click(document.activeElement); - act(() => {jest.runAllTimers();}); - expect(document.activeElement).toBe(getAllByRole('textbox')[5]); - act(() => {getAllByText('Open dialog')[2].focus();}); - await user.click(document.activeElement); - act(() => {jest.runAllTimers();}); - expect(document.activeElement).toBe(getAllByRole('textbox')[8]); - expect(focusScopeTree.size).toBe(4); - if (!contain) { + `( + "contain=$contain, isPortaled=$isPortaled should restore focus to previous nodeToRestore when the nodeToRestore for the unmounting scope in no longer in the DOM", + async function ({ contain, isPortaled }) { + expect(focusScopeTree.size).toBe(1); + let { getAllByText, getAllByRole } = render( + , + ); + expect(focusScopeTree.size).toBe(1); act(() => { - getAllByText('close')[1].focus(); + getAllByText("Open dialog")[0].focus(); }); await user.click(document.activeElement); - } else { - fireEvent.click(getAllByText('close')[1]); - } - act(() => {jest.runAllTimers();}); - expect(document.activeElement).toBe(getAllByText('Open dialog')[1]); - act(() => {getAllByText('close')[0].focus();}); - await user.click(document.activeElement); - act(() => {jest.runAllTimers();}); - expect(document.activeElement).toBe(getAllByText('Open dialog')[0]); - expect(focusScopeTree.size).toBe(1); - }); + act(() => { + jest.runAllTimers(); + }); + expect(document.activeElement).toBe(getAllByRole("textbox")[2]); + act(() => { + getAllByText("Open dialog")[1].focus(); + }); + await user.click(document.activeElement); + act(() => { + jest.runAllTimers(); + }); + expect(document.activeElement).toBe(getAllByRole("textbox")[5]); + act(() => { + getAllByText("Open dialog")[2].focus(); + }); + await user.click(document.activeElement); + act(() => { + jest.runAllTimers(); + }); + expect(document.activeElement).toBe(getAllByRole("textbox")[8]); + expect(focusScopeTree.size).toBe(4); + if (!contain) { + act(() => { + getAllByText("close")[1].focus(); + }); + await user.click(document.activeElement); + } else { + fireEvent.click(getAllByText("close")[1]); + } + act(() => { + jest.runAllTimers(); + }); + expect(document.activeElement).toBe(getAllByText("Open dialog")[1]); + act(() => { + getAllByText("close")[0].focus(); + }); + await user.click(document.activeElement); + act(() => { + jest.runAllTimers(); + }); + expect(document.activeElement).toBe(getAllByText("Open dialog")[0]); + expect(focusScopeTree.size).toBe(1); + }, + ); - describe('focusable first in scope', function () { - it('should restore focus to the first focusable or tabbable element within the scope when focus is lost within the scope', async function () { - let {getByTestId} = render( + describe("focusable first in scope", function () { + it("should restore focus to the first focusable or tabbable element within the scope when focus is lost within the scope", async function () { + let { getByTestId } = render(
- Remove me! - Remove me, too! - Remove me, three! + + Remove me! + + + Remove me, too! + + + Remove me, three! +
-
+ , ); function Item(props) { let focusManager = useFocusManager(); - let onClick = e => { + let onClick = (e) => { focusManager.focusNext(); act(() => { // remove fails to fire blur event in jest-dom @@ -725,10 +828,10 @@ describe('FocusScope', function () { }; return {' '} + onClick={() => setDisplay((state) => !state)} + > + {display ? "Close dialog" : "Open dialog"} + {" "} {display && ( @@ -780,68 +885,78 @@ describe('FocusScope', function () { ); } - let {getByTestId} = render(); - let button1 = getByTestId('button1'); - let button2 = getByTestId('button2'); + let { getByTestId } = render(); + let button1 = getByTestId("button1"); + let button2 = getByTestId("button2"); await user.click(button1); - act(() => {jest.runAllTimers();}); + act(() => { + jest.runAllTimers(); + }); expect(document.activeElement).toBe(button1); - let input1 = getByTestId('input1'); + let input1 = getByTestId("input1"); expect(input1).toBeVisible(); await user.click(button2); - act(() => {jest.runAllTimers();}); + act(() => { + jest.runAllTimers(); + }); expect(document.activeElement).toBe(button2); expect(input1).not.toBeInTheDocument(); await user.click(button1); - act(() => {jest.runAllTimers();}); - input1 = getByTestId('input1'); + act(() => { + jest.runAllTimers(); + }); + input1 = getByTestId("input1"); expect(input1).toBeVisible(); await user.tab(); - fireEvent.keyDown(document.activeElement, {key: 'Escape'}); - fireEvent.keyUp(document.activeElement, {key: 'Escape'}); - act(() => {jest.runAllTimers();}); + fireEvent.keyDown(document.activeElement, { key: "Escape" }); + fireEvent.keyUp(document.activeElement, { key: "Escape" }); + act(() => { + jest.runAllTimers(); + }); expect(document.activeElement).toBe(button2); expect(input1).not.toBeInTheDocument(); }); - it('should allow restoration to be overridden with a custom event', async function () { + it("should allow restoration to be overridden with a custom event", async function () { function Test() { let [show, setShow] = React.useState(false); let ref = React.useRef(null); - useEvent(ref, 'react-aria-focus-scope-restore', e => { + useEvent(ref, "react-aria-focus-scope-restore", (e) => { e.preventDefault(); }); return (
- {show && - setShow(false)} /> - } + {show && ( + + setShow(false)} /> + + )}
); } - let {getByRole} = render(); - let button = getByRole('button'); + let { getByRole } = render(); + let button = getByRole("button"); await user.click(button); - let input = getByRole('textbox'); + let input = getByRole("textbox"); expect(document.activeElement).toBe(input); - await user.keyboard('{Escape}'); + await user.keyboard("{Escape}"); act(() => jest.runAllTimers()); expect(input).not.toBeInTheDocument(); expect(document.activeElement).toBe(document.body); }); - it('should not bubble focus scope restoration event out of nested focus scopes', async function () { + it("should not bubble focus scope restoration event out of nested focus scopes", async function () { function Test() { let [show, setShow] = React.useState(false); let ref = React.useRef(null); - useEvent(ref, 'react-aria-focus-scope-restore', e => { + useEvent(ref, "react-aria-focus-scope-restore", (e) => { e.preventDefault(); }); @@ -849,62 +964,66 @@ describe('FocusScope', function () {
- {show && - setShow(false)} /> - } + {show && ( + + setShow(false)} /> + + )}
); } - let {getByRole} = render(); - let button = getByRole('button'); + let { getByRole } = render(); + let button = getByRole("button"); await user.click(button); - let input = getByRole('textbox'); + let input = getByRole("textbox"); expect(document.activeElement).toBe(input); - await user.keyboard('{Escape}'); + await user.keyboard("{Escape}"); act(() => jest.runAllTimers()); expect(input).not.toBeInTheDocument(); expect(document.activeElement).toBe(button); }); }); - describe('auto focus', function () { - it('should auto focus the first tabbable element in the scope on mount', function () { - let {getByTestId} = render( + describe("auto focus", function () { + it("should auto focus the first tabbable element in the scope on mount", function () { + let { getByTestId } = render(
- + , ); - act(() => {jest.runAllTimers();}); + act(() => { + jest.runAllTimers(); + }); - let input1 = getByTestId('input1'); + let input1 = getByTestId("input1"); expect(document.activeElement).toBe(input1); }); - it('should do nothing if something is already focused in the scope', function () { - let {getByTestId} = render( + it("should do nothing if something is already focused in the scope", function () { + let { getByTestId } = render(
- + , ); - let input2 = getByTestId('input2'); + let input2 = getByTestId("input2"); expect(document.activeElement).toBe(input2); }); }); - describe('focus manager', function () { - it('should move focus forward', async function () { + describe("focus manager", function () { + it("should move focus forward", async function () { function Test() { return ( @@ -924,12 +1043,14 @@ describe('FocusScope', function () { return
; } - let {getByTestId} = render(); - let item1 = getByTestId('item1'); - let item2 = getByTestId('item2'); - let item3 = getByTestId('item3'); + let { getByTestId } = render(); + let item1 = getByTestId("item1"); + let item2 = getByTestId("item2"); + let item3 = getByTestId("item3"); - act(() => {item1.focus();}); + act(() => { + item1.focus(); + }); await user.click(item1); expect(document.activeElement).toBe(item2); @@ -941,7 +1062,7 @@ describe('FocusScope', function () { expect(document.activeElement).toBe(item3); }); - it('should move focus forward and wrap around', async function () { + it("should move focus forward and wrap around", async function () { function Test() { return ( @@ -955,18 +1076,20 @@ describe('FocusScope', function () { function Item(props) { let focusManager = useFocusManager(); let onClick = () => { - focusManager.focusNext({wrap: true}); + focusManager.focusNext({ wrap: true }); }; // eslint-disable-next-line jsx-a11y/click-events-have-key-events return
; } - let {getByTestId} = render(); - let item1 = getByTestId('item1'); - let item2 = getByTestId('item2'); - let item3 = getByTestId('item3'); + let { getByTestId } = render(); + let item1 = getByTestId("item1"); + let item2 = getByTestId("item2"); + let item3 = getByTestId("item3"); - act(() => {item1.focus();}); + act(() => { + item1.focus(); + }); await user.click(item1); expect(document.activeElement).toBe(item2); @@ -978,15 +1101,15 @@ describe('FocusScope', function () { expect(document.activeElement).toBe(item1); }); - it('should move focus forward but only to tabbable elements', async function () { + it("should move focus forward but only to tabbable elements", async function () { function Test() { return ( - - - + + + ); @@ -995,34 +1118,36 @@ describe('FocusScope', function () { function Item(props) { let focusManager = useFocusManager(); let onClick = () => { - focusManager.focusNext({tabbable: true}); + focusManager.focusNext({ tabbable: true }); }; // eslint-disable-next-line jsx-a11y/click-events-have-key-events return
; } - let {getByTestId} = render(); - let item1 = getByTestId('item1'); - let item3 = getByTestId('item3'); + let { getByTestId } = render(); + let item1 = getByTestId("item1"); + let item3 = getByTestId("item3"); - act(() => {item1.focus();}); + act(() => { + item1.focus(); + }); await user.click(item1); expect(document.activeElement).toBe(item3); }); - it('should move focus forward but only to tabbable elements while accounting for container elements within the scope', function () { + it("should move focus forward but only to tabbable elements while accounting for container elements within the scope", function () { function Test() { return ( - + - - + + @@ -1035,18 +1160,18 @@ describe('FocusScope', function () { function Group(props) { let focusManager = useFocusManager(); - let onMouseDown = e => { - focusManager.focusNext({from: e.target, tabbable: true}); + let onMouseDown = (e) => { + focusManager.focusNext({ from: e.target, tabbable: true }); }; // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions return
; } - let {getByTestId} = render(); - let group1 = getByTestId('group1'); - let group2 = getByTestId('group2'); - let item2 = getByTestId('item2'); - let item3 = getByTestId('item3'); + let { getByTestId } = render(); + let group1 = getByTestId("group1"); + let group2 = getByTestId("group2"); + let item2 = getByTestId("item2"); + let item3 = getByTestId("item3"); fireEvent.mouseDown(group2); expect(document.activeElement).toBe(item3); @@ -1055,7 +1180,7 @@ describe('FocusScope', function () { expect(document.activeElement).toBe(item2); }); - it('should move focus forward and allow users to skip certain elements', async function () { + it("should move focus forward and allow users to skip certain elements", async function () { function Test() { return ( @@ -1071,18 +1196,20 @@ describe('FocusScope', function () { let onClick = () => { focusManager.focusNext({ wrap: true, - accept: (e) => !e.getAttribute('data-skip') + accept: (e) => !e.getAttribute("data-skip"), }); }; // eslint-disable-next-line jsx-a11y/click-events-have-key-events return
; } - let {getByTestId} = render(); - let item1 = getByTestId('item1'); - let item3 = getByTestId('item3'); + let { getByTestId } = render(); + let item1 = getByTestId("item1"); + let item3 = getByTestId("item3"); - act(() => {item1.focus();}); + act(() => { + item1.focus(); + }); await user.click(item1); expect(document.activeElement).toBe(item3); @@ -1091,7 +1218,7 @@ describe('FocusScope', function () { expect(document.activeElement).toBe(item1); }); - it('should move focus backward', async function () { + it("should move focus backward", async function () { function Test() { return ( @@ -1111,12 +1238,14 @@ describe('FocusScope', function () { return
; } - let {getByTestId} = render(); - let item1 = getByTestId('item1'); - let item2 = getByTestId('item2'); - let item3 = getByTestId('item3'); + let { getByTestId } = render(); + let item1 = getByTestId("item1"); + let item2 = getByTestId("item2"); + let item3 = getByTestId("item3"); - act(() => {item3.focus();}); + act(() => { + item3.focus(); + }); await user.click(item3); expect(document.activeElement).toBe(item2); @@ -1128,7 +1257,7 @@ describe('FocusScope', function () { expect(document.activeElement).toBe(item1); }); - it('should move focus backward and wrap around', async function () { + it("should move focus backward and wrap around", async function () { function Test() { return ( @@ -1142,18 +1271,20 @@ describe('FocusScope', function () { function Item(props) { let focusManager = useFocusManager(); let onClick = () => { - focusManager.focusPrevious({wrap: true}); + focusManager.focusPrevious({ wrap: true }); }; // eslint-disable-next-line jsx-a11y/click-events-have-key-events return
; } - let {getByTestId} = render(); - let item1 = getByTestId('item1'); - let item2 = getByTestId('item2'); - let item3 = getByTestId('item3'); + let { getByTestId } = render(); + let item1 = getByTestId("item1"); + let item2 = getByTestId("item2"); + let item3 = getByTestId("item3"); - act(() => {item3.focus();}); + act(() => { + item3.focus(); + }); await user.click(item3); expect(document.activeElement).toBe(item2); @@ -1165,15 +1296,15 @@ describe('FocusScope', function () { expect(document.activeElement).toBe(item3); }); - it('should move focus backward but only to tabbable elements', async function () { + it("should move focus backward but only to tabbable elements", async function () { function Test() { return ( - - - + + + ); @@ -1182,34 +1313,36 @@ describe('FocusScope', function () { function Item(props) { let focusManager = useFocusManager(); let onClick = () => { - focusManager.focusPrevious({tabbable: true}); + focusManager.focusPrevious({ tabbable: true }); }; // eslint-disable-next-line jsx-a11y/click-events-have-key-events return
; } - let {getByTestId} = render(); - let item1 = getByTestId('item1'); - let item3 = getByTestId('item3'); + let { getByTestId } = render(); + let item1 = getByTestId("item1"); + let item3 = getByTestId("item3"); - act(() => {item3.focus();}); + act(() => { + item3.focus(); + }); await user.click(item3); expect(document.activeElement).toBe(item1); }); - it('should move focus backward but only to tabbable elements while accounting for container elements within the scope', function () { + it("should move focus backward but only to tabbable elements while accounting for container elements within the scope", function () { function Test() { return ( - + - - + + @@ -1222,17 +1355,17 @@ describe('FocusScope', function () { function Group(props) { let focusManager = useFocusManager(); - let onMouseDown = e => { - focusManager.focusPrevious({from: e.target, tabbable: true}); + let onMouseDown = (e) => { + focusManager.focusPrevious({ from: e.target, tabbable: true }); }; // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions return
; } - let {getByTestId} = render(); - let group1 = getByTestId('group1'); - let group2 = getByTestId('group2'); - let item1 = getByTestId('item1'); + let { getByTestId } = render(); + let group1 = getByTestId("group1"); + let group2 = getByTestId("group2"); + let item1 = getByTestId("item1"); fireEvent.mouseDown(group2); expect(document.activeElement).toBe(item1); @@ -1244,7 +1377,7 @@ describe('FocusScope', function () { expect(document.activeElement).toBe(item1); }); - it('should move focus backward and allow users to skip certain elements', async function () { + it("should move focus backward and allow users to skip certain elements", async function () { function Test() { return ( @@ -1260,18 +1393,20 @@ describe('FocusScope', function () { let onClick = () => { focusManager.focusPrevious({ wrap: true, - accept: (e) => !e.getAttribute('data-skip') + accept: (e) => !e.getAttribute("data-skip"), }); }; // eslint-disable-next-line jsx-a11y/click-events-have-key-events return
; } - let {getByTestId} = render(); - let item1 = getByTestId('item1'); - let item3 = getByTestId('item3'); + let { getByTestId } = render(); + let item1 = getByTestId("item1"); + let item3 = getByTestId("item3"); - act(() => {item1.focus();}); + act(() => { + item1.focus(); + }); await user.click(item1); expect(document.activeElement).toBe(item3); @@ -1281,7 +1416,7 @@ describe('FocusScope', function () { }); }); - it('skips radio buttons that are in the same group and are not the selectable one forwards', async function () { + it("skips radio buttons that are in the same group and are not the selectable one forwards", async function () { function Test() { return ( @@ -1291,7 +1426,13 @@ describe('FocusScope', function () { Select a maintenance drone:
- +
@@ -1329,25 +1470,25 @@ describe('FocusScope', function () { ); } - let {getByTestId, getAllByRole} = render(); - let radios = getAllByRole('radio'); + let { getByTestId, getAllByRole } = render(); + let radios = getAllByRole("radio"); await user.tab(); - expect(document.activeElement).toBe(getByTestId('button1')); + expect(document.activeElement).toBe(getByTestId("button1")); await user.tab(); expect(document.activeElement).toBe(radios[0]); await user.tab(); - expect(document.activeElement).toBe(getByTestId('button2')); + expect(document.activeElement).toBe(getByTestId("button2")); await user.tab(); expect(document.activeElement).toBe(radios[3]); await user.tab(); - expect(document.activeElement).toBe(getByTestId('button3')); + expect(document.activeElement).toBe(getByTestId("button3")); await user.tab(); expect(document.activeElement).toBe(radios[5]); await user.tab(); - expect(document.activeElement).toBe(getByTestId('button4')); + expect(document.activeElement).toBe(getByTestId("button4")); }); - it('skips radio buttons that are in the same group and are not the selectable one forwards outside of a form', async function () { + it("skips radio buttons that are in the same group and are not the selectable one forwards outside of a form", async function () { function Test() { return ( @@ -1356,7 +1497,13 @@ describe('FocusScope', function () { Select a maintenance drone:
- +
@@ -1393,25 +1540,25 @@ describe('FocusScope', function () { ); } - let {getByTestId, getAllByRole} = render(); - let radios = getAllByRole('radio'); + let { getByTestId, getAllByRole } = render(); + let radios = getAllByRole("radio"); await user.tab(); - expect(document.activeElement).toBe(getByTestId('button1')); + expect(document.activeElement).toBe(getByTestId("button1")); await user.tab(); expect(document.activeElement).toBe(radios[0]); await user.tab(); - expect(document.activeElement).toBe(getByTestId('button2')); + expect(document.activeElement).toBe(getByTestId("button2")); await user.tab(); expect(document.activeElement).toBe(radios[3]); await user.tab(); - expect(document.activeElement).toBe(getByTestId('button3')); + expect(document.activeElement).toBe(getByTestId("button3")); await user.tab(); expect(document.activeElement).toBe(radios[5]); await user.tab(); - expect(document.activeElement).toBe(getByTestId('button4')); + expect(document.activeElement).toBe(getByTestId("button4")); }); - it('skips radio buttons that are in the same group and are not the selectable one backwards', async function () { + it("skips radio buttons that are in the same group and are not the selectable one backwards", async function () { function Test() { return ( @@ -1421,7 +1568,13 @@ describe('FocusScope', function () { Select a maintenance drone:
- +
@@ -1459,24 +1612,24 @@ describe('FocusScope', function () { ); } - let {getByTestId, getAllByRole} = render(); - let radios = getAllByRole('radio'); - await user.click(getByTestId('button4')); - await user.tab({shift: true}); + let { getByTestId, getAllByRole } = render(); + let radios = getAllByRole("radio"); + await user.click(getByTestId("button4")); + await user.tab({ shift: true }); expect(document.activeElement).toBe(radios[5]); - await user.tab({shift: true}); - expect(document.activeElement).toBe(getByTestId('button3')); - await user.tab({shift: true}); + await user.tab({ shift: true }); + expect(document.activeElement).toBe(getByTestId("button3")); + await user.tab({ shift: true }); expect(document.activeElement).toBe(radios[4]); - await user.tab({shift: true}); - expect(document.activeElement).toBe(getByTestId('button2')); - await user.tab({shift: true}); + await user.tab({ shift: true }); + expect(document.activeElement).toBe(getByTestId("button2")); + await user.tab({ shift: true }); expect(document.activeElement).toBe(radios[0]); - await user.tab({shift: true}); - expect(document.activeElement).toBe(getByTestId('button1')); + await user.tab({ shift: true }); + expect(document.activeElement).toBe(getByTestId("button1")); }); - it('skips radio buttons that are in the same group and are not the selectable one backwards outside of a form', async function () { + it("skips radio buttons that are in the same group and are not the selectable one backwards outside of a form", async function () { function Test() { return ( @@ -1485,7 +1638,13 @@ describe('FocusScope', function () { Select a maintenance drone:
- +
@@ -1522,63 +1681,67 @@ describe('FocusScope', function () { ); } - let {getByTestId, getAllByRole} = render(); - let radios = getAllByRole('radio'); - await user.click(getByTestId('button4')); - await user.tab({shift: true}); + let { getByTestId, getAllByRole } = render(); + let radios = getAllByRole("radio"); + await user.click(getByTestId("button4")); + await user.tab({ shift: true }); expect(document.activeElement).toBe(radios[5]); - await user.tab({shift: true}); - expect(document.activeElement).toBe(getByTestId('button3')); - await user.tab({shift: true}); + await user.tab({ shift: true }); + expect(document.activeElement).toBe(getByTestId("button3")); + await user.tab({ shift: true }); expect(document.activeElement).toBe(radios[4]); - await user.tab({shift: true}); - expect(document.activeElement).toBe(getByTestId('button2')); - await user.tab({shift: true}); + await user.tab({ shift: true }); + expect(document.activeElement).toBe(getByTestId("button2")); + await user.tab({ shift: true }); expect(document.activeElement).toBe(radios[0]); - await user.tab({shift: true}); - expect(document.activeElement).toBe(getByTestId('button1')); + await user.tab({ shift: true }); + expect(document.activeElement).toBe(getByTestId("button1")); }); - describe('nested focus scopes', function () { - it('should make child FocusScopes the active scope regardless of DOM structure', function () { + describe("nested focus scopes", function () { + it("should make child FocusScopes the active scope regardless of DOM structure", function () { function ChildComponent(props) { return ReactDOM.createPortal(props.children, document.body); } - function Test({show}) { + function Test({ show }) { return (
- {show && + {show && ( - } + )}
); } - let {getByTestId, rerender} = render(); + let { getByTestId, rerender } = render(); // Set a focused node and make first FocusScope the active scope - let input1 = getByTestId('input1'); - act(() => {input1.focus();}); + let input1 = getByTestId("input1"); + act(() => { + input1.focus(); + }); fireEvent.focusIn(input1); expect(document.activeElement).toBe(input1); rerender(); expect(document.activeElement).toBe(input1); - let input3 = getByTestId('input3'); - act(() => {input3.focus();}); + let input3 = getByTestId("input3"); + act(() => { + input3.focus(); + }); fireEvent.focusIn(input3); expect(document.activeElement).toBe(input3); }); - it('should lock tab navigation inside direct child focus scope', async function () { + it("should lock tab navigation inside direct child focus scope", async function () { function Test() { return (
@@ -1597,28 +1760,38 @@ describe('FocusScope', function () { ); } - let {getByTestId} = render(); - let child1 = getByTestId('child1'); - let child2 = getByTestId('child2'); - let child3 = getByTestId('child3'); + let { getByTestId } = render(); + let child1 = getByTestId("child1"); + let child2 = getByTestId("child2"); + let child3 = getByTestId("child3"); - act(() => {jest.runAllTimers();}); + act(() => { + jest.runAllTimers(); + }); expect(document.activeElement).toBe(child1); await user.tab(); - act(() => {jest.runAllTimers();}); + act(() => { + jest.runAllTimers(); + }); expect(document.activeElement).toBe(child2); await user.tab(); - act(() => {jest.runAllTimers();}); + act(() => { + jest.runAllTimers(); + }); expect(document.activeElement).toBe(child3); await user.tab(); - act(() => {jest.runAllTimers();}); + act(() => { + jest.runAllTimers(); + }); expect(document.activeElement).toBe(child1); - await user.tab({shift: true}); - act(() => {jest.runAllTimers();}); + await user.tab({ shift: true }); + act(() => { + jest.runAllTimers(); + }); expect(document.activeElement).toBe(child3); }); - it('should lock tab navigation inside nested child focus scope', async function () { + it("should lock tab navigation inside nested child focus scope", async function () { function Test() { return (
@@ -1641,10 +1814,10 @@ describe('FocusScope', function () { ); } - let {getByTestId} = render(); - let child1 = getByTestId('child1'); - let child2 = getByTestId('child2'); - let child3 = getByTestId('child3'); + let { getByTestId } = render(); + let child1 = getByTestId("child1"); + let child2 = getByTestId("child2"); + let child3 = getByTestId("child3"); expect(document.activeElement).toBe(child1); await user.tab(); @@ -1653,11 +1826,11 @@ describe('FocusScope', function () { expect(document.activeElement).toBe(child3); await user.tab(); expect(document.activeElement).toBe(child1); - await user.tab({shift: true}); + await user.tab({ shift: true }); expect(document.activeElement).toBe(child3); }); - it('should not lock tab navigation inside a nested focus scope without contain', async function () { + it("should not lock tab navigation inside a nested focus scope without contain", async function () { function Test() { return (
@@ -1678,11 +1851,11 @@ describe('FocusScope', function () { ); } - let {getByTestId} = render(); - let parent = getByTestId('parent'); - let child1 = getByTestId('child1'); - let child2 = getByTestId('child2'); - let child3 = getByTestId('child3'); + let { getByTestId } = render(); + let parent = getByTestId("parent"); + let child1 = getByTestId("child1"); + let child2 = getByTestId("child2"); + let child3 = getByTestId("child3"); expect(document.activeElement).toBe(parent); await user.tab(); @@ -1693,11 +1866,11 @@ describe('FocusScope', function () { expect(document.activeElement).toBe(child3); await user.tab(); expect(document.activeElement).toBe(parent); - await user.tab({shift: true}); + await user.tab({ shift: true }); expect(document.activeElement).toBe(child3); }); - it('should not lock tab navigation inside a nested focus scope with restore and not contain', async function () { + it("should not lock tab navigation inside a nested focus scope with restore and not contain", async function () { function Test() { return (
@@ -1718,11 +1891,11 @@ describe('FocusScope', function () { ); } - let {getByTestId} = render(); - let parent = getByTestId('parent'); - let child1 = getByTestId('child1'); - let child2 = getByTestId('child2'); - let child3 = getByTestId('child3'); + let { getByTestId } = render(); + let parent = getByTestId("parent"); + let child1 = getByTestId("child1"); + let child2 = getByTestId("child2"); + let child3 = getByTestId("child3"); expect(document.activeElement).toBe(parent); await user.tab(); @@ -1733,39 +1906,39 @@ describe('FocusScope', function () { expect(document.activeElement).toBe(child3); await user.tab(); expect(document.activeElement).toBe(parent); - await user.tab({shift: true}); + await user.tab({ shift: true }); expect(document.activeElement).toBe(child3); }); - it('should restore to the correct scope on unmount', async function () { - function Test({show1, show2, show3}) { + it("should restore to the correct scope on unmount", async function () { + function Test({ show1, show2, show3 }) { return (
- {show1 && + {show1 && ( - {show2 && + {show2 && ( - {show3 && + {show3 && ( - } + )} - } + )} - } + )}
); } - let {rerender, getByTestId} = render(); - let parent = getByTestId('parent'); + let { rerender, getByTestId } = render(); + let parent = getByTestId("parent"); expect(document.activeElement).toBe(parent); @@ -1773,7 +1946,7 @@ describe('FocusScope', function () { expect(document.activeElement).toBe(parent); // Can move into a child, but not out. - let child1 = getByTestId('child1'); + let child1 = getByTestId("child1"); await user.tab(); expect(document.activeElement).toBe(child1); @@ -1783,7 +1956,7 @@ describe('FocusScope', function () { rerender(); expect(document.activeElement).toBe(child1); - let child2 = getByTestId('child2'); + let child2 = getByTestId("child2"); await user.tab(); expect(document.activeElement).toBe(child2); @@ -1795,7 +1968,7 @@ describe('FocusScope', function () { rerender(); - let child3 = getByTestId('child3'); + let child3 = getByTestId("child3"); await user.tab(); expect(document.activeElement).toBe(child3); @@ -1808,7 +1981,7 @@ describe('FocusScope', function () { expect(document.activeElement).toBe(child1); }); - it('should not lock focus inside a focus scope with a child scope in a portal', function () { + it("should not lock focus inside a focus scope with a child scope in a portal", function () { function Portal(props) { return ReactDOM.createPortal(props.children, document.body); } @@ -1830,9 +2003,9 @@ describe('FocusScope', function () { ); } - let {getByTestId} = render(); - let parent = getByTestId('parent'); - let child = getByTestId('child'); + let { getByTestId } = render(); + let parent = getByTestId("parent"); + let child = getByTestId("child"); expect(document.activeElement).toBe(parent); act(() => child.focus()); @@ -1841,7 +2014,7 @@ describe('FocusScope', function () { expect(document.activeElement).toBe(parent); }); - it('should lock focus inside a child focus scope with contain in a portal', function () { + it("should lock focus inside a child focus scope with contain in a portal", function () { function Portal(props) { return ReactDOM.createPortal(props.children, document.body); } @@ -1863,9 +2036,9 @@ describe('FocusScope', function () { ); } - let {getByTestId} = render(); - let parent = getByTestId('parent'); - let child = getByTestId('child'); + let { getByTestId } = render(); + let parent = getByTestId("parent"); + let child = getByTestId("child"); expect(document.activeElement).toBe(parent); act(() => child.focus()); @@ -1875,8 +2048,8 @@ describe('FocusScope', function () { }); }); - describe('scope child of document.body', function () { - it('should navigate in and out of scope in DOM order when the nodeToRestore is the document.body', async function () { + describe("scope child of document.body", function () { + it("should navigate in and out of scope in DOM order when the nodeToRestore is the document.body", async function () { function Test() { return (
@@ -1889,21 +2062,25 @@ describe('FocusScope', function () { ); } - let {getByTestId} = render(); - let beforeScope = getByTestId('beforeScope'); - let inScope = getByTestId('inScope'); - let afterScope = getByTestId('afterScope'); + let { getByTestId } = render(); + let beforeScope = getByTestId("beforeScope"); + let inScope = getByTestId("inScope"); + let afterScope = getByTestId("afterScope"); - act(() => {inScope.focus();}); + act(() => { + inScope.focus(); + }); await user.tab(); expect(document.activeElement).toBe(afterScope); - act(() => {inScope.focus();}); - await user.tab({shift: true}); + act(() => { + inScope.focus(); + }); + await user.tab({ shift: true }); expect(document.activeElement).toBe(beforeScope); }); }); - describe('node to restore edge cases', () => { - it('tracks node to restore if the node to restore was removed in another part of the tree', async () => { + describe("node to restore edge cases", () => { + it("tracks node to restore if the node to restore was removed in another part of the tree", async () => { function Test() { let [showMenu, setShowMenu] = useState(false); let [showDialog, setShowDialog] = useState(false); @@ -1917,7 +2094,7 @@ describe('FocusScope', function () { - {}}> + { }}> {showMenu && ( @@ -1925,7 +2102,7 @@ describe('FocusScope', function () { )} - {}}> + { }}> {showDialog && ( @@ -1943,16 +2120,16 @@ describe('FocusScope', function () { }); await user.tab(); await user.tab(); - expect(document.activeElement.textContent).toBe('Open Menu'); + expect(document.activeElement.textContent).toBe("Open Menu"); - await user.keyboard('[Enter]'); + await user.keyboard("[Enter]"); act(() => { jest.runAllTimers(); }); - expect(document.activeElement.textContent).toBe('Open Dialog'); + expect(document.activeElement.textContent).toBe("Open Dialog"); - await user.keyboard('[Enter]'); + await user.keyboard("[Enter]"); // Needed for onBlur raf in useFocusContainment act(() => { @@ -1963,9 +2140,9 @@ describe('FocusScope', function () { jest.runAllTimers(); }); - expect(document.activeElement.textContent).toBe('Close'); + expect(document.activeElement.textContent).toBe("Close"); - await user.keyboard('[Enter]'); + await user.keyboard("[Enter]"); act(() => { jest.runAllTimers(); }); @@ -1974,17 +2151,17 @@ describe('FocusScope', function () { }); expect(document.activeElement).not.toBe(document.body); - expect(document.activeElement.textContent).toBe('Open Menu'); + expect(document.activeElement.textContent).toBe("Open Menu"); }); }); }); -describe('FocusScope with Shadow DOM', function () { +describe("FocusScope with Shadow DOM", function () { let user; beforeAll(() => { enableShadowDOM(); - user = userEvent.setup({delay: null, pointerMap}); + user = userEvent.setup({ delay: null, pointerMap }); }); beforeEach(() => { @@ -1992,28 +2169,33 @@ describe('FocusScope with Shadow DOM', function () { }); afterEach(() => { // make sure to clean up any raf's that may be running to restore focus on unmount - act(() => {jest.runAllTimers();}); + act(() => { + jest.runAllTimers(); + }); }); - it('should contain focus within the shadow DOM scope', async function () { - const {shadowRoot} = createShadowRoot(); - const FocusableComponent = () => ReactDOM.createPortal( - - - - - , - shadowRoot - ); + it("should contain focus within the shadow DOM scope", async function () { + const { shadowRoot } = createShadowRoot(); + const FocusableComponent = () => + ReactDOM.createPortal( + + + + + , + shadowRoot, + ); - const {unmount} = render(); + const { unmount } = render(); const input1 = shadowRoot.querySelector('[data-testid="input1"]'); const input2 = shadowRoot.querySelector('[data-testid="input2"]'); const input3 = shadowRoot.querySelector('[data-testid="input3"]'); // Simulate focusing the first input - act(() => {input1.focus();}); + act(() => { + input1.focus(); + }); expect(document.activeElement).toBe(shadowRoot.host); expect(shadowRoot.activeElement).toBe(input1); @@ -2033,23 +2215,29 @@ describe('FocusScope with Shadow DOM', function () { document.body.removeChild(shadowRoot.host); }); - it('should manage focus within nested shadow DOMs', async function () { - const {shadowRoot: parentShadowRoot} = createShadowRoot(); - const nestedDiv = document.createElement('div'); + it("should manage focus within nested shadow DOMs", async function () { + const { shadowRoot: parentShadowRoot } = createShadowRoot(); + const nestedDiv = document.createElement("div"); parentShadowRoot.appendChild(nestedDiv); - const childShadowRoot = nestedDiv.attachShadow({mode: 'open'}); + const childShadowRoot = nestedDiv.attachShadow({ mode: "open" }); - const FocusableComponent = () => ReactDOM.createPortal( - - - , childShadowRoot); + const FocusableComponent = () => + ReactDOM.createPortal( + + + + , + childShadowRoot, + ); - const {unmount} = render(); + const { unmount } = render(); - const input1 = childShadowRoot.querySelector('[data-testid=input1]'); - const input2 = childShadowRoot.querySelector('[data-testid=input2]'); + const input1 = childShadowRoot.querySelector("[data-testid=input1]"); + const input2 = childShadowRoot.querySelector("[data-testid=input2]"); - act(() => {input1.focus();}); + act(() => { + input1.focus(); + }); expect(childShadowRoot.activeElement).toBe(input1); await user.tab(); @@ -2068,7 +2256,7 @@ describe('FocusScope with Shadow DOM', function () { * │ └── Your custom elements and focusable elements here * └── Other elements */ - it('should restore focus to the element outside shadow DOM on unmount, with FocusScope outside as well', async () => { + it("should restore focus to the element outside shadow DOM on unmount, with FocusScope outside as well", async () => { const App = () => ( <> @@ -2078,27 +2266,32 @@ describe('FocusScope with Shadow DOM', function () { ); - const {getByTestId} = render(); - const shadowHost = document.getElementById('shadow-host'); - const shadowRoot = shadowHost.attachShadow({mode: 'open'}); + const { getByTestId } = render(); + const shadowHost = document.getElementById("shadow-host"); + const shadowRoot = shadowHost.attachShadow({ mode: "open" }); - const FocusableComponent = () => ReactDOM.createPortal( - - - - - , - shadowRoot - ); + const FocusableComponent = () => + ReactDOM.createPortal( + + + + + , + shadowRoot, + ); - const {unmount} = render(); + const { unmount } = render(); const input1 = shadowRoot.querySelector('[data-testid="input1"]'); - act(() => { input1.focus(); }); + act(() => { + input1.focus(); + }); expect(shadowRoot.activeElement).toBe(input1); - const externalInput = getByTestId('outside'); - act(() => { externalInput.focus(); }); + const externalInput = getByTestId("outside"); + act(() => { + externalInput.focus(); + }); expect(document.activeElement).toBe(externalInput); act(() => { @@ -2113,26 +2306,29 @@ describe('FocusScope with Shadow DOM', function () { /** * Test case: https://github.com/adobe/react-spectrum/issues/1472 */ - it('should autofocus and lock tab navigation inside shadow DOM', async function () { - const {shadowRoot, shadowHost} = createShadowRoot(); - - const FocusableComponent = () => ReactDOM.createPortal( - - - - - , - shadowRoot - ); + it("should autofocus and lock tab navigation inside shadow DOM", async function () { + const { shadowRoot, shadowHost } = createShadowRoot(); - const {unmount} = render(); + const FocusableComponent = () => + ReactDOM.createPortal( + + + + + , + shadowRoot, + ); + + const { unmount } = render(); const input1 = shadowRoot.querySelector('[data-testid="input1"]'); const input2 = shadowRoot.querySelector('[data-testid="input2"]'); const button = shadowRoot.querySelector('[data-testid="button"]'); // Simulate focusing the first input and tab through the elements - act(() => {input1.focus();}); + act(() => { + input1.focus(); + }); expect(shadowRoot.activeElement).toBe(input1); // Hit TAB key @@ -2152,252 +2348,42 @@ describe('FocusScope with Shadow DOM', function () { document.body.removeChild(shadowHost); }); - describe('Shadow DOM boundary containment issues (issue #8675)', function () { - it('should properly detect element containment across shadow DOM boundaries with UNSAFE_PortalProvider', async function () { - const {shadowRoot} = createShadowRoot(); - - // Create a menu-like structure that reproduces the issue with UNSAFE_PortalProvider - function MenuInPopoverWithPortalProvider() { - const [isOpen, setIsOpen] = React.useState(true); - - return ( - shadowRoot}> - -
- {isOpen && ( - -
- - -
-
- )} -
-
-
- ); - } - - const {unmount} = render(); - - const menuItem1 = shadowRoot.querySelector('[data-testid="menu-item-1"]'); - const menuItem2 = shadowRoot.querySelector('[data-testid="menu-item-2"]'); - const menu = shadowRoot.querySelector('[data-testid="menu"]'); - - // Focus the first menu item - act(() => { menuItem1.focus(); }); - expect(shadowRoot.activeElement).toBe(menuItem1); - - // Tab to second menu item should work - await user.tab(); - expect(shadowRoot.activeElement).toBe(menuItem2); - - // Tab should wrap back to first item due to focus containment - await user.tab(); - expect(shadowRoot.activeElement).toBe(menuItem1); - - // Menu should still be visible (not closed unexpectedly) - expect(menu).toBeInTheDocument(); - - // Cleanup - unmount(); - document.body.removeChild(shadowRoot.host); - }); - - it('should handle focus events correctly in shadow DOM with nested FocusScopes and UNSAFE_PortalProvider', async function () { - const {shadowRoot} = createShadowRoot(); - let menuItemClickHandled = false; - - function NestedScopeMenuWithPortalProvider() { - const handleMenuItemClick = () => { - menuItemClickHandled = true; - }; - - return ( - shadowRoot}> - -
- - -
- -
-
-
-
-
- ); - } - - const {unmount} = render(); - - const trigger = shadowRoot.querySelector('[data-testid="trigger"]'); - const menuItem = shadowRoot.querySelector('[data-testid="menu-item"]'); - - // Focus the trigger first - act(() => { trigger.focus(); }); - expect(shadowRoot.activeElement).toBe(trigger); - - // Tab to menu item - await user.tab(); - expect(shadowRoot.activeElement).toBe(menuItem); - - // Click the menu item - this should fire the onClick handler - await user.click(menuItem); - expect(menuItemClickHandled).toBe(true); - - // Cleanup - unmount(); - document.body.removeChild(shadowRoot.host); - }); - - - it('should handle focus manager operations across shadow DOM boundaries', async function () { - const {shadowRoot} = createShadowRoot(); - - function FocusManagerTest() { - const focusManager = useFocusManager(); - - return ReactDOM.createPortal( - -
- - - -
-
, - shadowRoot - ); - } - - const {unmount} = render(); - - const firstButton = shadowRoot.querySelector('[data-testid="first"]'); - const secondButton = shadowRoot.querySelector('[data-testid="second"]'); - const thirdButton = shadowRoot.querySelector('[data-testid="third"]'); + it("should reproduce the specific issue #8675: Menu items in popover close immediately with UNSAFE_PortalProvider", async function () { + const { shadowRoot, cleanup } = createShadowRoot(); + let actionExecuted = false; + let menuClosed = false; - // Focus first button - act(() => { firstButton.focus(); }); - expect(shadowRoot.activeElement).toBe(firstButton); + // Create portal container within the shadow DOM for the popover + const popoverPortal = document.createElement("div"); + popoverPortal.setAttribute("data-testid", "popover-portal"); + shadowRoot.appendChild(popoverPortal); - // Click first button to trigger focusNext - await user.click(firstButton); - expect(shadowRoot.activeElement).toBe(secondButton); + // This reproduces the exact scenario described in the issue + function WebComponentWithReactApp() { + const [isPopoverOpen, setIsPopoverOpen] = React.useState(true); - // Click second button to trigger focusPrevious - await user.click(secondButton); - expect(shadowRoot.activeElement).toBe(firstButton); + const handleMenuAction = (key) => { + actionExecuted = true; + // In the original issue, this never executes because the popover closes first + console.log("Menu action executed:", key); + }; - // Move to third button and test focusFirst - act(() => { thirdButton.focus(); }); - await user.click(thirdButton); - expect(shadowRoot.activeElement).toBe(firstButton); - - // Cleanup - unmount(); - document.body.removeChild(shadowRoot.host); - }); - - it('should correctly handle portaled elements within shadow DOM scopes', async function () { - const {shadowRoot} = createShadowRoot(); - const portalTarget = document.createElement('div'); - shadowRoot.appendChild(portalTarget); - - function PortalInShadowDOM() { - return ReactDOM.createPortal( - -
- - {ReactDOM.createPortal( - , - portalTarget - )} -
-
, - shadowRoot - ); - } - - const {unmount} = render(); - - const mainButton = shadowRoot.querySelector('[data-testid="main-button"]'); - const portaledButton = shadowRoot.querySelector('[data-testid="portaled-button"]'); - - // Focus main button - act(() => { mainButton.focus(); }); - expect(shadowRoot.activeElement).toBe(mainButton); - - // Focus portaled button - act(() => { portaledButton.focus(); }); - expect(shadowRoot.activeElement).toBe(portaledButton); - - // Tab navigation should work between main and portaled elements - await user.tab(); - // The exact behavior may vary, but focus should remain within the shadow DOM - expect(shadowRoot.activeElement).toBeTruthy(); - - // Cleanup - unmount(); - document.body.removeChild(shadowRoot.host); - }); - - it('should reproduce the specific issue #8675: Menu items in popover close immediately with UNSAFE_PortalProvider', async function () { - const {shadowRoot} = createShadowRoot(); - let actionExecuted = false; - let menuClosed = false; - - // This reproduces the exact scenario described in the issue - function WebComponentWithReactApp() { - const [isPopoverOpen, setIsPopoverOpen] = React.useState(true); - - const handleMenuAction = (key) => { - actionExecuted = true; - // In the original issue, this never executes because the popover closes first - console.log('Menu action executed:', key); - }; - - return ( - shadowRoot}> -
- {isPopoverOpen && ( + return ( + shadowRoot}> +
+ + {/* Portal the popover overlay to simulate real-world usage */} + {isPopoverOpen && + ReactDOM.createPortal(
@@ -2405,104 +2391,121 @@ describe('FocusScope with Shadow DOM', function () {
- + , + popoverPortal, )} - -
-
- ); - } +
+ + ); + } - const {unmount} = render(); - - const saveMenuItem = shadowRoot.querySelector('[data-testid="menu-item-save"]'); - const exportMenuItem = shadowRoot.querySelector('[data-testid="menu-item-export"]'); - const menuContainer = shadowRoot.querySelector('[data-testid="menu-container"]'); - const popoverOverlay = shadowRoot.querySelector('[data-testid="popover-overlay"]'); - - // Verify the menu is initially visible - expect(menuContainer).toBeInTheDocument(); - expect(popoverOverlay).toBeInTheDocument(); - - // Focus the first menu item - act(() => { saveMenuItem.focus(); }); - expect(shadowRoot.activeElement).toBe(saveMenuItem); - - // Click the menu item - this should execute the onAction handler, NOT close the menu - await user.click(saveMenuItem); - - // The action should have been executed (this would fail in the buggy version) - expect(actionExecuted).toBe(true); - - // The menu should still be open (this would fail in the buggy version where it closes immediately) - expect(menuClosed).toBe(false); - expect(menuContainer).toBeInTheDocument(); - - // Test focus containment within the menu - act(() => { saveMenuItem.focus(); }); - await user.tab(); - expect(shadowRoot.activeElement).toBe(exportMenuItem); + const { unmount } = render(); - await user.tab(); - // Focus should wrap back to first item due to containment - expect(shadowRoot.activeElement).toBe(saveMenuItem); + // Wait for rendering + act(() => { + jest.runAllTimers(); + }); + + // Query elements from shadow DOM + const saveMenuItem = shadowRoot.querySelector( + '[data-testid="menu-item-save"]', + ); + const exportMenuItem = shadowRoot.querySelector( + '[data-testid="menu-item-export"]', + ); + const menuContainer = shadowRoot.querySelector( + '[data-testid="menu-container"]', + ); + const popoverOverlay = shadowRoot.querySelector( + '[data-testid="popover-overlay"]', + ); + const closeButton = shadowRoot.querySelector( + '[data-testid="close-popover"]', + ); + + // Verify the menu is initially visible in shadow DOM + expect(popoverOverlay).not.toBeNull(); + expect(menuContainer).not.toBeNull(); + expect(saveMenuItem).not.toBeNull(); + expect(exportMenuItem).not.toBeNull(); - // Cleanup - unmount(); - document.body.removeChild(shadowRoot.host); + // Focus the first menu item + act(() => { + saveMenuItem.focus(); }); + expect(shadowRoot.activeElement).toBe(saveMenuItem); - it('should handle web component scenario with multiple nested portals and UNSAFE_PortalProvider', async function () { - const {shadowRoot} = createShadowRoot(); - - // Create nested portal containers within the shadow DOM - const modalPortal = document.createElement('div'); - modalPortal.setAttribute('data-testid', 'modal-portal'); - shadowRoot.appendChild(modalPortal); + // Click the menu item - this should execute the onAction handler, NOT close the menu + await user.click(saveMenuItem); - const tooltipPortal = document.createElement('div'); - tooltipPortal.setAttribute('data-testid', 'tooltip-portal'); - shadowRoot.appendChild(tooltipPortal); + // The action should have been executed (this would fail in the buggy version) + expect(actionExecuted).toBe(true); - function ComplexWebComponent() { - const [showModal, setShowModal] = React.useState(true); - const [showTooltip, setShowTooltip] = React.useState(true); + // The menu should still be open (this would fail in the buggy version where it closes immediately) + expect(menuClosed).toBe(false); + expect( + shadowRoot.querySelector('[data-testid="menu-container"]'), + ).not.toBeNull(); - return ( - shadowRoot}> -
- - - {/* Modal with its own focus scope */} - {showModal && ReactDOM.createPortal( + // Test focus containment within the menu + act(() => { + saveMenuItem.focus(); + }); + await user.tab(); + expect(shadowRoot.activeElement).toBe(exportMenuItem); + + await user.tab(); + // Focus should wrap back to first item due to containment + expect(shadowRoot.activeElement).toBe(saveMenuItem); + + // Cleanup + unmount(); + cleanup(); + }); + + it("should handle web component scenario with multiple nested portals and UNSAFE_PortalProvider", async function () { + const { shadowRoot, cleanup } = createShadowRoot(); + + // Create nested portal containers within the shadow DOM + const modalPortal = document.createElement("div"); + modalPortal.setAttribute("data-testid", "modal-portal"); + shadowRoot.appendChild(modalPortal); + + const tooltipPortal = document.createElement("div"); + tooltipPortal.setAttribute("data-testid", "tooltip-portal"); + shadowRoot.appendChild(tooltipPortal); + + function ComplexWebComponent() { + const [showModal, setShowModal] = React.useState(true); + const [showTooltip, setShowTooltip] = React.useState(true); + + return ( + shadowRoot}> +
+ + + {/* Modal with its own focus scope */} + {showModal && + ReactDOM.createPortal(
-
, - modalPortal + modalPortal, )} - {/* Tooltip with nested focus scope */} - {showTooltip && ReactDOM.createPortal( + {/* Tooltip with nested focus scope */} + {showTooltip && + ReactDOM.createPortal(
, - tooltipPortal + tooltipPortal, )} -
-
- ); - } +
+
+ ); + } - const {unmount} = render(); + const { unmount } = render(); - const modalButton1 = shadowRoot.querySelector('[data-testid="modal-button-1"]'); - const modalButton2 = shadowRoot.querySelector('[data-testid="modal-button-2"]'); - const tooltipAction = shadowRoot.querySelector('[data-testid="tooltip-action"]'); + const modalButton1 = shadowRoot.querySelector( + '[data-testid="modal-button-1"]', + ); + const modalButton2 = shadowRoot.querySelector( + '[data-testid="modal-button-2"]', + ); + const tooltipAction = shadowRoot.querySelector( + '[data-testid="tooltip-action"]', + ); - // Due to autoFocus, the first modal button should be focused - act(() => { jest.runAllTimers(); }); - expect(shadowRoot.activeElement).toBe(modalButton1); + // Due to autoFocus, the first modal button should be focused + act(() => { + jest.runAllTimers(); + }); + expect(shadowRoot.activeElement).toBe(modalButton1); - // Tab navigation should work within the modal - await user.tab(); - expect(shadowRoot.activeElement).toBe(modalButton2); + // Tab navigation should work within the modal + await user.tab(); + expect(shadowRoot.activeElement).toBe(modalButton2); - // Focus should be contained within the modal due to the contain prop - await user.tab(); - // Should cycle to the close button - expect(shadowRoot.activeElement.getAttribute('data-testid')).toBe('close-modal'); + // Focus should be contained within the modal due to the contain prop + await user.tab(); + // Should cycle to the close button + expect(shadowRoot.activeElement.getAttribute("data-testid")).toBe( + "close-modal", + ); - await user.tab(); - // Should wrap back to first modal button - expect(shadowRoot.activeElement).toBe(modalButton1); - - // The tooltip button should be focusable when we explicitly focus it - act(() => { tooltipAction.focus(); }); - // But due to modal containment, focus should be restored back to modal - act(() => { jest.runAllTimers(); }); - expect(shadowRoot.activeElement).toBe(modalButton1); - - // Cleanup - unmount(); - document.body.removeChild(shadowRoot.host); + await user.tab(); + // Should wrap back to first modal button + expect(shadowRoot.activeElement).toBe(modalButton1); + + // The tooltip button should be focusable when we explicitly focus it + act(() => { + tooltipAction.focus(); }); + act(() => { + jest.runAllTimers(); + }); + // But due to modal containment, focus should be restored back to modal + expect(shadowRoot.activeElement).toBe(modalButton1); + + // Cleanup + unmount(); + cleanup(); }); }); -describe('Unmounting cleanup', () => { +describe("Unmounting cleanup", () => { beforeAll(() => { jest.useFakeTimers(); }); @@ -2572,14 +2589,14 @@ describe('Unmounting cleanup', () => { }); // this test will fail in the 'afterAll' if there are any rafs left over - it('should not leak request animation frames', () => { + it("should not leak request animation frames", () => { let tree = render( - + , ); - let buttons = tree.getAllByRole('button'); + let buttons = tree.getAllByRole("button"); act(() => buttons[0].focus()); act(() => buttons[1].focus()); act(() => buttons[1].blur()); diff --git a/packages/@react-aria/interactions/src/useFocusWithin.ts b/packages/@react-aria/interactions/src/useFocusWithin.ts index 9e1c839b612..10f6254f69b 100644 --- a/packages/@react-aria/interactions/src/useFocusWithin.ts +++ b/packages/@react-aria/interactions/src/useFocusWithin.ts @@ -54,14 +54,14 @@ export function useFocusWithin(props: FocusWithinProps): FocusWithinResult { let onBlur = useCallback((e: FocusEvent) => { // Ignore events bubbling through portals. - if (!e.currentTarget.contains(e.target)) { + if (!nodeContains(e.currentTarget as Element, e.target as Element)) { return; } // We don't want to trigger onBlurWithin and then immediately onFocusWithin again // when moving focus inside the element. Only trigger if the currentTarget doesn't // include the relatedTarget (where focus is moving). - if (state.current.isFocusWithin && !(e.currentTarget as Element).contains(e.relatedTarget as Element)) { + if (state.current.isFocusWithin && !nodeContains(e.currentTarget as Element, e.relatedTarget as Element)) { state.current.isFocusWithin = false; removeAllGlobalListeners(); @@ -78,7 +78,7 @@ export function useFocusWithin(props: FocusWithinProps): FocusWithinResult { let onSyntheticFocus = useSyntheticBlurEvent(onBlur); let onFocus = useCallback((e: FocusEvent) => { // Ignore events bubbling through portals. - if (!e.currentTarget.contains(e.target)) { + if (!nodeContains(e.currentTarget as Element, e.target as Element)) { return; } diff --git a/packages/@react-aria/interactions/src/useInteractOutside.ts b/packages/@react-aria/interactions/src/useInteractOutside.ts index 94c1e65a46c..374d0f739d6 100644 --- a/packages/@react-aria/interactions/src/useInteractOutside.ts +++ b/packages/@react-aria/interactions/src/useInteractOutside.ts @@ -15,7 +15,7 @@ // NOTICE file in the root directory of this source tree. // See https://github.com/facebook/react/tree/cc7c1aece46a6b69b41958d731e0fd27c94bfc6c/packages/react-interactions -import {getOwnerDocument, useEffectEvent} from '@react-aria/utils'; +import {getOwnerDocument, nodeContains, useEffectEvent} from '@react-aria/utils'; import {RefObject} from '@react-types/shared'; import {useEffect, useRef} from 'react'; @@ -121,7 +121,7 @@ function isValidEvent(event, ref) { if (event.target) { // if the event target is no longer in the document, ignore const ownerDocument = event.target.ownerDocument; - if (!ownerDocument || !ownerDocument.documentElement.contains(event.target)) { + if (!ownerDocument || !nodeContains(ownerDocument.documentElement, event.target)) { return false; } // If the target is within a top layer element (e.g. toasts), ignore. diff --git a/packages/@react-aria/interactions/test/useInteractOutside.test.js b/packages/@react-aria/interactions/test/useInteractOutside.test.js index 2a671aea2c7..5bfb5ef6055 100644 --- a/packages/@react-aria/interactions/test/useInteractOutside.test.js +++ b/packages/@react-aria/interactions/test/useInteractOutside.test.js @@ -10,90 +10,94 @@ * governing permissions and limitations under the License. */ -import {act, createShadowRoot, fireEvent, installPointerEvent, pointerMap, render, waitFor} from '@react-spectrum/test-utils-internal'; -import {enableShadowDOM} from '@react-stately/flags'; -import React, {useEffect, useRef} from 'react'; -import ReactDOM, {createPortal} from 'react-dom'; -import {UNSAFE_PortalProvider} from '@react-aria/overlays'; -import {useInteractOutside} from '../'; -import userEvent from '@testing-library/user-event'; +import { + act, + createShadowRoot, + fireEvent, + installPointerEvent, + pointerMap, + render, + waitFor, +} from "@react-spectrum/test-utils-internal"; +import { enableShadowDOM } from "@react-stately/flags"; +import React, { useEffect, useRef } from "react"; +import ReactDOM, { createPortal } from "react-dom"; +import { UNSAFE_PortalProvider } from "@react-aria/overlays"; +import { useInteractOutside } from "../"; +import userEvent from "@testing-library/user-event"; function Example(props) { let ref = useRef(); - useInteractOutside({ref, ...props}); - return
test
; + useInteractOutside({ ref, ...props }); + return ( +
+ test +
+ ); } function pointerEvent(type, opts) { - let evt = new Event(type, {bubbles: true, cancelable: true}); + let evt = new Event(type, { bubbles: true, cancelable: true }); Object.assign(evt, opts); return evt; } -describe('useInteractOutside', function () { +describe("useInteractOutside", function () { // TODO: JSDOM doesn't yet support pointer events. Once they do, convert these tests. // https://github.com/jsdom/jsdom/issues/2527 - describe('pointer events', function () { + describe("pointer events", function () { installPointerEvent(); - it('should fire interact outside events based on pointer events', function () { + it("should fire interact outside events based on pointer events", function () { let onInteractOutside = jest.fn(); - let res = render( - - ); + let res = render(); - let el = res.getByText('test'); - fireEvent(el, pointerEvent('pointerdown')); - fireEvent(el, pointerEvent('pointerup')); + let el = res.getByText("test"); + fireEvent(el, pointerEvent("pointerdown")); + fireEvent(el, pointerEvent("pointerup")); fireEvent.click(el); expect(onInteractOutside).not.toHaveBeenCalled(); - fireEvent(document.body, pointerEvent('pointerdown')); - fireEvent(document.body, pointerEvent('pointerup')); + fireEvent(document.body, pointerEvent("pointerdown")); + fireEvent(document.body, pointerEvent("pointerup")); fireEvent.click(document.body); expect(onInteractOutside).toHaveBeenCalledTimes(1); }); - it('should only listen for the left mouse button', function () { + it("should only listen for the left mouse button", function () { let onInteractOutside = jest.fn(); - render( - - ); + render(); - fireEvent(document.body, pointerEvent('pointerdown', {button: 1})); - fireEvent(document.body, pointerEvent('pointerup', {button: 1})); - fireEvent.click(document.body, {button: 1}); + fireEvent(document.body, pointerEvent("pointerdown", { button: 1 })); + fireEvent(document.body, pointerEvent("pointerup", { button: 1 })); + fireEvent.click(document.body, { button: 1 }); expect(onInteractOutside).not.toHaveBeenCalled(); - fireEvent(document.body, pointerEvent('pointerdown', {button: 0})); - fireEvent(document.body, pointerEvent('pointerup', {button: 0})); - fireEvent.click(document.body, {button: 0}); + fireEvent(document.body, pointerEvent("pointerdown", { button: 0 })); + fireEvent(document.body, pointerEvent("pointerup", { button: 0 })); + fireEvent.click(document.body, { button: 0 }); expect(onInteractOutside).toHaveBeenCalledTimes(1); }); - it('should not fire interact outside if there is a pointer up event without a pointer down first', function () { + it("should not fire interact outside if there is a pointer up event without a pointer down first", function () { // Fire pointer down before component with useInteractOutside is mounted - fireEvent(document.body, pointerEvent('pointerdown')); + fireEvent(document.body, pointerEvent("pointerdown")); let onInteractOutside = jest.fn(); - render( - - ); + render(); - fireEvent(document.body, pointerEvent('pointerup')); + fireEvent(document.body, pointerEvent("pointerup")); fireEvent.click(document.body); expect(onInteractOutside).not.toHaveBeenCalled(); }); }); - describe('mouse events', function () { - it('should fire interact outside events based on mouse events', function () { + describe("mouse events", function () { + it("should fire interact outside events based on mouse events", function () { let onInteractOutside = jest.fn(); - let res = render( - - ); + let res = render(); - let el = res.getByText('test'); + let el = res.getByText("test"); fireEvent.mouseDown(el); fireEvent.mouseUp(el); expect(onInteractOutside).not.toHaveBeenCalled(); @@ -103,43 +107,37 @@ describe('useInteractOutside', function () { expect(onInteractOutside).toHaveBeenCalledTimes(1); }); - it('should only listen for the left mouse button', function () { + it("should only listen for the left mouse button", function () { let onInteractOutside = jest.fn(); - render( - - ); + render(); - fireEvent.mouseDown(document.body, {button: 1}); - fireEvent.mouseUp(document.body, {button: 1}); + fireEvent.mouseDown(document.body, { button: 1 }); + fireEvent.mouseUp(document.body, { button: 1 }); expect(onInteractOutside).not.toHaveBeenCalled(); - fireEvent.mouseDown(document.body, {button: 0}); - fireEvent.mouseUp(document.body, {button: 0}); + fireEvent.mouseDown(document.body, { button: 0 }); + fireEvent.mouseUp(document.body, { button: 0 }); expect(onInteractOutside).toHaveBeenCalledTimes(1); }); - it('should not fire interact outside if there is a mouse up event without a mouse down first', function () { + it("should not fire interact outside if there is a mouse up event without a mouse down first", function () { // Fire mouse down before component with useInteractOutside is mounted fireEvent.mouseDown(document.body); let onInteractOutside = jest.fn(); - render( - - ); + render(); fireEvent.mouseUp(document.body); expect(onInteractOutside).not.toHaveBeenCalled(); }); }); - describe('touch events', function () { - it('should fire interact outside events based on mouse events', function () { + describe("touch events", function () { + it("should fire interact outside events based on mouse events", function () { let onInteractOutside = jest.fn(); - let res = render( - - ); + let res = render(); - let el = res.getByText('test'); + let el = res.getByText("test"); fireEvent.touchStart(el); fireEvent.touchEnd(el); expect(onInteractOutside).not.toHaveBeenCalled(); @@ -149,13 +147,11 @@ describe('useInteractOutside', function () { expect(onInteractOutside).toHaveBeenCalledTimes(1); }); - it('should ignore emulated mouse events', function () { + it("should ignore emulated mouse events", function () { let onInteractOutside = jest.fn(); - let res = render( - - ); + let res = render(); - let el = res.getByText('test'); + let el = res.getByText("test"); fireEvent.touchStart(el); fireEvent.touchEnd(el); fireEvent.mouseUp(el); @@ -167,47 +163,39 @@ describe('useInteractOutside', function () { expect(onInteractOutside).toHaveBeenCalledTimes(1); }); - it('should not fire interact outside if there is a touch end event without a touch start first', function () { + it("should not fire interact outside if there is a touch end event without a touch start first", function () { // Fire mouse down before component with useInteractOutside is mounted fireEvent.touchStart(document.body); let onInteractOutside = jest.fn(); - render( - - ); + render(); fireEvent.touchEnd(document.body); expect(onInteractOutside).not.toHaveBeenCalled(); }); }); - describe('disable interact outside events', function () { - it('does not handle pointer events if disabled', function () { + describe("disable interact outside events", function () { + it("does not handle pointer events if disabled", function () { let onInteractOutside = jest.fn(); - render( - - ); + render(); - fireEvent(document.body, pointerEvent('mousedown')); - fireEvent(document.body, pointerEvent('mouseup')); + fireEvent(document.body, pointerEvent("mousedown")); + fireEvent(document.body, pointerEvent("mouseup")); expect(onInteractOutside).not.toHaveBeenCalled(); }); - it('does not handle touch events if disabled', function () { + it("does not handle touch events if disabled", function () { let onInteractOutside = jest.fn(); - render( - - ); + render(); fireEvent.touchStart(document.body); fireEvent.touchEnd(document.body); expect(onInteractOutside).not.toHaveBeenCalled(); }); - it('does not handle mouse events if disabled', function () { + it("does not handle mouse events if disabled", function () { let onInteractOutside = jest.fn(); - render( - - ); + render(); fireEvent.mouseDown(document.body); fireEvent.mouseUp(document.body); @@ -216,15 +204,15 @@ describe('useInteractOutside', function () { }); }); -describe('useInteractOutside (iframes)', function () { +describe("useInteractOutside (iframes)", function () { let iframe; let iframeRoot; let iframeDocument; beforeEach(() => { - iframe = document.createElement('iframe'); + iframe = document.createElement("iframe"); window.document.body.appendChild(iframe); iframeDocument = iframe.contentWindow.document; - iframeRoot = iframeDocument.createElement('div'); + iframeRoot = iframeDocument.createElement("div"); iframeDocument.body.appendChild(iframeRoot); }); @@ -238,82 +226,112 @@ describe('useInteractOutside (iframes)', function () { // TODO: JSDOM doesn't yet support pointer events. Once they do, convert these tests. // https://github.com/jsdom/jsdom/issues/2527 - describe('pointer events', function () { + describe("pointer events", function () { installPointerEvent(); - it('should fire interact outside events based on pointer events', async function () { + it("should fire interact outside events based on pointer events", async function () { let onInteractOutside = jest.fn(); - render( - - ); + render(); await waitFor(() => { - expect(document.querySelector('iframe').contentWindow.document.body.querySelector('div[data-testid="example"]')).toBeTruthy(); + expect( + document + .querySelector("iframe") + .contentWindow.document.body.querySelector( + 'div[data-testid="example"]', + ), + ).toBeTruthy(); }); - const el = document.querySelector('iframe').contentWindow.document.body.querySelector('div[data-testid="example"]'); - fireEvent(el, pointerEvent('pointerdown')); - fireEvent(el, pointerEvent('pointerup')); + const el = document + .querySelector("iframe") + .contentWindow.document.body.querySelector( + 'div[data-testid="example"]', + ); + fireEvent(el, pointerEvent("pointerdown")); + fireEvent(el, pointerEvent("pointerup")); fireEvent.click(el); expect(onInteractOutside).not.toHaveBeenCalled(); - fireEvent(iframeDocument.body, pointerEvent('pointerdown')); - fireEvent(iframeDocument.body, pointerEvent('pointerup')); + fireEvent(iframeDocument.body, pointerEvent("pointerdown")); + fireEvent(iframeDocument.body, pointerEvent("pointerup")); fireEvent.click(iframeDocument.body); expect(onInteractOutside).toHaveBeenCalledTimes(1); }); - it('should only listen for the left mouse button', async function () { + it("should only listen for the left mouse button", async function () { let onInteractOutside = jest.fn(); - render( - - ); + render(); await waitFor(() => { - expect(document.querySelector('iframe').contentWindow.document.body.querySelector('div[data-testid="example"]')).toBeTruthy(); + expect( + document + .querySelector("iframe") + .contentWindow.document.body.querySelector( + 'div[data-testid="example"]', + ), + ).toBeTruthy(); }); - fireEvent(iframeDocument.body, pointerEvent('pointerdown', {button: 1})); - fireEvent(iframeDocument.body, pointerEvent('pointerup', {button: 1})); - fireEvent.click(iframeDocument.body, {button: 0}); + fireEvent( + iframeDocument.body, + pointerEvent("pointerdown", { button: 1 }), + ); + fireEvent(iframeDocument.body, pointerEvent("pointerup", { button: 1 })); + fireEvent.click(iframeDocument.body, { button: 0 }); expect(onInteractOutside).not.toHaveBeenCalled(); - fireEvent(iframeDocument.body, pointerEvent('pointerdown', {button: 0})); - fireEvent(iframeDocument.body, pointerEvent('pointerup', {button: 0})); - fireEvent.click(iframeDocument.body, {button: 0}); + fireEvent( + iframeDocument.body, + pointerEvent("pointerdown", { button: 0 }), + ); + fireEvent(iframeDocument.body, pointerEvent("pointerup", { button: 0 })); + fireEvent.click(iframeDocument.body, { button: 0 }); expect(onInteractOutside).toHaveBeenCalledTimes(1); }); - it('should not fire interact outside if there is a pointer up event without a pointer down first', async function () { + it("should not fire interact outside if there is a pointer up event without a pointer down first", async function () { // Fire pointer down before component with useInteractOutside is mounted - fireEvent(iframeDocument.body, pointerEvent('pointerdown')); + fireEvent(iframeDocument.body, pointerEvent("pointerdown")); let onInteractOutside = jest.fn(); - render( - - ); + render(); await waitFor(() => { - expect(document.querySelector('iframe').contentWindow.document.body.querySelector('div[data-testid="example"]')).toBeTruthy(); + expect( + document + .querySelector("iframe") + .contentWindow.document.body.querySelector( + 'div[data-testid="example"]', + ), + ).toBeTruthy(); }); - fireEvent(iframeDocument.body, pointerEvent('pointerup')); + fireEvent(iframeDocument.body, pointerEvent("pointerup")); fireEvent.click(iframeDocument.body); expect(onInteractOutside).not.toHaveBeenCalled(); }); }); - describe('mouse events', function () { - it('should fire interact outside events based on mouse events', async function () { + describe("mouse events", function () { + it("should fire interact outside events based on mouse events", async function () { let onInteractOutside = jest.fn(); - render( - - ); + render(); await waitFor(() => { - expect(document.querySelector('iframe').contentWindow.document.body.querySelector('div[data-testid="example"]')).toBeTruthy(); + expect( + document + .querySelector("iframe") + .contentWindow.document.body.querySelector( + 'div[data-testid="example"]', + ), + ).toBeTruthy(); }); - const el = document.querySelector('iframe').contentWindow.document.body.querySelector('div[data-testid="example"]'); + const el = document + .querySelector("iframe") + .contentWindow.document.body.querySelector( + 'div[data-testid="example"]', + ); fireEvent.mouseDown(el); fireEvent.mouseUp(el); expect(onInteractOutside).not.toHaveBeenCalled(); @@ -323,54 +341,70 @@ describe('useInteractOutside (iframes)', function () { expect(onInteractOutside).toHaveBeenCalledTimes(1); }); - it('should only listen for the left mouse button', async function () { + it("should only listen for the left mouse button", async function () { let onInteractOutside = jest.fn(); - render( - - ); + render(); await waitFor(() => { - expect(document.querySelector('iframe').contentWindow.document.body.querySelector('div[data-testid="example"]')).toBeTruthy(); + expect( + document + .querySelector("iframe") + .contentWindow.document.body.querySelector( + 'div[data-testid="example"]', + ), + ).toBeTruthy(); }); - fireEvent.mouseDown(iframeDocument.body, {button: 1}); - fireEvent.mouseUp(iframeDocument.body, {button: 1}); + fireEvent.mouseDown(iframeDocument.body, { button: 1 }); + fireEvent.mouseUp(iframeDocument.body, { button: 1 }); expect(onInteractOutside).not.toHaveBeenCalled(); - fireEvent.mouseDown(iframeDocument.body, {button: 0}); - fireEvent.mouseUp(iframeDocument.body, {button: 0}); + fireEvent.mouseDown(iframeDocument.body, { button: 0 }); + fireEvent.mouseUp(iframeDocument.body, { button: 0 }); expect(onInteractOutside).toHaveBeenCalledTimes(1); }); - it('should not fire interact outside if there is a mouse up event without a mouse down first', async function () { + it("should not fire interact outside if there is a mouse up event without a mouse down first", async function () { // Fire mouse down before component with useInteractOutside is mounted fireEvent.mouseDown(iframeDocument.body); let onInteractOutside = jest.fn(); - render( - - ); + render(); await waitFor(() => { - expect(document.querySelector('iframe').contentWindow.document.body.querySelector('div[data-testid="example"]')).toBeTruthy(); + expect( + document + .querySelector("iframe") + .contentWindow.document.body.querySelector( + 'div[data-testid="example"]', + ), + ).toBeTruthy(); }); fireEvent.mouseUp(iframeDocument.body); expect(onInteractOutside).not.toHaveBeenCalled(); }); }); - describe('touch events', function () { - it('should fire interact outside events based on mouse events', async function () { + describe("touch events", function () { + it("should fire interact outside events based on mouse events", async function () { let onInteractOutside = jest.fn(); - render( - - ); + render(); await waitFor(() => { - expect(document.querySelector('iframe').contentWindow.document.body.querySelector('div[data-testid="example"]')).toBeTruthy(); + expect( + document + .querySelector("iframe") + .contentWindow.document.body.querySelector( + 'div[data-testid="example"]', + ), + ).toBeTruthy(); }); - const el = document.querySelector('iframe').contentWindow.document.body.querySelector('div[data-testid="example"]'); + const el = document + .querySelector("iframe") + .contentWindow.document.body.querySelector( + 'div[data-testid="example"]', + ); fireEvent.touchStart(el); fireEvent.touchEnd(el); expect(onInteractOutside).not.toHaveBeenCalled(); @@ -380,17 +414,25 @@ describe('useInteractOutside (iframes)', function () { expect(onInteractOutside).toHaveBeenCalledTimes(1); }); - it('should ignore emulated mouse events', async function () { + it("should ignore emulated mouse events", async function () { let onInteractOutside = jest.fn(); - render( - - ); + render(); await waitFor(() => { - expect(document.querySelector('iframe').contentWindow.document.body.querySelector('div[data-testid="example"]')).toBeTruthy(); + expect( + document + .querySelector("iframe") + .contentWindow.document.body.querySelector( + 'div[data-testid="example"]', + ), + ).toBeTruthy(); }); - const el = document.querySelector('iframe').contentWindow.document.body.querySelector('div[data-testid="example"]'); + const el = document + .querySelector("iframe") + .contentWindow.document.body.querySelector( + 'div[data-testid="example"]', + ); fireEvent.touchStart(el); fireEvent.touchEnd(el); fireEvent.mouseUp(el); @@ -402,36 +444,34 @@ describe('useInteractOutside (iframes)', function () { expect(onInteractOutside).toHaveBeenCalledTimes(1); }); - it('should not fire interact outside if there is a touch end event without a touch start first', function () { + it("should not fire interact outside if there is a touch end event without a touch start first", function () { // Fire mouse down before component with useInteractOutside is mounted fireEvent.touchStart(iframeDocument.body); let onInteractOutside = jest.fn(); - render( - - ); + render(); fireEvent.touchEnd(iframeDocument.body); expect(onInteractOutside).not.toHaveBeenCalled(); }); }); - describe('disable interact outside events', function () { - it('does not handle pointer events if disabled', function () { + describe("disable interact outside events", function () { + it("does not handle pointer events if disabled", function () { let onInteractOutside = jest.fn(); render( - + , ); - fireEvent(iframeDocument.body, pointerEvent('mousedown')); - fireEvent(iframeDocument.body, pointerEvent('mouseup')); + fireEvent(iframeDocument.body, pointerEvent("mousedown")); + fireEvent(iframeDocument.body, pointerEvent("mouseup")); expect(onInteractOutside).not.toHaveBeenCalled(); }); - it('does not handle touch events if disabled', function () { + it("does not handle touch events if disabled", function () { let onInteractOutside = jest.fn(); render( - + , ); fireEvent.touchStart(iframeDocument.body); @@ -439,10 +479,10 @@ describe('useInteractOutside (iframes)', function () { expect(onInteractOutside).not.toHaveBeenCalled(); }); - it('does not handle mouse events if disabled', function () { + it("does not handle mouse events if disabled", function () { let onInteractOutside = jest.fn(); render( - + , ); fireEvent.mouseDown(iframeDocument.body); @@ -452,24 +492,24 @@ describe('useInteractOutside (iframes)', function () { }); }); -describe('useInteractOutside shadow DOM', function () { +describe("useInteractOutside shadow DOM", function () { // Helper function to create a shadow root and render the component inside it function createShadowRootAndRender(ui) { - const shadowHost = document.createElement('div'); + const shadowHost = document.createElement("div"); document.body.appendChild(shadowHost); - const shadowRoot = shadowHost.attachShadow({mode: 'open'}); + const shadowRoot = shadowHost.attachShadow({ mode: "open" }); function WrapperComponent() { return ReactDOM.createPortal(ui, shadowRoot); } render(); - return {shadowRoot, cleanup: () => document.body.removeChild(shadowHost)}; + return { shadowRoot, cleanup: () => document.body.removeChild(shadowHost) }; } - function App({onInteractOutside}) { + function App({ onInteractOutside }) { const ref = useRef(null); - useInteractOutside({ref, onInteractOutside}); + useInteractOutside({ ref, onInteractOutside }); return (
@@ -481,13 +521,13 @@ describe('useInteractOutside shadow DOM', function () { ); } - it('does not trigger when clicking inside popover', function () { + it("does not trigger when clicking inside popover", function () { const onInteractOutside = jest.fn(); - const {shadowRoot, cleanup} = createShadowRootAndRender( - + const { shadowRoot, cleanup } = createShadowRootAndRender( + , ); - const insidePopover = shadowRoot.getElementById('inside-popover'); + const insidePopover = shadowRoot.getElementById("inside-popover"); fireEvent.mouseDown(insidePopover); fireEvent.mouseUp(insidePopover); @@ -495,13 +535,13 @@ describe('useInteractOutside shadow DOM', function () { cleanup(); }); - it('does not trigger when clicking the popover', function () { + it("does not trigger when clicking the popover", function () { const onInteractOutside = jest.fn(); - const {shadowRoot, cleanup} = createShadowRootAndRender( - + const { shadowRoot, cleanup } = createShadowRootAndRender( + , ); - const popover = shadowRoot.getElementById('popover'); + const popover = shadowRoot.getElementById("popover"); fireEvent.mouseDown(popover); fireEvent.mouseUp(popover); @@ -509,10 +549,10 @@ describe('useInteractOutside shadow DOM', function () { cleanup(); }); - it('triggers when clicking outside the popover', function () { + it("triggers when clicking outside the popover", function () { const onInteractOutside = jest.fn(); - const {cleanup} = createShadowRootAndRender( - + const { cleanup } = createShadowRootAndRender( + , ); // Clicking on the document body outside the shadow DOM @@ -523,13 +563,13 @@ describe('useInteractOutside shadow DOM', function () { cleanup(); }); - it('triggers when clicking a button outside the shadow dom altogether', function () { + it("triggers when clicking a button outside the shadow dom altogether", function () { const onInteractOutside = jest.fn(); - const {cleanup} = createShadowRootAndRender( - + const { cleanup } = createShadowRootAndRender( + , ); // Button outside shadow DOM and component - const button = document.createElement('button'); + const button = document.createElement("button"); document.body.appendChild(button); fireEvent.mouseDown(button); @@ -541,29 +581,29 @@ describe('useInteractOutside shadow DOM', function () { }); }); -describe('useInteractOutside shadow DOM extended tests', function () { +describe("useInteractOutside shadow DOM extended tests", function () { // Setup function similar to previous tests, but includes a dynamic element scenario function createShadowRootAndRender(ui) { - const shadowHost = document.createElement('div'); + const shadowHost = document.createElement("div"); document.body.appendChild(shadowHost); - const shadowRoot = shadowHost.attachShadow({mode: 'open'}); + const shadowRoot = shadowHost.attachShadow({ mode: "open" }); function WrapperComponent() { return ReactDOM.createPortal(ui, shadowRoot); } render(); - return {shadowRoot, cleanup: () => document.body.removeChild(shadowHost)}; + return { shadowRoot, cleanup: () => document.body.removeChild(shadowHost) }; } - function App({onInteractOutside, includeDynamicElement = false}) { + function App({ onInteractOutside, includeDynamicElement = false }) { const ref = useRef(null); - useInteractOutside({ref, onInteractOutside}); + useInteractOutside({ ref, onInteractOutside }); useEffect(() => { if (includeDynamicElement) { - const dynamicEl = document.createElement('div'); - dynamicEl.id = 'dynamic-outside'; + const dynamicEl = document.createElement("div"); + dynamicEl.id = "dynamic-outside"; document.body.appendChild(dynamicEl); return () => document.body.removeChild(dynamicEl); @@ -580,14 +620,14 @@ describe('useInteractOutside shadow DOM extended tests', function () { ); } - it('correctly identifies interaction with dynamically added external elements', function () { + it("correctly identifies interaction with dynamically added external elements", function () { jest.useFakeTimers(); const onInteractOutside = jest.fn(); - const {cleanup} = createShadowRootAndRender( - + const { cleanup } = createShadowRootAndRender( + , ); - const dynamicEl = document.getElementById('dynamic-outside'); + const dynamicEl = document.getElementById("dynamic-outside"); fireEvent.mouseDown(dynamicEl); fireEvent.mouseUp(dynamicEl); @@ -597,12 +637,12 @@ describe('useInteractOutside shadow DOM extended tests', function () { }); }); -describe('useInteractOutside with Shadow DOM and UNSAFE_PortalProvider', () => { +describe("useInteractOutside with Shadow DOM and UNSAFE_PortalProvider", () => { let user; beforeAll(() => { enableShadowDOM(); - user = userEvent.setup({delay: null, pointerMap}); + user = userEvent.setup({ delay: null, pointerMap }); }); beforeEach(() => { @@ -610,46 +650,66 @@ describe('useInteractOutside with Shadow DOM and UNSAFE_PortalProvider', () => { }); afterEach(() => { - act(() => {jest.runAllTimers();}); + act(() => { + jest.runAllTimers(); + }); }); - it('should handle interact outside events with UNSAFE_PortalProvider in shadow DOM', async () => { - const {shadowRoot} = createShadowRoot(); + it("should handle interact outside events with UNSAFE_PortalProvider in shadow DOM", async () => { + const { shadowRoot } = createShadowRoot(); let interactOutsideTriggered = false; + // Create portal container within the shadow DOM for the popover + const popoverPortal = document.createElement("div"); + popoverPortal.setAttribute("data-testid", "popover-portal"); + shadowRoot.appendChild(popoverPortal); + function ShadowInteractOutsideExample() { const ref = useRef(); useInteractOutside({ ref, onInteractOutside: () => { interactOutsideTriggered = true; - } + }, }); return ( shadowRoot}>
-
- - -
- + {ReactDOM.createPortal( + <> +
+ + +
+ + , + popoverPortal, + )}
); } - const {unmount} = render(); + const { unmount } = render(); const target = shadowRoot.querySelector('[data-testid="target"]'); - const innerButton = shadowRoot.querySelector('[data-testid="inner-button"]'); - const outsideButton = shadowRoot.querySelector('[data-testid="outside-button"]'); + const innerButton = shadowRoot.querySelector( + '[data-testid="inner-button"]', + ); + const outsideButton = shadowRoot.querySelector( + '[data-testid="outside-button"]', + ); // Click inside the target - should NOT trigger interact outside await user.click(innerButton); expect(interactOutsideTriggered).toBe(false); - // Click the target itself - should NOT trigger interact outside + // Click the target itself - should NOT trigger interact outside await user.click(target); expect(interactOutsideTriggered).toBe(false); @@ -662,8 +722,8 @@ describe('useInteractOutside with Shadow DOM and UNSAFE_PortalProvider', () => { document.body.removeChild(shadowRoot.host); }); - it('should correctly identify interactions across shadow DOM boundaries (issue #8675)', async () => { - const {shadowRoot} = createShadowRoot(); + it("should correctly identify interactions across shadow DOM boundaries (issue #8675)", async () => { + const { shadowRoot } = createShadowRoot(); let popoverClosed = false; function MenuPopoverExample() { @@ -672,34 +732,34 @@ describe('useInteractOutside with Shadow DOM and UNSAFE_PortalProvider', () => { ref: popoverRef, onInteractOutside: () => { popoverClosed = true; - } + }, }); return ( shadowRoot}>
-
- - - + {/* Modal */} {ReactDOM.createPortal( -
- + {/* Popover within modal */} -
, - modalPortal + modalPortal, )}
); } - const {unmount} = render(); + const { unmount } = render(); const mainButton = shadowRoot.querySelector('[data-testid="main-button"]'); - const modalButton = shadowRoot.querySelector('[data-testid="modal-button"]'); - const popoverButton = shadowRoot.querySelector('[data-testid="popover-button"]'); + const modalButton = shadowRoot.querySelector( + '[data-testid="modal-button"]', + ); + const popoverButton = shadowRoot.querySelector( + '[data-testid="popover-button"]', + ); // Click popover button - should NOT trigger either interact outside await user.click(popoverButton); @@ -821,10 +893,10 @@ describe('useInteractOutside with Shadow DOM and UNSAFE_PortalProvider', () => { document.body.removeChild(shadowRoot.host); }); - it('should handle pointer events correctly in shadow DOM with portal provider', async () => { + it("should handle pointer events correctly in shadow DOM with portal provider", async () => { installPointerEvent(); - const {shadowRoot} = createShadowRoot(); + const { shadowRoot } = createShadowRoot(); let interactOutsideCount = 0; function PointerEventsExample() { @@ -833,7 +905,7 @@ describe('useInteractOutside with Shadow DOM and UNSAFE_PortalProvider', () => { ref, onInteractOutside: () => { interactOutsideCount++; - } + }, }); return ( @@ -848,20 +920,24 @@ describe('useInteractOutside with Shadow DOM and UNSAFE_PortalProvider', () => { ); } - const {unmount} = render(); + const { unmount } = render(); - const targetButton = shadowRoot.querySelector('[data-testid="target-button"]'); - const outsideButton = shadowRoot.querySelector('[data-testid="outside-button"]'); + const targetButton = shadowRoot.querySelector( + '[data-testid="target-button"]', + ); + const outsideButton = shadowRoot.querySelector( + '[data-testid="outside-button"]', + ); // Simulate pointer events on target - should NOT trigger interact outside - fireEvent(targetButton, pointerEvent('pointerdown')); - fireEvent(targetButton, pointerEvent('pointerup')); + fireEvent(targetButton, pointerEvent("pointerdown")); + fireEvent(targetButton, pointerEvent("pointerup")); fireEvent.click(targetButton); expect(interactOutsideCount).toBe(0); // Simulate pointer events outside - should trigger interact outside - fireEvent(outsideButton, pointerEvent('pointerdown')); - fireEvent(outsideButton, pointerEvent('pointerup')); + fireEvent(outsideButton, pointerEvent("pointerdown")); + fireEvent(outsideButton, pointerEvent("pointerup")); fireEvent.click(outsideButton); expect(interactOutsideCount).toBe(1); @@ -870,26 +946,26 @@ describe('useInteractOutside with Shadow DOM and UNSAFE_PortalProvider', () => { document.body.removeChild(shadowRoot.host); }); - it('should handle interact outside with dynamic content in shadow DOM', async () => { - const {shadowRoot} = createShadowRoot(); + it("should handle interact outside with dynamic content in shadow DOM", async () => { + const { shadowRoot } = createShadowRoot(); let interactOutsideCount = 0; function DynamicContentExample() { const ref = useRef(); const [showContent, setShowContent] = React.useState(true); - + useInteractOutside({ ref, onInteractOutside: () => { interactOutsideCount++; - } + }, }); return ( shadowRoot}>
-
- , + ); - let input1 = getByTestId("input1"); - let input2 = getByTestId("input2"); - let input3 = getByTestId("input3"); + let input1 = getByTestId('input1'); + let input2 = getByTestId('input2'); + let input3 = getByTestId('input3'); - act(() => { - input1.focus(); - }); + act(() => {input1.focus();}); expect(document.activeElement).toBe(input1); await user.tab(); @@ -117,39 +104,37 @@ describe("FocusScope", function () { await user.tab(); expect(document.activeElement).toBe(input1); - await user.tab({ shift: true }); + await user.tab({shift: true}); expect(document.activeElement).toBe(input3); - await user.tab({ shift: true }); + await user.tab({shift: true}); expect(document.activeElement).toBe(input2); - await user.tab({ shift: true }); + await user.tab({shift: true}); expect(document.activeElement).toBe(input1); }); - it("should skip non-tabbable elements", async function () { - let { getByTestId } = render( + it('should skip non-tabbable elements', async function () { + let {getByTestId} = render(
- - - + + +
- , + ); - let input1 = getByTestId("input1"); - let input2 = getByTestId("input2"); - let input3 = getByTestId("input3"); + let input1 = getByTestId('input1'); + let input2 = getByTestId('input2'); + let input3 = getByTestId('input3'); - act(() => { - input1.focus(); - }); + act(() => {input1.focus();}); expect(document.activeElement).toBe(input1); await user.tab(); @@ -161,18 +146,18 @@ describe("FocusScope", function () { await user.tab(); expect(document.activeElement).toBe(input1); - await user.tab({ shift: true }); + await user.tab({shift: true}); expect(document.activeElement).toBe(input3); - await user.tab({ shift: true }); + await user.tab({shift: true}); expect(document.activeElement).toBe(input2); - await user.tab({ shift: true }); + await user.tab({shift: true}); expect(document.activeElement).toBe(input1); }); - it("should only skip content editable which are false", async function () { - let { getByTestId } = render( + it('should only skip content editable which are false', async function () { + let {getByTestId} = render( @@ -180,17 +165,15 @@ describe("FocusScope", function () { - , + ); - let input1 = getByTestId("input1"); - let input2 = getByTestId("input2"); - let input3 = getByTestId("input3"); - let input4 = getByTestId("input4"); + let input1 = getByTestId('input1'); + let input2 = getByTestId('input2'); + let input3 = getByTestId('input3'); + let input4 = getByTestId('input4'); - act(() => { - input1.focus(); - }); + act(() => {input1.focus();}); expect(document.activeElement).toBe(input1); await user.tab(); @@ -202,66 +185,62 @@ describe("FocusScope", function () { await user.tab(); expect(document.activeElement).toBe(input4); - await user.tab({ shift: true }); + await user.tab({shift: true}); expect(document.activeElement).toBe(input3); - await user.tab({ shift: true }); + await user.tab({shift: true}); expect(document.activeElement).toBe(input2); - await user.tab({ shift: true }); + await user.tab({shift: true}); expect(document.activeElement).toBe(input1); }); - it("should do nothing if a modifier key is pressed", function () { - let { getByTestId } = render( + it('should do nothing if a modifier key is pressed', function () { + let {getByTestId} = render( - , + ); - let input1 = getByTestId("input1"); + let input1 = getByTestId('input1'); - act(() => { - input1.focus(); - }); + act(() => {input1.focus();}); expect(document.activeElement).toBe(input1); - fireEvent.keyDown(document.activeElement, { key: "Tab", altKey: true }); + fireEvent.keyDown(document.activeElement, {key: 'Tab', altKey: true}); expect(document.activeElement).toBe(input1); }); - it("should work with multiple focus scopes", async function () { - let { getByTestId } = render( + it('should work with multiple focus scopes', async function () { + let {getByTestId} = render(
- - - + + + - - - + + + -
, +
); - let input1 = getByTestId("input1"); - let input2 = getByTestId("input2"); - let input3 = getByTestId("input3"); - let input4 = getByTestId("input4"); + let input1 = getByTestId('input1'); + let input2 = getByTestId('input2'); + let input3 = getByTestId('input3'); + let input4 = getByTestId('input4'); - act(() => { - input1.focus(); - }); + act(() => {input1.focus();}); expect(document.activeElement).toBe(input1); await user.tab(); @@ -273,23 +252,21 @@ describe("FocusScope", function () { await user.tab(); expect(document.activeElement).toBe(input1); - await user.tab({ shift: true }); + await user.tab({shift: true}); expect(document.activeElement).toBe(input3); - await user.tab({ shift: true }); + await user.tab({shift: true}); expect(document.activeElement).toBe(input2); - await user.tab({ shift: true }); + await user.tab({shift: true}); expect(document.activeElement).toBe(input1); - act(() => { - input4.focus(); - }); + act(() => {input4.focus();}); expect(document.activeElement).toBe(input1); }); - it("should restore focus to the last focused element in the scope when re-entering the browser", async function () { - let { getByTestId } = render( + it('should restore focus to the last focused element in the scope when re-entering the browser', async function () { + let {getByTestId} = render(
@@ -297,95 +274,75 @@ describe("FocusScope", function () { -
, +
); - let input1 = getByTestId("input1"); - let input2 = getByTestId("input2"); - let outside = getByTestId("outside"); + let input1 = getByTestId('input1'); + let input2 = getByTestId('input2'); + let outside = getByTestId('outside'); - act(() => { - input1.focus(); - }); + act(() => {input1.focus();}); fireEvent.focusIn(input1); // jsdom doesn't fire this automatically expect(document.activeElement).toBe(input1); await user.tab(); fireEvent.focusIn(input2); - act(() => { - jest.runAllTimers(); - }); + act(() => {jest.runAllTimers();}); expect(document.activeElement).toBe(input2); - act(() => { - input2.blur(); - }); - act(() => { - jest.runAllTimers(); - }); + act(() => {input2.blur();}); + act(() => {jest.runAllTimers();}); expect(document.activeElement).toBe(input2); - act(() => { - outside.focus(); - }); + act(() => {outside.focus();}); fireEvent.focusIn(outside); expect(document.activeElement).toBe(input2); }); - it("should restore focus to the last focused element in the scope on focus out", async function () { - let { getByTestId } = render( + it('should restore focus to the last focused element in the scope on focus out', async function () { + let {getByTestId} = render(
-
, +
); - let input1 = getByTestId("input1"); - let input2 = getByTestId("input2"); + let input1 = getByTestId('input1'); + let input2 = getByTestId('input2'); - act(() => { - input1.focus(); - }); + act(() => {input1.focus();}); fireEvent.focusIn(input1); // jsdom doesn't fire this automatically expect(document.activeElement).toBe(input1); await user.tab(); fireEvent.focusIn(input2); - act(() => { - jest.runAllTimers(); - }); + act(() => {jest.runAllTimers();}); expect(document.activeElement).toBe(input2); - act(() => { - input2.blur(); - }); - act(() => { - jest.runAllTimers(); - }); + act(() => {input2.blur();}); + act(() => {jest.runAllTimers();}); expect(document.activeElement).toBe(input2); fireEvent.focusOut(input2); expect(document.activeElement).toBe(input2); }); // This test setup is a bit contrived to just purely simulate the blur/focus events that would happen in a case like this - it("focus properly moves into child iframe on click", function () { - let { getByTestId } = render( + it('focus properly moves into child iframe on click', function () { + let {getByTestId} = render(
-
, +
); - let input1 = getByTestId("input1"); - let input2 = getByTestId("input2"); + let input1 = getByTestId('input1'); + let input2 = getByTestId('input2'); - act(() => { - input1.focus(); - }); + act(() => {input1.focus();}); fireEvent.focusIn(input1); // jsdom doesn't fire this automatically expect(document.activeElement).toBe(input1); @@ -393,16 +350,16 @@ describe("FocusScope", function () { // set document.activeElement to input2 input2.focus(); // if onBlur didn't fallback to checking document.activeElement, this would reset focus to input1 - fireEvent.blur(input1, { relatedTarget: null }); + fireEvent.blur(input1, {relatedTarget: null}); }); expect(document.activeElement).toBe(input2); }); }); - describe("focus restoration", function () { - it("should restore focus to the previously focused node on unmount", function () { - function Test({ show }) { + describe('focus restoration', function () { + it('should restore focus to the previously focused node on unmount', function () { + function Test({show}) { return (
@@ -417,52 +374,46 @@ describe("FocusScope", function () { ); } - let { getByTestId, rerender } = render(); + let {getByTestId, rerender} = render(); - let outside = getByTestId("outside"); - act(() => { - outside.focus(); - }); + let outside = getByTestId('outside'); + act(() => {outside.focus();}); rerender(); - let input1 = getByTestId("input1"); + let input1 = getByTestId('input1'); expect(document.activeElement).toBe(input1); rerender(); - act(() => { - jest.runAllTimers(); - }); + act(() => {jest.runAllTimers();}); expect(document.activeElement).toBe(outside); }); - it("should restore focus to the previously focused node after a child with autoFocus unmounts", function () { - function Test({ show }) { + it('should restore focus to the previously focused node after a child with autoFocus unmounts', function () { + function Test({show}) { return (
- {show && ( + {show && - )} + }
); } - let { getByTestId, rerender } = render(); + let {getByTestId, rerender} = render(); - let outside = getByTestId("outside"); - act(() => { - outside.focus(); - }); + let outside = getByTestId('outside'); + act(() => {outside.focus();}); rerender(); - let input2 = getByTestId("input2"); + let input2 = getByTestId('input2'); expect(document.activeElement).toBe(input2); rerender(); @@ -473,115 +424,105 @@ describe("FocusScope", function () { expect(document.activeElement).toBe(outside); }); - it("should move focus after the previously focused node when tabbing away from a scope with autoFocus", async function () { - function Test({ show }) { + it('should move focus after the previously focused node when tabbing away from a scope with autoFocus', async function () { + function Test({show}) { return (
- {show && ( + {show && - )} + }
); } - let { getByTestId, rerender } = render(); + let {getByTestId, rerender} = render(); - let outside = getByTestId("outside"); - act(() => { - outside.focus(); - }); + let outside = getByTestId('outside'); + act(() => {outside.focus();}); rerender(); - let input3 = getByTestId("input3"); + let input3 = getByTestId('input3'); expect(document.activeElement).toBe(input3); await user.tab(); - expect(document.activeElement).toBe(getByTestId("after")); + expect(document.activeElement).toBe(getByTestId('after')); }); - it("should move focus before the previously focused node when tabbing away from a scope with Shift+Tab", async function () { - function Test({ show }) { + it('should move focus before the previously focused node when tabbing away from a scope with Shift+Tab', async function () { + function Test({show}) { return (
- {show && ( + {show && - )} + }
); } - let { getByTestId, rerender } = render(); + let {getByTestId, rerender} = render(); - let outside = getByTestId("outside"); - act(() => { - outside.focus(); - }); + let outside = getByTestId('outside'); + act(() => {outside.focus();}); rerender(); - let input1 = getByTestId("input1"); + let input1 = getByTestId('input1'); expect(document.activeElement).toBe(input1); - await user.tab({ shift: true }); - expect(document.activeElement).toBe(getByTestId("before")); + await user.tab({shift: true}); + expect(document.activeElement).toBe(getByTestId('before')); }); - it("should restore focus to the previously focused node after children change", function () { - function Test({ show, showChild }) { + it('should restore focus to the previously focused node after children change', function () { + function Test({show, showChild}) { return (
- {show && ( + {show && {showChild && } - )} + }
); } - let { getByTestId, rerender } = render(); + let {getByTestId, rerender} = render(); - let outside = getByTestId("outside"); - act(() => { - outside.focus(); - }); + let outside = getByTestId('outside'); + act(() => {outside.focus();}); rerender(); rerender(); - let dynamic = getByTestId("dynamic"); - act(() => { - dynamic.focus(); - }); + let dynamic = getByTestId('dynamic'); + act(() => {dynamic.focus();}); expect(document.activeElement).toBe(dynamic); rerender(); - act(() => { - jest.runAllTimers(); - }); + act(() => {jest.runAllTimers();}); expect(document.activeElement).toBe(outside); }); - it("should move focus to the element after the previously focused node on Tab", async function () { - function Test({ show }) { + it('should move focus to the element after the previously focused node on Tab', async function () { + function Test({show}) { return (
@@ -598,138 +539,124 @@ describe("FocusScope", function () { ); } - let { getByTestId, rerender } = render(); + let {getByTestId, rerender} = render(); - let trigger = getByTestId("trigger"); - act(() => { - trigger.focus(); - }); + let trigger = getByTestId('trigger'); + act(() => {trigger.focus();}); rerender(); - let input1 = getByTestId("input1"); + let input1 = getByTestId('input1'); expect(document.activeElement).toBe(input1); - let input3 = getByTestId("input3"); - act(() => { - input3.focus(); - }); + let input3 = getByTestId('input3'); + act(() => {input3.focus();}); await user.tab(); - expect(document.activeElement).toBe(getByTestId("after")); + expect(document.activeElement).toBe(getByTestId('after')); }); - it("should move focus to the previous element after the previously focused node on Shift+Tab", async function () { - function Test({ show }) { + it('should move focus to the previous element after the previously focused node on Shift+Tab', async function () { + function Test({show}) { return (
); } - let { getByTestId, rerender } = render(); + let {getByTestId, rerender} = render(); - let trigger = getByTestId("trigger"); - act(() => { - trigger.focus(); - }); + let trigger = getByTestId('trigger'); + act(() => {trigger.focus();}); rerender(); - let input1 = getByTestId("input1"); + let input1 = getByTestId('input1'); expect(document.activeElement).toBe(input1); - await user.tab({ shift: true }); - expect(document.activeElement).toBe(getByTestId("before")); + await user.tab({shift: true}); + expect(document.activeElement).toBe(getByTestId('before')); }); - it("should skip over elements within the scope when moving focus to the next element", async function () { - function Test({ show }) { + it('should skip over elements within the scope when moving focus to the next element', async function () { + function Test({show}) { return (
); } - let { getByTestId, rerender } = render(); + let {getByTestId, rerender} = render(); - let trigger = getByTestId("trigger"); - act(() => { - trigger.focus(); - }); + let trigger = getByTestId('trigger'); + act(() => {trigger.focus();}); rerender(); - let input1 = getByTestId("input1"); + let input1 = getByTestId('input1'); expect(document.activeElement).toBe(input1); - let input3 = getByTestId("input3"); - act(() => { - input3.focus(); - }); + let input3 = getByTestId('input3'); + act(() => {input3.focus();}); await user.tab(); - expect(document.activeElement).toBe(getByTestId("after")); + expect(document.activeElement).toBe(getByTestId('after')); }); - it("should not handle tabbing if the focus scope does not restore focus", async function () { - function Test({ show }) { + it('should not handle tabbing if the focus scope does not restore focus', async function () { + function Test({show}) { return (
); } - let { getByTestId, rerender } = render(); + let {getByTestId, rerender} = render(); - let trigger = getByTestId("trigger"); - act(() => { - trigger.focus(); - }); + let trigger = getByTestId('trigger'); + act(() => {trigger.focus();}); rerender(); - let input1 = getByTestId("input1"); + let input1 = getByTestId('input1'); expect(document.activeElement).toBe(input1); - let input3 = getByTestId("input3"); - act(() => { - input3.focus(); - }); + let input3 = getByTestId('input3'); + act(() => {input3.focus();}); await user.tab(); - expect(document.activeElement).toBe(getByTestId("after")); + expect(document.activeElement).toBe(getByTestId('after')); }); it.each` @@ -739,65 +666,63 @@ describe("FocusScope", function () { ${false} | ${true} ${true} | ${true} `( - "contain=$contain, isPortaled=$isPortaled should restore focus to previous nodeToRestore when the nodeToRestore for the unmounting scope in no longer in the DOM", - async function ({ contain, isPortaled }) { + 'contain=$contain, isPortaled=$isPortaled should restore focus to previous nodeToRestore when the nodeToRestore for the unmounting scope in no longer in the DOM', + async function ({contain, isPortaled}) { expect(focusScopeTree.size).toBe(1); - let { getAllByText, getAllByRole } = render( - , - ); + let {getAllByText, getAllByRole} = render(); expect(focusScopeTree.size).toBe(1); act(() => { - getAllByText("Open dialog")[0].focus(); + getAllByText('Open dialog')[0].focus(); }); await user.click(document.activeElement); act(() => { jest.runAllTimers(); }); - expect(document.activeElement).toBe(getAllByRole("textbox")[2]); + expect(document.activeElement).toBe(getAllByRole('textbox')[2]); act(() => { - getAllByText("Open dialog")[1].focus(); + getAllByText('Open dialog')[1].focus(); }); await user.click(document.activeElement); act(() => { jest.runAllTimers(); }); - expect(document.activeElement).toBe(getAllByRole("textbox")[5]); + expect(document.activeElement).toBe(getAllByRole('textbox')[5]); act(() => { - getAllByText("Open dialog")[2].focus(); + getAllByText('Open dialog')[2].focus(); }); await user.click(document.activeElement); act(() => { jest.runAllTimers(); }); - expect(document.activeElement).toBe(getAllByRole("textbox")[8]); + expect(document.activeElement).toBe(getAllByRole('textbox')[8]); expect(focusScopeTree.size).toBe(4); if (!contain) { act(() => { - getAllByText("close")[1].focus(); + getAllByText('close')[1].focus(); }); await user.click(document.activeElement); } else { - fireEvent.click(getAllByText("close")[1]); + fireEvent.click(getAllByText('close')[1]); } act(() => { jest.runAllTimers(); }); - expect(document.activeElement).toBe(getAllByText("Open dialog")[1]); + expect(document.activeElement).toBe(getAllByText('Open dialog')[1]); act(() => { - getAllByText("close")[0].focus(); + getAllByText('close')[0].focus(); }); await user.click(document.activeElement); act(() => { jest.runAllTimers(); }); - expect(document.activeElement).toBe(getAllByText("Open dialog")[0]); + expect(document.activeElement).toBe(getAllByText('Open dialog')[0]); expect(focusScopeTree.size).toBe(1); - }, + } ); - describe("focusable first in scope", function () { - it("should restore focus to the first focusable or tabbable element within the scope when focus is lost within the scope", async function () { - let { getByTestId } = render( + describe('focusable first in scope', function () { + it('should restore focus to the first focusable or tabbable element within the scope when focus is lost within the scope', async function () { + let {getByTestId} = render(
@@ -812,12 +737,12 @@ describe("FocusScope", function () {
-
, +
); function Item(props) { let focusManager = useFocusManager(); - let onClick = (e) => { + let onClick = e => { focusManager.focusNext(); act(() => { // remove fails to fire blur event in jest-dom @@ -828,10 +753,10 @@ describe("FocusScope", function () { }; return - {" "} + {' '} {display && ( @@ -885,15 +802,15 @@ describe("FocusScope", function () { ); } - let { getByTestId } = render(); - let button1 = getByTestId("button1"); - let button2 = getByTestId("button2"); + let {getByTestId} = render(); + let button1 = getByTestId('button1'); + let button2 = getByTestId('button2'); await user.click(button1); act(() => { jest.runAllTimers(); }); expect(document.activeElement).toBe(button1); - let input1 = getByTestId("input1"); + let input1 = getByTestId('input1'); expect(input1).toBeVisible(); await user.click(button2); @@ -907,11 +824,11 @@ describe("FocusScope", function () { act(() => { jest.runAllTimers(); }); - input1 = getByTestId("input1"); + input1 = getByTestId('input1'); expect(input1).toBeVisible(); await user.tab(); - fireEvent.keyDown(document.activeElement, { key: "Escape" }); - fireEvent.keyUp(document.activeElement, { key: "Escape" }); + fireEvent.keyDown(document.activeElement, {key: 'Escape'}); + fireEvent.keyUp(document.activeElement, {key: 'Escape'}); act(() => { jest.runAllTimers(); }); @@ -919,11 +836,11 @@ describe("FocusScope", function () { expect(input1).not.toBeInTheDocument(); }); - it("should allow restoration to be overridden with a custom event", async function () { + it('should allow restoration to be overridden with a custom event', async function () { function Test() { let [show, setShow] = React.useState(false); let ref = React.useRef(null); - useEvent(ref, "react-aria-focus-scope-restore", (e) => { + useEvent(ref, 'react-aria-focus-scope-restore', e => { e.preventDefault(); }); @@ -939,24 +856,24 @@ describe("FocusScope", function () { ); } - let { getByRole } = render(); - let button = getByRole("button"); + let {getByRole} = render(); + let button = getByRole('button'); await user.click(button); - let input = getByRole("textbox"); + let input = getByRole('textbox'); expect(document.activeElement).toBe(input); - await user.keyboard("{Escape}"); + await user.keyboard('{Escape}'); act(() => jest.runAllTimers()); expect(input).not.toBeInTheDocument(); expect(document.activeElement).toBe(document.body); }); - it("should not bubble focus scope restoration event out of nested focus scopes", async function () { + it('should not bubble focus scope restoration event out of nested focus scopes', async function () { function Test() { let [show, setShow] = React.useState(false); let ref = React.useRef(null); - useEvent(ref, "react-aria-focus-scope-restore", (e) => { + useEvent(ref, 'react-aria-focus-scope-restore', e => { e.preventDefault(); }); @@ -974,56 +891,56 @@ describe("FocusScope", function () { ); } - let { getByRole } = render(); - let button = getByRole("button"); + let {getByRole} = render(); + let button = getByRole('button'); await user.click(button); - let input = getByRole("textbox"); + let input = getByRole('textbox'); expect(document.activeElement).toBe(input); - await user.keyboard("{Escape}"); + await user.keyboard('{Escape}'); act(() => jest.runAllTimers()); expect(input).not.toBeInTheDocument(); expect(document.activeElement).toBe(button); }); }); - describe("auto focus", function () { - it("should auto focus the first tabbable element in the scope on mount", function () { - let { getByTestId } = render( + describe('auto focus', function () { + it('should auto focus the first tabbable element in the scope on mount', function () { + let {getByTestId} = render(
- , + ); act(() => { jest.runAllTimers(); }); - let input1 = getByTestId("input1"); + let input1 = getByTestId('input1'); expect(document.activeElement).toBe(input1); }); - it("should do nothing if something is already focused in the scope", function () { - let { getByTestId } = render( + it('should do nothing if something is already focused in the scope', function () { + let {getByTestId} = render(
- , + ); - let input2 = getByTestId("input2"); + let input2 = getByTestId('input2'); expect(document.activeElement).toBe(input2); }); }); - describe("focus manager", function () { - it("should move focus forward", async function () { + describe('focus manager', function () { + it('should move focus forward', async function () { function Test() { return ( @@ -1043,10 +960,10 @@ describe("FocusScope", function () { return
; } - let { getByTestId } = render(); - let item1 = getByTestId("item1"); - let item2 = getByTestId("item2"); - let item3 = getByTestId("item3"); + let {getByTestId} = render(); + let item1 = getByTestId('item1'); + let item2 = getByTestId('item2'); + let item3 = getByTestId('item3'); act(() => { item1.focus(); @@ -1062,7 +979,7 @@ describe("FocusScope", function () { expect(document.activeElement).toBe(item3); }); - it("should move focus forward and wrap around", async function () { + it('should move focus forward and wrap around', async function () { function Test() { return ( @@ -1076,16 +993,16 @@ describe("FocusScope", function () { function Item(props) { let focusManager = useFocusManager(); let onClick = () => { - focusManager.focusNext({ wrap: true }); + focusManager.focusNext({wrap: true}); }; // eslint-disable-next-line jsx-a11y/click-events-have-key-events return
; } - let { getByTestId } = render(); - let item1 = getByTestId("item1"); - let item2 = getByTestId("item2"); - let item3 = getByTestId("item3"); + let {getByTestId} = render(); + let item1 = getByTestId('item1'); + let item2 = getByTestId('item2'); + let item3 = getByTestId('item3'); act(() => { item1.focus(); @@ -1101,15 +1018,15 @@ describe("FocusScope", function () { expect(document.activeElement).toBe(item1); }); - it("should move focus forward but only to tabbable elements", async function () { + it('should move focus forward but only to tabbable elements', async function () { function Test() { return ( - - - + + + ); @@ -1118,15 +1035,15 @@ describe("FocusScope", function () { function Item(props) { let focusManager = useFocusManager(); let onClick = () => { - focusManager.focusNext({ tabbable: true }); + focusManager.focusNext({tabbable: true}); }; // eslint-disable-next-line jsx-a11y/click-events-have-key-events return
; } - let { getByTestId } = render(); - let item1 = getByTestId("item1"); - let item3 = getByTestId("item3"); + let {getByTestId} = render(); + let item1 = getByTestId('item1'); + let item3 = getByTestId('item3'); act(() => { item1.focus(); @@ -1136,18 +1053,18 @@ describe("FocusScope", function () { expect(document.activeElement).toBe(item3); }); - it("should move focus forward but only to tabbable elements while accounting for container elements within the scope", function () { + it('should move focus forward but only to tabbable elements while accounting for container elements within the scope', function () { function Test() { return ( - + - - + + @@ -1160,18 +1077,18 @@ describe("FocusScope", function () { function Group(props) { let focusManager = useFocusManager(); - let onMouseDown = (e) => { - focusManager.focusNext({ from: e.target, tabbable: true }); + let onMouseDown = e => { + focusManager.focusNext({from: e.target, tabbable: true}); }; // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions return
; } - let { getByTestId } = render(); - let group1 = getByTestId("group1"); - let group2 = getByTestId("group2"); - let item2 = getByTestId("item2"); - let item3 = getByTestId("item3"); + let {getByTestId} = render(); + let group1 = getByTestId('group1'); + let group2 = getByTestId('group2'); + let item2 = getByTestId('item2'); + let item3 = getByTestId('item3'); fireEvent.mouseDown(group2); expect(document.activeElement).toBe(item3); @@ -1180,7 +1097,7 @@ describe("FocusScope", function () { expect(document.activeElement).toBe(item2); }); - it("should move focus forward and allow users to skip certain elements", async function () { + it('should move focus forward and allow users to skip certain elements', async function () { function Test() { return ( @@ -1196,16 +1113,16 @@ describe("FocusScope", function () { let onClick = () => { focusManager.focusNext({ wrap: true, - accept: (e) => !e.getAttribute("data-skip"), + accept: e => !e.getAttribute('data-skip') }); }; // eslint-disable-next-line jsx-a11y/click-events-have-key-events return
; } - let { getByTestId } = render(); - let item1 = getByTestId("item1"); - let item3 = getByTestId("item3"); + let {getByTestId} = render(); + let item1 = getByTestId('item1'); + let item3 = getByTestId('item3'); act(() => { item1.focus(); @@ -1218,7 +1135,7 @@ describe("FocusScope", function () { expect(document.activeElement).toBe(item1); }); - it("should move focus backward", async function () { + it('should move focus backward', async function () { function Test() { return ( @@ -1238,10 +1155,10 @@ describe("FocusScope", function () { return
; } - let { getByTestId } = render(); - let item1 = getByTestId("item1"); - let item2 = getByTestId("item2"); - let item3 = getByTestId("item3"); + let {getByTestId} = render(); + let item1 = getByTestId('item1'); + let item2 = getByTestId('item2'); + let item3 = getByTestId('item3'); act(() => { item3.focus(); @@ -1257,7 +1174,7 @@ describe("FocusScope", function () { expect(document.activeElement).toBe(item1); }); - it("should move focus backward and wrap around", async function () { + it('should move focus backward and wrap around', async function () { function Test() { return ( @@ -1271,16 +1188,16 @@ describe("FocusScope", function () { function Item(props) { let focusManager = useFocusManager(); let onClick = () => { - focusManager.focusPrevious({ wrap: true }); + focusManager.focusPrevious({wrap: true}); }; // eslint-disable-next-line jsx-a11y/click-events-have-key-events return
; } - let { getByTestId } = render(); - let item1 = getByTestId("item1"); - let item2 = getByTestId("item2"); - let item3 = getByTestId("item3"); + let {getByTestId} = render(); + let item1 = getByTestId('item1'); + let item2 = getByTestId('item2'); + let item3 = getByTestId('item3'); act(() => { item3.focus(); @@ -1296,15 +1213,15 @@ describe("FocusScope", function () { expect(document.activeElement).toBe(item3); }); - it("should move focus backward but only to tabbable elements", async function () { + it('should move focus backward but only to tabbable elements', async function () { function Test() { return ( - - - + + + ); @@ -1313,15 +1230,15 @@ describe("FocusScope", function () { function Item(props) { let focusManager = useFocusManager(); let onClick = () => { - focusManager.focusPrevious({ tabbable: true }); + focusManager.focusPrevious({tabbable: true}); }; // eslint-disable-next-line jsx-a11y/click-events-have-key-events return
; } - let { getByTestId } = render(); - let item1 = getByTestId("item1"); - let item3 = getByTestId("item3"); + let {getByTestId} = render(); + let item1 = getByTestId('item1'); + let item3 = getByTestId('item3'); act(() => { item3.focus(); @@ -1331,18 +1248,18 @@ describe("FocusScope", function () { expect(document.activeElement).toBe(item1); }); - it("should move focus backward but only to tabbable elements while accounting for container elements within the scope", function () { + it('should move focus backward but only to tabbable elements while accounting for container elements within the scope', function () { function Test() { return ( - + - - + + @@ -1355,17 +1272,17 @@ describe("FocusScope", function () { function Group(props) { let focusManager = useFocusManager(); - let onMouseDown = (e) => { - focusManager.focusPrevious({ from: e.target, tabbable: true }); + let onMouseDown = e => { + focusManager.focusPrevious({from: e.target, tabbable: true}); }; // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions return
; } - let { getByTestId } = render(); - let group1 = getByTestId("group1"); - let group2 = getByTestId("group2"); - let item1 = getByTestId("item1"); + let {getByTestId} = render(); + let group1 = getByTestId('group1'); + let group2 = getByTestId('group2'); + let item1 = getByTestId('item1'); fireEvent.mouseDown(group2); expect(document.activeElement).toBe(item1); @@ -1377,7 +1294,7 @@ describe("FocusScope", function () { expect(document.activeElement).toBe(item1); }); - it("should move focus backward and allow users to skip certain elements", async function () { + it('should move focus backward and allow users to skip certain elements', async function () { function Test() { return ( @@ -1393,16 +1310,16 @@ describe("FocusScope", function () { let onClick = () => { focusManager.focusPrevious({ wrap: true, - accept: (e) => !e.getAttribute("data-skip"), + accept: e => !e.getAttribute('data-skip') }); }; // eslint-disable-next-line jsx-a11y/click-events-have-key-events return
; } - let { getByTestId } = render(); - let item1 = getByTestId("item1"); - let item3 = getByTestId("item3"); + let {getByTestId} = render(); + let item1 = getByTestId('item1'); + let item3 = getByTestId('item3'); act(() => { item1.focus(); @@ -1416,7 +1333,7 @@ describe("FocusScope", function () { }); }); - it("skips radio buttons that are in the same group and are not the selectable one forwards", async function () { + it('skips radio buttons that are in the same group and are not the selectable one forwards', async function () { function Test() { return ( @@ -1426,13 +1343,7 @@ describe("FocusScope", function () { Select a maintenance drone:
- +
@@ -1470,25 +1381,25 @@ describe("FocusScope", function () { ); } - let { getByTestId, getAllByRole } = render(); - let radios = getAllByRole("radio"); + let {getByTestId, getAllByRole} = render(); + let radios = getAllByRole('radio'); await user.tab(); - expect(document.activeElement).toBe(getByTestId("button1")); + expect(document.activeElement).toBe(getByTestId('button1')); await user.tab(); expect(document.activeElement).toBe(radios[0]); await user.tab(); - expect(document.activeElement).toBe(getByTestId("button2")); + expect(document.activeElement).toBe(getByTestId('button2')); await user.tab(); expect(document.activeElement).toBe(radios[3]); await user.tab(); - expect(document.activeElement).toBe(getByTestId("button3")); + expect(document.activeElement).toBe(getByTestId('button3')); await user.tab(); expect(document.activeElement).toBe(radios[5]); await user.tab(); - expect(document.activeElement).toBe(getByTestId("button4")); + expect(document.activeElement).toBe(getByTestId('button4')); }); - it("skips radio buttons that are in the same group and are not the selectable one forwards outside of a form", async function () { + it('skips radio buttons that are in the same group and are not the selectable one forwards outside of a form', async function () { function Test() { return ( @@ -1497,13 +1408,7 @@ describe("FocusScope", function () { Select a maintenance drone:
- +
@@ -1540,25 +1445,25 @@ describe("FocusScope", function () { ); } - let { getByTestId, getAllByRole } = render(); - let radios = getAllByRole("radio"); + let {getByTestId, getAllByRole} = render(); + let radios = getAllByRole('radio'); await user.tab(); - expect(document.activeElement).toBe(getByTestId("button1")); + expect(document.activeElement).toBe(getByTestId('button1')); await user.tab(); expect(document.activeElement).toBe(radios[0]); await user.tab(); - expect(document.activeElement).toBe(getByTestId("button2")); + expect(document.activeElement).toBe(getByTestId('button2')); await user.tab(); expect(document.activeElement).toBe(radios[3]); await user.tab(); - expect(document.activeElement).toBe(getByTestId("button3")); + expect(document.activeElement).toBe(getByTestId('button3')); await user.tab(); expect(document.activeElement).toBe(radios[5]); await user.tab(); - expect(document.activeElement).toBe(getByTestId("button4")); + expect(document.activeElement).toBe(getByTestId('button4')); }); - it("skips radio buttons that are in the same group and are not the selectable one backwards", async function () { + it('skips radio buttons that are in the same group and are not the selectable one backwards', async function () { function Test() { return ( @@ -1568,13 +1473,7 @@ describe("FocusScope", function () { Select a maintenance drone:
- +
@@ -1612,24 +1511,24 @@ describe("FocusScope", function () { ); } - let { getByTestId, getAllByRole } = render(); - let radios = getAllByRole("radio"); - await user.click(getByTestId("button4")); - await user.tab({ shift: true }); + let {getByTestId, getAllByRole} = render(); + let radios = getAllByRole('radio'); + await user.click(getByTestId('button4')); + await user.tab({shift: true}); expect(document.activeElement).toBe(radios[5]); - await user.tab({ shift: true }); - expect(document.activeElement).toBe(getByTestId("button3")); - await user.tab({ shift: true }); + await user.tab({shift: true}); + expect(document.activeElement).toBe(getByTestId('button3')); + await user.tab({shift: true}); expect(document.activeElement).toBe(radios[4]); - await user.tab({ shift: true }); - expect(document.activeElement).toBe(getByTestId("button2")); - await user.tab({ shift: true }); + await user.tab({shift: true}); + expect(document.activeElement).toBe(getByTestId('button2')); + await user.tab({shift: true}); expect(document.activeElement).toBe(radios[0]); - await user.tab({ shift: true }); - expect(document.activeElement).toBe(getByTestId("button1")); + await user.tab({shift: true}); + expect(document.activeElement).toBe(getByTestId('button1')); }); - it("skips radio buttons that are in the same group and are not the selectable one backwards outside of a form", async function () { + it('skips radio buttons that are in the same group and are not the selectable one backwards outside of a form', async function () { function Test() { return ( @@ -1638,13 +1537,7 @@ describe("FocusScope", function () { Select a maintenance drone:
- +
@@ -1681,30 +1574,30 @@ describe("FocusScope", function () { ); } - let { getByTestId, getAllByRole } = render(); - let radios = getAllByRole("radio"); - await user.click(getByTestId("button4")); - await user.tab({ shift: true }); + let {getByTestId, getAllByRole} = render(); + let radios = getAllByRole('radio'); + await user.click(getByTestId('button4')); + await user.tab({shift: true}); expect(document.activeElement).toBe(radios[5]); - await user.tab({ shift: true }); - expect(document.activeElement).toBe(getByTestId("button3")); - await user.tab({ shift: true }); + await user.tab({shift: true}); + expect(document.activeElement).toBe(getByTestId('button3')); + await user.tab({shift: true}); expect(document.activeElement).toBe(radios[4]); - await user.tab({ shift: true }); - expect(document.activeElement).toBe(getByTestId("button2")); - await user.tab({ shift: true }); + await user.tab({shift: true}); + expect(document.activeElement).toBe(getByTestId('button2')); + await user.tab({shift: true}); expect(document.activeElement).toBe(radios[0]); - await user.tab({ shift: true }); - expect(document.activeElement).toBe(getByTestId("button1")); + await user.tab({shift: true}); + expect(document.activeElement).toBe(getByTestId('button1')); }); - describe("nested focus scopes", function () { - it("should make child FocusScopes the active scope regardless of DOM structure", function () { + describe('nested focus scopes', function () { + it('should make child FocusScopes the active scope regardless of DOM structure', function () { function ChildComponent(props) { return ReactDOM.createPortal(props.children, document.body); } - function Test({ show }) { + function Test({show}) { return (
@@ -1722,9 +1615,9 @@ describe("FocusScope", function () { ); } - let { getByTestId, rerender } = render(); + let {getByTestId, rerender} = render(); // Set a focused node and make first FocusScope the active scope - let input1 = getByTestId("input1"); + let input1 = getByTestId('input1'); act(() => { input1.focus(); }); @@ -1733,7 +1626,7 @@ describe("FocusScope", function () { rerender(); expect(document.activeElement).toBe(input1); - let input3 = getByTestId("input3"); + let input3 = getByTestId('input3'); act(() => { input3.focus(); }); @@ -1741,7 +1634,7 @@ describe("FocusScope", function () { expect(document.activeElement).toBe(input3); }); - it("should lock tab navigation inside direct child focus scope", async function () { + it('should lock tab navigation inside direct child focus scope', async function () { function Test() { return (
@@ -1760,10 +1653,10 @@ describe("FocusScope", function () { ); } - let { getByTestId } = render(); - let child1 = getByTestId("child1"); - let child2 = getByTestId("child2"); - let child3 = getByTestId("child3"); + let {getByTestId} = render(); + let child1 = getByTestId('child1'); + let child2 = getByTestId('child2'); + let child3 = getByTestId('child3'); act(() => { jest.runAllTimers(); @@ -1784,14 +1677,14 @@ describe("FocusScope", function () { jest.runAllTimers(); }); expect(document.activeElement).toBe(child1); - await user.tab({ shift: true }); + await user.tab({shift: true}); act(() => { jest.runAllTimers(); }); expect(document.activeElement).toBe(child3); }); - it("should lock tab navigation inside nested child focus scope", async function () { + it('should lock tab navigation inside nested child focus scope', async function () { function Test() { return (
@@ -1814,10 +1707,10 @@ describe("FocusScope", function () { ); } - let { getByTestId } = render(); - let child1 = getByTestId("child1"); - let child2 = getByTestId("child2"); - let child3 = getByTestId("child3"); + let {getByTestId} = render(); + let child1 = getByTestId('child1'); + let child2 = getByTestId('child2'); + let child3 = getByTestId('child3'); expect(document.activeElement).toBe(child1); await user.tab(); @@ -1826,11 +1719,11 @@ describe("FocusScope", function () { expect(document.activeElement).toBe(child3); await user.tab(); expect(document.activeElement).toBe(child1); - await user.tab({ shift: true }); + await user.tab({shift: true}); expect(document.activeElement).toBe(child3); }); - it("should not lock tab navigation inside a nested focus scope without contain", async function () { + it('should not lock tab navigation inside a nested focus scope without contain', async function () { function Test() { return (
@@ -1851,11 +1744,11 @@ describe("FocusScope", function () { ); } - let { getByTestId } = render(); - let parent = getByTestId("parent"); - let child1 = getByTestId("child1"); - let child2 = getByTestId("child2"); - let child3 = getByTestId("child3"); + let {getByTestId} = render(); + let parent = getByTestId('parent'); + let child1 = getByTestId('child1'); + let child2 = getByTestId('child2'); + let child3 = getByTestId('child3'); expect(document.activeElement).toBe(parent); await user.tab(); @@ -1866,11 +1759,11 @@ describe("FocusScope", function () { expect(document.activeElement).toBe(child3); await user.tab(); expect(document.activeElement).toBe(parent); - await user.tab({ shift: true }); + await user.tab({shift: true}); expect(document.activeElement).toBe(child3); }); - it("should not lock tab navigation inside a nested focus scope with restore and not contain", async function () { + it('should not lock tab navigation inside a nested focus scope with restore and not contain', async function () { function Test() { return (
@@ -1891,11 +1784,11 @@ describe("FocusScope", function () { ); } - let { getByTestId } = render(); - let parent = getByTestId("parent"); - let child1 = getByTestId("child1"); - let child2 = getByTestId("child2"); - let child3 = getByTestId("child3"); + let {getByTestId} = render(); + let parent = getByTestId('parent'); + let child1 = getByTestId('child1'); + let child2 = getByTestId('child2'); + let child3 = getByTestId('child3'); expect(document.activeElement).toBe(parent); await user.tab(); @@ -1906,12 +1799,12 @@ describe("FocusScope", function () { expect(document.activeElement).toBe(child3); await user.tab(); expect(document.activeElement).toBe(parent); - await user.tab({ shift: true }); + await user.tab({shift: true}); expect(document.activeElement).toBe(child3); }); - it("should restore to the correct scope on unmount", async function () { - function Test({ show1, show2, show3 }) { + it('should restore to the correct scope on unmount', async function () { + function Test({show1, show2, show3}) { return (
@@ -1937,8 +1830,8 @@ describe("FocusScope", function () { ); } - let { rerender, getByTestId } = render(); - let parent = getByTestId("parent"); + let {rerender, getByTestId} = render(); + let parent = getByTestId('parent'); expect(document.activeElement).toBe(parent); @@ -1946,7 +1839,7 @@ describe("FocusScope", function () { expect(document.activeElement).toBe(parent); // Can move into a child, but not out. - let child1 = getByTestId("child1"); + let child1 = getByTestId('child1'); await user.tab(); expect(document.activeElement).toBe(child1); @@ -1956,7 +1849,7 @@ describe("FocusScope", function () { rerender(); expect(document.activeElement).toBe(child1); - let child2 = getByTestId("child2"); + let child2 = getByTestId('child2'); await user.tab(); expect(document.activeElement).toBe(child2); @@ -1968,7 +1861,7 @@ describe("FocusScope", function () { rerender(); - let child3 = getByTestId("child3"); + let child3 = getByTestId('child3'); await user.tab(); expect(document.activeElement).toBe(child3); @@ -1981,7 +1874,7 @@ describe("FocusScope", function () { expect(document.activeElement).toBe(child1); }); - it("should not lock focus inside a focus scope with a child scope in a portal", function () { + it('should not lock focus inside a focus scope with a child scope in a portal', function () { function Portal(props) { return ReactDOM.createPortal(props.children, document.body); } @@ -2003,9 +1896,9 @@ describe("FocusScope", function () { ); } - let { getByTestId } = render(); - let parent = getByTestId("parent"); - let child = getByTestId("child"); + let {getByTestId} = render(); + let parent = getByTestId('parent'); + let child = getByTestId('child'); expect(document.activeElement).toBe(parent); act(() => child.focus()); @@ -2014,7 +1907,7 @@ describe("FocusScope", function () { expect(document.activeElement).toBe(parent); }); - it("should lock focus inside a child focus scope with contain in a portal", function () { + it('should lock focus inside a child focus scope with contain in a portal', function () { function Portal(props) { return ReactDOM.createPortal(props.children, document.body); } @@ -2036,9 +1929,9 @@ describe("FocusScope", function () { ); } - let { getByTestId } = render(); - let parent = getByTestId("parent"); - let child = getByTestId("child"); + let {getByTestId} = render(); + let parent = getByTestId('parent'); + let child = getByTestId('child'); expect(document.activeElement).toBe(parent); act(() => child.focus()); @@ -2048,8 +1941,8 @@ describe("FocusScope", function () { }); }); - describe("scope child of document.body", function () { - it("should navigate in and out of scope in DOM order when the nodeToRestore is the document.body", async function () { + describe('scope child of document.body', function () { + it('should navigate in and out of scope in DOM order when the nodeToRestore is the document.body', async function () { function Test() { return (
@@ -2062,10 +1955,10 @@ describe("FocusScope", function () { ); } - let { getByTestId } = render(); - let beforeScope = getByTestId("beforeScope"); - let inScope = getByTestId("inScope"); - let afterScope = getByTestId("afterScope"); + let {getByTestId} = render(); + let beforeScope = getByTestId('beforeScope'); + let inScope = getByTestId('inScope'); + let afterScope = getByTestId('afterScope'); act(() => { inScope.focus(); @@ -2075,12 +1968,12 @@ describe("FocusScope", function () { act(() => { inScope.focus(); }); - await user.tab({ shift: true }); + await user.tab({shift: true}); expect(document.activeElement).toBe(beforeScope); }); }); - describe("node to restore edge cases", () => { - it("tracks node to restore if the node to restore was removed in another part of the tree", async () => { + describe('node to restore edge cases', () => { + it('tracks node to restore if the node to restore was removed in another part of the tree', async () => { function Test() { let [showMenu, setShowMenu] = useState(false); let [showDialog, setShowDialog] = useState(false); @@ -2094,7 +1987,7 @@ describe("FocusScope", function () { - { }}> + {}}> {showMenu && ( @@ -2102,7 +1995,7 @@ describe("FocusScope", function () { )} - { }}> + {}}> {showDialog && ( @@ -2120,16 +2013,16 @@ describe("FocusScope", function () { }); await user.tab(); await user.tab(); - expect(document.activeElement.textContent).toBe("Open Menu"); + expect(document.activeElement.textContent).toBe('Open Menu'); - await user.keyboard("[Enter]"); + await user.keyboard('[Enter]'); act(() => { jest.runAllTimers(); }); - expect(document.activeElement.textContent).toBe("Open Dialog"); + expect(document.activeElement.textContent).toBe('Open Dialog'); - await user.keyboard("[Enter]"); + await user.keyboard('[Enter]'); // Needed for onBlur raf in useFocusContainment act(() => { @@ -2140,9 +2033,9 @@ describe("FocusScope", function () { jest.runAllTimers(); }); - expect(document.activeElement.textContent).toBe("Close"); + expect(document.activeElement.textContent).toBe('Close'); - await user.keyboard("[Enter]"); + await user.keyboard('[Enter]'); act(() => { jest.runAllTimers(); }); @@ -2151,17 +2044,17 @@ describe("FocusScope", function () { }); expect(document.activeElement).not.toBe(document.body); - expect(document.activeElement.textContent).toBe("Open Menu"); + expect(document.activeElement.textContent).toBe('Open Menu'); }); }); }); -describe("FocusScope with Shadow DOM", function () { +describe('FocusScope with Shadow DOM', function () { let user; beforeAll(() => { enableShadowDOM(); - user = userEvent.setup({ delay: null, pointerMap }); + user = userEvent.setup({delay: null, pointerMap}); }); beforeEach(() => { @@ -2174,8 +2067,8 @@ describe("FocusScope with Shadow DOM", function () { }); }); - it("should contain focus within the shadow DOM scope", async function () { - const { shadowRoot } = createShadowRoot(); + it('should contain focus within the shadow DOM scope', async function () { + const {shadowRoot} = createShadowRoot(); const FocusableComponent = () => ReactDOM.createPortal( @@ -2183,10 +2076,10 @@ describe("FocusScope with Shadow DOM", function () { , - shadowRoot, + shadowRoot ); - const { unmount } = render(); + const {unmount} = render(); const input1 = shadowRoot.querySelector('[data-testid="input1"]'); const input2 = shadowRoot.querySelector('[data-testid="input2"]'); @@ -2215,11 +2108,11 @@ describe("FocusScope with Shadow DOM", function () { document.body.removeChild(shadowRoot.host); }); - it("should manage focus within nested shadow DOMs", async function () { - const { shadowRoot: parentShadowRoot } = createShadowRoot(); - const nestedDiv = document.createElement("div"); + it('should manage focus within nested shadow DOMs', async function () { + const {shadowRoot: parentShadowRoot} = createShadowRoot(); + const nestedDiv = document.createElement('div'); parentShadowRoot.appendChild(nestedDiv); - const childShadowRoot = nestedDiv.attachShadow({ mode: "open" }); + const childShadowRoot = nestedDiv.attachShadow({mode: 'open'}); const FocusableComponent = () => ReactDOM.createPortal( @@ -2227,13 +2120,13 @@ describe("FocusScope with Shadow DOM", function () { , - childShadowRoot, + childShadowRoot ); - const { unmount } = render(); + const {unmount} = render(); - const input1 = childShadowRoot.querySelector("[data-testid=input1]"); - const input2 = childShadowRoot.querySelector("[data-testid=input2]"); + const input1 = childShadowRoot.querySelector('[data-testid=input1]'); + const input2 = childShadowRoot.querySelector('[data-testid=input2]'); act(() => { input1.focus(); @@ -2256,7 +2149,7 @@ describe("FocusScope with Shadow DOM", function () { * │ └── Your custom elements and focusable elements here * └── Other elements */ - it("should restore focus to the element outside shadow DOM on unmount, with FocusScope outside as well", async () => { + it('should restore focus to the element outside shadow DOM on unmount, with FocusScope outside as well', async () => { const App = () => ( <> @@ -2266,9 +2159,9 @@ describe("FocusScope with Shadow DOM", function () { ); - const { getByTestId } = render(); - const shadowHost = document.getElementById("shadow-host"); - const shadowRoot = shadowHost.attachShadow({ mode: "open" }); + const {getByTestId} = render(); + const shadowHost = document.getElementById('shadow-host'); + const shadowRoot = shadowHost.attachShadow({mode: 'open'}); const FocusableComponent = () => ReactDOM.createPortal( @@ -2277,10 +2170,10 @@ describe("FocusScope with Shadow DOM", function () { , - shadowRoot, + shadowRoot ); - const { unmount } = render(); + const {unmount} = render(); const input1 = shadowRoot.querySelector('[data-testid="input1"]'); act(() => { @@ -2288,7 +2181,7 @@ describe("FocusScope with Shadow DOM", function () { }); expect(shadowRoot.activeElement).toBe(input1); - const externalInput = getByTestId("outside"); + const externalInput = getByTestId('outside'); act(() => { externalInput.focus(); }); @@ -2306,8 +2199,8 @@ describe("FocusScope with Shadow DOM", function () { /** * Test case: https://github.com/adobe/react-spectrum/issues/1472 */ - it("should autofocus and lock tab navigation inside shadow DOM", async function () { - const { shadowRoot, shadowHost } = createShadowRoot(); + it('should autofocus and lock tab navigation inside shadow DOM', async function () { + const {shadowRoot, shadowHost} = createShadowRoot(); const FocusableComponent = () => ReactDOM.createPortal( @@ -2316,10 +2209,10 @@ describe("FocusScope with Shadow DOM", function () { , - shadowRoot, + shadowRoot ); - const { unmount } = render(); + const {unmount} = render(); const input1 = shadowRoot.querySelector('[data-testid="input1"]'); const input2 = shadowRoot.querySelector('[data-testid="input2"]'); @@ -2348,24 +2241,24 @@ describe("FocusScope with Shadow DOM", function () { document.body.removeChild(shadowHost); }); - it("should reproduce the specific issue #8675: Menu items in popover close immediately with UNSAFE_PortalProvider", async function () { - const { shadowRoot, cleanup } = createShadowRoot(); + it('should reproduce the specific issue #8675: Menu items in popover close immediately with UNSAFE_PortalProvider', async function () { + const {shadowRoot, cleanup} = createShadowRoot(); let actionExecuted = false; let menuClosed = false; // Create portal container within the shadow DOM for the popover - const popoverPortal = document.createElement("div"); - popoverPortal.setAttribute("data-testid", "popover-portal"); + const popoverPortal = document.createElement('div'); + popoverPortal.setAttribute('data-testid', 'popover-portal'); shadowRoot.appendChild(popoverPortal); // This reproduces the exact scenario described in the issue function WebComponentWithReactApp() { const [isPopoverOpen, setIsPopoverOpen] = React.useState(true); - const handleMenuAction = (key) => { + const handleMenuAction = key => { actionExecuted = true; // In the original issue, this never executes because the popover closes first - console.log("Menu action executed:", key); + console.log('Menu action executed:', key); }; return ( @@ -2377,7 +2270,7 @@ describe("FocusScope with Shadow DOM", function () { setIsPopoverOpen(false); menuClosed = true; }} - style={{ position: "absolute", top: 0, right: 0 }} + style={{position: 'absolute', top: 0, right: 0}} > Close @@ -2388,17 +2281,13 @@ describe("FocusScope with Shadow DOM", function () {
- @@ -2406,14 +2295,14 @@ describe("FocusScope with Shadow DOM", function () {
, - popoverPortal, + popoverPortal )}
); } - const { unmount } = render(); + const {unmount} = render(); // Wait for rendering act(() => { @@ -2421,21 +2310,11 @@ describe("FocusScope with Shadow DOM", function () { }); // Query elements from shadow DOM - const saveMenuItem = shadowRoot.querySelector( - '[data-testid="menu-item-save"]', - ); - const exportMenuItem = shadowRoot.querySelector( - '[data-testid="menu-item-export"]', - ); - const menuContainer = shadowRoot.querySelector( - '[data-testid="menu-container"]', - ); - const popoverOverlay = shadowRoot.querySelector( - '[data-testid="popover-overlay"]', - ); - const closeButton = shadowRoot.querySelector( - '[data-testid="close-popover"]', - ); + const saveMenuItem = shadowRoot.querySelector('[data-testid="menu-item-save"]'); + const exportMenuItem = shadowRoot.querySelector('[data-testid="menu-item-export"]'); + const menuContainer = shadowRoot.querySelector('[data-testid="menu-container"]'); + const popoverOverlay = shadowRoot.querySelector('[data-testid="popover-overlay"]'); + const closeButton = shadowRoot.querySelector('[data-testid="close-popover"]'); // Verify the menu is initially visible in shadow DOM expect(popoverOverlay).not.toBeNull(); @@ -2457,9 +2336,7 @@ describe("FocusScope with Shadow DOM", function () { // The menu should still be open (this would fail in the buggy version where it closes immediately) expect(menuClosed).toBe(false); - expect( - shadowRoot.querySelector('[data-testid="menu-container"]'), - ).not.toBeNull(); + expect(shadowRoot.querySelector('[data-testid="menu-container"]')).not.toBeNull(); // Test focus containment within the menu act(() => { @@ -2477,16 +2354,16 @@ describe("FocusScope with Shadow DOM", function () { cleanup(); }); - it("should handle web component scenario with multiple nested portals and UNSAFE_PortalProvider", async function () { - const { shadowRoot, cleanup } = createShadowRoot(); + it('should handle web component scenario with multiple nested portals and UNSAFE_PortalProvider', async function () { + const {shadowRoot, cleanup} = createShadowRoot(); // Create nested portal containers within the shadow DOM - const modalPortal = document.createElement("div"); - modalPortal.setAttribute("data-testid", "modal-portal"); + const modalPortal = document.createElement('div'); + modalPortal.setAttribute('data-testid', 'modal-portal'); shadowRoot.appendChild(modalPortal); - const tooltipPortal = document.createElement("div"); - tooltipPortal.setAttribute("data-testid", "tooltip-portal"); + const tooltipPortal = document.createElement('div'); + tooltipPortal.setAttribute('data-testid', 'tooltip-portal'); shadowRoot.appendChild(tooltipPortal); function ComplexWebComponent() { @@ -2505,15 +2382,12 @@ describe("FocusScope with Shadow DOM", function () {
-
, - modalPortal, + modalPortal )} {/* Tooltip with nested focus scope */} @@ -2524,24 +2398,18 @@ describe("FocusScope with Shadow DOM", function () {
, - tooltipPortal, + tooltipPortal )}
); } - const { unmount } = render(); + const {unmount} = render(); - const modalButton1 = shadowRoot.querySelector( - '[data-testid="modal-button-1"]', - ); - const modalButton2 = shadowRoot.querySelector( - '[data-testid="modal-button-2"]', - ); - const tooltipAction = shadowRoot.querySelector( - '[data-testid="tooltip-action"]', - ); + const modalButton1 = shadowRoot.querySelector('[data-testid="modal-button-1"]'); + const modalButton2 = shadowRoot.querySelector('[data-testid="modal-button-2"]'); + const tooltipAction = shadowRoot.querySelector('[data-testid="tooltip-action"]'); // Due to autoFocus, the first modal button should be focused act(() => { @@ -2556,9 +2424,7 @@ describe("FocusScope with Shadow DOM", function () { // Focus should be contained within the modal due to the contain prop await user.tab(); // Should cycle to the close button - expect(shadowRoot.activeElement.getAttribute("data-testid")).toBe( - "close-modal", - ); + expect(shadowRoot.activeElement.getAttribute('data-testid')).toBe('close-modal'); await user.tab(); // Should wrap back to first modal button @@ -2580,7 +2446,7 @@ describe("FocusScope with Shadow DOM", function () { }); }); -describe("Unmounting cleanup", () => { +describe('Unmounting cleanup', () => { beforeAll(() => { jest.useFakeTimers(); }); @@ -2589,14 +2455,14 @@ describe("Unmounting cleanup", () => { }); // this test will fail in the 'afterAll' if there are any rafs left over - it("should not leak request animation frames", () => { + it('should not leak request animation frames', () => { let tree = render( - , + ); - let buttons = tree.getAllByRole("button"); + let buttons = tree.getAllByRole('button'); act(() => buttons[0].focus()); act(() => buttons[1].focus()); act(() => buttons[1].blur()); diff --git a/packages/@react-aria/interactions/test/useInteractOutside.test.js b/packages/@react-aria/interactions/test/useInteractOutside.test.js index 5bfb5ef6055..b602d8728fb 100644 --- a/packages/@react-aria/interactions/test/useInteractOutside.test.js +++ b/packages/@react-aria/interactions/test/useInteractOutside.test.js @@ -17,18 +17,18 @@ import { installPointerEvent, pointerMap, render, - waitFor, -} from "@react-spectrum/test-utils-internal"; -import { enableShadowDOM } from "@react-stately/flags"; -import React, { useEffect, useRef } from "react"; -import ReactDOM, { createPortal } from "react-dom"; -import { UNSAFE_PortalProvider } from "@react-aria/overlays"; -import { useInteractOutside } from "../"; -import userEvent from "@testing-library/user-event"; + waitFor +} from '@react-spectrum/test-utils-internal'; +import {enableShadowDOM} from '@react-stately/flags'; +import React, {useEffect, useRef} from 'react'; +import ReactDOM, {createPortal} from 'react-dom'; +import {UNSAFE_PortalProvider} from '@react-aria/overlays'; +import {useInteractOutside} from '../'; +import userEvent from '@testing-library/user-event'; function Example(props) { let ref = useRef(); - useInteractOutside({ ref, ...props }); + useInteractOutside({ref, ...props}); return (
test @@ -37,67 +37,67 @@ function Example(props) { } function pointerEvent(type, opts) { - let evt = new Event(type, { bubbles: true, cancelable: true }); + let evt = new Event(type, {bubbles: true, cancelable: true}); Object.assign(evt, opts); return evt; } -describe("useInteractOutside", function () { +describe('useInteractOutside', function () { // TODO: JSDOM doesn't yet support pointer events. Once they do, convert these tests. // https://github.com/jsdom/jsdom/issues/2527 - describe("pointer events", function () { + describe('pointer events', function () { installPointerEvent(); - it("should fire interact outside events based on pointer events", function () { + it('should fire interact outside events based on pointer events', function () { let onInteractOutside = jest.fn(); let res = render(); - let el = res.getByText("test"); - fireEvent(el, pointerEvent("pointerdown")); - fireEvent(el, pointerEvent("pointerup")); + let el = res.getByText('test'); + fireEvent(el, pointerEvent('pointerdown')); + fireEvent(el, pointerEvent('pointerup')); fireEvent.click(el); expect(onInteractOutside).not.toHaveBeenCalled(); - fireEvent(document.body, pointerEvent("pointerdown")); - fireEvent(document.body, pointerEvent("pointerup")); + fireEvent(document.body, pointerEvent('pointerdown')); + fireEvent(document.body, pointerEvent('pointerup')); fireEvent.click(document.body); expect(onInteractOutside).toHaveBeenCalledTimes(1); }); - it("should only listen for the left mouse button", function () { + it('should only listen for the left mouse button', function () { let onInteractOutside = jest.fn(); render(); - fireEvent(document.body, pointerEvent("pointerdown", { button: 1 })); - fireEvent(document.body, pointerEvent("pointerup", { button: 1 })); - fireEvent.click(document.body, { button: 1 }); + fireEvent(document.body, pointerEvent('pointerdown', {button: 1})); + fireEvent(document.body, pointerEvent('pointerup', {button: 1})); + fireEvent.click(document.body, {button: 1}); expect(onInteractOutside).not.toHaveBeenCalled(); - fireEvent(document.body, pointerEvent("pointerdown", { button: 0 })); - fireEvent(document.body, pointerEvent("pointerup", { button: 0 })); - fireEvent.click(document.body, { button: 0 }); + fireEvent(document.body, pointerEvent('pointerdown', {button: 0})); + fireEvent(document.body, pointerEvent('pointerup', {button: 0})); + fireEvent.click(document.body, {button: 0}); expect(onInteractOutside).toHaveBeenCalledTimes(1); }); - it("should not fire interact outside if there is a pointer up event without a pointer down first", function () { + it('should not fire interact outside if there is a pointer up event without a pointer down first', function () { // Fire pointer down before component with useInteractOutside is mounted - fireEvent(document.body, pointerEvent("pointerdown")); + fireEvent(document.body, pointerEvent('pointerdown')); let onInteractOutside = jest.fn(); render(); - fireEvent(document.body, pointerEvent("pointerup")); + fireEvent(document.body, pointerEvent('pointerup')); fireEvent.click(document.body); expect(onInteractOutside).not.toHaveBeenCalled(); }); }); - describe("mouse events", function () { - it("should fire interact outside events based on mouse events", function () { + describe('mouse events', function () { + it('should fire interact outside events based on mouse events', function () { let onInteractOutside = jest.fn(); let res = render(); - let el = res.getByText("test"); + let el = res.getByText('test'); fireEvent.mouseDown(el); fireEvent.mouseUp(el); expect(onInteractOutside).not.toHaveBeenCalled(); @@ -107,20 +107,20 @@ describe("useInteractOutside", function () { expect(onInteractOutside).toHaveBeenCalledTimes(1); }); - it("should only listen for the left mouse button", function () { + it('should only listen for the left mouse button', function () { let onInteractOutside = jest.fn(); render(); - fireEvent.mouseDown(document.body, { button: 1 }); - fireEvent.mouseUp(document.body, { button: 1 }); + fireEvent.mouseDown(document.body, {button: 1}); + fireEvent.mouseUp(document.body, {button: 1}); expect(onInteractOutside).not.toHaveBeenCalled(); - fireEvent.mouseDown(document.body, { button: 0 }); - fireEvent.mouseUp(document.body, { button: 0 }); + fireEvent.mouseDown(document.body, {button: 0}); + fireEvent.mouseUp(document.body, {button: 0}); expect(onInteractOutside).toHaveBeenCalledTimes(1); }); - it("should not fire interact outside if there is a mouse up event without a mouse down first", function () { + it('should not fire interact outside if there is a mouse up event without a mouse down first', function () { // Fire mouse down before component with useInteractOutside is mounted fireEvent.mouseDown(document.body); @@ -132,12 +132,12 @@ describe("useInteractOutside", function () { }); }); - describe("touch events", function () { - it("should fire interact outside events based on mouse events", function () { + describe('touch events', function () { + it('should fire interact outside events based on mouse events', function () { let onInteractOutside = jest.fn(); let res = render(); - let el = res.getByText("test"); + let el = res.getByText('test'); fireEvent.touchStart(el); fireEvent.touchEnd(el); expect(onInteractOutside).not.toHaveBeenCalled(); @@ -147,11 +147,11 @@ describe("useInteractOutside", function () { expect(onInteractOutside).toHaveBeenCalledTimes(1); }); - it("should ignore emulated mouse events", function () { + it('should ignore emulated mouse events', function () { let onInteractOutside = jest.fn(); let res = render(); - let el = res.getByText("test"); + let el = res.getByText('test'); fireEvent.touchStart(el); fireEvent.touchEnd(el); fireEvent.mouseUp(el); @@ -163,7 +163,7 @@ describe("useInteractOutside", function () { expect(onInteractOutside).toHaveBeenCalledTimes(1); }); - it("should not fire interact outside if there is a touch end event without a touch start first", function () { + it('should not fire interact outside if there is a touch end event without a touch start first', function () { // Fire mouse down before component with useInteractOutside is mounted fireEvent.touchStart(document.body); @@ -174,17 +174,17 @@ describe("useInteractOutside", function () { expect(onInteractOutside).not.toHaveBeenCalled(); }); }); - describe("disable interact outside events", function () { - it("does not handle pointer events if disabled", function () { + describe('disable interact outside events', function () { + it('does not handle pointer events if disabled', function () { let onInteractOutside = jest.fn(); render(); - fireEvent(document.body, pointerEvent("mousedown")); - fireEvent(document.body, pointerEvent("mouseup")); + fireEvent(document.body, pointerEvent('mousedown')); + fireEvent(document.body, pointerEvent('mouseup')); expect(onInteractOutside).not.toHaveBeenCalled(); }); - it("does not handle touch events if disabled", function () { + it('does not handle touch events if disabled', function () { let onInteractOutside = jest.fn(); render(); @@ -193,7 +193,7 @@ describe("useInteractOutside", function () { expect(onInteractOutside).not.toHaveBeenCalled(); }); - it("does not handle mouse events if disabled", function () { + it('does not handle mouse events if disabled', function () { let onInteractOutside = jest.fn(); render(); @@ -204,15 +204,15 @@ describe("useInteractOutside", function () { }); }); -describe("useInteractOutside (iframes)", function () { +describe('useInteractOutside (iframes)', function () { let iframe; let iframeRoot; let iframeDocument; beforeEach(() => { - iframe = document.createElement("iframe"); + iframe = document.createElement('iframe'); window.document.body.appendChild(iframe); iframeDocument = iframe.contentWindow.document; - iframeRoot = iframeDocument.createElement("div"); + iframeRoot = iframeDocument.createElement('div'); iframeDocument.body.appendChild(iframeRoot); }); @@ -220,79 +220,73 @@ describe("useInteractOutside (iframes)", function () { iframe.remove(); }); - const IframeExample = (props) => { + const IframeExample = props => { return createPortal(, iframeRoot); }; // TODO: JSDOM doesn't yet support pointer events. Once they do, convert these tests. // https://github.com/jsdom/jsdom/issues/2527 - describe("pointer events", function () { + describe('pointer events', function () { installPointerEvent(); - it("should fire interact outside events based on pointer events", async function () { + it('should fire interact outside events based on pointer events', async function () { let onInteractOutside = jest.fn(); render(); await waitFor(() => { expect( document - .querySelector("iframe") + .querySelector('iframe') .contentWindow.document.body.querySelector( - 'div[data-testid="example"]', - ), + 'div[data-testid="example"]' + ) ).toBeTruthy(); }); const el = document - .querySelector("iframe") + .querySelector('iframe') .contentWindow.document.body.querySelector( - 'div[data-testid="example"]', + 'div[data-testid="example"]' ); - fireEvent(el, pointerEvent("pointerdown")); - fireEvent(el, pointerEvent("pointerup")); + fireEvent(el, pointerEvent('pointerdown')); + fireEvent(el, pointerEvent('pointerup')); fireEvent.click(el); expect(onInteractOutside).not.toHaveBeenCalled(); - fireEvent(iframeDocument.body, pointerEvent("pointerdown")); - fireEvent(iframeDocument.body, pointerEvent("pointerup")); + fireEvent(iframeDocument.body, pointerEvent('pointerdown')); + fireEvent(iframeDocument.body, pointerEvent('pointerup')); fireEvent.click(iframeDocument.body); expect(onInteractOutside).toHaveBeenCalledTimes(1); }); - it("should only listen for the left mouse button", async function () { + it('should only listen for the left mouse button', async function () { let onInteractOutside = jest.fn(); render(); await waitFor(() => { expect( document - .querySelector("iframe") + .querySelector('iframe') .contentWindow.document.body.querySelector( - 'div[data-testid="example"]', - ), + 'div[data-testid="example"]' + ) ).toBeTruthy(); }); - fireEvent( - iframeDocument.body, - pointerEvent("pointerdown", { button: 1 }), - ); - fireEvent(iframeDocument.body, pointerEvent("pointerup", { button: 1 })); - fireEvent.click(iframeDocument.body, { button: 0 }); + fireEvent(iframeDocument.body, pointerEvent('pointerdown', {button: 1})); + fireEvent(iframeDocument.body, pointerEvent('pointerup', {button: 1})); + fireEvent.click(iframeDocument.body, {button: 0}); expect(onInteractOutside).not.toHaveBeenCalled(); - fireEvent( - iframeDocument.body, - pointerEvent("pointerdown", { button: 0 }), - ); - fireEvent(iframeDocument.body, pointerEvent("pointerup", { button: 0 })); - fireEvent.click(iframeDocument.body, { button: 0 }); + fireEvent(iframeDocument.body, pointerEvent('pointerdown', {button: 0})); + fireEvent(iframeDocument.body, pointerEvent('pointerup', {button: 0})); + fireEvent.click(iframeDocument.body, {button: 0}); expect(onInteractOutside).toHaveBeenCalledTimes(1); }); - it("should not fire interact outside if there is a pointer up event without a pointer down first", async function () { + it('should not fire interact outside if there is a pointer up event without a pointer down first', async function () { // Fire pointer down before component with useInteractOutside is mounted - fireEvent(iframeDocument.body, pointerEvent("pointerdown")); + fireEvent(iframeDocument.body, pointerEvent('pointerdown')); let onInteractOutside = jest.fn(); render(); @@ -300,37 +294,37 @@ describe("useInteractOutside (iframes)", function () { await waitFor(() => { expect( document - .querySelector("iframe") + .querySelector('iframe') .contentWindow.document.body.querySelector( - 'div[data-testid="example"]', - ), + 'div[data-testid="example"]' + ) ).toBeTruthy(); }); - fireEvent(iframeDocument.body, pointerEvent("pointerup")); + fireEvent(iframeDocument.body, pointerEvent('pointerup')); fireEvent.click(iframeDocument.body); expect(onInteractOutside).not.toHaveBeenCalled(); }); }); - describe("mouse events", function () { - it("should fire interact outside events based on mouse events", async function () { + describe('mouse events', function () { + it('should fire interact outside events based on mouse events', async function () { let onInteractOutside = jest.fn(); render(); await waitFor(() => { expect( document - .querySelector("iframe") + .querySelector('iframe') .contentWindow.document.body.querySelector( - 'div[data-testid="example"]', - ), + 'div[data-testid="example"]' + ) ).toBeTruthy(); }); const el = document - .querySelector("iframe") + .querySelector('iframe') .contentWindow.document.body.querySelector( - 'div[data-testid="example"]', + 'div[data-testid="example"]' ); fireEvent.mouseDown(el); fireEvent.mouseUp(el); @@ -341,30 +335,30 @@ describe("useInteractOutside (iframes)", function () { expect(onInteractOutside).toHaveBeenCalledTimes(1); }); - it("should only listen for the left mouse button", async function () { + it('should only listen for the left mouse button', async function () { let onInteractOutside = jest.fn(); render(); await waitFor(() => { expect( document - .querySelector("iframe") + .querySelector('iframe') .contentWindow.document.body.querySelector( - 'div[data-testid="example"]', - ), + 'div[data-testid="example"]' + ) ).toBeTruthy(); }); - fireEvent.mouseDown(iframeDocument.body, { button: 1 }); - fireEvent.mouseUp(iframeDocument.body, { button: 1 }); + fireEvent.mouseDown(iframeDocument.body, {button: 1}); + fireEvent.mouseUp(iframeDocument.body, {button: 1}); expect(onInteractOutside).not.toHaveBeenCalled(); - fireEvent.mouseDown(iframeDocument.body, { button: 0 }); - fireEvent.mouseUp(iframeDocument.body, { button: 0 }); + fireEvent.mouseDown(iframeDocument.body, {button: 0}); + fireEvent.mouseUp(iframeDocument.body, {button: 0}); expect(onInteractOutside).toHaveBeenCalledTimes(1); }); - it("should not fire interact outside if there is a mouse up event without a mouse down first", async function () { + it('should not fire interact outside if there is a mouse up event without a mouse down first', async function () { // Fire mouse down before component with useInteractOutside is mounted fireEvent.mouseDown(iframeDocument.body); @@ -374,10 +368,10 @@ describe("useInteractOutside (iframes)", function () { await waitFor(() => { expect( document - .querySelector("iframe") + .querySelector('iframe') .contentWindow.document.body.querySelector( - 'div[data-testid="example"]', - ), + 'div[data-testid="example"]' + ) ).toBeTruthy(); }); fireEvent.mouseUp(iframeDocument.body); @@ -385,25 +379,25 @@ describe("useInteractOutside (iframes)", function () { }); }); - describe("touch events", function () { - it("should fire interact outside events based on mouse events", async function () { + describe('touch events', function () { + it('should fire interact outside events based on mouse events', async function () { let onInteractOutside = jest.fn(); render(); await waitFor(() => { expect( document - .querySelector("iframe") + .querySelector('iframe') .contentWindow.document.body.querySelector( - 'div[data-testid="example"]', - ), + 'div[data-testid="example"]' + ) ).toBeTruthy(); }); const el = document - .querySelector("iframe") + .querySelector('iframe') .contentWindow.document.body.querySelector( - 'div[data-testid="example"]', + 'div[data-testid="example"]' ); fireEvent.touchStart(el); fireEvent.touchEnd(el); @@ -414,24 +408,24 @@ describe("useInteractOutside (iframes)", function () { expect(onInteractOutside).toHaveBeenCalledTimes(1); }); - it("should ignore emulated mouse events", async function () { + it('should ignore emulated mouse events', async function () { let onInteractOutside = jest.fn(); render(); await waitFor(() => { expect( document - .querySelector("iframe") + .querySelector('iframe') .contentWindow.document.body.querySelector( - 'div[data-testid="example"]', - ), + 'div[data-testid="example"]' + ) ).toBeTruthy(); }); const el = document - .querySelector("iframe") + .querySelector('iframe') .contentWindow.document.body.querySelector( - 'div[data-testid="example"]', + 'div[data-testid="example"]' ); fireEvent.touchStart(el); fireEvent.touchEnd(el); @@ -444,7 +438,7 @@ describe("useInteractOutside (iframes)", function () { expect(onInteractOutside).toHaveBeenCalledTimes(1); }); - it("should not fire interact outside if there is a touch end event without a touch start first", function () { + it('should not fire interact outside if there is a touch end event without a touch start first', function () { // Fire mouse down before component with useInteractOutside is mounted fireEvent.touchStart(iframeDocument.body); @@ -456,22 +450,22 @@ describe("useInteractOutside (iframes)", function () { }); }); - describe("disable interact outside events", function () { - it("does not handle pointer events if disabled", function () { + describe('disable interact outside events', function () { + it('does not handle pointer events if disabled', function () { let onInteractOutside = jest.fn(); render( - , + ); - fireEvent(iframeDocument.body, pointerEvent("mousedown")); - fireEvent(iframeDocument.body, pointerEvent("mouseup")); + fireEvent(iframeDocument.body, pointerEvent('mousedown')); + fireEvent(iframeDocument.body, pointerEvent('mouseup')); expect(onInteractOutside).not.toHaveBeenCalled(); }); - it("does not handle touch events if disabled", function () { + it('does not handle touch events if disabled', function () { let onInteractOutside = jest.fn(); render( - , + ); fireEvent.touchStart(iframeDocument.body); @@ -479,10 +473,10 @@ describe("useInteractOutside (iframes)", function () { expect(onInteractOutside).not.toHaveBeenCalled(); }); - it("does not handle mouse events if disabled", function () { + it('does not handle mouse events if disabled', function () { let onInteractOutside = jest.fn(); render( - , + ); fireEvent.mouseDown(iframeDocument.body); @@ -492,24 +486,24 @@ describe("useInteractOutside (iframes)", function () { }); }); -describe("useInteractOutside shadow DOM", function () { +describe('useInteractOutside shadow DOM', function () { // Helper function to create a shadow root and render the component inside it function createShadowRootAndRender(ui) { - const shadowHost = document.createElement("div"); + const shadowHost = document.createElement('div'); document.body.appendChild(shadowHost); - const shadowRoot = shadowHost.attachShadow({ mode: "open" }); + const shadowRoot = shadowHost.attachShadow({mode: 'open'}); function WrapperComponent() { return ReactDOM.createPortal(ui, shadowRoot); } render(); - return { shadowRoot, cleanup: () => document.body.removeChild(shadowHost) }; + return {shadowRoot, cleanup: () => document.body.removeChild(shadowHost)}; } - function App({ onInteractOutside }) { + function App({onInteractOutside}) { const ref = useRef(null); - useInteractOutside({ ref, onInteractOutside }); + useInteractOutside({ref, onInteractOutside}); return (
@@ -521,13 +515,13 @@ describe("useInteractOutside shadow DOM", function () { ); } - it("does not trigger when clicking inside popover", function () { + it('does not trigger when clicking inside popover', function () { const onInteractOutside = jest.fn(); - const { shadowRoot, cleanup } = createShadowRootAndRender( - , + const {shadowRoot, cleanup} = createShadowRootAndRender( + ); - const insidePopover = shadowRoot.getElementById("inside-popover"); + const insidePopover = shadowRoot.getElementById('inside-popover'); fireEvent.mouseDown(insidePopover); fireEvent.mouseUp(insidePopover); @@ -535,13 +529,13 @@ describe("useInteractOutside shadow DOM", function () { cleanup(); }); - it("does not trigger when clicking the popover", function () { + it('does not trigger when clicking the popover', function () { const onInteractOutside = jest.fn(); - const { shadowRoot, cleanup } = createShadowRootAndRender( - , + const {shadowRoot, cleanup} = createShadowRootAndRender( + ); - const popover = shadowRoot.getElementById("popover"); + const popover = shadowRoot.getElementById('popover'); fireEvent.mouseDown(popover); fireEvent.mouseUp(popover); @@ -549,10 +543,10 @@ describe("useInteractOutside shadow DOM", function () { cleanup(); }); - it("triggers when clicking outside the popover", function () { + it('triggers when clicking outside the popover', function () { const onInteractOutside = jest.fn(); - const { cleanup } = createShadowRootAndRender( - , + const {cleanup} = createShadowRootAndRender( + ); // Clicking on the document body outside the shadow DOM @@ -563,13 +557,13 @@ describe("useInteractOutside shadow DOM", function () { cleanup(); }); - it("triggers when clicking a button outside the shadow dom altogether", function () { + it('triggers when clicking a button outside the shadow dom altogether', function () { const onInteractOutside = jest.fn(); - const { cleanup } = createShadowRootAndRender( - , + const {cleanup} = createShadowRootAndRender( + ); // Button outside shadow DOM and component - const button = document.createElement("button"); + const button = document.createElement('button'); document.body.appendChild(button); fireEvent.mouseDown(button); @@ -581,29 +575,29 @@ describe("useInteractOutside shadow DOM", function () { }); }); -describe("useInteractOutside shadow DOM extended tests", function () { +describe('useInteractOutside shadow DOM extended tests', function () { // Setup function similar to previous tests, but includes a dynamic element scenario function createShadowRootAndRender(ui) { - const shadowHost = document.createElement("div"); + const shadowHost = document.createElement('div'); document.body.appendChild(shadowHost); - const shadowRoot = shadowHost.attachShadow({ mode: "open" }); + const shadowRoot = shadowHost.attachShadow({mode: 'open'}); function WrapperComponent() { return ReactDOM.createPortal(ui, shadowRoot); } render(); - return { shadowRoot, cleanup: () => document.body.removeChild(shadowHost) }; + return {shadowRoot, cleanup: () => document.body.removeChild(shadowHost)}; } - function App({ onInteractOutside, includeDynamicElement = false }) { + function App({onInteractOutside, includeDynamicElement = false}) { const ref = useRef(null); - useInteractOutside({ ref, onInteractOutside }); + useInteractOutside({ref, onInteractOutside}); useEffect(() => { if (includeDynamicElement) { - const dynamicEl = document.createElement("div"); - dynamicEl.id = "dynamic-outside"; + const dynamicEl = document.createElement('div'); + dynamicEl.id = 'dynamic-outside'; document.body.appendChild(dynamicEl); return () => document.body.removeChild(dynamicEl); @@ -620,14 +614,14 @@ describe("useInteractOutside shadow DOM extended tests", function () { ); } - it("correctly identifies interaction with dynamically added external elements", function () { + it('correctly identifies interaction with dynamically added external elements', function () { jest.useFakeTimers(); const onInteractOutside = jest.fn(); - const { cleanup } = createShadowRootAndRender( - , + const {cleanup} = createShadowRootAndRender( + ); - const dynamicEl = document.getElementById("dynamic-outside"); + const dynamicEl = document.getElementById('dynamic-outside'); fireEvent.mouseDown(dynamicEl); fireEvent.mouseUp(dynamicEl); @@ -637,12 +631,12 @@ describe("useInteractOutside shadow DOM extended tests", function () { }); }); -describe("useInteractOutside with Shadow DOM and UNSAFE_PortalProvider", () => { +describe('useInteractOutside with Shadow DOM and UNSAFE_PortalProvider', () => { let user; beforeAll(() => { enableShadowDOM(); - user = userEvent.setup({ delay: null, pointerMap }); + user = userEvent.setup({delay: null, pointerMap}); }); beforeEach(() => { @@ -655,13 +649,13 @@ describe("useInteractOutside with Shadow DOM and UNSAFE_PortalProvider", () => { }); }); - it("should handle interact outside events with UNSAFE_PortalProvider in shadow DOM", async () => { - const { shadowRoot } = createShadowRoot(); + it('should handle interact outside events with UNSAFE_PortalProvider in shadow DOM', async () => { + const {shadowRoot} = createShadowRoot(); let interactOutsideTriggered = false; // Create portal container within the shadow DOM for the popover - const popoverPortal = document.createElement("div"); - popoverPortal.setAttribute("data-testid", "popover-portal"); + const popoverPortal = document.createElement('div'); + popoverPortal.setAttribute('data-testid', 'popover-portal'); shadowRoot.appendChild(popoverPortal); function ShadowInteractOutsideExample() { @@ -670,7 +664,7 @@ describe("useInteractOutside with Shadow DOM and UNSAFE_PortalProvider", () => { ref, onInteractOutside: () => { interactOutsideTriggered = true; - }, + } }); return ( @@ -681,28 +675,28 @@ describe("useInteractOutside with Shadow DOM and UNSAFE_PortalProvider", () => {
, - popoverPortal, + popoverPortal )}
); } - const { unmount } = render(); + const {unmount} = render(); const target = shadowRoot.querySelector('[data-testid="target"]'); const innerButton = shadowRoot.querySelector( - '[data-testid="inner-button"]', + '[data-testid="inner-button"]' ); const outsideButton = shadowRoot.querySelector( - '[data-testid="outside-button"]', + '[data-testid="outside-button"]' ); // Click inside the target - should NOT trigger interact outside @@ -722,8 +716,8 @@ describe("useInteractOutside with Shadow DOM and UNSAFE_PortalProvider", () => { document.body.removeChild(shadowRoot.host); }); - it("should correctly identify interactions across shadow DOM boundaries (issue #8675)", async () => { - const { shadowRoot } = createShadowRoot(); + it('should correctly identify interactions across shadow DOM boundaries (issue #8675)', async () => { + const {shadowRoot} = createShadowRoot(); let popoverClosed = false; function MenuPopoverExample() { @@ -732,7 +726,7 @@ describe("useInteractOutside with Shadow DOM and UNSAFE_PortalProvider", () => { ref: popoverRef, onInteractOutside: () => { popoverClosed = true; - }, + } }); return ( @@ -742,7 +736,7 @@ describe("useInteractOutside with Shadow DOM and UNSAFE_PortalProvider", () => {
@@ -845,30 +839,30 @@ describe("useInteractOutside with Shadow DOM and UNSAFE_PortalProvider", () => { ref={popoverRef} data-testid="popover-in-modal" style={{ - background: "white", - border: "1px solid gray", - padding: "10px", + background: 'white', + border: '1px solid gray', + padding: '10px' }} >
, - modalPortal, + modalPortal )}
); } - const { unmount } = render(); + const {unmount} = render(); const mainButton = shadowRoot.querySelector('[data-testid="main-button"]'); const modalButton = shadowRoot.querySelector( - '[data-testid="modal-button"]', + '[data-testid="modal-button"]' ); const popoverButton = shadowRoot.querySelector( - '[data-testid="popover-button"]', + '[data-testid="popover-button"]' ); // Click popover button - should NOT trigger either interact outside @@ -893,10 +887,10 @@ describe("useInteractOutside with Shadow DOM and UNSAFE_PortalProvider", () => { document.body.removeChild(shadowRoot.host); }); - it("should handle pointer events correctly in shadow DOM with portal provider", async () => { + it('should handle pointer events correctly in shadow DOM with portal provider', async () => { installPointerEvent(); - const { shadowRoot } = createShadowRoot(); + const {shadowRoot} = createShadowRoot(); let interactOutsideCount = 0; function PointerEventsExample() { @@ -905,7 +899,7 @@ describe("useInteractOutside with Shadow DOM and UNSAFE_PortalProvider", () => { ref, onInteractOutside: () => { interactOutsideCount++; - }, + } }); return ( @@ -920,24 +914,24 @@ describe("useInteractOutside with Shadow DOM and UNSAFE_PortalProvider", () => { ); } - const { unmount } = render(); + const {unmount} = render(); const targetButton = shadowRoot.querySelector( - '[data-testid="target-button"]', + '[data-testid="target-button"]' ); const outsideButton = shadowRoot.querySelector( - '[data-testid="outside-button"]', + '[data-testid="outside-button"]' ); // Simulate pointer events on target - should NOT trigger interact outside - fireEvent(targetButton, pointerEvent("pointerdown")); - fireEvent(targetButton, pointerEvent("pointerup")); + fireEvent(targetButton, pointerEvent('pointerdown')); + fireEvent(targetButton, pointerEvent('pointerup')); fireEvent.click(targetButton); expect(interactOutsideCount).toBe(0); // Simulate pointer events outside - should trigger interact outside - fireEvent(outsideButton, pointerEvent("pointerdown")); - fireEvent(outsideButton, pointerEvent("pointerup")); + fireEvent(outsideButton, pointerEvent('pointerdown')); + fireEvent(outsideButton, pointerEvent('pointerup')); fireEvent.click(outsideButton); expect(interactOutsideCount).toBe(1); @@ -946,8 +940,8 @@ describe("useInteractOutside with Shadow DOM and UNSAFE_PortalProvider", () => { document.body.removeChild(shadowRoot.host); }); - it("should handle interact outside with dynamic content in shadow DOM", async () => { - const { shadowRoot } = createShadowRoot(); + it('should handle interact outside with dynamic content in shadow DOM', async () => { + const {shadowRoot} = createShadowRoot(); let interactOutsideCount = 0; function DynamicContentExample() { @@ -958,7 +952,7 @@ describe("useInteractOutside with Shadow DOM and UNSAFE_PortalProvider", () => { ref, onInteractOutside: () => { interactOutsideCount++; - }, + } }); return ( @@ -983,16 +977,16 @@ describe("useInteractOutside with Shadow DOM and UNSAFE_PortalProvider", () => { ); } - const { unmount } = render(); + const {unmount} = render(); const toggleButton = shadowRoot.querySelector( - '[data-testid="toggle-button"]', + '[data-testid="toggle-button"]' ); const dynamicButton = shadowRoot.querySelector( - '[data-testid="dynamic-button"]', + '[data-testid="dynamic-button"]' ); const outsideButton = shadowRoot.querySelector( - '[data-testid="outside-button"]', + '[data-testid="outside-button"]' ); // Click dynamic content - should NOT trigger interact outside @@ -1007,7 +1001,7 @@ describe("useInteractOutside with Shadow DOM and UNSAFE_PortalProvider", () => { // Toggle content back and click it - should still NOT trigger interact outside await user.click(toggleButton); const newDynamicButton = shadowRoot.querySelector( - '[data-testid="dynamic-button"]', + '[data-testid="dynamic-button"]' ); await user.click(newDynamicButton); expect(interactOutsideCount).toBe(1); // Should remain 1 @@ -1017,14 +1011,14 @@ describe("useInteractOutside with Shadow DOM and UNSAFE_PortalProvider", () => { document.body.removeChild(shadowRoot.host); }); - it("should handle interact outside across mixed shadow DOM and regular DOM boundaries", async () => { - const { shadowRoot } = createShadowRoot(); + it('should handle interact outside across mixed shadow DOM and regular DOM boundaries', async () => { + const {shadowRoot} = createShadowRoot(); let interactOutsideTriggered = false; // Create a regular DOM button outside the shadow DOM - const regularDOMButton = document.createElement("button"); - regularDOMButton.textContent = "Regular DOM Button"; - regularDOMButton.setAttribute("data-testid", "regular-dom-button"); + const regularDOMButton = document.createElement('button'); + regularDOMButton.textContent = 'Regular DOM Button'; + regularDOMButton.setAttribute('data-testid', 'regular-dom-button'); document.body.appendChild(regularDOMButton); function MixedDOMExample() { @@ -1033,7 +1027,7 @@ describe("useInteractOutside with Shadow DOM and UNSAFE_PortalProvider", () => { ref, onInteractOutside: () => { interactOutsideTriggered = true; - }, + } }); return ( @@ -1048,13 +1042,13 @@ describe("useInteractOutside with Shadow DOM and UNSAFE_PortalProvider", () => { ); } - const { unmount } = render(); + const {unmount} = render(); const shadowButton = shadowRoot.querySelector( - '[data-testid="shadow-button"]', + '[data-testid="shadow-button"]' ); const shadowOutside = shadowRoot.querySelector( - '[data-testid="shadow-outside"]', + '[data-testid="shadow-outside"]' ); // Click inside shadow target - should NOT trigger @@ -1078,7 +1072,7 @@ describe("useInteractOutside with Shadow DOM and UNSAFE_PortalProvider", () => { }); function pointerEvent(type, opts) { - let evt = new Event(type, { bubbles: true, cancelable: true }); + let evt = new Event(type, {bubbles: true, cancelable: true}); Object.assign(evt, opts); return evt; } From 2e0e86e1e3b147a8b9cbe6efe0f458cb64eac13d Mon Sep 17 00:00:00 2001 From: John Pangalos Date: Tue, 2 Sep 2025 11:10:28 +0200 Subject: [PATCH 4/6] Deleting AI gen tests that weren't working There are a bunch of redundant tests. Getting rid of them for now. --- .../interactions/test/useFocusWithin.test.js | 356 +--------------- .../test/useInteractOutside.test.js | 348 ---------------- .../overlays/test/usePopover.test.tsx | 385 +++--------------- 3 files changed, 53 insertions(+), 1036 deletions(-) diff --git a/packages/@react-aria/interactions/test/useFocusWithin.test.js b/packages/@react-aria/interactions/test/useFocusWithin.test.js index fd5cef60f4a..a5cd33a45b0 100644 --- a/packages/@react-aria/interactions/test/useFocusWithin.test.js +++ b/packages/@react-aria/interactions/test/useFocusWithin.test.js @@ -10,13 +10,9 @@ * governing permissions and limitations under the License. */ -import {act, createShadowRoot, pointerMap, render, waitFor} from '@react-spectrum/test-utils-internal'; -import {enableShadowDOM} from '@react-stately/flags'; +import {act, render, waitFor} from '@react-spectrum/test-utils-internal'; import React, {useState} from 'react'; -import ReactDOM from 'react-dom'; -import {UNSAFE_PortalProvider} from '@react-aria/overlays'; import {useFocusWithin} from '../'; -import userEvent from '@testing-library/user-event'; function Example(props) { let {focusWithinProps} = useFocusWithin(props); @@ -199,353 +195,3 @@ describe('useFocusWithin', function () { ]); }); }); - -describe('useFocusWithin with Shadow DOM and UNSAFE_PortalProvider', () => { - let user; - - beforeAll(() => { - enableShadowDOM(); - user = userEvent.setup({delay: null, pointerMap}); - }); - - beforeEach(() => { - jest.useFakeTimers(); - }); - - afterEach(() => { - act(() => {jest.runAllTimers();}); - }); - - it('should handle focus within events in shadow DOM with UNSAFE_PortalProvider', async () => { - const {shadowRoot} = createShadowRoot(); - let focusWithinTriggered = false; - let blurWithinTriggered = false; - let focusChangeEvents = []; - - function ShadowFocusWithinExample() { - const handleFocusWithin = () => { - focusWithinTriggered = true; - }; - - const handleBlurWithin = () => { - blurWithinTriggered = true; - }; - - const handleFocusWithinChange = (isFocused) => { - focusChangeEvents.push(isFocused); - }; - - return ( - shadowRoot}> -
- - - - - -
-
- ); - } - - const {unmount} = render(); - - const innerButton = shadowRoot.querySelector('[data-testid="inner-button"]'); - const innerInput = shadowRoot.querySelector('[data-testid="inner-input"]'); - const outerButton = shadowRoot.querySelector('[data-testid="outer-button"]'); - - // Focus within the example container - act(() => { innerButton.focus(); }); - expect(shadowRoot.activeElement).toBe(innerButton); - expect(focusWithinTriggered).toBe(true); - expect(focusChangeEvents).toContain(true); - - // Move focus within the container (should not trigger blur) - act(() => { innerInput.focus(); }); - expect(shadowRoot.activeElement).toBe(innerInput); - expect(blurWithinTriggered).toBe(false); - - // Move focus outside the container - act(() => { outerButton.focus(); }); - expect(shadowRoot.activeElement).toBe(outerButton); - expect(blurWithinTriggered).toBe(true); - expect(focusChangeEvents).toContain(false); - - // Cleanup - unmount(); - document.body.removeChild(shadowRoot.host); - }); - - it('should handle focus within detection across shadow DOM boundaries (issue #8675)', async () => { - const {shadowRoot} = createShadowRoot(); - let focusWithinEvents = []; - - function MenuWithFocusWithinExample() { - const handleFocusWithinChange = (isFocused) => { - focusWithinEvents.push({type: 'focusWithinChange', isFocused}); - }; - - return ( - shadowRoot}> -
- - -
- - -
-
-
-
- ); - } - - const {unmount} = render(); - - const menuItem1 = shadowRoot.querySelector('[data-testid="menu-item-1"]'); - const menuItem2 = shadowRoot.querySelector('[data-testid="menu-item-2"]'); - const menuTrigger = shadowRoot.querySelector('[data-testid="menu-trigger"]'); - - // Focus enters the menu - act(() => { menuItem1.focus(); }); - expect(shadowRoot.activeElement).toBe(menuItem1); - expect(focusWithinEvents).toContainEqual({type: 'focusWithinChange', isFocused: true}); - - // Click menu item (this should not cause focus within to be lost) - await user.click(menuItem1); - - // Focus should remain within the menu area - expect(focusWithinEvents.filter(e => e.isFocused === false)).toHaveLength(0); - - // Move focus within menu - act(() => { menuItem2.focus(); }); - expect(shadowRoot.activeElement).toBe(menuItem2); - - // Only when focus moves completely outside should focus within be false - act(() => { menuTrigger.focus(); }); - expect(shadowRoot.activeElement).toBe(menuTrigger); - expect(focusWithinEvents).toContainEqual({type: 'focusWithinChange', isFocused: false}); - - // Cleanup - unmount(); - document.body.removeChild(shadowRoot.host); - }); - - it('should handle nested focus within containers in shadow DOM with portals', async () => { - const {shadowRoot} = createShadowRoot(); - let outerFocusEvents = []; - let innerFocusEvents = []; - - function NestedFocusWithinExample() { - return ( - shadowRoot}> - outerFocusEvents.push(isFocused)} - data-testid="outer-container" - > - - innerFocusEvents.push(isFocused)} - data-testid="inner-container" - > - - - - - - - ); - } - - const {unmount} = render(); - - const outerButton = shadowRoot.querySelector('[data-testid="outer-button"]'); - const innerButton1 = shadowRoot.querySelector('[data-testid="inner-button-1"]'); - const innerButton2 = shadowRoot.querySelector('[data-testid="inner-button-2"]'); - const outerButton2 = shadowRoot.querySelector('[data-testid="outer-button-2"]'); - - // Focus enters outer container - act(() => { outerButton.focus(); }); - expect(outerFocusEvents).toContain(true); - expect(innerFocusEvents).toHaveLength(0); - - // Focus enters inner container - act(() => { innerButton1.focus(); }); - expect(innerFocusEvents).toContain(true); - expect(outerFocusEvents.filter(e => e === false)).toHaveLength(0); // Outer should still be focused - - // Move within inner container - act(() => { innerButton2.focus(); }); - expect(innerFocusEvents.filter(e => e === false)).toHaveLength(0); - - // Move to outer container (leaves inner) - act(() => { outerButton2.focus(); }); - expect(innerFocusEvents).toContain(false); - expect(outerFocusEvents.filter(e => e === false)).toHaveLength(0); - - // Cleanup - unmount(); - document.body.removeChild(shadowRoot.host); - }); - - it('should handle focus within with complex portal hierarchies in shadow DOM', async () => { - const {shadowRoot} = createShadowRoot(); - const modalPortal = document.createElement('div'); - modalPortal.setAttribute('data-testid', 'modal-portal'); - shadowRoot.appendChild(modalPortal); - - let modalFocusEvents = []; - let popoverFocusEvents = []; - - function ComplexPortalExample() { - return ( - shadowRoot}> -
- - - {/* Modal with focus within */} - {ReactDOM.createPortal( - modalFocusEvents.push(isFocused)} - data-testid="modal" - > -
- - - - {/* Nested popover within modal */} - popoverFocusEvents.push(isFocused)} - data-testid="popover" - > -
- - -
-
-
-
, - modalPortal - )} -
-
- ); - } - - const {unmount} = render(); - - const modalButton1 = shadowRoot.querySelector('[data-testid="modal-button-1"]'); - const popoverItem1 = shadowRoot.querySelector('[data-testid="popover-item-1"]'); - const popoverItem2 = shadowRoot.querySelector('[data-testid="popover-item-2"]'); - const mainButton = shadowRoot.querySelector('[data-testid="main-button"]'); - - // Focus enters modal - act(() => { modalButton1.focus(); }); - expect(modalFocusEvents).toContain(true); - - // Focus enters popover within modal - act(() => { popoverItem1.focus(); }); - expect(popoverFocusEvents).toContain(true); - expect(modalFocusEvents.filter(e => e === false)).toHaveLength(0); // Modal should still have focus within - - // Move within popover - act(() => { popoverItem2.focus(); }); - expect(popoverFocusEvents.filter(e => e === false)).toHaveLength(0); - - // Move back to modal (leaves popover) - act(() => { modalButton1.focus(); }); - expect(popoverFocusEvents).toContain(false); - expect(modalFocusEvents.filter(e => e === false)).toHaveLength(0); - - // Move completely outside (leaves modal) - act(() => { mainButton.focus(); }); - expect(modalFocusEvents).toContain(false); - - // Cleanup - unmount(); - document.body.removeChild(shadowRoot.host); - }); - - it('should correctly handle focus within when elements are dynamically added/removed in shadow DOM', async () => { - const {shadowRoot} = createShadowRoot(); - let focusWithinEvents = []; - - function DynamicFocusWithinExample() { - const [showItems, setShowItems] = React.useState(true); - - return ( - shadowRoot}> - focusWithinEvents.push(isFocused)} - data-testid="dynamic-container" - > - - {showItems && ( -
- - -
- )} -
-
- ); - } - - const {unmount} = render(); - - const toggleButton = shadowRoot.querySelector('[data-testid="toggle-button"]'); - const dynamicItem1 = shadowRoot.querySelector('[data-testid="dynamic-item-1"]'); - - // Focus within the container - act(() => { dynamicItem1.focus(); }); - expect(focusWithinEvents).toContain(true); - - // Click toggle to remove items while focused on one - await user.click(toggleButton); - - // Focus should now be on the toggle button, still within container - expect(shadowRoot.activeElement).toBe(toggleButton); - expect(focusWithinEvents.filter(e => e === false)).toHaveLength(0); - - // Toggle back to show items - await user.click(toggleButton); - - // Focus should still be within the container - const newDynamicItem1 = shadowRoot.querySelector('[data-testid="dynamic-item-1"]'); - act(() => { newDynamicItem1.focus(); }); - expect(focusWithinEvents.filter(e => e === false)).toHaveLength(0); - - // Cleanup - unmount(); - document.body.removeChild(shadowRoot.host); - }); -}); diff --git a/packages/@react-aria/interactions/test/useInteractOutside.test.js b/packages/@react-aria/interactions/test/useInteractOutside.test.js index 2d3438d4cd1..81a6f0efba7 100644 --- a/packages/@react-aria/interactions/test/useInteractOutside.test.js +++ b/packages/@react-aria/interactions/test/useInteractOutside.test.js @@ -681,352 +681,4 @@ describe('useInteractOutside with Shadow DOM and UNSAFE_PortalProvider', () => { unmount(); document.body.removeChild(shadowRoot.host); }); - - it('should correctly identify interactions across shadow DOM boundaries (issue #8675)', async () => { - const {shadowRoot} = createShadowRoot(); - let popoverClosed = false; - - function MenuPopoverExample() { - const popoverRef = useRef(); - useInteractOutside({ - ref: popoverRef, - onInteractOutside: () => { - popoverClosed = true; - } - }); - - return ( - shadowRoot}> -
- -
-
- - -
-
-
-
- ); - } - - const {unmount} = render(); - - const menuItem1 = shadowRoot.querySelector('[data-testid="menu-item-1"]'); - const menuTrigger = shadowRoot.querySelector( - '[data-testid="menu-trigger"]' - ); - const menuPopover = shadowRoot.querySelector( - '[data-testid="menu-popover"]' - ); - - // Click menu item - should NOT close popover (this is the bug being tested) - await user.click(menuItem1); - expect(popoverClosed).toBe(false); - - // Click on the popover itself - should NOT close popover - await user.click(menuPopover); - expect(popoverClosed).toBe(false); - - // Click outside the popover - SHOULD close popover - await user.click(menuTrigger); - expect(popoverClosed).toBe(true); - - // Cleanup - unmount(); - document.body.removeChild(shadowRoot.host); - }); - - it('should handle nested portal scenarios with interact outside in shadow DOM', async () => { - const {shadowRoot} = createShadowRoot(); - const modalPortal = document.createElement('div'); - modalPortal.setAttribute('data-testid', 'modal-portal'); - shadowRoot.appendChild(modalPortal); - - let modalInteractOutside = false; - let popoverInteractOutside = false; - - function NestedPortalsExample() { - const modalRef = useRef(); - const popoverRef = useRef(); - - useInteractOutside({ - ref: modalRef, - onInteractOutside: () => { - modalInteractOutside = true; - } - }); - - useInteractOutside({ - ref: popoverRef, - onInteractOutside: () => { - popoverInteractOutside = true; - } - }); - - return ( - shadowRoot}> -
- - - {/* Modal */} - {ReactDOM.createPortal( -
-
- - - {/* Popover within modal */} -
- -
-
-
, - modalPortal - )} -
-
- ); - } - - const {unmount} = render(); - - const mainButton = shadowRoot.querySelector('[data-testid="main-button"]'); - const modalButton = shadowRoot.querySelector( - '[data-testid="modal-button"]' - ); - const popoverButton = shadowRoot.querySelector( - '[data-testid="popover-button"]' - ); - - // Click popover button - should NOT trigger either interact outside - await user.click(popoverButton); - expect(popoverInteractOutside).toBe(false); - expect(modalInteractOutside).toBe(false); - - // Click modal button - should trigger popover interact outside but NOT modal - await user.click(modalButton); - expect(popoverInteractOutside).toBe(true); - expect(modalInteractOutside).toBe(false); - - // Reset and click completely outside - popoverInteractOutside = false; - modalInteractOutside = false; - - await user.click(mainButton); - expect(modalInteractOutside).toBe(true); - - // Cleanup - unmount(); - document.body.removeChild(shadowRoot.host); - }); - - it('should handle pointer events correctly in shadow DOM with portal provider', async () => { - installPointerEvent(); - - const {shadowRoot} = createShadowRoot(); - let interactOutsideCount = 0; - - function PointerEventsExample() { - const ref = useRef(); - useInteractOutside({ - ref, - onInteractOutside: () => { - interactOutsideCount++; - } - }); - - return ( - shadowRoot}> -
-
- -
- -
-
- ); - } - - const {unmount} = render(); - - const targetButton = shadowRoot.querySelector( - '[data-testid="target-button"]' - ); - const outsideButton = shadowRoot.querySelector( - '[data-testid="outside-button"]' - ); - - // Simulate pointer events on target - should NOT trigger interact outside - fireEvent(targetButton, pointerEvent('pointerdown')); - fireEvent(targetButton, pointerEvent('pointerup')); - fireEvent.click(targetButton); - expect(interactOutsideCount).toBe(0); - - // Simulate pointer events outside - should trigger interact outside - fireEvent(outsideButton, pointerEvent('pointerdown')); - fireEvent(outsideButton, pointerEvent('pointerup')); - fireEvent.click(outsideButton); - expect(interactOutsideCount).toBe(1); - - // Cleanup - unmount(); - document.body.removeChild(shadowRoot.host); - }); - - it('should handle interact outside with dynamic content in shadow DOM', async () => { - const {shadowRoot} = createShadowRoot(); - let interactOutsideCount = 0; - - function DynamicContentExample() { - const ref = useRef(); - const [showContent, setShowContent] = React.useState(true); - - useInteractOutside({ - ref, - onInteractOutside: () => { - interactOutsideCount++; - } - }); - - return ( - shadowRoot}> -
-
- - {showContent && ( -
- -
- )} -
- -
-
- ); - } - - const {unmount} = render(); - - const toggleButton = shadowRoot.querySelector( - '[data-testid="toggle-button"]' - ); - const dynamicButton = shadowRoot.querySelector( - '[data-testid="dynamic-button"]' - ); - const outsideButton = shadowRoot.querySelector( - '[data-testid="outside-button"]' - ); - - // Click dynamic content - should NOT trigger interact outside - await user.click(dynamicButton); - expect(interactOutsideCount).toBe(0); - - // Toggle to remove content, then click outside - should trigger interact outside - await user.click(toggleButton); - await user.click(outsideButton); - expect(interactOutsideCount).toBe(1); - - // Toggle content back and click it - should still NOT trigger interact outside - await user.click(toggleButton); - const newDynamicButton = shadowRoot.querySelector( - '[data-testid="dynamic-button"]' - ); - await user.click(newDynamicButton); - expect(interactOutsideCount).toBe(1); // Should remain 1 - - // Cleanup - unmount(); - document.body.removeChild(shadowRoot.host); - }); - - it('should handle interact outside across mixed shadow DOM and regular DOM boundaries', async () => { - const {shadowRoot} = createShadowRoot(); - let interactOutsideTriggered = false; - - // Create a regular DOM button outside the shadow DOM - const regularDOMButton = document.createElement('button'); - regularDOMButton.textContent = 'Regular DOM Button'; - regularDOMButton.setAttribute('data-testid', 'regular-dom-button'); - document.body.appendChild(regularDOMButton); - - function MixedDOMExample() { - const ref = useRef(); - useInteractOutside({ - ref, - onInteractOutside: () => { - interactOutsideTriggered = true; - } - }); - - return ( - shadowRoot}> -
-
- -
- -
-
- ); - } - - const {unmount} = render(); - - const shadowButton = shadowRoot.querySelector( - '[data-testid="shadow-button"]' - ); - const shadowOutside = shadowRoot.querySelector( - '[data-testid="shadow-outside"]' - ); - - // Click inside shadow target - should NOT trigger - await user.click(shadowButton); - expect(interactOutsideTriggered).toBe(false); - - // Click outside in shadow DOM - should trigger - await user.click(shadowOutside); - expect(interactOutsideTriggered).toBe(true); - - // Reset and test regular DOM interaction - interactOutsideTriggered = false; - await user.click(regularDOMButton); - expect(interactOutsideTriggered).toBe(true); - - // Cleanup - document.body.removeChild(regularDOMButton); - unmount(); - document.body.removeChild(shadowRoot.host); - }); }); diff --git a/packages/@react-aria/overlays/test/usePopover.test.tsx b/packages/@react-aria/overlays/test/usePopover.test.tsx index a47a3c2f20e..aa57850a1d1 100644 --- a/packages/@react-aria/overlays/test/usePopover.test.tsx +++ b/packages/@react-aria/overlays/test/usePopover.test.tsx @@ -56,33 +56,42 @@ describe('usePopover with Shadow DOM and UNSAFE_PortalProvider', () => { }); afterEach(() => { - act(() => {jest.runAllTimers();}); + act(() => { + jest.runAllTimers(); + }); }); - it('should handle popover interactions within shadow DOM with UNSAFE_PortalProvider', async () => { + it('should handle popover interactions with UNSAFE_PortalProvider in shadow DOM', async () => { const {shadowRoot} = createShadowRoot(); let triggerClicked = false; let popoverInteracted = false; + const popoverPortal = document.createElement('div'); + popoverPortal.setAttribute('data-testid', 'popover-portal'); + shadowRoot.appendChild(popoverPortal); + function ShadowPopoverExample() { const triggerRef = useRef(null); const popoverRef = useRef(null); const state = useOverlayTriggerState({ defaultOpen: false, - onOpenChange: (isOpen) => { + onOpenChange: isOpen => { // Track state changes } }); - + useOverlayTrigger({type: 'listbox'}, state, triggerRef); - const {popoverProps} = usePopover({ - triggerRef, - popoverRef, - placement: 'bottom start' - }, state); + const {popoverProps} = usePopover( + { + triggerRef, + popoverRef, + placement: 'bottom start' + }, + state + ); return ( - shadowRoot}> + shadowRoot as unknown as HTMLElement}>
+ {ReactDOM.createPortal( + <> + {state.isOpen && ( -
- - -
- )} + + +
+ )} + , + popoverPortal + )} +
); @@ -129,7 +142,7 @@ describe('usePopover with Shadow DOM and UNSAFE_PortalProvider', () => { const {unmount} = render(); const trigger = shadowRoot.querySelector('[data-testid="popover-trigger"]'); - + // Click trigger to open popover await user.click(trigger); expect(triggerClicked).toBe(true); @@ -149,305 +162,11 @@ describe('usePopover with Shadow DOM and UNSAFE_PortalProvider', () => { // Close popover const closeButton = shadowRoot.querySelector('[data-testid="close-popover"]'); await user.click(closeButton); - - // Wait for any cleanup - act(() => {jest.runAllTimers();}); - - // Cleanup - unmount(); - document.body.removeChild(shadowRoot.host); - }); - - it('should handle focus management in shadow DOM popover with nested interactive elements', async () => { - const {shadowRoot} = createShadowRoot(); - - function FocusTestPopover() { - const triggerRef = useRef(null); - const popoverRef = useRef(null); - const state = useOverlayTriggerState({defaultOpen: true}); - - useOverlayTrigger({type: 'dialog'}, state, triggerRef); - const {popoverProps} = usePopover({ - triggerRef, - popoverRef - }, state); - - return ( - shadowRoot}> -
- - {state.isOpen && ( -
-
- - - -
-
- )} -
-
- ); - } - - const {unmount} = render(); - - const menuItem1 = shadowRoot.querySelector('[data-testid="menu-item-1"]'); - const menuItem2 = shadowRoot.querySelector('[data-testid="menu-item-2"]'); - const menuItem3 = shadowRoot.querySelector('[data-testid="menu-item-3"]'); - - // Focus first menu item - act(() => { menuItem1.focus(); }); - expect(shadowRoot.activeElement).toBe(menuItem1); - - // Tab through menu items - await user.tab(); - expect(shadowRoot.activeElement).toBe(menuItem2); - - await user.tab(); - expect(shadowRoot.activeElement).toBe(menuItem3); - - // Cleanup - unmount(); - document.body.removeChild(shadowRoot.host); - }); - it('should properly handle click events on popover content within shadow DOM (issue #8675)', async () => { - const {shadowRoot} = createShadowRoot(); - let menuActionExecuted = false; - let popoverClosedUnexpectedly = false; - - function MenuPopoverExample() { - const triggerRef = useRef(null); - const popoverRef = useRef(null); - const [isOpen, setIsOpen] = React.useState(true); - - const state = useOverlayTriggerState({ - isOpen, - onOpenChange: (open) => { - setIsOpen(open); - if (!open) { - popoverClosedUnexpectedly = true; - } - } - }); - - useOverlayTrigger({type: 'listbox'}, state, triggerRef); - const {popoverProps} = usePopover({ - triggerRef, - popoverRef - }, state); - - const handleMenuAction = (action) => { - menuActionExecuted = true; - // In the buggy version, this wouldn't execute because popover closes first - console.log('Menu action:', action); - }; - - return ( - shadowRoot}> -
- - {state.isOpen && ( -
-
- - -
-
- )} -
-
- ); - } - - const {unmount} = render(); - - const saveItem = shadowRoot.querySelector('[data-testid="save-item"]'); - const menuPopover = shadowRoot.querySelector('[data-testid="menu-popover"]'); - - // Verify popover is initially open - expect(menuPopover).toBeInTheDocument(); - - // Focus the menu item - act(() => { saveItem.focus(); }); - expect(shadowRoot.activeElement).toBe(saveItem); - - // Click the menu item - this should execute the action, NOT close the popover - await user.click(saveItem); - - // The action should have been executed (this fails in the buggy version) - expect(menuActionExecuted).toBe(true); - - // The popover should NOT have closed unexpectedly (this fails in the buggy version) - expect(popoverClosedUnexpectedly).toBe(false); - - // Menu should still be visible - expect(shadowRoot.querySelector('[data-testid="menu-popover"]')).toBeInTheDocument(); - - // Cleanup - unmount(); - document.body.removeChild(shadowRoot.host); - }); - - it('should handle multiple overlapping popovers in shadow DOM with portal provider', async () => { - const {shadowRoot} = createShadowRoot(); - - function MultiplePopoversExample() { - const trigger1Ref = useRef(null); - const popover1Ref = useRef(null); - const trigger2Ref = useRef(null); - const popover2Ref = useRef(null); - - const state1 = useOverlayTriggerState({defaultOpen: true}); - const state2 = useOverlayTriggerState({defaultOpen: true}); - - useOverlayTrigger({type: 'dialog'}, state1, trigger1Ref); - useOverlayTrigger({type: 'dialog'}, state2, trigger2Ref); - - const {popoverProps: popover1Props} = usePopover({ - triggerRef: trigger1Ref, - popoverRef: popover1Ref - }, state1); - - const {popoverProps: popover2Props} = usePopover({ - triggerRef: trigger2Ref, - popoverRef: popover2Ref - }, state2); - - return ( - shadowRoot}> -
- - - - {state1.isOpen && ( -
- -
- )} - - {state2.isOpen && ( -
- -
- )} -
-
- ); - } - - const {unmount} = render(); - - const popover1 = shadowRoot.querySelector('[data-testid="popover-1"]'); - const popover2 = shadowRoot.querySelector('[data-testid="popover-2"]'); - const popover1Action = shadowRoot.querySelector('[data-testid="popover-1-action"]'); - const popover2Action = shadowRoot.querySelector('[data-testid="popover-2-action"]'); - - // Both popovers should be present - expect(popover1).toBeInTheDocument(); - expect(popover2).toBeInTheDocument(); - - // Should be able to interact with both popovers - await user.click(popover1Action); - await user.click(popover2Action); - - // Both should still be present after interactions - expect(shadowRoot.querySelector('[data-testid="popover-1"]')).toBeInTheDocument(); - expect(shadowRoot.querySelector('[data-testid="popover-2"]')).toBeInTheDocument(); - - // Cleanup - unmount(); - document.body.removeChild(shadowRoot.host); - }); - - it('should handle popover positioning and containment in shadow DOM', async () => { - const {shadowRoot} = createShadowRoot(); - - function PositionedPopoverExample() { - const triggerRef = useRef(null); - const popoverRef = useRef(null); - const state = useOverlayTriggerState({defaultOpen: true}); - - useOverlayTrigger({type: 'listbox'}, state, triggerRef); - const {popoverProps} = usePopover({ - triggerRef, - popoverRef, - placement: 'bottom start', - containerPadding: 12 - }, state); - - return ( - shadowRoot}> -
- - {state.isOpen && ( -
-
-

This is a positioned popover

- -
-
- )} -
-
- ); - } - - const {unmount} = render(); - - const trigger = shadowRoot.querySelector('[data-testid="positioned-trigger"]'); - const popover = shadowRoot.querySelector('[data-testid="positioned-popover"]'); - const actionButton = shadowRoot.querySelector('[data-testid="action-button"]'); - - // Verify popover exists and is positioned - expect(popover).toBeInTheDocument(); - expect(trigger).toBeInTheDocument(); - - // Verify we can interact with popover content - await user.click(actionButton); - - // Popover should still be present after interaction - expect(shadowRoot.querySelector('[data-testid="positioned-popover"]')).toBeInTheDocument(); + // Wait for any cleanup + act(() => { + jest.runAllTimers(); + }); // Cleanup unmount(); From 06a538017d3a9973b6907505d72af1849d781d45 Mon Sep 17 00:00:00 2001 From: John Pangalos Date: Wed, 3 Sep 2025 09:06:22 +0200 Subject: [PATCH 5/6] Update packages/@react-aria/interactions/test/useInteractOutside.test.js Co-authored-by: Robert Snow --- .../@react-aria/interactions/test/useInteractOutside.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-aria/interactions/test/useInteractOutside.test.js b/packages/@react-aria/interactions/test/useInteractOutside.test.js index 81a6f0efba7..c5f6aab8211 100644 --- a/packages/@react-aria/interactions/test/useInteractOutside.test.js +++ b/packages/@react-aria/interactions/test/useInteractOutside.test.js @@ -617,7 +617,7 @@ describe('useInteractOutside with Shadow DOM and UNSAFE_PortalProvider', () => { }); it('should handle interact outside events with UNSAFE_PortalProvider in shadow DOM', async () => { - const {shadowRoot} = createShadowRoot(); + const {shadowRoot, cleanup} = createShadowRoot(); let interactOutsideTriggered = false; // Create portal container within the shadow DOM for the popover From a9b85aade2e21187a896cab7b3e678f5f2b8a582 Mon Sep 17 00:00:00 2001 From: John Pangalos Date: Wed, 3 Sep 2025 09:06:28 +0200 Subject: [PATCH 6/6] Update packages/@react-aria/interactions/test/useInteractOutside.test.js Co-authored-by: Robert Snow --- .../@react-aria/interactions/test/useInteractOutside.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-aria/interactions/test/useInteractOutside.test.js b/packages/@react-aria/interactions/test/useInteractOutside.test.js index c5f6aab8211..844705874f7 100644 --- a/packages/@react-aria/interactions/test/useInteractOutside.test.js +++ b/packages/@react-aria/interactions/test/useInteractOutside.test.js @@ -679,6 +679,6 @@ describe('useInteractOutside with Shadow DOM and UNSAFE_PortalProvider', () => { // Cleanup unmount(); - document.body.removeChild(shadowRoot.host); + cleanup(); }); });