Ways to maintain hover functionality when the submenu is visible

My website features a simple table that displays devices and their characteristics. A condensed version of the table can be found in the link below.

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

When hovering over a row in the table, it turns gray and reveals an additional button. Clicking on this button opens a submenu for the user.

The issue at hand is that when the cursor moves to the submenu, the hover effect (gray) disappears from the table row. Any suggestions on how to maintain the hover effect while the submenu is active (open)?

Answer №1

To keep track of the state, you can assign an id to the item that is currently being "hovered". Then add a specific class and style it based on that class, as shown in the example below:

MainApp.tsx

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

const subMenuSlice = <SubMenu />;

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

export default function MainApp() {
  const [isHovered, setIsHovered] = useState(null);

  const handleMouseEnter = (id) => {
    setIsHovered(id);
  };

  const handleMouseLeave = () => {
    setIsHovered(null);
  };

  return (
    <table>
      <tbody>
        {itemsList.map((item, key) => (
          <tr
            key={key}
            onMouseEnter={() => handleMouseEnter(item.id)}
            onMouseLeave={handleMouseLeave}
            className={item.id === isHovered ? "hovered" : ""} // Add class if id matches
          >
            <td>{item.name}</td>
            <td>{item.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;
}

Ensure that all ids are unique in this setup. If duplicates exist, opt for another unique identifier.

Update

If you wish to maintain this state not just during hover but also when active, make these adjustments:

Rename the state to "isActive" and pass this value from the child component to the parent so it can be used in the className. Additionally, reinstate the original styles for the hovered state to see them both during hover and when active. Here's how you can modify your code:

MainApp.tsx

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

const subMenuSlice = <SubMenu />;

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

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

  return (
    <table>
      <tbody>
        {itemsList.map((item, key) => (
          <tr key={key} className={isActive === item.id ? "active" : ""}>
            <td>{item.name}</td>
            <td>
              {/* Check if the callback returns true, then set isActive to the id */}
              <SubMenu
                openCallback={(boolValue) =>
                  boolValue ? setIsActive(item.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 opposite boolean value of 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>
  );
};

Answer №2

If you are not concerned about supporting outdated browsers, you can incorporate the :has() CSS relational pseudo-class to style the tr element when it contains a button with data-state="open". It is important to note that this feature is currently unsupported in Firefox. However, as of now, it enjoys widespread adoption at approximately 87.53%.

If this approach aligns with your needs, the steps are as follows:

  1. Apply a grey background to the .sss class nested within open buttons:

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

  2. Assign a grey background to the tr element that includes an open button:

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

Your complete style.css should resemble the following:

.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

Answer №3

To prevent content from being pasted into the body, remove the use of <DropdownMenu.Portal>. You can ensure there is no space between cells by adding the CSS rule table { border-spacing: 0; }. For a demonstration, check out this example on CodeSandbox.

Answer №4

Exploring various methods, let's dive into one.

-Main.tsx

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

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

export default function Main() {
  const [activeItem, setActiveItem] = useState<string | null>(null);

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

  const addSubMenuItems = items.map((item) => ({
    ...item,
    subMenu: generateSubMenu(item.id)
  }));
  //customization of submenu can be applied to each item in the list.

  return (
    <table>
      <tbody>
        {addSubMenuItems.map((val, key) => (
          <tr key={key} className={activeItem === 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 [openState, setOpenState] = useState(false);

  const handleOpen = () => {
    setOpenState((prevState) => !prevState);
    if (openCallback) {
      openCallback(!openState); 
    }
  };

  return (
    <DropdownMenu.Root open={openState} 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;
}

This setup is configured for optimal functionality.

May this assistance prove valuable to you.

Best wishes on your endeavors.

If this solution meets your needs, kindly consider endorsing it.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Should you include the dollar sign in a Vue HTML variable or not?

I’m a bit confused about whether or not I should include $ when using a Vue HTML variable: new Vue({ data: { a: "myData" } }); Do I need to use: <h1>My value is {{ a }}</h1> or <h1>My value is {{ $a }}</h1> What ...

Inheriting the viewBox attribute with SvgIcon

I have a collection of unique custom SVGs, each with its own distinct viewBox. First Custom SVG <svg viewBox="-4 -4 24 24"><path something/></svg> Second Custom SVG <svg viewBox="-5 -7 24 24"><path something ...

Pressing the button disables the button's click functionality

I am currently developing a react application that incorporates Button from material-ui. I am aiming to implement drag and drop functionality for a component that contains a button. To facilitate drag and drop, I have integrated react-sortable-hoc into my ...

What are the steps for implementing the innerClass style on the searchBox within the appBase?

I'm currently incorporating the searchBox component from appbase io into my upcoming js web application, but I've been facing challenges in customizing the layout of both the search box and the list. Despite following the documentation which sug ...

Hover over an element repeatedly to trigger an action with jQuery, as opposed to just once

Is it possible to have my p tag transition whenever I hover over it? I am looking to create a hover effect on an image that displays text in a div tag with scaling transitions. Can someone assist me with this? Instead of using jQuery or JavaScript to dyn ...

Error encountered when attempting to display a particular user with a specific _id in MongoDB, Node, and React: Failed to convert value "undefined" to ObjectId in the "user" model

I am experiencing an issue on my page where multiple users are displayed. Whenever I click on a user, it should redirect me to their individual page, but instead, I encounter the following error: Cast to ObjectId failed for value "undefined" at path "_id" ...

Managing the loading state within a React application that utilizes Redux in conjunction with redux-thunk, navigating back to the component once the data has been successfully fetched for the

In my React app, I am using Redux along with redux-thunk. When a certain component needs to fetch data from an API, I trigger the request by calling a bound action creator in the componentDidMount lifecycle method: componentDidMount() { this.props.fetch ...

Rendering JSON responses in React.js can be achieved using either the fetch function or axios library

I've spent way too much time struggling and I can't seem to concentrate anymore. My goal is simple - I just want to fetch JSON data from a URL and display it visually in the browser. It doesn't even have to be formatted, at least not until ...

Axios cancel token preemptively cancelling request before it is initiated

Currently, I am working on implementing cancellation of axios calls in my project. After reviewing the axios documentation, it appears to be quite straightforward, which can be found here. To begin, I have defined some variables at the top of my Vue compo ...

Invoking a class method in Javascriptcore on iOS

I'm currently trying to comprehend the inner workings of JavascriptCore. Initially, I attempted calling a single function. Now, my focus has shifted to invoking a function within a class. This is what my javascript code looks like: var sayHelloAlf ...

Issue: Trouble with Rotating Tooltips in Javascript

I am facing a challenge with the tooltips on my website. I want to ensure that all tooltips have a consistent look and transition effects, but I am struggling to achieve this. The rotation and other effects applied using javascript are not functioning prop ...

Does React Native have a picker similar to this?

[I struggled to find a way to combine an icon and text in the placeholder and label of items] (https://i.sstatic.net/TzOxe.jpg) ...

Sending data from PHP to JavaScript using AJAX

I'm encountering an issue trying to pass data from a PHP file to a JavaScript file using echo. My PHP file contains the following code snippet: <?php ...establishing connection and retrieving list from database... for($i=0;$i<sizeOf($li ...

Using the combination of PHP and AJAX, modify files in real-time

Looking to dynamically change files based on screen resolution, I have three files: index.php main.js testA.php testB.php The index.php file will dynamically call testA and testB. If the screen resolution is desktop size, index.php should include testA ...

I am facing difficulty in incorporating an IP address or URL within an IFRAME in my Cordova Android application

This page does not allow any iframes to load except for the YouTube video URL. When attempting to use any other iframe URL, the following error code is displayed: Error : net::ERR_BLOCKED_BY_RESPONSE In the example below, any URL or IP address fails to l ...

Incorporating JSON data seamlessly into a visually appealing Highcharts pie chart

It seems like I'm facing a minor issue here. Everything was working fine until... $(document).ready(function() { // Original data var data = [{ "name": "Tokyo", "y": 3.0 }, { "name": "NewYork", "y": 2.0 }, { "name": "Berlin", ...

`I'm encountering issues when trying to pass an array through localStorage into a new array`

This is a complex and detailed question that I am struggling to find a solution for. Despite using deprecated mysql due to hosting limitations, the problem lies elsewhere. Part 1 involves dataLoader.php, which queries the database and retrieves posx and p ...

Issue reported: "Usage of variable 'someVar' before assignment" ; however, it is being properly assigned before usage

This piece of code showcases the issue: let someVar: number; const someFunc = async () => { someVar = 1; } await someFunc(); if (someVar == 1) { console.log('It is 1'); } As a result, you will encounter ...

Is there a disparity in how the mandatory field validator functions in Edge compared to Chrome?

https://i.sstatic.net/qUzDN.pngThere doesn't seem to be any red color https://i.sstatic.net/EqHyW.png How can I ensure consistency? Both elements should either have color or none at all. <form action=""> <input id="email" type="email ...

Currently working on enhancing the responsiveness of the login form

I am encountering difficulties while trying to make the page responsive. I've checked my basic code, but something seems off. Could you please take a look at the plnkr link below and give me some feedback on where I might have gone wrong? Here's ...