1
- import { useState , useEffect , useCallback } from 'react' ;
1
+ import { useState , useEffect , useCallback , useRef } from 'react' ;
2
2
import Link from 'next/link' ;
3
3
4
4
import FadeLeftToRight from '@/components/Animations/FadeLeftToRight.component' ;
@@ -22,34 +22,69 @@ const opacityFull = 'opacity-100 group-hover:opacity-100';
22
22
const Hamburger = ( ) => {
23
23
const [ isExpanded , setisExpanded ] = useState ( false ) ;
24
24
const [ hidden , setHidden ] = useState ( 'invisible' ) ;
25
+ const [ isAnimating , setIsAnimating ] = useState ( false ) ;
26
+ const animationTimeoutRef = useRef < NodeJS . Timeout | null > ( null ) ;
25
27
26
28
useEffect ( ( ) => {
27
29
if ( isExpanded ) {
28
30
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
29
42
} 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 ( ( ) => {
31
52
setHidden ( 'invisible' ) ;
32
- } , 1000 ) ;
53
+ setIsAnimating ( false ) ;
54
+ } , 1000 ) ; // Match this with the animation duration
33
55
}
56
+
57
+ // Cleanup function to clear timeout when component unmounts
58
+ return ( ) => {
59
+ if ( animationTimeoutRef . current ) {
60
+ clearTimeout ( animationTimeoutRef . current ) ;
61
+ }
62
+ } ;
34
63
} , [ isExpanded ] ) ;
35
64
36
65
const handleMobileMenuClick = useCallback ( ( ) => {
66
+ // Prevent clicks during animation
67
+ if ( isAnimating ) {
68
+ return ;
69
+ }
70
+
37
71
/**
38
72
* Anti-pattern: setisExpanded(!isExpanded)
39
73
* Even if your state updates are batched and multiple updates to the enabled/disabled state are made together
40
74
* each update will rely on the correct previous state so that you always end up with the result you expect.
41
75
*/
42
76
setisExpanded ( ( prevExpanded ) => ! prevExpanded ) ;
43
- } , [ setisExpanded ] ) ;
77
+ } , [ setisExpanded , isAnimating ] ) ;
44
78
45
79
return (
46
80
< div className = "z-50 md:hidden lg:hidden xl:hidden bg-blue-800" >
47
81
< 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' } ` }
49
83
data-cy = "hamburger"
50
84
data-testid = "hamburger"
51
85
onClick = { handleMobileMenuClick }
52
86
aria-expanded = { isExpanded }
87
+ disabled = { isAnimating }
53
88
type = "button"
54
89
>
55
90
< span className = "sr-only text-white text-2xl" > Hamburger</ span >
@@ -95,11 +130,13 @@ const Hamburger = () => {
95
130
< span
96
131
className = "text-xl inline-block px-4 py-2 no-underline hover:text-black hover:underline"
97
132
onClick = { ( ) => {
98
- setisExpanded ( ( prevExpanded ) => ! prevExpanded ) ;
133
+ if ( ! isAnimating ) {
134
+ setisExpanded ( ( prevExpanded ) => ! prevExpanded ) ;
135
+ }
99
136
} }
100
137
onKeyDown = { ( event ) => {
101
138
// 'Enter' key or 'Space' key
102
- if ( event . key === 'Enter' || event . key === ' ' ) {
139
+ if ( ( event . key === 'Enter' || event . key === ' ' ) && ! isAnimating ) {
103
140
setisExpanded ( ( prevExpanded ) => ! prevExpanded ) ;
104
141
}
105
142
} }
0 commit comments