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 && (
)
}
{
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 (
<>
>
)
}