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