Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TreeView] Expand/collapse node only when clicking on expand/collapse icon #19953

Closed
1 task done
LaurianeDPX opened this issue Mar 3, 2020 · 32 comments 路 Fixed by #20657
Closed
1 task done

[TreeView] Expand/collapse node only when clicking on expand/collapse icon #19953

LaurianeDPX opened this issue Mar 3, 2020 · 32 comments 路 Fixed by #20657
Labels
component: tree view TreeView, TreeItem. This is the name of the generic UI component, not the React module!

Comments

@LaurianeDPX
Copy link

LaurianeDPX commented Mar 3, 2020

  • I have searched the issues of this repository and believe that this is not a duplicate.

Summary 馃挕

When I click on a tree item that has children, the node expands, wherever I click on the line. While this behavior might be ok for most users, I would like to have the possibility to expand/collapse only when I click on the expand/collapse icon - not on the label.

Motivation 馃敠

In my project, I have a tree and I'd like to display a panel with information about a node when I click on it.
When my node does not have children, it is very easy to achieve this behavior. But when my node has children, it expends at the same time its information are displayed, which is not the behavior I want. So I lack a way to have a different behavior when I click on the label (display node info) and when I click on the expand/collapse icon (expand/collapse the node).

@MatthieuCoelho
Copy link

I have the same use case, it could be useful !

@oliviertassinari oliviertassinari added the component: tree view TreeView, TreeItem. This is the name of the generic UI component, not the React module! label Mar 3, 2020
@tonyhallett
Copy link
Contributor

Here is a workaround.

You need to import the TreeViewContext

Prevent the label click from being processed by the wrapped TreeItem.

Use the normal click handling code omitting the toggle expansion logic.

function ExpandIconOnlyTreeItem(props:TreeItemProps){
  const {label,...other} = props;
  const context = useContext(TreeViewContext.default) as any;

  const focused = context.isFocused ? context.isFocused(props.nodeId) : false;
  const labelClicked = useCallback(
    (event:any) => {
      if (!focused) {
        context.focus(props.nodeId);
      }
  
      const multiple = context.multiSelect && (event.shiftKey || event.ctrlKey || event.metaKey);
  
      
  
      if (!context.selectionDisabled) {
        if (multiple) {
          if (event.shiftKey) {
            context.selectRange(event, { end: props.nodeId });
          } else {
            context.selectNode(event, props.nodeId, true);
          }
        } else {
          context.selectNode(event, props.nodeId);
        }
      }
      event.stopPropagation()
      if (props.onClick) {
        props.onClick(event);
      }
    }
  ,[]);
  
  return <TreeItem {...other} label={<span onClick={labelClicked}>{label}</span>}/>
}

@guicostaarantes
Copy link

@tonyhallett can you please specify how exactly you imported TreeViewContext in your workaround?

Also thanks for the commit, it'll be much more elegant to solve it by adding the iconClickExpandOnly prop to TreeView.

@tonyhallett
Copy link
Contributor

tonyhallett commented Mar 19, 2020

const TreeViewContext = require('@material-ui/lab/esm/TreeView/TreeViewContext');

Also as mentioned you can use expanded and onNodeToggle and check the event as I did in the pull request instead of using the private TreeViewContext.
https://github.com/mui-org/material-ui/blob/176bf440590df95cccae46768cb5c0475cc6746f/packages/material-ui-lab/src/TreeItem/TreeItem.js#L175

@savissimo
Copy link

Could we have an option to expand when clicking the icon or the label, but collapsing only when clicking the icon?

@eps1lon
Copy link
Member

eps1lon commented Mar 30, 2020

Could we have an option to expand when clicking the icon or the label, but collapsing only when clicking the icon?

We're probably going with a solution that enables checking in an onClick handler if the click came from the icon or label and then you can customize this behavior. Props-based approaches don't scale very well for one-off use cases.

@savissimo
Copy link

We're probably going with a solution that enables checking in an onClick handler if the click came from the icon or label and then you can customize this behavior. Props-based approaches don't scale very well for one-off use cases.

