UNPKG

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