diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index 2b56b4dc..8e3f36fd 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -208,6 +208,14 @@ export interface BaseSelectProps popupRender?: (menu: React.ReactElement) => React.ReactElement; popupAlign?: AlignType; + /** + * Get dynamic popup offset based on input element. + * Useful for positioning dropdown relative to cursor in multi-line textarea. + * @param inputElement - The input/textarea element + * @returns [x, y] offset array or null to use default positioning + */ + getPopupOffset?: (inputElement: HTMLElement) => [number, number] | null; + placement?: Placement; builtinPlacements?: BuildInPlacements; getPopupContainer?: RenderDOMFunc; @@ -299,6 +307,7 @@ const BaseSelect = React.forwardRef((props, ref) popupMatchSelectWidth, popupRender, popupAlign, + getPopupOffset, placement, builtinPlacements, getPopupContainer, @@ -789,6 +798,7 @@ const BaseSelect = React.forwardRef((props, ref) popupMatchSelectWidth={popupMatchSelectWidth} popupRender={popupRender} popupAlign={popupAlign} + getPopupOffset={getPopupOffset} placement={placement} builtinPlacements={builtinPlacements} getPopupContainer={getPopupContainer} diff --git a/src/SelectTrigger.tsx b/src/SelectTrigger.tsx index bbf719fa..fc10ce3b 100644 --- a/src/SelectTrigger.tsx +++ b/src/SelectTrigger.tsx @@ -71,6 +71,8 @@ export interface SelectTriggerProps { popupRender?: (menu: React.ReactElement) => React.ReactElement; getPopupContainer?: RenderDOMFunc; popupAlign: AlignType; + /** Get dynamic popup offset based on input element for textarea cursor positioning */ + getPopupOffset?: (inputElement: HTMLElement) => [number, number] | null; empty: boolean; onPopupVisibleChange?: (visible: boolean) => void; @@ -100,6 +102,7 @@ const SelectTrigger: React.ForwardRefRenderFunction builtinPlacements || getBuiltInPlacements(popupMatchSelectWidth), - [builtinPlacements, popupMatchSelectWidth], - ); + const mergedBuiltinPlacements = React.useMemo(() => { + const base = builtinPlacements || getBuiltInPlacements(popupMatchSelectWidth); + + if (!getPopupOffset) { + return base; + } + + // Apply dynamic offset to placements + const dynamicPlacements: Record = {}; + Object.keys(base).forEach((key) => { + const placement = base[key]; + dynamicPlacements[key] = { + ...placement, + offset: (node: HTMLElement) => { + // Get the offset from getPopupOffset callback + const customOffset = getPopupOffset(node); + if (customOffset) { + return customOffset; + } + // Default offset from base placement + return placement.offset as [number, number]; + }, + }; + }); + + return dynamicPlacements; + }, [builtinPlacements, popupMatchSelectWidth, getPopupOffset]); // ===================== Motion ====================== const mergedTransitionName = animation ? `${popupPrefixCls}-${animation}` : transitionName;