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