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/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/react-aria/useKeyboard.mdx b/packages/dev/s2-docs/pages/react-aria/useKeyboard.mdx
index bfd9b51df0e..3081ade2f29 100644
--- a/packages/dev/s2-docs/pages/react-aria/useKeyboard.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/useKeyboard.mdx
@@ -20,7 +20,7 @@ export const description = 'Handles keyboard interactions with improved event pr
{docs.exports.useKeyboard.description}
```tsx render
-"use client"
+"use client";
import React from 'react';
import {useKeyboard} from 'react-aria/useKeyboard';
@@ -41,12 +41,11 @@ function Example() {
-
+
{events.map((e, i) =>
{e}
)}
>
@@ -62,6 +61,83 @@ This provides better modularity by default, so that a parent component doesn't r
that a child already handled. If the child doesn't handle the event (e.g. it was for an unknown key),
it can call `event.continuePropagation()` to allow parents to handle the event.
+### Shortcuts
+
+`useKeyboard` also accepts a `shortcuts` prop, which maps shortcut strings to handler functions.
+Shortcuts combine modifiers and keys with `+` (e.g. `"Mod+s"`, `"Shift+ArrowLeft"`). Modifier names
+are case-insensitive and can appear in any order. **Mod** means Command on macOS and Control on
+other platforms. (You can also use dynamic keys to create platform-specific shortcuts.
+`[key + (isMac() ? '+Alt' : '+Control')]`)
+
+When a key is pressed, the event is matched against the shortcuts map. If a handler is found, it is
+called after any `onKeyDown` handler. Handlers may return:
+
+* Nothing — the shortcut is handled. Propagation is stopped and the default action is prevented.
+* `true` or `false` — shorthand for preventing the default action (`true`) or allowing the browser
+ default and propagation to continue (`false`).
+* An object with `shouldContinuePropagation` and/or `shouldPreventDefault` for fine-grained control.
+
+If no shortcut matches, the event is propagated to parent elements.
+
+```tsx render
+"use client";
+import React from 'react';
+import {useKeyboard} from 'react-aria/useKeyboard';
+
+function Example() {
+ let [events, setEvents] = React.useState([]);
+ let add = (message: string) => setEvents(events => [message, ...events]);
+
+ let {keyboardProps: parentProps} = useKeyboard({
+ onKeyDown: () => add('parent onKeyDown')
+ });
+
+ let {keyboardProps: childProps} = useKeyboard({
+ shortcuts: {
+ 'Mod+s': () => add('child shortcut: Mod+s (prevents save dialog)'),
+ 'ArrowLeft': () => add('child shortcut: ArrowLeft (prevents default, stops propagation)'),
+ 'ArrowRight': () => {
+ add('child shortcut: ArrowRight (allows default, continues propagation)');
+ return false;
+ }
+ },
+ onKeyDown: () => add('child onKeyDown')
+ });
+
+ return (
+ <>
+
+ Focus the text field and press Mod+S, ←,
+ or →. ← prevents the cursor from moving. → moves the
+ cursor and propagates to the parent. Press any other key to see unmatched events propagate.
+
+
+
+
+
+
+ {events.map((e, i) =>
{e}
)}
+
+ >
+ );
+}
+```
+
## API
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/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}>
-
+