Material UI Menu with subitems won't close whole menu - javascript

I have a Material UI menu component with custom MenuItems. Now when the menu and a submenu is opened I would like to be able to close the whole menu when clicking outside of the menu. I still need to do two clicks, first closing the submenu and then the actual menu.
The documentation MUI menu refers to using the anchorEl as the boolean to determine if menu is open but even i send the close callback function from the custom menu items it only closes itself with the handleClose() when clicking outside the component. So in the picture attached only the Container containing Corporate finance is closed and not also the menu.
Can't understand, as the anchorEl is turning to null in the closeFunction the core menu still stays open. Tried with menu and MenuItems and Popover that are built on the Modal component.
MenuComponent:
const MoreMenu = ({
userTagData,
onChangeItemName,
selectedTags,
onTagSelected,
onRemoveItem,
data,
item,
}) => {
const [anchorEl, setAnchorEl] = useState(null);
const [subMenuPopUpName, setSubMenuPopUpName] = useState('');
const renderChangeTitlePopUp = () => (
<ChangeTitleContainer
onChangeItemName={onChangeItemName}
closeMenuItem={handleClose}
item={item}
/>
);
const renderAddTagPopUp = () => (
<AddTagContainer
item={item}
userTagData={userTagData}
selectedTags={selectedTags}
onTagSelected={onTagSelected}
/>
);
const renderRemoveItemContainer = () => (
<RemoveItemContainer
item={item}
onRemoveItem={onRemoveItem}
closeMenuItem={handeleClose}
/>
);
const renderDefaultPopup = () => (
<div><p>Standard menu post</p></div>
);
const menuItemPopUpSwitcher = (name) => {
switch (name) {
case ADDTAG:
return renderAddTagPopUp();
case CHANGETITLE:
return renderChangeTitlePopUp();
case REMOVEITEM:
return renderRemoveItemContainer();
default:
return renderDefaultPopup();
}
};
const handleMenuItemClick = (item, event) => {
setAnchorEl(event.currentTarget);
setSubMenuPopUpName(item.title);
setAnchorEl(event.currentTarget);
menuItemPopUpSwitcher(item);
};
const handleClose = () => {
setAnchorEl(null);
};
return (
<Fragment>
<MoreButton
data={data}
handleMenuItemClick={handleMenuItemClick}
/>
<Menu
open={Boolean(anchorEl)}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: 'top',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
>
{menuItemPopUpSwitcher(subMenuPopUpName)}
</Menu>
</Fragment>
);
};custom

Was able to solve problem by adding another state for the parent anchor. So the parent menu has its own anchor and the child popup its own anchor and then handleClose() disables them both. See example and 5 lines marked with //THIS IS NEW:
const MoreMenu = ({
userTagData,
onChangeItemName,
selectedTags,
onTagSelected,
onRemoveItem,
data,
item,
}) => {
const [parentAnchorEl, setParentAnchorEl] = useState(null); // THIS IS NEW
const [anchorEl, setAnchorEl] = useState(null);
const [subMenuPopUpName, setSubMenuPopUpName] = useState('');
const handleMoreButtonClick = (event) => { // THIS IS NEW
setParentAnchorEl(event.currentTarget);
};
const renderChangeTitlePopUp = () => (
<ChangeTitleContainer
onChangeItemName={onChangeItemName}
closeMenuItem={handleClose}
item={item}
/>
);
const renderAddTagPopUp = () => (
<AddTagContainer
item={item}
userTagData={userTagData}
selectedTags={selectedTags}
onTagSelected={onTagSelected}
/>
);
const renderRemoveItemContainer = () => (
<RemoveItemContainer
item={item}
onRemoveItem={onRemoveItem}
closeMenuItem={handeleClose}
/>
);
const renderDefaultPopup = () => (
<div><p>Standard menu post</p></div>
);
const menuItemPopUpSwitcher = (name) => {
switch (name) {
case ADDTAG:
return renderAddTagPopUp();
case CHANGETITLE:
return renderChangeTitlePopUp();
case REMOVEITEM:
return renderRemoveItemContainer();
default:
return renderDefaultPopup();
}
};
const handleMenuItemClick = (item, event) => {
setAnchorEl(event.currentTarget);
setSubMenuPopUpName(item.title);
setAnchorEl(event.currentTarget);
menuItemPopUpSwitcher(item);
};
const handleClose = () => {
setAnchorEl(null);
setParentAnchorEl(null); //THIS IS NEW
};
return (
<Fragment>
<MoreButton
anchorEl={parentAnchorEl} //THIS IS NEW
data={data}
handleMenuItemClick={handleMenuItemClick}
handleClick={handleMoreButtonClick} //THIS IS NEW
/>
<Menu
open={Boolean(anchorEl)}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: 'top',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
>
{menuItemPopUpSwitcher(subMenuPopUpName)}
</Menu>
</Fragment>
);
};
const MoreButton = ({
data,
handleMenuItemClick,
anchorEl,
handleClick,
handleClose,
}) => (
<div>
<MoreIconMUI onClick={handleClick} />
<Menu
id="menu-appbar"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleClose}
>
{data.map(item => (
<MenuItem
key={item.title}
onClick={event => handleMenuItemClick(item, event)}
>
{item.title}
</MenuItem>
))}
</Menu>
</div>
);

