Close material-ui popper when on clickAway - javascript

I have a material ui popper and I am trying to make it close when I click outside of the popper using
ClickAwayListener, but I cannot get this to work. I added the ClickAwayListener around the popper and tried adding it around the content in the popper but nothing seams to work.
I am really new to material-ui so I am a bit lost on how this should be done
This is my code
const Experiences = memo(
(props) => {
const { className } = props;
const classes = useStyles(props);
const [anchorEl, setAnchorEl] = React.useState(null);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
// const open = Boolean(anchorEl);
const handleClickAway = () => {
setAnchorEl(false);
};
const experience = (img, title, id, popoverCategory) => (
<div
className="experience"
aria-describedby={id}
id={id}
onClick={handleClick}
onKeyDown={handleClick}
role="button"
tabIndex="0"
>
<img
data-sizes="auto"
className="lazyload"
data-src={img}
alt={title}
/>
<div className="experience-title">
<Typography
color="textSecondary"
variant="subtitle2"
className="highlight highlight1"
display="inline"
>
{ title }
</Typography>
</div>
<ClickAwayListener onClickAway={handleClickAway}>
<Popper
id={id}
open={anchorEl && anchorEl.id === id}
anchorEl={anchorEl}
className={clsx(classes[id])}
modifiers={{
flip: {
enabled: false,
},
}}
>
<Button >x</Button>
<div className={clsx(classes.paper)}>
{
popoverCategory.map(url => (
<img
key={id}
data-sizes="auto"
className="lazyload"
src={url}
alt={title}
/>
))
}
</div>
</Popper>
</ClickAwayListener>
</div>
);

You may toggle your <Popper /> component visibility using the variable in the local state of the parent component and pass it down as a prop:
//dependencies
const { render } = ReactDOM,
{ useState } = React,
{ Popper, Button, Paper, ClickAwayListener } = MaterialUI
//custom popper
const MyPopper = ({isOpen,clickAwayHandler}) => (
<ClickAwayListener onClickAway={clickAwayHandler}>
<Popper open={isOpen}>
<Paper className="popper">There goes my custom popper</Paper>
</Popper>
</ClickAwayListener>
)
//main page
const MainPage = () => {
const [isOpen, setIsOpen] = useState(true),
clickAwayHandler = () => setIsOpen(false),
clickHandler = () => setIsOpen(true)
return (
<div>
<Button onClick={clickHandler}>Toggle pop-up</Button>
{
isOpen && <MyPopper {...{clickAwayHandler,isOpen}} />
}
</div>
)
}
//render
render (
<MainPage />,
document.getElementById('root')
)
.popper {
display: block;
position: relative;
top: 50px;
left: 100px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><script src="https://unpkg.com/#material-ui/core#latest/umd/material-ui.development.js"></script><div id="root"></div>

It is not working because por the Popper component is a Portal and with portals you have to use de ClickAwayListener in a small different way.
You can check the example at MUI documentation but the short summary is that the button that opens the popper has to be inside the ClickAwayListener too and you have to check if it's open before rendering the Popper.
const Experiences = memo(
(props) => {
const { className } = props;
const classes = useStyles(props);
const [anchorEl, setAnchorEl] = React.useState(null);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const open = Boolean(anchorEl);
const handleClickAway = () => {
setAnchorEl(false);
};
const experience = (img, title, id, popoverCategory) => (
<ClickAwayListener onClickAway={handleClickAway}> // This wraps all
<div
className="experience"
aria-describedby={id}
id={id}
onClick={handleClick}
onKeyDown={handleClick}
role="button"
tabIndex="0"
>
<img
data-sizes="auto"
className="lazyload"
data-src={img}
alt={title}
/>
<div className="experience-title">
<Typography
color="textSecondary"
variant="subtitle2"
className="highlight highlight1"
display="inline"
>
{ title }
</Typography>
</div>
{open ? ( // You do the check here
<Popper
id={id}
open={anchorEl && anchorEl.id === id}
anchorEl={anchorEl}
className={clsx(classes[id])}
modifiers={{
flip: {
enabled: false,
},
}}
>
<Button >x</Button>
<div className={clsx(classes.paper)}>
{
popoverCategory.map(url => (
<img
key={id}
data-sizes="auto"
className="lazyload"
src={url}
alt={title}
/>
))
}
</div>
</Popper>
) : null}
</div>
</ClickAwayListener>
);

Perhaps MUI has created a new component that handles this for us since the time of this post.
Popover Component: https://mui.com/material-ui/react-popover/
It works similar to the Modal component that handles on click out for you.
Hope this helps.

Related

Long press event in ReactJs

I have a favorites page where when I single click a button it will redirect to another page but when I hold it, it will have a pop up to remove from favorites how can I achieve that?
This is my favorite page
const FavoritePage = () => {
const getArray = JSON.parse(
localStorage.getItem(favoriteProductsStorageKey) || []
)
return (
<>
<Grid container spacing={3} className={classes.heading}>
<Grid item xs={2}>
<Box pt={0.5}>
<Link to="#">
<ArrowBackIcon className={classes.backSize} />
</Link>
</Box>
</Grid>
<Grid item xs={10}>
<Typography variant="h6" className={classes.header}>
My Favorites
</Typography>
</Grid>
</Grid>
<Box pt={1}>
{getArray.length > 0 ? (
<div className={classes.root}>
{getArray.map((product) => (
<div className="ProductCard" key={product._id}>
<div>
<Link to="product">
<img
className="ProductImage"
src={product.imagePrimary}
alt={product.name}
/>
</Link>
</div>
<div className="ProductCardDetails">
<div className="NameAndPrice">
<div className="ProductName">{product.name}</div>
</div>
</div>
</div>
))}
</div>
) : (
<h4 className={classes.top}>
Add your favorite products here by tapping the{" "}
<span className={classes.icon}>♥</span> symbol on the product.
</h4>
)}
</Box>
</>
)
}
export default FavoritePage
this is the imported function coming from my helpers.js to remove the favorites from local storage
function removeFavorite(product) {
const newFavoriteProducts = favorites.filter(
(iteratedProduct) => iteratedProduct._id !== product._id
)
setFavorites(newFavoriteProducts)
setToLocalStorage(favoriteProductsStorageKey, newFavoriteProducts)
}
I'm trying to find a solution on how to implement the long press to a single product to be removed from the favorites in the local storage.
As far as I am concerned There is no pure html or javascript way to accomplish this. But there is a trick in javascript you use the onmousedown listener and onmouseup listener. Here is a code to do so:
var timeout;
var clicked = function (){
timeout = window.setTimeout(function(){
//Do work here
alert('You clicked the button for 1300');
},1300)//Change time according to need but most commonly used time is 1300 milliseconds.
}
var unclicked = function (){
clearTimeout(timeout);
}
span{
font-size: 130%;
border: 2px solid blue;
}
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div><span onmousedown="clicked();" onmouseup="unclicked();">Click me long!</span> To see the magic.</div>
</body>
</html>
What this code does is very simple it just sets timeout and if it is completed it executes the code.
Implementation using react hooks
codesandbox.io
Full code:
import { useState, useEffect } from "react";
const LongPressButton = (props) => {
const [waitingForLongPress, setWaitingForLongPress] = useState(false);
const [timeoutId, setTimeoutId] = useState(null);
useEffect(() => {
if (timeoutId) {
clearTimeout(timeoutId);
}
if (waitingForLongPress) {
setTimeoutId(
setTimeout(() => {
props.onLongPress();
setWaitingForLongPress(false);
}, props.longPressDelayMS)
);
}
}, [waitingForLongPress]);
return (
<button
onMouseDown={() => setWaitingForLongPress(true)}
onMouseUp={() => setWaitingForLongPress(false)}
onClick={props.onClick}
>
{props.label}
</button>
);
};
export default function App() {
const [longPressCount, setLongPressCount] = useState(0);
const [clickCount, setClickCount] = useState(0);
return (
<div className="App">
<div>Times click: {clickCount}</div>
<div>Times long-pressed: {longPressCount}</div>
<LongPressButton
label="Press me"
onLongPress={() => {
setLongPressCount(longPressCount + 1);
}}
onClick={() => {
setClickCount(clickCount + 1);
}}
longPressDelayMS={1000}
/>
</div>
);
}

Accessing functionality from parent component in child component ReactJS

I have working code in App.jsx. Everything is working when this written together in one file.
const App = props => {
const [cartProducts, setCartProducts] = useState([]);
const [products, setProducts] = useState(getProducts());
//const [searchValue, setSearchValue] = useState();
const handleAddProductToCart = productID => {
setCartProducts([...cartProducts, productID]);
};
const handleRemoveFromCart = productID => {
const newCartProducts = cartProducts.filter(id => id !== productID);
setCartProducts(newCartProducts);
};
/*const filterItems = ({ description, title }) => {
return title.toLocaleLowerCase().indexOf(searchValue.toLocaleLowerCase())
|| description.toLocaleLowerCase().indexOf(searchValue.toLocaleLowerCase())
}*/
return (
<>
<Header/>
<Search/>
<Sidebar/>
<div>
{products.map(product => {
const { id, title, description, image } = product;
let haveInCart = false;
cartProducts.forEach(productID => {
if (productID === id) {
haveInCart = true;
}
});
return (
<Card key={id} className="item-card">
<CardImg src={image} alt={title} className="item-img" style={{ height: '260px' }} />
<CardBody style={{ border: 'none' }} className="custom-card-body">
<CardTitle style={{ border: 'none' }} className="custom-card-title">{title}</CardTitle>
<CardText style={{ border: 'none' }} className="custom-card-text">{description}</CardText>
<FontAwesomeIcon icon={faCartPlus} className="add-icon" onClick={() => handleAddProductToCart(id)} />
</CardBody>
</Card>
);
})}
<h2>Your Cart</h2>
{cartProducts.length > 0
? cartProducts.map(productID => {
const productIndex = products.findIndex(product => {
return product.id === productID;
});
let { id, title, image } = products[productIndex];
return (
<Card key={id} className="item-card">
<CardImg src={image} alt={title} className="item-img" style={{ height: '260px' }} />
<CardBody style={{ border: 'none' }} className="custom-card-body">
<CardTitle style={{ border: 'none' }} className="custom-card-title">{title}</CardTitle>
<FontAwesomeIcon icon={faTrash} className="remove-icon" onClick={() => handleRemoveFromCart(id)} />
</CardBody>
</Card>
);
})
: "Yor Cart is Empty :("}
</div>
</>
)
}
export default App;
I want to put product Card and cart Card into it's own component. And when I'm doing it, like this
const Product = ({product}) => {
const { id, title, description, image } = product;
return (
<Card key={id} className="item-card">
<CardImg src={image} alt={title} className="item-img" style={{ height: '260px' }} />
<CardBody style={{ border: 'none' }} className="custom-card-body">
<CardTitle style={{ border: 'none' }} className="custom-card-title">{title}</CardTitle>
<CardText style={{ border: 'none' }} className="custom-card-text">{description}</CardText>
<FontAwesomeIcon icon={faCartPlus} className="add-icon" /*onClick={() => handleAddProductToCart(id)}*/ />
</CardBody>
</Card>
)
}
I'm not getting properties of product in my Card. I want to make my code better organized and save it's functionality. How to access those states from Product and Cart component?
EDIT
Here's the link to codesandbox
https://codesandbox.io/s/late-cookies-r2inh?file=/src/App.jsx&fbclid=IwAR38tcE39tVL51YpG4_6A1HRz-kth1GSIocQWMPrU3QXepc5CHUNn-ZqiG8
EDIT 2 How I can make items in cart be displaying beside Product component, on the left side? Should I create entire Cart component for it?
Your Product.jsx file should look like this:
import React from "react";
import { FontAwesomeIcon } from "#fortawesome/react-fontawesome";
import { faCartPlus } from "#fortawesome/free-solid-svg-icons";
import Card from "react-bootstrap/Card";
import CardBody from "react-bootstrap/Card";
import CardImg from "react-bootstrap/CardImg";
import CardTitle from "react-bootstrap/Card";
import CardText from "react-bootstrap/Card";
const Product = ({ product, handleAddProductToCart }) => {
const { id, title, description, image } = product;
return (
<Card key={id} className="item-card">
<CardImg
src={image}
alt={title}
className="item-img"
style={{ height: "260px" }}
/>
<CardBody style={{ border: "none" }} className="custom-card-body">
<CardTitle style={{ border: "none" }} className="custom-card-title">
{title}
</CardTitle>
<CardText style={{ border: "none" }} className="custom-card-text">
{description}
</CardText>
<FontAwesomeIcon
icon={faCartPlus}
className="add-icon"
onClick={() => handleAddProductToCart(id)}
/>
</CardBody>
</Card>
);
};
export default Product;
And in your App.jsx you can map the products array like this:
{products.map((product) => (
<Product
product={product}
key={product.id}
handleAddProductToCart={handleAddProductToCart}
/>
))}
EDIT:
https://codesandbox.io/s/determined-swartz-5q2fo
I edited the codesandbox, i made the Product component accept both add and remove product callback and the "haveInCart" prop. It will decide which icon and which callback to use base on that prop. I'm not sure about this approch tho

React hide sidebar on mobile and show on big screen

I got a Sidebar from MaterialUI and I want it to hide on mobile screen, but if you click on the menu icon it should be visible and on big screens it should be visible by default, if you click on the menu icon it should close. I would like to use the library "react-responsive" for this, but I don't understand how to use it.
Sidebar.js
// ...imports and styles..
export const SideBar = ({ open }) => {
const history = useHistory();
return (
<StyledSideBar variant="permanent" open={open}>
<ModuleButton onClick={() => history.push('/dashboard')}>
<Tooltip title="Dashdoard" placement="right" arrow >
<FontAwesomeIcon icon={faTachometerAltFast} />
</Tooltip >
</ModuleButton>
<ModuleButton onClick={() => history.push('/chat')}>
<Tooltip title="Chat" placement="right" arrow>
<FontAwesomeIcon icon={faCommentsAlt} />
</Tooltip>
</ModuleButton>
<ModuleButton onClick={() => history.push('/calendar')}>
<Tooltip title="Calendar" placement="right" arrow>
<FontAwesomeIcon icon={faCalendarAlt} />
</Tooltip>
</ModuleButton>
</StyledSideBar>
);
}
export default SideBar;
MainPage.js
...imports and styles...
export const MainPage = () => {
const dispatch = useDispatch();
const onLogout = () => dispatch(logout());
const [showSidebar, setShowSidebar] = useState(false);
const onToggleSidebar = () => {
setShowSidebar(!showSidebar);
};
const isDesktopOrLaptop = useMediaQuery({
query: '(min-device-width: 1224px)'
})
const isTabletOrMobileDevice = useMediaQuery({
query: '(max-device-width: 1224px)'
})
return (
<>
<MainHeader onLogout={onLogout} onToggleSidebar={onToggleSidebar} />
{isDesktopOrLaptop &&
<SideBar open={showSidebar} />
}
{isTabletOrMobileDevice &&
<SmSideBar open={showSidebar} />
}
<Container>
<BrowserRouter>
<Route path="*" exact component={MainContent} />
</BrowserRouter>
</Container>
</>
);
}
export default authGuardFactory(Redirect)(emailGuardFactory(Redirect)(MainPage));
SmSideBar.js
...import and styles...
export const SmSideBar = ({ open }) => {
const history = useHistory();
return (
<StyledSideBar variant="persistent" open={open} >
<ModuleButton onClick={() => history.push('/dashboard')}>
<Tooltip title="Dashdoard" placement="right" arrow >
<FontAwesomeIcon icon={faTachometerAltFast} />
</Tooltip >
</ModuleButton>
<ModuleButton onClick={() => history.push('/chat')}>
<Tooltip title="Chat" placement="right" arrow>
<FontAwesomeIcon icon={faCommentsAlt} />
</Tooltip>
</ModuleButton>
<ModuleButton onClick={() => history.push('/calendar')}>
<Tooltip title="Calendar" placement="right" arrow>
<FontAwesomeIcon icon={faCalendarAlt} />
</Tooltip>
</ModuleButton>
</StyledSideBar>
);
}
export default SmSideBar;
I tried to add another Sidebar only for Tablet and Mobile, but it dont work.
The only difference between the two is the variant.
I solved the Problem with methods by using react-responsive library and js Breakpoints.
MainPage.js
export const MainPage = ( { match }) => {
const dispatch = useDispatch();
const onLogout = () => dispatch(logout());
const onSizeChange = (matches) => {
setShowSidebar(matches);
}
const isDesktopOrLaptop = useMediaQuery(breakpoints.MAndUp, undefined, onSizeChange)
const [showSidebar, setShowSidebar] = useState(isDesktopOrLaptop);
const onToggleSidebar = () => {
setShowSidebar(!showSidebar);
};
Now everything works fine :)

React Material UI + React Router >>>> I cannot use Redirect inside a onClick function in a button

I am trying to trigger the Redirect React Dom
that is my button component in the handleMenuItemClick() function. But nothing happens.
I have tried a bunch of stuff but but still no success.
How can I make the both work together? My best try was to make a function that return the Redirect component as I saw in one post around, but still no success.
My Code:
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import { Grid, Button, ButtonGroup, ArrowDropDownIcon, ClickAwayListener, Grow, Paper, Popper, MenuItem, MenuList, Link } from '#material-ui/core/Grid';
const SplitButton = (props) => {
const [open, setOpen] = React.useState(false);
const anchorRef = React.useRef(null);
const [selectedIndex, setSelectedIndex] = React.useState(1);
const myGroups = props.myGroups
const handleMenuItemClick = (event, index) => {
setSelectedIndex(index);
setOpen(false);
return <Redirect to={`/groups/${index}`} />
};
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen);
};
const handleClose = (event) => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
setOpen(false);
};
return (
<>
<ButtonGroup variant="contained" color="primary" ref={anchorRef} aria-label="split button">
<Button onClick={null}>My Groups</Button>
<Button
color="primary"
size="small"
aria-controls={open ? 'split-button-menu' : undefined}
aria-expanded={open ? 'true' : undefined}
aria-label="select merge strategy"
aria-haspopup="menu"
onClick={handleToggle}
>
<ArrowDropDownIcon />
</Button>
</ButtonGroup>
<Popper open={open} anchorEl={anchorRef.current} role={undefined} transition disablePortal>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
}}
>
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MenuList id="split-button-menu">
{ myGroups.map((group) => (
<MenuItem
key={group.id}
onClick={(event) => handleMenuItemClick(event, group.id)}
>
{group.title}
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</>
);
}
export default SplitButton
You can redirect user via 2 methods: useHistory or <Redirect />
useHistory hook
If you want to redirect the user directly on click, you can treat the code imperatively and tell React what to do:
const history = useHistory();
const handleMenuItemClick = (event, index) => {
setSelectedIndex(index);
setOpen(false);
history.push(`/groups/${index}`)
};
More info https://reactrouter.com/web/api/Hooks/usehistory
Redirect component
Or if you feel more comfortable using React's default declarative model, you can say what's changed and allow your code to react to this change:
const [redirectUrl, setRedirectUrl] = useState('')
const handleMenuItemClick = (event, index) => {
setSelectedIndex(index);
setOpen(false);
setRedirectUrl(`/groups/${index}`)
};
if (redirectUrl) {
return <Redirect to={redirectUrl} />
}
return (
<>
<ButtonGroup variant="contained" color="primary" ref={anchorRef} aria-label="split button">
<Button onClick={null}>My Groups</Button>
<Button
...
More info https://reactrouter.com/web/api/Redirect

Temporary Navigation bar not closing after clicking on a icon button reactjs

I created a navigation bar from the material-ui website and I have a onClick that when the user clicks the icon button the navigation will get redirected to a new page and I would like for the navigation to close afterwards. I'v tried different things, but for some reason it will not close.
The only thing that it does now is gets redirected to a new page and the navigation drawer continues to stay open.
I have a a function called handleDrawerClose() that closes the drawer, a const called navigation that creates the text and components and I created a const called handleNavigation that pushes the links, which makes the page redirect. Is there a way to call both of these someway. Thank you.
Below is my code:
const navigation = [
{ to: '/', text: 'Upload', Icon: InboxIcon },
{ to: '/email', text: 'Send', Icon: MailIcon }
]
const NavLinks = ({ links, onClick }) => {
const _onClick = to => () => onClick(to);
return (
<List>
{links.map(({ to, text, Icon }) => (
<ListItem key={to} button onClick={_onClick(to)}>
<ListItemIcon>
<Icon />
</ListItemIcon>
<ListItemText primary={text} />
</ListItem>
))}
</List>
)
}
const IconArrow = ({ onClick }) => {
return (
<IconButton onClick={onClick}>
<ChevronLeftIcon />
</IconButton>
)
}
export default withRouter(({ history }) => {
const classes = useStyles();
const [_, { logout }] = useAppAuth();
const [open, setOpen] = React.useState(false);
const [state, setState] = React.useState({
left: false,
});
function handleDrawerOpen() {
setOpen(true);
}
function handleDrawerClose() {
setOpen(false);
}
const handleNavigation = to => () => history.push(to);
return (
<Fragment>
<AppBar position='static'>
<Toolbar className={classes.toolbar}>
<IconButton
color='inherit'
edge='start'
aria-label='open menu drawer'
onClick={handleDrawerOpen}
>
<MenuIcon />
</IconButton>
<Link to='/'>
<img alt='Logo' src={logo} className={classes.image} />
</Link>
<Typography variant='h6' className={classes.title}>
{process.env.REACT_APP_NAME}
</Typography>
<Tooltip title='Logout'>
<Link to='/login'>
<IconButton onClick={logout} className={classes.logout}>
<LogoutIcon />
</IconButton>
</Link>
</Tooltip>
</Toolbar>
</AppBar>
<Drawer
className={classes.drawer}
variant='temporary'
anchor='left'
open={open}
>
<div className={classes.iconArrow}>
<IconArrow onClick={handleDrawerClose} />
</div>
<Divider />
<NavLinks
links={navigation}
onClick={() => handleNavigation}
/>
</Drawer>
</Fragment>
)
})
Why dont you just close it in the handler?
const handleNavigation = to => {
setOpen(false); // add this
history.push(to);
}

Categories