diff --git a/packages/components/dialog/__tests__/dialog.spec.ts b/packages/components/dialog/__tests__/dialog.spec.ts index 7d91843eba..522a545c2a 100644 --- a/packages/components/dialog/__tests__/dialog.spec.ts +++ b/packages/components/dialog/__tests__/dialog.spec.ts @@ -6,25 +6,14 @@ import { Delete } from '@element-plus/icons-vue' import Dialog from '../src/dialog.vue' const AXIOM = 'Rem is the best girl' - -const _mount = ({ slots, ...rest }: Record) => { - return mount(Dialog, { - slots: { - default: AXIOM, - ...slots, - }, - ...rest, - }) -} +const defaultSlots = { default: () => AXIOM } jest.useFakeTimers() describe('Dialog.vue', () => { test('render test', async () => { - const wrapper = _mount({ - slots: { - default: AXIOM, - }, + const wrapper = mount(Dialog, { + slots: defaultSlots, props: { modelValue: true, }, @@ -38,9 +27,10 @@ describe('Dialog.vue', () => { test('dialog should have a title when title has been given', async () => { const HEADER = 'I am header' - let wrapper = _mount({ + let wrapper = mount(Dialog, { slots: { - title: HEADER, + ...defaultSlots, + title: () => HEADER, }, props: { modelValue: true, @@ -49,7 +39,8 @@ describe('Dialog.vue', () => { await nextTick() expect(wrapper.find('.el-dialog__header').text()).toBe(HEADER) - wrapper = _mount({ + wrapper = mount(Dialog, { + slots: defaultSlots, props: { title: HEADER, modelValue: true, @@ -61,9 +52,10 @@ describe('Dialog.vue', () => { }) test('dialog should have a footer when footer has been given', async () => { - const wrapper = _mount({ + const wrapper = mount(Dialog, { slots: { - footer: AXIOM, + ...defaultSlots, + footer: () => AXIOM, }, props: { modelValue: true, @@ -75,7 +67,8 @@ describe('Dialog.vue', () => { }) test('should append dialog to body when appendToBody is true', async () => { - const wrapper = _mount({ + const wrapper = mount(Dialog, { + slots: defaultSlots, props: { appendToBody: true, modelValue: true, @@ -89,7 +82,8 @@ describe('Dialog.vue', () => { }) test('should center dialog', async () => { - const wrapper = _mount({ + const wrapper = mount(Dialog, { + slots: defaultSlots, props: { center: true, modelValue: true, @@ -100,7 +94,8 @@ describe('Dialog.vue', () => { }) test('should show close button', async () => { - const wrapper = _mount({ + const wrapper = mount(Dialog, { + slots: defaultSlots, props: { modelValue: true, }, @@ -110,7 +105,8 @@ describe('Dialog.vue', () => { }) test('should hide close button when showClose = false', async () => { - const wrapper = _mount({ + const wrapper = mount(Dialog, { + slots: defaultSlots, props: { modelValue: true, showClose: false, @@ -121,19 +117,21 @@ describe('Dialog.vue', () => { }) test('should close dialog when click on close button', async () => { - const wrapper = _mount({ + const wrapper = mount(Dialog, { props: { modelValue: true, }, + slots: defaultSlots, }) await nextTick() await wrapper.find('.el-dialog__headerbtn').trigger('click') - expect((wrapper.vm as InstanceType).visible).toBe(false) + expect(wrapper.vm.visible).toBe(false) }) describe('mask related', () => { test('should not have overlay mask when mask is false', async () => { - const wrapper = _mount({ + const wrapper = mount(Dialog, { + slots: defaultSlots, props: { modal: false, modelValue: true, @@ -144,7 +142,8 @@ describe('Dialog.vue', () => { }) test('should close the modal when clicking on mask when `closeOnClickModal` is true', async () => { - const wrapper = _mount({ + const wrapper = mount(Dialog, { + slots: defaultSlots, props: { modelValue: true, }, @@ -161,7 +160,8 @@ describe('Dialog.vue', () => { describe('life cycles', () => { test('should call before close', async () => { const beforeClose = jest.fn() - const wrapper = _mount({ + const wrapper = mount(Dialog, { + slots: defaultSlots, props: { beforeClose, modelValue: true, @@ -177,7 +177,8 @@ describe('Dialog.vue', () => { .fn() .mockImplementation((hide: (cancel: boolean) => void) => hide(true)) - const wrapper = _mount({ + const wrapper = mount(Dialog, { + slots: defaultSlots, props: { beforeClose, modelValue: true, @@ -190,7 +191,8 @@ describe('Dialog.vue', () => { }) test('should open and close with delay', async () => { - const wrapper = _mount({ + const wrapper = mount(Dialog, { + slots: defaultSlots, props: { openDelay: 200, closeDelay: 200, @@ -203,16 +205,11 @@ describe('Dialog.vue', () => { await wrapper.setProps({ modelValue: true, }) - - // expect(wrapper.vm.visible).toBe(false) - - // jest.runOnlyPendingTimers() - - // expect(wrapper.vm.visible).toBe(true) }) test('should destroy on close', async () => { - const wrapper = _mount({ + const wrapper = mount(Dialog, { + slots: defaultSlots, props: { modelValue: true, destroyOnClose: true, @@ -238,7 +235,8 @@ describe('Dialog.vue', () => { let visible = true const onClose = jest.fn() const onClosed = jest.fn() - const wrapper = _mount({ + const wrapper = mount(Dialog, { + slots: defaultSlots, props: { modelValue: visible, 'onUpdate:modelValue': (val: boolean) => (visible = val), @@ -262,7 +260,8 @@ describe('Dialog.vue', () => { }) test('closeIcon', async () => { - const wrapper = _mount({ + const wrapper = mount(Dialog, { + slots: defaultSlots, props: { modelValue: true, closeIcon: markRaw(Delete), @@ -277,10 +276,8 @@ describe('Dialog.vue', () => { }) test('should render draggable prop', async () => { - const wrapper = _mount({ - slots: { - default: AXIOM, - }, + const wrapper = mount(Dialog, { + slots: defaultSlots, props: { modelValue: true, draggable: true, diff --git a/packages/components/dialog/src/dialog-content.vue b/packages/components/dialog/src/dialog-content.vue index 1519d41dc3..d7144e14c5 100644 --- a/packages/components/dialog/src/dialog-content.vue +++ b/packages/components/dialog/src/dialog-content.vue @@ -45,17 +45,16 @@ import { inject } from 'vue' import { ElIcon } from '@element-plus/components/icon' import { CloseComponents } from '@element-plus/utils' +import { dialogInjectionKey } from '@element-plus/tokens' import { dialogContentProps, dialogContentEmits } from './dialog-content' -import { elDialogInjectionKey } from './token' - const { Close } = CloseComponents defineOptions({ name: 'ElDialogContent' }) defineProps(dialogContentProps) defineEmits(dialogContentEmits) -const { dialogRef, headerRef, ns, style } = inject(elDialogInjectionKey)! +const { dialogRef, headerRef, ns, style } = inject(dialogInjectionKey)! // const { focusTrapRef, onKeydown } = inject(FOCUS_TRAP_INJECTION_KEY)! // const composedDialogRef = composeRefs(focusTrapRef, dialogRef) diff --git a/packages/components/dialog/src/dialog.ts b/packages/components/dialog/src/dialog.ts index a2f61c1688..ea2e3a5e2e 100644 --- a/packages/components/dialog/src/dialog.ts +++ b/packages/components/dialog/src/dialog.ts @@ -1,9 +1,12 @@ -import { buildProps, definePropType } from '@element-plus/utils' +import { buildProps, definePropType, isBoolean } from '@element-plus/utils' import { UPDATE_MODEL_EVENT } from '@element-plus/constants' import { dialogContentProps } from './dialog-content' import type { ExtractPropTypes } from 'vue' +type DoneFn = (cancel: boolean) => void +export type DialogBeforeCloseFn = (done: DoneFn) => void + export const dialogProps = buildProps({ ...dialogContentProps, appendToBody: { @@ -11,7 +14,7 @@ export const dialogProps = buildProps({ default: false, }, beforeClose: { - type: definePropType<(...args: any[]) => void>(Function), + type: definePropType(Function), }, destroyOnClose: { type: Boolean, @@ -68,7 +71,7 @@ export const dialogEmits = { opened: () => true, close: () => true, closed: () => true, - [UPDATE_MODEL_EVENT]: (value: boolean) => typeof value === 'boolean', + [UPDATE_MODEL_EVENT]: (value: boolean) => isBoolean(value), openAutoFocus: () => true, closeAutoFocus: () => true, } diff --git a/packages/components/dialog/src/dialog.vue b/packages/components/dialog/src/dialog.vue index 3f2ab0e07d..e5efb27dce 100644 --- a/packages/components/dialog/src/dialog.vue +++ b/packages/components/dialog/src/dialog.vue @@ -49,42 +49,35 @@ import { computed, ref, provide } from 'vue' import { ElOverlay } from '@element-plus/components/overlay' import { useNamespace, useDraggable, useSameTarget } from '@element-plus/hooks' +import { dialogInjectionKey } from '@element-plus/tokens' import ElDialogContent from './dialog-content.vue' import { dialogProps, dialogEmits } from './dialog' -import { elDialogInjectionKey } from './token' import { useDialog } from './use-dialog' -import type { SetupContext, Ref } from 'vue' -import type { DialogEmits } from './dialog' - defineOptions({ name: 'ElDialog', }) const props = defineProps(dialogProps) -const emit = defineEmits(dialogEmits) +defineEmits(dialogEmits) const ns = useNamespace('dialog') -const dialogRef = ref(null) -const headerRef = ref(null) +const dialogRef = ref() +const headerRef = ref() -const dialog = useDialog( - props, - { emit } as SetupContext, - dialogRef as Ref -) const { visible, + style, + rendered, + zIndex, afterEnter, afterLeave, beforeLeave, - style, handleClose, - rendered, - zIndex, -} = dialog + onModalClick, +} = useDialog(props, dialogRef) -provide(elDialogInjectionKey, { +provide(dialogInjectionKey, { dialogRef, headerRef, ns, @@ -92,13 +85,14 @@ provide(elDialogInjectionKey, { style, }) -const overlayEvent = useSameTarget(dialog.onModalClick) +const overlayEvent = useSameTarget(onModalClick) const draggable = computed(() => props.draggable && !props.fullscreen) -useDraggable( - dialogRef as Ref, - headerRef as Ref, - draggable -) +useDraggable(dialogRef, headerRef, draggable) + +defineExpose({ + /** @description whether the dialog is visible */ + visible, +}) diff --git a/packages/components/dialog/src/use-dialog.ts b/packages/components/dialog/src/use-dialog.ts index 9529feb800..40498ab024 100644 --- a/packages/components/dialog/src/use-dialog.ts +++ b/packages/components/dialog/src/use-dialog.ts @@ -1,4 +1,11 @@ -import { computed, ref, watch, nextTick, onMounted } from 'vue' +import { + computed, + ref, + watch, + nextTick, + onMounted, + getCurrentInstance, +} from 'vue' import { useTimeoutFn, isClient } from '@vueuse/core' import { @@ -15,14 +22,16 @@ import type { DialogEmits, DialogProps } from './dialog' export const useDialog = ( props: DialogProps, - { emit }: SetupContext, targetRef: Ref ) => { + const instance = getCurrentInstance()! + const emit = instance.emit as SetupContext['emit'] + const { nextZIndex } = useZIndex() + let lastPosition = '' const visible = ref(false) const closed = ref(false) const rendered = ref(false) // when desctroyOnClose is true, we initialize it as false vise versa - const { nextZIndex } = useZIndex() const zIndex = ref(props.zIndex || nextZIndex()) let openTimer: (() => void) | undefined = undefined @@ -74,7 +83,6 @@ export const useDialog = ( } function close() { - // if (this.willClose && !this.willClose()) return; openTimer?.() closeTimer?.() @@ -85,13 +93,13 @@ export const useDialog = ( } } - function hide(shouldCancel: boolean) { - if (shouldCancel) return - closed.value = true - visible.value = false - } - function handleClose() { + function hide(shouldCancel: boolean) { + if (shouldCancel) return + closed.value = true + visible.value = false + } + if (props.beforeClose) { props.beforeClose(hide) } else { @@ -106,13 +114,7 @@ export const useDialog = ( } function doOpen() { - if (!isClient) { - return - } - - // if (props.willOpen?.()) { - // return - // } + if (!isClient) return visible.value = true } diff --git a/packages/components/focus-trap/src/tokens.ts b/packages/components/focus-trap/src/tokens.ts index 2549194526..897ea8eb17 100644 --- a/packages/components/focus-trap/src/tokens.ts +++ b/packages/components/focus-trap/src/tokens.ts @@ -11,7 +11,7 @@ export const ON_MOUNT_FOCUS_EVT = 'mountOnFocus' export const ON_UNMOUNT_FOCUS_EVT = 'unmountOnFocus' export type FocusTrapInjectionContext = { - focusTrapRef: Ref + focusTrapRef: Ref onKeydown: (e: KeyboardEvent) => void } diff --git a/packages/hooks/use-draggable/index.ts b/packages/hooks/use-draggable/index.ts index d4e8d8ce89..f5e636aedd 100644 --- a/packages/hooks/use-draggable/index.ts +++ b/packages/hooks/use-draggable/index.ts @@ -17,7 +17,7 @@ export const useDraggable = ( const downY = e.clientY const { offsetX, offsetY } = transform - const targetRect = targetRef.value.getBoundingClientRect() + const targetRect = targetRef.value!.getBoundingClientRect() const targetLeft = targetRect.left const targetTop = targetRect.top const targetWidth = targetRect.width @@ -45,9 +45,9 @@ export const useDraggable = ( offsetX: moveX, offsetY: moveY, } - targetRef.value.style.transform = `translate(${addUnit(moveX)}, ${addUnit( - moveY - )})` + targetRef.value!.style.transform = `translate(${addUnit( + moveX + )}, ${addUnit(moveY)})` } const onMouseup = () => { diff --git a/packages/components/dialog/src/token.ts b/packages/tokens/dialog.ts similarity index 59% rename from packages/components/dialog/src/token.ts rename to packages/tokens/dialog.ts index 0191ffab82..1158681af5 100644 --- a/packages/components/dialog/src/token.ts +++ b/packages/tokens/dialog.ts @@ -2,13 +2,12 @@ import type { ComputedRef, CSSProperties, InjectionKey, Ref } from 'vue' import type { useNamespace } from '@element-plus/hooks' export type DialogContext = { - dialogRef: Ref - headerRef: Ref + dialogRef: Ref + headerRef: Ref ns: ReturnType rendered: Ref style: ComputedRef } -export const elDialogInjectionKey: InjectionKey = Symbol( - 'elDialogInjectionKey' -) +export const dialogInjectionKey: InjectionKey = + Symbol('dialogInjectionKey') diff --git a/packages/tokens/index.ts b/packages/tokens/index.ts index e910abc672..6ec6e42e71 100644 --- a/packages/tokens/index.ts +++ b/packages/tokens/index.ts @@ -1,9 +1,10 @@ -export * from './form' +export * from './breadcrumb' export * from './button' export * from './collapse' -export * from './breadcrumb' -export * from './pagination' export * from './config-provider' +export * from './dialog' +export * from './form' +export * from './pagination' export * from './radio' -export * from './tabs' export * from './scrollbar' +export * from './tabs' diff --git a/packages/utils/vue/refs.ts b/packages/utils/vue/refs.ts index 9c7c267a48..eae59f571c 100644 --- a/packages/utils/vue/refs.ts +++ b/packages/utils/vue/refs.ts @@ -1,9 +1,9 @@ import type { ComponentPublicInstance, Ref } from 'vue' -export const composeRefs = (...refs: Ref[]) => { +export const composeRefs = (...refs: Ref[]) => { return (el: Element | ComponentPublicInstance | null) => { refs.forEach((ref) => { - ref.value = el as HTMLElement | null + ref.value = el as HTMLElement | undefined }) } }