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