diff --git a/packages/react-aria-components/src/Navigation.tsx b/packages/react-aria-components/src/Navigation.tsx new file mode 100644 index 00000000000..cb0470d9fec --- /dev/null +++ b/packages/react-aria-components/src/Navigation.tsx @@ -0,0 +1,183 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {ContextValue, Provider, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot, useSlottedContext} from './utils'; +import {DOMAttributes, DOMProps, forwardRefType, Key, MultipleSelection} from '@react-types/shared'; +import {filterDOMProps, useId} from '@react-aria/utils'; +import {HeaderContext} from './Header'; +import {LinkContext} from './Link'; +import {Node} from 'react-stately'; +import {Orientation} from 'react-aria'; +import React, {createContext, ForwardedRef, forwardRef, ReactElement, ReactNode} from 'react'; + +// TODO: Replace NavigationRenderProps with AriaNavigationProps, once it exists +export interface NavigationProps extends NavigationRenderProps, /* RenderProps, */ DOMProps, SlotProps { + /** Whether the navigation is disabled. */ + isDisabled?: boolean, + /** Handler that is called when a navigation item is clicked. */ + onAction?: (key: Key) => void +} + +export interface NavigationRenderProps { + /** + * The orientation of the navigation. + * @selector [data-orientation="horizontal | vertical"] + */ + orientation: Orientation +} + +export const NavigationContext = createContext>(null); + +function Navigation(props: NavigationProps, ref: ForwardedRef) { + [props, ref] = useContextProps(props, ref, NavigationContext); + + let renderProps = useRenderProps({ + ...props, + defaultClassName: 'react-aria-Navigation', + values: {isDisabled: props.isDisabled} + }); + + let domProps = filterDOMProps(props); + + return ( + + ); + +} + +// TODO: Remove NavigationItemRenderProps and use AriaNavigationItemProps when it exists +export interface NavigationItemProps extends NavigationItemRenderProps, RenderProps { + /** A unique id for the navigation item, which will be passed to `onAction` when the breadcrumb is pressed. */ + id?: Key +} + +export interface NavigationItemRenderProps { + /** + * Whether the navigation item is for the current page. + * @selector [data-current] + */ + isCurrent: boolean, + /** + * Whether the navigation item is disabled. + * @selector [data-disabled] + */ + isDisabled: boolean +} + +// TODO: Does this need a context? + +function NavigationItem(props: NavigationItemProps, ref: ForwardedRef) { + let { + id, + isCurrent, + ...otherProps + } = props; + let {isDisabled, onAction} = useSlottedContext(NavigationContext)!; + + // Generate an id if one wasn't provided. + // (can't pass id into useId since it can also be a number) + let defaultId = useId(); + id ||= defaultId; + + let linkProps = { + 'aria-current': isCurrent ? 'page' : null, + isDisabled: isDisabled || otherProps.isDisabled || isCurrent, + onPress: () => onAction?.(id) + }; + + let renderProps = useRenderProps({ + ...otherProps, + defaultClassName: 'react-aria-NavigationItem', + values: { + isDisabled: isDisabled || isCurrent, + isCurrent + } + }); + + let domProps = filterDOMProps(otherProps as any); + + return ( +
  • + + {renderProps.children} + +
  • + ); +} + +// TODO: Remove the Node +export interface NavigationSectionProps extends Node, MultipleSelection { + // TODO: describe or refactor + children: ReactNode | ((item: object) => ReactElement) +} + +function NavigationSection(props: NavigationSectionProps, ref: ForwardedRef, section: Node, className = 'react-aria-MenuSection') { + // TODO: State for selection + let [headingRef] = useSlot(); + // TODO: Do we need a use hook for NavigationSection? + let renderProps = useRenderProps({ + defaultClassName: className, + // className: section.props?.className, + // style: section.props?.style, + values: {} + }); + + return ( +
    + + {props.children} + +
    + ); +} + +/** + * A navigation is a grouping of navigation links. + */ +const _Navigation = /*#__PURE__*/ (forwardRef as forwardRefType)(Navigation); +export {_Navigation as Navigation}; + +/** + * A navigation item is a navigation link. + */ +const _NavigationItem = /*#__PURE__*/ (forwardRef as forwardRefType)(NavigationItem); +export {_NavigationItem as NavigationItem}; + +/** + * A NavigationSection represents a section within a navigation. + */ +const _NavigationSection = /*#__PURE__*/ /*#__PURE__*/ (forwardRef as forwardRefType)(NavigationSection); +export {_NavigationSection as NavigationSection}; diff --git a/packages/react-aria-components/stories/Navigation.stories.tsx b/packages/react-aria-components/stories/Navigation.stories.tsx new file mode 100644 index 00000000000..2948239e4b6 --- /dev/null +++ b/packages/react-aria-components/stories/Navigation.stories.tsx @@ -0,0 +1,91 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {Button} from '../src/Button'; +import {Disclosure, DisclosurePanel} from '../src/Disclosure'; +import {Header} from '../src/Header'; +import {Link} from '../src/Link'; +import {Navigation, NavigationItem, NavigationSection} from '../src/Navigation'; +import React from 'react'; +import './styles.css'; + +export default { + title: 'React Aria Components', + component: Navigation, + args: { + orientation: 'vertical' + }, + argTypes: { + isDisabled: { + control: 'boolean' + }, + orientation: { + control: 'radio', + options: ['horizontal', 'vertical'] + } + } +}; + +export const NavigationExample = (args: any) => ( + + + Releases + + +
    Libraries
    + + Internationslized + + + React Spectrum + + + React Aria + + + React Stately + + + React Spectrum 2 + +
    + + + {({isExpanded}) => ( + <> +
    + +
    + + + Button + + + Disclosure + + + Button + + + Link + + + Menu + + + + )} +
    +
    +
    +); + diff --git a/packages/react-aria-components/stories/styles.css b/packages/react-aria-components/stories/styles.css index 451d50eb9ba..b203a4236e5 100644 --- a/packages/react-aria-components/stories/styles.css +++ b/packages/react-aria-components/stories/styles.css @@ -398,3 +398,29 @@ align-items: center } } + + +:global(.react-aria-Navigation) { + width: fit-content; + + :global(.react-aria-NavigationList) { + display: flex; + flex-direction: column; + gap: 8px; + list-style-type: none; + padding: 0; + + [data-current] { + font-weight: bold; + opacity: 1; + } + + &[data-orientation=horizontal] { + flex-direction: row; + } + } + + &[data-disabled], [data-disabled] { + opacity: 0.4; + } +}