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.
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).
I have the same use case, it could be useful !
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>}/>
}
@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.
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
Could we have an option to expand when clicking the icon or the label, but collapsing only when clicking the icon?
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.
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 :) )
@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.
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
https://github.com/mui-org/material-ui/pull/20087#issuecomment-602176492
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.
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.
@tonyhallett is it possible to attach onClick event on label only with useseparatetoggleclick package?
@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.
@tonyhallett is it possible to set default expanded nodes with useseparatetoggleclick package?
@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?)
@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
}
`
@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?
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>
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!!!
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);
}
};
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)
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.
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();}}
@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!
@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!
I have same issue, it always close on click even when I dont want it to be closed
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,
Most helpful comment
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/