diff --git a/packages/components/src/navigation/README.md b/packages/components/src/navigation/README.md index 78c9776ff8bf57..d2bc0d8ccd2776 100644 --- a/packages/components/src/navigation/README.md +++ b/packages/components/src/navigation/README.md @@ -137,12 +137,19 @@ The parent menu slug; used by nested menus to indicate their parent menu. When `hasSearch` is active and `onSearch` is provided, this controls the value of the search input. Required when the `onSearch` prop is provided. +### `isEmpty` + +- Type: `boolean` +- Required: No + +Indicates whether the menu is empty or not. Used together with the `hideIfTargetMenuEmpty` prop of Navigation Item. + ### `title` - Type: `string` - Required: No -The menu title. It's also the field used by the menu search function. +The menu title. It's also the field used by the menu search function. ## Navigation Group Props @@ -201,6 +208,13 @@ The unique identifier of the item. The child menu slug. If provided, clicking on the item will navigate to the target menu. +### `hideIfTargetMenuEmpty` + +- Type: `boolean` +- Required: No + +Indicates whether this item should be hidden if the menu specified in `navigateToMenu` is marked as empty in the `isEmpty` prop. Used together with the `isEmpty` prop of Navigation Menu. + ### `onClick` - Type: `function` diff --git a/packages/components/src/navigation/context.js b/packages/components/src/navigation/context.js index c283c3899a89fe..5f47fd9b48d812 100644 --- a/packages/components/src/navigation/context.js +++ b/packages/components/src/navigation/context.js @@ -17,6 +17,7 @@ export const NavigationContext = createContext( { activeItem: undefined, activeMenu: ROOT_MENU, setActiveMenu: noop, + isMenuEmpty: noop, navigationTree: { items: {}, @@ -28,6 +29,9 @@ export const NavigationContext = createContext( { getMenu: noop, addMenu: noop, removeMenu: noop, + childMenu: {}, + traverseMenu: noop, + isMenuEmpty: noop, }, } ); export const useNavigationContext = () => useContext( NavigationContext ); diff --git a/packages/components/src/navigation/item/index.js b/packages/components/src/navigation/item/index.js index 08ba83004731d2..edae98f4f62484 100644 --- a/packages/components/src/navigation/item/index.js +++ b/packages/components/src/navigation/item/index.js @@ -29,6 +29,7 @@ export default function NavigationItem( props ) { navigateToMenu, onClick = noop, title, + hideIfTargetMenuEmpty, ...restProps } = props; @@ -46,6 +47,17 @@ export default function NavigationItem( props ) { return null; } + // If hideIfTargetMenuEmpty prop is true + // And the menu we are supposed to navigate to + // Is marked as empty, then we skip rendering the item + if ( + hideIfTargetMenuEmpty && + navigateToMenu && + navigationTree.isMenuEmpty( navigateToMenu ) + ) { + return null; + } + const classes = classnames( 'components-navigation__item', className, { 'is-active': item && activeItem === item, } ); diff --git a/packages/components/src/navigation/stories/hide-if-empty.js b/packages/components/src/navigation/stories/hide-if-empty.js new file mode 100644 index 00000000000000..0977a41ab18463 --- /dev/null +++ b/packages/components/src/navigation/stories/hide-if-empty.js @@ -0,0 +1,57 @@ +/** + * Internal dependencies + */ +import Navigation from '..'; +import NavigationItem from '../item'; +import NavigationMenu from '../menu'; + +export function HideIfEmptyStory() { + return ( + <> + + + + + + + + + + + + + + + + +

+ This story contains 3 navigation items and 4 menus. You should + only see one item: To sub 2 (visible) +

+ + ); +} diff --git a/packages/components/src/navigation/stories/index.js b/packages/components/src/navigation/stories/index.js index d6352091d7c501..2b8cb59a906619 100644 --- a/packages/components/src/navigation/stories/index.js +++ b/packages/components/src/navigation/stories/index.js @@ -11,6 +11,7 @@ import { GroupStory } from './group'; import { ControlledStateStory } from './controlled-state'; import { SearchStory } from './search'; import { MoreExamplesStory } from './more-examples'; +import { HideIfEmptyStory } from './hide-if-empty'; import './style.css'; export default { @@ -29,3 +30,4 @@ export const controlledState = () => ; export const groups = () => ; export const search = () => ; export const moreExamples = () => ; +export const hideIfEmpty = () => ; diff --git a/packages/components/src/navigation/use-create-navigation-tree.js b/packages/components/src/navigation/use-create-navigation-tree.js index 52414e540dd624..63d5d4a6e2bbb9 100644 --- a/packages/components/src/navigation/use-create-navigation-tree.js +++ b/packages/components/src/navigation/use-create-navigation-tree.js @@ -1,3 +1,8 @@ +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + /** * Internal dependencies */ @@ -18,6 +23,50 @@ export const useCreateNavigationTree = () => { removeNode: removeMenu, } = useNavigationTreeNodes(); + /** + * Stores direct nested menus of menus + * This makes it easy to traverse menu tree + * + * Key is the menu prop of the menu + * Value is an array of menu keys + */ + const [ childMenu, setChildMenu ] = useState( {} ); + const getChildMenu = ( menu ) => childMenu[ menu ] || []; + + const traverseMenu = ( startMenu, callback ) => { + const visited = []; + let queue = [ startMenu ]; + let current; + + while ( queue.length > 0 ) { + current = getMenu( queue.shift() ); + + if ( ! current || visited.includes( current.menu ) ) { + continue; + } + + visited.push( current.menu ); + queue = [ ...queue, ...getChildMenu( current.menu ) ]; + + if ( callback( current ) === false ) { + break; + } + } + }; + + const isMenuEmpty = ( menuToCheck ) => { + let isEmpty = true; + + traverseMenu( menuToCheck, ( current ) => { + if ( ! current.isEmpty ) { + isEmpty = false; + return false; + } + } ); + + return isEmpty; + }; + return { items, getItem, @@ -26,7 +75,24 @@ export const useCreateNavigationTree = () => { menus, getMenu, - addMenu, + addMenu: ( key, value ) => { + setChildMenu( ( state ) => { + const newState = { ...state }; + + if ( ! newState[ value.parentMenu ] ) { + newState[ value.parentMenu ] = []; + } + + newState[ value.parentMenu ].push( key ); + + return newState; + } ); + + addMenu( key, value ); + }, removeMenu, + childMenu, + traverseMenu, + isMenuEmpty, }; };