import React, { createContext, CSSProperties, forwardRef, ReactElement, useContext, useMemo } from 'react' import RcDropdown from 'rc-dropdown' import type { DropdownProps as RcDropdownProps } from 'rc-dropdown/lib/Dropdown' import { usePrefix } from '../ConfigProvider' import { useGetPopupContainer } from '../PopupContainerProvider' import { useValue } from '../utils/hooks' export type DropdownAlign = { offset?: number[] } export type DropdownProps = { /** 是否可见,若指定,则 Dropdown 表现为受控的状态 */ visible?: boolean /** 响应可见状态变更的回调函数 */ onVisibleChange?: (visible: boolean) => void /** * 触发可见状态变更的行为 * @default "hover" */ trigger?: 'click' | 'hover' /** * 下拉框相对主体内容的放置位置 * @default "bottomLeft" */ placement?: 'topLeft' | 'top' | 'topRight' | 'bottomLeft' | 'bottom' | 'bottomRight' | 'leftTop' | 'left' | 'leftBottom' | 'rightTop' | 'right' | 'rightBottom' /** 对下拉框位置的精细控制 */ align?: DropdownAlign /** 下拉浮层的内容 */ overlay?: (() => ReactElement) | ReactElement /** 自定义 overlay 样式名 */ overlayClassName?: string /** 自定义 overlay 样式 */ overlayStyle?: CSSProperties /** 主体内容 */ children: ReactElement } export interface DropdownRef { /** 校准下拉框位置,如果主体内容的尺寸、位置发生了不预期的变化,可能需要通过调用该方法来确保下拉框位置是正确的 */ forcePopupAlign: () => void } const autoAdjustOverflow = { adjustX: 1, adjustY: 1 } // 垂直方向上的默认偏移,单位 px const defaultOffsetY = 8 const topOffset = [0, -defaultOffsetY] const bottomOffset = [0, defaultOffsetY] const leftOffset = [-defaultOffsetY, 0] const rightOffset = [defaultOffsetY, 0] const targetOffset = [0, 0] // 所有支持的 placement 配置 // 在 rc-dropdown 实现的基础上调整了 placement 名 & offset 值,其他保持一致 const placements = { topLeft: { points: ['bl', 'tl'], overflow: autoAdjustOverflow, offset: topOffset, targetOffset }, top: { points: ['bc', 'tc'], overflow: autoAdjustOverflow, offset: topOffset, targetOffset }, topRight: { points: ['br', 'tr'], overflow: autoAdjustOverflow, offset: topOffset, targetOffset }, bottomLeft: { points: ['tl', 'bl'], overflow: autoAdjustOverflow, offset: bottomOffset, targetOffset }, bottom: { points: ['tc', 'bc'], overflow: autoAdjustOverflow, offset: bottomOffset, targetOffset }, bottomRight: { points: ['tr', 'br'], overflow: autoAdjustOverflow, offset: bottomOffset, targetOffset }, leftTop: { points: ['tr', 'tl'], overflow: autoAdjustOverflow, offset: leftOffset, targetOffset }, left: { points: ['cr', 'cl'], overflow: autoAdjustOverflow, offset: leftOffset, targetOffset }, leftBottom: { points: ['br', 'bl'], overflow: autoAdjustOverflow, offset: leftOffset, targetOffset }, rightTop: { points: ['tl', 'tr'], overflow: autoAdjustOverflow, offset: rightOffset, targetOffset }, right: { points: ['cl', 'cr'], overflow: autoAdjustOverflow, offset: rightOffset, targetOffset }, rightBottom: { points: ['bl', 'br'], overflow: autoAdjustOverflow, offset: rightOffset, targetOffset } } /** 提供给 `Dropdown` overlay 中内容的信息 */ export interface DropdownCtxValue { /** 设置可见状态 */ setVisible: (visible: boolean) => void } const dropdownCtx = createContext(null) /** 供 `Dropdown` overlay 内容与 `Dropdown` 进行交互,若当前组件未被 `Dropdown` 包裹,函数返回 `null` */ export function useDropdownCtx(): DropdownCtxValue | null { return useContext(dropdownCtx) } const verticalPlacements = ['topLeft', 'top', 'topRight', 'bottomLeft', 'bottom', 'bottomRight'] export default forwardRef(function Dropdown({ trigger = 'hover', placement = 'bottomLeft', visible: visibleFromProps, onVisibleChange, ...otherProps }: DropdownProps, ref) { const dropdownClassName = usePrefix('dropdown') const getPopupContainer = useGetPopupContainer() const { value: visible, setValue: setVisible } = useValue({ valueFromProps: visibleFromProps, defaultValue: false, onChange: onVisibleChange }) const ctrlCtxValue = useMemo(() => ({ setVisible }), [setVisible]) const extraProps: Partial = {} // 对于垂直方向的下拉浮层,兼容原行为,走默认逻辑(无 alignPoint 时设置最小宽度); // 对于非垂直方向的下拉浮层,不去设置浮层的最小宽度 if (!verticalPlacements.includes(placement)) { extraProps.minOverlayWidthMatchTrigger = false } return ( ) })