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}> - +