Skip to content
Open
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
353 changes: 57 additions & 296 deletions packages/@react-spectrum/s2/src/SideNav.tsx

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions packages/dev/s2-docs/pages/react-aria/RoutedSideNav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client';
import {RouterProvider} from 'react-aria-components';
import React, {ReactNode, useState} from 'react';

export function RoutedSideNav(props: {
children: ({selectedRoute}: {selectedRoute: string}) => ReactNode;
defaultSelectedRoute: string;
}) {
let {children} = props;
let [selectedRoute, setSelectedRoute] = useState<string>(props.defaultSelectedRoute);

let updateSelection = (href: string) => {
setSelectedRoute(href);
};

return <RouterProvider navigate={updateSelection}>{children({selectedRoute})}</RouterProvider>;
}
204 changes: 204 additions & 0 deletions packages/dev/s2-docs/pages/react-aria/SideNav.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import {Layout} from '../../src/Layout';
export default Layout;

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

export const tags = ['navigation', 'nav', 'sidebar'];
export const version = 'alpha';
export const description = 'A navigation component that displays a nested, hierarchical set of links, with support for keyboard navigation and a current route indicator.';

# SideNav

<PageDescription>{docs.exports.SideNav.description}</PageDescription>

<ExampleSwitcher>
```tsx render docs={docs.exports.SideNav} links={docs.links} props={[]} type="vanilla" files={["starters/docs/src/SideNav.tsx", "starters/docs/src/SideNav.css", "./RoutedSideNav.tsx"]}
"use client";
import {SideNav, SideNavItem} from 'vanilla-starter/SideNav';
import {RoutedSideNav} from './RoutedSideNav';

<RoutedSideNav defaultSelectedRoute="/photos">
{({selectedRoute}) => (
<SideNav aria-label="Files" selectedRoute={selectedRoute} defaultExpandedKeys={['files']}>
<SideNavItem id="home" href="/home" title="Home" />
<SideNavItem id="files" href="/files" title="Files">
<SideNavItem id="photos" href="/photos" title="Photos" />
<SideNavItem id="videos" href="/videos" title="Videos" />
</SideNavItem>
<SideNavItem id="shared" href="/shared" title="Shared" />
</SideNav>
)}
</RoutedSideNav>
```

```tsx render docs={docs.exports.SideNav} links={docs.links} props={[]} type="tailwind" files={["starters/tailwind/src/SideNav.tsx", "./RoutedSideNav.tsx"]}
"use client";
import {SideNav, SideNavItem} from 'tailwind-starter/SideNav';
import {RoutedSideNav} from './RoutedSideNav';

<RoutedSideNav defaultSelectedRoute="/photos">
{({selectedRoute}) => (
<SideNav aria-label="Files" selectedRoute={selectedRoute} defaultExpandedKeys={['files']}>
<SideNavItem id="home" href="/home" title="Home" />
<SideNavItem id="files" href="/files" title="Files">
<SideNavItem id="photos" href="/photos" title="Photos" />
<SideNavItem id="videos" href="/videos" title="Videos" />
</SideNavItem>
<SideNavItem id="shared" href="/shared" title="Shared" />
</SideNav>
)}
</RoutedSideNav>
```

</ExampleSwitcher>