Related

How can I use a popper for menu subcategory in MUI

I have a list of menu categories displayed in my app. On Hover, I want to use a popper to get the subcategories that belongs to the category and display on the popper.
Here I have the states declared and once page loads, I populate the categories and subcategories from the db.
const [categories, setCategories] = useState([])
const [subCategories, setSubCategories] = useState([])
const [placement, setPlacement] = useState()
const [categorySub, setCategorySub] = useState([])
const [anchorEl, setAnchorEl] = React.useState(null)
const handlePopoverOpen = (e) => {
setAnchorEl(e.currentTarget)
}
const handlePopoverClose = () => {
setAnchorEl(null)
}
const open = Boolean(anchorEl)
const id = open ? 'simple-popper' : undefined
useEffect(() => {
loadCategories()
loadSubCategories()
}, [])
const loadCategories = async () => {
const res = await getAllCategories()
setCategories(res.category)
}
const loadSubCategories = async () => {
const res = await getAllSubCategories()
setSubCategories(res.subCategory)
}
Then I create a function that should filter the subCategories based on the categories
const getSub = (categoryId) => {
const sub = subCategories.filter((a) => a.parent === categoryId)
const data = sub.map((s) => s.name)
setCategorySub(data)
}
Here's what gets returned.
here, I display the menu categories. but I am having issues running the function to display on the popper.
Don't know how to call. the getSub() function, passing the category ID before displaying the popper. Is there a better way of resolving this?
return (
<Paper>
<MenuList>
<MenuItem>
<Typography variant='h6' component='body'>
ALL CATEGORIES
</Typography>
</MenuItem>
<Divider />
{categories &&
categories.map((category) => (
<>
<MenuItem
key={category._id}
sx={{ pl: 4 }}
aria-describedby={id}
onMouseOver={() => {
getSub(category._id)
}}
onMouseEnter={handlePopoverOpen}
onMouseLeave={handlePopoverClose}
>
<Typography>{category.name}</Typography>
</MenuItem>
<Divider />
<Popper
id={id}
open={open}
anchorEl={anchorEl}
placement='right'
>
<Box sx={{ border: 1, p: 1, bgcolor: 'background.paper' }}>
{categorySub.map((item) => (
<Typography>{item.name}</Typography>
))}
</Box>
</Popper>
</>
))}
</MenuList>
</Paper>

React - Close MUI drawer from nested menu

I'm using this excellent example (Nested sidebar menu with material ui and Reactjs) to build a dynamic nested menu for my application. On top of that I'm trying to go one step further and put it into a Material UI appbar/temporary drawer. What I'd like to achieve is closing the drawer when the user clicks on one of the lowest level item (SingleLevel) however I'm having a tough time passing the toggleDrawer function down to the menu. When I handle the click at SingleLevel I consistently get a 'toggle is not a function' error.
I'm relatively new to this so I'm sure it's something easy and obvious. Many thanks for any answers/comments.
EDIT: Here's a sandbox link
https://codesandbox.io/s/temporarydrawer-material-demo-forked-v11ur
Code is as follows:
Appbar.js
export default function AppBar(props) {
const [drawerstate, setDrawerstate] = React.useState(false);
const toggleDrawer = (state, isopen) => (event) => {
if (event.type === 'keydown' && (event.key === 'Tab' || event.key === 'Shift')) {
return;
}
setDrawerstate({ ...state, left: isopen });
};
return (
<Box sx={{ flexGrow: 1 }}>
<AppBar position="static" color="secondary">
<Toolbar>
<IconButton
size="large"
edge="start"
color="primary"
aria-label="menu"
onClick={toggleDrawer('left', true)}
>
<MenuIcon />
</IconButton>
<img src={logo} alt="logo" />
</Toolbar>
<Drawer
anchor='left'
open={drawerstate['left']}
onClose={toggleDrawer('left', false)}
>
<Box>
<AppMenu toggleDrawer={toggleDrawer} />
</Box>
</Drawer>
</AppBar>
</Box >
)
}
Menu.js
export default function AppMenu(props) {
return MenuItemsJSON.map((item, key) => <MenuItem key={key} item={item} toggleDrawer={props.toggleDrawer} />);
}
const MenuItem = ({ item, toggleDrawer }) => {
const MenuComponent = hasChildren(item) ? MultiLevel : SingleLevel;
return <MenuComponent item={item} toggleDrawer={toggleDrawer} />;
};
const SingleLevel = ({ item, toggleDrawer }) => {
const [toggle, setToggle] = React.useState(toggleDrawer);
return (
<ListItem button onClick={() => { toggle('left', false) }}>
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={item.title} />
</ListItem>
);
};
const MultiLevel = ({ item }) => {
const { items: children } = item;
const [open, setOpen] = useState(false);
const handleClick = () => {
setOpen((prev) => !prev);
};
return (
<React.Fragment>
<ListItem button onClick={handleClick}>
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={item.title} secondary={item.description} />
{open ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{children.map((child, key) => (
<MenuItem key={key} item={child} />
))}
</List>
</Collapse>
</React.Fragment>
);
};
You shouldn't call a react hook inside of any function that is not a react component. Please see React Rules of Hooks
What you could do instead is pass setToggle directly into the Drawer component as a prop and do something like this for it's onClick attribute:
onClick={() => setToggle(<value>)}

Why useState sets data only when you click to button for the second time?

I tried to set value in useState, but it works only when I click the button for the second time, why it can happens?
const DropDownMenu = ({ element, segmentEnd, segmentStart }) => {
const [open, setOpen] = React.useState(false);
const [ratio, setRatio] = React.useState("");
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen);
};
const handleClose = () => {
setOpen(false);
};
const openFitToScreen = () => {
setRatio("Fit to screen");
setOpen(false);
};
const openFitToHeight = () => {
setRatio("Fit to height");
setOpen(false);
};
const openFitToWidth = () => {
setOpen(false);
setRatio("Fit to width");
};
const openOriginal = () => {
setOpen(false);
setRatio("Original");
};
return (
<Box style={{ display: "flex" }}>
<Box>
<Box onClick={handleToggle} style={{ cursor: "pointer" }}>
{ratio !== "" ? ratio : element.ratio}
</Box>
<Popper
open={open}
role={undefined}
transition
disablePortal
style={{ position: "absolute", top: 20, zIndex: 1000 }}
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin:
placement === "bottom" ? "center top" : "center bottom",
}}
>
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MenuList autoFocusItem={open} id="menu-list-grow">
<MenuItem value="Original" onClick={() => openOriginal()}>
Original
</MenuItem>
<MenuItem
value="Fit to screen"
onClick={() => openFitToScreen()}
>
Fit to screen
</MenuItem>
<MenuItem
value="Fit to height"
onClick={() => openFitToHeight()}
>
Fit to height
</MenuItem>
<MenuItem
value="Fit to width"
onClick={() => openFitToWidth()}
>
Fit to width
</MenuItem>
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</Box>
</Box>
);
};
I tried to set value in useState, but it works only when I click the button for the second time, why it can happens?
I tried to set value in useState, but it works only when I click the button for the second time, why it can happens?
Which value is set the second time?
useCallBack(() => <Popper>....</Popper>, [open])
I don't understand why you do that :
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen); // your state need to be a boolean not a function
};
instead try to do this :
const handleToggle = () => {
setOpen(!prevOpen);
};

