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
7 changes: 3 additions & 4 deletions packages/@react-aria/interactions/src/PressResponder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,8 @@ export const PressResponder:
React.forwardRef(({children, ...props}: PressResponderProps, ref: ForwardedRef<FocusableElement>) => {
let isRegistered = useRef(false);
let prevContext = useContext(PressResponderContext);
ref = useObjectRef(ref || prevContext?.ref);
let context = mergeProps(prevContext || {}, {
let context: any = mergeProps(prevContext || {}, {
...props,
ref,
register() {
isRegistered.current = true;
if (prevContext) {
Expand All @@ -37,7 +35,8 @@ React.forwardRef(({children, ...props}: PressResponderProps, ref: ForwardedRef<F
}
});

useSyncRef(prevContext, ref);
context.ref = useObjectRef(ref || prevContext?.ref);
useSyncRef(prevContext, context.ref);

useEffect(() => {
if (!isRegistered.current) {
Expand Down
4 changes: 3 additions & 1 deletion packages/@react-aria/interactions/src/usePress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ function usePressResponderContext(props: PressHookProps): PressHookProps {
// Consume context from <PressResponder> and merge with props.
let context = useContext(PressResponderContext);
if (context) {
let {register, ...contextProps} = context;
// Prevent mergeProps from merging ref.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let {register, ref, ...contextProps} = context;
props = mergeProps(contextProps, props) as PressHookProps;
register();
}
Expand Down
5 changes: 4 additions & 1 deletion packages/@react-aria/utils/src/mergeProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import {chain} from './chain';
import clsx from 'clsx';
import {mergeIds} from './useId';
import {mergeRefs} from './mergeRefs';

interface Props {
[key: string]: any
Expand All @@ -28,7 +29,7 @@ type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (

/**
* Merges multiple props objects together. Event handlers are chained,
* classNames are combined, and ids are deduplicated.
* classNames are combined, ids are deduplicated, and refs are merged.
* For all other props, the last prop object overrides all previous ones.
* @param args - Multiple sets of props to merge together.
*/
Expand Down Expand Up @@ -63,6 +64,8 @@ export function mergeProps<T extends PropsArg[]>(...args: T): UnionToIntersectio
result[key] = clsx(a, b);
} else if (key === 'id' && a && b) {
result.id = mergeIds(a, b);
} else if (key === 'ref' && a && b) {
result.ref = mergeRefs(a, b);
// Override others
} else {
result[key] = b !== undefined ? b : a;
Expand Down
10 changes: 10 additions & 0 deletions packages/@react-aria/utils/test/mergeProps.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import clsx from 'clsx';
import { mergeIds, useId } from '../src/useId';
import { mergeProps } from '../src/mergeProps';
import { render } from '@react-spectrum/test-utils-internal';
import { createRef } from 'react';

describe('mergeProps', function () {
it('handles one argument', function () {
Expand Down Expand Up @@ -122,4 +123,13 @@ describe('mergeProps', function () {
let mergedProps = mergeProps({ data: id1 }, { data: id2 });
expect(mergedProps.data).toBe(id2);
});

it('merges refs', function () {
let ref = createRef();
let ref1 = createRef();
let merged = mergeProps({ref}, {ref: ref1});
merged.ref(2);
expect(ref.current).toBe(2);
expect(ref1.current).toBe(2);
});
});
7 changes: 6 additions & 1 deletion packages/dev/s2-docs/pages/react-aria/GridList.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ function AsyncLoadingExample() {

### Links

Use the `href` prop on a `<GridListItem>` to create a link. See the [framework setup guide](frameworks) to learn how to integrate with your framework. Link interactions vary depending on the selection behavior. See the [selection guide](selection?component=GridList#selection-behavior) for more details.
Use the `href` prop on a `<GridListItem>` to create a link. Link interactions vary depending on the selection behavior. See the [selection guide](selection?component=GridList#selection-behavior) for more details.

```tsx render docs={docs.exports.GridList} links={docs.links} props={['selectionBehavior']} initialProps={{'aria-label': 'Links', selectionMode: 'multiple'}} wide
"use client";
Expand Down Expand Up @@ -520,6 +520,11 @@ let images = [
</GridList>
```

<InlineAlert variant="notice">
<Heading>Client-side routing</Heading>
<Content>Due to [HTML spec limitations](https://github.com/w3c/html-aria/issues/473), GridListItems cannot be rendered as `<a>` elements. React Aria handles link clicks with JavaScript and triggers native navigation. When using a client-side router, use the `onAction` event to programmatically trigger navigation instead of the `href` prop.</Content>
</InlineAlert>

### Empty state

```tsx render hideImports
Expand Down
14 changes: 13 additions & 1 deletion packages/dev/s2-docs/pages/react-aria/ListBox.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ function AsyncLoadingExample() {

### Links

Use the `href` prop on a `<ListBoxItem>` to create a link. See the [framework setup guide](frameworks) to learn how to integrate with your framework.
Use the `href` prop on a `<ListBoxItem>` to create a link.

By default, link items in a ListBox are not selectable, and only perform navigation when the user interacts with them. However, with `selectionBehavior="replace"`, items will be selected when single clicking or pressing the <Keyboard>Space</Keyboard> key, and navigate to the link when double clicking or pressing the <Keyboard>Enter</Keyboard> key.

Expand All @@ -214,6 +214,18 @@ import {ListBox, ListBoxItem} from 'react-aria-components';
</ListBox>
```

By default, links are rendered as an `<a>` element. Use the `render` prop to integrate your framework's link component. An `href` should still be passed to `ListBoxItem` so React Aria knows it is a link.

```tsx
<ListBoxItem
{...props}
render={domProps =>
'href' in domProps
? <RouterLink {...domProps} />
: <div {...domProps} />
} />
```

### Empty state

```tsx render hideImports
Expand Down
14 changes: 13 additions & 1 deletion packages/dev/s2-docs/pages/react-aria/Menu.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ import {Button} from 'vanilla-starter/Button';

### Links

Use the `href` prop on a `<MenuItem>` to create a link. See the [framework setup guide](frameworks) to learn how to integrate with your framework.
Use the `href` prop on a `<MenuItem>` to create a link.

```tsx render hideImports
"use client";
Expand All @@ -305,6 +305,18 @@ import {Button} from 'vanilla-starter/Button';
</MenuTrigger>
```

By default, links are rendered as an `<a>` element. Use the `render` prop to integrate your framework's link component. An `href` should still be passed to `MenuItem` so React Aria knows it is a link.

```tsx
<MenuItem
{...props}
render={domProps =>
'href' in domProps
? <RouterLink {...domProps} />
: <div {...domProps} />
} />
```

### Autocomplete

Popovers can include additional components as siblings of a menu. This example uses an [Autocomplete](Autocomplete) with a [SearchField](SearchField) to let the user filter the items.
Expand Down
7 changes: 6 additions & 1 deletion packages/dev/s2-docs/pages/react-aria/Table.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ function AsyncSortTable() {

### Links

Use the `href` prop on a `<Row>` to create a link. See the [framework setup guide](frameworks) to learn how to integrate with your framework. Link interactions vary depending on the selection behavior. See the [selection guide](selection) for more details.
Use the `href` prop on a `<Row>` to create a link. Link interactions vary depending on the selection behavior. See the [selection guide](selection) for more details.

```tsx render docs={docs.exports.ListBox} links={docs.links} props={['selectionBehavior']} initialProps={{'aria-label': 'Bookmarks', selectionMode: 'multiple'}} wide
"use client";
Expand Down Expand Up @@ -295,6 +295,11 @@ import {Table, TableHeader, Column, Row, TableBody, Cell} from 'vanilla-starter/
</Table>
```

<InlineAlert variant="notice">
<Heading>Client-side routing</Heading>
<Content>Due to [HTML spec limitations](https://github.com/w3c/html-aria/issues/473), table rows cannot be rendered as `<a>` elements. React Aria handles link clicks with JavaScript and triggers native navigation. When using a client-side router, use the `onAction` event to programmatically trigger navigation instead of the `href` prop.</Content>
</InlineAlert>

### Empty state

```tsx render hideImports
Expand Down
50 changes: 10 additions & 40 deletions packages/dev/s2-docs/pages/react-aria/Tabs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -186,46 +186,16 @@ function Example() {

### Links

Use the `href` prop on a `<Tab>` to create a link. See the [framework setup guide](frameworks) to learn how to integrate with your framework. This example uses a simple hash-based router to sync the selected tab to the URL.

```tsx render
"use client";
import {Tabs, TabList, Tab, TabPanels, TabPanel} from 'vanilla-starter/Tabs';
import {useSyncExternalStore} from 'react';

export default function Example() {
let hash = useSyncExternalStore(subscribe, getHash, getHashServer);

return (
<Tabs selectedKey={hash}>
<TabList aria-label="Tabs">
{/*- begin highlight -*/}
<Tab id="#/" href="#/">Home</Tab>
{/*- end highlight -*/}
<Tab id="#/shared" href="#/shared">Shared</Tab>
<Tab id="#/deleted" href="#/deleted">Deleted</Tab>
</TabList>
<TabPanels>
<TabPanel id="#/">Home</TabPanel>
<TabPanel id="#/shared">Shared</TabPanel>
<TabPanel id="#/deleted">Deleted</TabPanel>
</TabPanels>
</Tabs>
);
}

function getHash() {
return location.hash.startsWith('#/') ? location.hash : '#/';
}

function getHashServer() {
return '#/';
}

function subscribe(fn) {
addEventListener('hashchange', fn);
return () => removeEventListener('hashchange', fn);
}
Use the `href` prop on a `<Tab>` to create a link. By default, links are rendered as an `<a>` element. Use the `render` prop to integrate your framework's link component.

```tsx
<Tab
href="/home"
render={domProps =>
'href' in domProps
? <RouterLink {...domProps} />
: <div {...domProps} />
} />
```

## Selection
Expand Down
8 changes: 7 additions & 1 deletion packages/dev/s2-docs/pages/react-aria/TagGroup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {TagGroup as VanillaTagGroup, Tag} from 'vanilla-starter/TagGroup';
import vanillaDocs from 'docs:vanilla-starter/TagGroup';
import '../../tailwind/tailwind.css';
import Anatomy from '@react-aria/tag/docs/anatomy.svg';
import {InlineAlert, Heading, Content} from '@react-spectrum/s2';

export const tags = ['chips', 'pills'];
export const relatedPages = [{'title': 'useTagGroup', 'url': 'TagGroup/useTagGroup.html'}];
Expand Down Expand Up @@ -77,7 +78,7 @@ function Example() {

### Links

Use the `href` prop on a `<Tag>` to create a link. See the [framework setup guide](frameworks) to learn how to integrate with your framework.
Use the `href` prop on a `<Tag>` to create a link.

```tsx render
"use client";
Expand All @@ -95,6 +96,11 @@ import {TagGroup, Tag} from 'vanilla-starter/TagGroup';
</TagGroup>
```

<InlineAlert variant="notice">
<Heading>Client-side routing</Heading>
<Content>Due to [HTML spec limitations](https://github.com/w3c/html-aria/issues/473), tags cannot be rendered as `<a>` elements. React Aria handles link clicks with JavaScript and triggers native navigation. When using a client-side router, use the `onAction` event to programmatically trigger navigation instead of the `href` prop.</Content>
</InlineAlert>

## Selection

Use the `selectionMode` prop to enable single or multiple selection. The selected items can be controlled via the `selectedKeys` prop, matching the `id` prop of the items. Items can be disabled with the `isDisabled` prop. See the [selection guide](selection?component=TagGroup) for more details.
Expand Down
7 changes: 6 additions & 1 deletion packages/dev/s2-docs/pages/react-aria/Tree.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ function AsyncLoadingExample() {

### Links

Use the `href` prop on a `<TreeItem>` to create a link. See the [framework setup guide](frameworks) to learn how to integrate with your framework. Link interactions vary depending on the selection behavior. See the [selection guide](selection?component=Tree#selection-behavior) for more details.
Use the `href` prop on a `<TreeItem>` to create a link. Link interactions vary depending on the selection behavior. See the [selection guide](selection?component=Tree#selection-behavior) for more details.

```tsx render docs={docs.exports.Tree} links={docs.links} props={['selectionBehavior']} initialProps={{selectionMode: 'multiple'}} wide
"use client";
Expand Down Expand Up @@ -219,6 +219,11 @@ import {Tree, TreeItem} from 'vanilla-starter/Tree';
</Tree>
```

<InlineAlert variant="notice">
<Heading>Client-side routing</Heading>
<Content>Due to [HTML spec limitations](https://github.com/w3c/html-aria/issues/473), TreeItems cannot be rendered as `<a>` elements. React Aria handles link clicks with JavaScript and triggers native navigation. When using a client-side router, use the `onAction` event to programmatically trigger navigation instead of the `href` prop.</Content>
</InlineAlert>

### Empty state

```tsx render
Expand Down
34 changes: 34 additions & 0 deletions packages/dev/s2-docs/pages/react-aria/customization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {Layout} from '../../src/Layout';
export default Layout;

import docs from 'docs:react-aria-components';
import {InlineAlert, Heading, Content} from '@react-spectrum/s2';

export const section = 'Guides';
export const description = 'How to build custom component patterns.';
Expand All @@ -10,6 +11,39 @@ export const description = 'How to build custom component patterns.';

<PageDescription>React Aria is built using a flexible and composable API. Learn how to use contexts and slots to create custom component patterns, or mix and match with the lower level Hook-based API for even more control over rendering and behavior.</PageDescription>

## DOM elements

Use the `render` prop on any React Aria component to render a custom component in place of the default DOM element. This accepts a function which receives the DOM props to pass through, and states such as `isPressed` and `isSelected`.

For example, you can render a [Motion](https://motion.dev) button and use the state to drive an animation.

```tsx
import {Button} from 'react-aria-components';
import {motion} from 'motion/react';

<Button
render={(domProps, {isPressed}) => (
<motion.button
{...domProps}
animate={{scale: isPressed ? 0.9 : 1}} />
)}>
Press me
</Button>
```

The `render` prop is also useful for rendering link components from client-side routers, or reusing existing presentational components.

<InlineAlert variant="notice">
<Heading>Follow these rules to avoid breaking the behavior and accessibility of the component:</Heading>
<Content>
<ul style={{paddingLeft: 12}}>
<li>Always render the expected element type (e.g. if `<button>` is expected, do not render an `<a>`). You will see a warning in development if a mismatch is detected.</li>
<li>Only render a single root DOM element (no fragments).</li>
<li>Always pass the provided props the underlying DOM element, merging with your own props via [mergeProps](mergeProps) as needed.</li>
</ul>
</Content>
</InlineAlert>

## Contexts

The React Aria Components API is designed around composition. Components are reused between patterns to build larger composite components. For example, there is no dedicated `NumberFieldIncrementButton` or `SelectPopover` component. Instead, the standalone [Button](Button) and [Popover](Popover) components are reused within [NumberField](NumberField) and [Select](Select). This reduces the amount of duplicate styling code you need to write and maintain, and provides powerful composition capabilities you can use in your own components.
Expand Down
Loading