I have a react Sidebar component that appear when i toggle a state. I added a fadeInLeft animation class with Animate.style so i thought it would be nice to have a fadeOutLeft effect too whenever i set the state to false condition. All went smooth for the fadeInLeft class effect but when i toggle the state to be false the component just disappear without the fadeOutEffect class that i added, how is that happened?
video of it: https://www.loom.com/share/ff26dc014bbb43649419b1ecd6bae01e
Sidebar Component
const Sidebar = ({ isDisplayed }) => {
if(isDisplayed === true) {
return(
<Box
position="absolute"
top={0}
left={0}
width="155px"
height="100vh"
bgColor="#1e1e1e"
className={`${Styles.SidebarBox} ${isDisplayed ?'animate__animated animate__fadeInLeft' : 'animate__animated animate__fadeOutLeft'}`}
>
<Text color="#fff">Test</Text>
</Box>
)
}
}
Main Component
const Navbar = () => {
const [display, setDisplay] = useState(false)
const newRef = useRef(null)
const handleClickOutside = (e) => {
if(newRef.current && !newRef.current.contains(e.target)){
setDisplay(false)
}
}
useEffect(() => {
document.addEventListener('click', handleClickOutside, true);
return () => {
document.removeEventListener('click', handleClickOutside, true);
}
},[])
return (
<Flex
justifyContent="space-between"
alignItems="center"
w="100%"
h="110px"
px="150px"
py="15px"
bgColor="#1e1e1e"
boxShadow="md"
ref={newRef}
>
<Sidebar isDisplayed={display} />
How can i have that fadeOutLeft effect whenever i change my display state to false?
I'm midway through creating shopping cart of some website. I want the shopping cart to close when clicked outside of it. I managed to get it working, but for some reason it still closes when clicked on the div of the cart. I managed to find the problem. But i need help solving it. The logic behind it is that when website is clicked I get the event using listener and try to find if ref contains the click. Here's the code:
function CartItem(props) {
// handling outside clicks
const [open, setOpen] = useState(false);
const cartRef = useRef(null);
useEffect(() => {
document.addEventListener("click", handleClickOutside, true)
}, []);
const handleClickOutside = (e) => {
if(!cartRef.current.contains(e.target)) {
setOpen(false);
}
}
return (
<div className='nav-item' ref={cartRef} onClick={() => setOpen(!open)} >
<div className='navItemImage'>
<img src={CartImage} alt="Shopping Cart Icon" title="Shopping Cart Icon"/>
</div>
{open && props.children}
</div>
);
}
Now the problem is that when click happens on the opened nav-item DIV (which is the shopping cart DIV) props.children isn't rendered, thus .contains is false and changes the state. What can I do to solve this problem. Thanks!
Here is the full code for context:
import React, { useEffect, useState, useRef } from 'react';
import "./Cart.css";
import CartImage from "../../assets/Cart.svg";
import { findCurrencySymbol } from "../../constructors/functions"
import { findAmount } from "../../constructors/functions"
function Cart(props) {
return(
<CartItem >
<CartMenu cart={props.cart} currencyLabel={props.currencyLabel}/>
</CartItem>
);
}
function CartItem(props) {
// handling outside clicks
const [open, setOpen] = useState(false);
const cartRef = useRef(null);
useEffect(() => {
document.addEventListener("click", handleClickOutside, true)
}, []);
const handleClickOutside = (e) => {
console.log(cartRef.current);
console.log(e.target);
if(!cartRef.current.contains(e.target)) {
setOpen(false);
}
}
return (
<div className='nav-item' ref={cartRef} onClick={() => setOpen(!open)} >
<div className='navItemImage'>
<img src={CartImage} alt="Shopping Cart Icon" title="Shopping Cart Icon"/>
</div>
{open && props.children}
</div>
);
}
function CartMenu(props) {
function CartMenuItem(props) {
return (
<div className='cart-item'>
<h1 className='cart-header'>My bag, <span>{props.cart.length} items</span></h1>
<ul className='cart-products'>
{
props.cart.map((product) => {
return (
<li className='cart-product' key={product}>
<div className='cart-left'>
<h2 className='cart-product-title'>{product.brand}</h2>
<h2 className='cart-product-title'>{product.name}</h2>
<h2 className='cart-product-price'>{findCurrencySymbol(product.prices, props.currencyLabel)} {findAmount(product.prices, props.currencyLabel)}</h2>
{/* {
product.category.map((item) => {
return (
console.log(item)
);
})
} */}
{/* {console.log(product)} */}
</div>
<div className='cart-middle'>
<div className='quantity-increase' onClick={() => increaseQuantity()}>+</div>
<h2>{product.quantity}</h2>
<div className='quantity-decrease' onClick={() => decreaseQuantity()}>-</div>
</div>
<div className='cart-right'>
<img src={product.image} />
</div>
</li>
);
})
}
</ul>
</div>
);
}
return (
<div className='dropdown-cart'>
{props.cart.map((item) => {
return (
<CartMenuItem cart={props.cart} key={item} currencyLabel={props.currencyLabel}/>
);
})}
</div>
);
}
function increaseQuantity() {
console.log(+1);
}
function decreaseQuantity() {
console.log(-1);
}
export default Cart;
Here is the console logged cartRef and e.target:
I tried using forwarding refs but got similar results
I'm trying to simulate a scroll down. After that, the state of isVisible should change to true and the Button component should be displayed.
That's working correctly, but I can't simulate it in the test.
Main Component Below
const ScrollToTop = (props: ScrollToTopProps) => {
const {
message = 'Back To Top',
visibilityHeight = 300,
containerClassName = '',
...restProps
} = props;
const [isVisible, setIsVisible] = useState(false);
const classNames = useClassNameWithPrefixCls();
const toggleVisibility = () => {
if (window.pageYOffset > visibilityHeight) {
setIsVisible(true);
} else {
setIsVisible(false);
}
};
const scrollToTopOfPage = () => {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
};
useEffect(() => {
window.addEventListener('scroll', toggleVisibility);
return () => {
window.removeEventListener('scroll', toggleVisibility);
};
}, []);
return (
<>
{isVisible && (
<div
className={`${classNames('scroll-to-top')} ${containerClassName}`}
onClick={scrollToTopOfPage}
>
<Button {...restProps}>{message}</Button>
</div>
)}
</>
);
};
export default ScrollToTop;
Test below
import React from 'react';
import { render, screen, fireEvent } from '#testing-library/react';
import ScrollToTop from '../ScrollToTop';
describe('ScrollToTop', () => {
test('Renders ScrollToTop with aria-label', async () => {
render(<ScrollToTop aria-label="Go to the top" />);
fireEvent.scroll(window, { target: { scrollY: 1000 } });
const testy = await screen.findByLabelText('Go to the top');
expect(testy).toBeDefined();
});
});
Error Below
TestingLibraryElementError: Unable to find a label with the text of: Go to the top
<body
class=""
style=""
>
<div />
</body>
<body
class=""
style=""
>
<div />
</body>Error: Unable to find a label with the text of: Go to the top
<body
class=""
style=""
>
<div />
</body>
7 | render(<ScrollToTop aria-label="Go to the top" />);
8 | fireEvent.scroll(window, { target: { scrollY: 1000 } });
> 9 | const testy = await screen.findByLabelText('Go to the top');
| ^
10 | expect(testy).toBeDefined();
11 | });
12 | });
As you can see, the file is empty even using async/await.
note: Button is a component that just renders a
It's working as expected. I didn't upload that code because it could be confusing.
Thanks
Hello I need to setup a progress bar. So when I press on the button progress bar shows, the button and the content inside of it dispears proggress bar starts to go 0 100 and as well shows some text above, and
when it reaches the 100% progress bar disppears and text above, but after that new text shows. Thanks in Advance
import React,{useEffect, useState} from 'react'
import LinearProgress from '#material-ui/core/LinearProgress'
const useStyles = makeStyles({
root: {
width: '100%',
},
});
const Content =(props)=> {
const classes = useStyles();
const[hideContent, setHideContent]= React.useState(false)
const [progress, setProgress] = React.useState(10);
function handleClick12 ()
{setHideEset(true) }
useEffect(() => {
const timer = setInterval(() => {
setProgress((prevProgress) => (prevProgress >= 100 ? 10 : prevProgress + 10));
}, 800);
return () => {
clearInterval(timer);
};
}, []);
return (
{!hideContent &&
<div className='esetNod__info'>
<h3>Hello</h3>
<Button onClick={handleClick12} className='fix__button'variant='outlined'></Button>
<div >
<LinearProgress value={progress} />
</div>
</div>
}
</div>
)
}
export default Content;
I tried to write something for you:
import React, { useState } from "react";
import LinearProgress from "#material-ui/core/LinearProgress";
const Content = () => {
const [isLoading, setIsLoading] = useState(false);
const [hasLoaded, setHasLoaded] = useState(false);
const [progress, setProgress] = useState(0);
const handleClick = () => {
setIsLoading(true);
const interval = setInterval(() => {
setProgress((prevProgress) => {
const next = prevProgress + 10;
if (next === 100) {
clearInterval(interval);
setIsLoading(false);
setHasLoaded(true);
}
return next;
});
}, 800);
};
if (!isLoading && !hasLoaded) {
return (
<div className="esetNod__info">
<h3>Pre load content</h3>
<button onClick={handleClick} className="fix__button">
Load Content
</button>
</div>
);
} else if (isLoading && !hasLoaded) {
return (
<diV>
<h3>Loading content</h3>
<LinearProgress value={progress} />
</diV>
);
} else {
return (
<div>
<h3>Post load content</h3>
</div>
);
}
};
export default Content;
I have created a basic modal using react without any library and it works perfectly, now when I click outside of the modal, I want to close the modal.
here is the CodeSandbox live preview
my index.js:
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
class App extends React.Component {
constructor() {
super();
this.state = {
showModal: false
};
}
handleClick = () => {
this.setState(prevState => ({
showModal: !prevState.showModal
}));
};
render() {
return (
<>
<button onClick={this.handleClick}>Open Modal</button>
{this.state.showModal && (
<div className="modal">
I'm a modal!
<button onClick={() => this.handleClick()}>close modal</button>
</div>
)}
</>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
The easiest way to get this is to call the closeModal function in the wrapper and stop propagation in the actual modal
For example
<ModalWrapper onClick={closeModal} >
<InnerModal onClick={e => e.stopPropagation()} />
</ModalWrapper>
Without using ref, it would be a little tricky
Watch this CodeSandBox
Or
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
class App extends React.Component {
constructor() {
super();
this.state = {
showModal: false
};
}
handleClick = () => {
if (!this.state.showModal) {
document.addEventListener("click", this.handleOutsideClick, false);
} else {
document.removeEventListener("click", this.handleOutsideClick, false);
}
this.setState(prevState => ({
showModal: !prevState.showModal
}));
};
handleOutsideClick = e => {
if (!this.node.contains(e.target)) this.handleClick();
};
render() {
return (
<div
ref={node => {
this.node = node;
}}
>
<button onClick={this.handleClick}>Open Modal</button>
{this.state.showModal && (
<div className="modal">
I'm a modal!
<button onClick={() => this.handleClick()}>close modal</button>
</div>
)}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
You can do it by creating a div for the modal backdrop which sits adjacent to the modal body. Make it cover the whole screen using position absolute and 100% height and width values.
That way the modal body is sitting over the backdrop. If you click on the modal body nothing happens because the backdrop is not receiving the click event. But if you click on the backdrop, you can handle the click event and close the modal.
The key thing is that the modal backdrop does not wrap the modal body but sits next to it. If it wraps the body then any click on the backdrop or the body will close the modal.
const {useState} = React;
const Modal = () => {
const [showModal,setShowModal] = useState(false)
return (
<React.Fragment>
<button onClick={ () => setShowModal(true) }>Open Modal</button>
{ showModal && (
<React.Fragment>
<div className='modal-backdrop' onClick={() => setShowModal(false)}></div>
<div className="modal">
<div>I'm a modal!</div>
<button onClick={() => setShowModal(false)}>close modal</button>
</div>
</React.Fragment>
)}
</React.Fragment>
);
}
ReactDOM.render(
<Modal />,
document.getElementById("react")
);
.modal-backdrop {
position: absolute;
top: 0;
left: 0;
background: #252424cc;
height: 100%;
width: 100vw;
}
.modal {
position: relative;
width: 70%;
background-color: white;
border-radius: 10px;
padding: 20px;
margin:20px auto;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Please see the attached Codesandbox for a working example.
You were almost there. Firstly, you need to do a callback function in your handleClick() that will add a closeMenu method to the document:
handleClick = event => {
event.preventDefault();
this.setState({ showModal: true }, () => {
document.addEventListener("click", this.closeMenu);
});
};
And then toggle the state inside closeMenu():
closeMenu = () => {
this.setState({ menuOpen: false }, () => {
document.removeEventListener('click', this.closeMenu);
});
}
Any time you click outside of the component, then it'll close it. :)
This worked for me:
const [showModal, setShowModal] = React.useState(false)
React.useEffect(() => {
document.body.addEventListener('click', () => {
setShowModal(false)
})
})
return <>
<Modal
style={{ display: showModal ? 'block' : 'none'}}
onClick={(e) => e.stopPropagation()}
/>
<button onClick={(e) => {
e.stopPropagation()
setShowModal(true)
}}>Show Modal</button>
</>
This works for me:
Need to use e.stopPropagation to prevent loop
handleClick = e => {
if (this.state.showModal) {
this.closeModal();
return;
}
this.setState({ showModal: true });
e.stopPropagation();
document.addEventListener("click", this.closeModal);
};
then:
closeModal = () => {
this.setState({ showModal: false });
document.removeEventListener("click", this.closeModal);
};
Hope will help
This is how I solved it:
BTW, I"m a junior dev, so check it, GL.
In index.html:
<div id="root"></div>
<div id="modal-root"></div>
In index.js:
ReactDOM.render(
<React.StrictMode>
<ModalBase />
</React.StrictMode>,
document.getElementById("modal-root")
);
In the App.js:
const [showModal, setShowModal] = useState(false);
{showModal && (
<ModalBase setShowModal={setShowModal}>
{/*Your modal goes here*/}
<YourModal setShowModal={setShowModal} />
</ModalBase>
In the modal container:
import React, { useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom";
const modalRoot: HTMLElement | null = document.getElementById("modal-root");
const Modal: React.FC<{
children: React.ReactNode;
setShowModal: React.Dispatch<boolean>;
}> = ({ children, setShowModal }) => {
const [el] = useState(document.createElement("div"));
const outClick = useRef(el);
useEffect(() => {
const handleOutsideClick = (
e: React.MouseEvent<HTMLDivElement, MouseEvent> | MouseEvent
) => {
const { current } = outClick;
console.log(current.childNodes[0], e.target);
if (current.childNodes[0] === e.target) {
setShowModal(false);
}
};
if (modalRoot) {
modalRoot.appendChild(el);
outClick.current?.addEventListener(
"click",
(e) => handleOutsideClick(e),
false
);
}
return () => {
if (modalRoot) {
modalRoot.removeChild(el);
el.removeEventListener("click", (e) => handleOutsideClick(e), false);
}
};
}, [el, setShowModal]);
return ReactDOM.createPortal(children, el);
};
export default Modal;
Use the following onClick method,
<div className='modal-backdrop' onClick={(e) => {
if (e.target.className === 'modal-backdrop') {
setShowModal(false)
}
}}></div>
<div className="modal">
<div>I'm a modal!</div>
<button onClick={() => setShowModal(false)}>close modal</button>
</div>
</div>
.modal-backdrop {
position: absolute;
top: 0;
left: 0;
background: #252424cc;
height: 100%;
width: 100vw;
}
You can check the event.target.className if it's contain the parent class you can close the Modal as below, in case you clicked inside the popup div it will not closed:
handleClick = () => {
if (e.target.className === "PARENT_CLASS") {
this.setState(prevState => ({
showModal: false
}));
}
// You should use e.stopPropagation to prevent looping
e.stopPropagation();
};
I will explain with functional components:
first create ref to get a reference to the modal element
import { useEffect, useState, useRef } from "react";
const [isModalOpen,setIsModalOpen]=useState(false)
const modalEl = useRef();
<div className="modal" ref={modalEl} >
I'm a modal!
<button onClick={() => this.handleClick()}>close modal</button>
</div>
second in useEffect create an event handler to detect an event outside the modal element. For this we need to implement capture phase on an element. (explained here: What is event bubbling and capturing? ). Basically, we are going to register an event handler so that when the browser detects any event, browser will start to look for the event handlers from the top parent HTML element and if it finds it, it will call it.
useEffect(() => {
const handler = (event) => {
if (!modalEl.current) {
return;
}
// if click was not inside of the element. "!" means not
// in other words, if click is outside the modal element
if (!modalEl.current.contains(event.target)) {
setIsModalOpen(false);
}
};
// the key is using the `true` option
// `true` will enable the `capture` phase of event handling by browser
document.addEventListener("click", handler, true);
return () => {
document.removeEventListener("click", handler);
};
}, []);