diff --git a/packages/@react-aria/overlays/src/useOverlay.ts b/packages/@react-aria/overlays/src/useOverlay.ts index 8fdcfa39410..06792f54c07 100644 --- a/packages/@react-aria/overlays/src/useOverlay.ts +++ b/packages/@react-aria/overlays/src/useOverlay.ts @@ -12,7 +12,7 @@ import {DOMAttributes, RefObject} from '@react-types/shared'; import {isElementInChildOfActiveScope} from '@react-aria/focus'; -import {useEffect} from 'react'; +import {useEffect, useRef} from 'react'; import {useFocusWithin, useInteractOutside} from '@react-aria/interactions'; export interface AriaOverlayProps { @@ -70,6 +70,8 @@ export function useOverlay(props: AriaOverlayProps, ref: RefObject>(undefined); + // Add the overlay ref to the stack of visible overlays on mount, and remove on unmount. useEffect(() => { if (isOpen && !visibleOverlays.includes(ref)) { @@ -91,8 +93,10 @@ export function useOverlay(props: AriaOverlayProps, ref: RefObject { + const topMostOverlay = visibleOverlays[visibleOverlays.length - 1]; + lastVisibleOverlay.current = topMostOverlay; if (!shouldCloseOnInteractOutside || shouldCloseOnInteractOutside(e.target as Element)) { - if (visibleOverlays[visibleOverlays.length - 1] === ref) { + if (topMostOverlay === ref) { e.stopPropagation(); e.preventDefault(); } @@ -105,8 +109,11 @@ export function useOverlay(props: AriaOverlayProps, ref: RefObject + + + + + {/* @ts-ignore */} + + + + + + ); +} + +export const DateRangePickerInsideModalStory = { + render: () => , + parameters: { + description: { + data: 'Open the Modal, then open the DateRangePicker and select a start date. Clicking outside the Modal should close the picker but keep the Modal open.' + } + } +}; diff --git a/packages/react-aria-components/test/Dialog.test.js b/packages/react-aria-components/test/Dialog.test.js index 8cad80bef59..fa5a043b8d8 100644 --- a/packages/react-aria-components/test/Dialog.test.js +++ b/packages/react-aria-components/test/Dialog.test.js @@ -27,11 +27,15 @@ import { Popover, TextField } from '../'; +import {composeStories} from '@storybook/react'; import React, {useRef} from 'react'; +import * as stories from '../stories/Modal.stories'; import {UNSAFE_PortalProvider} from '@react-aria/overlays'; import {User} from '@react-aria/test-utils'; import userEvent from '@testing-library/user-event'; +let {DateRangePickerInsideModalStory: DateRangePickerInsideModal} = composeStories(stories); + describe('Dialog', () => { let user; let testUtilUser = new User({advanceTimer: jest.advanceTimersByTime}); @@ -461,4 +465,26 @@ describe('Dialog', () => { const input = getByTestId('email'); expect(document.activeElement).toBe(input); }); + + it('should not close Modal when DateRangePicker is dismissed by outside click', async () => { + let {getAllByRole, getByRole} = render(); + await user.click(getByRole('button')); + + let modal = getByRole('dialog').closest('.react-aria-ModalOverlay'); + expect(modal).toBeInTheDocument(); + + let button = getByRole('group').querySelector('.react-aria-Button'); + expect(button).toHaveAttribute('aria-label', 'Calendar'); + await user.click(button); + + let popover = getByRole('dialog').closest('.react-aria-Popover'); + expect(popover).toBeInTheDocument(); + expect(popover).toHaveAttribute('data-trigger', 'DateRangePicker'); + + let cells = getAllByRole('gridcell'); + await user.click(cells[5].children[0]); + await user.click(document.body); + expect(popover).not.toBeInTheDocument(); + expect(modal).toBeInTheDocument(); + }); });