Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/@react-spectrum/s2/src/Switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export const SwitchContext =
const field = style(
{
display: 'grid',
position: 'relative',
gridTemplateColumns: {
default: ['max-content', '1fr'],
isNoVisibleLabel: ['max-content']
Expand Down
5 changes: 0 additions & 5 deletions packages/dev/s2-docs/pages/react-aria/DropZone.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

<ExampleList tag="dropzone" pages={props.pages} />
Expand Down
54 changes: 1 addition & 53 deletions packages/dev/s2-docs/pages/react-aria/GridList.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -736,66 +736,14 @@ let photos = [
<GridListItem textValue={item.title}>
<img src={item.src} alt="" />
<Text>
<TextField style={{paddingTop: 2, paddingBottom: 2}} aria-label="title" defaultValue={item.title} />
<TextField style={{paddingTop: 2, paddingBottom: 2}} aria-label="title" defaultValue={item.title} placeholder="Enter a title" />
</Text>
<Text slot="description">{item.description}</Text>
</GridListItem>
)}
</GridList>
```

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 (
<GridList
items={items}
layout="stack"
orientation="vertical"
keyboardNavigationBehavior="tab"
aria-label="Shopping cart">
{item => (
<GridListItem
/*- begin highlight -*/
focusMode="child"
allowsArrowNavigation
/*- end highlight -*/
textValue={item.name}>
<img src={item.image} width={60} height={60} alt="" />
<Text>{item.name}</Text>
<Button
style={{width: 150, marginInlineStart: 10}}
variant="secondary"
onPress={() => setItems(prev => prev.filter(i => i.id !== item.id))}>
Remove from cart
</Button>
</GridListItem>
)}
</GridList>
);
}
```

## Drag and drop

GridList supports drag and drop interactions when the `dragAndDropHooks` prop is provided using the <TypeLink links={docs.links} type={docs.exports.useDragAndDrop} /> 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.
Expand Down
7 changes: 1 addition & 6 deletions packages/dev/s2-docs/pages/react-aria/Table.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -762,18 +762,13 @@ let files = [
<Cell>{item.name}</Cell>
<Cell>{item.type}</Cell>
<Cell>{item.date}</Cell>
<Cell><TextField aria-label={`${item.name} notes`} defaultValue="" /></Cell>
<Cell><TextField aria-label={`${item.name} notes`} placeholder="Enter notes" /></Cell>
</Row>
)}
</TableBody>
</Table>
```

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 <TypeLink links={docs.links} type={docs.exports.useDragAndDrop} /> 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.
Expand Down
90 changes: 83 additions & 7 deletions packages/dev/s2-docs/pages/react-aria/useKeyboard.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const description = 'Handles keyboard interactions with improved event pr
<PageDescription>{docs.exports.useKeyboard.description}</PageDescription>

```tsx render
"use client"
"use client";
import React from 'react';
import {useKeyboard} from 'react-aria/useKeyboard';

Expand All @@ -41,12 +41,11 @@ function Example() {
<input
{...keyboardProps}
id="example" />
<ul style={{
height: 100,
overflow: 'auto',
border: '1px solid gray',
width: 200
}}>
<ul
style={{
maxHeight: '200px',
overflow: 'auto'
}}>
{events.map((e, i) => <li key={i}>{e}</li>)}
</ul>
</>
Expand All @@ -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<string[]>([]);
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 (
<>
<p>
Focus the text field and press <kbd>Mod</kbd>+<kbd>S</kbd>, <kbd>←</kbd>,
or <kbd>→</kbd>. <kbd>←</kbd> prevents the cursor from moving. <kbd>→</kbd> moves the
cursor and propagates to the parent. Press any other key to see unmatched events propagate.
</p>
<div
{...parentProps}
style={{
border: '1px solid gray',
padding: 16
}}>
<label htmlFor="shortcuts-example" style={{display: 'block', marginBottom: 8}}>
Text field
</label>
<input
{...childProps}
id="shortcuts-example"
defaultValue="Move the cursor with arrow keys"
style={{width: '100%'}} />
</div>
<ul
style={{
maxHeight: '200px',
overflow: 'auto'
}}>
{events.map((e, i) => <li key={i}>{e}</li>)}
</ul>
</>
);
}
```

## API

<FunctionAPI function={docs.exports.useKeyboard} links={docs.links} />
Expand Down
2 changes: 1 addition & 1 deletion packages/dev/s2-docs/pages/s2/ListView.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ let documents = [
<ListViewItem textValue={item.name}>
<File />
<Text>
<TextField styles={style({margin: 4})} aria-label="title" defaultValue={item.name} />
<TextField styles={style({margin: 4})} aria-label="title" defaultValue={item.name} placeholder="Enter a name" />
</Text>
</ListViewItem>
)}
Expand Down
2 changes: 1 addition & 1 deletion packages/dev/s2-docs/pages/s2/TableView.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -984,7 +984,7 @@ let files = [
<Cell>{item.name}</Cell>
<Cell>{item.type}</Cell>
<Cell>{item.date}</Cell>
<Cell><TextField aria-label={`${item.name} notes`} defaultValue="" /></Cell>
<Cell><TextField aria-label={`${item.name} notes`} placeholder="Enter notes" /></Cell>
</Row>
)}
</TableBody>
Expand Down
15 changes: 5 additions & 10 deletions packages/react-aria-components/src/DropZone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -87,11 +88,6 @@ export interface DropZoneProps
* @default 'react-aria-DropZone'
*/
className?: ClassNameOrFunction<DropZoneRenderProps>;
/**
* The inline style for the visually hidden drop button used for keyboard and screen reader drop
* interactions.
*/
dropButtonStyle?: CSSProperties;
}

export const DropZoneContext = createContext<ContextValue<DropZoneProps, HTMLDivElement>>(null);
Expand All @@ -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<HTMLButtonElement>(null);
let {dropProps, dropButtonProps, isDropTarget} = useDrop({
Expand Down Expand Up @@ -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;
}

Expand All @@ -169,7 +164,7 @@ export const DropZone = forwardRef(function DropZone(
data-focus-visible={isFocusVisible || undefined}
data-drop-target={isDropTarget || undefined}
data-disabled={isDisabled || undefined}>
<VisuallyHidden style={dropButtonStyle}>
<VisuallyHidden>
<button
{...mergeProps(buttonProps, focusProps, clipboardProps, labelProps)}
ref={buttonRef}
Expand Down
1 change: 1 addition & 0 deletions packages/react-aria-components/src/FileTrigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export const FileTrigger = forwardRef(function FileTrigger(
className=""
type="file"
ref={inputRef}
onClick={e => e.stopPropagation()}
style={{display: 'none'}}
accept={acceptedFileTypes?.toString()}
onChange={e => onSelect?.(getEventTarget(e).files)}
Expand Down
4 changes: 2 additions & 2 deletions packages/react-aria-components/src/GridList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -511,8 +511,8 @@ export interface GridListItemProps<T = object>
*/
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';
/**
Expand Down
12 changes: 0 additions & 12 deletions packages/react-aria-components/test/DropZone.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -420,18 +420,6 @@ describe('DropZone', () => {
await user.click(button);
expect(document.activeElement).toBe(button);
});

it('should support custom styles on the visually hidden drop button wrapper', () => {
let {getByRole} = render(
<DropZone dropButtonStyle={{position: 'fixed', top: 0, left: 0}} />
);

expect(getByRole('button').parentElement).toHaveStyle({
position: 'fixed',
top: '0px',
left: '0px'
});
});
});

describe('via keyboard', function () {
Expand Down
4 changes: 2 additions & 2 deletions packages/react-aria/src/dnd/useDrag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions packages/react-aria/src/interactions/useKeyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ export interface KeyboardProps extends KeyboardEvents {
isDisabled?: boolean;
/** Keyboard shortcuts to handle. */
shortcuts?: KeyboardShortcutBindings;
/** Whether to allow repeating keys. Only affects shortcuts. */
allowRepeats?: boolean;
/** Whether to allow composing keys. Only affects shortcuts. */
allowComposing?: boolean;
}

Expand Down
3 changes: 2 additions & 1 deletion packages/react-aria/src/menu/useSubmenuTrigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,8 @@ export function useSubmenuTrigger<T>(

return {
submenuTriggerProps: {
...(submenuTriggerKeyboardProps as any), // TODO: fix this
onKeyDown: submenuTriggerKeyboardProps.onKeyDown,
onKeyUp: submenuTriggerKeyboardProps.onKeyUp,
id: submenuTriggerId,
'aria-controls': state.isOpen ? overlayId : undefined,
'aria-haspopup': !isDisabled ? type : undefined,
Expand Down
Loading
Loading