Stop vertical scroll when modal is open React - reactjs

I have a navigation bar in functional React with a mobile menu in it. The issue is that I have an animation that causes the navigation to hide unless the user scrolls up. When the navigation bar hides with this animation it drags mobile menu with it making it impossible to close the menu without being directed to a different page.
I would like it so that when the mobile menu is open scrolling is disabled until the user moves to a new page or closes the menu. Avoiding the issue of the navigation bar disappearing.
I have tried using in the event listener document.body.style.overflow = "hidden"; However this make the page stop scrolling regardless of whether the menu is open.
The scroll animation is triggering using an event listener and useEffect hook so I need a way of working it into this event listener so that when the menu is visible scroll can be disabled.
Heres the component:
import * as React from 'react';
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { Squash as Hamburger } from 'hamburger-react';
import { debounce } from './utilities/helpers';
const links = [
{name: "Home", path: "/"},
{name: "Projects", path: "/projects"},
{name: "Contact", path: "/contact"}
];
export default function Header() {
//Navbar scroll hide
const [prevScrollPos, setPrevScrollPos] = useState(0);
const [visible, setVisible] = useState(true);
const handleScroll = debounce(() => {
const currentScrollPos = window.pageYOffset;
setVisible((prevScrollPos > currentScrollPos && prevScrollPos - currentScrollPos > 75) || currentScrollPos < 10); //Check if user has scrolled beyond the navbar height to hide or less then 10 to show navbar
setPrevScrollPos(currentScrollPos); //Sets last scroll position
}, 100); //Time interval for debounce
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [prevScrollPos, visible, handleScroll]);
//Hamburger state
const [isOpen, setOpen] = useState(false);
//Set navbar background without scroll
function ToggleNavLinks() {
var navBar = document.getElementById('navbar');
navBar.style.background = '#181a1d';
};
//Scroll event
window.addEventListener('scroll', function() {;
var navBar = document.getElementById('navbar');
//If user scrolls 1 or more pixels
if(window.scrollY >= 1){
navBar.style.background = '#181a1d';
navBar.style.boxShadow = 'box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)';
}
//Else they haven't scrolled
else {
navBar.style.background = 'none';
navBar.style.boxShadow = 'none'
}
});
return (
<nav style={{ top: visible ? '0' : '-75px' }} id="navbar" className="w-full h-[75px] fixed py-2 px-3 sm:px-6 flex justify-between items-center z-20 transition-all duration-300 ease-in-out">
{/* Logo */}
<Link to="/">
<div className="bg-[url('../public/Images/logo.png')] hover:bg-[url('../public/Images/logo-hover.png')] w-[260px] h-[60px]"></div>
</Link>
<div className="flex"> {/* Container for links and menu */}
{/* Hamburger */}
<div className="lg:hidden mb-0.5 z-10" onClick={(ToggleNavLinks)}> {/* Had to use parent element as Tailwind doesn't effect custom React components */}
<Hamburger
color="#ffffff"
easing="ease-in-out"
rounded
toggled={isOpen}
toggle={setOpen}
/>
</div>
{/* If menu is open */}
{isOpen && (
<>
<div className="h-screen w-screen absolute mt-[3.75rem] left-0 bg-black opacity-60 blur-sm"></div>
<ul className="h-screen w-3/5 sm:w-1/2 md:w-2/5 bg-quaternary flex flex-col lg:flex-row items-center absolute top-0 right-0 pt-32">
{links.map((link) => (
<li className="m-5 lg:my-0 lg:mx-5 leading-10">
<Link
className="text-2xl font-medium text-white hover:text-primary transition duration-300 ease-in-out"
to={link.path}
onClick={() => setOpen(false)}
>
{link.name}
</Link>
</li>
))}
</ul>
</>
)}
{/* Links */}
<ul className= "hidden lg:flex flex-row">
{links.map((link) => (
<li className="m-5 lg:my-0 lg:mx-5">
<Link className="text-2xl font-medium text-white hover:text-primary transition duration-300 ease-in-out" to={link.path}>
{link.name}
</Link>
</li>
))}
</ul>
</div>
</nav>
);
}
Heres the current bug:
Heres how its intended too look with scroll locked:
enter image description here

