Skip to content

RangeCalendar inside shadow DOM commits range on first click (endDragging uses retargeted e.target) #10330

Description

@eliezzer

Provide a general summary of the issue here

When a RangeCalendar (including the calendar inside DateRangePicker from react-aria-components) is rendered inside a shadow root, clicking a single date immediately commits a range where start === end. It is impossible to select a range with two separate clicks.

The enableShadowDOM() flag is enabled and event retargeting is handled correctly in most of the codebase, but one line in useRangeCalendar still reads the raw e.target from a window-level listener.

🤔 Expected Behavior?

First click sets the anchor (start) date; second click on another day commits the range start → end — same behavior as in light DOM.

😯 Current Behavior

First click immediately commits { start: clickedDate, end: clickedDate }. Every subsequent click does the same, so a multi-day range can never be selected by clicking.

Drag-selection coincidentally still works, because the unintended commit on pointerup happens to be exactly what drag-selection wants.

💁 Possible Solution

useRangeCalendar registers a window-level pointerup listener (endDragging) to support drag selection:

// packages/react-aria/src/calendar/useRangeCalendar.ts
let endDragging = (e: PointerEvent) => {
  // ...
  state.setDragging(false);
  if (!state.anchorDate) {
    return;
  }
  let target = e.target as Element;
  if (ref.current && isFocusWithin(ref.current) && (!nodeContains(ref.current, target) || !target.closest('button, [role="button"]'))) {
    commitBehaviorMapping[commitBehavior]();
  }
};

For a listener attached to window, events that originate inside a shadow root are retargeted: e.target is the shadow host, not the actual calendar cell button. Therefore nodeContains(ref.current, target) is always false, the hook believes the pointer was released outside the calendar, and commitSelection() runs on every single click — committing start === end right after the first click.

One-line fix — read the real target through the composed path:

let target = (typeof e.composedPath === 'function' ? e.composedPath()[0] : e.target) as Element;

(or use the existing getEventTarget shadow-DOM helper used elsewhere in the codebase.)

🔦 Context

We render React micro-frontends inside a shadow root (to isolate them from a host application's global CSS), with the enableShadowDOM() flag enabled via @react-stately/flags. Everything else in react-aria behaves correctly in this setup (focus management, presses, overlays via UNSAFE_PortalProvider) — only range selection by click is broken. We are currently shipping the one-line fix above via patch-package.

🖥️ Steps to Reproduce

  1. Enable shadow DOM support: import {enableShadowDOM} from '@react-stately/flags'; enableShadowDOM();
  2. Attach a shadow root to a host element and render a RangeCalendar inside it with createRoot(shadowRootContainer).render(...).
  3. Click one day in the calendar.
  4. Observe onChange fires immediately with start === end. Clicking a second day never extends the range.

Minimal repro sketch:

import {enableShadowDOM} from '@react-stately/flags';
import {createRoot} from 'react-dom/client';
import {RangeCalendar, Heading, Button, CalendarGrid, CalendarCell} from 'react-aria-components';

enableShadowDOM();

const host = document.getElementById('host');
const shadow = host.attachShadow({mode: 'open'});
const container = document.createElement('div');
shadow.appendChild(container);

createRoot(container).render(
  <RangeCalendar aria-label="Trip dates" onChange={(v) => console.log('committed', v)}>
    <header>
      <Button slot="previous"></Button>
      <Heading />
      <Button slot="next"></Button>
    </header>
    <CalendarGrid>
      {(date) => <CalendarCell date={date} />}
    </CalendarGrid>
  </RangeCalendar>
);

Click any date → committed {start, end} logs immediately with the same date for both.

Version

react-aria 3.50.0 (latest); the unguarded e.target read is still present on current main (packages/react-aria/src/calendar/useRangeCalendar.ts)

What browsers are you seeing the problem on?

Chrome, Firefox, Microsoft Edge

If other, please specify.

No response

What operating system are you using?

Windows 11 (OS-independent)

🧢 Your Company/Team

No response

🕷 Tracking Issue

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions