My custom modal doesn't close because of react's useState hook - javascript

My custom modal window opens up but doesn't close when I click on the darkened area. I investigated a bit and found out that the setActive function in the modal component doesn't set the active to false for some reason. How can I fix this?
The modal file
import React from 'react'
import './style.css'
const Modal = ({active, setActive, children}) => {
return (
<div className={active?'modal_main active':'modal_main'} onClick={()=>{setActive(false)}}>
<div className={active?'modal_content active':'modal_content'} onClick={e=>e.stopPropagation()}>
{children}
</div>
</div>
)
}
export default Modal
Where I use the modal window
import React, { useState } from 'react'
import Modal from '../../modal'
import './style.css'
function TagItem(props) {
const [tagActive, setTagActive] = useState(false);
return (
<div className='tag-item' onClick={()=>setTagActive(true)}>
{props.tag}
<Modal active = {tagActive} setActive = {setTagActive}>
<div >{props.tag}</div>
</Modal>
</div>
)
}
export default TagItem
modal's css
.modal_main{
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.4);
position: fixed;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: 0.5s;
z-index:1;
}
.modal_main.active{
opacity: 1;
pointer-events: all;
}
.modal_content{
padding: 20px;
border-radius: 12px;
background-color: white;
height: fit-content;
width: fit-content;
transform: scale(0.5);
transition: 0.4s all;
}
.modal_content.active{
transform: scale(1);
}
tag-item's css
.tag-item{
border: 1px limegreen solid;
border-radius: 8px;
background-color: rgb(178, 246, 119);
width: fit-content;
padding: 2px;
margin: 2px;
cursor: default;
}
.tag-item:hover{
cursor: default;
background-color: rgb(1, 152, 1) ;
}

Issue
The click event from the modals's outer div elementis triggering the state update, but it's also propagated out of theModalcomponent to theTagItemcomponent'sdivelement and this enqueues atagActivestate update totrue`. The state update to close the modal is overwritten.
Solution
Stop the propagation of the outer div element's click event.
const Modal = ({ active, setActive, children }) => {
return (
<div
className={active ? "modal_main active" : "modal_main"}
onClick={(e) => {
e.stopPropagation(); // <-- stop propagation to parent component
setActive(false);
}}
>
<div
className={active ? "modal_content active" : "modal_content"}
onClick={(e) => {
e.stopPropagation();
}}
>
{children}
</div>
</div>
);
};

Related

How to close the floating menu if you click outside it with React

