mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-12-02 11:17:46 +08:00
fix(overlay): Fix overlay event triggering issue (#1235)
This commit is contained in:
parent
e4ced422c6
commit
30f1947c47
@ -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()
|
||||
|
@ -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">
|
||||
|
@ -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', () => {
|
||||
|
@ -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)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
48
packages/hooks/__tests__/use-prevent-global.ts
Normal file
48
packages/hooks/__tests__/use-prevent-global.ts
Normal 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
|
||||
})
|
||||
|
||||
})
|
@ -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'
|
||||
|
@ -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()
|
||||
}
|
||||
|
19
packages/hooks/use-prevent-global/index.ts
Normal file
19
packages/hooks/use-prevent-global/index.ts
Normal 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 })
|
||||
}
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
@ -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,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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')],
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
21
packages/test-utils/composite-click.ts
Normal file
21
packages/test-utils/composite-click.ts
Normal 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()
|
||||
}
|
@ -14,7 +14,7 @@ export const rAF = async () => {
|
||||
return new Promise(res => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(async () => {
|
||||
res()
|
||||
res(null)
|
||||
await nextTick()
|
||||
})
|
||||
})
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user