UNPKG

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