Search code examples
javascriptreactjstypescriptnext.jsevent-bubbling

Hover states on React nested menu


In the following code I have a nested menu. On hover I wanted to add selected class name to the Nav.Item and only remove it when I hover over another Nav.Item. I could achive that with onMouseOver. Unfortunately, it doesn't add selected class to any of the sub-level Nav.Item.

So the criterias would be: for hover on any nav.item selected class should be added

  • don't remove selected class if mouse leaves the item
  • keep selected class on the Nav.Item when a child menu appears
  • remove selected class only, if hover over another Nav.Item but on the same level
"use client"; 
 
import Nav from 'react-bootstrap/Nav'; 
import Link from 'next/link'; 
 
import { getMenu } from '@/lib/APIs/menu'; 
import { use, useEffect, useRef, useState } from 'react'; 
import { usePathname } from 'next/navigation'; 
import { Button, NavDropdown } from 'react-bootstrap'; 
 
const dataPromise = getMenu(); 
 
const GlobalNav = () => { 
    const MENU = use(dataPromise); 
    const [selectedItemId, setSelectedItemId] = useState<string | null>(null); 
 
    function handleItemHover(itemId: string) { 
        setSelectedItemId(itemId); 
    } 
 
    function renderMenuItems(menuItems: any[]) { 
        return menuItems.map((item: any) => ( 
            <Nav.Item 
                as='li' 
                key={item.id}    
                onMouseOver={() => handleItemHover(item.id)} 
                className={selectedItemId === item.id ? 'selected' : ''} 
            > 
                <Nav.Link 
                    as={Link} 
                    href={item.url} 
                > 
                    <span> 
                        {item.title} 
                    </span> 
                </Nav.Link> 
                {item.children && item.children.length > 0 && ( 
                    <Nav as='ul'> 
                        {renderMenuItems(item.children)} 
                    </Nav> 
                )} 
            </Nav.Item> 
        )); 
    } 
 
    return ( 
        <Nav as='ul'> 
            {renderMenuItems(MENU)} 
        </Nav> 
    ); 
} 
 
export default GlobalNav; 

I hope someone can help with it.


Solution

  • Try this:

    function hasSelectedChild(menuItem) {
       return menuItem.children && menuItem.children.length > 0 && menuItem.children.find(item => item.id === selectedItemId || hasSelectedChild(item)) 
    }
    
    function renderMenuItems(menuItems: any[]) { 
            return menuItems.map((item: any) => ( 
                <Nav.Item 
                    as='li' 
                    key={item.id}    
                    onMouseOver={(e) => {
                      e.stopPropagation();
                      handleItemHover(item.id)
                    }} 
                    className={(hasSelectedChild(item) || selectedItemId === item.id) ? 'selected' : ''}
                > 
                    <Nav.Link 
                        as={Link} 
                        href={item.url} 
                    > 
                        <span> 
                            {item.title} 
                        </span> 
                    </Nav.Link> 
                    {item.children && item.children.length > 0 && ( 
                        <Nav as='ul'> 
                            {renderMenuItems(item.children)} 
                        </Nav> 
                    )} 
                </Nav.Item> 
            )); 
        }