@rc-component/trigger
Version:
base abstract trigger component for react
174 lines (163 loc) • 6.06 kB
JavaScript
import * as React from 'react';
import Portal from '@rc-component/portal';
import TriggerContext, { UniqueContext } from "../context";
import useDelay from "../hooks/useDelay";
import useAlign from "../hooks/useAlign";
import Popup from "../Popup";
import { useEvent } from '@rc-component/util';
import useTargetState from "./useTargetState";
import { isDOM } from "@rc-component/util/es/Dom/findDOMNode";
import UniqueContainer from "./UniqueContainer";
import { clsx } from 'clsx';
import { getAlignPopupClassName } from "../util";
const UniqueProvider = ({
children,
postTriggerProps
}) => {
const [trigger, open, options, onTargetVisibleChanged] = useTargetState();
// ========================== Options ===========================
const mergedOptions = React.useMemo(() => {
if (!options || !postTriggerProps) {
return options;
}
return postTriggerProps(options);
}, [options, postTriggerProps]);
// =========================== Popup ============================
const [popupEle, setPopupEle] = React.useState(null);
const [popupSize, setPopupSize] = React.useState(null);
// Used for forwardRef popup. Not use internal
const externalPopupRef = React.useRef(null);
const setPopupRef = useEvent(node => {
externalPopupRef.current = node;
if (isDOM(node) && popupEle !== node) {
setPopupEle(node);
}
});
// ========================== Register ==========================
// Store the isOpen function from the latest show call
const isOpenRef = React.useRef(null);
const delayInvoke = useDelay();
const show = useEvent((showOptions, isOpen) => {
// Store the isOpen function for later use in hide
isOpenRef.current = isOpen;
delayInvoke(() => {
trigger(showOptions);
}, showOptions.delay);
});
const hide = delay => {
delayInvoke(() => {
// Check if we should still hide by calling the isOpen function
// If isOpen returns true, it means another trigger wants to keep it open
if (isOpenRef.current?.()) {
return; // Don't hide if something else wants it open
}
trigger(false);
// Don't clear target, currentNode, options immediately, wait until animation completes
}, delay);
};
// Callback after animation completes
const onVisibleChanged = useEvent(visible => {
// Call useTargetState callback to handle animation state
onTargetVisibleChanged(visible);
});
// =========================== Align ============================
const [ready, offsetX, offsetY, offsetR, offsetB, arrowX, arrowY,
// scaleX - not used in UniqueProvider
,,
// scaleY - not used in UniqueProvider
alignInfo, onAlign] = useAlign(open, popupEle, mergedOptions?.target, mergedOptions?.popupPlacement, mergedOptions?.builtinPlacements || {}, mergedOptions?.popupAlign, undefined,
// onPopupAlign
false // isMobile
);
const alignedClassName = React.useMemo(() => {
if (!mergedOptions) {
return '';
}
const baseClassName = getAlignPopupClassName(mergedOptions.builtinPlacements || {}, mergedOptions.prefixCls || '', alignInfo, false // alignPoint is false for UniqueProvider
);
return clsx(baseClassName, mergedOptions.getPopupClassNameFromAlign?.(alignInfo));
}, [alignInfo, mergedOptions?.getPopupClassNameFromAlign, mergedOptions?.builtinPlacements, mergedOptions?.prefixCls]);
const contextValue = React.useMemo(() => ({
show,
hide
}), []);
// =========================== Align ============================
React.useEffect(() => {
onAlign();
}, [mergedOptions?.target]);
// =========================== Motion ===========================
const onPrepare = useEvent(() => {
onAlign();
return Promise.resolve();
});
// ======================== Trigger Context =====================
const subPopupElements = React.useRef({});
const parentContext = React.useContext(TriggerContext);
const triggerContextValue = React.useMemo(() => ({
registerSubPopup: (id, subPopupEle) => {
subPopupElements.current[id] = subPopupEle;
parentContext?.registerSubPopup(id, subPopupEle);
}
}), [parentContext]);
// =========================== Render ===========================
const prefixCls = mergedOptions?.prefixCls;
return /*#__PURE__*/React.createElement(UniqueContext.Provider, {
value: contextValue
}, children, mergedOptions && /*#__PURE__*/React.createElement(TriggerContext.Provider, {
value: triggerContextValue
}, /*#__PURE__*/React.createElement(Popup, {
ref: setPopupRef,
portal: Portal,
prefixCls: prefixCls,
popup: mergedOptions.popup,
className: clsx(mergedOptions.popupClassName, alignedClassName, `${prefixCls}-unique-controlled`),
style: mergedOptions.popupStyle,
target: mergedOptions.target,
open: open,
keepDom: true,
fresh: true,
autoDestroy: false,
onVisibleChanged: onVisibleChanged,
ready: ready,
offsetX: offsetX,
offsetY: offsetY,
offsetR: offsetR,
offsetB: offsetB,
onAlign: onAlign,
onPrepare: onPrepare,
onResize: size => setPopupSize({
width: size.offsetWidth,
height: size.offsetHeight
}),
arrowPos: {
x: arrowX,
y: arrowY
},
align: alignInfo,
zIndex: mergedOptions.zIndex,
mask: mergedOptions.mask,
arrow: mergedOptions.arrow,
motion: mergedOptions.popupMotion,
maskMotion: mergedOptions.maskMotion,
getPopupContainer: mergedOptions.getPopupContainer
}, /*#__PURE__*/React.createElement(UniqueContainer, {
prefixCls: prefixCls,
isMobile: false,
ready: ready,
open: open,
align: alignInfo,
offsetR: offsetR,
offsetB: offsetB,
offsetX: offsetX,
offsetY: offsetY,
arrowPos: {
x: arrowX,
y: arrowY
},
popupSize: popupSize,
motion: mergedOptions.popupMotion,
uniqueContainerClassName: clsx(mergedOptions.uniqueContainerClassName, alignedClassName),
uniqueContainerStyle: mergedOptions.uniqueContainerStyle
}))));
};
export default UniqueProvider;