I created a Header component containing the site navbar which through useState and aria-attribute in the css shows or hides the menu in the mobile version of the site.
I used onFocus to manage the automatic closing of the side menu but it doesn't work if you click outside the menu ... it only works if you click on a menu item.
I also tried with onBlur but still it doesn't work.
How do I make the menu close if I click outside its panel?
Header component:
import { useRef, useState } from 'react';
import { Link } from 'react-router-dom';
import logo from '../imgs/logo.webp';
function Header() {
const refNavBar = useRef(),
[isOpen, setIsOpen] = useState(false),
handleToggle = () => setIsOpen(!isOpen);
return (
<header>
<div className="container g-2 pbk-1">
<Link to="/">
<img src={logo} alt="" />
</Link>
<button
className="nav-toggle"
aria-controls={refNavBar}
aria-expanded={isOpen}
onClick={handleToggle}
>
<div className="bar1" />
<div className="bar2" />
<div className="bar3" />
</button>
<nav
id="navbar"
ref={refNavBar}
data-visible={isOpen}
onFocus={handleToggle}
>
<ul className="flex g-2">
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="/gallery">Gallery</Link></li>
<li><Link to="/contact">Contact</Link></li>
</ul>
</nav>
</div>
</header>
);
}
export default Header;
style:
.flex {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}
.g-2 { gap: 2rem; }
.pbk-1 { padding-block: 1rem; }
header {
position: fixed;
width: 100%;
background-color: var(--bg);
}
header .container {
display: grid;
justify-items: end;
align-items: center;
grid-template-columns: 10rem 1fr;
}
button.nav-toggle {
display: none;
}
header nav ul li > a {
color: var(--white);
text-decoration: none;
}
header nav ul li > a:hover {
opacity: .8;
}
#media (max-width: 44em) {
.flex {
flex-direction: column;
}
button.nav-toggle {
display: block;
position: absolute;
right: 1rem;
width: 2.5rem;
border-radius: 0;
padding: 0;
background: transparent;
z-index: 9999;
}
button.nav-toggle .bar1,
button.nav-toggle .bar2,
button.nav-toggle .bar3 {
width: 100%;
height: .12rem;
margin-block: .65rem;
background-color: var(--white);
transition: var(--ease-in-out);
}
button.nav-toggle[aria-expanded="true"] .bar1 {
transform: rotate(-45deg) translate(-50%, -50%);
}
button.nav-toggle[aria-expanded="true"] .bar2 {
opacity: 0;
}
button.nav-toggle[aria-expanded="true"] .bar3 {
transform: rotate(45deg) translate(-50%, -50%);
}
nav#navbar {
position: fixed;
inset: 0 0 0 28%;
padding: min(20rem, 15vh) 2rem;
background: var(--bg);
z-index: 9998;
transform: translateX(100%);
transition: transform var(--ease-in-out);
}
nav#navbar[data-visible="true"] {
transform: translateX(0);
}
What am I doing wrong?
A thousand thanks
Detect click outside component example, can this work at your example?
import React, { useRef, useEffect } from "react";
/**
* Hook that alerts clicks outside of the passed ref
*/
function useOutsideAlerter(ref) {
useEffect(() => {
/**
* Alert if clicked on outside of element
*/
function handleClickOutside(event) {
if (ref.current && !ref.current.contains(event.target)) {
alert("You clicked outside of me!");
}
}
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref]);
}
/**
* Component that alerts if you click outside of it
*/
export default function OutsideAlerter(props) {
const wrapperRef = useRef(null);
useOutsideAlerter(wrapperRef);
return <div ref={wrapperRef}>{props.children}</div>;
}

When I resize my window below 700px, the dropdown animation transition goes off; how do I block this animation from happening upon resizing window?

I have a navigation bar that has a logo and a burgerbar that changes from a burger bar to an "x" when opened.
The problem is that the dropdown should go down when you click on the burger and go back up when you press the "x". This functions smoothly and perfectly so far, but the problem I noticed is that when I resize the browser window from >700px to <700px, the dropdown menu animation going up occurs.
import React, { useState, useRef, useEffect } from "react";
import { BurgerBar } from "../../styles/Navbar.style";
import Navlinks from "./Navlinks";
function Burger() {
const [open, setOpen] = useState(false);
const [width, setWidth] = useState(window.innerWidth);
const prevWidth = useRef();
useEffect(() => {
prevWidth.current = width;
}, [width]);
function checkResize() {
setWidth(window.innerWidth);
if (prevWidth.current <= 700 && width > 700) {
setOpen(false);
};
};
window.addEventListener("resize", checkResize);
return (
<>
<BurgerBar open={open} onClick={() => {setOpen(!open);}}>
<span />
<span />
<span />
</BurgerBar>
<Navlinks open={open} />
</>
)
};
export default Burger;
This is my styled-components file with the relevant styles:
export const BurgerBar = styled.span`
z-index: 1;
width: 2rem;
position: fixed;
display: flex;
justify-content: space-around;
flex-flow: column;
top: ${({open}) => open ? 22 : 24}px;
height: ${({open}) => open ? 2.3 : 2}rem;
right: ${({open}) => open ? 17 : 20}px;
span {
width: 2rem;
height: 0.25rem;
background-color: rgb(46, 203, 64);
border-radius: 10px;
transform-origin: 1px;
&:nth-child(1) {
transform: ${({open}) => open ? 'rotate(45deg)' : 'rotate(0)'};
width: ${({open}) => open ? 2.3 : 2}rem;
}
&:nth-child(2) {
transform: ${({open}) => open ? 'translateX(100%)' : 'translateX(0)'};
opacity: ${({open}) => open ? 0 : 1};
}
&:nth-child(3) {
transform: ${({open}) => open ? 'rotate(-45deg)' : 'rotate(0)'};
width: ${({open}) => open ? 2.3 : 2}rem;
}
}
&:hover {
opacity: 0.7;
cursor: pointer;
}
#media (min-width: 701px) {
visibility: hidden;
}
`;
export const Dropdown = styled.ul`
z-index: 1;
position: fixed;
list-style: none;
display: flex;
flex-flow: row nowrap;
padding: 0;
margin: 0;
top: 0;
li {
padding: 20px 20px;
&:hover {
opacity: 0.7;
cursor: pointer;
}
}
#media (max-width: 700px) {
position: fixed;
z-index: 0;
flex-flow: column nowrap;
background-color: rgb(32, 80, 36);
transform: ${({ open }) => (open) ? 'translateY(0)' : 'translateY(-100%)'};
vertical-align: middle;
transition: transform 0.3s ease-in-out;
top: 85px;
align-items: center;
border-bottom-left-radius: 4%;
li {
padding-top: 10px;
padding-bottom: 4px;
}
}
`;
I recognize that the problem lies in these two lines:
transform: ${({ open }) => (open) ? 'translateY(0)' : 'translateY(-100%)'};
and
transition: transform 0.3s ease-in-out;
I'm new to Javascript and React though and I cannot think of an efficient way to block the transition animation from going off upon browser resizing.
I can link my repo if more code/reference is necessary.

