Good days. This is the problem: on my NextJS project, there is a full-screen menu made using React Portal, which opens in the form of a slide bar that exits from the right side of the screen. I needed to make an animation of the appearance and disappearance of this screen. I decided not to use libraries like CSSTransition, I used the @keyframes animation on css. Everything turned out fine with the animation of the appearance of the screen. But there is a problem with his disappearance. This exit screen is closed in three ways – by pressing the Escape key, by pressing one of the menu items on this screen, and by pressing the button that previously served to open this screen. It is the animation that does not work when the screen is closed by pressing the button. You can see it on the website https://next-site-ivory-six.vercel.app /. Burger button in the upper right corner.
Since Portal simply deletes the created content when the state changes to false, it is almost impossible to animate it without libraries. But I decided to animate not just the modal block itself, but its internal container, and start the animation at the moment of clicking on the menu, escape or button, and close the modal block itself with some delay, which I created using setTimeout.
const [endAnimation, setEndAnimation] = useState(false);
const checkIfClickedEsc = e => {
if ( e.code === "Escape" ) {
setEndAnimation(true);
setTimeout(() => {
onClose()
}, 100)
}
}
const handleCloseClick = () => {
setEndAnimation(true);
setTimeout(() => {
onClose();
}, 100)
};
Here is a code that simply changes the state to the opposite (toggle) when clicked.
const handleClick = () => {
setShowSlideScreen(!showSlideScreen);
};
Perhaps the problem is that I incorrectly distributed two useState hooks (with showing and hiding the modal block and with the start of the animation) by files (only three files are involved in this case), maybe I just don’t understand what and where yet, since I recently started studying React.
Below I give the full code of my three components – Header, which integrates the button, slideScreen, which contains the code of the modal block and Nav, which contains the menu located inside the slideScreen. Please help anyone who can. Thanks
//header.js
import React, { useState, useEffect } from "react";
import Link from "next/link";
import ThemeToggle from "@/components/themeSwitcher";
import SlideScreen from "@/components/global/slideScreen";
import styles from "@/styles/header.module.css"
export default function Header() {
const headerInnerClasses = [styles.headerInner, "wrapper"].join(" ")
const [showSlideScreen, setShowSlideScreen] = useState(false);
const handleClick = () => {
setShowSlideScreen(!showSlideScreen);
};
const menuBtnClasses = [showSlideScreen ? `${styles.menuBtnActive} ${styles.menuBtn}` : `${styles.menuBtn}`, "mainBtn"].join(" ")
useEffect(() => {
document.body.className = showSlideScreen ? "isSlideActive" : "";
});
return (
<header className={styles.header}>
<div className={headerInnerClasses}>
<Link className={styles.headerLogo} href='/'>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 160"><path d="M10 20 0 29.9v-5.5L5 20zM28 20 0 46.9v-5.5L22 20zM40 27 0 64.9v-5.5L40 21zM40 44 0 81.9v-5.5L40 38zM40 61 0 98.9v-5.5L40 55zM40 78 0 115.9v-5.5L40 72zM40 95 0 132.9v-5.5L40 89zM40 112 0 149.9v-5.5L40 106zM40 129 7 160H1l39-37zM40 146l-15 14h-6l21-20zM40 160h-3l3-3z" fill="#494949"/><path d="M128 107.7 69.7 73.9l58.3-34V0L0 73.9 88 126v34h40z" fill="#f65f59"/></svg>
</Link>
<div className={styles.headerActions}>
<ThemeToggle/>
<button className={menuBtnClasses} onClick={handleClick}>
<div className={styles.burger}></div>
</button>
</div>
</div>
{showSlideScreen &&
<SlideScreen
onClose={() => setShowSlideScreen(false)}
showSlideScreen={showSlideScreen}
/>}
</header>
)
}
//slideScreen.js
import React, { useEffect, useState, useRef } from "react";
import ReactDOM from "react-dom";
import styles from "@/styles/slideScreen.module.css"
import Nav from "@/components/nav";
//import { CSSTransition } from "react-transition-group";
const SlideScreen = ({onClose}) => {
const [isBrowser, setIsBrowser] = useState(false);
const [endAnimation, setEndAnimation] = useState(false);
const checkIfClickedEsc = e => {
if ( e.code === "Escape" ) {
setEndAnimation(true);
setTimeout(() => {
onClose()
}, 100)
}
}
const handleCloseClick = () => {
setEndAnimation(true);
setTimeout(() => {
onClose();
}, 100)
};
useEffect(() => {
setIsBrowser(true);
document.addEventListener("keydown", checkIfClickedEsc)
return () => {
document.removeEventListener("keydown", checkIfClickedEsc)
}
}, []);
const inScreenClasses = [endAnimation ? `${styles.inScreen} ${styles.close}` : `${styles.inScreen}`, "dotsBg"].join(" ")
const slideScreenContent = (
<div className={styles.slideScreen}>
<div className={inScreenClasses}>
<div className="wrapper">
<Nav handleCloseClick={handleCloseClick} />
</div>
</div>
</div>
)
if (isBrowser) {
return ReactDOM.createPortal(
slideScreenContent,
document.getElementById("modal-root")
)
} else {
return null;
}
}
export default SlideScreen;
//nav.js
import React, { useState } from "react";
import Link from "next/link";
import {useRouter} from "next/router";
import styles from "@/styles/header.module.css"
const Nav = ({handleCloseClick}) => {
const router = useRouter();
const routes = ['Blog', 'Work', 'Contact'];
const routesRu = ['Блог', 'Работы', 'Контакты'];
return (
<nav className={styles.headerNav}>
<ul className={styles.headerMenu}>
{routes.map((route, i) => {
return (
<li key={route} className={`${router.pathname === `/${route.toLowerCase()}`}`}>
<Link
href={`/${route.toLowerCase()}`}
onClick={handleCloseClick}
>{routesRu[i]}</Link>
</li>
)
})}
</ul>
</nav>
)
}
export default Nav