This commit is contained in:
2026-05-25 17:08:18 +08:00
parent df501f6151
commit f1259b71b5
9178 changed files with 1626125 additions and 0 deletions
@@ -0,0 +1,24 @@
import React from 'react';
import type { CSSMotionProps } from '@rc-component/motion';
import type { AlignType, ArrowPos } from '../interface';
export interface UniqueContainerProps {
prefixCls: string;
isMobile: boolean;
ready: boolean;
open: boolean;
align: AlignType;
offsetR: number;
offsetB: number;
offsetX: number;
offsetY: number;
arrowPos?: ArrowPos;
popupSize?: {
width: number;
height: number;
};
motion?: CSSMotionProps;
uniqueContainerClassName?: string;
uniqueContainerStyle?: React.CSSProperties;
}
declare const UniqueContainer: (props: UniqueContainerProps) => React.JSX.Element;
export default UniqueContainer;
@@ -0,0 +1,76 @@
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
import React from 'react';
import useOffsetStyle from "../hooks/useOffsetStyle";
import { clsx } from 'clsx';
import CSSMotion from '@rc-component/motion';
const UniqueContainer = props => {
const {
prefixCls,
isMobile,
ready,
open,
align,
offsetR,
offsetB,
offsetX,
offsetY,
arrowPos,
popupSize,
motion,
uniqueContainerClassName,
uniqueContainerStyle
} = props;
const containerCls = `${prefixCls}-unique-container`;
const [motionVisible, setMotionVisible] = React.useState(false);
// ========================= Styles =========================
const offsetStyle = useOffsetStyle(isMobile, ready, open, align, offsetR, offsetB, offsetX, offsetY);
// Cache for offsetStyle when ready is true
const cachedOffsetStyleRef = React.useRef(offsetStyle);
// Update cached offset style when ready is true
if (ready) {
cachedOffsetStyleRef.current = offsetStyle;
}
// Apply popup size if available
const sizeStyle = {};
if (popupSize) {
sizeStyle.width = popupSize.width;
sizeStyle.height = popupSize.height;
}
// ========================= Render =========================
return /*#__PURE__*/React.createElement(CSSMotion, _extends({
motionAppear: true,
motionEnter: true,
motionLeave: true,
removeOnLeave: false,
leavedClassName: `${containerCls}-hidden`
}, motion, {
visible: open,
onVisibleChanged: nextVisible => {
setMotionVisible(nextVisible);
}
}), ({
className: motionClassName,
style: motionStyle
}) => {
const cls = clsx(containerCls, motionClassName, uniqueContainerClassName, {
[`${containerCls}-visible`]: motionVisible
});
return /*#__PURE__*/React.createElement("div", {
className: cls,
style: {
'--arrow-x': `${arrowPos?.x || 0}px`,
'--arrow-y': `${arrowPos?.y || 0}px`,
...cachedOffsetStyleRef.current,
...sizeStyle,
...motionStyle,
...uniqueContainerStyle
}
});
});
};
export default UniqueContainer;
@@ -0,0 +1,9 @@
import * as React from 'react';
import { type UniqueShowOptions } from '../context';
export interface UniqueProviderProps {
children: React.ReactNode;
/** Additional handle options data to do the customize info */
postTriggerProps?: (options: UniqueShowOptions) => UniqueShowOptions;
}
declare const UniqueProvider: ({ children, postTriggerProps, }: UniqueProviderProps) => React.JSX.Element;
export default UniqueProvider;
@@ -0,0 +1,175 @@
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,
onEsc: mergedOptions.onEsc,
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;
@@ -0,0 +1,16 @@
import type { UniqueShowOptions } from '../context';
/**
* Control the state of popup bind target:
* 1. When set `target`. Do show the popup.
* 2. When `target` is removed. Do hide the popup.
* 3. When `target` change to another one:
* a. We wait motion finish of previous popup.
* b. Then we set new target and show the popup.
* 4. During appear/enter animation, cache new options and apply after animation completes.
*/
export default function useTargetState(): [
trigger: (options: UniqueShowOptions | false) => void,
open: boolean,
cacheOptions: UniqueShowOptions | null,
onVisibleChanged: (visible: boolean) => void
];
@@ -0,0 +1,55 @@
import React from 'react';
import { useEvent } from '@rc-component/util';
/**
* Control the state of popup bind target:
* 1. When set `target`. Do show the popup.
* 2. When `target` is removed. Do hide the popup.
* 3. When `target` change to another one:
* a. We wait motion finish of previous popup.
* b. Then we set new target and show the popup.
* 4. During appear/enter animation, cache new options and apply after animation completes.
*/
export default function useTargetState() {
const [options, setOptions] = React.useState(null);
const [open, setOpen] = React.useState(false);
const [isAnimating, setIsAnimating] = React.useState(false);
const pendingOptionsRef = React.useRef(null);
const trigger = useEvent(nextOptions => {
if (nextOptions === false) {
// Clear pending options when hiding
pendingOptionsRef.current = null;
setOpen(false);
} else {
if (isAnimating && open) {
// If animating (appear or enter), cache new options
pendingOptionsRef.current = nextOptions;
} else {
setOpen(true);
// Set new options
setOptions(nextOptions);
pendingOptionsRef.current = null;
// Only mark as animating when transitioning from closed to open
if (!open) {
setIsAnimating(true);
}
}
}
});
const onVisibleChanged = useEvent(visible => {
if (visible) {
// Animation enter completed, check if there are pending options
setIsAnimating(false);
if (pendingOptionsRef.current) {
// Apply pending options
setOptions(pendingOptionsRef.current);
pendingOptionsRef.current = null;
}
} else {
// Animation leave completed
setIsAnimating(false);
pendingOptionsRef.current = null;
}
});
return [trigger, open, options, onVisibleChanged];
}