Skip to content

Commit c8c7267

Browse files
committed
Add useRootClose hook
1 parent ea80750 commit c8c7267

File tree

6 files changed

+191
-73
lines changed

6 files changed

+191
-73
lines changed

babel.config.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,6 @@ module.exports = {
2727
transform: 'lodash/${member}',
2828
preventFullImport: true,
2929
},
30-
'react-overlays': {
31-
transform: 'react-overlays/${member}',
32-
preventFullImport: true,
33-
},
3430
},
3531
],
3632
],

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@
5555
"invariant": "^2.2.1",
5656
"lodash.debounce": "^4.0.8",
5757
"prop-types": "^15.5.8",
58-
"react-overlays": "^5.2.0",
5958
"scroll-into-view-if-needed": "^3.1.0",
6059
"warning": "^4.0.1"
6160
},

src/behaviors/token.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import React, {
99
MouseEventHandler,
1010
useState,
1111
} from 'react';
12-
import { useRootClose } from 'react-overlays';
1312

13+
import { useRootClose } from '../components/RootClose';
1414
import { getDisplayName, isFunction, warn } from '../utils';
1515

1616
import { optionType } from '../propTypes';
@@ -43,7 +43,6 @@ export function useToken<T extends HTMLElement>({
4343
...props
4444
}: UseTokenProps<T>) {
4545
const [active, setActive] = useState<boolean>(false);
46-
const [rootElement, attachRef] = useState<T | null>(null);
4746

4847
const handleBlur = (e: Event) => {
4948
setActive(false);
@@ -72,7 +71,7 @@ export function useToken<T extends HTMLElement>({
7271
}
7372
};
7473

75-
useRootClose(rootElement, handleBlur, {
74+
const attachRef = useRootClose(handleBlur, {
7675
...props,
7776
disabled: !active,
7877
});
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { useCallback, useEffect, useRef } from 'react';
2+
import useEventCallback from '@restart/hooks/useEventCallback';
3+
4+
import { noop, warn } from '../../utils';
5+
6+
export type MouseEvents = {
7+
[K in keyof GlobalEventHandlersEventMap]: GlobalEventHandlersEventMap[K] extends MouseEvent
8+
? K
9+
: never;
10+
}[keyof GlobalEventHandlersEventMap];
11+
12+
function isLeftClickEvent(event: MouseEvent) {
13+
return event.button === 0;
14+
}
15+
16+
function isModifiedEvent(event: MouseEvent) {
17+
return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
18+
}
19+
20+
export interface ClickOutsideOptions {
21+
disabled?: boolean;
22+
clickTrigger?: MouseEvents;
23+
}
24+
25+
const InitialTriggerEvents: Partial<Record<MouseEvents, MouseEvents>> = {
26+
click: 'mousedown',
27+
mouseup: 'mousedown',
28+
pointerup: 'pointerdown',
29+
};
30+
31+
/**
32+
* The `useClickOutside` hook registers your callback on the document that fires
33+
* when a pointer event is registered outside of the provided ref or element.
34+
*/
35+
function useClickOutside(
36+
ref: React.RefObject<Element>,
37+
onClickOutside: (e: MouseEvent) => void = noop,
38+
{ disabled, clickTrigger = 'click' }: ClickOutsideOptions = {}
39+
) {
40+
const preventMouseClickOutsideRef = useRef(false);
41+
const waitingForTrigger = useRef(false);
42+
43+
const handleMouseCapture = useCallback(
44+
(e: MouseEvent) => {
45+
const currentTarget = ref.current;
46+
47+
warn(
48+
!!currentTarget,
49+
'ClickOutside captured a close event but does not have a ref to compare it to. ' +
50+
'useClickOutside(), should be passed a ref that resolves to a DOM node'
51+
);
52+
53+
preventMouseClickOutsideRef.current =
54+
!currentTarget ||
55+
isModifiedEvent(e) ||
56+
!isLeftClickEvent(e) ||
57+
!!currentTarget.contains(e.target as Node) ||
58+
waitingForTrigger.current;
59+
60+
waitingForTrigger.current = false;
61+
},
62+
[ref]
63+
);
64+
65+
const handleInitialMouse = useEventCallback((e: MouseEvent) => {
66+
const currentTarget = ref.current;
67+
68+
if (currentTarget?.contains(e.target as Node)) {
69+
waitingForTrigger.current = true;
70+
} else {
71+
// When clicking on scrollbars within current target, click events are not
72+
// triggered, so this ref is never reset inside `handleMouseCapture`. This
73+
// would cause a bug where it requires 2 clicks to close the overlay.
74+
waitingForTrigger.current = false;
75+
}
76+
});
77+
78+
const handleMouse = useEventCallback((e: MouseEvent) => {
79+
if (!preventMouseClickOutsideRef.current) {
80+
onClickOutside(e);
81+
}
82+
});
83+
84+
useEffect(() => {
85+
if (disabled || ref == null) return undefined;
86+
87+
const doc = ref.current?.ownerDocument || document;
88+
const ownerWindow = doc.defaultView || window;
89+
90+
// Store the current event to avoid triggering handlers immediately
91+
// For things rendered in an iframe, the event might originate on the parent window
92+
// so we should fall back to that global event if the local one doesn't exist
93+
// https://github.com/facebook/react/issues/20074
94+
let currentEvent = ownerWindow.event ?? ownerWindow.parent?.event;
95+
96+
let removeInitialTriggerListener: (() => void) | null = null;
97+
if (InitialTriggerEvents[clickTrigger]) {
98+
doc.addEventListener(
99+
InitialTriggerEvents[clickTrigger]!,
100+
handleInitialMouse,
101+
true
102+
);
103+
104+
removeInitialTriggerListener = () => {
105+
doc.removeEventListener(
106+
InitialTriggerEvents[clickTrigger]!,
107+
handleInitialMouse
108+
);
109+
};
110+
}
111+
112+
const handleMouseTrigger = (e: MouseEvent) => {
113+
// skip if this event is the same as the one running when we added the handlers
114+
if (e === currentEvent) {
115+
currentEvent = undefined;
116+
return;
117+
}
118+
handleMouse(e);
119+
};
120+
121+
// Use capture for this listener so it fires before React's listener, to
122+
// avoid false positives in the contains() check below if the target DOM
123+
// element is removed in the React mouse callback.
124+
doc.addEventListener(clickTrigger, handleMouseCapture, true);
125+
doc.addEventListener(clickTrigger, handleMouseTrigger, true);
126+
127+
return () => {
128+
removeInitialTriggerListener?.();
129+
doc.removeEventListener(clickTrigger, handleMouseCapture);
130+
doc.removeEventListener(clickTrigger, handleMouseTrigger);
131+
};
132+
}, [
133+
ref,
134+
disabled,
135+
clickTrigger,
136+
handleMouseCapture,
137+
handleInitialMouse,
138+
handleMouse,
139+
]);
140+
}
141+
142+
export default useClickOutside;
Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,51 @@
1-
import { useRef } from 'react';
2-
import _useRootClose, { RootCloseOptions } from 'react-overlays/useRootClose';
1+
import { useEffect, useRef } from 'react';
2+
import useEventCallback from '@restart/hooks/useEventCallback';
3+
4+
import useClickOutside, { ClickOutsideOptions } from './useClickOutside';
5+
import { noop } from '../../utils';
36

47
function useRootClose(
58
onRootClose: (e: Event) => void,
6-
options: RootCloseOptions
9+
options: ClickOutsideOptions
710
) {
8-
const rootElementRef = useRef<HTMLDivElement>(null);
9-
_useRootClose(rootElementRef.current, onRootClose, options);
10-
return rootElementRef;
11+
const ref = useRef<HTMLDivElement>(null);
12+
13+
const onClose = onRootClose || noop;
14+
15+
useClickOutside(ref, onClose, options);
16+
17+
const handleKeyUp = useEventCallback((e: KeyboardEvent) => {
18+
if (e.key === 'Escape') {
19+
onClose(e);
20+
}
21+
});
22+
23+
useEffect(() => {
24+
if (options.disabled || ref == null) return undefined;
25+
26+
const doc = ref.current?.ownerDocument || document;
27+
28+
// Store the current event to avoid triggering handlers immediately
29+
// https://github.com/facebook/react/issues/20074
30+
let currentEvent = (doc.defaultView || window).event;
31+
32+
const onKeyUp = (e: KeyboardEvent) => {
33+
// skip if this event is the same as the one running when we added the handlers
34+
if (e === currentEvent) {
35+
currentEvent = undefined;
36+
return;
37+
}
38+
handleKeyUp(e);
39+
};
40+
41+
doc.addEventListener('keyup', onKeyUp);
42+
43+
return () => {
44+
doc.removeEventListener('keyup', onKeyUp);
45+
};
46+
}, [ref, options.disabled, handleKeyUp]);
47+
48+
return ref;
1149
}
1250

1351
export default useRootClose;

yarn.lock

Lines changed: 3 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,7 +1228,7 @@
12281228
core-js-pure "^3.20.2"
12291229
regenerator-runtime "^0.13.4"
12301230

1231-
"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.8", "@babel/runtime@^7.14.6", "@babel/runtime@^7.17.8", "@babel/runtime@^7.5.0", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
1231+
"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.17.8", "@babel/runtime@^7.5.0", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
12321232
version "7.23.2"
12331233
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885"
12341234
integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==
@@ -1807,12 +1807,7 @@
18071807
schema-utils "^3.0.0"
18081808
source-map "^0.7.3"
18091809

1810-
"@popperjs/core@^2.11.6":
1811-
version "2.11.8"
1812-
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
1813-
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
1814-
1815-
"@restart/hooks@^0.4.0", "@restart/hooks@^0.4.7":
1810+
"@restart/hooks@^0.4.0":
18161811
version "0.4.11"
18171812
resolved "https://registry.yarnpkg.com/@restart/hooks/-/hooks-0.4.11.tgz#8876ccce1d4ad2a4b793a31689d63df36cf56088"
18181813
integrity sha512-Ft/ncTULZN6ldGHiF/k5qt72O8JyRMOeg0tApvCni8LkoiEahO+z3TNxfXIVGy890YtWVDvJAl662dVJSJXvMw==
@@ -3140,15 +3135,6 @@
31403135
dependencies:
31413136
"@types/react" "^17"
31423137

3143-
"@types/react@>=16.9.11":
3144-
version "18.2.30"
3145-
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.30.tgz#b84f786864fc46f18545364a54d5e1316308e59b"
3146-
integrity sha512-OfqdJnDsSo4UNw0bqAjFCuBpLYQM7wvZidz0hVxHRjrEkzRlvZL1pJVyOSY55HMiKvRNEo9DUBRuEl7FNlJ/Vg==
3147-
dependencies:
3148-
"@types/prop-types" "*"
3149-
"@types/scheduler" "*"
3150-
csstype "^3.0.2"
3151-
31523138
"@types/react@^17", "@types/react@^17.0.14":
31533139
version "17.0.69"
31543140
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.69.tgz#245a0cf2f5b0fb1d645691d3083e3c7d4409b98f"
@@ -3214,11 +3200,6 @@
32143200
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.9.tgz#72e164381659a49557b0a078b28308f2c6a3e1ce"
32153201
integrity sha512-zC0iXxAv1C1ERURduJueYzkzZ2zaGyc+P2c95hgkikHPr3z8EdUZOlgEQ5X0DRmwDZn+hekycQnoeiiRVrmilQ==
32163202

3217-
"@types/warning@^3.0.0":
3218-
version "3.0.2"
3219-
resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.2.tgz#264f1f93a68f5dcb598db9764e40f14e13b0e630"
3220-
integrity sha512-S/2+OjBIcBl8Kur23YLe0hG1e7J5m2bHfB4UuMNoLZjIFhQWhTf1FeS+WFoXHUC6QsCEfk4pftj4J1KIKC1glA==
3221-
32223203
"@types/webpack-env@^1.16.0":
32233204
version "1.18.3"
32243205
resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.18.3.tgz#e81f769199a5609c751f34fcc6f6095ceac7831f"
@@ -5844,14 +5825,6 @@ dom-converter@^0.2.0:
58445825
dependencies:
58455826
utila "~0.4"
58465827

5847-
dom-helpers@^5.2.0:
5848-
version "5.2.1"
5849-
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
5850-
integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==
5851-
dependencies:
5852-
"@babel/runtime" "^7.8.7"
5853-
csstype "^3.0.2"
5854-
58555828
dom-serializer@^1.0.1:
58565829
version "1.4.1"
58575830
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30"
@@ -11020,25 +10993,6 @@ react-is@^18.0.0:
1102010993
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
1102110994
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
1102210995

11023-
react-lifecycles-compat@^3.0.4:
11024-
version "3.0.4"
11025-
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
11026-
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
11027-
11028-
react-overlays@^5.2.0:
11029-
version "5.2.1"
11030-
resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-5.2.1.tgz#49dc007321adb6784e1f212403f0fb37a74ab86b"
11031-
integrity sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==
11032-
dependencies:
11033-
"@babel/runtime" "^7.13.8"
11034-
"@popperjs/core" "^2.11.6"
11035-
"@restart/hooks" "^0.4.7"
11036-
"@types/warning" "^3.0.0"
11037-
dom-helpers "^5.2.0"
11038-
prop-types "^15.7.2"
11039-
uncontrollable "^7.2.1"
11040-
warning "^4.0.3"
11041-
1104210996
react-refresh@^0.11.0:
1104310997
version "0.11.0"
1104410998
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046"
@@ -12844,16 +12798,6 @@ unbox-primitive@^1.0.2:
1284412798
has-symbols "^1.0.3"
1284512799
which-boxed-primitive "^1.0.2"
1284612800

12847-
uncontrollable@^7.2.1:
12848-
version "7.2.1"
12849-
resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-7.2.1.tgz#1fa70ba0c57a14d5f78905d533cf63916dc75738"
12850-
integrity sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==
12851-
dependencies:
12852-
"@babel/runtime" "^7.6.3"
12853-
"@types/react" ">=16.9.11"
12854-
invariant "^2.2.4"
12855-
react-lifecycles-compat "^3.0.4"
12856-
1285712801
undici-types@~5.25.1:
1285812802
version "5.25.3"
1285912803
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.25.3.tgz#e044115914c85f0bcbb229f346ab739f064998c3"
@@ -13199,7 +13143,7 @@ walker@^1.0.7, walker@^1.0.8, walker@~1.0.5:
1319913143
dependencies:
1320013144
makeerror "1.0.12"
1320113145

13202-
warning@^4.0.1, warning@^4.0.3:
13146+
warning@^4.0.1:
1320313147
version "4.0.3"
1320413148
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
1320513149
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==

0 commit comments

Comments
 (0)