Skip to content

Commit

Permalink
Make nav components more atomic (#24266)
Browse files Browse the repository at this point in the history
* Break nav components into smaller pieces

* Get visible menu items based on menu id and active state

* Allow null and root text in back button

* Allow root text for navigation title

* Add line break to make use of multiple menus more obvious

* Fix back button case at root level

* Remove secondary nav styling

* Simplify component props

* Pass parsed data back to navigation children

* Move nav menu item logic into menu

* Simplify nav components to bare essentials

* Allow menu level navigation without changing active item

* Handle PR feedback
  • Loading branch information
joshuatf authored and psealock committed Aug 25, 2020
1 parent 0834e0f commit 417f9c6
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 163 deletions.
95 changes: 34 additions & 61 deletions packages/components/src/navigation/index.js
Original file line number Diff line number Diff line change
@@ -1,74 +1,47 @@
/**
* WordPress dependencies
*/
import { useState } from '@wordpress/element';
import { Icon, arrowLeft } from '@wordpress/icons';
import { useEffect, useState } from '@wordpress/element';

/**
* Internal dependencies
*/
import Button from '../button';
import Text from '../text';
import Item from './item';
const Navigation = ( { activeItemId, children, data, rootTitle } ) => {
const [ activeLevel, setActiveLevel ] = useState( 'root' );

const mapItemData = ( items ) => {
return items.map( ( item ) => {
const itemChildren = data.filter( ( i ) => i.parent === item.id );
return {
...item,
children: itemChildren,
parent: item.parent || 'root',
isActive: item.id === activeItemId,
hasChildren: itemChildren.length > 0,
};
} );
};
const items = [ { id: 'root', title: rootTitle }, ...mapItemData( data ) ];

const Navigation = ( { data, initial } ) => {
const initialActive = data.find( ( item ) => item.slug === initial );
const [ active, setActive ] = useState( initialActive );
const parent = data.find( ( item ) => item.slug === active.parent );
const items = data.filter( ( item ) => item.parent === active.parent );
const activeItem = items.find( ( item ) => item.id === activeItemId );
const level = items.find( ( item ) => item.id === activeLevel );
const levelItems = items.filter( ( item ) => item.parent === level.id );
const parentLevel =
level.id === 'root'
? null
: items.find( ( item ) => item.id === level.parent );

const goBack = () => {
if ( ! parent.parent ) {
// We are at top level, will need to handle this case.
return;
}
const parentalSiblings = data.filter(
( item ) => item.parent === parent.parent
);
if ( parentalSiblings.length ) {
setActive( parentalSiblings[ 0 ] );
useEffect( () => {
if ( activeItem ) {
setActiveLevel( activeItem.parent );
}
};
}, [] );

return (
<div className="components-navigation">
<Button
isSecondary
className="components-navigation__back"
onClick={ goBack }
>
<Icon icon={ arrowLeft } />
{ parent.back }
</Button>
<div className="components-navigation__title">
<Text variant="title.medium">{ parent.title }</Text>
</div>
<div className="components-navigation__menu-items">
{ items.map( ( item ) =>
item.menu !== 'secondary' ? (
<Item
key={ item.slug }
data={ data }
item={ item }
setActive={ setActive }
isActive={ item.slug === active.slug }
/>
) : null
) }
</div>
<div className="components-navigation__menu-items is-secondary">
{ items.map( ( item ) =>
item.menu === 'secondary' ? (
<Item
key={ item.slug }
data={ data }
item={ item }
setActive={ setActive }
isActive={ item.slug === active.slug }
/>
) : null
) }
</div>
{ children( {
level,
levelItems,
parentLevel,
setActiveLevel,
} ) }
</div>
);
};
Expand Down
36 changes: 0 additions & 36 deletions packages/components/src/navigation/item.js

This file was deleted.

51 changes: 51 additions & 0 deletions packages/components/src/navigation/menu-item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* External dependencies
*/
import classnames from 'classnames';

/**
* WordPress dependencies
*/
import { Icon, chevronRight } from '@wordpress/icons';

/**
* Internal dependencies
*/
import Button from '../button';
import Text from '../text';

const NavigationMenuItem = ( props ) => {
const {
children,
hasChildren,
id,
isActive,
onClick,
setActiveLevel,
title,
} = props;
const classes = classnames( 'components-navigation__menu-item', {
'is-active': isActive,
} );

const handleClick = () => {
if ( children.length ) {
setActiveLevel( id );
return;
}
onClick( props );
};

return (
<li className={ classes }>
<Button className={ classes } onClick={ handleClick }>
<Text variant="body.small">
<span>{ title }</span>
</Text>
{ hasChildren ? <Icon icon={ chevronRight } /> : null }
</Button>
</li>
);
};

export default NavigationMenuItem;
5 changes: 5 additions & 0 deletions packages/components/src/navigation/menu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const NavigationMenu = ( { children } ) => {
return <ul className="components-navigation__menu">{ children }</ul>;
};

export default NavigationMenu;
118 changes: 57 additions & 61 deletions packages/components/src/navigation/stories/index.js
Original file line number Diff line number Diff line change
@@ -1,88 +1,84 @@
/**
* WordPress dependencies
*/
import { Button } from '@wordpress/components';
import { useState } from '@wordpress/element';

/**
* Internal dependencies
*/
import Navigation from '../';
import NavigationMenu from '../menu';
import NavigationMenuItem from '../menu-item';

export default {
title: 'Components/Navigation',
component: Navigation,
};

const data = [
{ title: 'WooCommerce', slug: 'root', back: 'Dashboard' },
{ title: 'Home', slug: 'home', parent: 'root', menu: 'primary' },
{
title: 'Analytics',
slug: 'analytics',
parent: 'root',
back: 'WooCommerce Home',
menu: 'primary',
},
{
title: 'Orders',
slug: 'orders',
parent: 'root',
back: 'WooCommerce Home',
menu: 'primary',
},
{
title: 'Overview',
slug: 'overview',
parent: 'analytics',
},
{
title: 'Products report',
slug: 'products',
parent: 'analytics',
title: 'Item 1',
id: 'item-1',
},
{
title: 'All orders',
slug: 'all_orders',
parent: 'orders',
title: 'Item 2',
id: 'item-2',
},
{
title: 'Payouts',
slug: 'payouts',
parent: 'orders',
title: 'Category',
id: 'item-3',
},
{
title: 'Settings',
slug: 'settings',
parent: 'root',
back: 'WooCommerce Home',
menu: 'secondary',
title: 'Child 1',
id: 'child-1',
parent: 'item-3',
},
{
title: 'Extensions',
slug: 'extensions',
parent: 'root',
back: 'WooCommerce Home',
menu: 'secondary',
},
{
title: 'General',
slug: 'general',
parent: 'settings',
},
{
title: 'Tax',
slug: 'tax',
parent: 'settings',
},
{
title: 'My extensions',
slug: 'my_extensions',
parent: 'extensions',
},
{
title: 'Marketplace',
slug: 'marketplace',
parent: 'extensions',
title: 'Child 2',
id: 'child-2',
parent: 'item-3',
},
];

function Example() {
return <Navigation data={ data } initial="home" />;
const [ active, setActive ] = useState( 'item-1' );

return (
<Navigation activeItemId={ active } data={ data } rootTitle="Home">
{ ( { level, levelItems, parentLevel, setActiveLevel } ) => {
return (
<>
{ parentLevel && (
<Button
isPrimary
onClick={ () =>
setActiveLevel( parentLevel.id )
}
>
Back
</Button>
) }
<h1>{ level.title }</h1>
<NavigationMenu>
{ levelItems.map( ( item ) => {
return (
<NavigationMenuItem
{ ...item }
key={ item.id }
onClick={ ( selected ) =>
setActive( selected.id )
}
setActiveLevel={ setActiveLevel }
/>
);
} ) }
</NavigationMenu>
</>
);
} }
</Navigation>
);
}

export const _default = () => {
Expand Down
6 changes: 1 addition & 5 deletions packages/components/src/navigation/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,9 @@
margin-bottom: 40px;
}

.components-navigation__menu-items {
.components-navigation__menu {
display: flex;
flex-direction: column;

&.is-secondary {
margin-top: 24px;
}
}

.components-navigation__menu-item {
Expand Down

0 comments on commit 417f9c6

Please sign in to comment.