Search code examples
javascriptcssreactjshoversubmenu

how keep the hover enabled while the submenu is open


I have a simple table on my website listing devices and their characteristics (in the example at the link below there will be a shortened version of the table).

import "./styles.css";
import { SubMenu } from "./SubMenu";

const subMenuSlice = <SubMenu />;

const nodes = [
  {
    id: "0",
    name: "Samsung Galaxy",
    subMenu: subMenuSlice
  },
  {
    id: "0",
    name: "Iphone",
    subMenu: subMenuSlice
  }
];

export default function App() {
  return (
    <table>
      <tbody>
        {nodes.map((val, key) => (
          <tr key={key}>
            <td>{val.name}</td>
            <td>{val.subMenu}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

SubMenu.tsx

import { useState } from "react";
import AppsIcon from "@mui/icons-material/Apps";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import "./styles.css";

export const SubMenu = () => {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <DropdownMenu.Root open={isOpen} onOpenChange={setIsOpen}>
      <DropdownMenu.Trigger>
        <AppsIcon className="sss" />
      </DropdownMenu.Trigger>
      <DropdownMenu.Portal>
        <DropdownMenu.Content
          side="bottom"
          sideOffset={-30}
          align="start"
          alignOffset={80}
        >
          <button className="style-button">Edit </button>
          <button className="style-button">Make </button>
          <button className="style-button">Delete </button>
        </DropdownMenu.Content>
      </DropdownMenu.Portal>
    </DropdownMenu.Root>
  );
};

styles.css

    .sss {
  visibility: hidden;
}

tr:hover .sss {
  background: gray;
  visibility: visible;
}

tr:hover {
  background: gray;
  visibility: visible;
  pointer-events: initial !important;
}

.style-button:hover {
  background-color: aqua;
}

https://codesandbox.io/s/romantic-rgb-5t7xkq

As you can see, when you hover over any of the lines, the entire line turns gray and an additional button appears. By clicking on this button the user gets a submenu.

Description of the problem: the problem is that when the user moves the cursor to the submenu, the hover (gray) disappears from the table row. Please tell me how to keep the hover enabled while the submenu is active (open)


Solution

  • I would keep track of some state, for example the current id of the item that is being "hovered". Then for that item add an extra class and style it depending on that class, in your example:

    App.tsx

    import "./styles.css";
    import { SubMenu } from "./SubMenu";
    import { useState } from "react";
    
    const subMenuSlice = <SubMenu />;
    
    const nodes = [
      {
        id: "0",
        name: "Samsung Galaxy",
        subMenu: subMenuSlice
      },
      {
        id: "1",
        name: "Iphone",
        subMenu: subMenuSlice
      }
    ];
    
    export default function App() {
      const [isHovered, setIsHovered] = useState(null);
    
      const handleMouseEnter = (id) => {
        setIsHovered(id);
      };
    
      const handleMouseLeave = () => {
        setIsHovered(null);
      };
    
      return (
        <table>
          <tbody>
            {nodes.map((val, key) => (
              <tr
                key={key}
                onMouseEnter={() => handleMouseEnter(val.id)}
                onMouseLeave={handleMouseLeave}
                className={val.id === isHovered ? "hovered" : ""} // here you set the class if the id matches. 
              >
                <td>{val.name}</td>
                <td>{val.subMenu}</td>
              </tr>
            ))}
          </tbody>
        </table>
      );
    }
    

    styles.css

    .sss {
      visibility: hidden;
    }
    
    tr.hovered .sss {
      background: gray;
      visibility: visible;
    }
    
    tr.hovered {
      background: gray;
      visibility: visible;
      pointer-events: initial !important;
    }
    
    .style-button:hover {
      background-color: aqua;
    }
    

    You must make sure in this example all ids are unique. If this cannot be the case, use another unique value.

    Updated

    If you want to have this state not only when your component is being hovered, but generally when active, I would do the following:

    Rename the state to "isActive" and pass the value from the subcomponent to the parent so you can use this value on your className. Also, re-add your original styles for when the component is being hovered, in that case you will have the styles both when the component is hovered and while it is active. This is how you would do it:

    App.tsx

    import "./styles.css";
    import { SubMenu } from "./SubMenu";
    import { useState } from "react";
    
    const subMenuSlice = <SubMenu />;
    
    const nodes = [
      {
        id: "0",
        name: "Samsung Galaxy",
        subMenu: subMenuSlice
      },
      {
        id: "1",
        name: "Iphone",
        subMenu: subMenuSlice
      }
    ];
    
    export default function App() {
      const [isActive, setIsActive] = useState<string | null>(null);
    
      return (
        <table>
          <tbody>
            {nodes.map((val, key) => (
              <tr key={key} className={isActive === val.id ? "active" : ""}>
                <td>{val.name}</td>
                <td>
                  {/* Check if the callback returns true, if so set isActive to the id */}
                  <SubMenu
                    openCallback={(boolValue) =>
                      boolValue ? setIsActive(val.id) : setIsActive(null)
                    }
                  />
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      );
    }
    

    styles.css

    .sss {
      visibility: hidden;
    }
    
    tr.active .sss, tr:hover .sss {
      background: gray;
      visibility: visible;
    }
    
    tr.active, tr:hover {
      background: gray;
      visibility: visible;
      pointer-events: initial !important;
    }
    
    .style-button:hover {
      background-color: aqua;
    }
    

    SubMenu.tsx

    import { useState } from "react";
    import AppsIcon from "@mui/icons-material/Apps";
    import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
    import "./styles.css";
    
    export const SubMenu = ({ openCallback }: { openCallback?: (arg: boolean) => void }) => {
      const [isOpen, setIsOpen] = useState(false);
    
      const handleOpen = () => {
        setIsOpen((prevState) => !prevState);
        if (openCallback) {
          openCallback(!isOpen); // call the function with the boolean value !isOpen
        }
      };
    
      return (
        <DropdownMenu.Root open={isOpen} onOpenChange={handleOpen}>
          <DropdownMenu.Trigger>
            <AppsIcon className="sss" />
          </DropdownMenu.Trigger>
          <DropdownMenu.Portal>
            <DropdownMenu.Content
              side="bottom"
              sideOffset={-30}
              align="start"
              alignOffset={80}
            >
              <button className="style-button">Edit </button>
              <button className="style-button">Make </button>
              <button className="style-button">Delete </button>
            </DropdownMenu.Content>
          </DropdownMenu.Portal>
        </DropdownMenu.Root>
      );
    };