1- import { useState , useEffect , useCallback } from 'react' ;
1+ import { useState , useEffect , useCallback , useRef } from 'react' ;
22import Link from 'next/link' ;
33
44import FadeLeftToRight from '@/components/Animations/FadeLeftToRight.component' ;
@@ -22,34 +22,71 @@ const opacityFull = 'opacity-100 group-hover:opacity-100';
2222const Hamburger = ( ) => {
2323 const [ isExpanded , setisExpanded ] = useState ( false ) ;
2424 const [ hidden , setHidden ] = useState ( 'invisible' ) ;
25+ const [ isAnimating , setIsAnimating ] = useState ( false ) ;
26+ const animationTimeoutRef = useRef < ReturnType < typeof setTimeout > | null > (
27+ null ,
28+ ) ;
2529
2630 useEffect ( ( ) => {
2731 if ( isExpanded ) {
2832 setHidden ( '' ) ;
33+ setIsAnimating ( true ) ;
34+
35+ // Clear any existing timeout
36+ if ( animationTimeoutRef . current ) {
37+ clearTimeout ( animationTimeoutRef . current ) ;
38+ }
39+
40+ // Set a timeout for the animation duration
41+ animationTimeoutRef . current = setTimeout ( ( ) => {
42+ setIsAnimating ( false ) ;
43+ } , 1000 ) ; // Match this with the animation duration
2944 } else {
30- setTimeout ( ( ) => {
45+ setIsAnimating ( true ) ;
46+
47+ // Clear any existing timeout
48+ if ( animationTimeoutRef . current ) {
49+ clearTimeout ( animationTimeoutRef . current ) ;
50+ }
51+
52+ // Set a timeout for the animation duration and hiding
53+ animationTimeoutRef . current = setTimeout ( ( ) => {
3154 setHidden ( 'invisible' ) ;
32- } , 1000 ) ;
55+ setIsAnimating ( false ) ;
56+ } , 1000 ) ; // Match this with the animation duration
3357 }
58+
59+ // Cleanup function to clear timeout when component unmounts
60+ return ( ) => {
61+ if ( animationTimeoutRef . current ) {
62+ clearTimeout ( animationTimeoutRef . current ) ;
63+ }
64+ } ;
3465 } , [ isExpanded ] ) ;
3566
3667 const handleMobileMenuClick = useCallback ( ( ) => {
68+ // Prevent clicks during animation
69+ if ( isAnimating ) {
70+ return ;
71+ }
72+
3773 /**
3874 * Anti-pattern: setisExpanded(!isExpanded)
3975 * Even if your state updates are batched and multiple updates to the enabled/disabled state are made together
4076 * each update will rely on the correct previous state so that you always end up with the result you expect.
4177 */
4278 setisExpanded ( ( prevExpanded ) => ! prevExpanded ) ;
43- } , [ setisExpanded ] ) ;
79+ } , [ setisExpanded , isAnimating ] ) ;
4480
4581 return (
4682 < div className = "z-50 md:hidden lg:hidden xl:hidden bg-blue-800" >
4783 < button
48- className = " flex flex-col w-16 rounded justify-center items-center group"
84+ className = { ` flex flex-col w-16 rounded justify-center items-center group ${ isAnimating ? 'cursor-not-allowed' : 'cursor-pointer' } ` }
4985 data-cy = "hamburger"
5086 data-testid = "hamburger"
5187 onClick = { handleMobileMenuClick }
5288 aria-expanded = { isExpanded }
89+ disabled = { isAnimating }
5390 type = "button"
5491 >
5592 < span className = "sr-only text-white text-2xl" > Hamburger</ span >
@@ -95,11 +132,16 @@ const Hamburger = () => {
95132 < span
96133 className = "text-xl inline-block px-4 py-2 no-underline hover:text-black hover:underline"
97134 onClick = { ( ) => {
98- setisExpanded ( ( prevExpanded ) => ! prevExpanded ) ;
135+ if ( ! isAnimating ) {
136+ setisExpanded ( ( prevExpanded ) => ! prevExpanded ) ;
137+ }
99138 } }
100139 onKeyDown = { ( event ) => {
101140 // 'Enter' key or 'Space' key
102- if ( event . key === 'Enter' || event . key === ' ' ) {
141+ if (
142+ ( event . key === 'Enter' || event . key === ' ' ) &&
143+ ! isAnimating
144+ ) {
103145 setisExpanded ( ( prevExpanded ) => ! prevExpanded ) ;
104146 }
105147 } }
0 commit comments