Hover states on React nested menu

147 views Asked by At

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.

2

There are 2 answers

5
Emil J On BEST ANSWER

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> 
        )); 
    }
0
KGS On

To achieve the desired behavior of adding the "selected" class to the Nav.Item elements on hover and keeping it when a child menu appears, you can modify the code as below:

import Nav from 'react-bootstrap/Nav';
import Link from 'next/link';
import { getMenu } from '@/lib/APIs/menu';
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 handleItemMouseLeave() {
    setSelectedItemId(null);
  }

  function renderMenuItems(menuItems: any[]) {
    return menuItems.map((item: any) => (
      <Nav.Item
        as='li'
        key={item.id}
        onMouseOver={() => handleItemHover(item.id)}
        onMouseLeave={handleItemMouseLeave}
        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' className={selectedItemId === item.id ? 'selected' : ''}>
            {renderMenuItems(item.children)}
          </Nav>
        )}
      </Nav.Item>
    ));
  }

  return (
    <Nav as='ul'>
      {renderMenuItems(MENU)}
    </Nav>
  );
}

export default GlobalNav;

Adding the className prop to the nested Nav component to add the "selected" class when the parent Nav.Item is selected. And adding the onMouseLeave={handleItemMouseLeave} event handler to the Nav.Item to trigger the reset of selectedItemId