fix(overlay): Fix overlay event triggering issue (#1235)

This commit is contained in:
jeremywu 2021-01-14 17:01:37 +08:00 committed by GitHub
parent e4ced422c6
commit 30f1947c47
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 795 additions and 657 deletions

View File

@ -1,8 +1,10 @@
import { nextTick } from 'vue'
import { mount } from '@vue/test-utils'
import { rAF } from '@element-plus/test-utils/tick'
import triggerCompositeClick from '@element-plus/test-utils/composite-click'
import Dialog from '../'
const AXIOM = 'Rem is the best girl'
const _mount = ({ slots, ...rest }: Indexable<any>) => {
@ -150,7 +152,7 @@ describe('Dialog.vue', () => {
await nextTick()
expect(wrapper.find('.el-overlay').exists()).toBe(true)
await wrapper.find('.el-overlay').trigger('click')
await triggerCompositeClick(wrapper.find('.el-overlay'))
expect(wrapper.vm.visible).toBe(false)
})
})
@ -249,7 +251,7 @@ describe('Dialog.vue', () => {
await rAF()
await nextTick()
await wrapper.find('.el-overlay').trigger('click')
await triggerCompositeClick(wrapper.find('.el-overlay'))
await nextTick()
await rAF()
await nextTick()

View File

@ -28,7 +28,7 @@
role="dialog"
:aria-label="title || 'dialog'"
:style="style"
@click="$event.stopPropagation()"
@click.stop=""
>
<div class="el-dialog__header">
<slot name="title">

View File

@ -9,7 +9,6 @@ const isVisibleMock = jest
import TrapFocus, {
ITrapFocusElement,
FOCUSABLE_CHILDREN,
TRAP_FOCUS_HANDLER,
} from '../trap-focus'
let wrapper
@ -45,9 +44,6 @@ describe('v-trap-focus', () => {
expect(
(wrapper.element as ITrapFocusElement)[FOCUSABLE_CHILDREN].length,
).toBe(1)
expect(
(wrapper.element as ITrapFocusElement)[TRAP_FOCUS_HANDLER].length,
).toBeDefined()
})
test('should not fetch disabled element', () => {

View File

@ -12,52 +12,62 @@ export interface ITrapFocusElement extends HTMLElement {
[TRAP_FOCUS_HANDLER]: (e: KeyboardEvent) => void
}
const FOCUS_STACK = []
const FOCUS_HANDLER = (e: KeyboardEvent) => {
// Getting the top layer.
if (FOCUS_STACK.length === 0) return
const focusableElement = FOCUS_STACK[FOCUS_STACK.length - 1][FOCUSABLE_CHILDREN]
if (focusableElement.length > 0 && e.code === EVENT_CODE.tab) {
if (focusableElement.length === 1) {
e.preventDefault()
if (document.activeElement !== focusableElement[0]) {
focusableElement[0].focus()
}
return
}
const goingBackward = e.shiftKey
const isFirst = e.target === focusableElement[0]
const isLast = e.target === focusableElement[focusableElement.length - 1]
if (isFirst && goingBackward) {
e.preventDefault()
focusableElement[focusableElement.length - 1].focus()
}
if (isLast && !goingBackward) {
e.preventDefault()
focusableElement[0].focus()
}
// the is critical since jsdom did not implement user actions, you can only mock it
// DELETE ME: when testing env switches to puppeteer
if (process.env.NODE_ENV === 'test') {
const index = focusableElement.findIndex((element: Element) => element === e.target)
if (index !== -1) {
focusableElement[goingBackward ? index - 1 : index + 1]?.focus()
}
}
}
}
const TrapFocus: ObjectDirective = {
beforeMount(el: ITrapFocusElement) {
el[FOCUSABLE_CHILDREN] = obtainAllFocusableElements(el)
el[TRAP_FOCUS_HANDLER] = (e: KeyboardEvent) => {
const focusableElement = el[FOCUSABLE_CHILDREN]
if (focusableElement.length > 0 && e.code === EVENT_CODE.tab) {
if (focusableElement.length === 1) {
e.preventDefault()
if (document.activeElement !== focusableElement[0]) {
focusableElement[0].focus()
}
return
}
const goingBackward = e.shiftKey
const isFirst = e.target === focusableElement[0]
const isLast = e.target === focusableElement[focusableElement.length - 1]
if (isFirst && goingBackward) {
e.preventDefault()
focusableElement[focusableElement.length - 1].focus()
}
if (isLast && !goingBackward) {
e.preventDefault()
focusableElement[0].focus()
}
// the is critical since jsdom did not implement user actions, you can only mock it
// DELETE ME: when testing env switches to puppeteer
if (process.env.NODE_ENV === 'test') {
const index = focusableElement.findIndex(element => element === e.target)
if (index !== -1) {
focusableElement[goingBackward ? index - 1 : index + 1]?.focus()
}
}
}
FOCUS_STACK.push(el)
if (FOCUS_STACK.length <= 1) {
on(document, 'keydown', FOCUS_HANDLER)
}
on(document, 'keydown', el[TRAP_FOCUS_HANDLER])
},
updated(el: ITrapFocusElement) {
nextTick(() => {
el[FOCUSABLE_CHILDREN] = obtainAllFocusableElements(el)
})
},
unmounted(el: ITrapFocusElement) {
off(document, 'keydown', el[TRAP_FOCUS_HANDLER])
unmounted() {
FOCUS_STACK.shift()
if (FOCUS_STACK.length === 0) {
off(document, 'keydown', FOCUS_HANDLER)
}
},
}

View File

@ -14,50 +14,41 @@
@click="onModalClick"
>
<div
class="el-drawer__container"
:class="{ 'el-drawer__open': visible }"
tabindex="-1"
role="document"
ref="drawerRef"
v-trap-focus
aria-modal="true"
aria-labelledby="el-drawer__title"
:aria-label="title"
:class="['el-drawer', direction, customClass]"
:style="isHorizontal ? 'width: ' + size : 'height: ' + size"
role="dialog"
@click.stop
>
<div
ref="drawerRef"
v-trap-focus
aria-modal="true"
aria-labelledby="el-drawer__title"
:aria-label="title"
class="el-drawer"
:class="[direction, customClass]"
:style="isHorizontal ? 'width: ' + size : 'height: ' + size"
role="dialog"
tabindex="-1"
@click.stop
<header
v-if="withHeader"
id="el-drawer__title"
class="el-drawer__header"
>
<header
v-if="withHeader"
id="el-drawer__title"
class="el-drawer__header"
<slot name="title">
<span role="heading" :title="title">
{{ title }}
</span>
</slot>
<button
v-if="showClose"
:aria-label="'close ' + (title || 'drawer')"
class="el-drawer__close-btn"
type="button"
@click="handleClose"
>
<slot name="title">
<span role="heading" tabindex="-1" :title="title">
{{ title }}
</span>
</slot>
<button
v-if="showClose"
:aria-label="'close ' + (title || 'drawer')"
class="el-drawer__close-btn"
type="button"
@click="handleClose"
>
<i class="el-drawer__close el-icon el-icon-close"></i>
</button>
</header>
<template v-if="rendered">
<section class="el-drawer__body">
<slot></slot>
</section>
</template>
</div>
<i class="el-drawer__close el-icon el-icon-close"></i>
</button>
</header>
<template v-if="rendered">
<section class="el-drawer__body">
<slot></slot>
</section>
</template>
</div>
</el-overlay>
</transition>

View File

@ -0,0 +1,48 @@
import { ref } from 'vue'
import { on, off } from '@element-plus/utils/dom'
import triggerEvent from '@element-plus/test-utils/trigger-event'
import usePreventGlobal from '../use-prevent-global'
describe('usePreventGlobal', () => {
const evtName = 'keydown'
const evt = jest.fn()
beforeAll(() => {
on(document.body, evtName, evt)
})
beforeEach(() => {
evt.mockClear()
})
afterAll(() => {
off(document.body, evtName, evt)
})
it('should prevent global event from happening', () => {
const visible = ref(true)
const evt2Trigger = jest.fn().mockReturnValue(true)
usePreventGlobal(visible, evtName, evt2Trigger)
triggerEvent(document.body, evtName)
expect(evt).not.toBeCalled()
expect(evt2Trigger).toHaveBeenCalled()
visible.value = false
// clean up
})
it('should not prevent global event from happening', () => {
const visible = ref(true)
const evt2Trigger = jest.fn().mockReturnValue(false)
usePreventGlobal(visible, evtName, evt2Trigger)
triggerEvent(document.body, evtName)
expect(evt).toHaveBeenCalled()
expect(evt2Trigger).toHaveBeenCalled()
visible.value = false
})
})

View File

@ -6,3 +6,4 @@ export { default as useModal } from './use-modal'
export { default as useMigrating } from './use-migrating'
export { default as useFocus } from './use-focus'
export { default as useThrottleRender } from './use-throttle-render'
export { default as usePreventGlobal } from './use-prevent-global'

View File

@ -14,6 +14,7 @@ const modalStack: ModalInstance[] = []
const closeModal = (e: KeyboardEvent) => {
if (modalStack.length === 0) return
if (e.code === EVENT_CODE.esc) {
e.stopPropagation()
const topModal = modalStack[modalStack.length - 1]
topModal.handleClose()
}

View File

@ -0,0 +1,19 @@
import { watch } from 'vue'
import { on, off } from '@element-plus/utils/dom'
import type { Ref } from 'vue'
export default (indicator: Ref<boolean>, evt: string, cb: (e: Event) => boolean) => {
const prevent = (e: Event) => {
if (cb(e)) {
e.stopImmediatePropagation()
}
}
watch(() => indicator.value, val => {
if (val) {
on(document, evt, prevent, true)
} else {
off(document, evt, prevent, true)
}
}, { immediate: true })
}

View File

@ -1,18 +1,28 @@
import { mount } from '@vue/test-utils'
import MessageBox from '../src/messageBox'
import { sleep } from '@element-plus/test-utils'
import { nextTick } from 'vue'
import { rAF } from '@element-plus/test-utils/tick'
import { triggerNativeCompositeClick } from '@element-plus/test-utils/composite-click'
const selector = '.el-message-box__wrapper'
const selector = '.el-overlay'
const _mount = (invoker: () => void) => {
return mount(
{
template: '<div></div>',
mounted() {
invoker()
},
},
{
attachTo: 'body',
},
)
}
describe('MessageBox', () => {
afterEach(() => {
const el = document.querySelector('.el-message-box__wrapper')
if (!el) return
if (el.parentNode) {
el.parentNode.removeChild(el)
}
afterEach(async () => {
MessageBox.close()
await rAF()
})
test('create and close', async () => {
@ -22,17 +32,23 @@ describe('MessageBox', () => {
message: '这是一段内容',
})
const msgbox: HTMLElement = document.querySelector(selector)
expect(msgbox).toBeDefined()
await sleep()
expect(msgbox.querySelector('.el-message-box__title span').textContent).toEqual('消息')
expect(msgbox.querySelector('.el-message-box__message').querySelector('p').textContent).toEqual('这是一段内容')
await rAF()
expect(
msgbox.querySelector('.el-message-box__title span').textContent,
).toEqual('消息')
expect(
msgbox.querySelector('.el-message-box__message').querySelector('p')
.textContent,
).toEqual('这是一段内容')
MessageBox.close()
await sleep(250)
await rAF()
expect(msgbox.style.display).toEqual('none')
})
test('invoke with strings', () => {
MessageBox('消息', '这是一段内容')
MessageBox({ title: '消息', message: '这是一段内容' })
const msgbox = document.querySelector(selector)
expect(msgbox).toBeDefined()
})
@ -43,7 +59,7 @@ describe('MessageBox', () => {
iconClass: 'el-icon-question',
message: '这是一段内容',
})
await sleep()
await rAF()
const icon = document.querySelector('.el-message-box__status')
expect(icon.classList.contains('el-icon-question')).toBe(true)
})
@ -54,25 +70,32 @@ describe('MessageBox', () => {
dangerouslyUseHTMLString: true,
message: '<strong>html string</strong>',
})
await sleep()
await rAF()
const message = document.querySelector('.el-message-box__message strong')
expect(message.textContent).toEqual('html string')
})
test('distinguish cancel and close', async () => {
let msgAction = ''
MessageBox({
title: '消息',
message: '这是一段内容',
distinguishCancelAndClose: true,
callback: action => {
msgAction = action
},
})
await sleep()
const btn = document.querySelector('.el-message-box__close') as HTMLButtonElement
const invoker = () => {
MessageBox({
title: '消息',
message: '这是一段内容',
distinguishCancelAndClose: true,
callback: action => {
msgAction = action
},
})
}
_mount(invoker)
await rAF()
const btn = document.querySelector(
'.el-message-box__close',
) as HTMLButtonElement
btn.click()
await sleep()
await rAF()
expect(msgAction).toEqual('close')
})
@ -81,26 +104,27 @@ describe('MessageBox', () => {
title: '标题名称',
type: 'warning',
})
await sleep()
const vModal: HTMLElement = document.querySelector('.v-modal')
vModal.click()
await sleep(250)
await rAF()
await triggerNativeCompositeClick(document.querySelector(selector))
await rAF()
const msgbox: HTMLElement = document.querySelector(selector)
expect(msgbox.style.display).toEqual('')
expect(msgbox.querySelector('.el-icon-warning')).toBeDefined()
})
test('confirm', async () => {
test('confirm', async () => {
MessageBox.confirm('这是一段内容', {
title: '标题名称',
type: 'warning',
})
await sleep()
const btn = document.querySelector(selector).querySelector('.el-button--primary') as HTMLButtonElement
await rAF()
const btn = document
.querySelector(selector)
.querySelector('.el-button--primary') as HTMLButtonElement
btn.click()
await sleep(250)
await rAF()
const msgbox: HTMLElement = document.querySelector(selector)
expect(msgbox.style.display).toEqual('none')
expect(msgbox).toBe(null)
})
test('prompt', async () => {
@ -109,9 +133,13 @@ describe('MessageBox', () => {
inputPattern: /test/,
inputErrorMessage: 'validation failed',
})
await sleep(0)
const inputElm = document.querySelector(selector).querySelector('.el-message-box__input')
const haveFocus = inputElm.querySelector('input').isSameNode(document.activeElement)
await rAF()
const inputElm = document
.querySelector(selector)
.querySelector('.el-message-box__input')
const haveFocus = inputElm
.querySelector('input')
.isSameNode(document.activeElement)
expect(inputElm).toBeDefined()
expect(haveFocus).toBe(true)
})
@ -121,8 +149,10 @@ describe('MessageBox', () => {
inputType: 'textarea',
title: '标题名称',
})
await sleep()
const textareaElm = document.querySelector(selector).querySelector('textarea')
await rAF()
const textareaElm = document
.querySelector(selector)
.querySelector('textarea')
const haveFocus = textareaElm.isSameNode(document.activeElement)
expect(haveFocus).toBe(true)
})
@ -136,55 +166,65 @@ describe('MessageBox', () => {
msgAction = action
},
})
await sleep()
const closeBtn = document.querySelector('.el-message-box__close') as HTMLButtonElement
await rAF()
const closeBtn = document.querySelector(
'.el-message-box__close',
) as HTMLButtonElement
closeBtn.click()
await sleep()
await rAF()
expect(msgAction).toEqual('cancel')
})
test('beforeClose', async() => {
test('beforeClose', async () => {
let msgAction = ''
MessageBox({
callback: action => {
msgAction = action
},
title: '消息',
message: '这是一段内容',
beforeClose: (action, instance) => {
instance.close()
beforeClose: (_, __, done) => {
done()
},
}, action => {
msgAction = action
})
await sleep();
(document.querySelector('.el-message-box__wrapper .el-button--primary') as HTMLButtonElement).click()
await nextTick()
await sleep()
await rAF()
;(document.querySelector(
'.el-message-box__btns .el-button--primary',
) as HTMLButtonElement).click()
await rAF()
expect(msgAction).toEqual('confirm')
})
describe('promise', () => {
test('resolve',async () => {
test('resolve', async () => {
let msgAction = ''
MessageBox.confirm('此操作将永久删除该文件, 是否继续?', '提示')
.then(action => {
MessageBox.confirm('此操作将永久删除该文件, 是否继续?', '提示').then(
action => {
msgAction = action
})
await sleep()
const btn = document.querySelector('.el-message-box__btns .el-button--primary') as HTMLButtonElement
},
)
await rAF()
const btn = document.querySelector(
'.el-message-box__btns .el-button--primary',
) as HTMLButtonElement
btn.click()
await sleep()
await rAF()
expect(msgAction).toEqual('confirm')
})
test('reject', async () => {
let msgAction = ''
MessageBox.confirm('此操作将永久删除该文件, 是否继续?', '提示')
.catch(action => {
MessageBox.confirm('此操作将永久删除该文件, 是否继续?', '提示').catch(
action => {
msgAction = action
})
await sleep()
const btn = document.querySelectorAll('.el-message-box__btns .el-button') as NodeListOf<HTMLButtonElement>
btn[0].click()
await sleep()
},
)
await rAF()
const btn = document.querySelector(
'.el-message-box__btns .el-button',
)
;(btn as HTMLButtonElement).click()
await rAF()
expect(msgAction).toEqual('cancel')
})
})

View File

@ -1,23 +1,32 @@
<template>
<transition name="msgbox-fade">
<div
<transition name="fade-in-linear" @after-leave="$emit('vanish')">
<el-overlay
v-show="visible"
ref="root"
:aria-label="title || 'dialog'"
class="el-message-box__wrapper"
tabindex="-1"
role="dialog"
aria-modal="true"
:z-index="state.zIndex"
:overlay-class="modalClass"
:mask="modal"
@click.self="handleWrapperClick"
>
<div class="el-message-box" :class="[customClass, center && 'el-message-box--center']">
<div v-if="title !== null && title !== undefined" class="el-message-box__header">
<div
ref="root"
v-trap-focus
:aria-label="title || 'dialog'"
aria-modal="true"
:class="[
'el-message-box',
customClass,
{ 'el-message-box--center': center },
]"
>
<div
v-if="title !== null && title !== undefined"
class="el-message-box__header"
>
<div class="el-message-box__title">
<div
v-if="icon && center"
:class="['el-message-box__status', icon]"
>
</div>
></div>
<span>{{ title }}</span>
</div>
<button
@ -25,8 +34,12 @@
type="button"
class="el-message-box__headerbtn"
aria-label="Close"
@click="handleAction(distinguishCancelAndClose ? 'close' : 'cancel')"
@keydown.enter="handleAction(distinguishCancelAndClose ? 'close' : 'cancel')"
@click="
handleAction(distinguishCancelAndClose ? 'close' : 'cancel')
"
@keydown.enter="
handleAction(distinguishCancelAndClose ? 'close' : 'cancel')
"
>
<i class="el-message-box__close el-icon-close"></i>
</button>
@ -36,8 +49,7 @@
<div
v-if="icon && !center && hasMessage"
:class="['el-message-box__status', icon]"
>
</div>
></div>
<div v-if="hasMessage" class="el-message-box__message">
<slot>
<p v-if="!dangerouslyUseHTMLString">{{ message }}</p>
@ -47,47 +59,54 @@
</div>
<div v-show="showInput" class="el-message-box__input">
<el-input
ref="input"
v-model="inputValue"
ref="inputRef"
v-model="state.inputValue"
:type="inputType"
:placeholder="inputPlaceholder"
:class="{ invalid: validateError }"
@keydown.enter="handleInputEnter"
:class="{ invalid: state.validateError }"
@keydown.prevent.enter="handleInputEnter"
/>
<div class="el-message-box__errormsg" :style="{ visibility: !!editorErrorMessage ? 'visible' : 'hidden' }">{{ editorErrorMessage }}</div>
<div
class="el-message-box__errormsg"
:style="{
visibility: !!state.editorErrorMessage ? 'visible' : 'hidden',
}"
>
{{ state.editorErrorMessage }}
</div>
</div>
</div>
<div class="el-message-box__btns">
<el-button
v-if="showCancelButton"
:loading="cancelButtonLoading"
:class="[ cancelButtonClass ]"
:loading="state.cancelButtonLoading"
:class="[cancelButtonClass]"
:round="roundButton"
size="small"
@click="handleAction('cancel')"
@keydown.enter="handleAction('cancel')"
>
{{ cancelButtonText || t('el.messagebox.cancel') }}
{{ state.cancelButtonText || t('el.messagebox.cancel') }}
</el-button>
<el-button
v-show="showConfirmButton"
ref="confirm"
:loading="confirmButtonLoading"
:class="[ confirmButtonClasses ]"
ref="confirmRef"
:loading="state.confirmButtonLoading"
:class="[confirmButtonClasses]"
:round="roundButton"
:disabled="confirmButtonDisabled"
:disabled="state.confirmButtonDisabled"
size="small"
@click="handleAction('confirm')"
@keydown.enter="handleAction('confirm')"
>
{{ confirmButtonText || t('el.messagebox.confirm') }}
{{ state.confirmButtonText || t('el.messagebox.confirm') }}
</el-button>
</div>
</div>
</div>
</el-overlay>
</transition>
</template>
<script lang='ts'>
<script lang="ts">
import {
defineComponent,
nextTick,
@ -95,19 +114,21 @@ import {
onBeforeUnmount,
computed,
watch,
onBeforeMount,
getCurrentInstance,
reactive,
toRefs,
ref,
} from 'vue'
import ElButton from '@element-plus/button'
import ElInput from '@element-plus/input'
import { t } from '@element-plus/locale'
import Dialog from '@element-plus/utils/aria-dialog'
import usePopup from '@element-plus/utils/popup/usePopup'
import { Overlay as ElOverlay } from '@element-plus/overlay'
import { useModal, useLockScreen, useRestoreActive, usePreventGlobal } from '@element-plus/hooks'
import { TrapFocus } from '@element-plus/directives'
import PopupManager from '@element-plus/utils/popup-manager'
import { on, off } from '@element-plus/utils/dom'
import { EVENT_CODE } from '@element-plus/utils/aria'
let dialog
import type { ComponentPublicInstance, PropType } from 'vue'
import type { Action, MessageBoxState } from './message-box.type'
const TypeMap: Indexable<string> = {
success: 'success',
@ -121,41 +142,23 @@ export default defineComponent({
components: {
ElButton,
ElInput,
ElOverlay,
},
directives: {
TrapFocus,
},
props: {
openDelay: {
type: Number,
default: 0,
beforeClose: {
type: Function as PropType<(action: Action, state: MessageBoxState, doClose: () => void) => any>,
default: undefined,
},
closeDelay: {
type: Number,
default: 0,
},
zIndex: Number,
modalFade: {
type: Boolean,
default: true,
},
modalClass: {
callback: Function,
cancelButtonText: {
type: String,
default: '',
},
modalAppendToBody: {
type: Boolean,
default: false,
},
modal: {
type: Boolean,
default: true,
},
lockScroll: {
type: Boolean,
default: true,
},
showClose: {
type: Boolean,
default: true,
default: 'Cancel',
},
cancelButtonClass: String,
center: Boolean,
closeOnClickModal: {
type: Boolean,
default: true,
@ -168,78 +171,113 @@ export default defineComponent({
type: Boolean,
default: true,
},
center: {
default: false,
type: Boolean,
confirmButtonText: {
type: String,
default: 'OK',
},
roundButton: {
default: false,
type: Boolean,
confirmButtonClass: String,
container: {
type: String, // default append to body
default: 'body',
},
} ,
setup(props) {
let vm
const popup = usePopup(props, doClose)
customClass: String,
dangerouslyUseHTMLString: Boolean,
distinguishCancelAndClose: Boolean,
iconClass: String,
inputPattern: {
type: Object as PropType<RegExp>,
default: () => undefined,
validator: (val: unknown) => (val instanceof RegExp || val === 'undefined'),
},
inputPlaceholder: {
type: String,
},
inputType: {
type: String,
default: 'text',
},
inputValue: {
type: String,
},
inputValidator: {
type: Function as PropType<(...args: any[]) => boolean | string>,
default: null,
},
inputErrorMessage: String,
lockScroll: {
type: Boolean,
default: true,
},
message: String,
modalFade: { // implement this feature
type: Boolean,
default: true,
},
modalClass: String,
modal: {
type: Boolean,
default: true,
},
roundButton: Boolean,
showCancelButton: Boolean,
showConfirmButton: {
type: Boolean,
default: true,
},
showClose: {
type: Boolean,
default: true,
},
type: String,
title: String,
showInput: Boolean,
zIndex: Number,
},
emits: ['vanish', 'action'],
setup(props, { emit }) {
// const popup = usePopup(props, doClose)
const visible = ref(false)
// s represents state
const state = reactive({
uid: 1,
title: undefined,
message: '',
type: '',
iconClass: '',
customClass: '',
showInput: false,
inputValue: null,
inputPlaceholder: '',
inputType: 'text',
inputPattern: null,
inputValidator: null,
inputErrorMessage: '',
showConfirmButton: true,
showCancelButton: false,
action: '',
confirmButtonText: '',
cancelButtonText: '',
action: '' as Action,
inputValue: props.inputValue,
confirmButtonLoading: false,
cancelButtonLoading: false,
confirmButtonClass: '',
cancelButtonText: props.cancelButtonText,
confirmButtonDisabled: false,
cancelButtonClass: '',
editorErrorMessage: null,
callback: null,
dangerouslyUseHTMLString: false,
focusAfterClosed: null,
isOnComposition: false,
distinguishCancelAndClose: false,
type$: '',
visible: false,
confirmButtonText: props.confirmButtonText,
editorErrorMessage: '',
// refer to: https://github.com/ElemeFE/element/commit/2999279ae34ef10c373ca795c87b020ed6753eed
// seemed ok for now without this state.
// isOnComposition: false, // temporary remove
validateError: false,
zIndex: PopupManager.nextZIndex(),
})
const icon = computed(() => state.iconClass || (state.type && TypeMap[state.type] ? `el-icon-${ TypeMap[state.type] }` : ''))
const hasMessage = computed(() => !!state.message)
const icon = computed(() => props.iconClass || (props.type && TypeMap[props.type] ? `el-icon-${TypeMap[props.type]}` : ''))
const hasMessage = computed(() => !!props.message)
const inputRef = ref<ComponentPublicInstance>(null)
const confirmRef = ref<ComponentPublicInstance>(null)
const confirmButtonClasses = computed(() => `el-button--primary ${ state.confirmButtonClass }`)
const confirmButtonClasses = computed(() => `el-button--primary ${props.confirmButtonClass}`)
watch(() => state.inputValue, async val => {
await nextTick()
if (state.type$ === 'prompt' && val !== null) {
if (props.type === 'prompt' && val !== null) {
validate()
}
}, { immediate: true })
watch(() => state.visible, val => {
popup.state.visible = val
watch(() => visible.value, val => {
if (val) {
state.uid++
if (state.type$ === 'alert' || state.type$ === 'confirm') {
nextTick().then(() => { vm.refs.confirm.$el.focus() })
if (props.type === 'alert' || props.type === 'confirm') {
nextTick().then(() => { confirmRef.value?.$el?.focus?.() })
}
state.focusAfterClosed = document.activeElement
dialog = new Dialog(vm.vnode.el, state.focusAfterClosed, getFirstFocus())
state.zIndex = PopupManager.nextZIndex()
}
if (state.type$ !== 'prompt') return
if (props.type !== 'prompt') return
if (val) {
nextTick().then(() => {
if (vm.refs.input && vm.refs.input.$el) {
if (inputRef.value && inputRef.value.$el) {
getInputElement().focus()
}
})
@ -249,98 +287,66 @@ export default defineComponent({
}
})
onBeforeMount(() => {
vm = getCurrentInstance()
vm.setupInstall = {
state,
doClose,
}
})
onMounted(async () => {
await nextTick()
if (props.closeOnHashChange) {
on(window, 'hashchange', popup.close)
on(window, 'hashchange', doClose)
}
})
onBeforeUnmount(() => {
if (props.closeOnHashChange) {
off(window, 'hashchange', popup.close)
off(window, 'hashchange', doClose)
}
setTimeout(() => {
dialog.closeDialog()
})
})
function getSafeClose () {
const currentId = state.uid
return async () => {
await nextTick()
if (currentId === state.uid) doClose()
}
}
function doClose() {
if (!state.visible) return
state.visible = false
popup.updateClosingFlag(true)
dialog.closeDialog()
if (props.lockScroll) {
setTimeout(popup.restoreBodyStyle, 200)
}
popup.state.opened = false
popup.doAfterClose()
setTimeout(() => {
if (state.action) state.callback(state.action, state)
if (!visible.value) return
visible.value = false
nextTick(() => {
if (state.action) emit('action', state.action)
})
}
const getFirstFocus = () => {
const btn = vm.vnode.el.querySelector('.el-message-box__btns .el-button')
const title = vm.vnode.el.querySelector('.el-message-box__btns .el-message-box__title')
return btn || title
}
const handleWrapperClick = () => {
if (props.closeOnClickModal) {
handleAction(state.distinguishCancelAndClose ? 'close' : 'cancel')
handleAction(props.distinguishCancelAndClose ? 'close' : 'cancel')
}
}
const handleInputEnter = () => {
if (state.inputType !== 'textarea') {
if (props.inputType !== 'textarea') {
return handleAction('confirm')
}
}
const handleAction = action => {
if (state.type$ === 'prompt' && action === 'confirm' && !validate()) {
const handleAction = (action: Action) => {
if (props.type === 'prompt' && action === 'confirm' && !validate()) {
return
}
state.action = action
if (typeof vm.setupInstall.state.beforeClose === 'function') {
vm.setupInstall.state.close = getSafeClose()
vm.setupInstall.state.beforeClose(action, state, popup.close)
if (props.beforeClose) {
props.beforeClose?.(action, state, doClose)
} else {
doClose()
}
}
const validate = () => {
if (state.type$ === 'prompt') {
const inputPattern = state.inputPattern
if (props.type === 'prompt') {
const inputPattern = props.inputPattern
if (inputPattern && !inputPattern.test(state.inputValue || '')) {
state.editorErrorMessage = state.inputErrorMessage || t('el.messagebox.error')
state.editorErrorMessage = props.inputErrorMessage || t('el.messagebox.error')
state.validateError = true
return false
}
const inputValidator = state.inputValidator
const inputValidator = props.inputValidator
if (typeof inputValidator === 'function') {
const validateResult = inputValidator(state.inputValue)
if (validateResult === false) {
state.editorErrorMessage = state.inputErrorMessage || t('el.messagebox.error')
state.editorErrorMessage = props.inputErrorMessage || t('el.messagebox.error')
state.validateError = true
return false
}
@ -357,25 +363,50 @@ export default defineComponent({
}
const getInputElement = () => {
const inputRefs = vm.refs.input.$refs
return inputRefs.input || inputRefs.textarea
const inputRefs = inputRef.value.$refs
return (inputRefs.input || inputRefs.textarea) as HTMLElement
}
const handleClose = () => {
handleAction('close')
}
// when close on press escape is disabled, pressing esc should not callout
// any other message box and close any other dialog-ish elements
// e.g. Dialog has a close on press esc feature, and when it closes, it calls
// props.beforeClose method to make a intermediate state by callout a message box
// for some verification or alerting. then if we allow global event liek this
// to dispatch, it could callout another message box.
if (props.closeOnPressEscape) {
useModal({
handleClose,
}, visible)
} else {
usePreventGlobal(visible, 'keydown', (e: KeyboardEvent) => e.code === EVENT_CODE.esc)
}
// locks the screen to prevent scroll
if (props.lockScroll) {
useLockScreen(visible)
}
// restore to prev active element.
useRestoreActive(visible)
return {
...toRefs(state),
state,
visible,
hasMessage,
icon,
confirmButtonClasses,
inputRef,
confirmRef,
doClose, // for outside usage
handleClose, // for out side usage
handleWrapperClick,
handleInputEnter,
handleAction,
handleClose,
t,
doClose,
}
},
})

View File

@ -1,17 +1,31 @@
import { VNode } from 'vue'
export type MessageType = 'success' | 'warning' | 'info' | 'error'
export type MessageBoxCloseAction = 'confirm' | 'cancel' | 'close'
export type MessageBoxData = MessageBoxInputData
import type { VNode } from 'vue'
export type Action = 'confirm' | 'close' | 'cancel'
export type MessageType = 'success' | 'warning' | 'info' | 'error'
export type MessageBoxData = MessageBoxInputData & Action
export interface MessageBoxInputData {
value: string
action: MessageBoxCloseAction
action: Action
}
export interface MessageBoxInputValidator {
(value: string): boolean | string
}
export interface MessageBoxState {
action: Action
cancelButtonLoading: boolean
cancelButtonText: string
confirmButtonLoading: boolean
confirmButtonDisabled: boolean
confirmButtonText: string
editorErrorMessage: string
// isOnComposition: boolean temporary commented
inputValue: string
validateError: boolean
zIndex: number
}
export declare class ElMessageBoxComponent {
title: string
message: string
@ -28,7 +42,7 @@ export declare class ElMessageBoxComponent {
inputErrorMessage: string
showConfirmButton: boolean
showCancelButton: boolean
action: MessageBoxCloseAction
action: Action
dangerouslyUseHTMLString: boolean
confirmButtonText: string
cancelButtonText: string
@ -42,40 +56,25 @@ export declare class ElMessageBoxComponent {
close(): any
}
export type Callback =
| ((value: string, action: Action) => any)
| ((action: Action) => any)
/** Options used in MessageBox */
export interface ElMessageBoxOptions {
/** Title of the MessageBox */
title?: string
/** Content of the MessageBox */
message?: string | VNode
/** Message type, used for icon display */
type?: MessageType
/** Custom icon's class */
iconClass?: string
/** Callback before MessageBox closes, and it will prevent MessageBox from closing */
beforeClose?: (
action: Action,
instance: ElMessageBoxComponent,
done: () => void,
) => void
/** Custom class name for MessageBox */
customClass?: string
/** MessageBox closing callback if you don't prefer Promise */
callback?: (action: MessageBoxCloseAction, instance: ElMessageBoxComponent) => void
/** Callback before MessageBox closes, and it will prevent MessageBox from closing */
beforeClose?: (action: MessageBoxCloseAction, instance: ElMessageBoxComponent, done: (() => void)) => void
/** Whether to lock body scroll when MessageBox prompts */
lockScroll?: boolean
/** Whether to show a cancel button */
showCancelButton?: boolean
/** Whether to show a confirm button */
showConfirmButton?: boolean
/** Whether to show a close button */
showClose?: boolean
callback?: Callback
/** Text content of cancel button */
cancelButtonText?: string
@ -92,9 +91,36 @@ export interface ElMessageBoxOptions {
/** Whether to align the content in center */
center?: boolean
/** Content of the MessageBox */
message?: string | VNode
/** Title of the MessageBox */
title?: string
/** Message type, used for icon display */
type?: MessageType
/** Custom icon's class */
iconClass?: string
/** Whether message is treated as HTML string */
dangerouslyUseHTMLString?: boolean
/** Whether to distinguish canceling and closing */
distinguishCancelAndClose?: boolean
/** Whether to lock body scroll when MessageBox prompts */
lockScroll?: boolean
/** Whether to show a cancel button */
showCancelButton?: boolean
/** Whether to show a confirm button */
showConfirmButton?: boolean
/** Whether to show a close button */
showClose?: boolean
/** Whether to use round button */
roundButton?: boolean
@ -128,18 +154,22 @@ export interface ElMessageBoxOptions {
/** Error message when validation fails */
inputErrorMessage?: string
/** Whether to distinguish canceling and closing */
distinguishCancelAndClose?: boolean
}
export interface ElMessageBoxShortcutMethod {
(message: string, title: string, options?: ElMessageBoxOptions): Promise<MessageBoxData>
(message: string, options?: ElMessageBoxOptions): Promise<MessageBoxData>
}
export type ElMessageBoxShortcutMethod =
((
message: string,
title: string,
options?: ElMessageBoxOptions,
) => Promise<MessageBoxData>)
& ((
message: string,
options?: ElMessageBoxOptions,
) => Promise<MessageBoxData>)
export interface ElMessageBox {
/** Show a message box */
(message: string, title?: string, type?: string): Promise<MessageBoxData>
// (message: string, title?: string, type?: string): Promise<MessageBoxData>
/** Show a message box */
(options: ElMessageBoxOptions): Promise<MessageBoxData>
@ -153,9 +183,6 @@ export interface ElMessageBox {
/** Show a prompt message box */
prompt: ElMessageBoxShortcutMethod
/** Set default options of message boxes */
setDefaults (defaults: ElMessageBoxOptions): void
/** Close current message box */
close (): void
close(): void
}

View File

@ -1,179 +1,130 @@
import { createVNode, render } from 'vue'
import { h, render } from 'vue'
import MessageBoxConstructor from './index.vue'
import isServer from '@element-plus/utils/isServer'
import { isVNode } from '../../utils/util'
import { ElMessageBoxOptions } from './message-box.type'
import { isVNode, isString } from '@element-plus/utils/util'
let currentMsg, instance
// component default props
const PROP_KEYS = [
'lockScroll',
'showClose',
'closeOnClickModal',
'closeOnPressEscape',
'closeOnHashChange',
'center',
'roundButton',
'closeDelay',
'zIndex',
'modal',
'modalFade',
'modalClass',
'modalAppendToBody',
'lockScroll',
]
import type { ComponentPublicInstance, VNode } from 'vue'
import type {
Action,
Callback,
MessageBoxState,
ElMessageBox,
ElMessageBoxOptions,
MessageBoxData,
} from './message-box.type'
// component default merge props & data
const defaults = {
title: null,
message: '',
type: '',
iconClass: '',
showInput: false,
showClose: true,
modalFade: true,
lockScroll: true,
closeOnClickModal: true,
closeOnPressEscape: true,
closeOnHashChange: true,
inputValue: null,
inputPlaceholder: '',
inputType: 'text',
inputPattern: null,
inputValidator: null,
inputErrorMessage: '',
showConfirmButton: true,
showCancelButton: false,
confirmButtonPosition: 'right',
confirmButtonHighlight: false,
cancelButtonHighlight: false,
confirmButtonText: '',
cancelButtonText: '',
confirmButtonClass: '',
cancelButtonClass: '',
customClass: '',
beforeClose: null,
dangerouslyUseHTMLString: false,
center: false,
roundButton: false,
distinguishCancelAndClose: false,
}
let msgQueue = []
const defaultCallback = (action, ctx) => {
if (currentMsg) {
const callback = currentMsg.callback
if (typeof callback === 'function') {
if (ctx.showInput) {
callback(ctx.inputValue, action)
} else {
callback(action)
}
}
if (currentMsg.resolve) {
if (action === 'confirm') {
if (ctx.showInput) {
currentMsg.resolve({ value: ctx.inputValue, action })
} else {
currentMsg.resolve(action)
}
} else if (currentMsg.reject && (action === 'cancel' || action === 'close')) {
currentMsg.reject(action)
}
}
const messageInstance = new Map<
ComponentPublicInstance<{ doClose: () => void; }>, // marking doClose as function
{
options: any
callback: Callback
resolve: (res: any) => void
reject: (reason?: any) => void
}
}
>()
const initInstance = () => {
const container = document.createElement('div')
const vnode = createVNode(MessageBoxConstructor)
const initInstance = (props: any, container: HTMLElement) => {
const vnode = h(MessageBoxConstructor, props)
render(vnode, container)
instance = vnode.component
document.body.appendChild(container.firstElementChild)
return vnode.component
}
const showNextMsg = async () => {
if (!instance) {
initInstance()
const genContainer = () => {
return document.createElement('div')
}
const showMessage = (options: any) => {
const container = genContainer()
// Adding destruct method.
// when transition leaves emitting `vanish` evt. so that we can do the clean job.
options.onVanish = () => {
// not sure if this causes mem leak, need proof to verify that.
// maybe calling out like 1000 msg-box then close them all.
render(null, container)
messageInstance.delete(vm) // Remove vm to avoid mem leak.
// here we were suppose to call document.body.removeChild(container.firstElementChild)
// but render(null, container) did that job for us. so that we do not call that directly
}
if (instance && instance.setupInstall.state.visible) { return }
if (msgQueue.length > 0) {
const props = {}
const state = {}
currentMsg = msgQueue.shift()
const options = currentMsg.options
Object.keys(options).forEach(key => {
if (PROP_KEYS.includes(key)) {
props[key] = options[key]
options.onAction = (action: Action) => {
const currentMsg = messageInstance.get(vm)
let resolve: Action | { value: string; action: Action; }
if (options.showInput) {
resolve = { value: vm.state.inputValue, action }
} else {
resolve = action
}
if (options.callback) {
options.callback(resolve, instance.proxy)
} else {
if (action === 'cancel' || action === 'close') {
if (options.distinguishCancelAndClose && action !== 'cancel') {
currentMsg.reject('close')
} else {
currentMsg.reject('cancel')
}
} else {
state[key] = options[key]
}
})
// update props to instance/**/
const vmProps = instance.props
for (const prop in props) {
if (props.hasOwnProperty(prop)) {
vmProps[prop] = props[prop]
currentMsg.resolve(resolve)
}
}
const vmState = instance.setupInstall.state
vmState.action = ''
if (options.callback === undefined) {
options.callback = defaultCallback
}
for (const prop in state) {
if (state.hasOwnProperty(prop)) {
vmState[prop] = state[prop]
}
}
if (isVNode(options.message)) {
instance.slots.default = () => [options.message]
}
const oldCb = options.callback
vmState.callback = (action, inst) => {
oldCb(action, inst)
showNextMsg()
}
document.body.appendChild(instance.vnode.el)
vmState.visible = true
}
const instance = initInstance(options, container)
// This is how we use message box programmably.
// Maybe consider releasing a template version?
// get component instance like v2.
const vm = instance.proxy as ComponentPublicInstance<{
visible: boolean
state: MessageBoxState
doClose: () => void
}>
if (isVNode(options.message)) {
// Override slots since message is vnode type.
instance.slots.default = () => [options.message]
}
// change visibility after everything is settled
vm.visible = true
return vm
}
const MessageBox = function(options: ElMessageBoxOptions | string, callback?): Promise<any> {
async function MessageBox(options: ElMessageBoxOptions): Promise<MessageBoxData>
function MessageBox(
options: ElMessageBoxOptions | string | VNode,
): Promise<{value: string; action: Action;} | Action> {
if (isServer) return
if (typeof options === 'string' || isVNode(options)) {
let callback
if (isString(options) || isVNode(options)) {
options = {
message: options,
}
if (typeof callback === 'string') {
options.title = callback
}
} else if (options.callback && !callback) {
} else {
callback = options.callback
}
if (typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
msgQueue.push({
options: Object.assign({}, defaults, options),
callback: callback,
resolve: resolve,
reject: reject,
})
showNextMsg()
return new Promise((resolve, reject) => {
const vm = showMessage(options)
// collect this vm in order to handle upcoming events.
messageInstance.set(vm, {
options,
callback,
resolve,
reject,
})
} else {
msgQueue.push({
options: Object.assign({}, defaults, options),
callback: callback,
})
showNextMsg()
}
})
}
MessageBox.alert = (message, title, options?: ElMessageBoxOptions) => {
MessageBox.alert = (
message: string,
title: string,
options?: ElMessageBoxOptions,
) => {
if (typeof title === 'object') {
options = title
title = ''
@ -181,51 +132,78 @@ MessageBox.alert = (message, title, options?: ElMessageBoxOptions) => {
title = ''
}
return MessageBox(Object.assign({
title: title,
message: message,
type$: 'alert',
closeOnPressEscape: false,
closeOnClickModal: false,
}, options))
return MessageBox(
Object.assign(
{
title: title,
message: message,
type: 'alert',
closeOnPressEscape: false,
closeOnClickModal: false,
},
options,
),
)
}
MessageBox.confirm = (message, title, options?: ElMessageBoxOptions) => {
MessageBox.confirm = (
message: string,
title: string,
options?: ElMessageBoxOptions,
) => {
if (typeof title === 'object') {
options = title
title = ''
} else if (title === undefined) {
title = ''
}
return MessageBox(Object.assign({
title: title,
message: message,
type$: 'confirm',
showCancelButton: true,
}, options))
return MessageBox(
Object.assign(
{
title: title,
message: message,
type: 'confirm',
showCancelButton: true,
},
options,
),
)
}
MessageBox.prompt = (message, title, options?: ElMessageBoxOptions) => {
MessageBox.prompt = (
message: string,
title: string,
options?: ElMessageBoxOptions,
) => {
if (typeof title === 'object') {
options = title
title = ''
} else if (title === undefined) {
title = ''
}
return MessageBox(Object.assign({
title: title,
message: message,
showCancelButton: true,
showInput: true,
type$: 'prompt',
}, options))
return MessageBox(
Object.assign(
{
title: title,
message: message,
showCancelButton: true,
showInput: true,
type: 'prompt',
},
options,
),
)
}
MessageBox.close = () => {
instance.setupInstall.doClose()
instance.setupInstall.state.visible = false
msgQueue = []
currentMsg = null
// instance.setupInstall.doClose()
// instance.setupInstall.state.visible = false
messageInstance.forEach((_, vm) => {
vm.doClose()
})
messageInstance.clear()
}
export default MessageBox
export default MessageBox as ElMessageBox

View File

@ -18,9 +18,22 @@ export default defineComponent({
},
emits: ['click'],
setup(props, { slots, emit }) {
const onMaskClick = () => {
emit('click')
let mousedownTarget = false
let mouseupTarget = false
// refer to this https://javascript.info/mouse-events-basics
// events fired in the order: mousedown -> mouseup -> click
// we need to set the mousedown handle to false after click
// fired.
const onMaskClick = (e: MouseEvent) => {
// due to these two value were set only when props.mask is true
// so there is no need to do any extra judgment here.
// if and only if
if (mousedownTarget && mouseupTarget) {
emit('click', e)
}
mousedownTarget = mouseupTarget = false
}
// init here
return () => {
// when the vnode meets the same structure but with different change trigger
@ -34,21 +47,36 @@ export default defineComponent({
zIndex: props.zIndex,
},
onClick: onMaskClick,
onMousedown: (e: MouseEvent) => {
// marking current mousedown target.
if (props.mask) {
mousedownTarget = e.target === e.currentTarget
}
},
onMouseup: (e: MouseEvent) => {
if (props.mask) {
mouseupTarget = e.target === e.currentTarget
}
},
},
[renderSlot(slots, 'default')],
PatchFlags.STYLE | PatchFlags.CLASS | PatchFlags.PROPS,
['onClick'],
['onClick', 'onMouseup', 'onMousedown'],
)
: h('div', {
style: {
zIndex: props.zIndex,
position: 'fixed',
top: '0px',
right: '0px',
bottom: '0px',
left: '0px',
: h(
'div',
{
style: {
zIndex: props.zIndex,
position: 'fixed',
top: '0px',
right: '0px',
bottom: '0px',
left: '0px',
},
},
}, [renderSlot(slots, 'default')])
[renderSlot(slots, 'default')],
)
}
},
})

View File

@ -0,0 +1,21 @@
import { nextTick } from 'vue'
import triggerEvent from './trigger-event'
import type { DOMWrapper, VueWrapper } from '@vue/test-utils'
const triggerCompositeClick = async <T extends (VueWrapper<any> | DOMWrapper<Element>)>(wrapper: T) => {
await wrapper.trigger('mousedown')
await wrapper.trigger('mouseup')
await wrapper.trigger('click')
}
export default triggerCompositeClick
export const triggerNativeCompositeClick = async (el: Element) => {
triggerEvent(el, 'mousedown')
await nextTick()
triggerEvent(el, 'mouseup')
await nextTick()
triggerEvent(el, 'click')
return nextTick()
}

View File

@ -14,7 +14,7 @@ export const rAF = async () => {
return new Promise(res => {
requestAnimationFrame(() => {
requestAnimationFrame(async () => {
res()
res(null)
await nextTick()
})
})

View File

@ -1,6 +1,6 @@
@import "mixins/mixins";
@import "common/var";
@import "./overlay.scss";
@import 'mixins/mixins';
@import 'common/var';
@import './overlay.scss';
@keyframes el-drawer-fade-in {
0% {
@ -12,54 +12,14 @@
}
@mixin drawer-animation($direction) {
@keyframes #{$direction}-drawer-in {
0% {
@if $direction == ltr {
transform: translate(-100%, 0px);
}
@if $direction == rtl {
transform: translate(100%, 0px);
}
@if $direction == ttb {
transform: translate(0px, -100%);
}
@if $direction == btt {
transform: translate(0px, 100%);
}
}
100% {
@if $direction == ltr {
transform: translate(0px, 0px);
}
@if $direction == rtl {
transform: translate(0px, 0px);
}
@if $direction == ttb {
transform: translate(0px, 0px);
}
@if $direction == btt {
transform: translate(0px, 0px);
}
}
}
@keyframes #{$direction}-drawer-out {
@keyframes #{$direction}-drawer-animation {
0% {
@if $direction == ltr {
transform: translate(0px, 0px);
}
@if $direction == rtl {
transform: translate(0px, 0px);;
transform: translate(0px, 0px);
}
@if $direction == ttb {
@ -67,7 +27,7 @@
}
@if $direction == btt {
transform: translate(0px, 0);
transform: translate(0px, 0px);
}
}
@ -92,14 +52,14 @@
}
@mixin animation-in($direction) {
.el-drawer__open &.#{$direction} {
animation: #{$direction}-drawer-in .3s 1ms;
&.#{$direction} {
animation: #{$direction}-drawer-animation 0.3s linear reverse;
}
}
@mixin animation-out($direction) {
&.#{$direction} {
animation: #{$direction}-drawer-out .3s;
animation: #{$direction}-drawer-animation 0.3s linear;
}
}
@ -111,29 +71,23 @@
$directions: rtl, ltr, ttb, btt;
@include b(drawer) {
position: fixed;
position: absolute;
box-sizing: border-box;
background-color: $--dialog-background-color;
display: flex;
flex-direction: column;
box-shadow: 0 8px 10px -5px rgba(0, 0, 0, 0.2),
0 16px 24px 2px rgba(0, 0, 0, 0.14),
0 6px 30px 5px rgba(0, 0, 0, 0.12);
0 16px 24px 2px rgba(0, 0, 0, 0.14), 0 6px 30px 5px rgba(0, 0, 0, 0.12);
overflow: hidden;
@each $direction in $directions {
@include animation-out($direction);
@include animation-in($direction);
}
.el-drawer-fade-enter-active & {
@include animation-in($direction);
}
&__wrapper {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: hidden;
margin: 0;
.el-drawer-fade-leave-active & {
@include animation-out($direction);
}
}
&__header {
@ -170,13 +124,15 @@ $directions: rtl, ltr, ttb, btt;
}
}
&.ltr, &.rtl {
&.ltr,
&.rtl {
height: 100%;
top: 0;
bottom: 0;
}
&.ttb, &.btt {
&.ttb,
&.btt {
width: 100%;
left: 0;
right: 0;
@ -199,20 +155,12 @@ $directions: rtl, ltr, ttb, btt;
}
}
.el-drawer__container {
position: relative;
left: 0;
right: 0;
top: 0;
bottom: 0;
height: 100%;
width: 100%;
}
.el-drawer-fade-enter-active {
animation: el-drawer-fade-in .3s;
animation: el-drawer-fade-in 0.3s;
overflow: hidden !important;
}
.el-drawer-fade-leave-active {
animation: el-drawer-fade-in .3s reverse;
overflow: hidden !important;
animation: el-drawer-fade-in 0.3s reverse;
}

View File

@ -18,23 +18,6 @@
overflow: hidden;
backface-visibility: hidden;
@include e(wrapper) {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
text-align: center;
&::after {
content: "";
display: inline-block;
height: 100%;
width: 0;
vertical-align: middle;
}
}
@include e(header) {
position: relative;
padding: $--msgbox-padding-primary;
@ -195,12 +178,16 @@
}
}
.msgbox-fade-enter-active {
animation: msgbox-fade-in .3s;
.fade-in-linear-enter-active {
.el-message-box {
animation: msgbox-fade-in .3s;
}
}
.msgbox-fade-leave-active {
animation: msgbox-fade-out .3s;
.fade-in-linear-leave-active {
.el-message-box {
animation: msgbox-fade-in .3s reverse;
}
}
@keyframes msgbox-fade-in {

View File

@ -12,6 +12,15 @@
left: 0;
z-index: 2000;
height: 100%;
text-align: center;
background-color: rgba(0, 0, 0, 0.5);
overflow: auto;
&::after {
content: "";
display: inline-block;
height: 100%;
width: 0;
vertical-align: middle;
}
}

View File

@ -23,9 +23,10 @@ export const off = function(
element: HTMLElement | Document | Window,
event: string,
handler: EventListenerOrEventListenerObject,
useCapture = false,
): void {
if (element && event && handler) {
element.removeEventListener(event, handler, false)
element.removeEventListener(event, handler, useCapture)
}
}