I have a nav menu built with material-ui/core in Navbar.
I use useRef to track the position of clicked button on toggle menu close.
anchorRef.current.contains(event.target)
And I am getting 'Uncaught TypeError: anchorRef.current.contains is not a function' .
I tried 'Object.values(anchorRef.current).includes(event.target)' instead, it always returns false.
-- update --
anchorRef.current.props Object.
withStyles {
props:{
aria-haspopup: "true"
aria-owns: undefined
children: "계정"
className: "nav-menu--btn"
onClic: f onClick()
get ref: f()
isReactWarning: true
arguments: (...)
caller: (...)
length: 0
name: "warnAboutAccessingRef"
...
}, context{...}, refs{...}, ...}
ToggleMenuList
const ToggleMenuList = ({ navAdminList, navAdminItems, classes }) => {
const [activeId, setActiveId] = useState(null);
const anchorRef = useRef(null);
const handleToggle = id => {
setActiveId(id);
};
const handleClose = event => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
setActiveId(null);
};
return (
<React.Fragment>
<div className={`nav-menu--admin ${classes.root}`}>
{navAdminList.map(e => (
<div key={e.id}>
<Button
ref={anchorRef}
aria-owns={activeId === e.id ? 'menu-list-grow' : undefined}
aria-haspopup="true"
onClick={() => handleToggle(e.id)}
>
{e.name}
</Button>
{activeId === e.id && (
<ToggleMenuItems
id={e.id}
activeId={activeId}
handleClose={handleClose}
anchorRef={anchorRef}
items={navAdminItems[e.id]}
/>
)}
</div>
))}
</div>
</React.Fragment>
);
};
export default withStyles(styles)(ToggleMenuList);
ToggleMenuItems
const ToggleMenuItems = ({
listId,
activeId,
handleClose,
anchorRef,
items,
}) => {
const isOpen = activeId === listId;
const leftSideMenu = activeId === 3 || activeId === 4 ? 'leftSideMenu' : '';
return (
<Popper
open={isOpen}
anchorEl={anchorRef.current}
keepMounted
transition
disablePortal
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin:
placement === 'bottom' ? 'center top' : 'center bottom',
}}
className={`toggle-menu ${leftSideMenu}`}
>
<Paper id="menu-list-grow">
<ClickAwayListener
onClickAway={handleClose}
>
<MenuList className="toggle-menu--list">
{items.map(e => (
<MenuItem
key={e.id}
className="toggle-menu--item"
onClick={handleClose}
>
<Link
to={e.to}
className="anchor td-none c-text1 toggle-menu--link"
>
{e.name}
</Link>
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
);
};
export default ToggleMenuItems;
react: ^16.8.6
react-dom: ^16.8.6
react-router-dom: ^4.3.1
#material-ui/core: ^3.1.2
I assume your ToggleMenuItems sets up global(document-level?) event listener on click to collapse Menu on clicking somewhere outside.
And you have a sibling button element. Clicking on that you want to keep menu expanded, right? So that was the point to use .contains in onClick to check if we are clicked outside of ToggleMenuItems but in scope of specific Button. The reason why it does not work: <Button> is custom class-based React component so it returns React component instance in ref. And it does not have any DOM-specific methods like .contains
You can rework you current approach: just stop bubbling event in case Button has been clicked. It would stop global event handler set by ToggleMenuItems to react.
const stopPropagation = (event) => event.stopPropagation();
const ToggleMenuList = ({ navAdminList, navAdminItems, classes }) => {
const [activeId, setActiveId] = useState(null);
const anchorRef = useRef(null);
const handleToggle = id => {
setActiveId(id);
};
const handleClose = event => {
setActiveId(null);
};
return (
<React.Fragment>
<div className={`nav-menu--admin ${classes.root}`}>
{navAdminList.map(e => (
<div key={e.id}>
<div onClick={stopPropagation}>
<Button
aria-owns={activeId === e.id ? 'menu-list-grow' : undefined}
aria-haspopup="true"
onClick={() => handleToggle(e.id)}
>
{e.name}
</Button>
</div>
{activeId === e.id && (
<ToggleMenuItems
id={e.id}
activeId={activeId}
handleClose={handleClose}
anchorRef={anchorRef}
items={navAdminItems[e.id]}
/>
)}
</div>
))}
</div>
</React.Fragment>
);
};
export default withStyles(styles)(ToggleMenuList);
I've put stopPropagation handler outside since it does not depend on any internal variable.
Related
My modal causes a crash whenever I close it. The modal contains picture details and is displaying them.
The error message I am getting is:
"TypeError: Cannot read properties of undefined (reading 'name')"
Although the above prop worked fine while the modal was open. A thing I noted is that whenever the modal contains only the below paragraph with the id, it works fine and closes without any issues.
<p>{modalContent.id}</p>
Where the FavoritesModal gets its props
const FavoritesSidebar = () => {
const [modalOpen, setModalOpen] = useState(false)
const [modalContent, setModalContent] = useState(null)
const favoriteList = useSelector((state) => state.favoriteList)
const toggleModal = (arg) => {
setModalOpen(!modalOpen)
setModalContent(arg)
}
return (
<Container className="favorites-sidebar">
{/* {modalContent ? <p>yes</p> : <p>no</p>} */}
{favoriteList.length > 0 ? <h4>Your favorited pictures</h4> : null}
{favoriteList.length > 0 &&
favoriteList.map((item) => (
<Row key={item.id} className="favorite-item">
<Col onClick={() => toggleModal(item)}>
</Col>
</Row>
))}
{modalContent ? (
<FavoritesModal
modalOpen={modalOpen}
modalContent={modalContent}
toggleModal={toggleModal}
/>
) : null}
</Container>
)
}
export default FavoritesSidebar
FavoritesModal
const FavoritesModal = (props) => {
const { modalOpen, modalContent, toggleModal } = props
const dispatch = useDispatch()
if (modalContent)
return (
<Modal
isOpen={modalOpen}
toggle={toggleModal}
centered
fullscreen=""
size="xl"
>
<ModalHeader>
<p>
{modalContent.description !== null
? `${modalContent.description} by ${modalContent.user.name}`
: `Taken by: ${modalContent.user.name}`}
</p>
<p>{modalContent.id}</p>
</ModalHeader>
<ModalBody>
<img src={modalContent.urls.regular} alt={modalContent.description} />
</ModalBody>
<ModalFooter>
<Button
color="danger"
onClick={() => dispatch(removeFromFavorites(modalContent.id))}
>
Remove from favorites
</Button>
</ModalFooter>
</Modal>
)
else return null
}
export default FavoritesModal
Rewriting the toggleModal functioned solved the issue
const toggleModal = (arg) => {
if (modalOpen) {
setModalOpen(!modalOpen)
setModalContent(null)
} else {
setModalOpen(!modalOpen)
setModalContent(arg)
}
}
I have a button inside a cart and I want give it an animation whenever the cart is opened. To target it I used useRef and tried to log in the console the result:
const checkoutBtnRef = useRef("null");
useEffect(() => {
console.log(checkoutBtnRef.current);
}, [cartOpen]);
The problem is that current is null when I open the cart and returns the actual button only when I close the cart, I expected it to be the opposite.
Any idea?
FULL COMPONENT CODE
export default function TopNav() {
const classes = useStyles();
const [cartOpen, setCartOpen] = useState(false);
const checkoutBtnRef = useRef("null");
useEffect(() => {
console.log(checkoutBtnRef.current);
}, [cartOpen]);
return (
<>
<Container maxWidth="xl">
<div className={classes.root}>
<CardMedia
image="https://www.example.com"
className={classes.media}
/>
<SearchBar placeholder="Search" className={classes.searchBar} />
<div className={classes.iconsContainer}>
<PersonIcon className={classes.icon} />
<FavoriteBorderIcon className={classes.icon} />
<Badge
invisible={false}
variant="dot"
color="error"
onClick={() => setCartOpen(true)}
>
<ShoppingBasketIcon className={classes.icon} />
</Badge>
</div>
</div>
</Container>
<Drawer
classes={{
paper: classes.cart,
}}
anchor="right"
open={cartOpen}
transitionDuration={{ enter: 500, exit: 200 }}
onClose={() => setCartOpen(false)}
>
<div className={classes.topCartContent}>
<Typography variant="h5">Cart</Typography>
<CloseIcon
className={classes.closeIcon}
onClick={() => setCartOpen(false)}
/>
</div>
<Divider />
<List>
<CartItem />
</List>
<Button
classes={{
root: classes.checkoutBtn,
}}
variant="outlined"
onClick={() => setCartOpen(false)}
ref={checkoutBtnRef}
>
checkout
</Button>
</Drawer>
</>
);
}
EDIT :
your drawer has delay and your button didn't render yet so u cant see button on cartOpen change event and ...
so use this for storing and call your functions instead of useEffect.
const checkoutBtnRef = (ref) => {
console.log(ref);
if (ref){
// do ur staff here
}
};
If u need useEffect u can do it like this :
const [btnRef, setBtnRef] = useState(null);
const checkoutBtnRef = (ref) => {
if (ref) setBtnRef(ref);
};
useEffect(() => {
console.log(btnRef);
}, [btnRef]);
Old answer:
const App = ()=>{
const [cart,setCart]= React.useState(false);
const [btnRef, setBtnRef] = React.useState(null);
const checkoutBtnRef = (ref) => {
if (ref) setBtnRef(ref);
else setBtnRef(null)
};
React.useEffect(() => {
console.log(btnRef);
}, [btnRef]);
const cartHandler = () => {
setTimeout(()=>{
setCart((prev) => !prev)
},1000);
};
return (
<React.Fragment>
<button onClick={cartHandler}>
Cart
</button>
{cart && <button ref={checkoutBtnRef}>Check Out</button>}
</React.Fragment>
)
}
ReactDOM.render(
<App />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>
In React I created a component that holds a local state of popup. With a little help from onClick handler I change the local state to make the popup show up. The Popup component in turn contains confirm button. I would like to call a function deleteItem ONLY after the confirm button is clicked. But I don't get how to do it. In the code below the item gets deleted right after the popup shows up but it has to be deleted only if I press the button in the Popup component. If I understand correctely, the state of the components changes when the popup shows up and I have to get it khow to the MainComponent and only in this case the function deleteItem will be called.
import {deleteItem} from './item-reducer';
const MainComponent = ({items}) => {
const [visiblePopup, setVisiblePopup] = useState(false);
return(
{items.map(item => <li key={item.id}></li>
<img onClick={() => {
setVisiblePopup(true);
deleteList(item.id) // I have to call this function after the button
in the Popup component is pressed
}}
/>
)}
<Popup setVisiblePopup={setVisiblePopup}
)
}
Popup.jsx
<div onClick={() => setVisiblePopup(false)} />
Confirm
</div>
What I have to accomplish 1) I click img and popup shows up 2) I press
'Confirm' 3) function deleteItem is invoked 4)popup dissapeares.
If I understand you correctly this is what your looking for ?
const MainComponent = ({items}) => {
const [modalState, setModalState] = useState({
display: false,
deleteItemId: undefined
});
const modalCallback = useCallback((deleteItemId)=>{
deleteItem(deleteItemId)
setModalState({ display: false, deleteItemId: undefined })
},[])
return(
<Fragment>
{
items.map(item => (
<Fragment>
<li key={item.id}></li>
<img onClick={() => setModalState({ display: true, deleteItemId: item.id })} />
</Fragment>
))
}
<PopupModal
visible={modalState.display}
deleteItemId={modalState.deleteItemId}
callback={modalCallback}
/>
</Fragment>
)
}
const PopupModal = ({ visible, deleteItemId, callback }) => {
return (visible ? <div onClick={ () => callback(deleteItemId)}>Confirm</div> : null)
}
--- OR ----
const MainComponent = ({items}) => {
const [modalState, setModalState] = useState({
display: false,
deleteItemId: undefined
});
return(
<Fragment>
{
items.map(item => (
<Fragment>
<li key={item.id}></li>
<img onClick={() => setModalState({ display: true, deleteItemId: item.id })} />
</Fragment>
))
}
modalState.display ? <div onClick={() => [deleteList(modalState.deleteItemId), setModalState({display: false, deleteItemId: undefined}) ]}>Confirm</div> : null
</Fragment>
)
}
I'm trying to add a white outline via css box-shadow, but whenever I click on any of the buttons, they all get the outline instead of just the actual button I clicked.
Is there a way so only the button component I click on gets the outline and then toggles off if I click it again?
Here is my current code:
const [selectState, setSelectState] = useState(false);
const Button = ({ selected, text }) => {
function handleClick() {
setSelectState(true);
}
return (
<span
onClick={handleClick}
className={`btn-style ${selected ? "selected" : ""}`}
>
{text}
</span>
);
};
export default function Hello() {
return (
<Button selected={selectState} text='Blue'/>
<Button selected={selectState} text='Red'/>
<Button selected={selectState} text='Green'/>
);
}
.selected css:
.selected {
box-shadow: rgb(17 206 101) 0px 0px 0px 2px inset !important;
}
If you want to track the selected state of individual elements, you'd need to handle the onClick method and make corresponding state change in parent element.
const Button = ({ selected, text, onClick }) => {
return (
<span
onClick={onClick}
className={`btn-style ${selected ? "selected" : ""}`}
>
{text}
</span>
);
};
export default function Hello() {
const [selectState, setSelectState] = React.useState(0);
return (
<React.Fragment>
<Button
onClick={() => setSelectState(1)}
selected={selectState === 1}
text="Blue"
/>
<Button
onClick={() => setSelectState(2)}
selected={selectState === 2}
text="Red"
/>
<Button
onClick={() => setSelectState(3)}
selected={selectState === 3}
text="Green"
/>
</React.Fragment>
);
}
You can have the click handler tell the parent component to save the clicked button index in state, and pass that state down to determine whether the selected class is needed:
const Button = ({ selected, text, onClick }) => {
return (
<span
onClick={onClick}
className={`btn-style ${selected ? "selected" : ""}`}
>
{text}
</span>
);
};
export default function Hello({ texts }) {
const [selectedIndex, setSelectedIndex] = useState(-1);
return (<>
{
texts.map((text, i) => <Button selected={i === selectedIndex} text={text} onClick={() => setSelectedIndex(i)}} />)
}
</>);
}
ReactDOM.render(<Hello texts={['Blue', 'Red', 'Green']} />, document.querySelector('.react'));
I am new to material-ui and React and I have a requirement to create multiple menus dynamically in a loop. Please find the code snippet as:
state = {
anchorEl: null,
};
handleClick = event => {
this.setState({ anchorEl: event.currentTarget });
};
handleClose = () => {
this.setState({ anchorEl: null });
};
render() {
const { anchorEl } = this.state;
let items = _.map(results, (item, index) => {
return (
<ListItem
key={item.ID}
divider
>
<ListItemSecondaryAction>
<IconButton
aria-label="More"
aria-owns={anchorEl ? 'long-menu' : null}
aria-haspopup="true"
onClick={this.handleClick}
>
<MoreVertIcon />
</IconButton>
<Menu
id="long-menu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={this.handleClose}
PaperProps={{
style: {
maxHeight: 200,
width: 200,
},
}}
>
<MenuItem>
<IconButton onClick={() => this.props.delete(item.ID)} >
Delete entry<DeleteIcon />
</IconButton>
</MenuItem>
</Menu>
<ListItemSecondaryAction>
</ListItem>
)
})
return (
<Fragment>
<List>
{items}
</List>
</Fragment>
)
}
Now, with the above code, the menus work fine and the UI is good. But whenever I try to delete an entry by clicking on Delete icon inside the menu, always the last entry is deleted i.e. item.ID passes the value of the last element and the last entry is deleted.
Is there a way I can create unique menuitems for each entry and manage the state in such a way which makes sure that the correct item is deleted and not the last one always.
Note: 'results' is any list loaded dynamically and 'delete' function implements the functionality to delete the corresponding entry
Thanks in advance.
I would suggest use another child component for render your list item. In your current example you only one anchorEl, which means wherever you click, always one menu open and take action of that, which is last one. If you have child component for menu item, each component will have there own state and work for that item only.
Example
class Main extends Component {
render() {
let items = _.map(results, (item, index) => {
return (
<MenuItemComponent key={item.ID} item={item} onClick={this.handleClick} onDelete={(item) => this.props.delete(item.ID)} />
)
})
return (
<Fragment>
<List>
{items}
</List>
</Fragment>
)
}
}
class MenuItemComponent extends Component {
state = {
anchorEl: null,
};
handleClick = event => {
this.setState({ anchorEl: event.currentTarget });
};
handleClose = () => {
this.setState({ anchorEl: null });
};
render() {
const { item } = this.props;
const { anchorEl } = this.state;
return (
<ListItem
divider
>
<ListItemSecondaryAction>
<IconButton
aria-label="More"
aria-owns={anchorEl ? 'long-menu' : null}
aria-haspopup="true"
onClick={this.handleClick.bind(this)}
>
<MoreVertIcon />
</IconButton>
<Menu
id="long-menu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={this.handleClose.bind(this)}
PaperProps={{
style: {
maxHeight: 200,
width: 200,
},
}}
>
<MenuItem>
<IconButton onClick={() => this.props.onDelete(item)} >
Delete entry<DeleteIcon />
</IconButton>
</MenuItem>
</Menu>
</ListItemSecondaryAction>
</ListItem>
)
}
}
Here's a working example https://codesandbox.io/s/nn555l48xm
import * as React from "react";
import {
Menu,
MenuItem,
IconButton
} from "#material-ui/core";
import MoreVertIcon from "#material-ui/icons/MoreVert";
export default function Demo() {
const [openElem, setOpenElem] = React.useState(null);
const [anchorEl, setAnchorEl] = React.useState(null);
const handleClick = (elem) => (event) => {
setAnchorEl(event.currentTarget);
setOpenElem(elem);
};
const handleClose = () => {
setAnchorEl(null);
setOpenElem(null);
};
let arr = [0, 1, 2];
let body = arr.map((elem) => {
return (
<div key={elem}>
<IconButton
aria-label="more"
aria-controls={"long-menu" + elem}
aria-haspopup="true"
onClick={handleClick(elem)}
>
<MoreVertIcon />
</IconButton>
<Menu
id={"long-menu" + elem}
anchorEl={anchorEl}
keepMounted
open={openElem === elem}
onClose={handleClose}
>
<MenuItem
onClick={(e) => {
handleClose();
}}
>
{elem}
</MenuItem>
</Menu>
</div>
);
});
return <div>{body}</div>;
}