Skip to content

Commit

Permalink
Navigation Component: Composition Proposal (#25057)
Browse files Browse the repository at this point in the history
Expose a `<Navigation />` component from `@wordpress/components` to render menus in a hierarchal fashion.
  • Loading branch information
Copons authored Sep 11, 2020
1 parent a5f574c commit 668a710
Show file tree
Hide file tree
Showing 13 changed files with 855 additions and 0 deletions.
6 changes: 6 additions & 0 deletions docs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1061,6 +1061,12 @@
"markdown_source": "../packages/components/src/navigable-container/README.md",
"parent": "components"
},
{
"title": "Navigation",
"slug": "navigation",
"markdown_source": "../packages/components/src/navigation/README.md",
"parent": "components"
},
{
"title": "Notice",
"slug": "notice",
Expand Down
2 changes: 2 additions & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Introduce `Navigation` component as `__experimentalNavigation` for displaying a hierarchy of items.

## 10.0.0 (2020-07-07)

### Breaking Change
Expand Down
4 changes: 4 additions & 0 deletions packages/components/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ export { default as MenuItemsChoice } from './menu-items-choice';
export { default as Modal } from './modal';
export { default as ScrollLock } from './scroll-lock';
export { NavigableMenu, TabbableContainer } from './navigable-container';
export { default as __experimentalNavigation } from './navigation';
export { default as __experimentalNavigationGroup } from './navigation/group';
export { default as __experimentalNavigationItem } from './navigation/item';
export { default as __experimentalNavigationMenu } from './navigation/menu';
export { default as Notice } from './notice';
export { default as __experimentalNumberControl } from './number-control';
export { default as NoticeList } from './notice/list';
Expand Down
193 changes: 193 additions & 0 deletions packages/components/src/navigation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# Navigation

Render a navigation list with optional groupings and hierarchy.

## Usage

```jsx
import {
__experimentalNavigation as Navigation,
__experimentalNavigationGroup as NavigationGroup,
__experimentalNavigationItem as NavigationItem,
__experimentalNavigationMenu as NavigationMenu,
} from '@wordpress/components';

const MyNavigation = () => (
<Navigation>
<NavigationMenu title="Home">
<NavigationGroup title="Group 1">
<NavigationItem item="item-1" title="Item 1" />
<NavigationItem item="item-2" title="Item 2" />
</NavigationGroup>
<NavigationGroup title="Group 2">
<NavigationItem
item="item-3"
navigateToMenu="category"
title="Category"
/>
</NavigationGroup>
</NavigationMenu>

<NavigationMenu
backButtonLabel="Home"
menu="category"
parentMenu="root"
title="Category"
>
<ul>
<NavigationItem
badge="1"
item="child-1"
title="Child 1"
/>
<NavigationItem item="child-2" title="Child 2" />
</ul>
</NavigationMenu>
</Navigation>
);
```

## Navigation Props

`Navigation` supports the following props.

### `activeItem`

- Type: `string`
- Required: No

The active item slug.

### `activeMenu`

- Type: `string`
- Required: No
- Default: "root"

The active menu slug.

### className

- Type: `string`
- Required: No

Optional className for the `Navigation` component.

### `onActivateItem`

- Type: `function`
- Required: No

Sync the active item between the external state and the Navigation's internal state.

### `onActivateMenu`

- Type: `function`
- Required: No

Sync the active menu between the external state and the Navigation's internal state.

## Navigation Menu Props

`NavigationMenu` supports the following props.

### `backButtonLabel`

- Type: `string`
- Required: No
- Default: "Back"

The back button label used in nested menus.

### className

- Type: `string`
- Required: No

Optional className for the `NavigationMenu` component.

### `menu`

- Type: `string`
- Required: No
- Default: "root"

The menu slug.

### `parentMenu`

- Type: `string`
- Required: No

The parent menu slug; used by nested menus to indicate their parent menu.

### `title`

- Type: `string`
- Required: No

The menu title.

## Navigation Group Props

`NavigationGroup` supports the following props.

### className

- Type: `string`
- Required: No

Optional className for the `NavigationGroup` component.

### `title`

- Type: `string`
- Required: No

The group title.

## Navigation Item Props

`NavigationItem` supports the following props.

### `badge`

- Type: `string|Number`
- Required: No

The item badge content.

### className

- Type: `string`
- Required: No

Optional className for the `NavigationItem` component.

### `href`

- Type: `string`
- Required: No

If provided, renders `a` instead of `button`.

### `navigateToMenu`

- Type: `string`
- Required: No

The child menu slug. If provided, clicking on the item will navigate to the target menu.

### `onClick`

- Type: `function`
- Required: No

A callback to handle clicking on a menu item.

### `title`

- Type: `string`
- Required: No

The item title.
1 change: 1 addition & 0 deletions packages/components/src/navigation/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const ROOT_MENU = 'root';
22 changes: 22 additions & 0 deletions packages/components/src/navigation/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* External dependencies
*/
import { noop } from 'lodash';

/**
* WordPress dependencies
*/
import { createContext, useContext } from '@wordpress/element';

/**
* Internal dependencies
*/
import { ROOT_MENU } from './constants';

export const NavigationContext = createContext( {
activeItem: undefined,
activeMenu: ROOT_MENU,
setActiveItem: noop,
setActiveMenu: noop,
} );
export const useNavigationContext = () => useContext( NavigationContext );
28 changes: 28 additions & 0 deletions packages/components/src/navigation/group.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* External dependencies
*/
import classnames from 'classnames';

/**
* Internal dependencies
*/
import { GroupTitleUI } from './styles/navigation-styles';

export default function NavigationGroup( { children, className, title } ) {
const classes = classnames( 'components-navigation__group', className );

return (
<div className={ classes }>
{ title && (
<GroupTitleUI
as="h3"
className="components-navigation__group-title"
variant="caption"
>
{ title }
</GroupTitleUI>
) }
<ul>{ children }</ul>
</div>
);
}
91 changes: 91 additions & 0 deletions packages/components/src/navigation/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* External dependencies
*/
import classnames from 'classnames';
import { noop } from 'lodash';

/**
* WordPress dependencies
*/
import { useEffect, useRef, useState } from '@wordpress/element';

/**
* Internal dependencies
*/
import Animate from '../animate';
import { ROOT_MENU } from './constants';
import { NavigationContext } from './context';
import { NavigationUI } from './styles/navigation-styles';

export default function Navigation( {
activeItem,
activeMenu = ROOT_MENU,
children,
className,
onActivateItem = noop,
onActivateMenu = noop,
} ) {
const [ item, setItem ] = useState( activeItem );
const [ menu, setMenu ] = useState( activeMenu );
const [ slideOrigin, setSlideOrigin ] = useState();

const setActiveItem = ( itemId ) => {
setItem( itemId );
onActivateItem( itemId );
};

const setActiveMenu = ( menuId, slideInOrigin = 'left' ) => {
setSlideOrigin( slideInOrigin );
setMenu( menuId );
onActivateMenu( menuId );
};

// Used to prevent the sliding animation on mount
const isMounted = useRef( false );
useEffect( () => {
if ( ! isMounted.current ) {
isMounted.current = true;
}
}, [] );

useEffect( () => {
if ( activeItem !== item ) {
setActiveItem( activeItem );
}
if ( activeMenu !== menu ) {
setActiveMenu( activeMenu );
}
}, [ activeItem, activeMenu ] );

const context = {
activeItem: item,
activeMenu: menu,
setActiveItem,
setActiveMenu,
};

const classes = classnames( 'components-navigation', className );

return (
<NavigationUI className={ classes }>
<Animate
key={ menu }
type="slide-in"
options={ { origin: slideOrigin } }
>
{ ( { className: animateClassName } ) => (
<div
className={ classnames( {
[ animateClassName ]:
isMounted.current && slideOrigin,
} ) }
>
<NavigationContext.Provider value={ context }>
{ children }
</NavigationContext.Provider>
</div>
) }
</Animate>
</NavigationUI>
);
}
Loading

0 comments on commit 668a710

Please sign in to comment.