Related

How do I make tailwind scroll snap & smooth scroll work together in React?

I am trying to build a single page react app and I was wondering how I would apply tailwind scroll snap and smooth scroll together. Every time I enable scroll-snap in the y-direction, my code for smooth scroll no longer works. I'm a beginner to React so any help would be greatly appreciated. Below I have attached the app.jsx, the header, and the first component.
This is the closest stackoverflow post I've found: Making smooth anchor tag scrolling compatible with scroll snapping
import React from "react";
import Header from "./components/Header"
import First from "./components/First";
import Second from "./components/Second";
import Third from "./components/Third";
function App() {
return (
<div className="overflow-y-auto snap-y snap-mandatory h-[100vh] w-[100%]">
<Header />
<First />
<Second />
<Third />
</div>
);
}
export default App;
import React from "react";
const First = () => {
return (
<div
className="w-full h-screen flex justify-center items-center bg-blue-400 snap-center"
link="first"
>
First
</div>
);
};
export default First;
import React, { useState } from "react";
import { FaBars, FaTimes } from "react-icons/fa";
import { Link } from "react-scroll";
const Header = () => {
const [nav, setNav] = useState(false);
const links = [
{
id: 1,
link: "first",
},
{
id: 2,
link: "second",
},
{
id: 3,
link: "third",
},
];
return (
<div className="flex justify-between items-center w-full h-20 px-4 text-white bg-black fixed">
<div>
<h1 className="text-5xl font-signature ml-2">Practice Youtuber Tutorial</h1>
</div>
<ul className="hidden md:flex">
{links.map(({ id, link }) => (
<li
key={id}
className="px-4 cursor-pointer capitalize font-medium text-gray-500 hover:scale-105 duration-200"
>
<Link to={link} smooth duration={500}>
{link}
</Link>
</li>
))}
</ul>
<div
onClick={() => setNav(!nav)}
className="cursor-pointer pr-4 z-10 text-gray-500 md:hidden"
>
{nav ? <FaTimes size={30} /> : <FaBars size={30} />}
</div>
{nav && (
<ul className="flex flex-col justify-center items-center absolute top-0 left-0 w-full h-screen bg-gradient-to-b from-black to-gray-800 text-gray-500">
{links.map(({ id, link }) => (
<li
key={id}
className="px-4 cursor-pointer capitalize py-6 text-4xl"
>
<Link
onClick={() => setNav(!nav)}
to={link}
smooth
duration={500}
>
{link}
</Link>
</li>
))}
</ul>
)}
</div>
);
};
export default Header;
smooth scroll use by adding the following code
import { Link, animateScroll as scroll } from "react-scroll";

Hamburger menu will not close when LINK is clicked REACT

