Skip to content

Commit c45221b

Browse files
authored
docs: Document render prop (#9564)
* Add render prop to customization docs * merge refs in mergeProps * Document render prop to create links * lint
1 parent 24ca0ac commit c45221b

14 files changed

Lines changed: 126 additions & 184 deletions

File tree

packages/@react-aria/interactions/src/PressResponder.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,8 @@ export const PressResponder:
2525
React.forwardRef(({children, ...props}: PressResponderProps, ref: ForwardedRef<FocusableElement>) => {
2626
let isRegistered = useRef(false);
2727
let prevContext = useContext(PressResponderContext);
28-
ref = useObjectRef(ref || prevContext?.ref);
29-
let context = mergeProps(prevContext || {}, {
28+
let context: any = mergeProps(prevContext || {}, {
3029
...props,
31-
ref,
3230
register() {
3331
isRegistered.current = true;
3432
if (prevContext) {
@@ -37,7 +35,8 @@ React.forwardRef(({children, ...props}: PressResponderProps, ref: ForwardedRef<F
3735
}
3836
});
3937

40-
useSyncRef(prevContext, ref);
38+
context.ref = useObjectRef(ref || prevContext?.ref);
39+
useSyncRef(prevContext, context.ref);
4140

4241
useEffect(() => {
4342
if (!isRegistered.current) {

packages/@react-aria/interactions/src/usePress.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,9 @@ function usePressResponderContext(props: PressHookProps): PressHookProps {
9999
// Consume context from <PressResponder> and merge with props.
100100
let context = useContext(PressResponderContext);
101101
if (context) {
102-
let {register, ...contextProps} = context;
102+
// Prevent mergeProps from merging ref.
103+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
104+
let {register, ref, ...contextProps} = context;
103105
props = mergeProps(contextProps, props) as PressHookProps;
104106
register();
105107
}

packages/@react-aria/utils/src/mergeProps.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import {chain} from './chain';
1414
import clsx from 'clsx';
1515
import {mergeIds} from './useId';
16+
import {mergeRefs} from './mergeRefs';
1617

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

2930
/**
3031
* Merges multiple props objects together. Event handlers are chained,
31-
* classNames are combined, and ids are deduplicated.
32+
* classNames are combined, ids are deduplicated, and refs are merged.
3233
* For all other props, the last prop object overrides all previous ones.
3334
* @param args - Multiple sets of props to merge together.
3435
*/
@@ -63,6 +64,8 @@ export function mergeProps<T extends PropsArg[]>(...args: T): UnionToIntersectio
6364
result[key] = clsx(a, b);
6465
} else if (key === 'id' && a && b) {
6566
result.id = mergeIds(a, b);
67+
} else if (key === 'ref' && a && b) {
68+
result.ref = mergeRefs(a, b);
6669
// Override others
6770
} else {
6871
result[key] = b !== undefined ? b : a;

packages/@react-aria/utils/test/mergeProps.test.jsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import clsx from 'clsx';
1414
import { mergeIds, useId } from '../src/useId';
1515
import { mergeProps } from '../src/mergeProps';
1616
import { render } from '@react-spectrum/test-utils-internal';
17+
import { createRef } from 'react';
1718

1819
describe('mergeProps', function () {
1920
it('handles one argument', function () {
@@ -122,4 +123,13 @@ describe('mergeProps', function () {
122123
let mergedProps = mergeProps({ data: id1 }, { data: id2 });
123124
expect(mergedProps.data).toBe(id2);
124125
});
126+
127+
it('merges refs', function () {
128+
let ref = createRef();
129+
let ref1 = createRef();
130+
let merged = mergeProps({ref}, {ref: ref1});
131+
merged.ref(2);
132+
expect(ref.current).toBe(2);
133+
expect(ref1.current).toBe(2);
134+
});
125135
});

packages/dev/s2-docs/pages/react-aria/GridList.mdx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ function AsyncLoadingExample() {
429429

430430
### Links
431431

432-
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.
432+
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.
433433

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

523+
<InlineAlert variant="notice">
524+
<Heading>Client-side routing</Heading>
525+
<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>
526+
</InlineAlert>
527+
523528
### Empty state
524529

525530
```tsx render hideImports

packages/dev/s2-docs/pages/react-aria/ListBox.mdx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ function AsyncLoadingExample() {
196196

197197
### Links
198198

199-
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.
199+
Use the `href` prop on a `<ListBoxItem>` to create a link.
200200

201201
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.
202202

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

217+
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.
218+
219+
```tsx
220+
<ListBoxItem
221+
{...props}
222+
render={domProps =>
223+
'href' in domProps
224+
? <RouterLink {...domProps} />
225+
: <div {...domProps} />
226+
} />
227+
```
228+
217229
### Empty state
218230

219231
```tsx render hideImports

packages/dev/s2-docs/pages/react-aria/Menu.mdx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ import {Button} from 'vanilla-starter/Button';
285285

286286
### Links
287287

288-
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.
288+
Use the `href` prop on a `<MenuItem>` to create a link.
289289

290290
```tsx render hideImports
291291
"use client";
@@ -305,6 +305,18 @@ import {Button} from 'vanilla-starter/Button';
305305
</MenuTrigger>
306306
```
307307

308+
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.
309+
310+
```tsx
311+
<MenuItem
312+
{...props}
313+
render={domProps =>
314+
'href' in domProps
315+
? <RouterLink {...domProps} />
316+
: <div {...domProps} />
317+
} />
318+
```
319+
308320
### Autocomplete
309321

310322
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.

packages/dev/s2-docs/pages/react-aria/Table.mdx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ function AsyncSortTable() {
261261

262262
### Links
263263

264-
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.
264+
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.
265265

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

298+
<InlineAlert variant="notice">
299+
<Heading>Client-side routing</Heading>
300+
<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>
301+
</InlineAlert>
302+
298303
### Empty state
299304

300305
```tsx render hideImports

packages/dev/s2-docs/pages/react-aria/Tabs.mdx

Lines changed: 10 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -186,46 +186,16 @@ function Example() {
186186

187187
### Links
188188

189-
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.
190-
191-
```tsx render
192-
"use client";
193-
import {Tabs, TabList, Tab, TabPanels, TabPanel} from 'vanilla-starter/Tabs';
194-
import {useSyncExternalStore} from 'react';
195-
196-
export default function Example() {
197-
let hash = useSyncExternalStore(subscribe, getHash, getHashServer);
198-
199-
return (
200-
<Tabs selectedKey={hash}>
201-
<TabList aria-label="Tabs">
202-
{/*- begin highlight -*/}
203-
<Tab id="#/" href="#/">Home</Tab>
204-
{/*- end highlight -*/}
205-
<Tab id="#/shared" href="#/shared">Shared</Tab>
206-
<Tab id="#/deleted" href="#/deleted">Deleted</Tab>
207-
</TabList>
208-
<TabPanels>
209-
<TabPanel id="#/">Home</TabPanel>
210-
<TabPanel id="#/shared">Shared</TabPanel>
211-
<TabPanel id="#/deleted">Deleted</TabPanel>
212-
</TabPanels>
213-
</Tabs>
214-
);
215-
}
216-
217-
function getHash() {
218-
return location.hash.startsWith('#/') ? location.hash : '#/';
219-
}
220-
221-
function getHashServer() {
222-
return '#/';
223-
}
224-
225-
function subscribe(fn) {
226-
addEventListener('hashchange', fn);
227-
return () => removeEventListener('hashchange', fn);
228-
}
189+
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.
190+
191+
```tsx
192+
<Tab
193+
href="/home"
194+
render={domProps =>
195+
'href' in domProps
196+
? <RouterLink {...domProps} />
197+
: <div {...domProps} />
198+
} />
229199
```
230200

231201
## Selection

packages/dev/s2-docs/pages/react-aria/TagGroup.mdx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {TagGroup as VanillaTagGroup, Tag} from 'vanilla-starter/TagGroup';
66
import vanillaDocs from 'docs:vanilla-starter/TagGroup';
77
import '../../tailwind/tailwind.css';
88
import Anatomy from '@react-aria/tag/docs/anatomy.svg';
9+
import {InlineAlert, Heading, Content} from '@react-spectrum/s2';
910

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

7879
### Links
7980

80-
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.
81+
Use the `href` prop on a `<Tag>` to create a link.
8182

8283
```tsx render
8384
"use client";
@@ -95,6 +96,11 @@ import {TagGroup, Tag} from 'vanilla-starter/TagGroup';
9596
</TagGroup>
9697
```
9798

99+
<InlineAlert variant="notice">
100+
<Heading>Client-side routing</Heading>
101+
<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>
102+
</InlineAlert>
103+
98104
## Selection
99105

100106
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.

0 commit comments

Comments
 (0)