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
55 changes: 38 additions & 17 deletions packages/react-aria-components/docs/Breadcrumbs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ type: component
import {Breadcrumbs, Breadcrumb, Link} from 'react-aria-components';

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

* **Flexible** – Support for navigation links, JavaScript handled links, or custom element types (e.g. router links).
* **Flexible** – Support for HTML navigation links, JavaScript handled links, and client side routing.
* **Accessible** – Implemented as an ordered list of links. The last link is automatically marked as the current page using `aria-current`.
* **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.

Expand Down Expand Up @@ -207,19 +207,40 @@ function Example() {
}
```

## Router links
### Client side routing

The `<Link>` component can wrap a custom link element provided by a router like [React Router](https://reactrouter.com/en/main).
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.

This example uses React Router but the structure is applicable for any router.

```tsx
import {Link as RouterLink} from 'react-router-dom';
import {RouterProvider} from 'react-aria-components';
import {useNavigate} from 'react-router-dom';

<Breadcrumbs>
<Breadcrumb><Link><RouterLink to="/foo">Foo</RouterLink></Link></Breadcrumb>
<Breadcrumb><Link>Bar</Link></Breadcrumb>
</Breadcrumbs>
function App({children}) {
let navigate = useNavigate();
return (
<RouterProvider navigate={navigate}>
{children}
</RouterProvider>
);
}
```

With this setup in the root of your app, any link within it will automatically trigger client side routing.

```tsx
<App>
{/* ... */}
<Breadcrumbs>
<Breadcrumb><Link href="/foo">Foo</Link></Breadcrumb>
<Breadcrumb><Link>Bar</Link></Breadcrumb>
</Breadcrumbs>
</App>
```

Note that external links to different origins will not trigger client side routing.

## Separator icons

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.
Expand All @@ -229,7 +250,7 @@ import ChevronIcon from '@spectrum-icons/workflow/ChevronDoubleRight';

<Breadcrumbs>
<Breadcrumb className="my-item">
<Link><a href="/">Home</a></Link>
<Link href="/">Home</Link>
<ChevronIcon size="S" />
</Breadcrumb>
<Breadcrumb><Link>React Aria</Link></Breadcrumb>
Expand All @@ -256,8 +277,8 @@ When breadcrumbs are used as a main navigation element for a page, they can be p
```tsx example
<nav aria-label="Breadcrumbs">
<Breadcrumbs>
<Breadcrumb><Link><a href="/">Home</a></Link></Breadcrumb>
<Breadcrumb><Link><a href="/react-aria">React Aria</a></Link></Breadcrumb>
<Breadcrumb><Link href="/">Home</Link></Breadcrumb>
<Breadcrumb><Link href="/react-aria">React Aria</Link></Breadcrumb>
<Breadcrumb><Link>Breadcrumbs</Link></Breadcrumb>
</Breadcrumbs>
</nav>
Expand All @@ -271,8 +292,8 @@ Breadcrumbs can be disabled using the `isDisabled` prop. This indicates that nav

```tsx example
<Breadcrumbs isDisabled>
<Breadcrumb><Link><a href="/">Home</a></Link></Breadcrumb>
<Breadcrumb><Link><a href="/react-aria">React Aria</a></Link></Breadcrumb>
<Breadcrumb><Link href="/">Home</Link></Breadcrumb>
<Breadcrumb><Link href="/react-aria">React Aria</Link></Breadcrumb>
<Breadcrumb><Link>Breadcrumbs</Link></Breadcrumb>
</Breadcrumbs>
```
Expand All @@ -281,8 +302,8 @@ Individual breadcrumbs can also be disabled by passing the `isDisabled` prop to

```tsx example
<Breadcrumbs>
<Breadcrumb><Link><a href="/">Home</a></Link></Breadcrumb>
<Breadcrumb><Link isDisabled><a href="/react-aria">React Aria</a></Link></Breadcrumb>
<Breadcrumb><Link href="/">Home</Link></Breadcrumb>
<Breadcrumb><Link isDisabled href="/react-aria">React Aria</Link></Breadcrumb>
<Breadcrumb><Link>Breadcrumbs</Link></Breadcrumb>
</Breadcrumbs>
```
Expand Down
4 changes: 2 additions & 2 deletions packages/react-aria-components/docs/Button.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,8 @@ The `Button` component always represents a button semantically. To create a link
```tsx example
import {Link} from 'react-aria-components';

<Link className="react-aria-Button">
<a href="https://adobe.com/" target="_blank">Adobe</a>
<Link className="react-aria-Button" href="https://adobe.com/" target="_blank">
Adobe
</Link>
```

