UNPKG

@appbuckets/react-ui-core

Version:

Core utilities built for AppBuckets React UI Framework

425 lines (422 loc) 12.2 kB
import { __read, __spreadArray } from 'tslib'; import * as React from 'react'; import useAutoControlledValue from '../useAutoControlledValue/index.js'; import useDOMElementEvent from '../useDOMElementEvent/index.js'; import Ref from '../Ref/Ref.js'; import { handleRef } from '../utils/refUtils.js'; import { doesNodeContainClick } from '../utils/doesNodeContainClick.js'; import PortalInner from './PortalInner.js'; var Portal = function (props) { var _a, _b, _c, _d, _e; var children = props.children, closeOnDocumentClick = props.closeOnDocumentClick, closeOnEscape = props.closeOnEscape, closeOnPortalMouseLeave = props.closeOnPortalMouseLeave, closeOnTriggerBlur = props.closeOnTriggerBlur, closeOnTriggerClick = props.closeOnTriggerClick, closeOnTriggerMouseLeave = props.closeOnTriggerMouseLeave, userDefinedDefaultOpen = props.defaultOpen, mountNode = props.mountNode, mouseEnterDelay = props.mouseEnterDelay, mouseLeaveDelay = props.mouseLeaveDelay, userDefinedOpen = props.open, openOnTriggerClick = props.openOnTriggerClick, openOnTriggerFocus = props.openOnTriggerFocus, openOnTriggerMouseEnter = props.openOnTriggerMouseEnter, trigger = props.trigger, userDefinedTriggerRef = props.triggerRef, userDefinedOnCloseHandler = props.onClose, userDefinedOnMountHandler = props.onMount, userDefinedOnOpenHandler = props.onOpen, userDefinedOnUnmountHandler = props.onUnmount; // ---- // AutoControlled Props // ---- var _f = __read( useAutoControlledValue(false, { defaultProp: userDefinedDefaultOpen, prop: userDefinedOpen, }), 2 ), open = _f[0], trySetOpen = _f[1]; // ---- // DOM Ref // ---- var contentRef = React.useRef(); var triggerRef = React.useRef(); // ---- // Internal Variables // ---- var latestDocumentMouseEvent = React.useRef(null); var mouseEnterTimer = React.useRef(null); var mouseLeaveTimer = React.useRef(null); // ---- // Lifecycle event to cancel timer // ---- var clearMouseEnterTimer = React.useCallback(function () { if (mouseEnterTimer.current) { clearTimeout(mouseEnterTimer.current); mouseEnterTimer.current = null; } }, []); var clearMouseLeaveTimer = React.useCallback(function () { if (mouseEnterTimer.current) { clearTimeout(mouseEnterTimer.current); mouseEnterTimer.current = null; } }, []); React.useEffect( function () { return function () { clearMouseEnterTimer(); clearMouseLeaveTimer(); }; }, [clearMouseEnterTimer, clearMouseLeaveTimer] ); // ---- // Portal Control // ---- var openPortal = React.useCallback( function (e) { /** Avoid event propagation */ e.stopPropagation(); if (typeof userDefinedOnOpenHandler === 'function') { userDefinedOnOpenHandler(e); } trySetOpen(true); }, [userDefinedOnOpenHandler, trySetOpen] ); var openPortalWithTimeout = React.useCallback( function (e) { return setTimeout(function () { return openPortal(e); }, mouseEnterDelay); }, [openPortal, mouseEnterDelay] ); var closePortal = React.useCallback( function (e) { /** Avoid event propagation */ e.stopPropagation(); if (typeof userDefinedOnCloseHandler === 'function') { userDefinedOnCloseHandler(e); } trySetOpen(false); }, [userDefinedOnCloseHandler, trySetOpen] ); var closePortalWithTimeout = React.useCallback( function (e) { return setTimeout( function () { return closePortal(e); }, mouseLeaveDelay !== null && mouseLeaveDelay !== void 0 ? mouseLeaveDelay : 0 ); }, [closePortal, mouseLeaveDelay] ); // ---- // Trigger Handler // ---- var handleTriggerRef = function (component) { triggerRef.current = component; handleRef(userDefinedTriggerRef, component); }; var triggerOnBlurHandler = (_a = trigger === null || trigger === void 0 ? void 0 : trigger.props) === null || _a === void 0 ? void 0 : _a.onBlur; var handleTriggerBlur = React.useCallback( function (e) { var _a; var rest = []; for (var _i = 1; _i < arguments.length; _i++) { rest[_i - 1] = arguments[_i]; } /** Invoke original trigger event handler */ if (typeof triggerOnBlurHandler === 'function') { triggerOnBlurHandler.apply( void 0, __spreadArray([e], __read(rest), false) ); } /** Do not close if focus is given to the portal */ var target = e.relatedTarget || document.activeElement; var didFocusPortal = (_a = contentRef.current) === null || _a === void 0 ? void 0 : _a.contains(target); if (!closeOnTriggerBlur || didFocusPortal) { return; } closePortal(e); }, [triggerOnBlurHandler, closeOnTriggerBlur, closePortal] ); var triggerOnClickHandler = (_b = trigger === null || trigger === void 0 ? void 0 : trigger.props) === null || _b === void 0 ? void 0 : _b.onClick; var handleTriggerClick = React.useCallback( function (e) { var rest = []; for (var _i = 1; _i < arguments.length; _i++) { rest[_i - 1] = arguments[_i]; } /** Invoke original trigger event handler */ if (typeof triggerOnClickHandler === 'function') { triggerOnClickHandler.apply( void 0, __spreadArray([e], __read(rest), false) ); } /** Toggle the Portal */ if (open && closeOnTriggerClick) { closePortal(e); } else if (!open && openOnTriggerClick) { openPortal(e); } }, [ closeOnTriggerClick, closePortal, open, openOnTriggerClick, openPortal, triggerOnClickHandler, ] ); var triggerOnFocusHandler = (_c = trigger === null || trigger === void 0 ? void 0 : trigger.props) === null || _c === void 0 ? void 0 : _c.onFocus; var handleTriggerFocus = React.useCallback( function (e) { var rest = []; for (var _i = 1; _i < arguments.length; _i++) { rest[_i - 1] = arguments[_i]; } /** Invoke original trigger event handler */ if (typeof triggerOnFocusHandler === 'function') { triggerOnFocusHandler.apply( void 0, __spreadArray([e], __read(rest), false) ); } if (!openOnTriggerFocus) { return; } openPortal(e); }, [openOnTriggerFocus, openPortal, triggerOnFocusHandler] ); var triggerOnMouseEnterHandler = (_d = trigger === null || trigger === void 0 ? void 0 : trigger.props) === null || _d === void 0 ? void 0 : _d.onMouseEnter; var handleTriggerMouseEnter = React.useCallback( function (e) { var rest = []; for (var _i = 1; _i < arguments.length; _i++) { rest[_i - 1] = arguments[_i]; } /** Remove mouse leave timer */ clearMouseLeaveTimer(); /** Invoke original trigger event handler */ if (typeof triggerOnMouseEnterHandler === 'function') { triggerOnMouseEnterHandler.apply( void 0, __spreadArray([e], __read(rest), false) ); } if (!openOnTriggerMouseEnter) { return; } mouseEnterTimer.current = openPortalWithTimeout(e); }, [ clearMouseLeaveTimer, openOnTriggerMouseEnter, openPortalWithTimeout, triggerOnMouseEnterHandler, ] ); var triggerOnMouseLeaveHandler = (_e = trigger === null || trigger === void 0 ? void 0 : trigger.props) === null || _e === void 0 ? void 0 : _e.onMouseLeave; var handleTriggerMouseLeave = React.useCallback( function (e) { var rest = []; for (var _i = 1; _i < arguments.length; _i++) { rest[_i - 1] = arguments[_i]; } /** Remove mouse enter timer */ clearMouseEnterTimer(); /** Invoke original trigger event handler */ if (typeof triggerOnMouseLeaveHandler === 'function') { triggerOnMouseLeaveHandler.apply( void 0, __spreadArray([e], __read(rest), false) ); } if (!closeOnTriggerMouseLeave) { return; } mouseLeaveTimer.current = closePortalWithTimeout(e); }, [ clearMouseEnterTimer, closeOnTriggerMouseLeave, closePortalWithTimeout, triggerOnMouseLeaveHandler, ] ); // ---- // Document and DOM Event // ---- var handleDocumentMouseDown = React.useCallback(function (e) { latestDocumentMouseEvent.current = e; }, []); useDOMElementEvent({ disabled: !closeOnDocumentClick, event: 'mousedown', callback: handleDocumentMouseDown, }); var handleDocumentClick = React.useCallback( function (e) { var currentMouseDownEvent = latestDocumentMouseEvent.current; latestDocumentMouseEvent.current = null; var triggerElement = triggerRef.current; var contentElement = contentRef.current; /** * Ignore the click if there's no Portal * or the event happened in trigger, or the event * is originated in Portal but ended outside, or the event * happened in the portal */ if ( !contentElement || doesNodeContainClick(triggerElement, e) || (currentMouseDownEvent && doesNodeContainClick(contentElement, currentMouseDownEvent)) || doesNodeContainClick(contentElement, e) ) { return; } if (closeOnDocumentClick) { closePortal(e); } }, [closeOnDocumentClick, closePortal] ); useDOMElementEvent({ disabled: !closeOnDocumentClick, event: 'click', callback: handleDocumentClick, }); var handleEscapeKey = React.useCallback( function (e) { if (!closeOnEscape) { return; } if (e.key === 'Escape') { closePortal(e); } }, [closeOnEscape, closePortal] ); useDOMElementEvent({ disabled: !closeOnEscape, event: 'keydown', callback: handleEscapeKey, }); // ---- // Portal Mouse Event Handler // ---- var handlePortalMouseEnter = React.useCallback( function () { if (!closeOnPortalMouseLeave) { return; } clearMouseLeaveTimer(); }, [closeOnPortalMouseLeave, clearMouseLeaveTimer] ); useDOMElementEvent({ disabled: !closeOnPortalMouseLeave, target: contentRef.current, event: 'mouseenter', callback: handlePortalMouseEnter, }); var handlePortalMouseLeave = React.useCallback( function (e) { if (!closeOnPortalMouseLeave) { return; } if (e.target !== contentRef.current) { return; } mouseLeaveTimer.current = closePortalWithTimeout(e); }, [closeOnPortalMouseLeave, closePortalWithTimeout] ); useDOMElementEvent({ disabled: !closeOnPortalMouseLeave, target: contentRef.current, event: 'mouseleave', callback: handlePortalMouseLeave, }); // ---- // Component Render // ---- return React.createElement( React.Fragment, null, open && React.createElement( PortalInner, { innerRef: contentRef, mountNode: mountNode, onMount: userDefinedOnMountHandler, onUnmount: userDefinedOnUnmountHandler, }, children ), trigger && React.createElement( Ref, { innerRef: handleTriggerRef }, React.cloneElement(trigger, { onBlur: handleTriggerBlur, onClick: openOnTriggerClick || closeOnTriggerClick || triggerOnClickHandler ? handleTriggerClick : undefined, onFocus: handleTriggerFocus, onMouseEnter: handleTriggerMouseEnter, onMouseLeave: handleTriggerMouseLeave, }) ) ); }; Portal.displayName = 'Portal'; Portal.defaultProps = { closeOnDocumentClick: true, closeOnEscape: true, openOnTriggerClick: true, }; export { Portal as default };