I built a mobile nav hamburger dropdown. The NavMobile links are added to the dropdown in a seperate component. When I try to click on a link in the dropdown it will scroll to the proper section of the page but the mobile dropdown will have to be manually closed which is not what I was hoping for.
Here is the code in the header component:
import React, { useState, useEffect } from "react";
// import data
import { headerData } from "../data";
//import components
import Nav from "./Nav";
import NavMobile from "./NavMobile";
import Socials from "./Socials";
//import icons
import { TiThMenu } from "react-icons/ti";
const Header = () => {
//destructure header data
const { logo } = headerData;
//header state
const [isActive, setIsActive] = useState(false);
//nav mobile state
const [navMobile, setNavMobile] = useState(false);
//scroll event
useEffect(() => {
window.addEventListener("scroll", () => {
window.scrollY > 50 ? setIsActive(true) : setIsActive(false);
});
});
return (
<header
className={`${
isActive ? "h-[100px] lg:h-[110px] shadow-lg" : "h-[120px] lg:h-[150px]"
} fixed bg-white left-0 right-0 z-10 max-w-[1920px] w-full mx-auto transition-all duration-300`}
>
<div className="flex justify-between items-center h-full pl-[50px] pr-[60px]">
{/* logo */}
<a href="/">
<img className="w-[188px] h-[90px]" src={logo} alt="Tara Zep Logo" />
</a>
{/* nav hidden mobile*/}
<div className="hidden xl:flex">
<Nav />
</div>
{/* nav menu btn hidden desktop*/}
<div
onClick={() => setNavMobile(!navMobile)}
className="xl:hidden absolute right-[5%] bg-white text-dark p-2 rounded-md cursor-pointer"
>
<TiThMenu className="text-3xl" />
</div>
{/* nav mobile */}
<div
className={`${navMobile ? "max-h-full" : "max-h-0"} ${
isActive
? "top-[100px] lg:top-[110px]"
: "top-[120px] lg:top-[150px]"
} fixed bg-white w-full h-full left-0 -z-10 transition-all duration-300`}
>
<NavMobile onClick={() => setNavMobile(!navMobile)}/>
</div>
{/* social icons hidden show on desktop */}
<div className="hidden xl:flex">
<Socials />
</div>
</div>
</header>
);
};
export default Header;
And the NavMobile component:
import React from "react";
//import Link
import { Link } from "react-scroll";
//import nav data
import { navData } from "../data";
//import components
import Socials from "./Socials";
const NavMobile = () => {
//destructure nav data
const { items } = navData;
return (
<nav className="w-full h-full flex flex-col justify-evenly overflow-hidden">
<ul className="flex flex-col justify-center items-center gap-y-6 py-6 mb-8">
{items.map((item, index) => {
return (
<li key={index}>
<Link
to={item.href}
spy={true}
smooth={true}
offset={-70}
duration={500}
className="text-2xl font-primary cursor-pointer uppercase"
>
{item.name}
</Link>
</li>
);
})}
</ul>
<div className="text-2xl">
<Socials />
</div>
</nav>
);
};
export default NavMobile;
The problem is that when I try to add the
onClick={() => setNavMobile(!navMobile)} to the <NavMobile /> tag it does not work to collapse the dropdown. When i add it to the surrounding div it will work everywhere EXCEPT for on the clicked links. I am using React Scroll for the links.
you may try passing setNavMobile funcion to NavMobile as a prop and then calling it on Link onClick

How to make hamburger menu dissapear after clicking link with react scroll

I am trying to make my hamburger menu work with react scroll. It works as it should but when i click the links the menu does not dissapear. I tried the below but it makes it worst.
import React from 'react';
// import nav data
import { navData } from '../data';
// import components
import Socials from './Socials';
import { Link} from "react-scroll";
import { useState } from 'react';
const NavMobile = () => {
// destructure nav data
const [isActive, setIsActive] = useState(true)
const { items } = navData;
return (
<nav className={`${isActive? "w-full h-full flex flex-col justify-evenly overflow-hidden" : ""}`}>
<ul className='flex flex-col justify-center items-center gap-y-6 py-6 mb-8'>
{items.map((item, index) => {
return (
<Link
activeClass="active"
to={item.href}
spy={true}
smooth={true}
offset={-70}
duration={500}
>
<li key={index} onClick={setIsActive(!isActive)}>
<a className='text-2xl font-primary uppercase' href={item.href} >
{item.name}
</a>
</li>
</Link>
);
})}
</ul>
<div className='text-2xl'>
<Socials />
</div>
</nav>
);
};
export default NavMobile;
I just can't find out what's the problem
Your conditional classNames for your <nav> don't need to be wrapped in the template literal ${} the line is instead written as follows
<nav className={isActive ? "w-full h-full flex flex-col justify-evenly overflow-hidden" : ""}>
Your onClick handler should also have it's own defined function.
const handleClick = () => {setIsActive(!isActive)}
And then be parsed to the onClick method of the <li>
<li key={index} onClick={handleClick}>
See https://bobbyhadz.com/blog/react-onclick-pass-event-and-parameter#:~:text=To%20pass%20an%20event%20and,event%20and%20parameter%20to%20handleClick%20. for more help. This is necessary because you are parsing the function to the onClick method rather than the result of the function.

