skip to Main Content

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)

2

Answers


  1. 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>
      );
    };
    
    Login or Signup to reply.
  2. 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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search