/* eslint-disable no-case-declarations */ import { useRef, useState, useCallback, useMemo } from 'react'; export interface UseTogglableNavInterface { toggleProperty: string; singleOpen?: boolean; toggleOpenClose?: boolean; toggleParentOpen?: boolean; onToggleProperty: string; initial?: Record; firstOpen?: boolean; ariaNavigation?: boolean; toggleHook?: (id: string) => void; } export interface UseTogglableNavReturn { registerButton: (el: string) => any; registerLink: (el: string) => any; registerNavItem: (item: any) => any; registered: { current: Record }; opened: Record; toggle: (id: string) => void; setOpened: ( opened: | ((curOpened: Record) => Record) | Record ) => void; } export function useTogglableNav({ toggleProperty, toggleOpenClose, toggleParentOpen, onToggleProperty, initial, ariaNavigation, toggleHook, }: UseTogglableNavInterface): UseTogglableNavReturn { const [opened, setOpened] = useState(initial || {}); const registered = useRef>( {} ); const openedKeys = useRef([]); const toggle = useCallback((e) => { const key = e.currentTarget.getAttribute('aria-controls') as string; const selected = ((e.currentTarget.getAttribute( 'aria-selected' ) as string) === 'true') as boolean; if (toggleHook) { toggleHook(key); } e.preventDefault(); if (toggleOpenClose) { if (toggleParentOpen) { const items = key.split('__'); const currentKey = items.slice(0, items.length).join('__'); const totalKeys = items.map((_, index) => { return items.slice(0, index + 1).join('__'); }); if (!selected && !openedKeys.current.includes(currentKey)) { openedKeys.current.push(currentKey); } if (selected && openedKeys.current.includes(currentKey)) { openedKeys.current = openedKeys.current.filter( (k) => k !== currentKey ); } setOpened(() => ({ ...totalKeys.reduce((acc, cur) => { acc[cur] = cur === currentKey ? !selected : true; return acc; }, {}), })); } else { setOpened({ [key]: !selected }); } } else { setOpened({ [key]: true }); } }, []); const getMenuItems = (element) => { if (element) { if (element?.role === 'menuitem') { return element; } else { const childrenArray = Array.from(element?.children); const menuItemsArray: any[] = []; childrenArray.forEach((child) => { const menuItem = getMenuItems(child); if (menuItem && !menuItemsArray.includes(menuItem)) { menuItemsArray.push(menuItem); } }); return menuItemsArray.flatMap((x) => x); } } return null; }; const findChildren = (parent, items) => { return items ? items.filter( (child) => child.slice(0, child.lastIndexOf('__')) === parent ) : items; }; const findParent = (child, items) => { const parent = child.slice(0, child.lastIndexOf('__')); return items.find((item) => item === parent) ? parent : null; }; const findSiblings = (key, items) => { const parent = key.slice(0, key.lastIndexOf('__')); if (key.lastIndexOf('__') > 0) { return items.filter( (child) => child.slice(0, child.lastIndexOf('__')) === parent ); } return items.filter((child) => !child.includes('__')); }; const findLeftSibling = (key, items) => { const siblings = findSiblings(key, items); if (siblings.length > 0) { const keyIndex = siblings.findIndex((item) => item === key); return keyIndex > 0 ? siblings[keyIndex - 1] : null; } return null; }; const findRightSibling = (key, items) => { const siblings = findSiblings(key, items); if (siblings.length > 0) { const keyIndex = siblings.findIndex((item) => item === key); return keyIndex < siblings.length - 1 ? siblings[keyIndex + 1] : null; } return null; }; const getOpenItemChildren = (currentItem, items) => { const itemChildrenArray: any[] = []; const itemChildren = findChildren(currentItem, items); itemChildren.forEach((item) => { if (openedKeys.current.includes(item)) { itemChildrenArray.push(item); const internChildren = getOpenItemChildren(item, items); if (internChildren.length > 0) { itemChildrenArray.push(internChildren); } } else { itemChildrenArray.push(item); } }); return itemChildrenArray.flatMap((x) => x); }; const onKeyDown = useCallback((e) => { const orientation = document .querySelector(`[aria-orientation]`) ?.getAttribute('aria-orientation'); const key = e.target.getAttribute('aria-controls'); const keys = Object.entries(registered.current).map( ([entryKey]) => entryKey ); const handleArrowRight = () => { const arrowRightItem = findRightSibling(key, keys); if (arrowRightItem) { registered.current[arrowRightItem].el.focus(); } }; const handleArrowLeft = () => { const arrowLeftItem = findLeftSibling(key, keys); if (arrowLeftItem) { registered.current[arrowLeftItem].el.focus(); } }; switch (e.key) { case 'ArrowRight': handleArrowRight(); break; case 'ArrowDown': const openItem = getOpenItemChildren(key, keys); const arrowDownItems = findChildren(key, openItem); const menuItems = getMenuItems(document.getElementById(key)); if (arrowDownItems.length > 0) { registered.current[arrowDownItems[0]].el.focus(); } else { if (orientation === 'vertical') { if ( document.activeElement?.getAttribute('aria-selected') === 'true' && menuItems && menuItems.length > 0 ) { menuItems[0].focus(); } else { handleArrowRight(); } } else { if (menuItems && menuItems.length > 0) { menuItems[0].focus(); } } } break; case 'ArrowLeft': handleArrowLeft(); break; case 'ArrowUp': if (orientation === 'vertical') { handleArrowLeft(); } else { const arrowUpItem = findParent(key, keys); if (arrowUpItem) { registered.current[arrowUpItem].el.focus(); } } break; case ' ': case 'Enter': if (registered.current[key].type === 'link') { registered.current[key].el.click(); } else { toggle(e); } break; } }, []); const registerButtonRef = useCallback((el) => { if (!el) return; const key = el.getAttribute('aria-controls') as string; if (registered.current[key]) return; registered.current[key] = { el, type: 'button', }; }, []); const registerLinkRef = useCallback((el) => { if (!el) return; const key = el.getAttribute('aria-controls') as string; if (registered.current[key]) return; registered.current[key] = { el, type: 'link', }; }, []); const registerButton = useCallback( (key) => { const isOpen = !!opened[key]; return { ref: registerButtonRef, [toggleProperty]: isOpen, arrow: true, [onToggleProperty]: toggle, 'aria-controls': `${key}`, ...(ariaNavigation && { onKeyDown }), }; }, [opened] ); const registerLink = useCallback( (key) => { return { ref: registerLinkRef, 'aria-controls': `${key}`, ...(ariaNavigation && { onKeyDown }), }; }, [opened] ); const registerNavItem = useCallback( (item) => { if (item.subMenu || item.menu) { return { ...registerButton(item.name), role: 'button' }; } else if (item.href) { return { ...registerLink(item.name), href: item.href }; } else { console.error(item); throw new Error('Invalid nav item. Must be either button or link'); } }, [opened] ); return useMemo( () => ({ registerNavItem, registerButton, registerLink, registered, opened, toggle, setOpened, }), [opened] ); }