UNPKG

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