UNPKG

6.3 kBJavaScriptView Raw
1import * as React from 'react';
2import * as ReactDOM from 'react-dom';
3import PropTypes from 'prop-types';
4import ownerDocument from '../utils/ownerDocument';
5import useForkRef from '../utils/useForkRef';
6import useEventCallback from '../utils/useEventCallback';
7import { elementAcceptingRef, exactProp } from '@material-ui/utils';
8
9function mapEventPropToEvent(eventProp) {
10 return eventProp.substring(2).toLowerCase();
11}
12
13function clickedRootScrollbar(event) {
14 return document.documentElement.clientWidth < event.clientX || document.documentElement.clientHeight < event.clientY;
15}
16/**
17 * Listen for click events that occur somewhere in the document, outside of the element itself.
18 * For instance, if you need to hide a menu when people click anywhere else on your page.
19 */
20
21
22function ClickAwayListener(props) {
23 const {
24 children,
25 disableReactTree = false,
26 mouseEvent = 'onClick',
27 onClickAway,
28 touchEvent = 'onTouchEnd'
29 } = props;
30 const movedRef = React.useRef(false);
31 const nodeRef = React.useRef(null);
32 const activatedRef = React.useRef(false);
33 const syntheticEventRef = React.useRef(false);
34 React.useEffect(() => {
35 // Ensure that this component is not "activated" synchronously.
36 // https://github.com/facebook/react/issues/20074
37 setTimeout(() => {
38 activatedRef.current = true;
39 }, 0);
40 return () => {
41 activatedRef.current = false;
42 };
43 }, []); // can be removed once we drop support for non ref forwarding class components
44
45 const handleOwnRef = React.useCallback(instance => {
46 // #StrictMode ready
47 nodeRef.current = ReactDOM.findDOMNode(instance);
48 }, []);
49 const handleRef = useForkRef(children.ref, handleOwnRef); // The handler doesn't take event.defaultPrevented into account:
50 //
51 // event.preventDefault() is meant to stop default behaviours like
52 // clicking a checkbox to check it, hitting a button to submit a form,
53 // and hitting left arrow to move the cursor in a text input etc.
54 // Only special HTML elements have these default behaviors.
55
56 const handleClickAway = useEventCallback(event => {
57 // Given developers can stop the propagation of the synthetic event,
58 // we can only be confident with a positive value.
59 const insideReactTree = syntheticEventRef.current;
60 syntheticEventRef.current = false; // 1. IE 11 support, which trigger the handleClickAway even after the unbind
61 // 2. The child might render null.
62 // 3. Behave like a blur listener.
63
64 if (!activatedRef.current || !nodeRef.current || clickedRootScrollbar(event)) {
65 return;
66 } // Do not act if user performed touchmove
67
68
69 if (movedRef.current) {
70 movedRef.current = false;
71 return;
72 }
73
74 let insideDOM; // If not enough, can use https://github.com/DieterHolvoet/event-propagation-path/blob/master/propagationPath.js
75
76 if (event.composedPath) {
77 insideDOM = event.composedPath().indexOf(nodeRef.current) > -1;
78 } else {
79 // TODO v6 remove dead logic https://caniuse.com/#search=composedPath.
80 const doc = ownerDocument(nodeRef.current);
81 insideDOM = !doc.documentElement.contains(event.target) || nodeRef.current.contains(event.target);
82 }
83
84 if (!insideDOM && (disableReactTree || !insideReactTree)) {
85 onClickAway(event);
86 }
87 }); // Keep track of mouse/touch events that bubbled up through the portal.
88
89 const createHandleSynthetic = handlerName => event => {
90 syntheticEventRef.current = true;
91 const childrenPropsHandler = children.props[handlerName];
92
93 if (childrenPropsHandler) {
94 childrenPropsHandler(event);
95 }
96 };
97
98 const childrenProps = {
99 ref: handleRef
100 };
101
102 if (touchEvent !== false) {
103 childrenProps[touchEvent] = createHandleSynthetic(touchEvent);
104 }
105
106 React.useEffect(() => {
107 if (touchEvent !== false) {
108 const mappedTouchEvent = mapEventPropToEvent(touchEvent);
109 const doc = ownerDocument(nodeRef.current);
110
111 const handleTouchMove = () => {
112 movedRef.current = true;
113 };
114
115 doc.addEventListener(mappedTouchEvent, handleClickAway);
116 doc.addEventListener('touchmove', handleTouchMove);
117 return () => {
118 doc.removeEventListener(mappedTouchEvent, handleClickAway);
119 doc.removeEventListener('touchmove', handleTouchMove);
120 };
121 }
122
123 return undefined;
124 }, [handleClickAway, touchEvent]);
125
126 if (mouseEvent !== false) {
127 childrenProps[mouseEvent] = createHandleSynthetic(mouseEvent);
128 }
129
130 React.useEffect(() => {
131 if (mouseEvent !== false) {
132 const mappedMouseEvent = mapEventPropToEvent(mouseEvent);
133 const doc = ownerDocument(nodeRef.current);
134 doc.addEventListener(mappedMouseEvent, handleClickAway);
135 return () => {
136 doc.removeEventListener(mappedMouseEvent, handleClickAway);
137 };
138 }
139
140 return undefined;
141 }, [handleClickAway, mouseEvent]);
142 return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.cloneElement(children, childrenProps));
143}
144
145process.env.NODE_ENV !== "production" ? ClickAwayListener.propTypes = {
146 // ----------------------------- Warning --------------------------------
147 // | These PropTypes are generated from the TypeScript type definitions |
148 // | To update them edit the d.ts file and run "yarn proptypes" |
149 // ----------------------------------------------------------------------
150
151 /**
152 * The wrapped element.
153 */
154 children: elementAcceptingRef.isRequired,
155
156 /**
157 * If `true`, the React tree is ignored and only the DOM tree is considered.
158 * This prop changes how portaled elements are handled.
159 */
160 disableReactTree: PropTypes.bool,
161
162 /**
163 * The mouse event to listen to. You can disable the listener by providing `false`.
164 */
165 mouseEvent: PropTypes.oneOf(['onClick', 'onMouseDown', 'onMouseUp', false]),
166
167 /**
168 * Callback fired when a "click away" event is detected.
169 */
170 onClickAway: PropTypes.func.isRequired,
171
172 /**
173 * The touch event to listen to. You can disable the listener by providing `false`.
174 */
175 touchEvent: PropTypes.oneOf(['onTouchEnd', 'onTouchStart', false])
176} : void 0;
177
178if (process.env.NODE_ENV !== 'production') {
179 // eslint-disable-next-line
180 ClickAwayListener['propTypes' + ''] = exactProp(ClickAwayListener.propTypes);
181}
182
183export default ClickAwayListener;
\No newline at end of file