diff --git a/.eslintrc.js b/.eslintrc.js index 85c3e74be..379a93b10 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -99,7 +99,7 @@ module.exports = { 'comma-style': ['error', 'last'], 'comma-dangle': ['error', 'always-multiline'], 'eol-last': 'error', - indent: ['error', 2, { SwitchCase: 1 }], + indent: 'off', 'no-trailing-spaces': 'error', 'object-curly-spacing': ['error', 'always'], quotes: ['error', 'single', { avoidEscape: true, allowTemplateLiterals: true }], diff --git a/packages/components/_private/footer/src/Footer.tsx b/packages/components/_private/footer/src/Footer.tsx index 8ebcd2ebc..3a944dd5f 100644 --- a/packages/components/_private/footer/src/Footer.tsx +++ b/packages/components/_private/footer/src/Footer.tsx @@ -6,10 +6,12 @@ */ import type { FooterButtonProps } from './types' -import type { VNodeTypes } from 'vue' +import type { VNodeChild } from 'vue' import { computed, defineComponent, isVNode } from 'vue' +import { isFunction } from 'lodash-es' + import { IxButton } from '@idux/components/button' import { footerProps } from './types' @@ -54,9 +56,11 @@ export default defineComponent({ return undefined } - let children: VNodeTypes + let children: VNodeChild if (footerSlot) { children = footerSlot(props) + } else if (isFunction(footerProp)) { + children = footerProp() } else if (isVNode(footerProp)) { children = footerProp } else { @@ -70,7 +74,8 @@ export default defineComponent({ } children = buttonProps.map(item => { const { text, ...rest } = item - return {text} + const _text = isFunction(text) ? text() : text + return {_text} }) } diff --git a/packages/components/_private/footer/src/types.ts b/packages/components/_private/footer/src/types.ts index 8ed5aaa98..eee9760c9 100644 --- a/packages/components/_private/footer/src/types.ts +++ b/packages/components/_private/footer/src/types.ts @@ -7,12 +7,12 @@ import type { ExtractInnerPropTypes, ExtractPublicPropTypes, VKey } from '@idux/cdk/utils' import type { ButtonProps } from '@idux/components/button' -import type { ButtonHTMLAttributes, DefineComponent, HTMLAttributes, PropType, VNode } from 'vue' +import type { ButtonHTMLAttributes, DefineComponent, HTMLAttributes, PropType, VNode, VNodeChild } from 'vue' export interface FooterButtonProps extends ButtonHTMLAttributes, ButtonProps { disabled?: boolean key?: VKey - text?: string | VNode + text?: string | VNode | (() => VNodeChild) onClick?: (evt: Event) => void } @@ -25,7 +25,7 @@ export const footerProps = { type: Boolean, default: true, }, - footer: [Boolean, Array, Object] as PropType, + footer: [Boolean, Array, Object, Function] as PropType VNodeChild)>, ok: Function as PropType<(evt?: Event | unknown) => Promise | void>, okButton: Object as PropType, okLoading: Boolean, diff --git a/packages/components/_private/header/src/types.ts b/packages/components/_private/header/src/types.ts index 6469159d3..ebeda5bd1 100644 --- a/packages/components/_private/header/src/types.ts +++ b/packages/components/_private/header/src/types.ts @@ -6,11 +6,11 @@ */ import type { HeaderProps as IxHeaderProps } from '@idux/components/header' -import type { VNode } from 'vue' +import type { VNode, VNodeChild } from 'vue' export interface HeaderProps { closable?: boolean - closeIcon?: string | VNode + closeIcon?: string | VNode | (() => VNodeChild) header?: string | IxHeaderProps size?: 'lg' | 'md' | 'sm' onClose?: (evt: Event) => void diff --git a/packages/components/config/src/types.ts b/packages/components/config/src/types.ts index e7c60d517..47f5b7f2a 100644 --- a/packages/components/config/src/types.ts +++ b/packages/components/config/src/types.ts @@ -316,7 +316,7 @@ export interface MessageConfig { container?: PortalTargetType destroyOnHover: boolean duration: number - icon: Partial> + icon: Partial VNodeChild)>> maxCount: number top?: number | string } @@ -328,7 +328,7 @@ export interface ModalConfig { closeIcon: string closeOnEsc: boolean container?: PortalTargetType - icon?: Partial> + icon?: Partial VNodeChild)>> mask: boolean maskClosable: boolean /** @@ -341,7 +341,7 @@ export interface NotificationConfig { container?: PortalTargetType destroyOnHover: boolean duration: number - icon?: Partial> + icon?: Partial VNodeChild)>> closeIcon?: string | VNode maxCount: number offset: number | string | (string | number)[] diff --git a/packages/components/drawer/docs/Api.zh.md b/packages/components/drawer/docs/Api.zh.md index 8aeabe62c..07b11df91 100644 --- a/packages/components/drawer/docs/Api.zh.md +++ b/packages/components/drawer/docs/Api.zh.md @@ -7,12 +7,12 @@ | --- | --- | --- | --- | --- | --- | | `v-model:visible` | 是否可见 | `boolean` | - | - | - | | `closable` | 是否显示右上角的关闭按钮 | `boolean` | `true` | ✅ | - | -| `closeIcon` | 自定义关闭图标 | `string \| VNode \| #closeIcon` | `close` | ✅ | - | +| `closeIcon` | 自定义关闭图标 | `string \| VNode \| (() => VNodeChild) \| #closeIcon` | `close` | ✅ | - | | `closeOnDeactivated` | 是否在 [onDeactivated](https://cn.vuejs.org/api/composition-api-lifecycle.html#ondeactivated) 时关闭 | `boolean` | `true` | - | - | | `closeOnEsc` | 是否支持键盘 `esc` 关闭 | `boolean` | `true` | ✅ | - | | `container` | 自定义抽屉挂载的容器节点 | `string \| HTMLElement \| () => string \| HTMLElement` | - | ✅ | - | | `destroyOnHide` | 关闭时销毁子元素 | `boolean` | `false` | - | - | -| `footer` | 自定义底部按钮 | `DrawerButtonProps[] \| VNode \| #footer` | - | - | - | +| `footer` | 自定义底部按钮 | `DrawerButtonProps[] \| VNode \| (() => VNodeChild) \| #footer` | - | - | - | | `header` | 抽屉的标题 | `string \| HeaderProps \| #header={closable, closeIcon, onClose}` | - | - | - | | `height` | 抽屉高度 | `string \| number` | `'256'` | ✅ | 默认值仅在 `placement为` 为 `top/bottom` 时生效,其他情况默认为 `100%` | | `mask` | 是否展示蒙层 | `boolean` | `true` | ✅ | - | @@ -90,7 +90,7 @@ export interface DrawerProviderRef { export interface DrawerOptions extends DrawerProps { key?: VKey // 抽屉的内容 - content?: string | VNode + content?: string | VNode | (() => VNodeChild) // 当 content 为 VNode 时, 可以传递 props 或者绑定 ref contentProps?: Record | VNodeProps // 抽屉销毁后的回调 diff --git a/packages/components/drawer/src/DrawerProvider.tsx b/packages/components/drawer/src/DrawerProvider.tsx index 158b18111..6c6b1ea7f 100644 --- a/packages/components/drawer/src/DrawerProvider.tsx +++ b/packages/components/drawer/src/DrawerProvider.tsx @@ -10,6 +10,8 @@ import type { VKey } from '@idux/cdk/utils' import { cloneVNode, defineComponent, isVNode, provide, shallowRef } from 'vue' +import { isFunction } from 'lodash-es' + import { NoopFunction, callEmit, convertArray, uniqueId } from '@idux/cdk/utils' import Drawer from './Drawer' @@ -33,7 +35,11 @@ export default defineComponent({ const onAfterClose = destroyOnHide ? () => destroy(key!) : undefined const mergedProps = { key, visible, ref: setRef, 'onUpdate:visible': onUpdateVisible, onAfterClose } - const contentNode = isVNode(content) ? cloneVNode(content, contentProps, true) : content + const contentNode = isFunction(content) + ? content() + : isVNode(content) + ? cloneVNode(content, contentProps, true) + : content return ( {contentNode} diff --git a/packages/components/drawer/src/types.ts b/packages/components/drawer/src/types.ts index 0ab3728e4..dad50c842 100644 --- a/packages/components/drawer/src/types.ts +++ b/packages/components/drawer/src/types.ts @@ -10,7 +10,7 @@ import type { ScrollStrategy } from '@idux/cdk/scroll' import type { ExtractInnerPropTypes, ExtractPublicPropTypes, MaybeArray, VKey } from '@idux/cdk/utils' import type { ɵFooterButtonProps } from '@idux/components/_private/footer' import type { HeaderProps } from '@idux/components/header' -import type { DefineComponent, HTMLAttributes, PropType, VNode, VNodeProps } from 'vue' +import type { DefineComponent, HTMLAttributes, PropType, VNode, VNodeChild, VNodeProps } from 'vue' export type DrawerPlacement = 'top' | 'bottom' | 'start' | 'end' @@ -18,7 +18,7 @@ export type DrawerButtonProps = ɵFooterButtonProps export interface DrawerOptions extends DrawerPublicProps { key?: K - content?: string | VNode + content?: string | VNode | (() => VNodeChild) contentProps?: Record | VNodeProps onDestroy?: (key: K) => void } @@ -41,7 +41,7 @@ export const drawerProps = { type: Boolean, default: undefined, }, - closeIcon: [String, Object] as PropType, + closeIcon: [String, Object, Function] as PropType VNodeChild)>, closeOnDeactivated: { type: Boolean, default: true, @@ -58,7 +58,7 @@ export const drawerProps = { type: Boolean, default: false, }, - footer: [Array, Object] as PropType, + footer: [Array, Object, Function] as PropType VNodeChild)>, header: [String, Object] as PropType, height: [String, Number] as PropType, mask: { diff --git a/packages/components/header/src/types.ts b/packages/components/header/src/types.ts index 50845700f..6bba5daa6 100644 --- a/packages/components/header/src/types.ts +++ b/packages/components/header/src/types.ts @@ -15,11 +15,17 @@ export const headerProps = { avatar: { type: [String, Object] as PropType, default: undefined }, description: { type: String, default: undefined }, disabled: { type: Boolean, default: false }, - prefix: { type: [String, Object] as PropType, default: undefined }, + prefix: { + type: [String, Object, Function] as PropType VNodeChild)>, + default: undefined, + }, size: { type: String as PropType, default: 'md' }, showBar: { type: Boolean, default: false }, subTitle: { type: String, default: undefined }, - suffix: { type: [String, Object] as PropType, default: undefined }, + suffix: { + type: [String, Object, Function] as PropType VNodeChild)>, + default: undefined, + }, title: { type: String, default: undefined }, // events diff --git a/packages/components/message/demo/Content.md b/packages/components/message/demo/Content.md index 4a846453c..b40893c36 100644 --- a/packages/components/message/demo/Content.md +++ b/packages/components/message/demo/Content.md @@ -7,7 +7,7 @@ order: 1 ## zh -通过传入一个 `VNode` 自定义提示内容。 +通过传入一个 `VNode` 或函数来自定义提示内容。 ## en diff --git a/packages/components/message/demo/Content.vue b/packages/components/message/demo/Content.vue index b844b9aa5..b8cd3bcca 100644 --- a/packages/components/message/demo/Content.vue +++ b/packages/components/message/demo/Content.vue @@ -12,7 +12,7 @@ export default defineComponent({ setup() { const message = useMessage() - const content = h('span', {}, [h('span', {}, 'This is a VNode content '), h(IxIcon, { name: 'star' })]) + const content = () => h('span', {}, [h('span', {}, 'This is a VNode content '), h(IxIcon, { name: 'star' })]) const open = () => message.info(content) diff --git a/packages/components/message/demo/Icon.md b/packages/components/message/demo/Icon.md index 0ecfa5839..9b5bfe5e6 100644 --- a/packages/components/message/demo/Icon.md +++ b/packages/components/message/demo/Icon.md @@ -7,8 +7,8 @@ order: 3 ## zh -自定义图标:支持 `string` 和 `VNode` 两种类型。 +自定义图标:支持 `string` , `VNode` , `() => VNodeChild` ## en -Customized icon: supports `string` and `VNode` type. +Customized icon: supports `string` , `VNode` and `() => VNodeChild` type. diff --git a/packages/components/message/demo/Icon.vue b/packages/components/message/demo/Icon.vue index 0b0939314..61c4a7566 100644 --- a/packages/components/message/demo/Icon.vue +++ b/packages/components/message/demo/Icon.vue @@ -16,8 +16,8 @@ export default defineComponent({ const { info } = useMessage() const open = () => info('This is a star message', { icon: 'star' }) - const iconVNode = h(IxIcon, { name: 'star', style: { color: 'red' } }) - const openVNode = () => info('This is a star message', { icon: iconVNode }) + const renderIcon = () => h(IxIcon, { name: 'star', style: { color: 'red' } }) + const openVNode = () => info('This is a star message', { icon: renderIcon }) return { open, openVNode } }, diff --git a/packages/components/message/docs/Api.zh.md b/packages/components/message/docs/Api.zh.md index 75910db4a..3425a95d5 100644 --- a/packages/components/message/docs/Api.zh.md +++ b/packages/components/message/docs/Api.zh.md @@ -8,7 +8,7 @@ | `v-model:visible` | 是否可见 | `boolean` | - | - | - | | `destroyOnHover` | 鼠标悬浮时自动销毁 | `boolean` | `false` | ✅ | - | | `duration` | 自动销毁的延时,单位毫秒 | `number` | - | - | 传入 `0` 表示不自动销毁 | -| `icon` | 自定义图标 | `string \| VNode` | - | ✅ | - | +| `icon` | 自定义图标 | `string \| VNode \| (() => VNodeChild)` | - | ✅ | - | | `type` | 提示类型 | `'info' \| 'success' \| 'warning' \| 'error' \| 'loading'` | `info` | - | - | | `onClose` | 提示框关闭的回调 | `() => void` | - | - | - | @@ -55,11 +55,11 @@ export const useMessage: () => MessageProviderRef; export interface MessageProviderRef { // 打开提示框 open: (options: MessageOptions) => MessageRef - info: (content: string | VNode, options?: Omit) => MessageRef - success: (content: string | VNode, options?: Omit) => MessageRef - warning: (content: string | VNode, options?: Omit) => MessageRef - error: (content: string | VNode, options?: Omit) => MessageRef - loading: (content: string | VNode, options?: Omit) => MessageRef + info: (content: string | VNode | (() => VNodeChild), options?: Omit) => MessageRef + success: (content: string | VNode | (() => VNodeChild), options?: Omit) => MessageRef + warning: (content: string | VNode | (() => VNodeChild), options?: Omit) => MessageRef + error: (content: string | VNode | (() => VNodeChild), options?: Omit) => MessageRef + loading: (content: string | VNode | (() => VNodeChild), options?: Omit) => MessageRef // 更新指定 key 的提示框的配置信息 update: (key: VKey, options: MessageOptions) => void // 销毁指定 key 的提示框 @@ -71,7 +71,7 @@ export interface MessageProviderRef { export interface MessageOptions extends MessageProps { key?: string // 提示框的内容 - content?: string | VNode + content?: string | VNode | (() => VNodeChild) // 提示框销毁后的回调 onDestroy?: (key: VKey) => void } diff --git a/packages/components/message/src/Message.tsx b/packages/components/message/src/Message.tsx index c255fee9d..af90ca0ff 100644 --- a/packages/components/message/src/Message.tsx +++ b/packages/components/message/src/Message.tsx @@ -10,11 +10,9 @@ import type { MessageConfig } from '@idux/components/config' import { computed, defineComponent, onBeforeUnmount, onMounted, watchEffect } from 'vue' -import { isString } from 'lodash-es' - import { callEmit, useControlledProp } from '@idux/cdk/utils' import { useGlobalConfig } from '@idux/components/config' -import { IxIcon } from '@idux/components/icon' +import { convertIconVNode } from '@idux/components/utils' import { messageProps } from './types' @@ -40,7 +38,7 @@ export default defineComponent({ return () => { const icon = mergedIcon.value - const iconNode = isString(icon) ? : icon + const iconNode = convertIconVNode(slots.icon, icon) const prefixCls = mergedPrefixCls.value return (
diff --git a/packages/components/message/src/MessageProvider.tsx b/packages/components/message/src/MessageProvider.tsx index 328e2fe87..5386fda02 100644 --- a/packages/components/message/src/MessageProvider.tsx +++ b/packages/components/message/src/MessageProvider.tsx @@ -5,12 +5,21 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import { type ComputedRef, TransitionGroup, type VNode, computed, defineComponent, provide, ref } from 'vue' +import { + type ComputedRef, + TransitionGroup, + type VNode, + type VNodeChild, + computed, + defineComponent, + provide, + ref, +} from 'vue' import { CdkPortal } from '@idux/cdk/portal' import { VKey, callEmit, convertArray, convertCssPixel, uniqueId } from '@idux/cdk/utils' import { useGlobalConfig } from '@idux/components/config' -import { usePortalTarget } from '@idux/components/utils' +import { convertStringVNode, usePortalTarget } from '@idux/components/utils' import Message from './Message' import { MESSAGE_PROVIDER_TOKEN } from './token' @@ -42,9 +51,10 @@ export default defineComponent({ const { key, content, visible = true, onDestroy, ...restProps } = item const onClose = () => destroy(key!) const mergedProps = { key, visible, onClose } + const contentNode = convertStringVNode(undefined, content) return ( - {content} + {contentNode} ) }) @@ -133,7 +143,7 @@ const useMessage = (maxCount: ComputedRef) => { const messageTypes = ['info', 'success', 'warning', 'error', 'loading'] as const const [info, success, warning, error, loading] = messageTypes.map(type => { - return (content: string | VNode, options?: Omit) => + return (content: string | VNode | (() => VNodeChild), options?: Omit) => open({ ...options, content, type }) }) diff --git a/packages/components/message/src/types.ts b/packages/components/message/src/types.ts index 7a560875b..95cfb28cf 100644 --- a/packages/components/message/src/types.ts +++ b/packages/components/message/src/types.ts @@ -7,13 +7,13 @@ import type { PortalTargetType } from '@idux/cdk/portal' import type { ExtractInnerPropTypes, ExtractPublicPropTypes, MaybeArray, VKey } from '@idux/cdk/utils' -import type { DefineComponent, HTMLAttributes, PropType, VNode } from 'vue' +import type { DefineComponent, HTMLAttributes, PropType, VNode, VNodeChild } from 'vue' export type MessageType = 'info' | 'success' | 'warning' | 'error' | 'loading' export interface MessageOptions extends MessagePublicProps { key?: K - content?: string | VNode + content?: string | VNode | (() => VNodeChild) onDestroy?: (key: K) => void } export interface MessageRef { @@ -32,7 +32,7 @@ export const messageProps = { default: undefined, }, duration: Number, - icon: [String, Object] as PropType, + icon: [String, Object, Function] as PropType VNodeChild)>, type: { type: String as PropType, default: 'info', @@ -58,11 +58,26 @@ export const messageProviderProps = { } as const export interface MessageProviderRef { open: (options: MessageOptions) => MessageRef - info: (content: string | VNode, options?: Omit, 'type' | 'content'>) => MessageRef - success: (content: string | VNode, options?: Omit, 'type' | 'content'>) => MessageRef - warning: (content: string | VNode, options?: Omit, 'type' | 'content'>) => MessageRef - error: (content: string | VNode, options?: Omit, 'type' | 'content'>) => MessageRef - loading: (content: string | VNode, options?: Omit, 'type' | 'content'>) => MessageRef + info: ( + content: string | VNode | (() => VNodeChild), + options?: Omit, 'type' | 'content'>, + ) => MessageRef + success: ( + content: string | VNode | (() => VNodeChild), + options?: Omit, 'type' | 'content'>, + ) => MessageRef + warning: ( + content: string | VNode | (() => VNodeChild), + options?: Omit, 'type' | 'content'>, + ) => MessageRef + error: ( + content: string | VNode | (() => VNodeChild), + options?: Omit, 'type' | 'content'>, + ) => MessageRef + loading: ( + content: string | VNode | (() => VNodeChild), + options?: Omit, 'type' | 'content'>, + ) => MessageRef update: (key: K, options: MessageOptions) => void destroy: (key: K | K[]) => void destroyAll: () => void diff --git a/packages/components/modal/docs/Api.zh.md b/packages/components/modal/docs/Api.zh.md index 5d0a7a1e9..a4d830a3a 100644 --- a/packages/components/modal/docs/Api.zh.md +++ b/packages/components/modal/docs/Api.zh.md @@ -10,22 +10,22 @@ | `cancelText` | 取消按钮的文本 | `string` | `取消` | - | - | | `centered` | 垂直居中展示 | `boolean` | `false` | ✅ | `seer` 主题默认为 `true` | | `closable` | 是否显示右上角的关闭按钮 | `boolean` | `true` | ✅ | - | -| `closeIcon` | 自定义关闭图标 | `string \| VNode \| #closeIcon` | `close` | ✅ | - | +| `closeIcon` | 自定义关闭图标 | `string \| VNode \| (() => VNodeChild) \| #closeIcon` | `close` | ✅ | - | | `closeOnDeactivated` | 是否在 [onDeactivated](https://cn.vuejs.org/api/composition-api-lifecycle.html#ondeactivated) 时关闭 | `boolean` | `true` | - | - | | `closeOnEsc` | 是否支持键盘 `esc` 关闭 | `boolean` | `true` | ✅ | - | | `container` | 自定义对话框挂载的容器节点 | `string \| HTMLElement \| () => string \| HTMLElement` | - | ✅ | - | | `destroyOnHide` | 关闭时销毁子元素 | `boolean` | `false` | - | - | | `draggable` | 是否支持拖放| `boolean\| 'native'\| 'pointer'` | `false` | - | 当置为`true`时默认激活 `pointer`模式 | -| `footer` | 自定义底部按钮 | `boolean \| ModalButtonProps[] \| VNode \| #footer` | `true` | - | 默认会根据 `type` 的不同渲染相应的按钮,如果传入 `false` 则不显示 | +| `footer` | 自定义底部按钮 | `boolean \| ModalButtonProps[] \| VNode \| (() => VNodeChild) \| #footer` | `true` | - | 默认会根据 `type` 的不同渲染相应的按钮,如果传入 `false` 则不显示 | | `header` | 对话框标题 | `string \| HeaderProps \| #header={closable, closeIcon, onClose}` | - | - | - | -| `icon` | 自定义图标 | `string \| VNode \| #icon` | - | ✅ | 当 `type` 不为 `default` 时有效 | +| `icon` | 自定义图标 | `string \| VNode \| (() => VNodeChild) \| #icon` | - | ✅ | 当 `type` 不为 `default` 时有效 | | `mask` | 是否展示蒙层 | `boolean` | `true` | ✅ | - | | `maskClosable` | 点击蒙层是否允许关闭 | `boolean` | `true` | ✅ | `seer` 主题默认为 `false` | | `animatable` | 是否开启弹窗动画效果 | `boolean` | `true` | - | - | | `offset` | 对话框偏移量 | `number \| string` | `128` | - | 为顶部偏移量,仅在`centered=false` 时生效 | | `okButton` | 确认按钮的属性 | `ButtonProps` | - | - | - | | `okText` | 确认按钮的文本 | `string` | `确定` | - | - | -| `title` | 对话框次标题 | `string \| VNode \| #title` | - | - | 当 `type` 不为 `default` 时有效 | +| `title` | 对话框次标题 | `string \| VNode \| (() => VNodeChild) \| #title` | - | - | 当 `type` 不为 `default` 时有效 | | `type` | 对话框类型 | `'default' \| 'confirm' \| 'info' \| 'success' \| 'warning' \| 'error'` | `default` | - | - | | `width` | 对话框宽度 | `string \| number` | - | - | `default` 类型默认宽度 `480px`, 其他类型默认宽度 `400` | | `zIndex` | 设置对话框的 `z-index` | `number` | - | - | - | @@ -107,7 +107,7 @@ export interface ModalProviderRef { export interface ModalOptions extends ModalProps { key?: VKey // 对话框的内容 - content?: string | VNode + content?: string | VNode | (() => VNodeChild) // 当 content 为 VNode 时, 可以传递 props 或者绑定 ref contentProps?: Record | VNodeProps // 对话框销毁后的回调 diff --git a/packages/components/modal/src/ModalBody.tsx b/packages/components/modal/src/ModalBody.tsx index de72fefac..0e9914c67 100644 --- a/packages/components/modal/src/ModalBody.tsx +++ b/packages/components/modal/src/ModalBody.tsx @@ -5,11 +5,11 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import type { Slot, VNode, VNodeTypes } from 'vue' +import type { Slot, VNode, VNodeChild } from 'vue' import { computed, defineComponent, inject } from 'vue' -import { isString } from 'lodash-es' +import { isFunction, isString } from 'lodash-es' import { IxIcon } from '@idux/components/icon' @@ -44,6 +44,7 @@ export default defineComponent({ const classes = `${prefixCls} ${prefixCls}-${props.type}` const iconNode = renderIcon(prefixCls, slots.icon, iconName.value) const titleNode = renderTitle(prefixCls, slots.title, props.title) + return (
{iconNode} @@ -57,23 +58,42 @@ export default defineComponent({ }, }) -const renderIcon = (prefixCls: string, iconSlot: Slot | undefined, icon: string | VNode | undefined) => { +const renderIcon = ( + prefixCls: string, + iconSlot: Slot | undefined, + icon: string | VNode | (() => VNodeChild) | undefined, +) => { if (!iconSlot && !icon) { return null } - let children: VNodeTypes + let children: VNodeChild if (iconSlot) { children = iconSlot() + } else if (isFunction(icon)) { + children = icon() } else { children = isString(icon) ? : icon! } return
{children}
} -const renderTitle = (prefixCls: string, titleSlot: Slot | undefined, title: string | VNode | undefined) => { +const renderTitle = ( + prefixCls: string, + titleSlot: Slot | undefined, + title: string | VNode | (() => VNodeChild) | undefined, +) => { if (!titleSlot && !title) { return null } - const children = titleSlot ? titleSlot() : title + + let children: VNodeChild + if (titleSlot) { + children = titleSlot() + } else if (isFunction(title)) { + children = title() + } else { + children = title + } + return
{children}
} diff --git a/packages/components/modal/src/ModalProvider.tsx b/packages/components/modal/src/ModalProvider.tsx index 32cd503da..393b2a6b9 100644 --- a/packages/components/modal/src/ModalProvider.tsx +++ b/packages/components/modal/src/ModalProvider.tsx @@ -10,6 +10,8 @@ import type { VKey } from '@idux/cdk/utils' import { cloneVNode, defineComponent, isVNode, provide, shallowRef } from 'vue' +import { isFunction } from 'lodash-es' + import { NoopFunction, callEmit, convertArray, uniqueId } from '@idux/cdk/utils' import Modal from './Modal' @@ -34,7 +36,11 @@ export default defineComponent({ const onAfterClose = destroyOnHide ? () => destroy(key!) : undefined const mergedProps = { key, visible, ref: setRef, 'onUpdate:visible': onUpdateVisible, onAfterClose } - const contentNode = isVNode(content) ? cloneVNode(content, contentProps, true) : content + const contentNode = isFunction(content) + ? content() + : isVNode(content) + ? cloneVNode(content, contentProps, true) + : content return }) return ( diff --git a/packages/components/modal/src/types.ts b/packages/components/modal/src/types.ts index 1170895fa..849839a12 100644 --- a/packages/components/modal/src/types.ts +++ b/packages/components/modal/src/types.ts @@ -11,13 +11,13 @@ import type { ExtractInnerPropTypes, ExtractPublicPropTypes, MaybeArray, VKey } import type { ɵFooterButtonProps } from '@idux/components/_private/footer' import type { ButtonProps } from '@idux/components/button' import type { HeaderProps } from '@idux/components/header' -import type { DefineComponent, HTMLAttributes, PropType, VNode, VNodeProps } from 'vue' +import type { DefineComponent, HTMLAttributes, PropType, VNode, VNodeChild, VNodeProps } from 'vue' export type ModalType = 'default' | 'confirm' | 'info' | 'success' | 'warning' | 'error' export type ModalButtonProps = ɵFooterButtonProps export interface ModalOptions extends ModalPublicProps { key?: K - content?: string | VNode + content?: string | VNode | (() => VNodeChild) contentProps?: Record | VNodeProps onDestroy?: (key: K) => void } @@ -42,7 +42,7 @@ export const modalProps = { type: Boolean, default: undefined, }, - closeIcon: [String, Object] as PropType, + closeIcon: [String, Object, Function] as PropType VNodeChild)>, closeOnDeactivated: { type: Boolean, default: true, @@ -61,11 +61,11 @@ export const modalProps = { }, draggable: { type: Boolean, default: false }, footer: { - type: [Boolean, Array, Object] as PropType, + type: [Boolean, Array, Object, Function] as PropType VNodeChild)>, default: true, }, header: [String, Object] as PropType, - icon: [String, Object] as PropType, + icon: [String, Object, Function] as PropType VNodeChild)>, mask: { type: Boolean, default: undefined, @@ -85,7 +85,7 @@ export const modalProps = { okButton: Object as PropType, okText: String, scrollStrategy: Object as PropType, - title: [String, Object] as PropType, + title: [String, Object, Function] as PropType VNodeChild)>, type: { type: String as PropType, default: 'default', @@ -105,7 +105,7 @@ export const modalProps = { onOk: [Function, Array] as PropType unknown>>, // private - __content_node: [String, Object] as PropType, + __content_node: [String, Object] as PropType, } as const export type ModalProps = ExtractInnerPropTypes diff --git a/packages/components/notification/demo/Content.vue b/packages/components/notification/demo/Content.vue index eeb361ef5..aafd9ae64 100644 --- a/packages/components/notification/demo/Content.vue +++ b/packages/components/notification/demo/Content.vue @@ -9,9 +9,10 @@ import { useNotification } from '@idux/components/notification' const notification = useNotification() -const title = h('h3', { style: { color: 'red' } }, 'vnode title') +const title = () => h('h3', { style: { color: 'red' } }, 'vnode title') -const content = h('ul', {}, [h('li', {}, 'This is a VNode content data'), h('li', {}, 'This is a VNode content data')]) +const content = () => + h('ul', {}, [h('li', {}, 'This is a VNode content data'), h('li', {}, 'This is a VNode content data')]) const open = () => notification.info({ diff --git a/packages/components/notification/demo/Footer.vue b/packages/components/notification/demo/Footer.vue index 3e9e935ee..80db7f8c9 100644 --- a/packages/components/notification/demo/Footer.vue +++ b/packages/components/notification/demo/Footer.vue @@ -20,13 +20,13 @@ const openStringFooter = () => footer: 'system message', }) -const footerVNode = h('div', { style: { color: 'red' } }, 'system message') +const renderFooter = () => h('div', { style: { color: 'red' } }, 'system message') const oenVNodeFooter = () => info({ title: 'VNode', content: 'The bottom area is configured as VNode', - footer: footerVNode, + footer: renderFooter, }) const openIxButton = () => { diff --git a/packages/components/notification/demo/Icon.vue b/packages/components/notification/demo/Icon.vue index de10c8b44..0e01c7822 100644 --- a/packages/components/notification/demo/Icon.vue +++ b/packages/components/notification/demo/Icon.vue @@ -20,10 +20,10 @@ const openCustomIconByName = () => title: 'icon', content: 'This is a custom icon by name', }) -const iconVNode = h(IxIcon, { name: 'star', style: { color: 'red' } }) +const renderIcon = () => h(IxIcon, { name: 'star', style: { color: 'red' } }) const openCustomIconByVNode = () => info({ - icon: iconVNode, + icon: renderIcon, title: 'icon', content: 'This is a custom icon by VNode', }) @@ -34,10 +34,10 @@ const openCustomCloseIconByName = () => title: 'close icon', content: 'This is a custom close icon by name', }) -const closeIconVNode = h(IxIcon, { name: 'right', style: { color: 'red' } }) +const renderCloseIcon = () => h(IxIcon, { name: 'right', style: { color: 'red' } }) const openCustomCloseIconByVNode = () => info({ - closeIcon: closeIconVNode, + closeIcon: renderCloseIcon, title: 'close icon', content: 'This is a custom close icon by VNode', }) diff --git a/packages/components/notification/demo/Id.vue b/packages/components/notification/demo/Id.vue index 619aeb3b7..88b4e5021 100644 --- a/packages/components/notification/demo/Id.vue +++ b/packages/components/notification/demo/Id.vue @@ -12,7 +12,7 @@ import { useNotification } from '@idux/components/notification' const { info, destroyAll, destroy } = useNotification() let count = 0 -let notificationKey: string +let notificationKey: string | number | symbol const open = () => { const content = `click count: ${count++}` if (!notificationKey) { diff --git a/packages/components/notification/docs/Api.zh.md b/packages/components/notification/docs/Api.zh.md index 5e3a3b392..8cf32a3e1 100644 --- a/packages/components/notification/docs/Api.zh.md +++ b/packages/components/notification/docs/Api.zh.md @@ -8,8 +8,8 @@ | `maxCount` | 同一时间可展示的最大提示数量 | `number` | `5` | - | | `destroyOnHover` | 鼠标悬浮时是否允许销毁 | `boolean` | `false` | - | | `duration` | 自动销毁的延时,单位毫秒 | `number` | `4500` | - | -| `icon` | 自定义通知图标映射表 | `Partial>` | `{ success: 'check-circle', error: 'close-circle', info: 'info-circle', warning: 'exclamation-circle' }` | - | -| `closeIcon` | 自定义关闭图标 | `string \| VNode` | `close` | - | +| `icon` | 自定义通知图标映射表 | `Partial VNodeChild)>>` | `{ success: 'check-circle', error: 'close-circle', info: 'info-circle', warning: 'exclamation-circle' }` | - | +| `closeIcon` | 自定义关闭图标 | `string \| VNode \| (() => VNodeChild)` | `close` | - | | `offset` | 通知消息弹出时,距离边缘的位置 | `number \| string \|[number \| string, number \| string]` | `24` | number时:单位为px;
string时:可为`vh` \ `vw` \| `%` \| `px`;
array时:[上下边缘,左右边缘];
设置为非array时上下边缘和左右边缘相等 | | `placement` | 弹出的位置 | `'topStart' \| 'topEnd' \| 'bottomStart' \| 'bottomEnd'` | `'topEnd'` | - | @@ -57,14 +57,14 @@ const openNotification = ()=> notification.info({ | --- | --- | --- | --- | --- | --- | | `destroyOnHover` | 鼠标悬浮时是否允许销毁 | `boolean` | `false` | ☑️ | - | | `duration` | 自动销毁的延时,单位毫秒 | `number` | 4500 | ☑️ | - | -| `icon` | 自定义通知图标 | `string \| VNode` | - | - | - | -| `closeIcon` | 自定义关闭图标 | `string \| VNode` | - | - | - | +| `icon` | 自定义通知图标 | `string \| VNode \| (() => VNodeChild)` | - | - | - | +| `closeIcon` | 自定义关闭图标 | `string \| VNode \| (() => VNodeChild)` | - | - | - | | `type` | 通知类型 | `'info' \| 'success' \| 'warning' \| 'error'` | - | - | - | | `key` | 唯一标识 | `string \| number \| symbol` | - | - | - | | `placement` | 弹出的位置 | `'topStart' \| 'topEnd' \| 'bottomStart' \| 'bottomEnd'` | `'topEnd'` | ☑️ | - | -| `title` | 通知的标题 | `string \| VNode` | - | - | 必填 | -| `content` | 通知的内容 | `string \| VNode` | - | - | 必填 | -| `footer` | 自定义底部按钮 | `string \| VNode \| ButtonProps[]` | - | - | 底部区域flex布局 | +| `title` | 通知的标题 | `string \| VNode \| (() => VNodeChild)` | - | - | 必填 | +| `content` | 通知的内容 | `string \| VNode \| (() => VNodeChild)` | - | - | 必填 | +| `footer` | 自定义底部按钮 | `string \| VNode \| (() => VNodeChild) \| ButtonProps[]` | - | - | 底部区域flex布局 | | `onDestroy` | 关闭通知时触发的回调 | `(key: VKey) => void` | - | - | - | ```ts @@ -114,8 +114,7 @@ const contentRef = ref() const openNotification = () => open({ title: 'Basic Notification', - content: h('div', 'Some contents...'), - contentProps: { ref: contentRef } + content: () => h('div', { ref: contentRef }, 'Some contents...'), }) ``` diff --git a/packages/components/notification/src/Notification.tsx b/packages/components/notification/src/Notification.tsx index 0d296879c..ee44917c1 100644 --- a/packages/components/notification/src/Notification.tsx +++ b/packages/components/notification/src/Notification.tsx @@ -5,19 +5,19 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import type { NotificationNodePropKey, NotificationProps, SlotProps } from './types' +import type { NotificationProps, SlotProps } from './types' import type { NotificationConfig } from '@idux/components/config' import type { ComputedRef, Slots } from 'vue' import { computed, defineComponent, onBeforeUnmount, onMounted, watchEffect } from 'vue' -import { isArray, isString } from 'lodash-es' +import { isArray, isFunction } from 'lodash-es' import { callEmit, useControlledProp } from '@idux/cdk/utils' import { IxButton } from '@idux/components/button' import { useGlobalConfig } from '@idux/components/config' -import { IxIcon } from '@idux/components/icon' import { IxSpace } from '@idux/components/space' +import { convertIconVNode, convertStringVNode } from '@idux/components/utils' import { notificationProps } from './types' @@ -43,11 +43,12 @@ export default defineComponent({ const { visible, close, onMouseEnter, onMouseLeave } = useVisible(props, config) return () => { - const iconNode = icon.value && getIconNode(icon.value) - const closeIconNode = getIconNode(closeIcon.value) + const iconNode = convertIconVNode(undefined, icon.value) + const closeIconNode = convertIconVNode(undefined, closeIcon.value) - const contentNode = getNode(props, slots, 'content') - const footerNode = getNode(props, slots, 'footer', { + const titleNode = convertStringVNode(slots, props, 'title') + const contentNode = convertStringVNode(slots.default, props.content) + const footerNode = getFooterNode(slots, props.footer, { visible: visible.value, close, }) @@ -67,7 +68,7 @@ export default defineComponent({ {closeIconNode}
-
{getNode(props, slots, 'title')}
+
{titleNode}
{contentNode &&
{contentNode}
}
@@ -146,39 +147,26 @@ function useVisible(props: NotificationProps, config: NotificationConfig) { return { visible, close, onMouseEnter, onMouseLeave } } -function getIconNode(icon: NotificationProps['icon']) { - return isString(icon) ? : icon -} - -function getNode( - props: T, - slots: Slots, - nodeKey: K, - slotProps?: SlotProps, -) { - const slotMap = { - title: 'title', - content: 'default', - footer: 'footer', - } as const - const getHandlePropsMap = { - title: (propsValue: NotificationProps[K]) => propsValue, - content: (propsValue: NotificationProps[K]) => propsValue, - footer: (propsValue: NotificationProps[K]) => getFooterNode(propsValue), - } as const - return slots[slotMap[nodeKey]]?.(slotProps) ?? getHandlePropsMap[nodeKey]?.(props[nodeKey]) -} +function getFooterNode(slots: Slots, footer: NotificationProps['footer'] | undefined, params: SlotProps) { + if (slots.footer) { + return slots.footer(params) + } -function getFooterNode(footer?: NotificationProps['footer']) { if (!footer) { return '' } - if (!isArray(footer)) { - return footer + + if (isFunction(footer)) { + return footer(params) + } + + if (isArray(footer)) { + const footerButtons = footer.map(item => { + const { text, ...rest } = item + return {text} + }) + return {footerButtons} } - const footerButtons = footer.map(item => { - const { text, ...rest } = item - return {text} - }) - return {footerButtons} + + return footer } diff --git a/packages/components/notification/src/types.ts b/packages/components/notification/src/types.ts index 46c0ca015..1fe09745a 100644 --- a/packages/components/notification/src/types.ts +++ b/packages/components/notification/src/types.ts @@ -8,11 +8,11 @@ import type { PortalTargetType } from '@idux/cdk/portal' import type { ExtractInnerPropTypes, ExtractPublicPropTypes, MaybeArray, VKey } from '@idux/cdk/utils' import type { ButtonProps } from '@idux/components/button' -import type { DefineComponent, HTMLAttributes, PropType, VNode, VNodeProps } from 'vue' +import type { DefineComponent, HTMLAttributes, PropType, VNode, VNodeChild, VNodeProps } from 'vue' export interface NotificationButtonProps extends ButtonProps { key?: K - text?: string | VNode + text?: string | VNode | (() => VNodeChild) onClick?: (evt: Event) => void } @@ -33,12 +33,14 @@ export const notificationProps = { default: undefined, }, duration: Number, - icon: [String, Object] as PropType, - closeIcon: [String, Object] as PropType, + icon: [String, Object, Function] as PropType VNodeChild)>, + closeIcon: [String, Object, Function] as PropType VNodeChild)>, type: String as PropType, - title: [String, Object] as PropType, - content: [String, Object] as PropType, - footer: [String, Array, Object] as PropType, + title: [String, Object, Function] as PropType VNodeChild)>, + content: [String, Object, Function] as PropType VNodeChild)>, + footer: [String, Array, Object] as PropType< + string | NotificationButtonProps[] | VNode | ((options: SlotProps) => VNodeChild) + >, placement: String as PropType, // event diff --git a/packages/components/utils/src/convertVNode.ts b/packages/components/utils/src/convertVNode.ts index 6443b4e5a..430a1844d 100644 --- a/packages/components/utils/src/convertVNode.ts +++ b/packages/components/utils/src/convertVNode.ts @@ -7,13 +7,13 @@ import { type Slot, type Slots, type VNode, type VNodeChild, createVNode } from 'vue' -import { isString } from 'lodash-es' +import { isFunction, isString } from 'lodash-es' import { IxIcon } from '@idux/components/icon' export function convertIconVNode( slot: Slot | undefined, - prop: string | VNode | undefined, + prop: string | VNode | ((params: Record | undefined) => VNodeChild) | undefined, slotParams?: Record, ): VNodeChild export function convertIconVNode( @@ -29,7 +29,7 @@ export function convertIconVNode( slotParams?: Record, ): VNodeChild { let iconSlot: Slot | undefined - let iconName: string | VNode | undefined + let iconName: string | VNode | ((params: Record | undefined) => VNodeChild) | undefined const isKey = isString(keyOrParams) const params = isKey ? slotParams : keyOrParams if (isKey) { @@ -40,19 +40,23 @@ export function convertIconVNode( } } else { iconSlot = slots as Slot | undefined - iconName = props as string | VNode | undefined + iconName = props as string | VNode | ((params: Record | undefined) => VNodeChild) | undefined } if (iconSlot) { return iconSlot(params) } - return isString(iconName) && iconName !== '' ? createVNode(IxIcon, { name: iconName }, null) : iconName + return isString(iconName) && iconName !== '' + ? createVNode(IxIcon, { name: iconName }, null) + : isFunction(iconName) + ? iconName(params) + : iconName } export function convertStringVNode( slot: Slot | undefined, - prop: string | VNode | undefined, + prop: string | VNode | ((params: Record | undefined) => VNodeChild) | undefined, slotParams?: Record, ): VNodeChild export function convertStringVNode( @@ -68,7 +72,7 @@ export function convertStringVNode( slotParams?: Record, ): VNodeChild { let labelSlot: Slot | undefined - let label: string | VNode | undefined + let label: string | VNode | ((params: Record | undefined) => VNodeChild) | undefined const isKey = isString(keyOrParams) const params = isKey ? slotParams : keyOrParams if (isKey) { @@ -79,8 +83,8 @@ export function convertStringVNode( } } else { labelSlot = slots as Slot | undefined - label = props as string | VNode | undefined + label = props as string | VNode | ((params: Record | undefined) => VNodeChild) | undefined } - return labelSlot ? labelSlot(params) : label + return labelSlot ? labelSlot(params) : isFunction(label) ? label(params) : label }