Line data Source code
1 : import * as React from 'react'
2 :
3 : type TouchOrMouseEventCallback = (event: MouseEvent | TouchEvent) => boolean | undefined
4 :
5 1 : const STOP_PROPAGATION = true;
6 1 : const REGISTRY: { [id: number]: TouchOrMouseEventCallback } = {}
7 1 : let NEXT_HANDLER_ID = 1
8 :
9 1 : function handleClick(event: MouseEvent) {
10 1 : if (!event.defaultPrevented) {
11 1 : for (const handler of Object.values(REGISTRY).reverse()) {
12 1 : if (handler(event) === STOP_PROPAGATION || event.defaultPrevented) {
13 1 : break
14 : }
15 : }
16 : }
17 1 : }
18 :
19 : interface UseOnOutsideClickArgs {
20 : containerRef: React.RefObject<HTMLDivElement | null> | React.RefObject<HTMLUListElement | null>
21 : ignoreClickRefs?: React.RefObject<Element | null>[]
22 : onClickOutside: (e: MouseEvent | TouchEvent) => void
23 : }
24 :
25 1 : export function useOnOutsideClick(args: UseOnOutsideClickArgs) {
26 1 : const id = React.useMemo(() => NEXT_HANDLER_ID++, [])
27 :
28 1 : const handler = React.useCallback<TouchOrMouseEventCallback>(
29 1 : event => {
30 : // Don't call click handler if the mouse event was triggered by an auxiliary button (wheel button/etc).
31 : // A right click closes the overlay like a native menu, the right click of a row may open the menu of another row.
32 1 : if (event instanceof MouseEvent && event.button !== 0 && event.button !== 2) {
33 0 : return STOP_PROPAGATION;
34 : }
35 : // Don't call handler if the click happened inside of the container
36 1 : if (args.containerRef.current?.contains(event.target as Node)) {
37 1 : return STOP_PROPAGATION;
38 : }
39 : // Don't call handler if click happened on an ignored ref
40 1 : if (args.ignoreClickRefs && args.ignoreClickRefs.some(({ current }) => current?.contains(event.target as Node))) {
41 0 : return STOP_PROPAGATION;
42 : }
43 :
44 1 : args.onClickOutside(event)
45 1 : },
46 1 : [args.containerRef, args.ignoreClickRefs, args.onClickOutside],
47 1 : )
48 :
49 1 : React.useEffect(() => {
50 1 : if (Object.keys(REGISTRY).length === 0) {
51 : // use capture to ensure we get all events
52 1 : document.addEventListener('mousedown', handleClick, { capture: true })
53 : }
54 1 : REGISTRY[id] = handler;
55 :
56 1 : return () => {
57 1 : delete REGISTRY[id];
58 1 : if (Object.keys(REGISTRY).length === 0) {
59 1 : document.removeEventListener('mousedown', handleClick, { capture: true })
60 : }
61 1 : }
62 1 : }, [id, handler])
63 1 : }
|