Skip to content

Commit b1a10d5

Browse files
committed
[feature] RAC Navigation component
1 parent ab9fd5c commit b1a10d5

File tree

3 files changed

+300
-0
lines changed

3 files changed

+300
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/*
2+
* Copyright 2024 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import {ContextValue, Provider, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot, useSlottedContext} from './utils';
14+
import {DOMAttributes, DOMProps, forwardRefType, Key, MultipleSelection} from '@react-types/shared';
15+
import {filterDOMProps, useId} from '@react-aria/utils';
16+
import {HeaderContext} from './Header';
17+
import {LinkContext} from './Link';
18+
import {Node} from 'react-stately';
19+
import {Orientation} from 'react-aria';
20+
import React, {createContext, ForwardedRef, forwardRef, ReactElement, ReactNode} from 'react';
21+
22+
// TODO: Replace NavigationRenderProps with AriaNavigationProps, once it exists
23+
export interface NavigationProps extends NavigationRenderProps, /* RenderProps<NavigationRenderProps>, */ DOMProps, SlotProps {
24+
/** Whether the navigation is disabled. */
25+
isDisabled?: boolean,
26+
/** Handler that is called when a navigation item is clicked. */
27+
onAction?: (key: Key) => void
28+
}
29+
30+
export interface NavigationRenderProps {
31+
/**
32+
* The orientation of the navigation.
33+
* @selector [data-orientation="horizontal | vertical"]
34+
*/
35+
orientation: Orientation
36+
}
37+
38+
export const NavigationContext = createContext<ContextValue<NavigationProps, HTMLUListElement>>(null);
39+
40+
function Navigation(props: NavigationProps, ref: ForwardedRef<HTMLDivElement>) {
41+
[props, ref] = useContextProps(props, ref, NavigationContext);
42+
43+
let renderProps = useRenderProps({
44+
...props,
45+
defaultClassName: 'react-aria-Navigation',
46+
values: {isDisabled: props.isDisabled}
47+
});
48+
49+
let domProps = filterDOMProps(props);
50+
51+
return (
52+
<nav
53+
aria-label="Navigation"
54+
{...domProps}
55+
{...renderProps}
56+
ref={ref}
57+
data-disabled={props.isDisabled || undefined}>
58+
<NavigationContext.Provider value={props}>
59+
<ul
60+
className="react-aria-NavigationList"
61+
data-orientation={props.orientation || 'horizontal'}>
62+
{renderProps.children}
63+
</ul>
64+
</NavigationContext.Provider>
65+
</nav>
66+
);
67+
68+
}
69+
70+
// TODO: Remove NavigationItemRenderProps and use AriaNavigationItemProps when it exists
71+
export interface NavigationItemProps extends NavigationItemRenderProps, RenderProps<NavigationItemRenderProps> {
72+
/** A unique id for the navigation item, which will be passed to `onAction` when the breadcrumb is pressed. */
73+
id?: Key
74+
}
75+
76+
export interface NavigationItemRenderProps {
77+
/**
78+
* Whether the navigation item is for the current page.
79+
* @selector [data-current]
80+
*/
81+
isCurrent: boolean,
82+
/**
83+
* Whether the navigation item is disabled.
84+
* @selector [data-disabled]
85+
*/
86+
isDisabled: boolean
87+
}
88+
89+
// TODO: Does this need a context?
90+
91+
function NavigationItem(props: NavigationItemProps, ref: ForwardedRef<HTMLDivElement>) {
92+
let {
93+
id,
94+
isCurrent,
95+
...otherProps
96+
} = props;
97+
let {isDisabled, onAction} = useSlottedContext(NavigationContext)!;
98+
99+
// Generate an id if one wasn't provided.
100+
// (can't pass id into useId since it can also be a number)
101+
let defaultId = useId();
102+
id ||= defaultId;
103+
104+
let linkProps = {
105+
'aria-current': isCurrent ? 'page' : null,
106+
isDisabled: isDisabled || otherProps.isDisabled || isCurrent,
107+
onPress: () => onAction?.(id)
108+
};
109+
110+
let renderProps = useRenderProps({
111+
...otherProps,
112+
defaultClassName: 'react-aria-NavigationItem',
113+
values: {
114+
isDisabled: isDisabled || isCurrent,
115+
isCurrent
116+
}
117+
});
118+
119+
let domProps = filterDOMProps(otherProps as any);
120+
121+
return (
122+
<li
123+
{...domProps}
124+
{...renderProps}
125+
ref={ref}
126+
data-current={isCurrent || undefined}
127+
data-disabled={isDisabled || isCurrent || undefined}>
128+
<LinkContext.Provider value={linkProps}>
129+
{renderProps.children}
130+
</LinkContext.Provider>
131+
</li>
132+
);
133+
}
134+
135+
// TODO: Remove the Node<T>
136+
export interface NavigationSectionProps<T> extends Node<T>, MultipleSelection {
137+
// TODO: describe or refactor
138+
children: ReactNode | ((item: object) => ReactElement)
139+
}
140+
141+
function NavigationSection<T extends object>(props: NavigationSectionProps<T>, ref: ForwardedRef<HTMLElement>, section: Node<T>, className = 'react-aria-MenuSection') {
142+
// TODO: State for selection
143+
let [headingRef] = useSlot();
144+
// TODO: Do we need a use hook for NavigationSection?
145+
let renderProps = useRenderProps({
146+
defaultClassName: className,
147+
// className: section.props?.className,
148+
// style: section.props?.style,
149+
values: {}
150+
});
151+
152+
return (
153+
<section
154+
{...filterDOMProps(props as any)}
155+
{...renderProps}
156+
ref={ref}>
157+
<Provider
158+
values={[
159+
[HeaderContext, {...{role: 'presentation'} as DOMAttributes, ref: headingRef}]
160+
]}>
161+
{props.children}
162+
</Provider>
163+
</section>
164+
);
165+
}
166+
167+
/**
168+
* A navigation is a grouping of navigation links.
169+
*/
170+
const _Navigation = /*#__PURE__*/ (forwardRef as forwardRefType)(Navigation);
171+
export {_Navigation as Navigation};
172+
173+
/**
174+
* A navigation item is a navigation link.
175+
*/
176+
const _NavigationItem = /*#__PURE__*/ (forwardRef as forwardRefType)(NavigationItem);
177+
export {_NavigationItem as NavigationItem};
178+
179+
/**
180+
* A NavigationSection represents a section within a navigation.
181+
*/
182+
const _NavigationSection = /*#__PURE__*/ /*#__PURE__*/ (forwardRef as forwardRefType)(NavigationSection);
183+
export {_NavigationSection as NavigationSection};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright 2024 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import {Button} from '../src/Button';
14+
import {Disclosure, DisclosurePanel} from '../src/Disclosure';
15+
import {Header} from '../src/Header';
16+
import {Link} from '../src/Link';
17+
import {Navigation, NavigationItem, NavigationSection} from '../src/Navigation';
18+
import React from 'react';
19+
import './styles.css';
20+
21+
export default {
22+
title: 'React Aria Components',
23+
component: Navigation,
24+
args: {
25+
orientation: 'vertical'
26+
},
27+
argTypes: {
28+
isDisabled: {
29+
control: 'boolean'
30+
},
31+
orientation: {
32+
control: 'radio',
33+
options: ['horizontal', 'vertical']
34+
}
35+
}
36+
};
37+
38+
export const NavigationExample = (args: any) => (
39+
<Navigation {...args}>
40+
<NavigationItem>
41+
<Link href="//react-spectrum.adobe.com/releases/index.html">Releases</Link>
42+
</NavigationItem>
43+
<NavigationSection>
44+
<Header>Libraries</Header>
45+
<NavigationItem>
46+
<Link href="//react-spectrum.adobe.com/">Internationslized</Link>
47+
</NavigationItem>
48+
<NavigationItem isCurrent>
49+
<Link href="//react-spectrum.adobe.com/">React Spectrum</Link>
50+
</NavigationItem>
51+
<NavigationItem>
52+
<Link href="//react-spectrum.adobe.com/react-aria/">React Aria</Link>
53+
</NavigationItem>
54+
<NavigationItem>
55+
<Link href="//react-spectrum.adobe.com/react-state/">React Stately</Link>
56+
</NavigationItem>
57+
<NavigationItem isDisabled>
58+
<Link href="//react-spectrum.adobe.com/s2/">React Spectrum 2</Link>
59+
</NavigationItem>
60+
</NavigationSection>
61+
<NavigationSection>
62+
<Disclosure>
63+
{({isExpanded}) => (
64+
<>
65+
<Header>
66+
<Button slot="trigger">{isExpanded ? '⬇️' : '➡️'} React Aria Components</Button>
67+
</Header>
68+
<DisclosurePanel>
69+
<NavigationItem>
70+
<Link href="//react-spectrum.adobe.com/react-aria/Button.html">Button</Link>
71+
</NavigationItem>
72+
<NavigationItem>
73+
<Link href="//react-spectrum.adobe.com/react-aria/Disclosure.html">Disclosure</Link>
74+
</NavigationItem>
75+
<NavigationItem>
76+
<Link href="//react-spectrum.adobe.com/react-aria/Button.html">Button</Link>
77+
</NavigationItem>
78+
<NavigationItem>
79+
<Link href="//react-spectrum.adobe.com/react-aria/Link.html">Link</Link>
80+
</NavigationItem>
81+
<NavigationItem>
82+
<Link href="//react-spectrum.adobe.com/react-aria/Menu.html">Menu</Link>
83+
</NavigationItem>
84+
</DisclosurePanel>
85+
</>
86+
)}
87+
</Disclosure>
88+
</NavigationSection>
89+
</Navigation>
90+
);
91+

packages/react-aria-components/stories/styles.css

+26
Original file line numberDiff line numberDiff line change
@@ -398,3 +398,29 @@
398398
align-items: center
399399
}
400400
}
401+
402+
403+
:global(.react-aria-Navigation) {
404+
width: fit-content;
405+
406+
:global(.react-aria-NavigationList) {
407+
display: flex;
408+
flex-direction: column;
409+
gap: 8px;
410+
list-style-type: none;
411+
padding: 0;
412+
413+
[data-current] {
414+
font-weight: bold;
415+
opacity: 1;
416+
}
417+
418+
&[data-orientation=horizontal] {
419+
flex-direction: row;
420+
}
421+
}
422+
423+
&[data-disabled], [data-disabled] {
424+
opacity: 0.4;
425+
}
426+
}

0 commit comments

Comments
 (0)