@@ -33,6 +33,7 @@ export type UseOnClickOutsideOptions = {
33
33
*/
34
34
export const useOnClickOutside = ( options : UseOnClickOutsideOptions ) => {
35
35
const { refs, callback, element, disabled, contains : containsProp } = options ;
36
+ const timeoutId = React . useRef < number | undefined > ( undefined ) ;
36
37
37
38
const listener = useEventCallback ( ( ev : MouseEvent | TouchEvent ) => {
38
39
const contains : UseOnClickOutsideOptions [ 'contains' ] =
@@ -45,14 +46,51 @@ export const useOnClickOutside = (options: UseOnClickOutsideOptions) => {
45
46
} ) ;
46
47
47
48
React . useEffect ( ( ) => {
49
+ // Store the current event to avoid triggering handlers immediately
50
+ // Note this depends on a deprecated but extremely well supported quirk of the web platform
51
+ // https://github.com/facebook/react/issues/20074
52
+ let currentEvent = getWindowEvent ( window ) ;
53
+
54
+ const conditionalHandler = ( event : MouseEvent | TouchEvent ) => {
55
+ // Skip if this event is the same as the one running when we added the handlers
56
+ if ( event === currentEvent ) {
57
+ currentEvent = undefined ;
58
+ return ;
59
+ }
60
+
61
+ listener ( event ) ;
62
+ } ;
63
+
48
64
if ( ! disabled ) {
49
- element ?. addEventListener ( 'click' , listener ) ;
50
- element ?. addEventListener ( 'touchstart' , listener ) ;
65
+ element ?. addEventListener ( 'click' , conditionalHandler ) ;
66
+ element ?. addEventListener ( 'touchstart' , conditionalHandler ) ;
51
67
}
52
68
69
+ // Garbage collect this event after it's no longer useful to avoid memory leaks
70
+ timeoutId . current = setTimeout ( ( ) => {
71
+ currentEvent = undefined ;
72
+ } , 1 ) ;
73
+
53
74
return ( ) => {
54
- element ?. removeEventListener ( 'click' , listener ) ;
55
- element ?. removeEventListener ( 'touchstart' , listener ) ;
75
+ element ?. removeEventListener ( 'click' , conditionalHandler ) ;
76
+ element ?. removeEventListener ( 'touchstart' , conditionalHandler ) ;
77
+
78
+ clearTimeout ( timeoutId . current ) ;
79
+ currentEvent = undefined ;
56
80
} ;
57
81
} , [ listener , element , disabled ] ) ;
58
82
} ;
83
+
84
+ const getWindowEvent = ( target : Node | Window ) : Event | undefined => {
85
+ if ( target ) {
86
+ if ( typeof ( target as Window ) . window === 'object' && ( target as Window ) . window === target ) {
87
+ // eslint-disable-next-line deprecation/deprecation
88
+ return target . event ;
89
+ }
90
+
91
+ // eslint-disable-next-line deprecation/deprecation
92
+ return ( target as Node ) . ownerDocument ?. defaultView ?. event ?? undefined ;
93
+ }
94
+
95
+ return undefined ;
96
+ } ;
0 commit comments