From a63256099d7e7317cffb315c2521b9e14dd306e4 Mon Sep 17 00:00:00 2001 From: chirokas <157580465+chirokas@users.noreply.github.com> Date: Sun, 18 Jan 2026 16:15:32 +0000 Subject: [PATCH 1/2] fix: prevent Modal from closing when dismissing DateRangePicker via outside click --- .../@react-aria/overlays/src/useOverlay.ts | 12 +++-- .../stories/DatePicker.stories.tsx | 2 +- .../stories/Modal.stories.tsx | 44 +++++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/packages/@react-aria/overlays/src/useOverlay.ts b/packages/@react-aria/overlays/src/useOverlay.ts index 8fdcfa39410..039d6939834 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,9 @@ export function useOverlay(props: AriaOverlayProps, ref: RefObject { + const topMostOverlay = (lastVisibleOverlay.current = visibleOverlays[visibleOverlays.length - 1]); if (!shouldCloseOnInteractOutside || shouldCloseOnInteractOutside(e.target as Element)) { - if (visibleOverlays[visibleOverlays.length - 1] === ref) { + if (topMostOverlay === ref) { e.stopPropagation(); e.preventDefault(); } @@ -105,8 +108,11 @@ export function useOverlay(props: AriaOverlayProps, ref: RefObject ( ); -export const DateRangePickerExample: DateRangePickerStory = (args) => ( +export const DateRangePickerExample = (args) => ( diff --git a/packages/react-aria-components/stories/Modal.stories.tsx b/packages/react-aria-components/stories/Modal.stories.tsx index 36e8193359a..db1513a710f 100644 --- a/packages/react-aria-components/stories/Modal.stories.tsx +++ b/packages/react-aria-components/stories/Modal.stories.tsx @@ -14,6 +14,7 @@ import {Button, ComboBox, Dialog, DialogTrigger, Heading, Input, Label, ListBox, import {Meta, StoryFn} from '@storybook/react'; import React from 'react'; import './styles.css'; +import {DateRangePickerExample} from './DatePicker.stories'; import {MyListBoxItem} from './utils'; import styles from '../example/index.css'; @@ -148,3 +149,46 @@ export const InertTestStory = { } } }; + +function DateRangePickerInsideModal() { + return ( + + + + + + + + + + + ); +} + +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.' + } + } +}; From 1e5fdd94fd5ce60df1cb99228f7d50c940f9f0f0 Mon Sep 17 00:00:00 2001 From: chirokas <157580465+chirokas@users.noreply.github.com> Date: Thu, 22 Jan 2026 15:45:41 +0000 Subject: [PATCH 2/2] add unit test --- .../@react-aria/overlays/src/useOverlay.ts | 3 ++- .../stories/DatePicker.stories.tsx | 2 +- .../stories/Modal.stories.tsx | 1 + .../react-aria-components/test/Dialog.test.js | 26 +++++++++++++++++++ 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/@react-aria/overlays/src/useOverlay.ts b/packages/@react-aria/overlays/src/useOverlay.ts index 039d6939834..06792f54c07 100644 --- a/packages/@react-aria/overlays/src/useOverlay.ts +++ b/packages/@react-aria/overlays/src/useOverlay.ts @@ -93,7 +93,8 @@ export function useOverlay(props: AriaOverlayProps, ref: RefObject { - const topMostOverlay = (lastVisibleOverlay.current = visibleOverlays[visibleOverlays.length - 1]); + const topMostOverlay = visibleOverlays[visibleOverlays.length - 1]; + lastVisibleOverlay.current = topMostOverlay; if (!shouldCloseOnInteractOutside || shouldCloseOnInteractOutside(e.target as Element)) { if (topMostOverlay === ref) { e.stopPropagation(); diff --git a/packages/react-aria-components/stories/DatePicker.stories.tsx b/packages/react-aria-components/stories/DatePicker.stories.tsx index a3ca10625d0..bc9cde5a648 100644 --- a/packages/react-aria-components/stories/DatePicker.stories.tsx +++ b/packages/react-aria-components/stories/DatePicker.stories.tsx @@ -125,7 +125,7 @@ export const DatePickerTriggerWidthExample: DatePickerStory = (args) => ( ); -export const DateRangePickerExample = (args) => ( +export const DateRangePickerExample: DateRangePickerStory = (args) => ( diff --git a/packages/react-aria-components/stories/Modal.stories.tsx b/packages/react-aria-components/stories/Modal.stories.tsx index db1513a710f..00193de3407 100644 --- a/packages/react-aria-components/stories/Modal.stories.tsx +++ b/packages/react-aria-components/stories/Modal.stories.tsx @@ -176,6 +176,7 @@ function DateRangePickerInsideModal() { padding: 30 }}> + {/* @ts-ignore */} 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(); + }); });