How do I open a dropdown menu when a row is right-clicked in a table?
I've tried using Table's onRow onContextMenu: event => { setVisible(true); }
function with dropdown outside the table <Dropdown overlay={menu} visible={visible}><Table>...</Table></Dropdown>
, but the dropdown menu opens at the bottom of the table, not where the mouse is.
I've found a working demo sandbox but its with ant design 3.0.0 and doesn't work with the latest version.
First, add visible state in your component:
const [visible, setVisible] = useState(false);
Then pass the visible state into Dropdown component that use contextMenu as trigger and your Menu component as the overlay:
<Dropdown overlay={menu} visible={visible} trigger={["contextMenu"]}>
<Table ... />
</Dropdown>
Add states that will keep position and value of our clicked cell:
const [posX, setPosX] = useState(0);
const [posY, setPosY] = useState(0);
const [value, setValue] = useState("");
Next, use onRow attribute where we pass a Function that return onContextMenu and onClick Function at your Table component. We will receive an event at onContextMenu by which we get the clicked position and use onClick to hide the menu
<Table
onRow={(_record, _rowIndex) => {
return {
onContextMenu: (event) => {
event.preventDefault();
// grab and keep the clicked position
const { clientX, clientY, target } = event;
setPosX(clientX);
setPosY(clientY);
// grab the clicked cell value
setValue(target.innerHTML);
// show the overlay Menu
setVisible(true);
},
// hide the overlay Menu on click
onClick: () => setVisible(false)
};
}}
...
/>
And finally, the overlay Menu positioning. Here we use inline style to set the position. Since Dropdown component default positioning is bottomLeft
so the position will be at:
left: 0 // left side
top: n // where n is the bottom of the table which is equal to the height of your table
This is why your dropdown menu opens at the bottom of the table. To fix this, we need to set the top value as:
// the formula
topPosition = clientY - tableHeight - menuHeight;
// in our example we get:
topPosition = clientY - 220 - 72
= clientY - 292;
So the clicked position becomes:
left: clientX,
top: clientY - 292
and at the menu we set by using inline style as follow:
const menu = (
<Menu
style={{ top: posY - 292, left: posX }}
onClick={onClick}
items={[
{
key: "1",
label: "Action 1"
},
{
key: "2",
label: "Action 2"
}
]}
/>
);
This is a minimal example:
If you need to access the row value from the menu, you can get the information from record props at onRow event handler and grab the value at onContextMenu event handler. So, when you do a right click at each row, the record value will be set as rowRecord state that could be used inside Menu component that is rendered inside a useMemo hook later. Since the menu now have been memoized, now the onClick method have to use useCallback hook too.
const [rowRecord, setRowRecord] = useState();
...
const onClick = useCallback(
(_item) => {
console.log(value);
setVisible(false);
},
[value]
);
...
const menu = useMemo(() => {
console.log(rowRecord);
return (
<Menu
style={{ top: posY - 292, left: posX }}
onClick={onClick}
items={[
{
key: "1",
label: rowRecord?.name
},
{
key: "2",
label: rowRecord?.email
}
]}
/>
);
}, [onClick, posX, posY, rowRecord]);
...
<Table
onRow={(record, _rowIndex) => {
return {
onContextMenu: (event) => {
event.preventDefault();
...
setRowRecord(record);
},
...
};
}}
...
/>
Actually, the rowRecord state could be used inside our component immediately after we set the record value by using setRowRecord setter. But again, it depends on your own preference.