Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b3e18bc
chore: additional shadow dom tests
snowystinger Feb 10, 2026
67ab690
test from 8806
snowystinger Feb 10, 2026
baff7aa
fix lint
snowystinger Feb 10, 2026
3fb01ce
from 7751
snowystinger Feb 10, 2026
89a2531
Add storybook story
snowystinger Feb 11, 2026
ccbfa4b
disable failing test for the moment so i get a build
snowystinger Feb 12, 2026
2ae2e90
skip other react 16 failure
snowystinger Feb 12, 2026
a57fef8
skip next react 16 failure
snowystinger Feb 12, 2026
02dc628
add combobox to the story
snowystinger Feb 12, 2026
6aa4a70
fix: combobox interactoutside
snowystinger Feb 12, 2026
139b202
fix dependencies
snowystinger Feb 12, 2026
a2a5116
fix lint
snowystinger Feb 12, 2026
d021c96
fix focus move to input
snowystinger Feb 12, 2026
f52affe
fix the non-shadow case again
snowystinger Feb 12, 2026
ff5ce98
Merge branch 'main' into additional-shadow-dom-tests
snowystinger Feb 12, 2026
ed13ba2
Add all of our S2 components so we can test manually
snowystinger Feb 13, 2026
4dee5ed
Merge branch 'main' into additional-shadow-dom-tests
snowystinger Feb 16, 2026
e9b5172
skip react 16 tests since it doesn't support shadow dom well with the…
snowystinger Feb 16, 2026
20a6537
fix lint
snowystinger Feb 16, 2026
af1f2cf
Merge branch 'main' into additional-shadow-dom-tests
snowystinger Mar 26, 2026
5b4a808
fix merge
snowystinger Mar 26, 2026
7952a83
remove skip and explain
snowystinger Mar 26, 2026
579ec2e
There's not enough support in 16/17 for this
snowystinger Mar 26, 2026
a571c1d
Merge branch 'main' into additional-shadow-dom-tests
snowystinger Mar 31, 2026
c1d46ba
add story with two shadow root setup, app + portal
snowystinger Mar 31, 2026
d6ed148
Merge branch 'main' into additional-shadow-dom-tests
snowystinger Aug 7, 2026
f29db05
add browser test for non-testable in jsdom
snowystinger Aug 7, 2026
415bea4
fix up more tests
snowystinger Aug 7, 2026
ff5313c
split test files so i don't unintentionally enable shadow dom for tests
snowystinger Aug 7, 2026
b22670f
cleanup
snowystinger Aug 7, 2026
0fab77b
Add DateRangePicker test and fix
snowystinger Aug 7, 2026
8d016d8
fix lint and tests
snowystinger Aug 7, 2026
60a137a
Add more tests
snowystinger Aug 10, 2026
ed37a15
don't run range calendar shadow dom test in 16, it's not supported an…
snowystinger Aug 12, 2026
62ba816
Merge branch 'main' into additional-shadow-dom-tests
snowystinger Aug 12, 2026
4b85559
Merge branch 'main' into additional-shadow-dom-tests
snowystinger Aug 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
731 changes: 731 additions & 0 deletions packages/@react-spectrum/s2/stories/ShadowDOM.stories.tsx

Large diffs are not rendered by default.

90 changes: 90 additions & 0 deletions packages/@react-spectrum/s2/test/DateRangePicker.browser.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright 2026 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import '../src/page';

import {createRoot} from 'react-dom/client';
import {DateRangePicker} from '../src/DateRangePicker';
import {enableShadowDOM} from 'react-stately/private/flags/flags';
import {expect, it, vi} from 'vitest';
import {parseDate} from '@internationalized/date';
import {Provider} from '../src/Provider';
import React from 'react';
import {UNSAFE_PortalProvider} from 'react-aria/PortalProvider';
import {userEvent} from 'vitest/browser';

// Must be enabled before mounting. This flag is one-way and cannot be turned off.
enableShadowDOM();

// Firefox has a bug that leaks a focus event and causes another test to fail.
let isFirefox = /firefox/i.test(navigator.userAgent);

