UNPKG

4.41 kBPlain TextView Raw
1/*
2 * Copyright 2020 Adobe. All rights reserved.
3 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License. You may obtain a copy
5 * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 *
7 * Unless required by applicable law or agreed to in writing, software distributed under
8 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 * OF ANY KIND, either express or implied. See the License for the specific language
10 * governing permissions and limitations under the License.
11 */
12
13// Portions of the code in this file are based on code from react.
14// Original licensing for the following can be found in the
15// NOTICE file in the root directory of this source tree.
16// See https://github.com/facebook/react/tree/cc7c1aece46a6b69b41958d731e0fd27c94bfc6c/packages/react-interactions
17
18import {RefObject, SyntheticEvent, useEffect, useRef} from 'react';
19
20interface InteractOutsideProps {
21 ref: RefObject<Element>,
22 onInteractOutside?: (e: SyntheticEvent) => void,
23 onInteractOutsideStart?: (e: SyntheticEvent) => void,
24 /** Whether the interact outside events should be disabled. */
25 isDisabled?: boolean
26}
27
28/**
29 * Example, used in components like Dialogs and Popovers so they can close
30 * when a user clicks outside them.
31 */
32export function useInteractOutside(props: InteractOutsideProps) {
33 let {ref, onInteractOutside, isDisabled, onInteractOutsideStart} = props;
34 let stateRef = useRef({
35 isPointerDown: false,
36 ignoreEmulatedMouseEvents: false,
37 onInteractOutside,
38 onInteractOutsideStart
39 });
40 let state = stateRef.current;
41 state.onInteractOutside = onInteractOutside;
42 state.onInteractOutsideStart = onInteractOutsideStart;
43
44 useEffect(() => {
45 if (isDisabled) {
46 return;
47 }
48
49 let onPointerDown = (e) => {
50 if (isValidEvent(e, ref) && state.onInteractOutside) {
51 if (state.onInteractOutsideStart) {
52 state.onInteractOutsideStart(e);
53 }
54 state.isPointerDown = true;
55 }
56 };
57
58 // Use pointer events if available. Otherwise, fall back to mouse and touch events.
59 if (typeof PointerEvent !== 'undefined') {
60 let onPointerUp = (e) => {
61 if (state.isPointerDown && state.onInteractOutside && isValidEvent(e, ref)) {
62 state.isPointerDown = false;
63 state.onInteractOutside(e);
64 }
65 };
66
67 // changing these to capture phase fixed combobox
68 document.addEventListener('pointerdown', onPointerDown, true);
69 document.addEventListener('pointerup', onPointerUp, true);
70
71 return () => {
72 document.removeEventListener('pointerdown', onPointerDown, true);
73 document.removeEventListener('pointerup', onPointerUp, true);
74 };
75 } else {
76 let onMouseUp = (e) => {
77 if (state.ignoreEmulatedMouseEvents) {
78 state.ignoreEmulatedMouseEvents = false;
79 } else if (state.isPointerDown && state.onInteractOutside && isValidEvent(e, ref)) {
80 state.isPointerDown = false;
81 state.onInteractOutside(e);
82 }
83 };
84
85 let onTouchEnd = (e) => {
86 state.ignoreEmulatedMouseEvents = true;
87 if (state.onInteractOutside && state.isPointerDown && isValidEvent(e, ref)) {
88 state.isPointerDown = false;
89 state.onInteractOutside(e);
90 }
91 };
92
93 document.addEventListener('mousedown', onPointerDown, true);
94 document.addEventListener('mouseup', onMouseUp, true);
95 document.addEventListener('touchstart', onPointerDown, true);
96 document.addEventListener('touchend', onTouchEnd, true);
97
98 return () => {
99 document.removeEventListener('mousedown', onPointerDown, true);
100 document.removeEventListener('mouseup', onMouseUp, true);
101 document.removeEventListener('touchstart', onPointerDown, true);
102 document.removeEventListener('touchend', onTouchEnd, true);
103 };
104 }
105 }, [ref, state, isDisabled]);
106}
107
108function isValidEvent(event, ref) {
109 if (event.button > 0) {
110 return false;
111 }
112
113 // if the event target is no longer in the document
114 if (event.target) {
115 const ownerDocument = event.target.ownerDocument;
116 if (!ownerDocument || !ownerDocument.documentElement.contains(event.target)) {
117 return false;
118 }
119 }
120
121 return ref.current && !ref.current.contains(event.target);
122}