<InlineAlert variant="notice">
<Heading>Accessibility</Heading>
<Content>`SideNav` renders as a tree so keyboard users can navigate and expand the hierarchy. When it acts as the main navigation for a page, place it inside a [navigation landmark](https://www.w3.org/WAI/ARIA/apg/patterns/landmarks/examples/navigation.html): wrap the `SideNav` in a `<nav>` element with an `aria-label` so assistive technology users can quickly find it.</Content>
</InlineAlert>

## Content

`SideNav` follows the [Collection Components API](collections?component=SideNav), accepting both static and dynamic collections. The example above shows a static collection. This example shows a dynamic collection, passing a list of objects to the `items` prop and a function to render the children.

```tsx render files={["./RoutedSideNav.tsx"]}
"use client";
import {SideNav, SideNavItem} from 'vanilla-starter/SideNav';
import {RoutedSideNav} from './RoutedSideNav';

function Example() {
let items = [
{id: 'overview', url: '/overview', label: 'Overview'},
{id: 'reports', url: '/reports', label: 'Reports'},
{id: 'settings', url: '/settings', label: 'Settings'}
];

return (
<RoutedSideNav defaultSelectedRoute="/reports">
{({selectedRoute}) => (
/*- begin highlight -*/
<SideNav aria-label="Sections" items={items} selectedRoute={selectedRoute}>
{item => <SideNavItem href={item.url} title={item.label} />}
</SideNav>
/*- end highlight -*/
)}
</RoutedSideNav>
);
}
```

## Current route

Each `SideNavItem` accepts an `href`. Set the `selectedRoute` prop on the `SideNav` to the current page's path, and the item whose `href` matches is marked with `aria-current="page"` (and a `data-current` attribute for styling).

Combine `SideNav` with a client side router by wrapping your app in a [RouterProvider](routing.html) so that activating a link updates the route. Then set `selectedRoute`. In this example the router navigation is stored in local state to show the current route updating as you activate links.

```tsx render
"use client";
import {SideNav, SideNavItem} from 'vanilla-starter/SideNav';
import {RouterProvider} from 'react-aria-components';
import {useState} from 'react';

function Example() {
let [route, setRoute] = useState('/inbox');
return (
/*- begin highlight -*/
<RouterProvider navigate={setRoute}>
<SideNav aria-label="Mail" selectedRoute={route}>
<SideNavItem href="/inbox" title="Inbox" />
<SideNavItem href="/drafts" title="Drafts" />
<SideNavItem href="/sent" title="Sent" />
</SideNav>
</RouterProvider>
/*- end highlight -*/
);
}
```

## Sections

Use `SideNavSection` to group related items, with an optional `SideNavHeader` to label each group.

```tsx render files={["./RoutedSideNav.tsx"]}
"use client";
import {SideNav, SideNavItem, SideNavSection, SideNavHeader} from 'vanilla-starter/SideNav';
import {RoutedSideNav} from './RoutedSideNav';

<RoutedSideNav defaultSelectedRoute="/projects/apollo">
{({selectedRoute}) => (
<SideNav aria-label="Workspace" selectedRoute={selectedRoute}>
{/*- begin highlight -*/}
<SideNavSection>
<SideNavHeader>Personal</SideNavHeader>
<SideNavItem href="/home" title="Home" />
<SideNavItem href="/starred" title="Starred" />
</SideNavSection>
{/*- end highlight -*/}
<SideNavSection>
<SideNavHeader>Projects</SideNavHeader>
<SideNavItem href="/projects/apollo" title="Apollo" />
<SideNavItem href="/projects/gemini" title="Gemini" />
</SideNavSection>
</SideNav>
)}
</RoutedSideNav>
```

## Disabled

Set the `isDisabled` prop on a `SideNavItem` to disable it. Disabled items cannot be navigated to or focused.

```tsx render files={["./RoutedSideNav.tsx"]}
"use client";
import {SideNav, SideNavItem} from 'vanilla-starter/SideNav';
import {RoutedSideNav} from './RoutedSideNav';

<RoutedSideNav defaultSelectedRoute="/home">
{({selectedRoute}) => (
<SideNav aria-label="Files" selectedRoute={selectedRoute}>
<SideNavItem href="/home" title="Home" />
{/*- begin highlight -*/}
<SideNavItem href="/archive" title="Archive" isDisabled />
{/*- end highlight -*/}
<SideNavItem href="/trash" title="Trash" />
</SideNav>
)}
</RoutedSideNav>
```

## API

```tsx links={{SideNav: '#sidenav', SideNavItem: '#sidenavitem', SideNavItemContent: '#sidenavitemcontent', SideNavSection: '#sidenavsection', SideNavHeader: '#sidenavheader', Link: 'Link'}}
<SideNav>
<SideNavSection>
<SideNavHeader />
<SideNavItem>
<SideNavItemContent>
<Link />
</SideNavItemContent>
</SideNavItem>
</SideNavSection>
</SideNav>
```

### SideNav

<PropTable component={docs.exports.SideNav} links={docs.links} showDescription />

### SideNavItem

<PropTable component={docs.exports.SideNavItem} links={docs.links} showDescription />

### SideNavItemContent

<PropTable component={docs.exports.SideNavItemContent} links={docs.links} showDescription />

### SideNavSection

<PropTable component={docs.exports.SideNavSection} links={docs.links} showDescription />

### SideNavHeader

<PropTable component={docs.exports.SideNavHeader} links={docs.links} showDescription />
37 changes: 37 additions & 0 deletions packages/dev/s2-docs/pages/react-aria/router.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use client';
import React, {createContext, ReactNode, useContext, useState} from 'react';

// A tiny in-memory router that mirrors the parts of the `react-router` API used
// to integrate with React Aria. In a real app, `MemoryRouter`, `useNavigate`, and
// `useLocation` would come from the `react-router` package — the wiring above is
// identical.

interface Location {
pathname: string;
}

interface RouterContextValue {
location: Location;
navigate: (pathname: string) => void;
}

const RouterContext = createContext<RouterContextValue>({
location: {pathname: '/'},
navigate: () => {}
});

export function MemoryRouter(props: {initialEntries?: string[]; children: ReactNode}) {
let {initialEntries = ['/'], children} = props;
let [location, setLocation] = useState<Location>({pathname: initialEntries[0]});
let navigate = (pathname: string) => setLocation({pathname});

return <RouterContext.Provider value={{location, navigate}}>{children}</RouterContext.Provider>;
}

export function useLocation(): Location {
return useContext(RouterContext).location;
}

export function useNavigate(): (pathname: string) => void {
return useContext(RouterContext).navigate;
}
Loading