reblend-ui
Version:
Utilities for creating robust overlay components
110 lines (106 loc) • 5.17 kB
JavaScript
;
exports.__esModule = true;
exports.getRefTarget = exports.default = void 0;
var _contains = require("dom-helpers/contains");
var _listen = require("dom-helpers/listen");
var _ownerDocument = require("dom-helpers/ownerDocument");
var _reblendjs = require("reblendjs");
var _reblendHooks = require("reblend-hooks");
var _warning = require("warning");
const noop = () => {};
function isLeftClickEvent(event) {
return event.button === 0;
}
function isModifiedEvent(event) {
return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
}
const getRefTarget = ref => ref && ('current' in ref ? ref.current : ref);
exports.getRefTarget = getRefTarget;
const InitialTriggerEvents = {
click: 'mousedown',
mouseup: 'mousedown',
pointerup: 'pointerdown'
};
/**
* The `useClickOutside` hook registers your callback on the document that fires
* when a pointer event is registered outside of the provided ref or element.
*
* @param {Ref<HTMLElement>| HTMLElement} ref The element boundary
* @param {function} onClickOutside
* @param {object=} options
* @param {boolean=} options.disabled
* @param {string=} options.clickTrigger The DOM event name (click, mousedown, etc) to attach listeners on
*/
function useClickOutside(ref, onClickOutside = noop, {
disabled,
clickTrigger = 'click'
} = {}) {
const preventMouseClickOutsideRef = _reblendjs.useRef.bind(this)(false);
this.state.preventMouseClickOutsideRef = preventMouseClickOutsideRef;
const waitingForTrigger = _reblendjs.useRef.bind(this)(false);
this.state.waitingForTrigger = waitingForTrigger;
const handleMouseCapture = _reblendjs.useCallback.bind(this)(e => {
const currentTarget = getRefTarget(ref);
(0, _warning.default)(!!currentTarget, 'ClickOutside captured a close event but does not have a ref to compare it to. ' + 'useClickOutside(), should be passed a ref that resolves to a DOM node');
this.state.preventMouseClickOutsideRef.current = !currentTarget || isModifiedEvent(e) || !isLeftClickEvent(e) || !!(0, _contains.default)(currentTarget, e.target) || this.state.waitingForTrigger.current;
this.state.waitingForTrigger.current = false;
}, [ref]);
this.state.handleMouseCapture = handleMouseCapture;
const handleInitialMouse = _reblendHooks.useEventCallback.bind(this)(e => {
const currentTarget = getRefTarget(ref);
if (currentTarget && (0, _contains.default)(currentTarget, e.target)) {
this.state.waitingForTrigger.current = true;
} else {
// When clicking on scrollbars within current target, click events are not triggered, so this ref
// is never reset inside `handleMouseCapture`. This would cause a bug where it requires 2 clicks
// to close the overlay.
this.state.waitingForTrigger.current = false;
}
});
this.state.handleInitialMouse = handleInitialMouse;
const handleMouse = _reblendHooks.useEventCallback.bind(this)(e => {
if (!this.state.preventMouseClickOutsideRef.current) {
onClickOutside(e);
}
});
this.state.handleMouse = handleMouse;
_reblendjs.useEffect.bind(this)(() => {
if (disabled || ref == null) return undefined;
const doc = (0, _ownerDocument.default)(getRefTarget(ref));
const ownerWindow = doc.defaultView || window;
// Store the current event to avoid triggering handlers immediately
// For things rendered in an iframe, the event might originate on the parent window
// so we should fall back to that global event if the local one doesn't exist
// https://github.com/facebook/react/issues/20074
let currentEvent = ownerWindow.event ?? ownerWindow.parent?.event;
let removeInitialTriggerListener = null;
if (InitialTriggerEvents[clickTrigger]) {
removeInitialTriggerListener = (0, _listen.default)(doc, InitialTriggerEvents[clickTrigger], this.state.handleInitialMouse, true);
}
// Use capture for this listener so it fires before React's listener, to
// avoid false positives in the contains() check below if the target DOM
// element is removed in the React mouse callback.
const removeMouseCaptureListener = (0, _listen.default)(doc, clickTrigger, this.state.handleMouseCapture, true);
const removeMouseListener = (0, _listen.default)(doc, clickTrigger, e => {
// skip if this event is the same as the one running when we added the handlers
if (e === currentEvent) {
currentEvent = undefined;
return;
}
this.state.handleMouse(e);
});
let mobileSafariHackListeners = [];
if ('ontouchstart' in doc.documentElement) {
mobileSafariHackListeners = [].slice.call(doc.body.children).map(el => (0, _listen.default)(el, 'mousemove', noop));
}
return () => {
removeInitialTriggerListener?.();
removeMouseCaptureListener();
removeMouseListener();
mobileSafariHackListeners.forEach(remove => remove());
};
}, (() => [ref, disabled, clickTrigger, this.state.handleMouseCapture, this.state.handleInitialMouse, this.state.handleMouse]).bind(this));
return;
}
/* @Reblend: Transformed from function to class */
var _default = exports.default = useClickOutside;