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,69 @@ 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 < NodeJS . Timeout | null > ( null ) ;
2527
2628 useEffect ( ( ) => {
2729 if ( isExpanded ) {
2830 setHidden ( '' ) ;
31+ setIsAnimating ( true ) ;
32+
33+ // Clear any existing timeout
34+ if ( animationTimeoutRef . current ) {
35+ clearTimeout ( animationTimeoutRef . current ) ;
36+ }
37+
38+ // Set a timeout for the animation duration
39+ animationTimeoutRef . current = setTimeout ( ( ) => {
40+ setIsAnimating ( false ) ;
41+ } , 1000 ) ; // Match this with the animation duration
2942 } else {
30- setTimeout ( ( ) => {
43+ setIsAnimating ( true ) ;
44+
45+ // Clear any existing timeout
46+ if ( animationTimeoutRef . current ) {
47+ clearTimeout ( animationTimeoutRef . current ) ;
48+ }
49+
50+ // Set a timeout for the animation duration and hiding
51+ animationTimeoutRef . current = setTimeout ( ( ) => {
3152 setHidden ( 'invisible' ) ;
32- } , 1000 ) ;
53+ setIsAnimating ( false ) ;
54+ } , 1000 ) ; // Match this with the animation duration
3355 }
56+
57+ // Cleanup function to clear timeout when component unmounts
58+ return ( ) => {
59+ if ( animationTimeoutRef . current ) {
60+ clearTimeout ( animationTimeoutRef . current ) ;
61+ }
62+ } ;
3463 } , [ isExpanded ] ) ;
3564
3665 const handleMobileMenuClick = useCallback ( ( ) => {
66+ // Prevent clicks during animation
67+ if ( isAnimating ) {
68+ return ;
69+ }
70+
3771 /**
3872 * Anti-pattern: setisExpanded(!isExpanded)
3973 * Even if your state updates are batched and multiple updates to the enabled/disabled state are made together
4074 * each update will rely on the correct previous state so that you always end up with the result you expect.
4175 */
4276 setisExpanded ( ( prevExpanded ) => ! prevExpanded ) ;
43- } , [ setisExpanded ] ) ;
77+ } , [ setisExpanded , isAnimating ] ) ;
4478
4579 return (
4680 < div className = "z-50 md:hidden lg:hidden xl:hidden bg-blue-800" >
4781 < button
48- className = " flex flex-col w-16 rounded justify-center items-center group"
82+ className = { ` flex flex-col w-16 rounded justify-center items-center group ${ isAnimating ? 'cursor-not-allowed' : 'cursor-pointer' } ` }
4983 data-cy = "hamburger"
5084 data-testid = "hamburger"
5185 onClick = { handleMobileMenuClick }
5286 aria-expanded = { isExpanded }
87+ disabled = { isAnimating }
5388 type = "button"
5489 >
5590 < span className = "sr-only text-white text-2xl" > Hamburger</ span >
@@ -95,11 +130,13 @@ const Hamburger = () => {
95130 < span
96131 className = "text-xl inline-block px-4 py-2 no-underline hover:text-black hover:underline"
97132 onClick = { ( ) => {
98- setisExpanded ( ( prevExpanded ) => ! prevExpanded ) ;
133+ if ( ! isAnimating ) {
134+ setisExpanded ( ( prevExpanded ) => ! prevExpanded ) ;
135+ }
99136 } }
100137 onKeyDown = { ( event ) => {
101138 // 'Enter' key or 'Space' key
102- if ( event . key === 'Enter' || event . key === ' ' ) {
139+ if ( ( event . key === 'Enter' || event . key === ' ' ) && ! isAnimating ) {
103140 setisExpanded ( ( prevExpanded ) => ! prevExpanded ) ;
104141 }
105142 } }
0 commit comments