Jack
Jack

Reputation: 16724

How do I get the current selected item in the TreeView from the context menu?

I need to add a Context Menu to my TreeWiew. I managed to do this with this mui.com's Menu but I don't know how to get the selected item in the tree where the user did the right click to open the context menu. For example, let's say the user open the context menu and click on "Copy message" i'd like to know which node the user selected (Calendar, OSS, index.js etc) and do the proper action. I've tried looking in the TreeView's thing like onNodeSelect but it doesn't fire when you open the context menu over the item. Currently the code look like this:

export default function FileSystemNavigator() {
  const [contextMenu, setContextMenu] = React.useState<{
    mouseX: number;
    mouseY: number;
  } | null>(null);

  const handleContextMenu = (event: React.MouseEvent) => {
    event.preventDefault();
    setContextMenu(
      contextMenu === null
        ? {
            mouseX: event.clientX + 2,
            mouseY: event.clientY - 6
          }
        : // repeated contextmenu when it is already open closes it with Chrome 84 on Ubuntu
          // Other native context menus might behave different.
          // With this behavior we prevent contextmenu from the backdrop to re-locale existing context menus.
          null
    );
  };

  const handleClose = () => {
    setContextMenu(null);
  };

  return (
    <div onContextMenu={handleContextMenu} style={{ cursor: "context-menu" }}>
      <TreeView
        aria-label="file system navigator"
        defaultCollapseIcon={<ExpandMoreIcon />}
        defaultExpandIcon={<ChevronRightIcon />}
        sx={{ height: 240, flexGrow: 1, maxWidth: 400, overflowY: "auto" }}
      >
        <TreeItem nodeId="1" label="Applications">
          <TreeItem nodeId="2" label="Calendar" />
        </TreeItem>
        <TreeItem nodeId="5" label="Documents">
          <TreeItem nodeId="10" label="OSS" />
          <TreeItem nodeId="6" label="MUI">
            <TreeItem nodeId="8" label="index.js" />
          </TreeItem>
        </TreeItem>
      </TreeView>
      <Menu
        open={contextMenu !== null}
        onClose={handleClose}
        anchorReference="anchorPosition"
        anchorPosition={
          contextMenu !== null
            ? { top: contextMenu.mouseY, left: contextMenu.mouseX }
            : undefined
        }
      >
        <MenuItem onClick={handleClose}>Copy Ctrl+C</MenuItem>
        <MenuItem onClick={handleClose}>Delete</MenuItem>
        <MenuItem onClick={handleClose}>Move</MenuItem>
        <MenuItem onClick={handleClose}>Email</MenuItem>
      </Menu>
    </div>
  );
}

Here's a live code example

Upvotes: 1

Views: 3186

Answers (3)

abarraford
abarraford

Reputation: 734

The solution accepted did not work quite as expected for me. Its 2 years old...

Instead what I did was created a CustomTreeItem: See here for a pretty good example of a CustomTreeItem:

You can wrap your CustomTreeItem with a <div onContextMenu={}></div>

Here is the example from MUI linked with a wrapper:

const CustomTreeItem = React.forwardRef(function CustomTreeItem(
  { id, itemId, label, disabled, children, ...other },
  ref,
) {
  const {
    getRootProps,
    getContentProps,
    getIconContainerProps,
    getCheckboxProps,
    getLabelProps,
    getGroupTransitionProps,
    getDragAndDropOverlayProps,
    status,
    publicAPI,
  } = useTreeItem2({ id, itemId, children, label, disabled, rootRef: ref });

// Rest of component...

    <div
      onContextMenu={(event) => {
        event.preventDefault();
        event.stopPropagation();
        console.log(itemId);
      }}
    >
      <TreeItem2Provider itemId={itemId}>
        <StyledTreeItemRoot {...getRootProps(other)}>
          <CustomTreeItemContent
            {...getContentProps({
              className: clsx("content", {
                "Mui-expanded": status.expanded,
                "Mui-selected": status.selected,
                "Mui-focused": status.focused,
                "Mui-disabled": status.disabled,
              }),
            })}
          >
            <TreeItem2IconContainer {...getIconContainerProps()}>
              <TreeItem2Icon status={status} />
            </TreeItem2IconContainer>
            <TreeItem2Checkbox {...getCheckboxProps()} />
            <CustomLabel
              {...getLabelProps({
                icon,
                expandable: expandable && status.expanded,
              })}
            />
            <TreeItem2DragAndDropOverlay {...getDragAndDropOverlayProps()} />
          </CustomTreeItemContent>
          {children && <TransitionComponent {...getGroupTransitionProps()} />}
        </StyledTreeItemRoot>
      </TreeItem2Provider>
    </div>

Now we could open a custom menu and perform actions around that specific Item.

Upvotes: 0

Entity
Entity

Reputation: 1044

You can achieve this by a small hack - trigger the click event on opening the context menu. That will fire an onNodeSelect callback of the TreeView component.

export default function FileSystemNavigator() {
  // ...

  const [selectedNodeId, setSelectedNodeId] = React.useState<string | null>(
    null
  );

  const handleContextMenu = (event: React.MouseEvent) => {
    event.target.click();

    event.preventDefault();
    // ...
  };

  const handleNodeSelect = (event, nodeId) => {
    setSelectedNodeId(nodeId);
  };

  return (
    <div onContextMenu={handleContextMenu} style={{ cursor: "context-menu" }}>
      <TreeView
        {/* ... */}
        onNodeSelect={handleNodeSelect}
      ></TreeView>
      <Menu>
        {/* ... */}
        <MenuItem onClick={handleClose}>Delete node {selectedNodeId}</MenuItem>
        {/* ... */}
      </Menu>
    </div>
  );
}

It will produce the following outcome:

Upvotes: 3

Milos Pavlovic
Milos Pavlovic

Reputation: 1450

Since you have only one context menu that have dynamically calculated position based on where right click happened, then you should use event from onContextMenu handler in order to find tree item that was right-clicked. For example, when you right click on 'Application' node, inside event.target of your handleContextMenu you will find <div class="MuiTreeItem-label">Applications</div> - and you just need to store this piece of information in inner state, and then reuse that same piece of state inside MenuItem click handlers. I assume you can attach some custom attributes, or maybe id to TreeItem, so you can additionally simplify resolver of clicked tree item(to avoid inspecting innerText of HTML div).

Upvotes: 1

Related Questions