How to reset the position of the carousel when the button was clicked

All button
Main Hall button
I'm working on an image gallery slider that filters the images when the button was clicked. The "All" button shows all of the images from "Main Hall" to "Parking Space". The "Main Hall" to "Parking Space" buttons shows only 3 images and doesn't need to be draggable. Please see the link of the images above. I'm using React, Tailwind, and Framer motion.
I'm trying to make the image slider to reset its position when the "Main Hall" to "Parking Space" buttons were clicked.
import { GalleryData } from "../constants";
import { useEffect, useState, useRef } from "react";
import styles, { layout } from "../style";
import { motion } from "framer-motion";
import { useCallback } from "react";
const Gallery = () => {
const [data, setData] = useState([]);
const [collection, setCollection] = useState([]);
const [width, setWidth] = useState(0);
const [stopper, setStopper] = useState(false);
const carousel = useRef();
const setNewCarouselWidth = useCallback(() => {
setWidth(carousel.current?.scrollWidth || 0);
}, [carousel]);
useEffect(() => {
setData(GalleryData);
setCollection([... new Set(GalleryData.map((item) => item.title))]);
window.addEventListener("resize", setNewCarouselWidth);
return () => {
window.removeEventListener("resize", setNewCarouselWidth);
}
}, [carousel]);
const gallery_filter = (itemData) => {
const filterData = GalleryData.filter((item) => item.title == itemData);
setData(filterData);
}
return (
<section className={layout.section}>
<div ref={carousel} className="flex-1 flex-col justify-start overflow-hidden">
<div className="filterItem">
<ul className="hidden md:flex items-center list-none md:pl-[170px] gap-[50px] mb-[30px]">
<li><button className="font-poppins py-[8px] px-[10px] focus:underline focus:underline-offset-8" onClick={() => {setData(GalleryData); setReset(false)}}>All</button></li>
{
collection.map((item) =>
<li>
<button className="font-poppins capitalize py-[8px] px-[10px] hover:text-gray-400 focus:underline focus:underline-offset-8 duration-500" onClick={() => {gallery_filter(item); setStopper(true)}}>
{item}
</button>
</li>
)
}
</ul>
</div>
<motion.div className="bg-pink-400 cursor-grab">
{/* inner carousel */}
<motion.div
className={`flex bg-blue-300 ${stopper ? "inset-y-0 left-0" : "right-0"}`}
ref={carousel}
drag={ "x" }
dragConstraints={{ right: 0, left: -(width - (carousel.current?.clientWidth || 0 )) }}
onLoad={setNewCarouselWidth}
>
{data.map((item) =>
<motion.div key={item.id} className="md:min-w-[27rem] min-w-[13rem]">
<img src={item.image} className="md:w-[400px] md:h-[508px] w-[200px] h-[300px] rounded-xl object-cover pointer-events-none" />
</motion.div>
)}
</motion.div>
</motion.div>
</div>
</section>
)
}
export default Gallery

React - Prevent re-render whole list when delete element

