Skip to content

Commit 24bffc0

Browse files
authored
Update RAC Link API (#5074)
1 parent b01500b commit 24bffc0

File tree

6 files changed

+95
-63
lines changed

6 files changed

+95
-63
lines changed

packages/react-aria-components/docs/Breadcrumbs.mdx

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ type: component
4646
import {Breadcrumbs, Breadcrumb, Link} from 'react-aria-components';
4747

4848
<Breadcrumbs>
49-
<Breadcrumb><Link><a href="/">Home</a></Link></Breadcrumb>
50-
<Breadcrumb><Link><a href="/react-aria">React Aria</a></Link></Breadcrumb>
49+
<Breadcrumb><Link href="/">Home</Link></Breadcrumb>
50+
<Breadcrumb><Link href="/react-aria">React Aria</Link></Breadcrumb>
5151
<Breadcrumb><Link>Breadcrumbs</Link></Breadcrumb>
5252
</Breadcrumbs>
5353
```
@@ -127,7 +127,7 @@ import {Breadcrumbs, Breadcrumb, Link} from 'react-aria-components';
127127
Breadcrumbs provide a list of links to parent pages of the current page in hierarchical order.
128128
`Breadcrumbs` helps implement these in an accessible way.
129129

130-
* **Flexible** – Support for navigation links, JavaScript handled links, or custom element types (e.g. router links).
130+
* **Flexible** – Support for HTML navigation links, JavaScript handled links, and client side routing.
131131
* **Accessible** – Implemented as an ordered list of links. The last link is automatically marked as the current page using `aria-current`.
132132
* **Styleable** – Hover, press, and keyboard focus states are provided for easy styling. These states only apply when interacting with an appropriate input device, unlike CSS pseudo classes.
133133

@@ -207,19 +207,40 @@ function Example() {
207207
}
208208
```
209209

210-
## Router links
210+
### Client side routing
211211

212-
The `<Link>` component can wrap a custom link element provided by a router like [React Router](https://reactrouter.com/en/main).
212+
The `<Link>` component works with frameworks and client side routers like [Next.js](https://nextjs.org/) and [React Router](https://reactrouter.com/en/main). As with other React Aria components that support links, this works via the `RouterProvider` component at the root of your app. Any `<Link>` within a `<RouterProvider>` will trigger the provided `navigate` function when pressed, and prevent the browser default navigation behavior.
213+
214+
This example uses React Router but the structure is applicable for any router.
213215

214216
```tsx
215-
import {Link as RouterLink} from 'react-router-dom';
217+
import {RouterProvider} from 'react-aria-components';
218+
import {useNavigate} from 'react-router-dom';
216219

217-
<Breadcrumbs>
218-
<Breadcrumb><Link><RouterLink to="/foo">Foo</RouterLink></Link></Breadcrumb>
219-
<Breadcrumb><Link>Bar</Link></Breadcrumb>
220-
</Breadcrumbs>
220+
function App({children}) {
221+
let navigate = useNavigate();
222+
return (
223+
<RouterProvider navigate={navigate}>
224+
{children}
225+
</RouterProvider>
226+
);
227+
}
228+
```
229+
230+
With this setup in the root of your app, any link within it will automatically trigger client side routing.
231+
232+
```tsx
233+
<App>
234+
{/* ... */}
235+
<Breadcrumbs>
236+
<Breadcrumb><Link href="/foo">Foo</Link></Breadcrumb>
237+
<Breadcrumb><Link>Bar</Link></Breadcrumb>
238+
</Breadcrumbs>
239+
</App>
221240
```
222241

242+
Note that external links to different origins will not trigger client side routing.
243+
223244
## Separator icons
224245

225246
The above examples use the CSS `:after` pseudo class to add separators between each item. These may also be DOM elements instead, e.g. SVG icons. Be sure that they have `aria-hidden="true"` so they are hidden from assistive technologies.
@@ -229,7 +250,7 @@ import ChevronIcon from '@spectrum-icons/workflow/ChevronDoubleRight';
229250

230251
<Breadcrumbs>
231252
<Breadcrumb className="my-item">
232-
<Link><a href="/">Home</a></Link>
253+
<Link href="/">Home</Link>
233254
<ChevronIcon size="S" />
234255
</Breadcrumb>
235256
<Breadcrumb><Link>React Aria</Link></Breadcrumb>
@@ -256,8 +277,8 @@ When breadcrumbs are used as a main navigation element for a page, they can be p
256277
```tsx example
257278
<nav aria-label="Breadcrumbs">
258279
<Breadcrumbs>
259-
<Breadcrumb><Link><a href="/">Home</a></Link></Breadcrumb>
260-
<Breadcrumb><Link><a href="/react-aria">React Aria</a></Link></Breadcrumb>
280+
<Breadcrumb><Link href="/">Home</Link></Breadcrumb>
281+
<Breadcrumb><Link href="/react-aria">React Aria</Link></Breadcrumb>
261282
<Breadcrumb><Link>Breadcrumbs</Link></Breadcrumb>
262283
</Breadcrumbs>
263284
</nav>
@@ -271,8 +292,8 @@ Breadcrumbs can be disabled using the `isDisabled` prop. This indicates that nav
271292

272293
```tsx example
273294
<Breadcrumbs isDisabled>
274-
<Breadcrumb><Link><a href="/">Home</a></Link></Breadcrumb>
275-
<Breadcrumb><Link><a href="/react-aria">React Aria</a></Link></Breadcrumb>
295+
<Breadcrumb><Link href="/">Home</Link></Breadcrumb>
296+
<Breadcrumb><Link href="/react-aria">React Aria</Link></Breadcrumb>
276297
<Breadcrumb><Link>Breadcrumbs</Link></Breadcrumb>
277298
</Breadcrumbs>
278299
```
@@ -281,8 +302,8 @@ Individual breadcrumbs can also be disabled by passing the `isDisabled` prop to
281302

282303
```tsx example
283304
<Breadcrumbs>
284-
<Breadcrumb><Link><a href="/">Home</a></Link></Breadcrumb>
285-
<Breadcrumb><Link isDisabled><a href="/react-aria">React Aria</a></Link></Breadcrumb>
305+
<Breadcrumb><Link href="/">Home</Link></Breadcrumb>
306+
<Breadcrumb><Link isDisabled href="/react-aria">React Aria</Link></Breadcrumb>
286307
<Breadcrumb><Link>Breadcrumbs</Link></Breadcrumb>
287308
</Breadcrumbs>
288309
```

packages/react-aria-components/docs/Button.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,8 @@ The `Button` component always represents a button semantically. To create a link
166166
```tsx example
167167
import {Link} from 'react-aria-components';
168168

169-
<Link className="react-aria-Button">
170-
<a href="https://adobe.com/" target="_blank">Adobe</a>
169+
<Link className="react-aria-Button" href="https://adobe.com/" target="_blank">
170+
Adobe
171171
</Link>
172172
```
173173

packages/react-aria-components/docs/Link.mdx

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,8 @@ type: component
4343
```tsx example
4444
import {Link} from 'react-aria-components';
4545

46-
<Link>
47-
<a href="https://www.imdb.com/title/tt6348138/" target="_blank">
48-
The missing link
49-
</a>
46+
<Link href="https://www.imdb.com/title/tt6348138/" target="_blank">
47+
The missing link
5048
</Link>
5149
```
5250

@@ -109,7 +107,7 @@ element with an `href` attribute. However, if the link does not have an href, an
109107
handled client side with JavaScript instead, it will not be exposed to assistive technology properly.
110108
`Link` helps achieve accessible links with either native HTML elements or custom element types.
111109

112-
* **Flexible** – Support for navigation links, JavaScript handled links, or custom element types (e.g. router links). Disabled links are also supported.
110+
* **Flexible** – Support for HTML navigation links, JavaScript handled links, and client side routing. Disabled links are also supported.
113111
* **Accessible** – Implemented as a custom ARIA link when handled via JavaScript, and otherwise as a native HTML link.
114112
* **Styleable** – Hover, press, and keyboard focus states are provided for easy styling. These states only apply when interacting with an appropriate input device, unlike CSS pseudo classes.
115113

@@ -122,23 +120,42 @@ keyboard users may activate links using the <Keyboard>Enter</Keyboard> key.
122120
If a visual label is not provided (e.g. an icon or image only link), then an `aria-label` or
123121
`aria-labelledby` prop must be passed to identify the link to assistive technology.
124122

125-
## Content
123+
## Events
124+
125+
### Client side routing
126126

127-
### Router links
127+
The `<Link>` component works with frameworks and client side routers like [Next.js](https://nextjs.org/) and [React Router](https://reactrouter.com/en/main). As with other React Aria components that support links, this works via the `RouterProvider` component at the root of your app. Any `<Link>` within a `<RouterProvider>` will trigger the provided `navigate` function when pressed, and prevent the browser default navigation behavior.
128128

129-
The `<Link>` component can wrap a custom link element provided by a router like [React Router](https://reactrouter.com/en/main).
129+
This example uses React Router but the structure is applicable for any router.
130130

131131
```tsx
132-
import {Link as RouterLink} from 'react-router-dom';
132+
import {RouterProvider} from 'react-aria-components';
133+
import {useNavigate} from 'react-router-dom';
133134

134-
<Link>
135-
<RouterLink to="/foo">Foo</RouterLink>
136-
</Link>
135+
function App({children}) {
136+
let navigate = useNavigate();
137+
return (
138+
<RouterProvider navigate={navigate}>
139+
{children}
140+
</RouterProvider>
141+
);
142+
}
137143
```
138144

139-
### Client handled links
145+
With this setup in the root of your app, any link within it will automatically trigger client side routing.
146+
147+
```tsx
148+
<App>
149+
{/* ... */}
150+
<Link href="/foo">Foo</Link>
151+
</App>
152+
```
153+
154+
Note that external links to different origins will not trigger client side routing.
155+
156+
### JavaScript handled links
140157

141-
When the content is plain text, a `<Link>` is rendered as a `<span>` but exposed to assistive technologies as a link. Events will need to be handled in JavaScript with the `onPress` prop.
158+
When a `<Link`> does not have an `href` prop, it is rendered as a `<span role="link">` instead of an `<a>`. Events will need to be handled in JavaScript with the `onPress` prop.
142159

143160
Note: this will not behave like a native link. Browser features like context menus and open in new tab will not apply.
144161

@@ -176,7 +193,7 @@ link elements as well as client handled links. Native navigation will be disable
176193
event will not be fired. The link will be exposed as disabled to assistive technology with ARIA.
177194

178195
```tsx example
179-
<Link isDisabled><a href="https://adobe.com" target="_blank">Disabled link</a></Link>
196+
<Link isDisabled href="https://adobe.com" target="_blank">Disabled link</Link>
180197
```
181198

182199
## Props
@@ -276,7 +293,7 @@ Now any `Link` inside a `Router` will update the router state when it is pressed
276293
```
277294

278295
```css hidden
279-
ul {
296+
ul:not([class]) {
280297
padding: 0px;
281298
}
282299
```

packages/react-aria-components/src/Link.tsx

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212

1313
import {AriaLinkOptions, mergeProps, useFocusRing, useHover, useLink} from 'react-aria';
1414
import {ContextValue, forwardRefType, RenderProps, SlotProps, useContextProps, useRenderProps} from './utils';
15-
import {filterDOMProps, mergeRefs} from '@react-aria/utils';
16-
import React, {createContext, ForwardedRef, forwardRef, useMemo} from 'react';
15+
import {LinkDOMProps} from '@react-types/shared';
16+
import React, {createContext, ElementType, ForwardedRef, forwardRef} from 'react';
1717

18-
export interface LinkProps extends Omit<AriaLinkOptions, 'elementType'>, RenderProps<LinkRenderProps>, SlotProps {}
18+
export interface LinkProps extends Omit<AriaLinkOptions, 'elementType'>, LinkDOMProps, RenderProps<LinkRenderProps>, SlotProps {}
1919

2020
export interface LinkRenderProps {
2121
/**
@@ -55,8 +55,8 @@ export const LinkContext = createContext<ContextValue<LinkProps, HTMLAnchorEleme
5555
function Link(props: LinkProps, ref: ForwardedRef<HTMLAnchorElement>) {
5656
[props, ref] = useContextProps(props, ref, LinkContext);
5757

58-
let elementType = typeof props.children === 'string' || typeof props.children === 'function' ? 'span' : 'a';
59-
let {linkProps, isPressed} = useLink({...props, elementType}, ref);
58+
let ElementType: ElementType = props.href ? 'a' : 'span';
59+
let {linkProps, isPressed} = useLink({...props, elementType: ElementType}, ref);
6060

6161
let {hoverProps, isHovered} = useHover(props);
6262
let {focusProps, isFocused, isFocusVisible} = useFocusRing();
@@ -74,26 +74,20 @@ function Link(props: LinkProps, ref: ForwardedRef<HTMLAnchorElement>) {
7474
}
7575
});
7676

77-
let DOMProps = filterDOMProps(props);
78-
delete DOMProps.id;
79-
80-
let element: any = typeof renderProps.children === 'string'
81-
? <span>{renderProps.children}</span>
82-
: React.Children.only(renderProps.children);
83-
84-
return React.cloneElement(element, {
85-
ref: useMemo(() => element.ref ? mergeRefs(element.ref, ref) : ref, [element.ref, ref]),
86-
slot: props.slot,
87-
...mergeProps(DOMProps, renderProps, linkProps, hoverProps, focusProps, {
88-
children: element.props.children,
89-
'data-focused': isFocused || undefined,
90-
'data-hovered': isHovered || undefined,
91-
'data-pressed': isPressed || undefined,
92-
'data-focus-visible': isFocusVisible || undefined,
93-
'data-current': !!props['aria-current'] || undefined,
94-
'data-disabled': props.isDisabled || undefined
95-
}, element.props)
96-
});
77+
return (
78+
<ElementType
79+
ref={ref}
80+
slot={props.slot}
81+
{...mergeProps(renderProps, linkProps, hoverProps, focusProps)}
82+
data-focused={isFocused || undefined}
83+
data-hovered={isHovered || undefined}
84+
data-pressed={isPressed || undefined}
85+
data-focus-visible={isFocusVisible || undefined}
86+
data-current={!!props['aria-current'] || undefined}
87+
data-disabled={props.isDisabled || undefined}>
88+
{renderProps.children}
89+
</ElementType>
90+
);
9791
}
9892

9993
/**

packages/react-aria-components/test/Breadcrumbs.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ import {render} from '@react-spectrum/test-utils';
1616

1717
let renderBreadcrumbs = (breadcrumbsProps, itemProps) => render(
1818
<Breadcrumbs {...breadcrumbsProps}>
19-
<Breadcrumb {...itemProps}><Link><a href="/">Home</a></Link></Breadcrumb>
20-
<Breadcrumb {...itemProps}><Link><a href="/react-aria">React Aria</a></Link></Breadcrumb>
19+
<Breadcrumb {...itemProps}><Link href="/">Home</Link></Breadcrumb>
20+
<Breadcrumb {...itemProps}><Link href="/react-aria">React Aria</Link></Breadcrumb>
2121
<Breadcrumb {...itemProps}><Link>useBreadcrumbs</Link></Breadcrumb>
2222
</Breadcrumbs>
2323
);

packages/react-aria-components/test/Link.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ describe('Link', () => {
6161
});
6262

6363
it('should render a link with <a> element', () => {
64-
let {getByRole} = render(<Link><a href="test">Test</a></Link>);
64+
let {getByRole} = render(<Link href="test">Test</Link>);
6565
let link = getByRole('link');
6666
expect(link.tagName).toBe('A');
6767
expect(link).toHaveAttribute('class', 'react-aria-Link');

0 commit comments

Comments
 (0)