From b670a536211dd866365bea24d8f74050e57662da Mon Sep 17 00:00:00 2001 From: Menachem Hornbacher Date: Sun, 8 Feb 2026 16:06:20 -0500 Subject: [PATCH 1/4] fix: collections nested within Activities (React 19.2) (#9300) * upgrade to react 19.2 * add failing tests * fix Activity with single re-render on minimal overhead * fix: linter * fix react version * add @storybook/blocks to resolve build issues * normalize imports * diagnose ci issue caused by package changes * add additional logging * fix packages * Simpler fix * fix ssr --------- Co-authored-by: Robert Snow Co-authored-by: Robert Snow Co-authored-by: Devon Govett --- package.json | 6 +-- .../@react-aria/collections/src/Document.ts | 17 +++++-- packages/dev/docs/package.json | 4 +- packages/dev/s2-docs/package.json | 4 +- .../test/ListBox.test.js | 36 ++++++++++++++ .../react-aria-components/test/Tabs.test.js | 38 +++++++++++++++ starters/docs/package.json | 8 ++-- yarn.lock | 48 +++++++++---------- 8 files changed, 121 insertions(+), 40 deletions(-) diff --git a/package.json b/package.json index 9760520038d..a66edf13f43 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/@react-aria/collections/src/Document.ts b/packages/@react-aria/collections/src/Document.ts index dd1dbeb4f36..1bde2a2fdb0 100644 --- a/packages/@react-aria/collections/src/Document.ts +++ b/packages/@react-aria/collections/src/Document.ts @@ -475,12 +475,10 @@ export class Document = BaseCollection> 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. @@ -540,9 +538,18 @@ export class Document = BaseCollection> 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; } diff --git a/packages/dev/docs/package.json b/packages/dev/docs/package.json index 274eb27e9db..d243c0dc6c1 100644 --- a/packages/dev/docs/package.json +++ b/packages/dev/docs/package.json @@ -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": { diff --git a/packages/dev/s2-docs/package.json b/packages/dev/s2-docs/package.json index 1e050fded2f..6190d5f041a 100644 --- a/packages/dev/s2-docs/package.json +++ b/packages/dev/s2-docs/package.json @@ -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", diff --git a/packages/react-aria-components/test/ListBox.test.js b/packages/react-aria-components/test/ListBox.test.js index 5b5c353be4f..197a15e7ad5 100644 --- a/packages/react-aria-components/test/ListBox.test.js +++ b/packages/react-aria-components/test/ListBox.test.js @@ -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 ( + <> + + + +

List should be visible

+ + + Item 1 + Item 2 + Item 3 + Item 4 + Item 5 + +
+ + ); + } + + let {getAllByRole, getByRole, queryAllByRole} = render(); + 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); + }); + } }); diff --git a/packages/react-aria-components/test/Tabs.test.js b/packages/react-aria-components/test/Tabs.test.js index 80c9ebfc2d4..bc18c2d1ee7 100644 --- a/packages/react-aria-components/test/Tabs.test.js +++ b/packages/react-aria-components/test/Tabs.test.js @@ -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 ( + <> + + + + + A + B + + C + Test + + + A + B + C + + + + ); + } + + let {getByRole, getAllByRole, queryAllByRole} = render(); + + let button = getByRole('button'); + expect(queryAllByRole('tab')).toHaveLength(0); + await user.click(button); + expect(getAllByRole('tab')).toHaveLength(3); + }); + } }); diff --git a/starters/docs/package.json b/starters/docs/package.json index 00a78daa303..ca577cdc6bd 100644 --- a/starters/docs/package.json +++ b/starters/docs/package.json @@ -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" } diff --git a/yarn.lock b/yarn.lock index 54984eab68a..09f406000e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6882,8 +6882,8 @@ __metadata: highlight.js: "npm:9.18.1" markdown-to-jsx: "npm:^6.11.0" quicklink: "npm:^2.3.0" - react: "npm:^19.1.0" - react-dom: "npm:^19.1.0" + react: "npm:^19.2.0" + react-dom: "npm:^19.2.0" react-lowlight: "npm:^2.0.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 @@ -7434,10 +7434,10 @@ __metadata: lz-string: "npm:^1.5.0" markdown-to-jsx: "npm:^6.11.0" playwright: "npm:^1.57.0" - react: "npm:^19" + react: "npm:^19.2.0" react-aria: "npm:^3.40.0" react-aria-components: "npm:^1.7.1" - react-dom: "npm:^19" + react-dom: "npm:^19.2.0" react-stately: "npm:^3.38.0" remark-mdx: "npm:^3.1.0" remark-parse: "npm:^11.0.0" @@ -24754,14 +24754,14 @@ __metadata: languageName: node linkType: hard -"react-dom@npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0, react-dom@npm:^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0, react-dom@npm:^19, react-dom@npm:^19.1.0": - version: 19.1.0 - resolution: "react-dom@npm:19.1.0" +"react-dom@npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0, react-dom@npm:^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0, react-dom@npm:^19.2.0": + version: 19.2.1 + resolution: "react-dom@npm:19.2.1" dependencies: - scheduler: "npm:^0.26.0" + scheduler: "npm:^0.27.0" peerDependencies: - react: ^19.1.0 - checksum: 10c0/3e26e89bb6c67c9a6aa86cb888c7a7f8258f2e347a6d2a15299c17eb16e04c19194e3452bc3255bd34000a61e45e2cb51e46292392340432f133e5a5d2dfb5fc + react: ^19.2.1 + checksum: 10c0/e56b6b3d72314df580ca800b70a69a21c6372703c8f45d9b5451ca6519faefb2496d76ffa9c5adb94136d2bbf2fd303d0dfc208a2cd77ede3132877471af9470 languageName: node linkType: hard @@ -24948,10 +24948,10 @@ __metadata: postcss: "npm:^8.4.24" postcss-custom-properties: "npm:^13.2.0" postcss-import: "npm:^15.1.0" - react: "npm:^19.1.0" - react-dom: "npm:^19.1.0" + react: "npm:^19.2.0" + react-dom: "npm:^19.2.0" react-frame-component: "npm:^5.0.0" - react-test-renderer: "npm:^19.1.0" + react-test-renderer: "npm:^19.2.0" recast: "npm:^0.23" recursive-readdir: "npm:^2.2.2" regenerator-runtime: "npm:0.13.3" @@ -25037,17 +25037,10 @@ __metadata: languageName: node linkType: hard -"react@npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0, react@npm:^19, react@npm:^19.1.0": - version: 19.1.0 - resolution: "react@npm:19.1.0" - checksum: 10c0/530fb9a62237d54137a13d2cfb67a7db6a2156faed43eecc423f4713d9b20c6f2728b026b45e28fcd72e8eadb9e9ed4b089e99f5e295d2f0ad3134251bdd3698 - languageName: node - linkType: hard - -"react@npm:^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0": - version: 19.1.1 - resolution: "react@npm:19.1.1" - checksum: 10c0/8c9769a2dfd02e603af6445058325e6c8a24b47b185d0e461f66a6454765ddcaecb3f0a90184836c68bb509f3c38248359edbc42f0d07c23eb500a5c30c87b4e +"react@npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0, react@npm:^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0, react@npm:^19.2.0": + version: 19.2.1 + resolution: "react@npm:19.2.1" + checksum: 10c0/2b5eaf407abb3db84090434c20d6c5a8e447ab7abcd8fe9eaf1ddc299babcf31284ee9db7ea5671d21c85ac5298bd632fa1a7da1ed78d5b368a537f5e1cd5d62 languageName: node linkType: hard @@ -26032,6 +26025,13 @@ __metadata: languageName: node linkType: hard +"scheduler@npm:^0.27.0": + version: 0.27.0 + resolution: "scheduler@npm:0.27.0" + checksum: 10c0/4f03048cb05a3c8fddc45813052251eca00688f413a3cee236d984a161da28db28ba71bd11e7a3dd02f7af84ab28d39fb311431d3b3772fed557945beb00c452 + languageName: node + linkType: hard + "section-matter@npm:^1.0.0": version: 1.0.0 resolution: "section-matter@npm:1.0.0" From bd2bdf74c907c8d2d22a4649796e75c059e00243 Mon Sep 17 00:00:00 2001 From: Poetry Of Code <101225909+poetryofcode@users.noreply.github.com> Date: Sun, 8 Feb 2026 16:33:11 -0500 Subject: [PATCH 2/4] fix: export LongPressEvent and MoveEvent types (#9522) Add missing type exports for LongPressEvent and MoveEvent from @react-aria/interactions and react-aria packages. These event types are defined in @react-types/shared but were not being re-exported, unlike similar event types (PressEvent, HoverEvent, MoveStartEvent, etc.). Fixes #9230 Co-authored-by: Claude Opus 4.5 --- packages/@react-aria/interactions/src/index.ts | 2 +- packages/react-aria/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@react-aria/interactions/src/index.ts b/packages/@react-aria/interactions/src/index.ts index 4e56054350a..421358ab7af 100644 --- a/packages/@react-aria/interactions/src/index.ts +++ b/packages/@react-aria/interactions/src/index.ts @@ -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'; diff --git a/packages/react-aria/src/index.ts b/packages/react-aria/src/index.ts index 5a9339044d4..93d543a1461 100644 --- a/packages/react-aria/src/index.ts +++ b/packages/react-aria/src/index.ts @@ -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'; From ce01db528cb97badfe1093f0fa8e820a1d93d2d2 Mon Sep 17 00:00:00 2001 From: XiaoYan Li Date: Mon, 9 Feb 2026 05:55:29 +0800 Subject: [PATCH 3/4] fix: only handle blur event for resizing viewport on iOS (#9393) * fix: only handle blur event for resizing viewport on iOS * Add a testing story * fix lint --------- Co-authored-by: Robert Snow --- .../@react-aria/utils/src/useViewportSize.ts | 9 ++++- .../utils/stories/useViewportSize.stories.tsx | 38 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 packages/@react-aria/utils/stories/useViewportSize.stories.tsx diff --git a/packages/@react-aria/utils/src/useViewportSize.ts b/packages/@react-aria/utils/src/useViewportSize.ts index b98ae0fc4e9..3c362b43efb 100644 --- a/packages/@react-aria/utils/src/useViewportSize.ts +++ b/packages/@react-aria/utils/src/useViewportSize.ts @@ -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'; @@ -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); @@ -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 { diff --git a/packages/@react-aria/utils/stories/useViewportSize.stories.tsx b/packages/@react-aria/utils/stories/useViewportSize.stories.tsx new file mode 100644 index 00000000000..e66bd12f629 --- /dev/null +++ b/packages/@react-aria/utils/stories/useViewportSize.stories.tsx @@ -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; + +export function Example() { + const viewportSize = useViewportSize(); + + return ( +
+ {JSON.stringify(viewportSize)} + +
+
+ ); +} + +Example.parameters = { + description: { + data: 'Clicking the input and then clicking outside should not cause the viewport size to change.' + } +}; From 6e9ba6ea22082f6469624ffc24828c5f23ab6a6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sonsu/=EC=9D=B4=EC=84=B1=EC=88=98?= <127682098+sonsu-lee@users.noreply.github.com> Date: Mon, 9 Feb 2026 06:55:52 +0900 Subject: [PATCH 4/4] fix(form): prevent TypeError when form contains elements without validity (#9614) --- .../@react-aria/form/src/useFormValidation.ts | 2 +- .../react-aria-components/test/Form.test.js | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/packages/@react-aria/form/src/useFormValidation.ts b/packages/@react-aria/form/src/useFormValidation.ts index b4699bf8046..707984cbdac 100644 --- a/packages/@react-aria/form/src/useFormValidation.ts +++ b/packages/@react-aria/form/src/useFormValidation.ts @@ -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; } } diff --git a/packages/react-aria-components/test/Form.test.js b/packages/react-aria-components/test/Form.test.js index ad0d3642f91..b2b931f2e11 100644 --- a/packages/react-aria-components/test/Form.test.js +++ b/packages/react-aria-components/test/Form.test.js @@ -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 ( +
+ + + + + + +
+ ); + } + + let {getByTestId, getByRole} = render(); + 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 + }); + }); });