React component overlap each other

component Publisher.js and other child components projectStatus.js are overlapping each other when Render the Child component. I don't know what's going wrong and how to fix this. You can see the image
this is my Publisher.js
//import useState hook to create menu collapse state
import React, { useState } from "react";
import {NavLink, Outlet} from "react-router-dom"
//import react pro sidebar components
import {
ProSidebar,
Menu,
MenuItem,
SidebarHeader,
SidebarFooter,
SidebarContent,
} from "react-pro-sidebar";
//import icons from react icons
import { FaFileContract } from "react-icons/fa";
import { FiLogOut} from "react-icons/fi";
import { HiDocumentReport } from "react-icons/hi";
import { BiCog } from "react-icons/bi";
import { GiHamburgerMenu } from "react-icons/gi";
//import sidebar css from react-pro-sidebar module and our custom css
import "react-pro-sidebar/dist/css/styles.css";
import "./publisherCss.css";
const Publisher = () => {
//create initial menuCollapse state using useState hook
const [menuCollapse, setMenuCollapse] = useState(false)
//create a custom function that will change menucollapse state from false to true and true to false
const menuIconClick = () => {
//condition checking to change state from true to false and vice versa
menuCollapse ? setMenuCollapse(false) : setMenuCollapse(true);
};
return (
<>
<div id="sidebarHeader">
{/* collapsed props to change menu size using menucollapse state */}
<ProSidebar collapsed={menuCollapse}>
<SidebarHeader>
<div className="logotext">
{/* small and big change using menucollapse state */}
<p>{menuCollapse ? "Evc" : "Publisher "}</p>
</div>
<div className="closemenu" onClick={menuIconClick}>
{/* changing menu collapse icon on click */}
{menuCollapse ? (
<GiHamburgerMenu/>
) : (
<GiHamburgerMenu/>
)}
</div>
</SidebarHeader>
<SidebarContent>
<Menu iconShape="square">
<NavLink to="/publisher/projectstatus"> <MenuItem icon={<FaFileContract />}>Project status</MenuItem> </NavLink>
<MenuItem icon={<HiDocumentReport />}>All project</MenuItem>
<MenuItem icon={<BiCog />}>Settings</MenuItem>
</Menu>
</SidebarContent>
<SidebarFooter>
<NavLink to="/login">
<Menu iconShape="square">
<MenuItem icon={<FiLogOut />}>Logout</MenuItem>
</Menu>
</NavLink>
</SidebarFooter>
</ProSidebar>
</div>
<Outlet />
</>
)
}
export default Publisher;
Publisher.css
#sidebarHeader {
position: absolute;
width: 220px;
display: flex;
}
#sidebarHeader .pro-sidebar {
height: 100vh;
/* position: absolute; */
}
#sidebarHeader .closemenu {
color: rgb(0,7,61);
position: absolute;
right: 0;
z-index: 9999;
line-height: 20px;
border-radius: 50%;
font-weight: bold;
font-size: 22px;
top: 55px;
cursor: pointer;
}
#sidebarHeader .pro-sidebar {
width: 100%;
min-width: 100%;
}
#sidebarHeader .pro-sidebar.collapsed {
width: 80px;
min-width: 80px;
}
#sidebarHeader .pro-sidebar-inner {
background-color: white;
box-shadow: 0.5px 0.866px 2px 0px rgba(0, 0, 0, 0.15);
}
#sidebarHeader .pro-sidebar-inner .pro-sidebar-layout {
overflow-y: hidden;
}
#sidebarHeader .pro-sidebar-inner .pro-sidebar-layout .logotext p {
font-size: 20px;
padding: 10px 20px;
color: #000;
font-weight: bold;
}
#sidebarHeader .pro-sidebar-inner .pro-sidebar-layout ul {
padding: 0 5px;
}
#sidebarHeader .pro-sidebar-inner .pro-sidebar-layout ul .pro-inner-item {
color: #000;
margin: 10px 0px;
font-weight: bold;
}
#sidebarHeader .pro-sidebar-inner .pro-sidebar-layout ul .pro-inner-item .pro-icon-wrapper {
background-color: #fbf4cd;
color: #000;
border-radius: 3px;
}
#sidebarHeader .pro-sidebar-inner .pro-sidebar-layout ul .pro-inner-item .pro-icon-wrapper .pro-item-content {
color: #000;
}
#sidebarHeader .pro-sidebar-inner .pro-sidebar-layout .active {
background-image: linear-gradient(0deg, #fece00 0%, #ffe172 100%);
}
#sidebarHeader .logo {
padding: 20px;
}
#media only screen and (max-width: 720px) {
html {
overflow: hidden;
}
}
.nav-link .active{
background-color: #ffe172;
}
I think I am doing some wrong CSS override property but I am unable to understand what's wrong I am doing. if anyone know please correct me
if anyone knows how to fix this please tell me. it's appreciated
update:
After updating the CSS display: flex it show the child content in flex but the problem is, I specified width: 220px for the sidebar but the child content not go above the 220px width. you can see the image.
Now how can I fix this to a child can use width?
Your picture is not complete, most likely it's styles or wrong location of "Outlet". I made simplified example, I hope it helps.

