@appbuckets/react-ui-core
Version:
Core utilities built for AppBuckets React UI Framework
425 lines (422 loc) • 12.2 kB
JavaScript
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 };