mirror of
https://github.com/element-plus/element-plus.git
synced 2025-02-23 11:59:34 +08:00
refactor(components): [dialog] refactor (#6300)
* refactor(components): [dialog] refactor - fix TS type. - enhance prop type `beforeClose` - move `token.ts` to `@element-plus/tokens` * refactor: resolve review comments * test: fix slots
This commit is contained in:
parent
ce614197d7
commit
cb6300c739
@ -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<string, any>) => {
|
||||
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<typeof Dialog>).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,
|
||||
|
@ -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)
|
||||
|
@ -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<DialogBeforeCloseFn>(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,
|
||||
}
|
||||
|
@ -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<HTMLElement | null>(null)
|
||||
const headerRef = ref<HTMLElement | null>(null)
|
||||
const dialogRef = ref<HTMLElement>()
|
||||
const headerRef = ref<HTMLElement>()
|
||||
|
||||
const dialog = useDialog(
|
||||
props,
|
||||
{ emit } as SetupContext<DialogEmits>,
|
||||
dialogRef as Ref<HTMLElement>
|
||||
)
|
||||
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<HTMLElement>,
|
||||
headerRef as Ref<HTMLElement>,
|
||||
draggable
|
||||
)
|
||||
useDraggable(dialogRef, headerRef, draggable)
|
||||
|
||||
defineExpose({
|
||||
/** @description whether the dialog is visible */
|
||||
visible,
|
||||
})
|
||||
</script>
|
||||
|
@ -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<DialogEmits>,
|
||||
targetRef: Ref<HTMLElement | undefined>
|
||||
) => {
|
||||
const instance = getCurrentInstance()!
|
||||
const emit = instance.emit as SetupContext<DialogEmits>['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
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@ export const ON_MOUNT_FOCUS_EVT = 'mountOnFocus'
|
||||
export const ON_UNMOUNT_FOCUS_EVT = 'unmountOnFocus'
|
||||
|
||||
export type FocusTrapInjectionContext = {
|
||||
focusTrapRef: Ref<HTMLElement | null>
|
||||
focusTrapRef: Ref<HTMLElement | undefined>
|
||||
onKeydown: (e: KeyboardEvent) => void
|
||||
}
|
||||
|
||||
|
@ -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 = () => {
|
||||
|
@ -2,13 +2,12 @@ import type { ComputedRef, CSSProperties, InjectionKey, Ref } from 'vue'
|
||||
import type { useNamespace } from '@element-plus/hooks'
|
||||
|
||||
export type DialogContext = {
|
||||
dialogRef: Ref<HTMLElement | null>
|
||||
headerRef: Ref<HTMLElement | null>
|
||||
dialogRef: Ref<HTMLElement | undefined>
|
||||
headerRef: Ref<HTMLElement | undefined>
|
||||
ns: ReturnType<typeof useNamespace>
|
||||
rendered: Ref<boolean>
|
||||
style: ComputedRef<CSSProperties>
|
||||
}
|
||||
|
||||
export const elDialogInjectionKey: InjectionKey<DialogContext> = Symbol(
|
||||
'elDialogInjectionKey'
|
||||
)
|
||||
export const dialogInjectionKey: InjectionKey<DialogContext> =
|
||||
Symbol('dialogInjectionKey')
|
@ -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'
|
||||
|
@ -1,9 +1,9 @@
|
||||
import type { ComponentPublicInstance, Ref } from 'vue'
|
||||
|
||||
export const composeRefs = (...refs: Ref<HTMLElement | null>[]) => {
|
||||
export const composeRefs = (...refs: Ref<HTMLElement | undefined>[]) => {
|
||||
return (el: Element | ComponentPublicInstance | null) => {
|
||||
refs.forEach((ref) => {
|
||||
ref.value = el as HTMLElement | null
|
||||
ref.value = el as HTMLElement | undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user