180 lines
7.2 KiB
Plaintext
180 lines
7.2 KiB
Plaintext
'use client';
|
|
|
|
import * as React from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import { 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
|
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
function mapEventPropToEvent(eventProp) {
|
|
return eventProp.substring(2).toLowerCase();
|
|
}
|
|
function clickedRootScrollbar(event, doc) {
|
|
return doc.documentElement.clientWidth < event.clientX || doc.documentElement.clientHeight < event.clientY;
|
|
}
|
|
/**
|
|
* Listen for click events that occur somewhere in the document, outside of the element itself.
|
|
* For instance, if you need to hide a menu when people click anywhere else on your page.
|
|
*
|
|
* Demos:
|
|
*
|
|
* - [Click-Away Listener](https://mui.com/base-ui/react-click-away-listener/)
|
|
*
|
|
* API:
|
|
*
|
|
* - [ClickAwayListener API](https://mui.com/base-ui/react-click-away-listener/components-api/#click-away-listener)
|
|
*/
|
|
function ClickAwayListener(props) {
|
|
var children = props.children,
|
|
_props$disableReactTr = props.disableReactTree,
|
|
disableReactTree = _props$disableReactTr === void 0 ? false : _props$disableReactTr,
|
|
_props$mouseEvent = props.mouseEvent,
|
|
mouseEvent = _props$mouseEvent === void 0 ? 'onClick' : _props$mouseEvent,
|
|
onClickAway = props.onClickAway,
|
|
_props$touchEvent = props.touchEvent,
|
|
touchEvent = _props$touchEvent === void 0 ? 'onTouchEnd' : _props$touchEvent;
|
|
var movedRef = React.useRef(false);
|
|
var nodeRef = React.useRef(null);
|
|
var activatedRef = React.useRef(false);
|
|
var syntheticEventRef = React.useRef(false);
|
|
React.useEffect(function () {
|
|
// Ensure that this component is not "activated" synchronously.
|
|
// https://github.com/facebook/react/issues/20074
|
|
setTimeout(function () {
|
|
activatedRef.current = true;
|
|
}, 0);
|
|
return function () {
|
|
activatedRef.current = false;
|
|
};
|
|
}, []);
|
|
var handleRef = useForkRef(
|
|
// @ts-expect-error TODO upstream fix
|
|
children.ref, nodeRef);
|
|
|
|
// The handler doesn't take event.defaultPrevented into account:
|
|
//
|
|
// event.preventDefault() is meant to stop default behaviors like
|
|
// clicking a checkbox to check it, hitting a button to submit a form,
|
|
// and hitting left arrow to move the cursor in a text input etc.
|
|
// Only special HTML elements have these default behaviors.
|
|
var handleClickAway = useEventCallback(function (event) {
|
|
// Given developers can stop the propagation of the synthetic event,
|
|
// we can only be confident with a positive value.
|
|
var insideReactTree = syntheticEventRef.current;
|
|
syntheticEventRef.current = false;
|
|
var doc = ownerDocument(nodeRef.current);
|
|
|
|
// 1. IE11 support, which trigger the handleClickAway even after the unbind
|
|
// 2. The child might render null.
|
|
// 3. Behave like a blur listener.
|
|
if (!activatedRef.current || !nodeRef.current || 'clientX' in event && clickedRootScrollbar(event, doc)) {
|
|
return;
|
|
}
|
|
|
|
// Do not act if user performed touchmove
|
|
if (movedRef.current) {
|
|
movedRef.current = false;
|
|
return;
|
|
}
|
|
var insideDOM;
|
|
|
|
// If not enough, can use https://github.com/DieterHolvoet/event-propagation-path/blob/master/propagationPath.js
|
|
if (event.composedPath) {
|
|
insideDOM = event.composedPath().indexOf(nodeRef.current) > -1;
|
|
} else {
|
|
insideDOM = !doc.documentElement.contains(
|
|
// @ts-expect-error returns `false` as intended when not dispatched from a Node
|
|
event.target) || nodeRef.current.contains(
|
|
// @ts-expect-error returns `false` as intended when not dispatched from a Node
|
|
event.target);
|
|
}
|
|
if (!insideDOM && (disableReactTree || !insideReactTree)) {
|
|
onClickAway(event);
|
|
}
|
|
});
|
|
|
|
// Keep track of mouse/touch events that bubbled up through the portal.
|
|
var createHandleSynthetic = function createHandleSynthetic(handlerName) {
|
|
return function (event) {
|
|
syntheticEventRef.current = true;
|
|
var childrenPropsHandler = children.props[handlerName];
|
|
if (childrenPropsHandler) {
|
|
childrenPropsHandler(event);
|
|
}
|
|
};
|
|
};
|
|
var childrenProps = {
|
|
ref: handleRef
|
|
};
|
|
if (touchEvent !== false) {
|
|
childrenProps[touchEvent] = createHandleSynthetic(touchEvent);
|
|
}
|
|
React.useEffect(function () {
|
|
if (touchEvent !== false) {
|
|
var mappedTouchEvent = mapEventPropToEvent(touchEvent);
|
|
var doc = ownerDocument(nodeRef.current);
|
|
var handleTouchMove = function handleTouchMove() {
|
|
movedRef.current = true;
|
|
};
|
|
doc.addEventListener(mappedTouchEvent, handleClickAway);
|
|
doc.addEventListener('touchmove', handleTouchMove);
|
|
return function () {
|
|
doc.removeEventListener(mappedTouchEvent, handleClickAway);
|
|
doc.removeEventListener('touchmove', handleTouchMove);
|
|
};
|
|
}
|
|
return undefined;
|
|
}, [handleClickAway, touchEvent]);
|
|
if (mouseEvent !== false) {
|
|
childrenProps[mouseEvent] = createHandleSynthetic(mouseEvent);
|
|
}
|
|
React.useEffect(function () {
|
|
if (mouseEvent !== false) {
|
|
var mappedMouseEvent = mapEventPropToEvent(mouseEvent);
|
|
var doc = ownerDocument(nodeRef.current);
|
|
doc.addEventListener(mappedMouseEvent, handleClickAway);
|
|
return function () {
|
|
doc.removeEventListener(mappedMouseEvent, handleClickAway);
|
|
};
|
|
}
|
|
return undefined;
|
|
}, [handleClickAway, mouseEvent]);
|
|
return /*#__PURE__*/_jsx(React.Fragment, {
|
|
children: /*#__PURE__*/React.cloneElement(children, childrenProps)
|
|
});
|
|
}
|
|
process.env.NODE_ENV !== "production" ? ClickAwayListener.propTypes /* remove-proptypes */ = {
|
|
// ┌────────────────────────────── Warning ──────────────────────────────┐
|
|
// │ These PropTypes are generated from the TypeScript type definitions. │
|
|
// │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
|
|
// └─────────────────────────────────────────────────────────────────────┘
|
|
/**
|
|
* The wrapped element.
|
|
*/
|
|
children: elementAcceptingRef.isRequired,
|
|
/**
|
|
* If `true`, the React tree is ignored and only the DOM tree is considered.
|
|
* This prop changes how portaled elements are handled.
|
|
* @default false
|
|
*/
|
|
disableReactTree: PropTypes.bool,
|
|
/**
|
|
* The mouse event to listen to. You can disable the listener by providing `false`.
|
|
* @default 'onClick'
|
|
*/
|
|
mouseEvent: PropTypes.oneOf(['onClick', 'onMouseDown', 'onMouseUp', 'onPointerDown', 'onPointerUp', false]),
|
|
/**
|
|
* Callback fired when a "click away" event is detected.
|
|
*/
|
|
onClickAway: PropTypes.func.isRequired,
|
|
/**
|
|
* The touch event to listen to. You can disable the listener by providing `false`.
|
|
* @default 'onTouchEnd'
|
|
*/
|
|
touchEvent: PropTypes.oneOf(['onTouchEnd', 'onTouchStart', false])
|
|
} : void 0;
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
// eslint-disable-next-line
|
|
ClickAwayListener['propTypes' + ''] = exactProp(ClickAwayListener.propTypes);
|
|
}
|
|
export { ClickAwayListener }; |