From b5508b46671f5f8072456202e98481952b79f348 Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Thu, 27 Aug 2020 02:52:06 +0300 Subject: [PATCH] Add animations to navigation component (#24771) * Navigation Component: Remove setActiveLevel child function arg (#24704) * remove setActiveLevel from public api * change to NavigationBackButton * return null for backButton if no parentLevel * Navigation Component: Expose __experimentalNavigation component (#24706) * Expose __experimentalNavigation component * fix typo * expose menut and menu-item as well * Loop over navigation levels and wrap with animate * Use map and set to organize items and levels * Simplify level and item logic * Remove unnecessary key prop * Fix parent level assignment * Add back in key prop to force animation * Use useMemo to map item data Co-authored-by: Paul Sealock --- packages/components/CHANGELOG.md | 2 + packages/components/src/index.js | 5 + packages/components/src/navigation/index.js | 117 +++++++++++++----- .../components/src/navigation/menu-item.js | 4 +- .../src/navigation/stories/index.js | 33 +++-- 5 files changed, 120 insertions(+), 41 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 693c21dc1a37f9..4b7a6b589d5b55 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Introduce `Navigation` component as `__experimentalNavigation` for displaying a heirarchy of items. + ## 10.0.0 (2020-07-07) ### Breaking Change diff --git a/packages/components/src/index.js b/packages/components/src/index.js index cc2bc1deb3ace8..697c18c7296c31 100644 --- a/packages/components/src/index.js +++ b/packages/components/src/index.js @@ -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'; diff --git a/packages/components/src/navigation/index.js b/packages/components/src/navigation/index.js index 62406951546e0d..42458839abfff6 100644 --- a/packages/components/src/navigation/index.js +++ b/packages/components/src/navigation/index.js @@ -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 ( + + ); + }; + return ( - { children( { - level, - levelItems, - parentLevel, - setActiveLevel, - } ) } + + { ( { className: animateClassName } ) => ( +
+ { children( { + level, + NavigationBackButton, + parentLevel, + } ) } +
+ ) } +
); }; export default Navigation; +export { default as NavigationMenu } from './menu'; +export { default as NavigationMenuItem } from './menu-item'; diff --git a/packages/components/src/navigation/menu-item.js b/packages/components/src/navigation/menu-item.js index 6a3dbbc9475ef4..d8a7cfdcbf5123 100644 --- a/packages/components/src/navigation/menu-item.js +++ b/packages/components/src/navigation/menu-item.js @@ -26,7 +26,7 @@ const NavigationMenuItem = ( props ) => { LinkComponent, linkProps, onClick, - setActiveLevel, + setActiveLevelId, title, } = props; const classes = classnames( 'components-navigation__menu-item', { @@ -35,7 +35,7 @@ const NavigationMenuItem = ( props ) => { const handleClick = () => { if ( children.length ) { - setActiveLevel( id ); + setActiveLevelId( id ); return; } onClick( props ); diff --git a/packages/components/src/navigation/stories/index.js b/packages/components/src/navigation/stories/index.js index 17284b91c26497..43e7b16bf78055 100644 --- a/packages/components/src/navigation/stories/index.js +++ b/packages/components/src/navigation/stories/index.js @@ -3,6 +3,7 @@ */ import { Button } from '@wordpress/components'; import { useState } from '@wordpress/element'; +import { Icon, arrowLeft } from '@wordpress/icons'; /** * Internal dependencies @@ -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', @@ -68,22 +84,18 @@ function Example() { return ( - { ( { level, levelItems, parentLevel, setActiveLevel } ) => { + { ( { level, parentLevel, NavigationBackButton } ) => { return ( <> { parentLevel && ( - + + + { parentLevel.title } + ) }

{ level.title }

- { levelItems.map( ( item ) => { + { level.children.map( ( item ) => { return ( setActive( selected.id ) } - setActiveLevel={ setActiveLevel } /> ); } ) }