Fine for me. It would be great to have a mechanism to stop the propagation of the event. Like, in the onSelect event handler, a boolean that says whether the item will expand, whether it will be selected. That would give us complete flexibility, covering pretty much any desired behaviour, while keeping the props under control.

(If that is exactly what you were planning to do... well, great :) )

@guicostaarantes
Copy link

guicostaarantes commented Apr 9, 2020

@eps1lon I think I don't completely understand the approach you described.

I think you may have suggested that the fix is removing the onClick={handleClick} from the content and inserting onClick = {() => handleClick('icon')} in the icon and onClick = {() => handleClick('label')} in the label, and then using this as input to control the behavior.

I think you may also have suggested that the code would use the event argument on handleClick to know if the click was in the icon or in the label (maybe via event.target).

I'm trying to implement this feature and PR but this is my first time contributing to an open source project, so I could really use some guidance. Thanks.

@tonyhallett
Copy link
Contributor

tonyhallett commented Apr 16, 2020

I have create a React hook that can be used to get the desired behaviour, including that desired by @savissimo.

npm useseparatetoggleclick.
codesandbox demo

@guicostaarantes - I can see where the confusion lies.

We're probably going with a solution that enables checking in an onClick handler if the click came from the icon or label and then you can customize this behavior.

@eps1lon I think I don't completely understand the approach you described.

If you see the comment from @eps1lon on the pull request that I made
#20087 (comment)

I think we should rather use the approach we use with the other components: Add labelProps that are spread to the {label} and then you can intercept clicks that happen on the label.

By spreading props on Typography it will be possible to add an onClick handler which can be used in the same manner as my hook ( which added an onClick to the icon. We only need the event information for all label clicks or all icon clicks ).

I will update my pull request accordingly tomorrow. Even so, to me the hook seems to be a better solution.

I will also create another pull request tomorrow that determines if the click is from label or icon and surfaces this information to onNodeToggle as an additional argument - 'label'|'icon'|'keyboard'.

Then @eps1lon can choose to have either / both and possibly to include the hook.

@tonyhallett
Copy link
Contributor