it.skipIf(isFirefox)('DateRangePicker opens and selects a range inside a shadow root', async () => {
let onChange = vi.fn();

let host = document.createElement('div');
document.body.appendChild(host);
let shadowRoot = host.attachShadow({mode: 'open'});
let appContainer = document.createElement('div');
shadowRoot.appendChild(appContainer);
// Portal the calendar overlay into the same shadow root (the real web-component scenario),
// rather than letting it default to the light-DOM document.body.
let portal = document.createElement('div');
shadowRoot.appendChild(portal);

// Match on the date portion of a day cell's aria-label so the query is robust to the weekday
// prefix / "selected" suffix. Cells live in the shadow root because of the portal above.
let findDay = (dateText: string) =>
Array.from(shadowRoot.querySelectorAll('[role="button"]')).find(el =>
el.getAttribute('aria-label')?.includes(dateText)
) as HTMLElement | undefined;

let root = createRoot(appContainer);
root.render(
<Provider locale="en-US" colorScheme="light">
<UNSAFE_PortalProvider getContainer={() => portal}>
{/* A fixed defaultValue pins the visible month to January 2024 so the test is
deterministic regardless of today's date. */}
<DateRangePicker
label="Reservation dates"
defaultValue={{start: parseDate('2024-01-10'), end: parseDate('2024-01-15')}}
onChange={onChange}
/>
</UNSAFE_PortalProvider>
</Provider>
);

// Open the calendar via its trigger button.
await expect.poll(() => shadowRoot.querySelector('button[aria-label="Calendar"]')).not.toBeNull();
let calendarButton = shadowRoot.querySelector(
'button[aria-label="Calendar"]'
) as HTMLButtonElement;
await userEvent.click(calendarButton);

await expect.poll(() => shadowRoot.querySelector('[role="grid"]')).not.toBeNull();

// Select a new range: click the start day, then the end day (both in the visible January 2024).
await expect.poll(() => findDay('January 20, 2024')).toBeTruthy();
await userEvent.click(findDay('January 20, 2024')!);

await expect.poll(() => findDay('January 25, 2024')).toBeTruthy();
await userEvent.click(findDay('January 25, 2024')!);

// The newly selected range should be committed.
await expect.poll(() => onChange.mock.calls.length).toBeGreaterThan(0);
let selected = onChange.mock.calls.at(-1)![0];
expect(selected.start.toString()).toBe('2024-01-20');
expect(selected.end.toString()).toBe('2024-01-25');

root.unmount();
document.body.removeChild(host);
});
275 changes: 275 additions & 0 deletions packages/react-aria-components/test/RangeCalendar.shadow.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
/*
* Copyright 2026 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import {
act,
createShadowRoot,
fireEvent,
installPointerEvent,
render,
within
} from '@react-spectrum/test-utils-internal';
import {Button} from '../src/Button';
import {CalendarCell, CalendarGrid, CalendarHeading, RangeCalendar} from '../src/Calendar';
import {CalendarDate} from '@internationalized/date';
import {enableShadowDOM} from 'react-stately/private/flags/flags';
import React from 'react';

let TestCalendar = props => (
<RangeCalendar aria-label="Trip dates" {...props}>
<header>
<Button slot="previous">◀</Button>
<CalendarHeading />
<Button slot="next">▶</Button>
</header>
<CalendarGrid>{date => <CalendarCell date={date} />}</CalendarGrid>
</RangeCalendar>
);

if (parseInt(React.version, 10) >= 17) {
describe('RangeCalendar shadow DOM', () => {
installPointerEvent();

beforeAll(() => {
enableShadowDOM();
});

let pointerOpts = {
pointerType: 'mouse',
pointerId: 1,
width: 1,
height: 1,
detail: 1,
pressure: 0.5
};
let pointerClick = (element: Element) => {
fireEvent.pointerDown(element, pointerOpts);
fireEvent.pointerUp(element, pointerOpts);
fireEvent.click(element, {detail: 1});
};

let renderInShadowRoot = (calendarProps = {}, attachTo?: HTMLElement) => {
let {shadowRoot, cleanup} = createShadowRoot(attachTo);
let container = document.createElement('div');
shadowRoot.appendChild(container);
let onChange = jest.fn();
render(
<TestCalendar
onChange={onChange}
defaultFocusedValue={new CalendarDate(2019, 6, 5)}
{...calendarProps}
/>,
{container}
);

return {
onChange,
shadowRoot,
cleanup,
calendar: shadowRoot.querySelector<HTMLElement>('[role="application"]')!,
grid: shadowRoot.querySelector<HTMLElement>('[role="grid"]')!
};
};

it('should support selecting a range by clicking two dates', () => {
let {grid, onChange, cleanup} = renderInShadowRoot();

let startCell = within(grid).getByText('17');
pointerClick(startCell);

expect(startCell).toHaveAttribute('data-selection-start', 'true');
expect(startCell).toHaveAttribute('data-selection-end', 'true');
expect(onChange).not.toHaveBeenCalled();

let endCell = within(grid).getByText('23');
pointerClick(endCell);

expect(startCell).toHaveAttribute('data-selection-start', 'true');
expect(endCell).toHaveAttribute('data-selection-end', 'true');
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith({
start: new CalendarDate(2019, 6, 17),
end: new CalendarDate(2019, 6, 23)
});

cleanup();
});

it('should support selecting a range by dragging', () => {
let {grid, onChange, cleanup} = renderInShadowRoot();

fireEvent.pointerDown(within(grid).getByText('17'), pointerOpts);
fireEvent.pointerLeave(within(grid).getByText('17'), pointerOpts);
fireEvent.pointerEnter(within(grid).getByText('20'), pointerOpts);
fireEvent.pointerLeave(within(grid).getByText('20'), pointerOpts);
fireEvent.pointerEnter(within(grid).getByText('23'), pointerOpts);
expect(onChange).not.toHaveBeenCalled();

let endCell = within(grid).getByText('23');
fireEvent.pointerUp(endCell, pointerOpts);
fireEvent.click(endCell, {detail: 1});

expect(within(grid).getByText('17')).toHaveAttribute('data-selection-start', 'true');
expect(endCell).toHaveAttribute('data-selection-end', 'true');
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith({
start: new CalendarDate(2019, 6, 17),
end: new CalendarDate(2019, 6, 23)
});

cleanup();
});

it('should not commit the selection when pressing the month navigation buttons', () => {
let {calendar, grid, onChange, cleanup} = renderInShadowRoot();

pointerClick(within(grid).getByText('17'));
expect(onChange).not.toHaveBeenCalled();

pointerClick(within(calendar).getAllByRole('button', {name: /Next/i})[0]);
expect(onChange).not.toHaveBeenCalled();

pointerClick(within(grid).getByText('5'));

expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith({
start: new CalendarDate(2019, 6, 17),
end: new CalendarDate(2019, 7, 5)
});

cleanup();
});

it('should not clear the selection when clicking a date with commitBehavior="clear"', () => {
let {grid, onChange, cleanup} = renderInShadowRoot({
commitBehavior: 'clear',
defaultValue: {start: new CalendarDate(2019, 6, 10), end: new CalendarDate(2019, 6, 20)}
});

let startCell = within(grid).getByText('17');
pointerClick(startCell);

expect(startCell).toHaveAttribute('data-selection-start', 'true');
expect(onChange).not.toHaveBeenCalled();

pointerClick(within(grid).getByText('23'));

expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith({
start: new CalendarDate(2019, 6, 17),
end: new CalendarDate(2019, 6, 23)
});

cleanup();
});

it('should support selecting a range inside nested shadow roots', () => {
let outer = createShadowRoot();
let wrapper = document.createElement('div');
outer.shadowRoot.appendChild(wrapper);
let {grid, onChange, cleanup} = renderInShadowRoot({}, wrapper);

pointerClick(within(grid).getByText('17'));
expect(onChange).not.toHaveBeenCalled();

pointerClick(within(grid).getByText('23'));

expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith({
start: new CalendarDate(2019, 6, 17),
end: new CalendarDate(2019, 6, 23)
});

cleanup();
outer.cleanup();
});

it('should commit the selection when tabbing away mid selection', () => {
let {shadowRoot, grid, onChange, cleanup} = renderInShadowRoot();
let outsideButton = document.createElement('button');
document.body.appendChild(outsideButton);

pointerClick(within(grid).getByText('17'));
fireEvent.pointerLeave(within(grid).getByText('17'), pointerOpts);
fireEvent.pointerEnter(within(grid).getByText('23'), pointerOpts);
expect(onChange).not.toHaveBeenCalled();

// userEvent's tab doesn't work in shadow, so fire the focus/blur events the browser
// would. The focused cell blurs with the outside button as relatedTarget, and the button
// takes focus. The blur path commits via relatedTarget rather than the pointerup target,
// so it must still resolve the outside control as outside the calendar across the boundary.
let focusedCell = shadowRoot.activeElement!;
fireEvent.keyDown(focusedCell, {key: 'Tab'});
act(() => {
outsideButton.focus();
});
fireEvent.keyUp(outsideButton, {key: 'Tab'});

expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith({
start: new CalendarDate(2019, 6, 17),
end: new CalendarDate(2019, 6, 23)
});

document.body.removeChild(outsideButton);
cleanup();
});

it('should commit the selection when releasing a drag outside the calendar', () => {
let {grid, onChange, cleanup} = renderInShadowRoot();

fireEvent.pointerDown(within(grid).getByText('17'), pointerOpts);
fireEvent.pointerLeave(within(grid).getByText('17'), pointerOpts);
fireEvent.pointerEnter(within(grid).getByText('23'), pointerOpts);
fireEvent.pointerLeave(within(grid).getByText('23'), pointerOpts);
expect(onChange).not.toHaveBeenCalled();

fireEvent.pointerUp(document.body, pointerOpts);

expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith({
start: new CalendarDate(2019, 6, 17),
end: new CalendarDate(2019, 6, 23)
});

cleanup();
});

it('should commit the selection when releasing a drag outside the calendar but inside the shadow root', () => {
let {shadowRoot, grid, onChange, cleanup} = renderInShadowRoot();
let sibling = document.createElement('div');
shadowRoot.appendChild(sibling);

fireEvent.pointerDown(within(grid).getByText('17'), pointerOpts);
fireEvent.pointerLeave(within(grid).getByText('17'), pointerOpts);
fireEvent.pointerEnter(within(grid).getByText('23'), pointerOpts);
fireEvent.pointerLeave(within(grid).getByText('23'), pointerOpts);
expect(onChange).not.toHaveBeenCalled();

fireEvent.pointerUp(sibling, pointerOpts);

expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith({
start: new CalendarDate(2019, 6, 17),
end: new CalendarDate(2019, 6, 23)
});

cleanup();
});
});
} else {
describe('RangeCalendar shadow DOM', () => {
it('should not run tests in React 16, we do not support it anyways', () => {
expect(true).toBe(true);
});
});
}
Loading