diff --git a/packages/components/src/navigation/children-utils.js b/packages/components/src/navigation/children-utils.js new file mode 100644 index 00000000000000..60217e41d50e9f --- /dev/null +++ b/packages/components/src/navigation/children-utils.js @@ -0,0 +1,106 @@ +/** + * External dependencies + */ +import { cloneDeep } from 'lodash'; + +/** + * WordPress dependencies + */ +import { Children } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { ROOT_MENU } from './constants'; +import NavigationItem from './item'; +import NavigationMenu from './menu'; + +/** + * + * @typedef {Object} ChildWithClosestParent + * @property {WPElement} child element that matched the predicate + * @property {WPElement?} parent closest matching parent that matched the parentPredicate + */ + +/** + * + * @param {WPElement} initialChildren children to iterate through recursively + * @param {Function} predicate children matching this function predicate will be returned + * @param {Function} parentPredicate closest parent will be determined by this function predicate + * + * @return {ChildWithClosestParent[]} children with their closest parent + */ +export const findChildrenWithClosestParent = ( + initialChildren, + predicate, + parentPredicate +) => { + const hasPredicate = typeof predicate === 'function'; + const hasParentPredicate = typeof parentPredicate === 'function'; + + const innerRecursion = ( children, lastParent ) => { + let items = []; + + Children.forEach( children, ( child ) => { + const predicateOk = hasPredicate && predicate( child ); + if ( predicateOk ) { + items.push( { child, parent: lastParent } ); + } + + const hasChildren = child?.props?.children; + if ( hasChildren ) { + const parentPredicateOk = + hasParentPredicate && parentPredicate( child ); + if ( parentPredicateOk ) { + lastParent = child; + } + + items = items.concat( + findChildrenWithClosestParent( + child.props.children, + predicate, + parentPredicate, + lastParent + ) + ); + } + } ); + + return items; + }; + + return innerRecursion( initialChildren, null ); +}; + +export const findNavigationItems = ( children ) => { + const items = findChildrenWithClosestParent( + children, + ( { type } ) => type === NavigationItem + ); + + return items.reduce( ( acc, { child, parent } ) => { + const key = child.props.item; + + acc[ key ] = cloneDeep( child.props ); + acc[ key ].menu = parent?.props?.menu ?? ROOT_MENU; + delete acc[ key ].children; + + return acc; + }, {} ); +}; + +export const findNavigationMenus = ( children ) => { + const menus = findChildrenWithClosestParent( + children, + ( { type } ) => type === NavigationMenu + ); + + return menus.reduce( ( acc, { child } ) => { + const key = child?.props?.menu ?? ROOT_MENU; + + acc[ key ] = cloneDeep( child.props ); + delete acc[ key ].children; + + return acc; + }, {} ); +}; diff --git a/packages/components/src/navigation/context.js b/packages/components/src/navigation/context.js index ab02e4efe56364..6c5e3111443580 100644 --- a/packages/components/src/navigation/context.js +++ b/packages/components/src/navigation/context.js @@ -18,5 +18,7 @@ export const NavigationContext = createContext( { activeMenu: ROOT_MENU, setActiveItem: noop, setActiveMenu: noop, + items: {}, + menus: {}, } ); export const useNavigationContext = () => useContext( NavigationContext ); diff --git a/packages/components/src/navigation/index.js b/packages/components/src/navigation/index.js index 577ac165b82ae7..29393591e8efa7 100644 --- a/packages/components/src/navigation/index.js +++ b/packages/components/src/navigation/index.js @@ -16,6 +16,7 @@ import Animate from '../animate'; import { ROOT_MENU } from './constants'; import { NavigationContext } from './context'; import { NavigationUI } from './styles/navigation-styles'; +import { findNavigationItems, findNavigationMenus } from './children-utils'; export default function Navigation( { activeItem, @@ -28,6 +29,8 @@ export default function Navigation( { const [ item, setItem ] = useState( activeItem ); const [ menu, setMenu ] = useState( activeMenu ); const [ slideOrigin, setSlideOrigin ] = useState(); + const [ navigationItems, setNavigationItems ] = useState( {} ); + const [ navigationMenus, setNavigationMenus ] = useState( {} ); const setActiveItem = ( itemId ) => { setItem( itemId ); @@ -57,11 +60,21 @@ export default function Navigation( { } }, [ activeItem, activeMenu ] ); + useEffect( () => { + const items = findNavigationItems( children ); + const menus = findNavigationMenus( children ); + + setNavigationItems( items ); + setNavigationMenus( menus ); + }, [] ); + const context = { activeItem: item, activeMenu: menu, setActiveItem, setActiveMenu, + items: navigationItems, + menus: navigationMenus, }; const classes = classnames( 'components-navigation', className );