I'm working on a toasts notifications system using React v17 and the React context API. I'm NOT using Redux.
The problem:
Toasts are dismissed automatically after a given delay. The toast element which is dismissed is removed from the list from the context. The problem is that each Toast component is re-render, the whole list is re-render, each time the list change.
I don't want that each component be re-rendered. Only the dismissed Toast component should be "re-render", understand deleted from the list displayed.
I put key attribute on my Toast components but it doesn't work as I expected it would.
Thank you for helping me !
The code below:
function Layout() {
const toastsContext = useContext(ToastsContext);
const toastsList = toastsContext.toastsList;
const [list, setList] = useState([]);
useEffect(() => {
setList(toastsList);
}, [toastsList]);
const displayToasts = list.map(toast =>
<Toast
key={toast.id.toString()}
id={toast.id}
color={toast.color}
title={toast.title}
message={toast.message}
dismissable={toast.dismissable}
showTime={toast.showTime}
autoDismissDelay={toast.autoDismissDelay}
redirectTo={toast.redirectTo} />
);
return(
<div className='bg-slate-800 text-slate-400 min-h-screen relative'>
<Header />
<Outlet />
<div className='fixed top-20 right-4 flex flex-col gap-2'>
{displayToasts}
</div>
</div>
);
}
export default Layout;
Toast component
import { memo, useCallback, useContext, useEffect, useState } from "react";
import { ToastsContext } from "../context/ToastsContext";
import { FiX, FiArrowRight } from 'react-icons/fi';
import { Link } from "react-router-dom";
function Toast({id, color, title, message, dismissable, autoDismissDelay, showTime, redirectTo}) {
const toastsContext = useContext(ToastsContext);
const [hiddenToast, setHiddenToast] = useState(false);
const getToastColor = () => {
switch (color) {
case 'primary':
return 'bg-sky-500';
case 'danger':
return 'bg-rose-500';
case 'success':
return 'bg-green-500';
default:
return 'bg-sky-500';
}
};
const dismissToast = useCallback(() => {
setHiddenToast(true);
setTimeout(() => {
document.getElementById(`toast${id}`).className = 'hidden';
toastsContext.dismissToast(id);
}, 310);
}, [id, toastsContext]);
useEffect(() => {
const interval = setInterval(() => {
dismissToast();
}, autoDismissDelay);
return () => {
clearInterval(interval);
}
}, [autoDismissDelay, dismissToast]);
return(
<div id={'toast'+id} className={`transition ease-out duration-300 text-slate-50 text-sm rounded-lg drop-shadow-lg opacity-100 ${getToastColor()} ${hiddenToast ? 'translate-x-20 opacity-0' : ''}`}>
<div className="w-72">
<div className={`${(redirectTo && redirectTo !== '') || (message && message !== '') ? 'py-2' : 'py-4'} px-4 flex font-semibold items-center`}>
<p>{title}</p>
<div className="ml-auto flex items-center">
{showTime ? <p className="text-xs mr-3">11m ago</p> : null}
{dismissable ?
<button type="button" className="flex items-center justify-center text-base" onClick={dismissToast}>
<FiX />
</button>
: null
}
</div>
</div>
{
redirectTo && redirectTo !== '' ?
<Link className={`border-t border-slate-700 bg-slate-800 rounded-b-lg px-4 py-3 font-medium hover:bg-slate-700 block`} to={redirectTo}>
<p className="flex items-center justify-between">
<span>{message ? message : 'See more'}</span>
<FiArrowRight />
</p>
</Link>
:
message ?
<div className={`border-t border-slate-700 bg-slate-800 rounded-b-lg px-4 py-3 font-medium`}>
<p className="flex items-center justify-between">
<span>{message}</span>
</p>
</div>
: null
}
</div>
</div>
);
};
export default memo(Toast);
That's the default behavior in react: When a component (eg, Layout) re renders, so to do all its children (the Toasts). If you want to skip rendering some of the toasts, then Toast will need to use React.memo. Also, for the memoization to work, the props to each Toast will need to stay the same from one render to the next. From looking at your code i think that will happen without any changes, but it's important to know so you don't think memo is enough on its own.
import { memo } from 'react';
function Toast() {
// ...
}
export default memo(Toast);

Resources