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

Add animations to navigation component #24771

Merged
merged 9 commits into from
Aug 26, 2020
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
117 changes: 89 additions & 28 deletions packages/components/src/navigation/index.js
Original file line number Diff line number Diff line change
@@ -1,54 +1,115 @@
/**
* External dependencies
*/
import classnames from 'classnames';

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

/**
* Internal dependencies
*/
import Animate from '../animate';
import { Root } from './styles/navigation-styles';
import Button from '../button';

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 [ activeLevelId, setActiveLevelId ] = useState( 'root' );

const appendItemData = ( item ) => {
return {
...item,
children: [],
parent: item.id === 'root' ? null : item.parent || 'root',
isActive: item.id === activeItemId,
setActiveLevelId,
};
};

const mapItems = ( itemData ) => {
const items = new Map(
[
{ id: 'root', parent: null, title: rootTitle },
...itemData,
].map( ( item ) => [ item.id, appendItemData( item ) ] )
);

items.forEach( ( item ) => {
const parentItem = items.get( item.parent );
if ( parentItem ) {
parentItem.children.push( item );
parentItem.hasChildren = true;
}
} );

return items;
};
const items = [ { id: 'root', title: rootTitle }, ...mapItemData( data ) ];

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 items = useMemo( () => mapItems( data ), [
data,
activeItemId,
rootTitle,
] );
const activeItem = items.get( activeItemId );
const previousActiveLevelId = usePrevious( activeLevelId );
const level = items.get( activeLevelId );
const parentLevel = level && items.get( level.parent );
const isNavigatingBack =
previousActiveLevelId &&
items.get( previousActiveLevelId ).parent === activeLevelId;

useEffect( () => {
if ( activeItem ) {
setActiveLevel( activeItem.parent );
setActiveLevelId( activeItem.parent );
}
}, [] );

const NavigationBackButton = ( { children: backButtonChildren } ) => {
if ( ! parentLevel ) {
return null;
}

return (
<Button
isPrimary
onClick={ () => setActiveLevelId( parentLevel.id ) }
>
{ backButtonChildren }
</Button>
);
};

return (
<Root className="components-navigation">
{ children( {
level,
levelItems,
parentLevel,
setActiveLevel,
} ) }
<Animate
key={ level.id }
type="slide-in"
options={ {
origin: isNavigatingBack ? 'right' : 'left',
} }
>
{ ( { className: animateClassName } ) => (
<div
className={ classnames(
'components-navigation__level',
animateClassName
) }
>
{ children( {
level,
NavigationBackButton,
parentLevel,
} ) }
</div>
) }
</Animate>
</Root>
);
};

export default Navigation;
export { default as NavigationMenu } from './menu';
export { default as NavigationMenuItem } from './menu-item';
4 changes: 2 additions & 2 deletions packages/components/src/navigation/menu-item.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const NavigationMenuItem = ( props ) => {
LinkComponent,
linkProps,
onClick,
setActiveLevel,
setActiveLevelId,
title,
} = props;
const classes = classnames( 'components-navigation__menu-item', {
Expand All @@ -35,7 +35,7 @@ const NavigationMenuItem = ( props ) => {

const handleClick = () => {
if ( children.length ) {
setActiveLevel( id );
setActiveLevelId( id );
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

return;
}
onClick( props );
Expand Down
33 changes: 22 additions & 11 deletions packages/components/src/navigation/stories/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
import { Button } from '@wordpress/components';
import { useState } from '@wordpress/element';
import { Icon, arrowLeft } from '@wordpress/icons';

/**
* Internal dependencies
Expand Down Expand Up @@ -48,6 +49,21 @@ const data = [
id: 'child-2',
parent: 'item-3',
},
{
title: 'Nested Category',
id: 'child-3',
parent: 'item-3',
},
{
title: 'Sub Child 1',
id: 'sub-child-1',
parent: 'child-3',
},
{
title: 'Sub Child 2',
id: 'sub-child-2',
parent: 'child-3',
},
{
title: 'External link',
id: 'item-4',
Expand All @@ -68,30 +84,25 @@ function Example() {

return (
<Navigation activeItemId={ active } data={ data } rootTitle="Home">
{ ( { level, levelItems, parentLevel, setActiveLevel } ) => {
{ ( { level, parentLevel, NavigationBackButton } ) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

More simplification, nice!

return (
<>
{ parentLevel && (
<Button
isPrimary
onClick={ () =>
setActiveLevel( parentLevel.id )
}
>
Back
</Button>
<NavigationBackButton>
<Icon icon={ arrowLeft } />
{ parentLevel.title }
</NavigationBackButton>
) }
<h1>{ level.title }</h1>
<NavigationMenu>
{ levelItems.map( ( item ) => {
{ level.children.map( ( item ) => {
return (
<NavigationMenuItem
{ ...item }
key={ item.id }
onClick={ ( selected ) =>
setActive( selected.id )
}
setActiveLevel={ setActiveLevel }
/>
);
} ) }
Expand Down