UNPKG

reblend-ui

Version:

Utilities for creating robust overlay components

110 lines (106 loc) 5.17 kB
"use strict"; 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;