how keep the hover enabled while the submenu is open

309 views Asked by At

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)

4

There are 4 answers

8
Dirk J. Faber On BEST ANSWER

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>
  );
};
2
Moob On

If you're not desperate to support old browsers you can use :has() CSS relational pseudo-class to style the tr when it contains a button having data-state="open". One big caveat is that this is not yet supported in Firefox. However, at the time of writing, it does have global support of ~87.53%.

So if that's for you, the approach is as follows:

  1. Give .sss descendent of open buttons a grey bg:

    tr td button[data-state="open"] .sss {...}

  2. Give tr that contains (/'has') an open button a grey bg:

    tr:has(button[data-state="open"]) {...}

Your complete style.css looks like this:

.sss {
  visibility: hidden;
}

tr:hover .sss,
tr td button[data-state="open"] .sss {
  background: gray;
  visibility: visible;
}

tr:hover,
tr:has(button[data-state="open"]) {
  background: gray;
  visibility: visible;
  pointer-events: initial !important;
}

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

Demo: https://codesandbox.io/s/sweet-tess-zcf23k

0
imhvost On

Remove <DropdownMenu.Portal> so content is not pasted into the body portal
I would also add table { border-spacing: 0; } that there is no space between the cells.
Here is an example: codesandbox

0
J.Porter On

There are many ways, I introduce one.

-App.tsx

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

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

export default function App() {
  const [isActive, setIsActive] = useState<string | null>(null);

  const subMenuSlice = (id) => (
    <SubMenu openCallback={(boolValue) => boolValue ? setIsActive(id) : setIsActive(null)} />
  );

  const addSubMenuNodes = nodes.map((node) => ({
    ...node,
    subMenu: subMenuSlice(node.id)
  }));
  //you can modify submenu for each node in nodes.

  return (
    <table>
      <tbody>
        {addSubMenuNodes.map((val, key) => (
          <tr key={key} className={isActive === val.id ? "active" : ""}>
            <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 = ({ openCallback }) => {
  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>
  );
};

-style.css

table {
  border-spacing: 0;
}

.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;
}

It will work correctly.

I hope my answer can help you.

Good luck.

Oh, If this is ok, please don't forget vote my answer