Problem with opening more than one Expansion Panel

I have a problem with my application. The component ExpansionPanel of Material-ui is opening more than one Panel. I need to open just one.
father.js
<div>
{props.listContratos.map((item, index) => {
if (item.ativo) {
return (
<PartesCardCollapse
listPartes={props.listPartes}
item={item}
key={index}
thunks={props.thunks}
hideSnackbar={props.hideSnackbar}
/>
);
}
})}
</div>
son.js
export default function PartesCardCollapse(props) {
const classes = useStyles();
const [expanded, setExpanded] = React.useState(false);
const handleChange = (expanded, panel) => (event, isExpanded) => {
setExpanded(isExpanded ? panel : false);
};
return (
<div className={classes.root} onClick={getListPartes}>
<ExpansionPanel
expanded={expanded === props.item.id}
onChange={handleChange(expanded, props.item.id)}
>

Add a function into a onClick

I want to use React.js to build a single page application and I want to create a list in a material-ui drawer. I want to add an element into an array every time I press a button but I don't how to write this function.
Here is my buttom:
<RaisedButton
label="Next"
primary={true}
onClick={this.onNext}
/>
Here is onNext function:
onNext = (event) => {
const current = this.state.controlledDate;
const date = current.add(1, 'days');
this.setState({
controlledDate: date
});
this.getImage(moment(date));
}
And this is the code I want to add into onNext function:
menuItems.push(<MenuItem onClick={this.handleClose}>{this.state.image.date}</MenuItem>);
This is a sample code that adds drawer menu items using state
const { RaisedButton, MuiThemeProvider, Drawer, getMuiTheme, MenuItem } = MaterialUI;
class Sample extends React.Component {
state = {
open: false,
items: [],
}
handleClose = () => {
this.setState({ open: false });
}
handleOpen = () => {
this.setState({ open: true })
}
onNext = () => {
this.setState(state => {
return Object.assign({}, state, {
items: state.items.concat([
{
// add any other button props here (date, image, etc.)
text: `Item ${state.items.length + 1}`
}
]),
});
})
}
render() {
return (
<div>
<Drawer
openSecondary={true}
width={200}
open={this.state.open}
>
{this.state.items.map(item => (
<MenuItem onClick={this.handleClose}>{item.text}</MenuItem>
))}
</Drawer>
<RaisedButton
label="Next"
primary={true}
style={{ margin: 12 }}
onClick={this.onNext} />
<RaisedButton
label="Open Drawer"
primary={true}
style={{ margin: 12 }}
onClick={this.handleOpen} />
</div>
);
}
}
const App = () => (
<MuiThemeProvider muiTheme={getMuiTheme()}>
<Sample />
</MuiThemeProvider>
);
ReactDOM.render(
<App />,
document.getElementById('container')
);
Try it here: https://jsfiddle.net/jprogd/eq533rzL/
Hope it should give you an idea how to go further

Categories

Resources