diff --git a/packages/react-aria-components/test/RangeCalendar.test.tsx b/packages/react-aria-components/test/RangeCalendar.test.tsx index b140c3772d7..6dded08ac76 100644 --- a/packages/react-aria-components/test/RangeCalendar.test.tsx +++ b/packages/react-aria-components/test/RangeCalendar.test.tsx @@ -10,7 +10,15 @@ * governing permissions and limitations under the License. */ -import {act, pointerMap, render, within} from '@react-spectrum/test-utils-internal'; +import { + act, + createShadowRoot, + fireEvent, + installPointerEvent, + pointerMap, + render, + within +} from '@react-spectrum/test-utils-internal'; import {Button} from '../src/Button'; import { @@ -34,6 +42,7 @@ import { today } from '@internationalized/date'; import {DateValue} from 'react-stately/useRangeCalendarState'; +import {enableShadowDOM} from 'react-stately/private/flags/flags'; import {RangeValue} from '@react-types/shared'; import React from 'react'; import userEvent from '@testing-library/user-event'; @@ -666,4 +675,84 @@ describe('RangeCalendar', () => { let heading = tree.container.querySelector('.react-aria-CalendarHeading'); expect(heading).toHaveTextContent('April 1 – 2, 2026'); }); + + describe('shadow DOM', () => { + installPointerEvent(); + + beforeAll(() => { + enableShadowDOM(); + }); + + let pointerOpts = {pointerType: 'mouse', pointerId: 1, width: 1, height: 1, detail: 1}; + let clickCell = (cell: Element) => { + fireEvent.pointerDown(cell, pointerOpts); + fireEvent.pointerUp(cell, pointerOpts); + fireEvent.click(cell, {detail: 1}); + }; + + it('should support selecting a range by clicking two dates', () => { + let {shadowRoot, cleanup} = createShadowRoot(); + let container = document.createElement('div'); + shadowRoot.appendChild(container); + let onChange = jest.fn(); + render( + , + {container} + ); + + let grid = shadowRoot.querySelector('[role="grid"]')!; + let startCell = within(grid).getByText('17'); + clickCell(startCell); + + // The window pointerup listener receives an event retargeted to the shadow host. + // It must resolve the real target and not treat the click as a release outside + // the calendar, which would commit the selection early. + expect(startCell).toHaveAttribute('data-selection-start', 'true'); + expect(startCell).toHaveAttribute('data-selection-end', 'true'); + expect(onChange).not.toHaveBeenCalled(); + + let endCell = within(grid).getByText('23'); + clickCell(endCell); + + expect(startCell).toHaveAttribute('data-selection-start', 'true'); + expect(endCell).toHaveAttribute('data-selection-end', 'true'); + expect(onChange).toHaveBeenCalledTimes(1); + let {start, end} = onChange.mock.calls[0][0]; + expect(start).toEqual(new CalendarDate(2019, 6, 17)); + expect(end).toEqual(new CalendarDate(2019, 6, 23)); + + cleanup(); + }); + + it('should commit the selection when releasing a drag outside the calendar', () => { + let {shadowRoot, cleanup} = createShadowRoot(); + let container = document.createElement('div'); + shadowRoot.appendChild(container); + let onChange = jest.fn(); + render( + , + {container} + ); + + let grid = shadowRoot.querySelector('[role="grid"]')!; + fireEvent.pointerDown(within(grid).getByText('17'), pointerOpts); + fireEvent.pointerEnter(within(grid).getByText('23')); + expect(onChange).not.toHaveBeenCalled(); + + // Guards the inverse path: resolving the real target must not make releases + // outside the shadow root look like they are inside the calendar. + fireEvent.pointerUp(document.body, pointerOpts); + + expect(onChange).toHaveBeenCalledTimes(1); + let {start, end} = onChange.mock.calls[0][0]; + expect(start).toEqual(new CalendarDate(2019, 6, 17)); + expect(end).toEqual(new CalendarDate(2019, 6, 23)); + + cleanup(); + }); + }); }); diff --git a/packages/react-aria/src/calendar/useRangeCalendar.ts b/packages/react-aria/src/calendar/useRangeCalendar.ts index 7708c00c56d..6aa7a1e9e93 100644 --- a/packages/react-aria/src/calendar/useRangeCalendar.ts +++ b/packages/react-aria/src/calendar/useRangeCalendar.ts @@ -13,7 +13,7 @@ import {AriaLabelingProps, DOMProps, FocusableElement, RefObject} from '@react-types/shared'; import {CalendarAria, useCalendarBase} from './useCalendarBase'; import {DateValue, RangeCalendarState} from 'react-stately/useRangeCalendarState'; -import {isFocusWithin, nodeContains} from '../utils/shadowdom/DOMFunctions'; +import {getEventTarget, isFocusWithin, nodeContains} from '../utils/shadowdom/DOMFunctions'; import {RangeCalendarProps} from 'react-stately/useRangeCalendarState'; import {useEvent} from '../utils/useEvent'; import {useRef} from 'react'; @@ -76,7 +76,7 @@ export function useRangeCalendar( return; } - let target = e.target as Element; + let target = getEventTarget(e) as Element; if ( ref.current && isFocusWithin(ref.current) &&