diff --git a/src/dialog/__tests__/index.test.jsx b/src/dialog/__tests__/index.test.jsx index 47e457cc5..b633036d0 100644 --- a/src/dialog/__tests__/index.test.jsx +++ b/src/dialog/__tests__/index.test.jsx @@ -1,18 +1,37 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import Dialog from '@/src/dialog/index.ts'; // every component needs four parts: props/events/slots/functions. describe('Dialog', () => { // test props api describe(':props', () => { - it('modeless', () => { + it('mode:modeless', () => { const wrapper = mount(Dialog, { propsData: { mode: 'modeless' }, }); expect(wrapper.find('.t-dialog__mask').exists()).toBe(false); }); + it('mode:normal', async () => { + const wrapper = mount(Dialog, { + propsData: { mode: 'normal' }, + }); + const ctx = wrapper.find('.t-dialog__ctx'); + await nextTick(); + expect(ctx.find('.t-dialog__position').exists()).toBeFalsy(); + }); + + it('mode:full-screen', async () => { + const wrapper = mount(Dialog, { + propsData: { mode: 'full-screen' }, + }); + const ctx = wrapper.find('.t-dialog__ctx'); + await nextTick(); + expect(ctx.find('.t-dialog__position_fullscreen').exists()).toBeTruthy(); + }); + it('placement', () => { const dialog = mount(Dialog).find('.t-dialog__position'); const centerDialog = mount(Dialog, { @@ -98,10 +117,160 @@ describe('Dialog', () => { await wrapper.setProps({ visible: false }); expect(wrapper.exists()).toBe(true); }); + + it('showOverlay', async () => { + const wrapper = mount(Dialog); + const ctx = wrapper.find('.t-dialog__ctx'); + await nextTick(); + expect(ctx.find('.t-dialog__mask').exists()).toBeTruthy(); + }); + + it('theme', async () => { + const themeList = ['default', 'success', 'info', 'warning', 'danger']; + themeList.forEach(async (theme) => { + const wrapper = mount(Dialog, { + propsData: { + theme, + }, + }); + const dialog = wrapper.find('.t-dialog'); + await nextTick(); + expect(dialog.classes()).toContain(`t-dialog__modal-${theme}`); + }); + }); + + it('width', async () => { + const wrapper = mount(Dialog, { + propsData: { + width: '80%', + }, + }); + const dialog = wrapper.find('.t-dialog'); + await nextTick(); + expect(getComputedStyle(dialog.element, null).width).toBe('80%'); + }); + + it('draggable', async () => { + const wrapper = mount(Dialog, { + propsData: { + mode: 'modeless', + draggable: true, + }, + }); + const dialog = wrapper.find('.t-dialog'); + await nextTick(); + expect(dialog.classes()).toContain('t-dialog--draggable'); + }); + + it('dialogClassName', async () => { + const wrapper = mount(Dialog, { + propsData: { + dialogClassName: 'custom-class', + mode: 'modeless', + }, + }); + const dialog = wrapper.find('.t-dialog'); + await nextTick(); + expect(dialog.classes()).toContain('custom-class'); + }); + + it('dialogStyle', async () => { + const wrapper = mount(Dialog, { + propsData: { + dialogStyle: { padding: '99px' }, + mode: 'modeless', + }, + }); + const dialog = wrapper.find('.t-dialog'); + await nextTick(); + expect(getComputedStyle(dialog.element, null).padding).toBe('99px'); + }); + + it('update dialog confirmBtnLoading', async () => { + const wrapper = mount({ + data() { + return { + loading: false, + }; + }, + render() { + return ( + + ); + }, + }); + const dialog = wrapper.find('.t-dialog'); + await nextTick(); + expect(dialog.find('.t-button--theme-primary.t-is-loading.t-dialog__confirm').exists()).toBeFalsy(); + await wrapper.setData({ loading: true }); + await nextTick(); + const updateDialog = wrapper.find('.t-dialog'); + expect(updateDialog.find('.t-button--theme-primary.t-is-loading.t-dialog__confirm').exists()).toBeTruthy(); + }); }); // test events - // describe('@event', () => {}); + describe(':events', () => { + it(':onCancel', async () => { + const fn = vi.fn(); + const wrapper = mount(Dialog, { + propsData: { + onCancel: fn, + }, + }); + const btn = wrapper.find('.t-dialog__footer .t-dialog__cancel'); + await nextTick(); + await btn.trigger('click'); + expect(fn).toBeCalled(); + }); + + it(':onConfirm', async () => { + const fn = vi.fn(); + const wrapper = mount(Dialog, { + propsData: { + onConfirm: fn, + }, + }); + const btn = wrapper.find('.t-dialog__footer .t-dialog__confirm'); + await nextTick(); + await btn.trigger('click'); + expect(fn).toBeCalled(); + }); + + it(':onClose', async () => { + const fn = vi.fn(); + const wrapper = mount(Dialog, { + propsData: { + onClose: fn, + }, + }); + const btn = wrapper.find('.t-dialog__close'); + await nextTick(); + await btn.trigger('click'); + expect(fn).toBeCalled(); + }); + + it(':onCloseBtnClick', async () => { + const fn = vi.fn(); + const wrapper = mount(Dialog, { + propsData: { + onCloseBtnClick: fn, + }, + }); + const btn = wrapper.find('.t-dialog__close'); + await nextTick(); + await btn.trigger('click'); + expect(fn).toBeCalled(); + }); + }); // // test slots // describe('', () => { diff --git a/src/dialog/dialog-card-props.ts b/src/dialog/dialog-card-props.ts new file mode 100644 index 000000000..801e96c23 --- /dev/null +++ b/src/dialog/dialog-card-props.ts @@ -0,0 +1,57 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { TdDialogCardProps } from './type'; +import { PropType } from 'vue'; + +export default { + /** 对话框内容 */ + body: { + type: [String, Function] as PropType, + }, + /** 取消按钮,可自定义。值为 null 则不显示取消按钮。值类型为字符串,则表示自定义按钮文本,值类型为 Object 则表示透传 Button 组件属性。使用 TNode 自定义按钮时,需自行控制取消事件 */ + cancelBtn: { + type: [String, Object, Function] as PropType, + }, + /** 关闭按钮,可以自定义。值为 true 显示默认关闭按钮,值为 false 不显示关闭按钮。值类型为 string 则直接显示值,如:“关闭”。值类型为 TNode,则表示呈现自定义按钮示例 */ + closeBtn: { + type: [String, Boolean, Function] as PropType, + default: true as TdDialogCardProps['closeBtn'], + }, + /** 确认按钮。值为 null 则不显示确认按钮。值类型为字符串,则表示自定义按钮文本,值类型为 Object 则表示透传 Button 组件属性。使用 TNode 自定义按钮时,需自行控制确认事件 */ + confirmBtn: { + type: [String, Object, Function] as PropType, + }, + /** 确认按钮加载状态 */ + confirmLoading: { + type: Boolean, + default: undefined, + }, + /** 底部操作栏,默认会有“确认”和“取消”两个按钮。值为 true 显示默认操作按钮,值为 false 不显示任何内容,值类型为 Function 表示自定义底部内容 */ + footer: { + type: [Boolean, Function] as PropType, + }, + /** 头部内容。值为 true 显示空白头部,值为 false 不显示任何内容,值类型为 string 则直接显示值,值类型为 Function 表示自定义头部内容 */ + header: { + type: [String, Boolean, Function] as PropType, + default: true as TdDialogCardProps['header'], + }, + /** 对话框风格 */ + theme: { + type: String as PropType, + default: 'default' as TdDialogCardProps['theme'], + validator(val: TdDialogCardProps['theme']): boolean { + if (!val) return true; + return ['default', 'info', 'warning', 'danger', 'success'].includes(val); + }, + }, + /** 如果“取消”按钮存在,则点击“取消”按钮时触发,同时触发关闭事件 */ + onCancel: Function as PropType, + /** 点击右上角关闭按钮时触发 */ + onCloseBtnClick: Function as PropType, + /** 如果“确认”按钮存在,则点击“确认”按钮时触发,或者键盘按下回车键时触发 */ + onConfirm: Function as PropType, +}; diff --git a/src/dialog/dialog-card.tsx b/src/dialog/dialog-card.tsx new file mode 100644 index 000000000..5861a03cd --- /dev/null +++ b/src/dialog/dialog-card.tsx @@ -0,0 +1,207 @@ +import Vue from 'vue'; +import { + CloseIcon as TdCloseIcon, + InfoCircleFilledIcon as TdInfoCircleFilledIcon, + CheckCircleFilledIcon as TdCheckCircleFilledIcon, + ErrorCircleFilledIcon as TdErrorCircleFilledIcon, +} from 'tdesign-icons-vue'; +import ActionMixin from './actions'; +import TButton from '../button'; +import { DialogCloseContext, TdDialogProps } from './type'; +import dialogProps from './props'; +import dialogCardProps from './dialog-card-props'; +import { renderContent, renderTNodeJSX } from '../utils/render-tnode'; +import mixins from '../utils/mixins'; +import getConfigReceiverMixins, { + DialogConfig, + getGlobalIconMixins, + getAttachConfigMixins, +} from '../config-provider/config-receiver'; +import { emitEvent } from '../utils/event'; +import { ClassName, Styles } from '../common'; +import { getCSSValue } from './utils'; + +export default mixins( + ActionMixin, + getConfigReceiverMixins('dialog'), + getGlobalIconMixins(), + getAttachConfigMixins('dialog'), +).extend({ + name: 'TDialogCard', + + components: { + TButton, + }, + + props: { + ...dialogProps, + ...dialogCardProps, + instanceGlobal: Object, + }, + + computed: { + isModal(): boolean { + return this.mode === 'modal'; + }, + isModeLess(): boolean { + return this.mode === 'modeless'; + }, + isFullScreen(): boolean { + return this.mode === 'full-screen'; + }, + dialogClass(): ClassName { + const dialogClass = [ + `${this.componentName}`, + `${this.componentName}__modal-${this.theme}`, + this.isModeLess && this.draggable && `${this.componentName}--draggable`, + ]; + if (this.isFullScreen) { + dialogClass.push(`${this.componentName}__fullscreen`); + } else { + dialogClass.push(`${this.componentName}--default`); + } + return dialogClass; + }, + computedDialogStyle(): Styles { + // width全屏模式不生效 + return !this.isFullScreen ? { width: getCSSValue(this.width), ...this.dialogStyle } : { ...this.dialogStyle }; + }, + }, + + methods: { + closeBtnAction(e: MouseEvent) { + emitEvent>(this, 'close-btn-click', { e }); + this.emitCloseEvent({ + trigger: 'close-btn', + e, + }); + }, + + emitCloseEvent(context: DialogCloseContext) { + emitEvent>(this, 'close', context); + }, + + // used in mixins of ActionMixin + cancelBtnAction(e: MouseEvent) { + emitEvent>(this, 'cancel', { e }); + this.emitCloseEvent({ + trigger: 'cancel', + e, + }); + }, + + // used in mixins of ActionMixin + confirmBtnAction(e: MouseEvent) { + emitEvent>(this, 'confirm', { e }); + }, + + onStopDown(e: MouseEvent) { + if (this.isModeLess && this.draggable) e.stopPropagation(); + }, + + renderHeader() { + // header 值为 true 显示空白头部 + const defaultHeader = ; + const header = renderTNodeJSX(this, 'header', defaultHeader); + const headerClassName = this.isFullScreen + ? [`${this.componentName}__header`, `${this.componentName}__header--fullscreen`] + : `${this.componentName}__header`; + const closeClassName = this.isFullScreen + ? [`${this.componentName}__close`, `${this.componentName}__close--fullscreen`] + : `${this.componentName}__close`; + const { CloseIcon } = this.useGlobalIcon({ + CloseIcon: TdCloseIcon, + }); + const defaultCloseBtn = ; + const getIcon = () => { + const { InfoCircleFilledIcon, CheckCircleFilledIcon, ErrorCircleFilledIcon } = this.useGlobalIcon({ + InfoCircleFilledIcon: TdInfoCircleFilledIcon, + CheckCircleFilledIcon: TdCheckCircleFilledIcon, + ErrorCircleFilledIcon: TdErrorCircleFilledIcon, + }); + const icon = { + default: null as null, + info: , + warning: , + danger: , + success: , + }; + return icon[this.theme]; + }; + return ( + (header || this?.closeBtn) && ( + + + {getIcon()} + {header} + + {this.closeBtn ? ( + + {renderTNodeJSX(this, 'closeBtn', defaultCloseBtn)} + + ) : null} + + ) + ); + }, + + renderBody() { + const body = renderContent(this, 'default', 'body'); + const bodyClassName = this.theme === 'default' + ? [`${this.componentName}__body`] + : [`${this.componentName}__body`, `${this.componentName}__body__icon`]; + + if (this.isFullScreen && !!this.footer) { + bodyClassName.push(`${this.componentName}__body--fullscreen`); + } else if (this.isFullScreen) { + bodyClassName.push(`${this.componentName}__body--fullscreen--without-footer`); + } + return ( + + {body} + + ); + }, + + renderFooter() { + const footerClassName = this.isFullScreen + ? [`${this.componentName}__footer`, `${this.componentName}__footer--fullscreen`] + : `${this.componentName}__footer`; + // this.getConfirmBtn is a function of ActionMixin + // this.getCancelBtn is a function of ActionMixin + const defaultFooter = ( + + {this.getCancelBtn({ + cancelBtn: this.cancelBtn, + globalCancel: this.instanceGlobal?.cancel || this.global.cancel, + className: `${this.componentName}__cancel`, + })} + {this.getConfirmBtn({ + theme: this.theme, + confirmBtn: this.confirmBtn, + confirmLoading: this.confirmLoading, + globalConfirm: this.instanceGlobal?.confirm || this.global.confirm, + globalConfirmBtnTheme: this.instanceGlobal?.confirmBtnTheme || this.global.confirmBtnTheme, + className: `${this.componentName}__confirm`, + })} + + ); + const footerContent = renderTNodeJSX(this, 'footer', defaultFooter); + return ( + + ); + }, + }, + + render() { + return ( + + {this.renderHeader()} + {this.renderBody()} + {!!this.footer && this.renderFooter()} + + ); + }, +}); diff --git a/src/dialog/dialog.md b/src/dialog/dialog.md index 2758b062d..552158a0e 100644 --- a/src/dialog/dialog.md +++ b/src/dialog/dialog.md @@ -35,6 +35,12 @@ {{ plugin }} ## API +### DialogCard Props + +名称 | 类型 | 默认值 | 描述 | 必传 +-- | -- | -- | -- | -- +`Pick` | String / Slot / Function | - | 继承 `Pick` 中的全部属性。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts) | N + ### Dialog Props 名称 | 类型 | 默认值 | 描述 | 必传 @@ -55,7 +61,7 @@ dialogStyle | Object | - | 作用于对话框本身的样式。TS 类型:`Styl draggable | Boolean | false | 对话框是否可以拖拽(仅在非模态对话框时有效) | N footer | Boolean / Slot / Function | true | 底部操作栏,默认会有“确认”和“取消”两个按钮。值为 true 显示默认操作按钮,值为 false 不显示任何内容,值类型为 Function 表示自定义底部内容。TS 类型:`boolean \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts) | N header | String / Boolean / Slot / Function | true | 头部内容。值为 true 显示空白头部,值为 false 不显示任何内容,值类型为 string 则直接显示值,值类型为 Function 表示自定义头部内容。TS 类型:`string \| boolean \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts) | N -mode | String | modal | 对话框类型,有 4 种:模态对话框、非模态对话框、普通对话框、全屏对话框。弹出「模态对话框」时,只能操作对话框里面的内容,不能操作其他内容。弹出「非模态对话框」时,则可以操作页面内所有内容。「普通对话框」是指没有脱离文档流的对话框,可以在这个基础上开发更多的插件。可选项:modal/modeless/normal/full-screen | N +mode | String | modal | 对话框类型,有 3 种:模态对话框、非模态对话框、全屏对话框。弹出「模态对话框」时,只能操作对话框里面的内容,不能操作其他内容。弹出「非模态对话框」时,则可以操作页面内所有内容。可选项:modal/modeless/full-screen | N placement | String | top | 对话框位置,内置两种:垂直水平居中显示 和 靠近顶部(top:20%)显示。默认情况,为避免贴顶或贴底,顶部和底部距离最小为 `48px`,可通过调整 `top` 覆盖默认大小。可选项:top/center | N preventScrollThrough | Boolean | true | 防止滚动穿透 | N showInAttachedElement | Boolean | false | 仅在挂载元素中显示抽屉,默认在浏览器可视区域显示。父元素需要有定位属性,如:position: relative | N diff --git a/src/dialog/dialog.tsx b/src/dialog/dialog.tsx index dc4b71d7c..f1c94d1b8 100644 --- a/src/dialog/dialog.tsx +++ b/src/dialog/dialog.tsx @@ -1,33 +1,17 @@ import Vue from 'vue'; import { isNumber, throttle } from 'lodash-es'; -import { - CloseIcon as TdCloseIcon, - InfoCircleFilledIcon as TdInfoCircleFilledIcon, - CheckCircleFilledIcon as TdCheckCircleFilledIcon, - ErrorCircleFilledIcon as TdErrorCircleFilledIcon, -} from 'tdesign-icons-vue'; - import TButton from '../button'; -import ActionMixin from './actions'; import { DialogCloseContext, TdDialogProps } from './type'; import props from './props'; -import { renderTNodeJSX, renderContent } from '../utils/render-tnode'; import mixins from '../utils/mixins'; -import getConfigReceiverMixins, { - DialogConfig, - getGlobalIconMixins, - getAttachConfigMixins, -} from '../config-provider/config-receiver'; +import getConfigReceiverMixins, { DialogConfig, getAttachConfigMixins } from '../config-provider/config-receiver'; import TransferDom from '../utils/transfer-dom'; import { emitEvent } from '../utils/event'; import { AttachNode, ClassName, Styles } from '../common'; import { updateElement } from '../hooks/useDestroyOnClose'; import stack from './stack'; import { getScrollbarWidth } from '../_common/js/utils/getScrollbarWidth'; - -function getCSSValue(v: string | number) { - return isNaN(Number(v)) ? v : `${Number(v)}px`; -} +import TDialogCard from './dialog-card'; let mousePosition: { x: number; y: number } | null; const getClickPosition = (e: MouseEvent) => { @@ -46,12 +30,7 @@ if (typeof window !== 'undefined' && window.document && window.document.document let key = 1; -export default mixins( - ActionMixin, - getConfigReceiverMixins('dialog'), - getGlobalIconMixins(), - getAttachConfigMixins('dialog'), -).extend({ +export default mixins(getConfigReceiverMixins('dialog'), getAttachConfigMixins('dialog')).extend({ name: 'TDialog', components: { @@ -87,41 +66,25 @@ export default mixins( isModeLess(): boolean { return this.mode === 'modeless'; }, - // 是否普通对话框,没有脱离文档流的对话框 - isNormal(): boolean { - return this.mode === 'normal'; - }, isFullScreen(): boolean { return this.mode === 'full-screen'; }, maskClass(): ClassName { return [`${this.componentName}__mask`, !this.showOverlay && `${this.classPrefix}-is-hidden`]; }, - dialogClass(): ClassName { - const dialogClass = [ - `${this.componentName}`, - `${this.componentName}__modal-${this.theme}`, - this.isModeLess && this.draggable && `${this.componentName}--draggable`, - ]; - if (this.isFullScreen) { - dialogClass.push(`${this.componentName}__fullscreen`); - } else { - dialogClass.push(`${this.componentName}--default`); - } - return dialogClass; - }, positionClass(): ClassName { - if (this.isNormal) return []; if (this.isFullScreen) return [`${this.componentName}__position_fullscreen`]; - const dialogClass = [ - `${this.componentName}__position`, - !!this.top && `${this.componentName}--top`, - `${this.placement && !this.top ? `${this.componentName}--${this.placement}` : ''}`, - ]; - return dialogClass; + if (this.isModal || this.isModeLess) { + return [ + `${this.componentName}__position`, + !!this.top && `${this.componentName}--top`, + `${this.placement && !this.top ? `${this.componentName}--${this.placement}` : ''}`, + ]; + } + return []; }, wrapClass(): ClassName { - return [!this.isNormal && `${this.componentName}__wrap`]; + return [(this.isModal || this.isModeLess || this.isFullScreen) && `${this.componentName}__wrap`]; }, ctxClass(): ClassName { // dialog__ctx--fixed 绝对定位 @@ -149,11 +112,10 @@ export default mixins( } return topStyle; }, - computedDialogStyle(): Styles { - return !this.isFullScreen ? { width: getCSSValue(this.width), ...this.dialogStyle } : { ...this.dialogStyle }; // width全屏模式不生效; - }, computedAttach(): AttachNode { - return this.showInAttachedElement || this.isNormal ? undefined : this.attach || this.globalAttach(); + return this.showInAttachedElement || !this.isModal || !this.isModeLess || !this.isFullScreen + ? undefined + : this.attach || this.globalAttach(); }, }, @@ -171,7 +133,7 @@ export default mixins( } this.$nextTick(() => { - const target = this.$refs.dialog as HTMLElement; + const target = (this.$refs.dialog as Vue).$el as HTMLElement; if (mousePosition && target) { target.style.transformOrigin = `${mousePosition.x - target.offsetLeft}px ${ mousePosition.y - target.offsetTop @@ -252,7 +214,7 @@ export default mixins( destroySelf() { this.$el.parentNode?.removeChild?.(this.$el); }, - // 多个dialog情况,若有些给了默认值true,出现ESC关闭不了弹窗问题解决 + // 解决多个dialog场景,若有些给了默认值true,出现ESC关闭不了弹窗的问题 storeUid(flag: boolean) { if (flag) { stack.push(this.uid); @@ -329,7 +291,7 @@ export default mixins( // 关闭弹窗动画结束时事件 afterLeave() { if (this.isModeLess && this.draggable) { - const target = this.$refs.dialog as HTMLElement; + const target = (this.$refs.dialog as Vue).$el as HTMLElement; if (!target) return; // 关闭弹窗 清空拖拽设置的相关css target.style.position = 'relative'; @@ -346,32 +308,18 @@ export default mixins( this.$emit('update:visible', false); }, - // Vue在引入阶段对事件的处理还做了哪些初始化操作。Vue在实例上用一个_events属性存贮管理事件的派发和更新, - // 暴露出$on, $once, $off, $emit方法给外部管理事件和派发执行事件 - // 所以通过判断_events某个事件下监听函数数组是否超过一个,可以判断出组件是否监听了当前事件 - hasEventOn(name: string) { - // _events 因没有被暴露在vue实例接口中,只能把这个规则注释掉 - /* eslint-disable dot-notation */ - const eventFuncs = this['_events']?.[name]; - return !!eventFuncs?.length; - }, + // // Vue在引入阶段对事件的处理还做了哪些初始化操作。Vue在实例上用一个_events属性存贮管理事件的派发和更新, + // // 暴露出$on, $once, $off, $emit方法给外部管理事件和派发执行事件 + // // 所以通过判断_events某个事件下监听函数数组是否超过一个,可以判断出组件是否监听了当前事件 + // hasEventOn(name: string) { + // // _events 因没有被暴露在vue实例接口中,只能把这个规则注释掉 + // /* eslint-disable dot-notation */ + // const eventFuncs = this['_events']?.[name]; + // return !!eventFuncs?.length; + // }, - getIcon() { - const { InfoCircleFilledIcon, CheckCircleFilledIcon, ErrorCircleFilledIcon } = this.useGlobalIcon({ - InfoCircleFilledIcon: TdInfoCircleFilledIcon, - CheckCircleFilledIcon: TdCheckCircleFilledIcon, - ErrorCircleFilledIcon: TdErrorCircleFilledIcon, - }); - const icon = { - info: , - warning: , - danger: , - success: , - }; - return icon[this.theme]; - }, mousedownHandler(targetEvent: MouseEvent) { - const target = this.$refs.dialog as HTMLElement; + const target = (this.$refs.dialog as Vue).$el as HTMLElement; // 算出鼠标相对元素的位置 this.disX = targetEvent.clientX - target.offsetLeft; this.disY = targetEvent.clientY - target.offsetTop; @@ -389,7 +337,7 @@ export default mixins( document.addEventListener('dragend', this.mouseUpHandler); }, mouseMoverHandler(documentEvent: MouseEvent) { - const target = this.$refs.dialog as HTMLElement; + const target = (this.$refs.dialog as Vue).$el as HTMLElement; // 用鼠标的位置减去鼠标相对元素的位置,得到元素的位置 let left = documentEvent.clientX - this.disX; let top = documentEvent.clientY - this.disY; @@ -410,7 +358,7 @@ export default mixins( document.removeEventListener('dragend', this.mouseUpHandler); }, initDragEvent(status: boolean) { - const target = this.$refs.dialog as HTMLElement; + const target = (this.$refs.dialog as Vue).$el as HTMLElement; if (status) { target.addEventListener('mousedown', this.mousedownHandler); } else { @@ -422,7 +370,7 @@ export default mixins( */ resizeAdjustPosition() { if (this.visible) { - const target = this.$refs.dialog as HTMLElement; + const target = (this.$refs.dialog as Vue).$el as HTMLElement; target.style.left = `${this.dLeft * (window.innerWidth / this.windowInnerWidth)}px`; target.style.top = `${this.dTop * (window.innerHeight / this.windowInnerHeight)}px`; } @@ -431,81 +379,31 @@ export default mixins( if (this.isModeLess && this.draggable) e.stopPropagation(); }, renderDialog() { - const { CloseIcon } = this.useGlobalIcon({ - CloseIcon: TdCloseIcon, - }); - // header 值为 true 显示空白头部 - const defaultHeader = ; - const defaultCloseBtn = ; - const body = renderContent(this, 'default', 'body'); - // this.getConfirmBtn is a function of ActionMixin - // this.getCancelBtn is a function of ActionMixin - const defaultFooter = ( - - {this.getCancelBtn({ - cancelBtn: this.cancelBtn, - globalCancel: this.instanceGlobal?.cancel || this.global.cancel, - className: `${this.componentName}__cancel`, - })} - {this.getConfirmBtn({ - theme: this.theme, - confirmBtn: this.confirmBtn, - confirmLoading: this.confirmLoading, - globalConfirm: this.instanceGlobal?.confirm || this.global.confirm, - globalConfirmBtnTheme: this.instanceGlobal?.confirmBtnTheme || this.global.confirmBtnTheme, - className: `${this.componentName}__confirm`, - })} - - ); - const headerClassName = this.isFullScreen - ? [`${this.componentName}__header`, `${this.componentName}__header--fullscreen`] - : `${this.componentName}__header`; - const closeClassName = this.isFullScreen - ? [`${this.componentName}__close`, `${this.componentName}__close--fullscreen`] - : `${this.componentName}__close`; - const bodyClassName = this.theme === 'default' - ? [`${this.componentName}__body`] - : [`${this.componentName}__body`, `${this.componentName}__body__icon`]; - - const footerContent = renderTNodeJSX(this, 'footer', defaultFooter); - - if (this.isFullScreen && footerContent) { - bodyClassName.push(`${this.componentName}__body--fullscreen`); - } else if (this.isFullScreen) { - bodyClassName.push(`${this.componentName}__body--fullscreen--without-footer`); - } - const footerClassName = this.isFullScreen - ? [`${this.componentName}__footer`, `${this.componentName}__footer--fullscreen`] - : `${this.componentName}__footer`; - - const footer = this.footer ? ( - - ) : null; + /* eslint-disable @typescript-eslint/no-unused-vars */ + const { + dialogClassName, onConfirm, onCancel, onClose, ...otherProps + } = this.$props; + /* eslint-enable @typescript-eslint/no-unused-vars */ // 此处获取定位方式 top 优先级较高 存在时 默认使用top定位 return ( // 非模态形态下draggable为true才允许拖拽 - - - - {this.getIcon()} - {renderTNodeJSX(this, 'header', defaultHeader)} - - {this.closeBtn ? ( - - {renderTNodeJSX(this, 'closeBtn', defaultCloseBtn)} - - ) : null} - - - - {body} - - {footer} - + ); diff --git a/src/dialog/index.ts b/src/dialog/index.ts index 5c2a194fa..05088052d 100644 --- a/src/dialog/index.ts +++ b/src/dialog/index.ts @@ -1,4 +1,5 @@ import _Dialog from './dialog'; +import _DialogCard from './dialog-card'; import withInstall from '../utils/withInstall'; import { TdDialogProps } from './type'; @@ -8,5 +9,7 @@ export * from './type'; export type DialogProps = TdDialogProps; export const Dialog = withInstall(_Dialog); +export const DialogCard = withInstall(_DialogCard); + export { default as DialogPlugin } from './plugin'; export default Dialog; diff --git a/src/dialog/type.ts b/src/dialog/type.ts index a74f1d6a4..099df97b8 100644 --- a/src/dialog/type.ts +++ b/src/dialog/type.ts @@ -80,10 +80,10 @@ export interface TdDialogProps { */ header?: string | boolean | TNode; /** - * 对话框类型,有 4 种:模态对话框、非模态对话框、普通对话框、全屏对话框。弹出「模态对话框」时,只能操作对话框里面的内容,不能操作其他内容。弹出「非模态对话框」时,则可以操作页面内所有内容。「普通对话框」是指没有脱离文档流的对话框,可以在这个基础上开发更多的插件 + * 对话框类型,有 3 种:模态对话框、非模态对话框、全屏对话框。弹出「模态对话框」时,只能操作对话框里面的内容,不能操作其他内容。弹出「非模态对话框」时,则可以操作页面内所有内容。 * @default modal */ - mode?: 'modal' | 'modeless' | 'normal' | 'full-screen'; + mode?: 'modal' | 'modeless' | 'full-screen'; /** * 对话框位置,内置两种:垂直水平居中显示 和 靠近顶部(top:20%)显示。默认情况,为避免贴顶或贴底,顶部和底部距离最小为 `48px`,可通过调整 `top` 覆盖默认大小 * @default top @@ -180,6 +180,7 @@ export interface TdDialogCardProps | 'onCancel' | 'onCloseBtnClick' | 'onConfirm' + | 'confirmLoading' > {} export interface DialogOptions extends Omit { diff --git a/src/dialog/utils/index.ts b/src/dialog/utils/index.ts new file mode 100644 index 000000000..3345beb25 --- /dev/null +++ b/src/dialog/utils/index.ts @@ -0,0 +1,5 @@ +export function getCSSValue(v: string | number) { + return isNaN(Number(v)) ? v : `${Number(v)}px`; +} + +export default getCSSValue; diff --git a/test/snap/__snapshots__/csr.test.js.snap b/test/snap/__snapshots__/csr.test.js.snap index 803b1e03a..22d0382c7 100644 --- a/test/snap/__snapshots__/csr.test.js.snap +++ b/test/snap/__snapshots__/csr.test.js.snap @@ -44576,7 +44576,84 @@ exports[`csr snapshot test > csr test ./src/dialog/_example/async.vue 1`] = ` + > + + + + + + + + 保存 + + + + + + + + + 保存中,请稍后 + + + + + + + `; @@ -44655,7 +44732,92 @@ exports[`csr snapshot test > csr test ./src/dialog/_example/attach.vue 1`] = ` data-v-d38b880a="" duration="300" name="t-dialog-zoom__vue" - /> + > + + + + + + + + 挂载在body + + + + + + + + + + + 被挂载到 body 元素的对话框 + + + + + + + + + csr test ./src/dialog/_example/attach.vue 1`] = ` - csr test ./src/dialog/_example/attach.vue 1`] = ` class="t-dialog__wrap" > csr test ./src/dialog/_example/attach.vue 1`] = ` - 对话框仅展示在挂载元素区域 + 函数返回挂载节点 csr test ./src/dialog/_example/attach.vue 1`] = ` - 父元素(挂载元素)需要有定位属性,如:position: relative + 指定函数返回的节点为挂载点 - showInAttachedElement API 仅针对模态对话框有效 + 函数返回为DOM节点对象 @@ -44856,95 +45013,101 @@ exports[`csr snapshot test > csr test ./src/dialog/_example/attach.vue 1`] = ` - - + - - 函数返回挂载节点 - - - - - - - - - 指定函数返回的节点为挂载点 + 对话框仅展示在挂载元素区域 + + + + + + + - 函数返回为DOM节点对象 + + 父元素(挂载元素)需要有定位属性,如:position: relative + + + showInAttachedElement API 仅针对模态对话框有效 + - - - + `; @@ -44963,255 +45126,238 @@ exports[`csr snapshot test > csr test ./src/dialog/_example/base.vue 1`] = ` - - -`; - -exports[`csr snapshot test > csr test ./src/dialog/_example/custom.vue 1`] = ` - - - 弹窗内容自定义 - - - + > - - - 隐藏标题 - - - + class="t-dialog__mask" + /> - - - 渲染函数定义内容 - - + + + 对话框标题 + + + + + + + + + + + + + 弹窗二 + + + + + + + 共 30 条数据 + + + + + + + + + 10 条/页 + + + + + + + + + + + + + + + + + + 1 + + + 2 + + + 3 + + + + + + + + + + + + + + + + + + + - - - 插槽方式定义内容 - - - - - - - - - - - - - 操作按钮自定义 - - - - 底部按钮有两个控制属性:confirmBtn 和 cancelBtn。属性类型有多种:string | ButtonProps | TNode。也可以通过 footer 来自定义控制 - - - - - - - 按钮文字 - - - - - - - 按钮属性 - - - - - - - 渲染函数按钮 - - - - - - - 隐藏底部 - - - - - - - - - -`; - -exports[`csr snapshot test > csr test ./src/dialog/_example/drag.vue 1`] = ` - - - - - - 非模态对话框-可拖拽 - - - - - - - 非模态对话框-不可拖拽 - - - - - - - 模态对话框-不支持拖拽 - - - - - - - - - -`; - -exports[`csr snapshot test > csr test ./src/dialog/_example/icon.vue 1`] = ` - - - - - - csr test ./src/dialog/_example/icon.vue 1`] = ` - - - - 下单确认 + 对话框标题二 csr test ./src/dialog/_example/icon.vue 1`] = ` - 信息已全部保存,是否确认下单? + 对话框内容二 + +`; + +exports[`csr snapshot test > csr test ./src/dialog/_example/custom.vue 1`] = ` + + + 弹窗内容自定义 + - + - - + 隐藏标题 + + + + + + + 渲染函数定义内容 + + + + + + + 插槽方式定义内容 + + + + + + + + + - - - - - 温馨提示 - - - - - - - - - 系统重启后会短暂影响页面访问,确认重启吗? - - - - - - - + + - - - - 推送失败 + 对话框标题 + + + + + + - - - - - - - - 请检查推送数据是否符合要求 - - + - - - - - + + - - - - 操作成功 + 对话框标题 + + + + + + - - - - - - - - 是否前往查看订单列表 - - + - - + + + + + + + 操作按钮自定义 + + + + 底部按钮有两个控制属性:confirmBtn 和 cancelBtn。属性类型有多种:string | ButtonProps | TNode。也可以通过 footer 来自定义控制 + - - - theme: info - - - - - - -`; - -exports[`csr snapshot test > csr test ./src/dialog/_example/modal.vue 1`] = ` - csr test ./src/dialog/_example/modal.vue 1`] = ` - 模态对话框 + 按钮文字 @@ -45603,7 +45764,7 @@ exports[`csr snapshot test > csr test ./src/dialog/_example/modal.vue 1`] = ` - 非模态对话框 + 按钮属性 @@ -45617,7 +45778,7 @@ exports[`csr snapshot test > csr test ./src/dialog/_example/modal.vue 1`] = ` - 非模态对话框-不可拖拽 + 渲染函数按钮 @@ -45631,36 +45792,27 @@ exports[`csr snapshot test > csr test ./src/dialog/_example/modal.vue 1`] = ` - 普通对话框-不可拖拽 + 隐藏底部 - - - + csr test ./src/dialog/_example/modal.vue 1`] = ` - 普通对话框-不可拖拽 + 提示 csr test ./src/dialog/_example/modal.vue 1`] = ` - - - 对话框内容 - - + 自定义底部按钮,直接传入文字 @@ -45730,176 +45878,232 @@ exports[`csr snapshot test > csr test ./src/dialog/_example/modal.vue 1`] = ` - -`; - -exports[`csr snapshot test > csr test ./src/dialog/_example/plugin.vue 1`] = ` - - - - - dialog - - - - - - - handleDialogNode - - - - - - - confirm - - - - - - - alert - - - - - - - DialogPlugin.confirm - - - - -`; - -exports[`csr snapshot test > csr test ./src/dialog/_example/position.vue 1`] = ` - - - - - 默认位置 - - - - - + - - 垂直居中 - - + + + + 提示 + + + + + + + + + 自定义底部按钮,传入 ButtonProps + + + + + + + - + - - 自定义top - - + + + + 对话框标题 + + + + + + + + + 自定义底部按钮,传入自定义组件 + + + + + + + - + - - 文本溢出 - - + + + + 对话框标题 + + + + + + + + + 不需要底部按钮的内容 + + + + - - - - - + `; -exports[`csr snapshot test > csr test ./src/dialog/_example/warning.vue 1`] = ` +exports[`csr snapshot test > csr test ./src/dialog/_example/drag.vue 1`] = ` csr test ./src/dialog/_example/warning.vue 1`] = ` - 提示反馈 + 非模态对话框-可拖拽 @@ -45929,7 +46133,7 @@ exports[`csr snapshot test > csr test ./src/dialog/_example/warning.vue 1`] = ` - 成功反馈 + 非模态对话框-不可拖拽 @@ -45943,13 +46147,1708 @@ exports[`csr snapshot test > csr test ./src/dialog/_example/warning.vue 1`] = ` - 警示反馈 + 模态对话框-不支持拖拽 - + + + + + + + + + + + 模态对话框 + + + + + + + + + + + 默认点击蒙层或按ESC可关闭 + + + 对话框内容 + + + + + + + + + + + + + + + + + 非模态对话框-可拖拽 + + + + + + + + + + + 对话框内容 + + + + + + + + + + + + + + + + + 非模态对话框-不可拖拽 + + + + + + + + + + + 对话框内容 + + + + + + + + + + +`; + +exports[`csr snapshot test > csr test ./src/dialog/_example/icon.vue 1`] = ` + + + + + + + + + + + + 下单确认 + + + + + + + + + 信息已全部保存,是否确认下单? + + + + + + + + + + + + + + + + + + + 温馨提示 + + + + + + + + + 系统重启后会短暂影响页面访问,确认重启吗? + + + + + + + + + + + + + + + + + + + 推送失败 + + + + + + + + + 请检查推送数据是否符合要求 + + + + + + + + + + + + + + + + + + + 操作成功 + + + + + + + + + 是否前往查看订单列表 + + + + + + + + + + + theme: info + + + + + + +`; + +exports[`csr snapshot test > csr test ./src/dialog/_example/modal.vue 1`] = ` + + + + + + 模态对话框 + + + + + + + 非模态对话框 + + + + + + + 非模态对话框-不可拖拽 + + + + + + + 普通对话框-不可拖拽 + + + + + + + + + + + + + 模态对话框 + + + + + + + + + + + 默认点击蒙层或按ESC可关闭 + + + 对话框内容 + + + + + + + + + + + + + + + + + 非模态对话框 + + + + + + + + + + + 对话框内容 + + + + + + + + + + + + + + + + + 非模态对话框-不可拖拽 + + + + + + + + + + + 对话框内容 + + + + + + + + + + + + + + + + + 普通对话框-不可拖拽 + + + + + + + + + + + 对话框内容 + + + + + + + + + + +`; + +exports[`csr snapshot test > csr test ./src/dialog/_example/plugin.vue 1`] = ` + + + + + dialog + + + + + + + handleDialogNode + + + + + + + confirm + + + + + + + alert + + + + + + + DialogPlugin.confirm + + + + +`; + +exports[`csr snapshot test > csr test ./src/dialog/_example/position.vue 1`] = ` + + + + + + 默认位置 + + + + + + + 垂直居中 + + + + + + + 自定义top + + + + + + + 文本溢出 + + + + + + + + + + + + + 对话框标题 + + + + + + + + + 对话框内容 + + + + + + + + + + + + + + + + 对话框标题 + + + + + + + + + + 水平居中显示的对话框 + + + + + + + + + + + + + + + + + 对话框标题 + + + + + + + + + 自定义对话框距离窗口顶部位置,top: 50px + + + + + + + + + + + + + + + + 文本溢出对话框标题 + + + + + + + + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + 水平居中显示的文本溢出对话框 + + + + + + + + + +`; + +exports[`csr snapshot test > csr test ./src/dialog/_example/warning.vue 1`] = ` + + + + + + 提示反馈 + + + + + + + 成功反馈 + + + + + + + 警示反馈 + + + + csr test ./src/dialog/_example/warning.vue 1`] = ` + + + 自定义图标 + + + + + + + + + + + + + + + + 提示 + + + + + + + + + 对话框内容 + + + + + + + + + + + + + + + + + + + 成功 + + + + + + + + + 对话框内容 + + + + + + + + + - + - - 自定义图标 - - + + + + + + + 警示 + + + + + + + + + 对话框内容 + + + + + - - - - + + > + + + + + + + + + + + 错误 + + + + + + + + + 对话框内容 + + + + + + + + > + + + + + + + + + + + + + 对话框标题 + + + + + + 对话框内容 + + + + + + + `;
- 弹窗内容自定义 -
- 操作按钮自定义 -
- 底部按钮有两个控制属性:confirmBtn 和 cancelBtn。属性类型有多种:string | ButtonProps | TNode。也可以通过 footer 来自定义控制 -
+ 弹窗内容自定义 +
+ 操作按钮自定义 +
+ 底部按钮有两个控制属性:confirmBtn 和 cancelBtn。属性类型有多种:string | ButtonProps | TNode。也可以通过 footer 来自定义控制 +
+ 水平居中显示的对话框 +
+ 水平居中显示的文本溢出对话框 +