From a60619755992642b72afa976def3354d2271b7c9 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 17 Jul 2026 14:22:01 -0700 Subject: [PATCH 1/4] fix: prevent Android Chrome taps from being detected as drag event (#10332) * fix: prevent Android Chrome taps from being detected as drag event * fix tests * account for future additional browser in iOS and fix test mocking --- packages/react-aria/src/dnd/useDrag.ts | 4 ++-- packages/react-aria/test/dnd/dnd.test.js | 21 +++++++++++++++++-- .../test/dnd/useDraggableCollection.test.js | 18 ++++++++++++++++ 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/packages/react-aria/src/dnd/useDrag.ts b/packages/react-aria/src/dnd/useDrag.ts index d619717242f..abb33b898d9 100644 --- a/packages/react-aria/src/dnd/useDrag.ts +++ b/packages/react-aria/src/dnd/useDrag.ts @@ -40,6 +40,7 @@ import { writeToDataTransfer } from './utils'; import intlMessages from '../../intl/dnd/*.json'; +import {isIOS, isWebKit} from '../utils/platform'; import {isVirtualClick, isVirtualPointerEvent} from '../utils/isVirtualEvent'; import {useDescription} from '../utils/useDescription'; import {useGlobalListeners} from '../utils/useGlobalListeners'; @@ -360,7 +361,6 @@ export function useDrag(options: DragOptions): DragResult { // events such as selection from also occurring. We attempt to infer whether a // pointer event (e.g. long press) came from a touch screen reader, and then initiate // dragging in the native onDragStart listener above. - interactions = { ...descriptionProps, onPointerDown(e) { @@ -369,7 +369,7 @@ export function useDrag(options: DragOptions): DragResult { : e.pointerType; // Try to detect virtual drag passthrough gestures. - if (e.width < 1 && e.height < 1) { + if (e.width < 1 && e.height < 1 && isIOS() && isWebKit()) { // iOS VoiceOver. modalityOnPointerDown.current = 'virtual'; } else { diff --git a/packages/react-aria/test/dnd/dnd.test.js b/packages/react-aria/test/dnd/dnd.test.js index bdd361eb5fa..e4d9aaf07e5 100644 --- a/packages/react-aria/test/dnd/dnd.test.js +++ b/packages/react-aria/test/dnd/dnd.test.js @@ -2710,12 +2710,21 @@ describe('useDrag and useDrop', function () { }); describe('screen reader', () => { + let platformGetter; + let userAgentGetter; + beforeEach(async () => { + platformGetter = jest.spyOn(navigator, 'platform', 'get').mockImplementation(() => 'iPhone'); + userAgentGetter = jest + .spyOn(navigator, 'userAgent', 'get') + .mockImplementation(() => 'AppleWebKit'); // reset focus visible state fireEvent.click(document.body, {detail: 0, pointerType: null}); }); afterEach(async () => { + platformGetter.mockRestore(); + userAgentGetter.mockRestore(); await user.keyboard('{Escape}'); }); @@ -2952,7 +2961,11 @@ describe('useDrag and useDrop', function () { draggable, pointerEvent('pointerup', {pointerId: 1, width: 1, height: 1, pressure: 0, detail: 0}) ); - await user.pointer({target: draggable, keys: '[MouseLeft]', coords: {width: 0, height: 0}}); + await user.pointer({ + target: draggable, + keys: '[MouseLeft]', + coords: {clientX: 50, clientY: 25, width: 1, height: 1} + }); act(() => jest.runAllTimers()); expect(draggable).toHaveAttribute('data-dragging', 'true'); expect(draggable).toHaveAttribute('aria-describedby'); @@ -3017,7 +3030,11 @@ describe('useDrag and useDrop', function () { draggable, pointerEvent('pointerup', {pointerId: 1, width: 1, height: 1, pressure: 0, detail: 0}) ); - await user.pointer({target: draggable, keys: '[MouseLeft]', coords: {width: 0, height: 0}}); + await user.pointer({ + target: draggable, + keys: '[MouseLeft]', + coords: {clientX: 50, clientY: 25, width: 1, height: 1} + }); act(() => jest.runAllTimers()); expect(draggable).toHaveAttribute('data-dragging', 'true'); diff --git a/packages/react-aria/test/dnd/useDraggableCollection.test.js b/packages/react-aria/test/dnd/useDraggableCollection.test.js index fc33abd2972..8d88e3e8697 100644 --- a/packages/react-aria/test/dnd/useDraggableCollection.test.js +++ b/packages/react-aria/test/dnd/useDraggableCollection.test.js @@ -1037,6 +1037,12 @@ describe('useDraggableCollection', () => { }); it('should work with a listbox without a drag button', async () => { + let platformGetter = jest + .spyOn(navigator, 'platform', 'get') + .mockImplementation(() => 'iPhone'); + let userAgentGetter = jest + .spyOn(navigator, 'userAgent', 'get') + .mockImplementation(() => 'AppleWebKit'); setMedia({pointer: 'coarse'}); let onDragStart = jest.fn(); let onDragEnd = jest.fn(); @@ -1125,10 +1131,19 @@ describe('useDraggableCollection', () => { keys: new Set(['foo', 'bar']), isInternal: false }); + + platformGetter.mockRestore(); + userAgentGetter.mockRestore(); }); it('should support row actions', async () => { setMedia({pointer: 'coarse'}); + let platformGetter = jest + .spyOn(navigator, 'platform', 'get') + .mockImplementation(() => 'iPhone'); + let userAgentGetter = jest + .spyOn(navigator, 'userAgent', 'get') + .mockImplementation(() => 'AppleWebKit'); let onDragStart = jest.fn(); let onDragEnd = jest.fn(); let onDrop = jest.fn(); @@ -1232,6 +1247,9 @@ describe('useDraggableCollection', () => { keys: new Set(['foo', 'bar']), isInternal: false }); + + platformGetter.mockRestore(); + userAgentGetter.mockRestore(); }); }); }); From 486198ddcfe30f2e5d3987a7091d95458e86992b Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 17 Jul 2026 14:22:38 -0700 Subject: [PATCH 2/4] chore: simplifying keyboard navigation docs copy (#10338) * chore: simplifying keyboard navigation docs copy * forgot listview --- .../dev/s2-docs/pages/react-aria/GridList.mdx | 54 +------------------ .../dev/s2-docs/pages/react-aria/Table.mdx | 7 +-- packages/dev/s2-docs/pages/s2/ListView.mdx | 2 +- packages/dev/s2-docs/pages/s2/TableView.mdx | 2 +- .../react-aria-components/src/GridList.tsx | 4 +- 5 files changed, 6 insertions(+), 63 deletions(-) diff --git a/packages/dev/s2-docs/pages/react-aria/GridList.mdx b/packages/dev/s2-docs/pages/react-aria/GridList.mdx index dadc43c5ade..3907002c2cc 100644 --- a/packages/dev/s2-docs/pages/react-aria/GridList.mdx +++ b/packages/dev/s2-docs/pages/react-aria/GridList.mdx @@ -736,7 +736,7 @@ let photos = [ - + {item.description} @@ -744,58 +744,6 @@ let photos = [ ``` -You can further control if a row automatically focuses itself or its children on keyboard focus via `focusMode`. Futhermore, `allowsArrowNavigation` can be used to allow arrow key navigation from the row's children to adjacent rows. -This allows keyboard users to navigate to the contents of the row without needing to tab in and out of the row even when in tab keyboard navigation. -Be sure to only set `allowsArrowNavigation` on rows whose interactive content don't use arrow keys. - -```tsx render -"use client"; -import {GridList, GridListItem, Text} from 'vanilla-starter/GridList'; -import {Button} from 'vanilla-starter/Button'; -import {useState} from 'react'; - -///- begin collapse -/// -let initialItems = [ - {id: 1, name: 'Apple', image: 'https://images.unsplash.com/photo-1630563451961-ac2ff27616ab?q=80&w=400&auto=format&fit=crop&ixlib=rb-4.1.0'}, - {id: 2, name: 'Peach', image: 'https://images.unsplash.com/photo-1642372849486-f88b963cb734?q=80&w=400&auto=format&fit=crop&ixlib=rb-4.1.0'}, - {id: 3, name: 'Blueberry', image: 'https://images.unsplash.com/photo-1606757389667-45c2024f9fa4?q=80&w=400&auto=format&fit=crop&ixlib=rb-4.1.0'}, - {id: 4, name: 'Broccoli', image: 'https://images.unsplash.com/photo-1685504445355-0e7bdf90d415?q=80&w=400&auto=format&fit=crop&ixlib=rb-4.1.0'}, - {id: 5, name: 'Brussels Sprouts', image: 'https://images.unsplash.com/photo-1685504507286-dc290728c01a?q=80&w=400&auto=format&fit=crop&ixlib=rb-4.1.0'}, - {id: 6, name: 'Peas', image: 'https://images.unsplash.com/photo-1587411768345-867e228218c8?q=80&w=400&auto=format&fit=crop&ixlib=rb-4.1.0'}, -]; -///- end collapse -/// - -function Example() { - let [items, setItems] = useState(initialItems); - return ( - - {item => ( - - - {item.name} - - - )} - - ); -} -``` - ## Drag and drop GridList supports drag and drop interactions when the `dragAndDropHooks` prop is provided using the hook. Users can drop data on the list as a whole, on individual items, insert new items between existing ones, or reorder items. React Aria supports drag and drop via mouse, touch, keyboard, and screen reader interactions. See the [drag and drop guide](dnd?component=GridList) to learn more. diff --git a/packages/dev/s2-docs/pages/react-aria/Table.mdx b/packages/dev/s2-docs/pages/react-aria/Table.mdx index 21a023a11f9..f619228b40b 100644 --- a/packages/dev/s2-docs/pages/react-aria/Table.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Table.mdx @@ -762,18 +762,13 @@ let files = [ {item.name} {item.type} {item.date} - + )} ``` -You can further control if a cell automatically focuses itself or its children on keyboard focus via `focusMode`. Futhermore, `allowsArrowNavigation` can be used to allow arrow key navigation from the cell's children to adjacent cells. -This allows keyboard users to navigate to the selection checkboxes without needing to tab in and out of the selection cell even when in tab keyboard navigation. -The vanilla CSS starter applies these to selection cells automatically, see the **Table.tsx** tab above. -Be sure to only set `allowsArrowNavigation` on cells whose interactive content don't use arrow keys. - ## Drag and drop Table supports drag and drop interactions when the `dragAndDropHooks` prop is provided using the hook. Users can drop data on the table as a whole, on individual rows, insert new rows between existing ones, or reorder rows. React Aria supports drag and drop via mouse, touch, keyboard, and screen reader interactions. See the [drag and drop guide](dnd?component=Table) to learn more. diff --git a/packages/dev/s2-docs/pages/s2/ListView.mdx b/packages/dev/s2-docs/pages/s2/ListView.mdx index 53afbb9bbcb..1933ff9ba8a 100644 --- a/packages/dev/s2-docs/pages/s2/ListView.mdx +++ b/packages/dev/s2-docs/pages/s2/ListView.mdx @@ -412,7 +412,7 @@ let documents = [ - + )} diff --git a/packages/dev/s2-docs/pages/s2/TableView.mdx b/packages/dev/s2-docs/pages/s2/TableView.mdx index eea1b318b7f..05dbabf1e01 100644 --- a/packages/dev/s2-docs/pages/s2/TableView.mdx +++ b/packages/dev/s2-docs/pages/s2/TableView.mdx @@ -984,7 +984,7 @@ let files = [ {item.name} {item.type} {item.date} - + )} diff --git a/packages/react-aria-components/src/GridList.tsx b/packages/react-aria-components/src/GridList.tsx index 7b1a62f5322..098cb5c7f49 100644 --- a/packages/react-aria-components/src/GridList.tsx +++ b/packages/react-aria-components/src/GridList.tsx @@ -511,8 +511,8 @@ export interface GridListItemProps */ onAction?: () => void; /** - * Whether the row or its first focusable child element should be focused when the row is - * focused. Defaults to 'row'. + * Whether the row or its first focusable child element should be focused when navigating + * to the row. Defaults to 'row'. */ focusMode?: 'child' | 'row'; /** From 0ec3fbb2e3924a997d28a0c5c4654fff2b816de8 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Sat, 18 Jul 2026 08:10:30 +1000 Subject: [PATCH 3/4] fix: hidden component behaviours S2 Switch and RAC DropZone (#10336) * add stories for verification * fix lint * remove docs reference * All the fixes * revert stories --- packages/@react-spectrum/s2/src/Switch.tsx | 1 + .../dev/s2-docs/pages/react-aria/DropZone.mdx | 5 ----- packages/react-aria-components/src/DropZone.tsx | 15 +++++---------- .../react-aria-components/src/FileTrigger.tsx | 1 + .../react-aria-components/test/DropZone.test.js | 12 ------------ 5 files changed, 7 insertions(+), 27 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Switch.tsx b/packages/@react-spectrum/s2/src/Switch.tsx index dad0d2129d2..65f1d2ca50b 100644 --- a/packages/@react-spectrum/s2/src/Switch.tsx +++ b/packages/@react-spectrum/s2/src/Switch.tsx @@ -83,6 +83,7 @@ export const SwitchContext = const field = style( { display: 'grid', + position: 'relative', gridTemplateColumns: { default: ['max-content', '1fr'], isNoVisibleLabel: ['max-content'] diff --git a/packages/dev/s2-docs/pages/react-aria/DropZone.mdx b/packages/dev/s2-docs/pages/react-aria/DropZone.mdx index ec09bbb9f1f..dcf1cb17703 100644 --- a/packages/dev/s2-docs/pages/react-aria/DropZone.mdx +++ b/packages/dev/s2-docs/pages/react-aria/DropZone.mdx @@ -152,11 +152,6 @@ function Example() { } ``` -If a `DropZone` is rendered in a positioned scroll container, focusing the visually hidden drop -button may cause the container to scroll. The `dropButtonStyle` prop can be used to adjust the -position of this internal button wrapper, for example `dropButtonStyle={{position: 'fixed', top: 0, -left: 0}}`. - ## Examples diff --git a/packages/react-aria-components/src/DropZone.tsx b/packages/react-aria-components/src/DropZone.tsx index 8cc4ed1cbe4..ec6829a9e6a 100644 --- a/packages/react-aria-components/src/DropZone.tsx +++ b/packages/react-aria-components/src/DropZone.tsx @@ -23,11 +23,12 @@ import { } from './utils'; import {DropOptions, useDrop} from 'react-aria/useDrop'; import {filterDOMProps} from 'react-aria/filterDOMProps'; +import {focusWithoutScrolling} from 'react-aria/private/utils/focusWithoutScrolling'; import {getEventTarget, nodeContains} from 'react-aria/private/utils/shadowdom/DOMFunctions'; import intlMessages from '../intl/*.json'; import {isFocusable} from 'react-aria/private/utils/isFocusable'; import {mergeProps} from 'react-aria/mergeProps'; -import React, {createContext, CSSProperties, ForwardedRef, forwardRef, useRef} from 'react'; +import React, {createContext, ForwardedRef, forwardRef, useRef} from 'react'; import {TextContext} from './Text'; import {useButton} from 'react-aria/useButton'; import {useClipboard} from 'react-aria/useClipboard'; @@ -87,11 +88,6 @@ export interface DropZoneProps * @default 'react-aria-DropZone' */ className?: ClassNameOrFunction; - /** - * The inline style for the visually hidden drop button used for keyboard and screen reader drop - * interactions. - */ - dropButtonStyle?: CSSProperties; } export const DropZoneContext = createContext>(null); @@ -106,7 +102,6 @@ export const DropZone = forwardRef(function DropZone( let {isDisabled = false} = props; // oxlint-disable-next-line react/react-compiler [props, ref] = useContextProps(props, ref, DropZoneContext); - let {dropButtonStyle} = props; let dropzoneRef = useObjectRef(ref); let buttonRef = useRef(null); let {dropProps, dropButtonProps, isDropTarget} = useDrop({ @@ -156,8 +151,8 @@ export const DropZone = forwardRef(function DropZone( while (target && nodeContains(dropzoneRef.current, target)) { if (isFocusable(target)) { break; - } else if (target === dropzoneRef.current) { - buttonRef.current?.focus(); + } else if (target === dropzoneRef.current && buttonRef.current) { + focusWithoutScrolling(buttonRef.current); break; } @@ -169,7 +164,7 @@ export const DropZone = forwardRef(function DropZone( data-focus-visible={isFocusVisible || undefined} data-drop-target={isDropTarget || undefined} data-disabled={isDisabled || undefined}> - +