Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,10 @@
"postcss": "^8.4.24",
"postcss-custom-properties": "^13.2.0",
"postcss-import": "^15.1.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-frame-component": "^5.0.0",
"react-test-renderer": "^19.1.0",
"react-test-renderer": "^19.2.0",
"recast": "^0.23",
"recursive-readdir": "^2.2.2",
"regenerator-runtime": "0.13.3",
Expand Down
17 changes: 12 additions & 5 deletions packages/@react-aria/collections/src/Document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,12 +475,10 @@ export class Document<T, C extends BaseCollection<T> = BaseCollection<T>> extend

/** Finalizes the collection update, updating all nodes and freezing the collection. */
getCollection(): C {
// If in a subscription update, return a clone of the existing collection.
// This ensures React will queue a render. React will call getCollection again
// during render, at which point all the updates will be complete and we can return
// the new collection.
// If in a subscription update, return return the existing collection.
// React will call getCollection again during render, at which point all the updates will be complete.
if (this.inSubscription) {
return this.collection.clone();
return this.collection;
}

// Reset queuedRender to false when getCollection is called during render.
Expand Down Expand Up @@ -540,9 +538,18 @@ export class Document<T, C extends BaseCollection<T> = BaseCollection<T>> extend
// we reset queuedRender back to false.
this.queuedRender = true;
this.inSubscription = true;

// Clone the collection to ensure that React queues a render. It will call getCollection again
// during render, at which point all the updates will be complete and we can return
// the new collection.
if (!this.isSSR) {
this.collection = this.collection.clone();
}

for (let fn of this.subscriptions) {
fn();
}

this.inSubscription = false;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/@react-aria/form/src/useFormValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ function getNativeValidity(input: ValidatableElement): ValidationResult {
function getFirstInvalidInput(form: HTMLFormElement): ValidatableElement | null {
for (let i = 0; i < form.elements.length; i++) {
let element = form.elements[i] as ValidatableElement;
if (!element.validity.valid) {
if (element.validity?.valid === false) {
return element;
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-aria/interactions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export type {HoverProps, HoverResult} from './useHover';
export type {InteractOutsideProps} from './useInteractOutside';
export type {KeyboardProps, KeyboardResult} from './useKeyboard';
export type {PressProps, PressHookProps, PressResult} from './usePress';
export type {PressEvent, PressEvents, MoveStartEvent, MoveMoveEvent, MoveEndEvent, MoveEvents, HoverEvent, HoverEvents, FocusEvents, KeyboardEvents} from '@react-types/shared';
export type {PressEvent, PressEvents, LongPressEvent, MoveStartEvent, MoveMoveEvent, MoveEndEvent, MoveEvent, MoveEvents, HoverEvent, HoverEvents, FocusEvents, KeyboardEvents} from '@react-types/shared';
export type {MoveResult} from './useMove';
export type {LongPressProps, LongPressResult} from './useLongPress';
export type {ScrollWheelProps} from './useScrollWheel';
Expand Down
9 changes: 7 additions & 2 deletions packages/@react-aria/utils/src/useViewportSize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* governing permissions and limitations under the License.
*/

import {isIOS} from './platform';
import {useEffect, useState} from 'react';
import {useIsSSR} from '@react-aria/ssr';
import {willOpenKeyboard} from './keyboard';
Expand Down Expand Up @@ -65,7 +66,9 @@ export function useViewportSize(): ViewportSize {

updateSize(getViewportSize());

window.addEventListener('blur', onBlur, true);
if (isIOS()) {
window.addEventListener('blur', onBlur, true);
}

if (!visualViewport) {
window.addEventListener('resize', onResize);
Expand All @@ -75,7 +78,9 @@ export function useViewportSize(): ViewportSize {

return () => {
cancelAnimationFrame(frame);
window.removeEventListener('blur', onBlur, true);
if (isIOS()) {
window.removeEventListener('blur', onBlur, true);
}
if (!visualViewport) {
window.removeEventListener('resize', onResize);
} else {
Expand Down
38 changes: 38 additions & 0 deletions packages/@react-aria/utils/stories/useViewportSize.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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 {Meta} from '@storybook/react';
import React from 'react';
import {useViewportSize} from '@react-aria/utils';

export default {
title: 'useViewportSize',
component: Example
} as Meta<typeof Example>;

export function Example() {
const viewportSize = useViewportSize();

return (
<div>
{JSON.stringify(viewportSize)}
<input />
<div style={{height: '200vh'}} />
</div>
);
}

Example.parameters = {
description: {
data: 'Clicking the input and then clicking outside should not cause the viewport size to change.'
}
};
4 changes: 2 additions & 2 deletions packages/dev/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
"highlight.js": "9.18.1",
"markdown-to-jsx": "^6.11.0",
"quicklink": "^2.3.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-lowlight": "^2.0.0"
},
"peerDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions packages/dev/s2-docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@
"json5": "^2.2.3",
"lz-string": "^1.5.0",
"markdown-to-jsx": "^6.11.0",
"react": "^19",
"react": "^19.2.0",
"react-aria": "^3.40.0",
"react-aria-components": "^1.7.1",
"react-dom": "^19",
"react-dom": "^19.2.0",
"react-stately": "^3.38.0",
"remark-mdx": "^3.1.0",
"remark-parse": "^11.0.0",
Expand Down
46 changes: 46 additions & 0 deletions packages/react-aria-components/test/Form.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,50 @@ describe('Form', () => {
let form = getByTestId('form');
expect(form).toHaveAttribute('data-custom', 'true');
});

it('should not throw when form contains elements without validity property', async () => {
function Test() {
return (
<Form data-testid="form">
<TextField name="name" isRequired>
<Label>Name</Label>
<Input />
<FieldError />
</TextField>
<Button type="submit">Submit</Button>
</Form>
);
}

let {getByTestId, getByRole} = render(<Test />);
let form = getByTestId('form');
let input = getByRole('textbox');

// Mock form.elements to include an element without validity property (simulates Web Component)
let originalElements = form.elements;
let mockElement = {name: 'custom-element'}; // No validity property
Object.defineProperty(form, 'elements', {
get: () => ({
length: originalElements.length + 1,
[Symbol.iterator]: function* () {
yield mockElement;
yield* originalElements;
},
0: mockElement,
...Array.from(originalElements).reduce((acc, el, i) => ({...acc, [i + 1]: el}), {})
}),
configurable: true
});

// Should not throw when iterating form.elements with element lacking validity
await user.click(getByRole('button'));

expect(document.activeElement).toBe(input);

// Restore original elements
Object.defineProperty(form, 'elements', {
get: () => originalElements,
configurable: true
});
});
});
36 changes: 36 additions & 0 deletions packages/react-aria-components/test/ListBox.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1948,4 +1948,40 @@ describe('ListBox', () => {
expect(onClick).toHaveBeenCalledTimes(1);
});
});

if (React.version.startsWith('19')) {
it('supports Activity', async () => {
function ActivityListBox() {
let [mode, setMode] = React.useState('hidden');

return (
<>
<Button onPress={() => setMode(mode === 'hidden' ? 'visible' : 'hidden')}>
Set {mode === 'hidden' ? 'visible' : 'hidden'}
</Button>

<React.Activity mode={mode}>
<p>List should be visible</p>

<ListBox aria-label="Activity Listbox">
<ListBoxItem>Item 1</ListBoxItem>
<ListBoxItem>Item 2</ListBoxItem>
<ListBoxItem>Item 3</ListBoxItem>
<ListBoxItem>Item 4</ListBoxItem>
<ListBoxItem>Item 5</ListBoxItem>
</ListBox>
</React.Activity>
</>
);
}

let {getAllByRole, getByRole, queryAllByRole} = render(<ActivityListBox />);
let button = getByRole('button');
expect(queryAllByRole('option')).toHaveLength(0);
await user.click(button);
expect(getAllByRole('option')).toHaveLength(5);
await user.click(button);
expect(queryAllByRole('option')).toHaveLength(0);
});
}
});
38 changes: 38 additions & 0 deletions packages/react-aria-components/test/Tabs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -729,4 +729,42 @@ describe('Tabs', () => {
expect(onClick).toHaveBeenCalledTimes(1);
});
});

if (React.version.startsWith('19')) {
it('supports Activity', async () => {
function ActivityTabs() {
let [mode, setMode] = React.useState('hidden');

return (
<>
<Button onPress={() => setMode(mode === 'hidden' ? 'visible' : 'hidden')}>
Set {mode === 'hidden' ? 'visible' : 'hidden'}
</Button>
<React.Activity mode={mode}>
<Tabs>
<TabList aria-label="Test">
<Tab id="a">A</Tab>
<Tab id="b">B</Tab>
<TooltipTrigger>
<Tab id="c">C</Tab>
<Tooltip>Test</Tooltip>
</TooltipTrigger>
</TabList>
<TabPanel id="a">A</TabPanel>
<TabPanel id="b">B</TabPanel>
<TabPanel id="c">C</TabPanel>
</Tabs>
</React.Activity>
</>
);
}

let {getByRole, getAllByRole, queryAllByRole} = render(<ActivityTabs />);

let button = getByRole('button');
expect(queryAllByRole('tab')).toHaveLength(0);
await user.click(button);
expect(getAllByRole('tab')).toHaveLength(3);
});
}
});
2 changes: 1 addition & 1 deletion packages/react-aria/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export type {DisclosureAria, AriaDisclosureProps} from '@react-aria/disclosure';
export type {AriaFocusRingProps, FocusableAria, FocusableOptions, FocusManager, FocusManagerOptions, FocusRingAria, FocusRingProps, FocusScopeProps} from '@react-aria/focus';
export type {DateFormatter, DateFormatterOptions, Filter, FormatMessage, I18nProviderProps, Locale, LocalizedStringFormatter, LocalizedStrings} from '@react-aria/i18n';
export type {ClipboardProps, ClipboardResult, DirectoryDropItem, DragEndEvent, DraggableCollectionEndEvent, DraggableCollectionMoveEvent, DraggableCollectionOptions, DraggableCollectionStartEvent, DraggableItemProps, DraggableItemResult, DragItem, DragMoveEvent, DragOptions, DragPreviewProps, DragPreviewRenderer, DragResult, DragStartEvent, DragTypes, DropEnterEvent, DropEvent, DropExitEvent, DropIndicatorAria, DropIndicatorProps, DropItem, DropMoveEvent, DropOperation, DropOptions, DroppableCollectionDropEvent, DroppableCollectionEnterEvent, DroppableCollectionExitEvent, DroppableCollectionInsertDropEvent, DroppableCollectionMoveEvent, DroppableCollectionOnItemDropEvent, DroppableCollectionOptions, DroppableCollectionReorderEvent, DroppableCollectionResult, DroppableCollectionRootDropEvent, DroppableItemOptions, DroppableItemResult, DropPosition, DropResult, DropTarget, DropTargetDelegate, FileDropItem, ItemDropTarget, RootDropTarget, TextDropItem} from '@react-aria/dnd';
export type {FocusProps, FocusResult, FocusVisibleProps, FocusVisibleResult, FocusWithinProps, FocusWithinResult, HoverProps, HoverResult, InteractOutsideProps, KeyboardProps, KeyboardResult, LongPressProps, LongPressResult, MoveEvents, MoveResult, PressHookProps, PressProps, PressResult, ScrollWheelProps, PressEvent, PressEvents, MoveStartEvent, MoveMoveEvent, MoveEndEvent, HoverEvent, HoverEvents, FocusEvents, KeyboardEvents} from '@react-aria/interactions';
export type {FocusProps, FocusResult, FocusVisibleProps, FocusVisibleResult, FocusWithinProps, FocusWithinResult, HoverProps, HoverResult, InteractOutsideProps, KeyboardProps, KeyboardResult, LongPressProps, LongPressResult, MoveEvents, MoveResult, PressHookProps, PressProps, PressResult, ScrollWheelProps, PressEvent, PressEvents, LongPressEvent, MoveStartEvent, MoveMoveEvent, MoveEndEvent, MoveEvent, HoverEvent, HoverEvents, FocusEvents, KeyboardEvents} from '@react-aria/interactions';
export type {AriaFieldProps, FieldAria, LabelAria, LabelAriaProps} from '@react-aria/label';
export type {AriaLandmarkRole, AriaLandmarkProps, LandmarkAria, LandmarkController} from '@react-aria/landmark';
export type {AriaLinkOptions, LinkAria} from '@react-aria/link';
Expand Down
8 changes: 4 additions & 4 deletions starters/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,16 @@
"clsx": "^2.1.1",
"lightningcss-loader": "^2.1.0",
"lucide-react": "^0.517.0",
"react": "^19.1.0",
"react": "^19.2.0",
"react-aria-components": "^1.14.0",
"react-dom": "^19.1.0",
"react-dom": "^19.2.0",
"storybook": "^8.6.14",
"storybook-dark-mode": "^4.0.2",
"typescript": "^5.8.2"
},
"resolutions": {
"react": "19.1.0",
"react-dom": "19.1.0",
"react": "19.2.0",
"react-dom": "19.2.0",
"@types/mime": "3.0.4",
"jackspeak": "2.1.1"
}
Expand Down
Loading
Loading