Skip to content

Commit 6ffca36

Browse files
authored
fix(useOnClickOutside): use conditional handler window event (#18323)
* fix(useOnClickOutside): use conditional handler window event Workaround already used by v0 to handle facebook/react#20074 * Change files * change file * mem leak fix
1 parent 1cfbb7a commit 6ffca36

File tree

2 files changed

+49
-4
lines changed

2 files changed

+49
-4
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "useOnClickOutside, workaround for facebook/react#20074",
4+
"packageName": "@fluentui/react-utilities",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

packages/react-utilities/src/hooks/useOnClickOutside.ts

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export type UseOnClickOutsideOptions = {
3333
*/
3434
export const useOnClickOutside = (options: UseOnClickOutsideOptions) => {
3535
const { refs, callback, element, disabled, contains: containsProp } = options;
36+
const timeoutId = React.useRef<number | undefined>(undefined);
3637

3738
const listener = useEventCallback((ev: MouseEvent | TouchEvent) => {
3839
const contains: UseOnClickOutsideOptions['contains'] =
@@ -45,14 +46,51 @@ export const useOnClickOutside = (options: UseOnClickOutsideOptions) => {
4546
});
4647

4748
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+
4864
if (!disabled) {
49-
element?.addEventListener('click', listener);
50-
element?.addEventListener('touchstart', listener);
65+
element?.addEventListener('click', conditionalHandler);
66+
element?.addEventListener('touchstart', conditionalHandler);
5167
}
5268

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+
5374
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;
5680
};
5781
}, [listener, element, disabled]);
5882
};
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

Comments
 (0)