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:
三咲智子 2022-03-01 23:43:50 +08:00 committed by GitHub
parent ce614197d7
commit cb6300c739
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 100 additions and 105 deletions

View File

@ -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,

View File

@ -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)

View File

@ -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,
}

View File

@ -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>

View File

@ -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
}

View File

@ -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
}

View File

@ -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 = () => {

View File

@ -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')

View File

@ -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'

View File

@ -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
})
}
}