custom css are applied randomly

I use scss and styled-components together. Recently I don't know why I started to see a race situation over which styles get applied on the final render. Sometimes, usually, when the app is loaded for the first/second time, styles from scss gets applied and when I do a couple of hard refreshes my custom styles written with styled-components get applied. Do you have any idea why is this happening? I don't want to put it! important everywhere to fix it. I would like to understand it.
The below screenshot shows that _sidebar.scss overrode .sidebar styles I wrote.
after some hard refreshes:
after some refreshes, it is another way around:
Here is a component
/* eslint-disable jsx-a11y/anchor-is-valid */
import React from 'react';
import SidebarLinks from './SidebarLinks';
import FormSelectPrinter from "../printer/FormSelectPrinter"
import sidebarData from "../data/";
import {ModalConsumer} from "../components/ModalContext";
import Modal from "../components/Modal";
import { createGlobalStyle } from "styled-components";
const GlobalSidebarStyle = createGlobalStyle`
.sidebar {
background-color: #303b4a;
padding-left: 0;
padding-right: 0;
padding-bottom:40px;
width: 240px;
}
.user__info {
background: #607D8B;
}
.user__info:hover {
background: #607D8B;
}
.user__name {
color: #fff;
}
.user__email {
color: #303b4a;
}
.dropdown-user-menu {
background: #607D8B;
position: relative;
width: 100%;
float: none;
}
.dropdown-select-language {
background: rgba(96, 125, 139, 0.6);
box-shadow: 0 4px 20px rgba(0,0,0,0.9);
right: -26px;
top: 53px;
user-select: none;
}
.dropdown-user-item:hover, .dropdown-user-item:focus {
color: black !important;
}
.dropdown-user-item {
color: white !important;
}
.navigation {
li {
a {
color: #adb5bd;
padding-left: 15px;
&.active {
color: white !important;
}
}
}
}
.navigation li:not(.navigation__active):not(.navigation__sub--active) a:hover {
background-color: rgba(0, 0, 0, 0.19);
color: white;
}
.id-green {
background-color: #73983F;
}
.language-text-color {
color: white !important;
}
.profile_image_style {
border-radius: 100%;
width: 80px;
height: 80px;
}
`
function Sidebar(props) {
const ModalSelectPrinter = ({onClose, ...otherProps}) => {
return (
<Modal>
<FormSelectPrinter onClose={onClose} md={12}></FormSelectPrinter>
</Modal>
)
};
return (
<>
<GlobalSidebarStyle />
<aside className={`sidebar ${props.sidebarToggled? 'toggled' : ''}`}>
<div className="scrollbar-inner">
<div style={{margin: '50px 0 30px 0'}} onClick={() => props.toggleUserInfo()}>
<div className={`user__info ${props.userInfoShown? 'show' : ''}`}>
<div style={{width: '100%', textAlign: 'center'}}>
<div className="user__name">{user.person === null ? user.name : user.person.full_name}</div>
<div className="user__email">{user.email}</div>
</div>
</div>
<div
className={`dropdown-menu dropdown-user-menu ${props.userInfoShown? 'show' : ''}`}>
<ModalConsumer>
{({showModal}) => (
<a className="dropdown-item dropdown-user-item" onClick = {()=>showModal(ModalSelectPrinter, {})}>Select</a>
)}
</ModalConsumer>
<a className="dropdown-item dropdown-user-item" onClick={() => props.logout()}>Log out</a>
</div>
</div>
<SidebarLinks sidebarData={sidebarData} toggleSidebar={props.toggleSidebar}></SidebarLinks>
</div>
</aside>
</>
)
}
export default Sidebar;

