-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Navigation Component: Composition Proposal #25057
Changes from 18 commits
53382ec
c08901d
63736b4
dbd96d6
81e8a5b
16141e0
e479ab9
c77b558
80273e2
5897bfa
e652c32
9cce14d
5991899
1365d0d
d5031eb
b60bc5c
6a34450
016dd82
c1e351c
21887bc
1a88aea
e78beb4
1bc526b
1b28149
3f5f943
140893d
3352d0c
0818991
e2d4bca
bf45b48
4f48fb4
49c9059
66370a7
ca124af
f9c1c10
8906756
6e2aa46
0985ad9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
# Navigation | ||
|
||
Render a flat array of menu items into a waterfall style hierarchy navigation. | ||
|
||
## Usage | ||
|
||
```jsx | ||
import { | ||
__experimentalNavigation as Navigation, | ||
__experimentalNavigationMenu as NavigationMenu, | ||
__experimentalNavigationMenuItem as NavigationMenuItem, | ||
} from '@wordpress/components'; | ||
import { useState } from '@wordpress/compose'; | ||
|
||
const data = [ | ||
{ | ||
title: 'Item 1', | ||
id: 'item-1', | ||
}, | ||
{ | ||
title: 'Item 2', | ||
id: 'item-2', | ||
}, | ||
{ | ||
title: 'Category', | ||
id: 'item-3', | ||
badge: '2', | ||
}, | ||
{ | ||
title: 'Child 1', | ||
id: 'child-1', | ||
parent: 'item-3', | ||
badge: '1', | ||
}, | ||
{ | ||
title: 'Child 2', | ||
id: 'child-2', | ||
parent: 'item-3', | ||
}, | ||
]; | ||
|
||
const MyNavigation = () => { | ||
const [ active, setActive ] = useState( 'item-1' ); | ||
|
||
return ( | ||
<Navigation activeItemId={ active } data={ data } rootTitle="Home"> | ||
{ ( { level, parentLevel, NavigationBackButton } ) => { | ||
return ( | ||
<> | ||
{ parentLevel && ( | ||
<NavigationBackButton> | ||
<Icon icon={ arrowLeft } /> | ||
{ parentLevel.title } | ||
</NavigationBackButton> | ||
) } | ||
<h1>{ level.title }</h1> | ||
<NavigationMenu> | ||
{ level.children.map( ( item ) => { | ||
return ( | ||
<NavigationMenuItem | ||
{ ...item } | ||
key={ item.id } | ||
onClick={ ( selected ) => | ||
setActive( selected.id ) | ||
} | ||
/> | ||
); | ||
} ) } | ||
</NavigationMenu> | ||
</> | ||
); | ||
} } | ||
</Navigation> | ||
}; | ||
``` | ||
|
||
## Navigation Props | ||
|
||
Navigation supports the following props. | ||
|
||
### `data` | ||
|
||
- Type: `array` | ||
- Required: Yes | ||
|
||
An array of config objects for each menu item. | ||
|
||
Config objects can be represented | ||
|
||
#### `data.title` | ||
|
||
- Type: `string` | ||
- Required: Yes | ||
|
||
A menu item's title. | ||
|
||
#### `data.id` | ||
|
||
- Type: `string|Number` | ||
- Required: Yes | ||
|
||
A menu item's id. | ||
|
||
#### `data.parent` | ||
|
||
- Type: `string|Number` | ||
- Required: No | ||
|
||
Specify a menu item's parent id. Defaults to the menu item's parent if none is provided. | ||
|
||
#### `data.href` | ||
|
||
- Type: `string` | ||
- Required: No | ||
|
||
Turn a menu item into a link by supplying a url. | ||
|
||
#### `data.linkProps` | ||
|
||
- Type: `object` | ||
- Required: No | ||
|
||
Supply properties passed to the menu-item. | ||
|
||
#### `data.LinkComponent` | ||
|
||
- Type: `Node` | ||
- Required: No | ||
|
||
Supply a React component to render as the menu item. This is useful for router link components for internal navigation. | ||
|
||
### `activeItemId` | ||
|
||
- Type: `string` | ||
- Required: Yes | ||
|
||
The active screen id. | ||
|
||
### `rootTitle` | ||
|
||
- Type: `string` | ||
- Required: No | ||
|
||
A top level title. | ||
|
||
## NavigationMenuItem Props | ||
|
||
NavigationMenuItem supports the following props. | ||
|
||
### `onClick` | ||
|
||
- Type: `function` | ||
- Required: No | ||
|
||
A callback to handle selection of a menu item. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import classnames from 'classnames'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useEffect, useState, useRef } from '@wordpress/element'; | ||
import { Icon, chevronLeft, chevronRight } from '@wordpress/icons'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import Animate from '../animate'; | ||
import { | ||
BackButtonUI, | ||
MenuItemTitleUI, | ||
MenuItemUI, | ||
MenuTitleUI, | ||
MenuUI, | ||
Root, | ||
} from './styles/navigation-styles'; | ||
import Button from '../button'; | ||
|
||
export default function ComponentsCompositionNavigation( { | ||
initialActiveLevel, | ||
children, | ||
} ) { | ||
const [ activeLevel, setActiveLevel ] = useState( initialActiveLevel ); | ||
const [ slideOrigin, setSlideOrigin ] = useState( 'left' ); | ||
|
||
const isMounted = useRef( false ); | ||
useEffect( () => { | ||
if ( ! isMounted.current ) { | ||
isMounted.current = true; | ||
} | ||
}, [] ); | ||
|
||
const navigateTo = ( level ) => { | ||
setActiveLevel( level ); | ||
setSlideOrigin( 'left' ); | ||
}; | ||
|
||
const navigateBack = ( level ) => { | ||
setActiveLevel( level ); | ||
setSlideOrigin( 'right' ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These methods are asynchronous, right? Seems like a small risk they aren't updated prior to the level change. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AFAIK set states are batched together before re-rendering, so level and slide origin should be both updated before the component replaces its content and perform the animation. 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh nice, you're right! I thought this only applied to synthetic event handlers, but looks like calls will also be batched in |
||
}; | ||
|
||
function NavigationLevel( { | ||
children: levelChildren, | ||
slug, | ||
title, | ||
parentLevel, | ||
parentTite, | ||
} ) { | ||
if ( activeLevel !== slug ) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<div className="components-navigation__level"> | ||
{ parentLevel ? ( | ||
<BackButtonUI | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice! If we can extract this to its own component with an |
||
className="components-navigation__back-button" | ||
isTertiary | ||
onClick={ () => navigateBack( parentLevel ) } | ||
> | ||
<Icon icon={ chevronLeft } /> | ||
{ parentTite } | ||
</BackButtonUI> | ||
) : null } | ||
<MenuUI> | ||
<MenuTitleUI | ||
variant="subtitle" | ||
className="components-navigation__menu-title" | ||
> | ||
{ title } | ||
</MenuTitleUI> | ||
<ul>{ levelChildren }</ul> | ||
</MenuUI> | ||
</div> | ||
); | ||
} | ||
|
||
const NavigationCategory = ( { title, navigateTo: to } ) => { | ||
return ( | ||
<MenuItemUI className="components-navigation__menu-item"> | ||
<Button onClick={ () => navigateTo( to ) }> | ||
{ title } | ||
<Icon icon={ chevronRight } /> | ||
</Button> | ||
</MenuItemUI> | ||
); | ||
}; | ||
|
||
return ( | ||
<Root className="components-navigation"> | ||
<Animate | ||
key={ activeLevel } | ||
type="slide-in" | ||
options={ { origin: slideOrigin } } | ||
> | ||
{ ( { className: animateClassName } ) => ( | ||
<div | ||
className={ classnames( { | ||
[ animateClassName ]: isMounted.current, | ||
} ) } | ||
> | ||
{ children( { | ||
activeLevel, | ||
navigateTo, | ||
NavigationCategory, | ||
NavigationLevel, | ||
} ) } | ||
</div> | ||
) } | ||
</Animate> | ||
</Root> | ||
); | ||
} | ||
|
||
export function NavigationItem( { slug, title, onClick, activeItem } ) { | ||
const classes = classnames( 'components-navigation__menu-item', { | ||
'is-active': activeItem === slug, | ||
} ); | ||
|
||
return ( | ||
<MenuItemUI className={ classes }> | ||
<Button onClick={ onClick }> | ||
<MenuItemTitleUI | ||
className="components-navigation__menu-item-title" | ||
variant="body.small" | ||
as="span" | ||
> | ||
{ title } | ||
</MenuItemTitleUI> | ||
</Button> | ||
</MenuItemUI> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpicky, but I'd prefer
setLevel
here asnavigateTo
could be ambiguous with navigating to a page/item.