Expand Down
49 changes: 33 additions & 16 deletions packages/react-aria-components/docs/Link.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,8 @@ type: component
```tsx example
import {Link} from 'react-aria-components';

<Link>
<a href="https://www.imdb.com/title/tt6348138/" target="_blank">
The missing link
</a>
<Link href="https://www.imdb.com/title/tt6348138/" target="_blank">
The missing link
</Link>
```

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

* **Flexible** – Support for navigation links, JavaScript handled links, or custom element types (e.g. router links). Disabled links are also supported.
* **Flexible** – Support for HTML navigation links, JavaScript handled links, and client side routing. Disabled links are also supported.
* **Accessible** – Implemented as a custom ARIA link when handled via JavaScript, and otherwise as a native HTML link.
* **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.

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

## Content
## Events

### Client side routing

### Router links
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.

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

```tsx
import {Link as RouterLink} from 'react-router-dom';
import {RouterProvider} from 'react-aria-components';
import {useNavigate} from 'react-router-dom';

<Link>
<RouterLink to="/foo">Foo</RouterLink>
</Link>
function App({children}) {
let navigate = useNavigate();
return (
<RouterProvider navigate={navigate}>
{children}
</RouterProvider>
);
}
```

### Client handled links
With this setup in the root of your app, any link within it will automatically trigger client side routing.

```tsx
<App>
{/* ... */}
<Link href="/foo">Foo</Link>
</App>
```

Note that external links to different origins will not trigger client side routing.

### JavaScript handled links

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

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

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

```tsx example
<Link isDisabled><a href="https://adobe.com" target="_blank">Disabled link</a></Link>
<Link isDisabled href="https://adobe.com" target="_blank">Disabled link</Link>
```

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

```css hidden
ul {
ul:not([class]) {
padding: 0px;
}
```
Expand Down
44 changes: 19 additions & 25 deletions packages/react-aria-components/src/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@

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

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

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

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

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

let DOMProps = filterDOMProps(props);
delete DOMProps.id;

let element: any = typeof renderProps.children === 'string'
? <span>{renderProps.children}</span>
: React.Children.only(renderProps.children);

return React.cloneElement(element, {
ref: useMemo(() => element.ref ? mergeRefs(element.ref, ref) : ref, [element.ref, ref]),
slot: props.slot,
...mergeProps(DOMProps, renderProps, linkProps, hoverProps, focusProps, {
children: element.props.children,
'data-focused': isFocused || undefined,
'data-hovered': isHovered || undefined,
'data-pressed': isPressed || undefined,
'data-focus-visible': isFocusVisible || undefined,
'data-current': !!props['aria-current'] || undefined,
'data-disabled': props.isDisabled || undefined
}, element.props)
});
return (
<ElementType
ref={ref}
slot={props.slot}
{...mergeProps(renderProps, linkProps, hoverProps, focusProps)}
data-focused={isFocused || undefined}
data-hovered={isHovered || undefined}
data-pressed={isPressed || undefined}
data-focus-visible={isFocusVisible || undefined}
data-current={!!props['aria-current'] || undefined}
data-disabled={props.isDisabled || undefined}>
{renderProps.children}
</ElementType>
);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/react-aria-components/test/Breadcrumbs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import {render} from '@react-spectrum/test-utils';

let renderBreadcrumbs = (breadcrumbsProps, itemProps) => render(
<Breadcrumbs {...breadcrumbsProps}>
<Breadcrumb {...itemProps}><Link><a href="/">Home</a></Link></Breadcrumb>
<Breadcrumb {...itemProps}><Link><a href="/react-aria">React Aria</a></Link></Breadcrumb>
<Breadcrumb {...itemProps}><Link href="/">Home</Link></Breadcrumb>
<Breadcrumb {...itemProps}><Link href="/react-aria">React Aria</Link></Breadcrumb>
<Breadcrumb {...itemProps}><Link>useBreadcrumbs</Link></Breadcrumb>
</Breadcrumbs>
);
Expand Down
2 changes: 1 addition & 1 deletion packages/react-aria-components/test/Link.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe('Link', () => {
});

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