sandbox using the new onNodeToggle (#20609) to allow expansion only for icon ( or label ) click. Hooks can be created that are similar ( and simpler ) to npm useseparatetoggleclick.

@isrsen1
Copy link

isrsen1 commented Apr 21, 2020

@tonyhallett is it possible to attach onClick event on label only with useseparatetoggleclick package?

@tonyhallett
Copy link
Contributor

@isrsen1 the purpose of the useseparatetoggleclick is to manage separated click toggling for you. If you specifically need an onclick on the label provide a span as the label and handle the click on that.

@adamkulasiak
Copy link

@tonyhallett is it possible to set default expanded nodes with useseparatetoggleclick package?

@turnkeyDoug
Copy link

@tonyhallett Thanks so much for useseparatetoggleclick - I am curious... is this a "temporary" solution that will be replaced once this feature comes out of lab? (and btw - what is the best way to find out when things leave lab and go to core?)

@turnkeyDoug
Copy link

@tonyhallett Just by way of update (and for future people with the problem)... I found that the best path to solving this was to take over the selection/expanded logic that you guys are already exposing and have a demo for on this page under "Controlled tree view"... (though I think it would be cool if there were behavior props right on the tree)... this method allows me do all sorts of other behaviour easily anyway

`
let selectingNode = false

const handleToggle = (event, nodeIds) => {
if ( !selectingNode ) {
setExpanded(nodeIds);
selectingNode = false
//doSomethingElse?
}
};

const handleSelect = (event, nodeIds) => {
selectingNode = true
// do something else
}
`

@NiroSenor
Copy link

@turnkeyDoug how would you prevent a node being selected i.e. stopping the onNodeSelect function when an icon is clicked? Could you share your code snippet please on how you resolved it as mentioned above?

@mikeizzy
Copy link

Not the most efficient, but the workaround that worked for me was to check if your handle is a parent of the event target:

const [expanded, setExpanded] = React.useState([]);
const handleToggle = (event, nodeIds) => {
  if (event.target.closest('.someClass')) {
    setExpanded(nodeIds);
  }
};

<TreeView onNodeToggle={handleToggle} expanded={expanded}>
  ...
</TreeView>

@NiroSenor
Copy link

Hi @mikeizzy, do you have some some sample code in somewhere I can look at... I am a total rookie in coding working my way around - Thanks!!!

@mikeizzy
Copy link

The controlled TreeView example is on their docs site:
https://material-ui.com/components/tree-view/#controlled-tree-view

Read about using state hooks on the React official site:
https://reactjs.org/docs/hooks-state.html

Using the DOM's "closest" method:
https://developer.mozilla.org/en-US/docs/Web/API/Element/closest

-you can setExpanded only if the clicked element is a child of whichever node you decide to be your control.

  const handleToggle = (event, nodeIds) => {
    if (event.target.closest(".MuiTreeItem-iconContainer")) {
      setExpanded(nodeIds);
    }
  };

@NiroSenor
Copy link

Thank you @mikeizzy,

I implemented your suggestion. May I request if you have a solution for how to prevent onNodeSelect when you expand/collapse icon. I would wish only to expand/collapse (onNodeToggle) icon when I click on Icon but not select any particular node (onNodeSelect)

@flora8984461
Copy link

Thank you @mikeizzy,

I implemented your suggestion. May I request if you have a solution for how to prevent onNodeSelect when you expand/collapse icon. I would wish only to expand/collapse (onNodeToggle) icon when I click on Icon but not select any particular node (onNodeSelect)

Also looking for this. Thank you.

@cogier
Copy link

cogier commented Aug 31, 2020

I don't think this has been mentioned yet, but since version v4.0.0-alpha.52 of material ui lab you can specify the onLabelClick prop.
You can then call event.preventDefault() to prevent onNodeToggle from being called.
Like this
onLabelClick={(event) => {event.preventDefault();}}

https://material-ui.com/api/tree-item/

@Jared-Dahlke
Copy link

Jared-Dahlke commented Sep 27, 2020

@NiroSenor @flora8984461
Here is @mikeizzy solution expanded to include "only select if clicking label" functionality:

//only expand if icon was clicked
const handleToggle = (event, nodeIds) => {
    event.persist()
    let iconClicked = event.target.closest(".MuiTreeItem-iconContainer")
    if(iconClicked) {
      setExpanded(nodeIds);
    }
  };

//only select if icon wasn't clicked
  const handleSelect = (event, accountId) => {
    event.persist()
    let iconClicked = event.target.closest(".MuiTreeItem-iconContainer")
    if(!iconClicked) {
      setSelected(accountId);
    }
  };

This works perfectly, but I can't believe I just spent an hour and a half trying to get this functionality to work!

@flora8984461
Copy link

@NiroSenor @flora8984461
Here is @mikeizzy solution expanded to include "only select if clicking label" functionality:

//only expand if icon was clicked
const handleToggle = (event, nodeIds) => {
    event.persist()
    let iconClicked = event.target.closest(".MuiTreeItem-iconContainer")
    if(iconClicked) {
      setExpanded(nodeIds);
    }
  };

//only select if icon wasn't clicked
  const handleSelect = (event, accountId) => {
    event.persist()
    let iconClicked = event.target.closest(".MuiTreeItem-iconContainer")
    if(!iconClicked) {
      setSelected(accountId);
    }
  };

This works perfectly, but I can't believe I just spent an hour and a half trying to get this functionality to work!

Thank you so much for your great work!

@mInzamamMalik
Copy link

I have same issue, it always close on click even when I dont want it to be closed

@mInzamamMalik
Copy link

mInzamamMalik commented Nov 17, 2020

Here is the solution I did:

 const handleToggle = (event, nodeIds) => {
    console.log("toggle changer");
    console.log("nodeIds:", nodeIds);
    console.log("selected:", selected);
    console.log("expanded:", expanded);

    const A = nodeIds;
    const B = expanded;
    const clicked = A.filter((n) => !B.includes(n))[0];

    const _A = expanded;
    const _B = nodeIds;
    const closed = _A.filter((n) => !_B.includes(n))[0];

    console.log("clicked: ", clicked);
    console.log("closed: ", closed);

    if (clicked === selected) {
      setExpanded(nodeIds);
    } else if (selected !== closed) {
      setSelected(nodeIds);
    } else {
      setExpanded(nodeIds);
    }
  };

I just tweaked this code a little bit https://material-ui.com/components/tree-view/#controlled-tree-view
and changed handleToggle functions a little bit according to my need,

Here is what I have achived for my specific usecase:

ezgif-1-6af846132ea6

any better solution is always welcome 馃槉

@suwampy
Copy link

suwampy commented Jan 28, 2021

korean plz :-(

@hoanvotran
Copy link

Fantastic Solution!

@NiroSenor @flora8984461
Here is @mikeizzy solution expanded to include "only select if clicking label" functionality:

//only expand if icon was clicked
const handleToggle = (event, nodeIds) => {
    event.persist()
    let iconClicked = event.target.closest(".MuiTreeItem-iconContainer")
    if(iconClicked) {
      setExpanded(nodeIds);
    }
  };

//only select if icon wasn't clicked
  const handleSelect = (event, accountId) => {
    event.persist()
    let iconClicked = event.target.closest(".MuiTreeItem-iconContainer")
    if(!iconClicked) {
      setSelected(accountId);
    }
  };

This works perfectly, but I can't believe I just spent an hour and a half trying to get this functionality to work!

@muhammada86
Copy link

muhammada86 commented Sep 21, 2021

here is the simple solution worked for me.

` const renderTree = (nodes) => {
return (
<StyledTreeItem
key={nodes.id}
nodeId={nodes.id}
label={
<div onClick={(e) => {

                    e.preventDefault();
                }
                }>
                    <p>{nodes.title}</p>
                </div>
            }
        >
            {Array.isArray(nodes.children)
                ? nodes.children.map((node) => renderTree(node))
                : null}
        </StyledTreeItem>
    );
};`

Prevent the default event and propogation in label on TreeItem and rest is same like

<TreeView onNodeToggle={handleToggle} expanded={expanded} defaultExpanded={[ "EC1", "EC2", "EC3", "EC4" ]} defaultCollapseIcon={<MinusSquare />} defaultExpandIcon={<PlusSquare />} defaultSelected={["EC4"]} defaultEndIcon={<CloseSquare />} > {listItem && renderTree(listItem[0])} </TreeView>

handle Toggle is
const handleToggle = (event, nodeIds) => {
setExpanded(nodeIds);
};
where the expanded is the array of parent ID's

@NickEmpetvee
Copy link

@mikeizzy How would your solution apply in v5.x?

@awerlang
Copy link

For v5.x:

<TreeItem label={<div onClick={event => event.stopPropagation()}>{label}</div>} />

@alexya
Copy link

alexya commented Mar 22, 2024

@NiroSenor @flora8984461 Here is @mikeizzy solution expanded to include "only select if clicking label" functionality:

//only expand if icon was clicked
const handleToggle = (event, nodeIds) => {
    event.persist()
    let iconClicked = event.target.closest(".MuiTreeItem-iconContainer")
    if(iconClicked) {
      setExpanded(nodeIds);
    }
  };

//only select if icon wasn't clicked
  const handleSelect = (event, accountId) => {
    event.persist()
    let iconClicked = event.target.closest(".MuiTreeItem-iconContainer")
    if(!iconClicked) {
      setSelected(accountId);
    }
  };

This works perfectly, but I can't believe I just spent an hour and a half trying to get this functionality to work!

Thanks. It works. And I refactored it to meet my requirement.

  function handleNodeToggle(event: React.SyntheticEvent, nodeIds: string[]) {
    event.persist();
    const iconClicked = (event.target as any).closest(".MuiTreeItem-iconContainer");
    const isDoubleClicked = (event as any).detail === 2;
    if (iconClicked || isDoubleClicked) {
      setExpanded(nodeIds);
    }
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: tree view TreeView, TreeItem. This is the name of the generic UI component, not the React module!
Projects
None yet
Development

Successfully merging a pull request may close this issue.