Why won't my React accordion animation work?

I've implemented my own responsive accordion in React, and I can't get it to animate the opening of a fold.
This is especially odd because I can get the icon before the title to animate up and down, and, other than the icon being a pseudo-element, I can't seem to see the difference between the two.
JS:
class Accordion extends React.Component {
constructor(props) {
super(props);
this.state = {
active: -1
};
}
/***
* Selects the given fold, and deselects if it has been clicked again by setting "active to -1"
* */
selectFold = foldNum => {
const current = this.state.active === foldNum ? -1 : foldNum;
this.setState(() => ({ active: current }));
};
render() {
return (
<div className="accordion">
{this.props.contents.map((content, i) => {
return (
<Fold
key={`${i}-${content.title}`}
content={content}
handle={() => this.selectFold(i)}
active={i === this.state.active}
/>
);
})}
</div>
);
}
}
class Fold extends React.Component {
render() {
return (
<div className="fold">
<button
className={`fold_trigger ${this.props.active ? "open" : ""}`}
onClick={this.props.handle}
>
{this.props.content.title}
</button>
<div
key="content"
className={`fold_content ${this.props.active ? "open" : ""}`}
>
{this.props.active ? this.props.content.inner : null}
</div>
</div>
);
}
}
CSS:
$line-color: rgba(34, 36, 38, 0.35);
.accordion {
width: 100%;
padding: 1rem 2rem;
display: flex;
flex-direction: column;
border-radius: 10%;
overflow-y: auto;
}
.fold {
.fold_trigger {
&:before {
font-family: FontAwesome;
content: "\f107";
display: block;
float: left;
padding-right: 1rem;
transition: transform 400ms;
transform-origin: 20%;
color: $line-color;
}
text-align: start;
width: 100%;
padding: 1rem;
border: none;
outline: none;
background: none;
cursor: pointer;
border-bottom: 1px solid $line-color;
&.open {
&:before {
transform: rotateZ(-180deg);
}
}
}
.fold_content {
display: none;
max-height: 0;
opacity: 0;
transition: max-height 400ms linear;
&.open {
display: block;
max-height: 400px;
opacity: 1;
}
}
border-bottom: 1px solid $line-color;
}
Here's the CodePen: https://codepen.io/renzyq19/pen/bovZKj
I wouldn't conditionally render the content if you want a smooth transition. It will make animating a slide-up especially tricky.
I would change this:
{this.props.active ? this.props.content.inner : null}
to this:
{this.props.content.inner}
and use this scss:
.fold_content {
max-height: 0;
overflow: hidden;
transition: max-height 400ms ease;
&.open {
max-height: 400px;
}
}
Try the snippet below or see the forked CodePen Demo.
class Accordion extends React.Component {
constructor(props) {
super(props);
this.state = {
active: -1
};
}
/***
* Selects the given fold, and deselects if it has been clicked again by setting "active to -1"
* */
selectFold = foldNum => {
const current = this.state.active === foldNum ? -1 : foldNum;
this.setState(() => ({ active: current }));
};
render() {
return (
<div className="accordion">
{this.props.contents.map((content, i) => {
return (
<Fold
key={`${i}-${content.title}`}
content={content}
handle={() => this.selectFold(i)}
active={i === this.state.active}
/>
);
})}
</div>
);
}
}
class Fold extends React.Component {
render() {
return (
<div className="fold">
<button
className={`fold_trigger ${this.props.active ? "open" : ""}`}
onClick={this.props.handle}
>
{this.props.content.title}
</button>
<div
key="content"
className={`fold_content ${this.props.active ? "open" : ""}`}
>
{this.props.content.inner}
</div>
</div>
);
}
}
const pictures = [
"http://unsplash.it/200",
"http://unsplash.it/200",
"http://unsplash.it/200",
];
var test = (title, text, imageURLs) => {
const images=
<div className='test-images' >
{imageURLs.map((url,i) => <img key={i} src={url} />)}
</div>;
const inner =
<div className='test-content' >
<p>{text} </p>
{images}
</div>;
return {title, inner};
};
const testData = [
test('Title', 'Content',pictures ),
test('Title', 'Content',pictures ),
test('Title', 'Content',pictures ),
test('Title', 'Content',pictures ),
test('Title', 'Content',pictures ),
];
ReactDOM.render(<Accordion contents={testData} />, document.getElementById('root'));
.accordion {
width: 100%;
padding: 1rem 2rem;
display: flex;
flex-direction: column;
border-radius: 10%;
overflow-y: auto;
}
.fold {
border-bottom: 1px solid rgba(34, 36, 38, 0.35);
}
.fold .fold_trigger {
text-align: start;
width: 100%;
padding: 1rem;
border: none;
outline: none;
background: none;
cursor: pointer;
border-bottom: 1px solid rgba(34, 36, 38, 0.35);
}
.fold .fold_trigger:before {
font-family: FontAwesome;
content: "\f107";
display: block;
float: left;
padding-right: 1rem;
transition: transform 400ms;
transform-origin: 20%;
color: rgba(34, 36, 38, 0.35);
}
.fold .fold_trigger.open:before {
transform: rotateZ(-180deg);
}
.fold .fold_content {
max-height: 0;
overflow: hidden;
transition: max-height 400ms ease;
}
.fold .fold_content.open {
max-height: 400px;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.4.0/css/font-awesome.min.css" rel="stylesheet" />
<div id='root'></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
Note:
I used ease instead of linear on the transition because I think it's a nicer effect. But that's just personal taste. linear will work as well.
Also, you can continue to conditionally render the content. A slide-down animation is possible, but a slide-up can't be easily achieved. There are some transition libraries that you could explore as well.
However, I think it's easiest to use the state just for conditional classes (as you are doing with the open class). I think conditionally rendering content to the DOM makes your life difficult if you're trying to do CSS animations.

Categories