import React, { createContext, CSSProperties, HTMLAttributes, ReactNode, useCallback, useContext, useMemo, useRef } from 'react' import RcDrawer from 'rc-drawer' import cns from 'classnames' import { useLangData, usePrefix, useMask } from '../ConfigProvider' import { concat, fillRef } from '../utils' import { isPromise } from '../utils/promise' import { useSafeState, useValue } from '../utils/hooks' import { CloseIcon } from '../icons' import Button, { ButtonProps, ButtonType } from '../Button' import Mask from '../internal/Mask' import { PopupContainerProvider } from '../PopupContainerProvider' import { WithRef } from '../utils/type' import { useLayer } from '../internal/Mask/layer' export interface Props { /** 是否打开 */ visible?: boolean title: ReactNode /** * 宽度 单位 px * * @default 480 */ width?: number /** * 自定义底部按钮区域 */ footer?: ReactNode /** 点击确定回调 */ onOk?: () => void | Promise /** 点击右上角叉、取消按钮、遮罩层或键盘按下 Esc 时的回调 */ onCancel?: () => void /** * 不可见时是否自动销毁 * * @default false */ autoDestroy?: boolean /** 自定义样式名 */ className?: string /** 自定义样式 */ style?: CSSProperties /** 抽屉内容 */ children?: ReactNode /** * 根元素的 html 属性 */ rootHtmlProps?: WithRef, 'onChange'>, HTMLDivElement> /** * 遮罩的 html 属性 */ maskHtmlProps?: WithRef, HTMLDivElement> /** * 点击 mask 是否允许关闭对话框;默认使用全局配置 mask.drawer.clickable (true) */ maskClickable?: boolean } export interface InternalDrawerProps extends Props { /** 自定义 drawer render 逻辑 */ renderDrawer?: (node: ReactNode) => ReactNode } export interface DrawerFooterCtxValue { /** 点击确定回调 */ onOk?: () => Promise | void /** 点击取消回调 */ onCancel?: () => void } // 用于 Drawer 与 DrawerFooter 间交互的 context const drawerFooterCtx = createContext({}) /** 获取用于自定义 Footer 实现的上下文信息 */ export function useDrawerFooterCtx() { return useContext(drawerFooterCtx) } /** 对(icecream)内的 Drawer 实现,相比对外的 Drawer 提供更高的可定制化程度 */ export function InternalDrawer(props: InternalDrawerProps) { const { title, width = 480, visible: visibleFromProps, footer = , onOk, onCancel, children, rootHtmlProps, className, style, maskHtmlProps, maskClickable: maskClickableFromProps, autoDestroy = false, renderDrawer } = props const maskConfig = useMask().drawer const { value: visible, setValue: setVisible } = useValue( { valueFromProps: visibleFromProps, defaultValue: true } ) const maskClickable = maskClickableFromProps ?? maskConfig.clickable const prefix = usePrefix('drawer') const contentPrefix = concat(prefix, 'content') const titlePrefix = concat(contentPrefix, 'title') const bodyPrefix = concat(contentPrefix, 'body') const footerPrefix = concat(contentPrefix, 'footer') const titleRef = useRef(null) const bodyRef = useRef(null) const footerRef = useRef(null) const { roleLevel, topmostRoleLevel, zIndex } = useLayer(visible, 'Drawer') const handleOk = useCallback(async () => { await onOk?.() setVisible(false) }, [onOk, setVisible]) const handleCancel = useCallback(() => { onCancel?.() setVisible(false) }, [onCancel, setVisible]) const handleMaskClick = useCallback((e: React.MouseEvent) => { maskHtmlProps?.onClick?.(e) if (e.defaultPrevented) return if (!maskClickable) return handleCancel() }, [handleCancel, maskHtmlProps, maskClickable]) const footerCtxValue = useMemo(() => ({ onOk: handleOk, onCancel: handleCancel }), [handleOk, handleCancel]) const drawerContent = ( <> { title && (
{title}
) }
{children}
{ footer && (
{footer}
) } ) const translateX = roleLevel > 0 ? (roleLevel - topmostRoleLevel) * translateXEveryLevel : 0 const computedStyle: CSSProperties = { zIndex: zIndex + 1, transform: `translateX(${translateX}px)` } return ( fillRef(rootHtmlProps?.ref || null, r?.dom)} getContainer={() => document.body} open={visible} showMask={false} // showMask 设置为 false 后还需要设置这个才能允许点击到 Mask 不关闭 maskClosable={maskClickable} prefixCls={prefix} level={null} handler={false} onClose={handleCancel} keyboard={false} placement="right" width={width} className={cns(className, rootHtmlProps?.className)} style={{ ...computedStyle, ...style, ...rootHtmlProps?.style }} > {/* 这个位置其实不够好,`renderDrawer` 的生效理应更靠外,但 `RcDrawer` 没有像 `RcDialog` 那样 提供 prop `modalRender`,所以先包在这里将就一下了;后续如果有更好的办法解决这个问题可以调整实现, `renderDrawer` 目前只用于 icecream 内部,因此不用担心产生 breaking change,主要对应检查下内部 用到的地方即可 */} {renderDrawer != null ? renderDrawer(drawerContent) : drawerContent} ) } export default function Drawer(props: Props) { return } // 每相隔 1 层需要的位移量(px) const translateXEveryLevel = 120 export interface DrawerFooterProps { /** 确定按钮文本 */ okText?: string /** 取消按钮文本 */ cancelText?: string /** 确定按钮类型 */ okType?: ButtonType /** 取消按钮类型 */ cancelType?: ButtonType /** 确定按钮的 loading 状态 */ okLoading?: boolean /** 确定按钮的 props */ okButtonProps?: ButtonProps /** 取消按钮的 props */ cancelButtonProps?: ButtonProps } export function DrawerFooter({ okText, cancelText, okType = 'primary', cancelType = 'default', okLoading = false, okButtonProps = {}, cancelButtonProps = {} }: DrawerFooterProps) { const langData = useLangData('modal') const { onOk, onCancel } = useDrawerFooterCtx() const [loading, setLoading] = useSafeState(false) const handleOk = useCallback(async () => { const returns = onOk?.() if (isPromise(returns)) { setLoading(true) try { await returns } finally { setLoading(false) } } }, [onOk, setLoading]) return ( <> ) }