Search code examples
reactjsantd

How to show Dropdown Menu when Right-Clicking on Table Row?


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.


Solution

  • 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:

    Edit polished-resonance-8kfw05

    Additional Info

    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.