Skip to content
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

Merged
merged 38 commits into from
Sep 11, 2020
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
53382ec
Add Navigation component [Feature branch] (#24107)
psealock Jul 29, 2020
c08901d
Make nav components more atomic (#24266)
joshuatf Aug 11, 2020
63736b4
Use css-in-js for styling in navigation component (#24522)
joshuatf Aug 18, 2020
dbd96d6
Add nav badges (#24528)
joshuatf Aug 18, 2020
81e8a5b
Add custom nav link props to Navigation component (#24570)
joshuatf Aug 20, 2020
16141e0
Add animations to navigation component (#24771)
joshuatf Aug 26, 2020
e479ab9
Navigation Component: Update docs (#24880)
psealock Aug 30, 2020
c77b558
Fix prop error and missing classes in Navigation component (#24878)
joshuatf Aug 30, 2020
80273e2
Add navigation component tests (#24860)
joshuatf Aug 31, 2020
5897bfa
PR feedback
psealock Aug 31, 2020
e652c32
Navigation Component: Apply effect on activeItem change (#24958)
psealock Sep 1, 2020
9cce14d
Navigation Component: Avoid animation on mounting (#24960)
psealock Sep 3, 2020
5991899
Update nav styles to match core designs (#24987)
joshuatf Sep 3, 2020
1365d0d
Navigation with component composition experiment
Copons Sep 3, 2020
d5031eb
Add a third nested menu
Copons Sep 3, 2020
b60bc5c
paul's tinkering
psealock Sep 4, 2020
6a34450
Sort rebase inconsistency
Copons Sep 4, 2020
016dd82
Separate the composition experiment from the original Nav proposal
Copons Sep 4, 2020
c1e351c
Simplify the exposed interface
Copons Sep 4, 2020
21887bc
Add badge and href
Copons Sep 4, 2020
1a88aea
Story parity with other approach
Copons Sep 4, 2020
e78beb4
Re-implement with Context API
Copons Sep 4, 2020
1bc526b
Make the navigation controllable from outside
Copons Sep 5, 2020
1b28149
Naming cleanup
Copons Sep 5, 2020
3f5f943
Don't change the active item when navigating to nested level or click…
Copons Sep 9, 2020
140893d
Add groups
Copons Sep 9, 2020
3352d0c
Replace original approach
Copons Sep 9, 2020
0818991
Delete leftover
Copons Sep 9, 2020
e2d4bca
Fix story's non-nav link positioning
Copons Sep 10, 2020
bf45b48
Rename utils to constants
Copons Sep 10, 2020
4f48fb4
Add custom classNames support
Copons Sep 10, 2020
49c9059
Update active colors to respect theme choice
Copons Sep 10, 2020
66370a7
Simplify the readme
Copons Sep 10, 2020
ca124af
Improve namings
Copons Sep 10, 2020
f9c1c10
Simplify and improve the story
Copons Sep 10, 2020
8906756
Rename level into menu
Copons Sep 10, 2020
6e2aa46
Refactor style names
Copons Sep 10, 2020
0985ad9
Rename parentMenuTitle as backButtonLabel
Copons Sep 10, 2020
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
6 changes: 6 additions & 0 deletions docs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1055,6 +1055,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 heirarchy of items.

## 10.0.0 (2020-07-07)

### Breaking Change
Expand Down
5 changes: 5 additions & 0 deletions packages/components/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ 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,
NavigationMenu as __experimentalNavigationMenu,
NavigationMenuItem as __experimentalNavigationMenuItem,
} from './navigation';
export { default as Notice } from './notice';
export { default as __experimentalNumberControl } from './number-control';
export { default as NoticeList } from './notice/list';
Expand Down
155 changes: 155 additions & 0 deletions packages/components/src/navigation/README.md
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 ) => {
Copy link
Contributor

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 as navigateTo could be ambiguous with navigating to a page/item.

setActiveLevel( level );
setSlideOrigin( 'left' );
};

const navigateBack = ( level ) => {
setActiveLevel( level );
setSlideOrigin( 'right' );
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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. 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The 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 useEffect. 👍

};

function NavigationLevel( {
children: levelChildren,
slug,
title,
parentLevel,
parentTite,
} ) {
if ( activeLevel !== slug ) {
return null;
}

return (
<div className="components-navigation__level">
{ parentLevel ? (
<BackButtonUI
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice! If we can extract this to its own component with an onClick prop then we can resolve the back button in the wc nav repo.

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>
);
}
Loading