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
355 changes: 58 additions & 297 deletions packages/@react-spectrum/s2/src/SideNav.tsx

Large diffs are not rendered by default.

182 changes: 182 additions & 0 deletions packages/dev/s2-docs/pages/react-aria/NavigationTree.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
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.';

# NavigationTree

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

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO the style is a bit heavy for a sidenav. Maybe we can make it a bit closer to what we have in S2 with the selection line

import {RoutedNavigationTree} from './RoutedNavigationTree';

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

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

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

</ExampleSwitcher>

<InlineAlert variant="notice">
<Heading>Accessibility</Heading>
<Content>`NavigationTree` 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 `NavigationTree` in a `<nav>` element with an `aria-label` so assistive technology users can quickly find it.</Content>
</InlineAlert>

## Content

`NavigationTree` follows the [Collection Components API](collections?component=NavigationTree), 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={["./RoutedNavigationTree.tsx"]}
"use client";
import {NavigationTree, NavigationTreeItem} from 'vanilla-starter/NavigationTree';
import {RoutedNavigationTree} from './RoutedNavigationTree';

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

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

## Current route

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noticed that you can't see the focus ring on the selected item with the current styles


Each `NavigationTreeItem` accepts an `href`. Set the `selectedRoute` prop on the `NavigationTree` 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 `NavigationTree` 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 {NavigationTree, NavigationTreeItem} from 'vanilla-starter/NavigationTree';
import {RouterProvider} from 'react-aria-components';
import {useState} from 'react';

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

## Sections

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nest the above sections under Content to match other pages


Use `NavigationTreeSection` to group related items, with an optional `NavigationTreeHeader` to label each group.

```tsx render files={["./RoutedNavigationTree.tsx"]}
"use client";
import {NavigationTree, NavigationTreeItem, NavigationTreeSection, NavigationTreeHeader} from 'vanilla-starter/NavigationTree';
import {RoutedNavigationTree} from './RoutedNavigationTree';

<RoutedNavigationTree defaultSelectedRoute="/projects/apollo">
{({selectedRoute}) => (
<NavigationTree aria-label="Workspace" selectedRoute={selectedRoute}>
{/*- begin highlight -*/}
<NavigationTreeSection>
<NavigationTreeHeader>Personal</NavigationTreeHeader>
<NavigationTreeItem href="/home" title="Home" />
<NavigationTreeItem href="/starred" title="Starred" />
</NavigationTreeSection>
{/*- end highlight -*/}
<NavigationTreeSection>
<NavigationTreeHeader>Projects</NavigationTreeHeader>
<NavigationTreeItem href="/projects/apollo" title="Apollo" />
<NavigationTreeItem href="/projects/gemini" title="Gemini" />
</NavigationTreeSection>
</NavigationTree>
)}
</RoutedNavigationTree>
```

## API

```tsx links={{NavigationTree: '#NavigationTree', NavigationTreeItem: '#NavigationTreeitem', NavigationTreeItemContent: '#NavigationTreeitemcontent', NavigationTreeSection: '#NavigationTreesection', NavigationTreeHeader: '#NavigationTreeheader', Link: 'Link'}}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to have an anatomy diagram...

<NavigationTree>
<NavigationTreeSection>
<NavigationTreeHeader />
<NavigationTreeItem>
<NavigationTreeItemContent>
<Link />
</NavigationTreeItemContent>
</NavigationTreeItem>
</NavigationTreeSection>
</NavigationTree>
```

### NavigationTree

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

### NavigationTreeItem

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

### NavigationTreeItemContent

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

### NavigationTreeSection

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

### NavigationTreeHeader

<PropTable component={docs.exports.NavigationTreeHeader} links={docs.links} showDescription />
17 changes: 17 additions & 0 deletions packages/dev/s2-docs/pages/react-aria/RoutedNavigationTree.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 RoutedNavigationTree(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>;
}
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