1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 | import {getOwnerDocument, useEffectEvent} from '@react-aria/utils';
|
19 | import {RefObject} from '@react-types/shared';
|
20 | import {useEffect, useRef} from 'react';
|
21 |
|
22 | export interface InteractOutsideProps {
|
23 | ref: RefObject<Element | null>,
|
24 | onInteractOutside?: (e: PointerEvent) => void,
|
25 | onInteractOutsideStart?: (e: PointerEvent) => void,
|
26 |
|
27 | isDisabled?: boolean
|
28 | }
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 | export function useInteractOutside(props: InteractOutsideProps) {
|
35 | let {ref, onInteractOutside, isDisabled, onInteractOutsideStart} = props;
|
36 | let stateRef = useRef({
|
37 | isPointerDown: false,
|
38 | ignoreEmulatedMouseEvents: false
|
39 | });
|
40 |
|
41 | let onPointerDown = useEffectEvent((e) => {
|
42 | if (onInteractOutside && isValidEvent(e, ref)) {
|
43 | if (onInteractOutsideStart) {
|
44 | onInteractOutsideStart(e);
|
45 | }
|
46 | stateRef.current.isPointerDown = true;
|
47 | }
|
48 | });
|
49 |
|
50 | let triggerInteractOutside = useEffectEvent((e: PointerEvent) => {
|
51 | if (onInteractOutside) {
|
52 | onInteractOutside(e);
|
53 | }
|
54 | });
|
55 |
|
56 | useEffect(() => {
|
57 | let state = stateRef.current;
|
58 | if (isDisabled) {
|
59 | return;
|
60 | }
|
61 |
|
62 | const element = ref.current;
|
63 | const documentObject = getOwnerDocument(element);
|
64 |
|
65 |
|
66 | if (typeof PointerEvent !== 'undefined') {
|
67 | let onPointerUp = (e) => {
|
68 | if (state.isPointerDown && isValidEvent(e, ref)) {
|
69 | triggerInteractOutside(e);
|
70 | }
|
71 | state.isPointerDown = false;
|
72 | };
|
73 |
|
74 |
|
75 | documentObject.addEventListener('pointerdown', onPointerDown, true);
|
76 | documentObject.addEventListener('pointerup', onPointerUp, true);
|
77 |
|
78 | return () => {
|
79 | documentObject.removeEventListener('pointerdown', onPointerDown, true);
|
80 | documentObject.removeEventListener('pointerup', onPointerUp, true);
|
81 | };
|
82 | } else {
|
83 | let onMouseUp = (e) => {
|
84 | if (state.ignoreEmulatedMouseEvents) {
|
85 | state.ignoreEmulatedMouseEvents = false;
|
86 | } else if (state.isPointerDown && isValidEvent(e, ref)) {
|
87 | triggerInteractOutside(e);
|
88 | }
|
89 | state.isPointerDown = false;
|
90 | };
|
91 |
|
92 | let onTouchEnd = (e) => {
|
93 | state.ignoreEmulatedMouseEvents = true;
|
94 | if (state.isPointerDown && isValidEvent(e, ref)) {
|
95 | triggerInteractOutside(e);
|
96 | }
|
97 | state.isPointerDown = false;
|
98 | };
|
99 |
|
100 | documentObject.addEventListener('mousedown', onPointerDown, true);
|
101 | documentObject.addEventListener('mouseup', onMouseUp, true);
|
102 | documentObject.addEventListener('touchstart', onPointerDown, true);
|
103 | documentObject.addEventListener('touchend', onTouchEnd, true);
|
104 |
|
105 | return () => {
|
106 | documentObject.removeEventListener('mousedown', onPointerDown, true);
|
107 | documentObject.removeEventListener('mouseup', onMouseUp, true);
|
108 | documentObject.removeEventListener('touchstart', onPointerDown, true);
|
109 | documentObject.removeEventListener('touchend', onTouchEnd, true);
|
110 | };
|
111 | }
|
112 | }, [ref, isDisabled, onPointerDown, triggerInteractOutside]);
|
113 | }
|
114 |
|
115 | function isValidEvent(event, ref) {
|
116 | if (event.button > 0) {
|
117 | return false;
|
118 | }
|
119 |
|
120 | if (event.target) {
|
121 |
|
122 | const ownerDocument = event.target.ownerDocument;
|
123 | if (!ownerDocument || !ownerDocument.documentElement.contains(event.target)) {
|
124 | return false;
|
125 | }
|
126 |
|
127 |
|
128 | if (event.target.closest('[data-react-aria-top-layer]')) {
|
129 | return false;
|
130 | }
|
131 | }
|
132 |
|
133 | return ref.current && !ref.current.contains(event.target);
|
134 | }
|