From f738a3855ba0812c98e7ceb44a1badde87b94040 Mon Sep 17 00:00:00 2001 From: Matt Vague Date: Mon, 26 Jan 2026 10:45:52 -0800 Subject: [PATCH 1/4] fix: Support range sliders in usePreventScroll on iOS (#9506) * Support range sliders in usePreventScroll on iOS * Add usePreventScroll range input --- packages/@react-aria/overlays/src/usePreventScroll.ts | 8 ++++++++ .../overlays/stories/usePreventScroll.stories.tsx | 3 +++ 2 files changed, 11 insertions(+) diff --git a/packages/@react-aria/overlays/src/usePreventScroll.ts b/packages/@react-aria/overlays/src/usePreventScroll.ts index 6c4b7ee5772..c8687d33b97 100644 --- a/packages/@react-aria/overlays/src/usePreventScroll.ts +++ b/packages/@react-aria/overlays/src/usePreventScroll.ts @@ -106,6 +106,14 @@ function preventScrollMobileSafari() { allowTouchMove = true; } + // If this is a range input, allow touch move to allow user to adjust the slider value + if (e.composedPath().some((el) => + el instanceof HTMLInputElement && + el.type === 'range' + )) { + allowTouchMove = true; + } + // If this is a focused input element with a selected range, allow user to drag the selection handles. if ( 'selectionStart' in target && diff --git a/packages/@react-aria/overlays/stories/usePreventScroll.stories.tsx b/packages/@react-aria/overlays/stories/usePreventScroll.stories.tsx index 20a411cedf5..09002fd1e93 100644 --- a/packages/@react-aria/overlays/stories/usePreventScroll.stories.tsx +++ b/packages/@react-aria/overlays/stories/usePreventScroll.stories.tsx @@ -46,6 +46,9 @@ function App(): JSX.Element { Click Me in safari and then scroll + +

Should be able to scroll the range input on iOS Safari

+ ); } From 68a8d1eede07852ef6fc0f4a84b52425ead70c0f Mon Sep 17 00:00:00 2001 From: chirokas <157580465+chirokas@users.noreply.github.com> Date: Tue, 27 Jan 2026 02:54:42 +0800 Subject: [PATCH 2/4] fix: prevent Modal from closing when dismissing DateRangePicker via outside click (#9505) * fix: prevent Modal from closing when dismissing DateRangePicker via outside click * add unit test --- .../@react-aria/overlays/src/useOverlay.ts | 13 ++++-- .../stories/Modal.stories.tsx | 45 +++++++++++++++++++ .../react-aria-components/test/Dialog.test.js | 26 +++++++++++ 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/packages/@react-aria/overlays/src/useOverlay.ts b/packages/@react-aria/overlays/src/useOverlay.ts index 8fdcfa39410..06792f54c07 100644 --- a/packages/@react-aria/overlays/src/useOverlay.ts +++ b/packages/@react-aria/overlays/src/useOverlay.ts @@ -12,7 +12,7 @@ import {DOMAttributes, RefObject} from '@react-types/shared'; import {isElementInChildOfActiveScope} from '@react-aria/focus'; -import {useEffect} from 'react'; +import {useEffect, useRef} from 'react'; import {useFocusWithin, useInteractOutside} from '@react-aria/interactions'; export interface AriaOverlayProps { @@ -70,6 +70,8 @@ export function useOverlay(props: AriaOverlayProps, ref: RefObject>(undefined); + // Add the overlay ref to the stack of visible overlays on mount, and remove on unmount. useEffect(() => { if (isOpen && !visibleOverlays.includes(ref)) { @@ -91,8 +93,10 @@ export function useOverlay(props: AriaOverlayProps, ref: RefObject { + const topMostOverlay = visibleOverlays[visibleOverlays.length - 1]; + lastVisibleOverlay.current = topMostOverlay; if (!shouldCloseOnInteractOutside || shouldCloseOnInteractOutside(e.target as Element)) { - if (visibleOverlays[visibleOverlays.length - 1] === ref) { + if (topMostOverlay === ref) { e.stopPropagation(); e.preventDefault(); } @@ -105,8 +109,11 @@ export function useOverlay(props: AriaOverlayProps, ref: RefObject + + + + + {/* @ts-ignore */} + + + + + + ); +} + +export const DateRangePickerInsideModalStory = { + render: () => , + parameters: { + description: { + data: 'Open the Modal, then open the DateRangePicker and select a start date. Clicking outside the Modal should close the picker but keep the Modal open.' + } + } +}; diff --git a/packages/react-aria-components/test/Dialog.test.js b/packages/react-aria-components/test/Dialog.test.js index 8cad80bef59..fa5a043b8d8 100644 --- a/packages/react-aria-components/test/Dialog.test.js +++ b/packages/react-aria-components/test/Dialog.test.js @@ -27,11 +27,15 @@ import { Popover, TextField } from '../'; +import {composeStories} from '@storybook/react'; import React, {useRef} from 'react'; +import * as stories from '../stories/Modal.stories'; import {UNSAFE_PortalProvider} from '@react-aria/overlays'; import {User} from '@react-aria/test-utils'; import userEvent from '@testing-library/user-event'; +let {DateRangePickerInsideModalStory: DateRangePickerInsideModal} = composeStories(stories); + describe('Dialog', () => { let user; let testUtilUser = new User({advanceTimer: jest.advanceTimersByTime}); @@ -461,4 +465,26 @@ describe('Dialog', () => { const input = getByTestId('email'); expect(document.activeElement).toBe(input); }); + + it('should not close Modal when DateRangePicker is dismissed by outside click', async () => { + let {getAllByRole, getByRole} = render(); + await user.click(getByRole('button')); + + let modal = getByRole('dialog').closest('.react-aria-ModalOverlay'); + expect(modal).toBeInTheDocument(); + + let button = getByRole('group').querySelector('.react-aria-Button'); + expect(button).toHaveAttribute('aria-label', 'Calendar'); + await user.click(button); + + let popover = getByRole('dialog').closest('.react-aria-Popover'); + expect(popover).toBeInTheDocument(); + expect(popover).toHaveAttribute('data-trigger', 'DateRangePicker'); + + let cells = getAllByRole('gridcell'); + await user.click(cells[5].children[0]); + await user.click(document.body); + expect(popover).not.toBeInTheDocument(); + expect(modal).toBeInTheDocument(); + }); }); From 6be9a11e77b5ba12f8dee91a32b974ffedcb812d Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Mon, 26 Jan 2026 15:12:11 -0600 Subject: [PATCH 3/4] fix: build by turning off canary (#9527) --- .circleci/config.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1f65571d3b7..344a6934521 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -897,7 +897,6 @@ workflows: - install-16 - install-17 - install-18 - - install-canary - test-ssr: requires: - install @@ -922,12 +921,6 @@ workflows: - test-ssr-18: requires: - install-18 - - test-canary: - requires: - - install-canary - - test-ssr-canary: - requires: - - install-canary - test-esm: requires: - install From 6c8fe491b0059107acd97ffdfb871f46e99a81d0 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Mon, 26 Jan 2026 13:23:41 -0800 Subject: [PATCH 4/4] docs: followup for after initial release (#9492) --- .../s2-docs/pages/react-aria/Virtualizer.mdx | 6 ++++ packages/dev/s2-docs/src/searchUtils.tsx | 32 +++++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx b/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx index 908ad7d17a1..c6663a908ad 100644 --- a/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx @@ -3,6 +3,7 @@ export default Layout; import docs from 'docs:react-aria-components'; import {GroupedPropTable} from '../../src/PropTable'; +import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; export const tags = ['windowing', 'list', 'grid', 'infinite']; export const description = 'Renders a scrollable collection of data using customizable layouts.'; @@ -41,6 +42,11 @@ for (let i = 0; i < 5000; i++) { Virtualizer uses objects to determine the position and size of each item, and provide the list of currently visible items. When using a Virtualizer, all items are positioned by the `Layout`, and CSS layout properties such as flexbox and grid do not apply. + + Virtualized components must have a defined size + This may be an explicit CSS `width` and `height`, or an implicit size (e.g. percentage or `flex`) bounded by an ancestor element. Without a bounded size, all items will be rendered to the DOM, negating the performance benefits of virtualized scrolling. + + ### List `ListLayout` supports layout of items in a vertical stack. Rows can be fixed or variable height. When using variable heights, set the `estimatedRowHeight` to a reasonable guess for how tall the rows will be on average. This allows the size of the scrollbar to be calculated. diff --git a/packages/dev/s2-docs/src/searchUtils.tsx b/packages/dev/s2-docs/src/searchUtils.tsx index 63809f10851..e8b5cd25ec1 100644 --- a/packages/dev/s2-docs/src/searchUtils.tsx +++ b/packages/dev/s2-docs/src/searchUtils.tsx @@ -12,7 +12,7 @@ import {type Library, TAB_DEFS} from './constants'; // @ts-ignore // eslint-disable-next-line monorepo/no-internal-import import NoSearchResults from '@react-spectrum/s2/illustrations/linear/NoSearchResults'; -import {Page} from '@parcel/rsc'; +import {Page, TocNode} from '@parcel/rsc'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; import {useSettings} from './SettingsContext'; @@ -136,13 +136,41 @@ export function stripMarkdown(description: string | undefined): string { return (description || '').replace(/\[(.*?)\]\(.*?\)/g, '$1'); } +/** + * Gets all of the subheadings underneath a heading within the Table of Contents. + */ +function getToCSubheadings(TocNode: TocNode, headings: string[]): string[] { + headings.push(TocNode.title); + + for (let node of TocNode.children) { + getToCSubheadings(node, headings); + } + + return headings; +} + /** * Transforms a page into a ComponentItem for search/display. */ export function transformPageToComponentItem(page: Page): ComponentItem { + // get all headings on a page and add them a tags for the search feature + let filterTags = new Set(['Content', 'Example', 'Examples', 'API', 'Accessibility', 'Events', 'Features', 'Introduction', 'Interface']); + let Toc = page.tableOfContents; + let headings: string[] = []; + if (Toc) { + for (let node of Toc) { + let subHeadings: string[] = getToCSubheadings(node, []); + headings.push(...subHeadings); + } + } + let allTags = (page.exports?.tags || page.exports?.keywords as string[]) || []; + let relatedPages = (page.exports?.relatedPages?.map(page => page.title)) || []; + allTags.push(...headings, ...relatedPages); + allTags = allTags.filter(tags => (!filterTags.has(tags) && !tags.startsWith('Testing'))); + const title = getPageTitle(page); const section: string = getSearchSection(page); - const tags: string[] = (page.exports?.tags || page.exports?.keywords as string[]) || []; + const tags: string[] = allTags; const description: string = stripMarkdown(page.exports?.description); const date: string | undefined = page.exports?.date; return {