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 { nextTick } from 'vue'
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import { rAF } from '@element-plus/test-utils/tick'
|
import { rAF } from '@element-plus/test-utils/tick'
|
||||||
|
import triggerCompositeClick from '@element-plus/test-utils/composite-click'
|
||||||
import Dialog from '../'
|
import Dialog from '../'
|
||||||
|
|
||||||
|
|
||||||
const AXIOM = 'Rem is the best girl'
|
const AXIOM = 'Rem is the best girl'
|
||||||
|
|
||||||
const _mount = ({ slots, ...rest }: Indexable<any>) => {
|
const _mount = ({ slots, ...rest }: Indexable<any>) => {
|
||||||
@ -150,7 +152,7 @@ describe('Dialog.vue', () => {
|
|||||||
await nextTick()
|
await nextTick()
|
||||||
expect(wrapper.find('.el-overlay').exists()).toBe(true)
|
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)
|
expect(wrapper.vm.visible).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -249,7 +251,7 @@ describe('Dialog.vue', () => {
|
|||||||
await rAF()
|
await rAF()
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
await wrapper.find('.el-overlay').trigger('click')
|
await triggerCompositeClick(wrapper.find('.el-overlay'))
|
||||||
await nextTick()
|
await nextTick()
|
||||||
await rAF()
|
await rAF()
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
role="dialog"
|
role="dialog"
|
||||||
:aria-label="title || 'dialog'"
|
:aria-label="title || 'dialog'"
|
||||||
:style="style"
|
:style="style"
|
||||||
@click="$event.stopPropagation()"
|
@click.stop=""
|
||||||
>
|
>
|
||||||
<div class="el-dialog__header">
|
<div class="el-dialog__header">
|
||||||
<slot name="title">
|
<slot name="title">
|
||||||
|
@ -9,7 +9,6 @@ const isVisibleMock = jest
|
|||||||
import TrapFocus, {
|
import TrapFocus, {
|
||||||
ITrapFocusElement,
|
ITrapFocusElement,
|
||||||
FOCUSABLE_CHILDREN,
|
FOCUSABLE_CHILDREN,
|
||||||
TRAP_FOCUS_HANDLER,
|
|
||||||
} from '../trap-focus'
|
} from '../trap-focus'
|
||||||
|
|
||||||
let wrapper
|
let wrapper
|
||||||
@ -45,9 +44,6 @@ describe('v-trap-focus', () => {
|
|||||||
expect(
|
expect(
|
||||||
(wrapper.element as ITrapFocusElement)[FOCUSABLE_CHILDREN].length,
|
(wrapper.element as ITrapFocusElement)[FOCUSABLE_CHILDREN].length,
|
||||||
).toBe(1)
|
).toBe(1)
|
||||||
expect(
|
|
||||||
(wrapper.element as ITrapFocusElement)[TRAP_FOCUS_HANDLER].length,
|
|
||||||
).toBeDefined()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should not fetch disabled element', () => {
|
test('should not fetch disabled element', () => {
|
||||||
|
@ -12,52 +12,62 @@ export interface ITrapFocusElement extends HTMLElement {
|
|||||||
[TRAP_FOCUS_HANDLER]: (e: KeyboardEvent) => void
|
[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 = {
|
const TrapFocus: ObjectDirective = {
|
||||||
beforeMount(el: ITrapFocusElement) {
|
beforeMount(el: ITrapFocusElement) {
|
||||||
el[FOCUSABLE_CHILDREN] = obtainAllFocusableElements(el)
|
el[FOCUSABLE_CHILDREN] = obtainAllFocusableElements(el)
|
||||||
|
FOCUS_STACK.push(el)
|
||||||
el[TRAP_FOCUS_HANDLER] = (e: KeyboardEvent) => {
|
if (FOCUS_STACK.length <= 1) {
|
||||||
const focusableElement = el[FOCUSABLE_CHILDREN]
|
on(document, 'keydown', FOCUS_HANDLER)
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
on(document, 'keydown', el[TRAP_FOCUS_HANDLER])
|
|
||||||
},
|
},
|
||||||
updated(el: ITrapFocusElement) {
|
updated(el: ITrapFocusElement) {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
el[FOCUSABLE_CHILDREN] = obtainAllFocusableElements(el)
|
el[FOCUSABLE_CHILDREN] = obtainAllFocusableElements(el)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
unmounted(el: ITrapFocusElement) {
|
unmounted() {
|
||||||
off(document, 'keydown', el[TRAP_FOCUS_HANDLER])
|
FOCUS_STACK.shift()
|
||||||
|
if (FOCUS_STACK.length === 0) {
|
||||||
|
off(document, 'keydown', FOCUS_HANDLER)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,50 +14,41 @@
|
|||||||
@click="onModalClick"
|
@click="onModalClick"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="el-drawer__container"
|
ref="drawerRef"
|
||||||
:class="{ 'el-drawer__open': visible }"
|
v-trap-focus
|
||||||
tabindex="-1"
|
aria-modal="true"
|
||||||
role="document"
|
aria-labelledby="el-drawer__title"
|
||||||
|
:aria-label="title"
|
||||||
|
:class="['el-drawer', direction, customClass]"
|
||||||
|
:style="isHorizontal ? 'width: ' + size : 'height: ' + size"
|
||||||
|
role="dialog"
|
||||||
|
@click.stop
|
||||||
>
|
>
|
||||||
<div
|
<header
|
||||||
ref="drawerRef"
|
v-if="withHeader"
|
||||||
v-trap-focus
|
id="el-drawer__title"
|
||||||
aria-modal="true"
|
class="el-drawer__header"
|
||||||
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
|
<slot name="title">
|
||||||
v-if="withHeader"
|
<span role="heading" :title="title">
|
||||||
id="el-drawer__title"
|
{{ title }}
|
||||||
class="el-drawer__header"
|
</span>
|
||||||
|
</slot>
|
||||||
|
<button
|
||||||
|
v-if="showClose"
|
||||||
|
:aria-label="'close ' + (title || 'drawer')"
|
||||||
|
class="el-drawer__close-btn"
|
||||||
|
type="button"
|
||||||
|
@click="handleClose"
|
||||||
>
|
>
|
||||||
<slot name="title">
|
<i class="el-drawer__close el-icon el-icon-close"></i>
|
||||||
<span role="heading" tabindex="-1" :title="title">
|
</button>
|
||||||
{{ title }}
|
</header>
|
||||||
</span>
|
<template v-if="rendered">
|
||||||
</slot>
|
<section class="el-drawer__body">
|
||||||
<button
|
<slot></slot>
|
||||||
v-if="showClose"
|
</section>
|
||||||
:aria-label="'close ' + (title || 'drawer')"
|
</template>
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</el-overlay>
|
</el-overlay>
|
||||||
</transition>
|
</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 useMigrating } from './use-migrating'
|
||||||
export { default as useFocus } from './use-focus'
|
export { default as useFocus } from './use-focus'
|
||||||
export { default as useThrottleRender } from './use-throttle-render'
|
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) => {
|
const closeModal = (e: KeyboardEvent) => {
|
||||||
if (modalStack.length === 0) return
|
if (modalStack.length === 0) return
|
||||||
if (e.code === EVENT_CODE.esc) {
|
if (e.code === EVENT_CODE.esc) {
|
||||||
|
e.stopPropagation()
|
||||||
const topModal = modalStack[modalStack.length - 1]
|
const topModal = modalStack[modalStack.length - 1]
|
||||||
topModal.handleClose()
|
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 MessageBox from '../src/messageBox'
|
||||||
import { sleep } from '@element-plus/test-utils'
|
import { rAF } from '@element-plus/test-utils/tick'
|
||||||
import { nextTick } from 'vue'
|
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', () => {
|
describe('MessageBox', () => {
|
||||||
|
afterEach(async () => {
|
||||||
afterEach(() => {
|
|
||||||
const el = document.querySelector('.el-message-box__wrapper')
|
|
||||||
if (!el) return
|
|
||||||
if (el.parentNode) {
|
|
||||||
el.parentNode.removeChild(el)
|
|
||||||
}
|
|
||||||
MessageBox.close()
|
MessageBox.close()
|
||||||
|
await rAF()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('create and close', async () => {
|
test('create and close', async () => {
|
||||||
@ -22,17 +32,23 @@ describe('MessageBox', () => {
|
|||||||
message: '这是一段内容',
|
message: '这是一段内容',
|
||||||
})
|
})
|
||||||
const msgbox: HTMLElement = document.querySelector(selector)
|
const msgbox: HTMLElement = document.querySelector(selector)
|
||||||
|
|
||||||
expect(msgbox).toBeDefined()
|
expect(msgbox).toBeDefined()
|
||||||
await sleep()
|
await rAF()
|
||||||
expect(msgbox.querySelector('.el-message-box__title span').textContent).toEqual('消息')
|
expect(
|
||||||
expect(msgbox.querySelector('.el-message-box__message').querySelector('p').textContent).toEqual('这是一段内容')
|
msgbox.querySelector('.el-message-box__title span').textContent,
|
||||||
|
).toEqual('消息')
|
||||||
|
expect(
|
||||||
|
msgbox.querySelector('.el-message-box__message').querySelector('p')
|
||||||
|
.textContent,
|
||||||
|
).toEqual('这是一段内容')
|
||||||
MessageBox.close()
|
MessageBox.close()
|
||||||
await sleep(250)
|
await rAF()
|
||||||
expect(msgbox.style.display).toEqual('none')
|
expect(msgbox.style.display).toEqual('none')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('invoke with strings', () => {
|
test('invoke with strings', () => {
|
||||||
MessageBox('消息', '这是一段内容')
|
MessageBox({ title: '消息', message: '这是一段内容' })
|
||||||
const msgbox = document.querySelector(selector)
|
const msgbox = document.querySelector(selector)
|
||||||
expect(msgbox).toBeDefined()
|
expect(msgbox).toBeDefined()
|
||||||
})
|
})
|
||||||
@ -43,7 +59,7 @@ describe('MessageBox', () => {
|
|||||||
iconClass: 'el-icon-question',
|
iconClass: 'el-icon-question',
|
||||||
message: '这是一段内容',
|
message: '这是一段内容',
|
||||||
})
|
})
|
||||||
await sleep()
|
await rAF()
|
||||||
const icon = document.querySelector('.el-message-box__status')
|
const icon = document.querySelector('.el-message-box__status')
|
||||||
expect(icon.classList.contains('el-icon-question')).toBe(true)
|
expect(icon.classList.contains('el-icon-question')).toBe(true)
|
||||||
})
|
})
|
||||||
@ -54,25 +70,32 @@ describe('MessageBox', () => {
|
|||||||
dangerouslyUseHTMLString: true,
|
dangerouslyUseHTMLString: true,
|
||||||
message: '<strong>html string</strong>',
|
message: '<strong>html string</strong>',
|
||||||
})
|
})
|
||||||
await sleep()
|
await rAF()
|
||||||
const message = document.querySelector('.el-message-box__message strong')
|
const message = document.querySelector('.el-message-box__message strong')
|
||||||
expect(message.textContent).toEqual('html string')
|
expect(message.textContent).toEqual('html string')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('distinguish cancel and close', async () => {
|
test('distinguish cancel and close', async () => {
|
||||||
let msgAction = ''
|
let msgAction = ''
|
||||||
MessageBox({
|
const invoker = () => {
|
||||||
title: '消息',
|
MessageBox({
|
||||||
message: '这是一段内容',
|
title: '消息',
|
||||||
distinguishCancelAndClose: true,
|
message: '这是一段内容',
|
||||||
callback: action => {
|
distinguishCancelAndClose: true,
|
||||||
msgAction = action
|
callback: action => {
|
||||||
},
|
msgAction = action
|
||||||
})
|
},
|
||||||
await sleep()
|
})
|
||||||
const btn = document.querySelector('.el-message-box__close') as HTMLButtonElement
|
}
|
||||||
|
|
||||||
|
_mount(invoker)
|
||||||
|
await rAF()
|
||||||
|
|
||||||
|
const btn = document.querySelector(
|
||||||
|
'.el-message-box__close',
|
||||||
|
) as HTMLButtonElement
|
||||||
btn.click()
|
btn.click()
|
||||||
await sleep()
|
await rAF()
|
||||||
expect(msgAction).toEqual('close')
|
expect(msgAction).toEqual('close')
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -81,26 +104,27 @@ describe('MessageBox', () => {
|
|||||||
title: '标题名称',
|
title: '标题名称',
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
})
|
})
|
||||||
await sleep()
|
await rAF()
|
||||||
const vModal: HTMLElement = document.querySelector('.v-modal')
|
await triggerNativeCompositeClick(document.querySelector(selector))
|
||||||
vModal.click()
|
await rAF()
|
||||||
await sleep(250)
|
|
||||||
const msgbox: HTMLElement = document.querySelector(selector)
|
const msgbox: HTMLElement = document.querySelector(selector)
|
||||||
expect(msgbox.style.display).toEqual('')
|
expect(msgbox.style.display).toEqual('')
|
||||||
expect(msgbox.querySelector('.el-icon-warning')).toBeDefined()
|
expect(msgbox.querySelector('.el-icon-warning')).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('confirm', async () => {
|
test('confirm', async () => {
|
||||||
MessageBox.confirm('这是一段内容', {
|
MessageBox.confirm('这是一段内容', {
|
||||||
title: '标题名称',
|
title: '标题名称',
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
})
|
})
|
||||||
await sleep()
|
await rAF()
|
||||||
const btn = document.querySelector(selector).querySelector('.el-button--primary') as HTMLButtonElement
|
const btn = document
|
||||||
|
.querySelector(selector)
|
||||||
|
.querySelector('.el-button--primary') as HTMLButtonElement
|
||||||
btn.click()
|
btn.click()
|
||||||
await sleep(250)
|
await rAF()
|
||||||
const msgbox: HTMLElement = document.querySelector(selector)
|
const msgbox: HTMLElement = document.querySelector(selector)
|
||||||
expect(msgbox.style.display).toEqual('none')
|
expect(msgbox).toBe(null)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('prompt', async () => {
|
test('prompt', async () => {
|
||||||
@ -109,9 +133,13 @@ describe('MessageBox', () => {
|
|||||||
inputPattern: /test/,
|
inputPattern: /test/,
|
||||||
inputErrorMessage: 'validation failed',
|
inputErrorMessage: 'validation failed',
|
||||||
})
|
})
|
||||||
await sleep(0)
|
await rAF()
|
||||||
const inputElm = document.querySelector(selector).querySelector('.el-message-box__input')
|
const inputElm = document
|
||||||
const haveFocus = inputElm.querySelector('input').isSameNode(document.activeElement)
|
.querySelector(selector)
|
||||||
|
.querySelector('.el-message-box__input')
|
||||||
|
const haveFocus = inputElm
|
||||||
|
.querySelector('input')
|
||||||
|
.isSameNode(document.activeElement)
|
||||||
expect(inputElm).toBeDefined()
|
expect(inputElm).toBeDefined()
|
||||||
expect(haveFocus).toBe(true)
|
expect(haveFocus).toBe(true)
|
||||||
})
|
})
|
||||||
@ -121,8 +149,10 @@ describe('MessageBox', () => {
|
|||||||
inputType: 'textarea',
|
inputType: 'textarea',
|
||||||
title: '标题名称',
|
title: '标题名称',
|
||||||
})
|
})
|
||||||
await sleep()
|
await rAF()
|
||||||
const textareaElm = document.querySelector(selector).querySelector('textarea')
|
const textareaElm = document
|
||||||
|
.querySelector(selector)
|
||||||
|
.querySelector('textarea')
|
||||||
const haveFocus = textareaElm.isSameNode(document.activeElement)
|
const haveFocus = textareaElm.isSameNode(document.activeElement)
|
||||||
expect(haveFocus).toBe(true)
|
expect(haveFocus).toBe(true)
|
||||||
})
|
})
|
||||||
@ -136,55 +166,65 @@ describe('MessageBox', () => {
|
|||||||
msgAction = action
|
msgAction = action
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await sleep()
|
await rAF()
|
||||||
const closeBtn = document.querySelector('.el-message-box__close') as HTMLButtonElement
|
const closeBtn = document.querySelector(
|
||||||
|
'.el-message-box__close',
|
||||||
|
) as HTMLButtonElement
|
||||||
closeBtn.click()
|
closeBtn.click()
|
||||||
await sleep()
|
await rAF()
|
||||||
expect(msgAction).toEqual('cancel')
|
expect(msgAction).toEqual('cancel')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('beforeClose', async() => {
|
test('beforeClose', async () => {
|
||||||
let msgAction = ''
|
let msgAction = ''
|
||||||
MessageBox({
|
MessageBox({
|
||||||
|
callback: action => {
|
||||||
|
msgAction = action
|
||||||
|
},
|
||||||
title: '消息',
|
title: '消息',
|
||||||
message: '这是一段内容',
|
message: '这是一段内容',
|
||||||
beforeClose: (action, instance) => {
|
beforeClose: (_, __, done) => {
|
||||||
instance.close()
|
done()
|
||||||
},
|
},
|
||||||
}, action => {
|
|
||||||
msgAction = action
|
|
||||||
})
|
})
|
||||||
await sleep();
|
await rAF()
|
||||||
(document.querySelector('.el-message-box__wrapper .el-button--primary') as HTMLButtonElement).click()
|
;(document.querySelector(
|
||||||
await nextTick()
|
'.el-message-box__btns .el-button--primary',
|
||||||
await sleep()
|
) as HTMLButtonElement).click()
|
||||||
|
await rAF()
|
||||||
expect(msgAction).toEqual('confirm')
|
expect(msgAction).toEqual('confirm')
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('promise', () => {
|
describe('promise', () => {
|
||||||
test('resolve',async () => {
|
test('resolve', async () => {
|
||||||
let msgAction = ''
|
let msgAction = ''
|
||||||
MessageBox.confirm('此操作将永久删除该文件, 是否继续?', '提示')
|
MessageBox.confirm('此操作将永久删除该文件, 是否继续?', '提示').then(
|
||||||
.then(action => {
|
action => {
|
||||||
msgAction = 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()
|
btn.click()
|
||||||
await sleep()
|
await rAF()
|
||||||
expect(msgAction).toEqual('confirm')
|
expect(msgAction).toEqual('confirm')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('reject', async () => {
|
test('reject', async () => {
|
||||||
let msgAction = ''
|
let msgAction = ''
|
||||||
MessageBox.confirm('此操作将永久删除该文件, 是否继续?', '提示')
|
MessageBox.confirm('此操作将永久删除该文件, 是否继续?', '提示').catch(
|
||||||
.catch(action => {
|
action => {
|
||||||
msgAction = action
|
msgAction = action
|
||||||
})
|
},
|
||||||
await sleep()
|
)
|
||||||
const btn = document.querySelectorAll('.el-message-box__btns .el-button') as NodeListOf<HTMLButtonElement>
|
await rAF()
|
||||||
btn[0].click()
|
const btn = document.querySelector(
|
||||||
await sleep()
|
'.el-message-box__btns .el-button',
|
||||||
|
)
|
||||||
|
;(btn as HTMLButtonElement).click()
|
||||||
|
await rAF()
|
||||||
expect(msgAction).toEqual('cancel')
|
expect(msgAction).toEqual('cancel')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,23 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<transition name="msgbox-fade">
|
<transition name="fade-in-linear" @after-leave="$emit('vanish')">
|
||||||
<div
|
<el-overlay
|
||||||
v-show="visible"
|
v-show="visible"
|
||||||
ref="root"
|
:z-index="state.zIndex"
|
||||||
:aria-label="title || 'dialog'"
|
:overlay-class="modalClass"
|
||||||
class="el-message-box__wrapper"
|
:mask="modal"
|
||||||
tabindex="-1"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
@click.self="handleWrapperClick"
|
@click.self="handleWrapperClick"
|
||||||
>
|
>
|
||||||
<div class="el-message-box" :class="[customClass, center && 'el-message-box--center']">
|
<div
|
||||||
<div v-if="title !== null && title !== undefined" class="el-message-box__header">
|
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 class="el-message-box__title">
|
||||||
<div
|
<div
|
||||||
v-if="icon && center"
|
v-if="icon && center"
|
||||||
:class="['el-message-box__status', icon]"
|
:class="['el-message-box__status', icon]"
|
||||||
>
|
></div>
|
||||||
</div>
|
|
||||||
<span>{{ title }}</span>
|
<span>{{ title }}</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@ -25,8 +34,12 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="el-message-box__headerbtn"
|
class="el-message-box__headerbtn"
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
@click="handleAction(distinguishCancelAndClose ? 'close' : 'cancel')"
|
@click="
|
||||||
@keydown.enter="handleAction(distinguishCancelAndClose ? 'close' : 'cancel')"
|
handleAction(distinguishCancelAndClose ? 'close' : 'cancel')
|
||||||
|
"
|
||||||
|
@keydown.enter="
|
||||||
|
handleAction(distinguishCancelAndClose ? 'close' : 'cancel')
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<i class="el-message-box__close el-icon-close"></i>
|
<i class="el-message-box__close el-icon-close"></i>
|
||||||
</button>
|
</button>
|
||||||
@ -36,8 +49,7 @@
|
|||||||
<div
|
<div
|
||||||
v-if="icon && !center && hasMessage"
|
v-if="icon && !center && hasMessage"
|
||||||
:class="['el-message-box__status', icon]"
|
:class="['el-message-box__status', icon]"
|
||||||
>
|
></div>
|
||||||
</div>
|
|
||||||
<div v-if="hasMessage" class="el-message-box__message">
|
<div v-if="hasMessage" class="el-message-box__message">
|
||||||
<slot>
|
<slot>
|
||||||
<p v-if="!dangerouslyUseHTMLString">{{ message }}</p>
|
<p v-if="!dangerouslyUseHTMLString">{{ message }}</p>
|
||||||
@ -47,47 +59,54 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-show="showInput" class="el-message-box__input">
|
<div v-show="showInput" class="el-message-box__input">
|
||||||
<el-input
|
<el-input
|
||||||
ref="input"
|
ref="inputRef"
|
||||||
v-model="inputValue"
|
v-model="state.inputValue"
|
||||||
:type="inputType"
|
:type="inputType"
|
||||||
:placeholder="inputPlaceholder"
|
:placeholder="inputPlaceholder"
|
||||||
:class="{ invalid: validateError }"
|
:class="{ invalid: state.validateError }"
|
||||||
@keydown.enter="handleInputEnter"
|
@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>
|
</div>
|
||||||
<div class="el-message-box__btns">
|
<div class="el-message-box__btns">
|
||||||
<el-button
|
<el-button
|
||||||
v-if="showCancelButton"
|
v-if="showCancelButton"
|
||||||
:loading="cancelButtonLoading"
|
:loading="state.cancelButtonLoading"
|
||||||
:class="[ cancelButtonClass ]"
|
:class="[cancelButtonClass]"
|
||||||
:round="roundButton"
|
:round="roundButton"
|
||||||
size="small"
|
size="small"
|
||||||
@click="handleAction('cancel')"
|
@click="handleAction('cancel')"
|
||||||
@keydown.enter="handleAction('cancel')"
|
@keydown.enter="handleAction('cancel')"
|
||||||
>
|
>
|
||||||
{{ cancelButtonText || t('el.messagebox.cancel') }}
|
{{ state.cancelButtonText || t('el.messagebox.cancel') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-show="showConfirmButton"
|
v-show="showConfirmButton"
|
||||||
ref="confirm"
|
ref="confirmRef"
|
||||||
:loading="confirmButtonLoading"
|
:loading="state.confirmButtonLoading"
|
||||||
:class="[ confirmButtonClasses ]"
|
:class="[confirmButtonClasses]"
|
||||||
:round="roundButton"
|
:round="roundButton"
|
||||||
:disabled="confirmButtonDisabled"
|
:disabled="state.confirmButtonDisabled"
|
||||||
size="small"
|
size="small"
|
||||||
@click="handleAction('confirm')"
|
@click="handleAction('confirm')"
|
||||||
@keydown.enter="handleAction('confirm')"
|
@keydown.enter="handleAction('confirm')"
|
||||||
>
|
>
|
||||||
{{ confirmButtonText || t('el.messagebox.confirm') }}
|
{{ state.confirmButtonText || t('el.messagebox.confirm') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</el-overlay>
|
||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
<script lang='ts'>
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
defineComponent,
|
defineComponent,
|
||||||
nextTick,
|
nextTick,
|
||||||
@ -95,19 +114,21 @@ import {
|
|||||||
onBeforeUnmount,
|
onBeforeUnmount,
|
||||||
computed,
|
computed,
|
||||||
watch,
|
watch,
|
||||||
onBeforeMount,
|
|
||||||
getCurrentInstance,
|
|
||||||
reactive,
|
reactive,
|
||||||
toRefs,
|
ref,
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
import ElButton from '@element-plus/button'
|
import ElButton from '@element-plus/button'
|
||||||
import ElInput from '@element-plus/input'
|
import ElInput from '@element-plus/input'
|
||||||
import { t } from '@element-plus/locale'
|
import { t } from '@element-plus/locale'
|
||||||
import Dialog from '@element-plus/utils/aria-dialog'
|
import { Overlay as ElOverlay } from '@element-plus/overlay'
|
||||||
import usePopup from '@element-plus/utils/popup/usePopup'
|
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 { 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> = {
|
const TypeMap: Indexable<string> = {
|
||||||
success: 'success',
|
success: 'success',
|
||||||
@ -121,41 +142,23 @@ export default defineComponent({
|
|||||||
components: {
|
components: {
|
||||||
ElButton,
|
ElButton,
|
||||||
ElInput,
|
ElInput,
|
||||||
|
ElOverlay,
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
TrapFocus,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
openDelay: {
|
beforeClose: {
|
||||||
type: Number,
|
type: Function as PropType<(action: Action, state: MessageBoxState, doClose: () => void) => any>,
|
||||||
default: 0,
|
default: undefined,
|
||||||
},
|
},
|
||||||
closeDelay: {
|
callback: Function,
|
||||||
type: Number,
|
cancelButtonText: {
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
zIndex: Number,
|
|
||||||
modalFade: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
modalClass: {
|
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: 'Cancel',
|
||||||
},
|
|
||||||
modalAppendToBody: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
modal: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
lockScroll: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
showClose: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
},
|
||||||
|
cancelButtonClass: String,
|
||||||
|
center: Boolean,
|
||||||
closeOnClickModal: {
|
closeOnClickModal: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
@ -168,78 +171,113 @@ export default defineComponent({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
center: {
|
confirmButtonText: {
|
||||||
default: false,
|
type: String,
|
||||||
type: Boolean,
|
default: 'OK',
|
||||||
},
|
},
|
||||||
roundButton: {
|
confirmButtonClass: String,
|
||||||
default: false,
|
container: {
|
||||||
type: Boolean,
|
type: String, // default append to body
|
||||||
|
default: 'body',
|
||||||
},
|
},
|
||||||
} ,
|
customClass: String,
|
||||||
setup(props) {
|
dangerouslyUseHTMLString: Boolean,
|
||||||
let vm
|
distinguishCancelAndClose: Boolean,
|
||||||
const popup = usePopup(props, doClose)
|
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({
|
const state = reactive({
|
||||||
uid: 1,
|
action: '' as Action,
|
||||||
title: undefined,
|
inputValue: props.inputValue,
|
||||||
message: '',
|
|
||||||
type: '',
|
|
||||||
iconClass: '',
|
|
||||||
customClass: '',
|
|
||||||
showInput: false,
|
|
||||||
inputValue: null,
|
|
||||||
inputPlaceholder: '',
|
|
||||||
inputType: 'text',
|
|
||||||
inputPattern: null,
|
|
||||||
inputValidator: null,
|
|
||||||
inputErrorMessage: '',
|
|
||||||
showConfirmButton: true,
|
|
||||||
showCancelButton: false,
|
|
||||||
action: '',
|
|
||||||
confirmButtonText: '',
|
|
||||||
cancelButtonText: '',
|
|
||||||
confirmButtonLoading: false,
|
confirmButtonLoading: false,
|
||||||
cancelButtonLoading: false,
|
cancelButtonLoading: false,
|
||||||
confirmButtonClass: '',
|
cancelButtonText: props.cancelButtonText,
|
||||||
confirmButtonDisabled: false,
|
confirmButtonDisabled: false,
|
||||||
cancelButtonClass: '',
|
confirmButtonText: props.confirmButtonText,
|
||||||
editorErrorMessage: null,
|
editorErrorMessage: '',
|
||||||
callback: null,
|
// refer to: https://github.com/ElemeFE/element/commit/2999279ae34ef10c373ca795c87b020ed6753eed
|
||||||
dangerouslyUseHTMLString: false,
|
// seemed ok for now without this state.
|
||||||
focusAfterClosed: null,
|
// isOnComposition: false, // temporary remove
|
||||||
isOnComposition: false,
|
|
||||||
distinguishCancelAndClose: false,
|
|
||||||
type$: '',
|
|
||||||
visible: false,
|
|
||||||
validateError: false,
|
validateError: false,
|
||||||
|
zIndex: PopupManager.nextZIndex(),
|
||||||
})
|
})
|
||||||
const icon = computed(() => state.iconClass || (state.type && TypeMap[state.type] ? `el-icon-${ TypeMap[state.type] }` : ''))
|
const icon = computed(() => props.iconClass || (props.type && TypeMap[props.type] ? `el-icon-${TypeMap[props.type]}` : ''))
|
||||||
const hasMessage = computed(() => !!state.message)
|
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 => {
|
watch(() => state.inputValue, async val => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
if (state.type$ === 'prompt' && val !== null) {
|
if (props.type === 'prompt' && val !== null) {
|
||||||
validate()
|
validate()
|
||||||
}
|
}
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
watch(() => state.visible, val => {
|
watch(() => visible.value, val => {
|
||||||
popup.state.visible = val
|
|
||||||
if (val) {
|
if (val) {
|
||||||
state.uid++
|
if (props.type === 'alert' || props.type === 'confirm') {
|
||||||
if (state.type$ === 'alert' || state.type$ === 'confirm') {
|
nextTick().then(() => { confirmRef.value?.$el?.focus?.() })
|
||||||
nextTick().then(() => { vm.refs.confirm.$el.focus() })
|
|
||||||
}
|
}
|
||||||
state.focusAfterClosed = document.activeElement
|
state.zIndex = PopupManager.nextZIndex()
|
||||||
dialog = new Dialog(vm.vnode.el, state.focusAfterClosed, getFirstFocus())
|
|
||||||
}
|
}
|
||||||
if (state.type$ !== 'prompt') return
|
if (props.type !== 'prompt') return
|
||||||
if (val) {
|
if (val) {
|
||||||
nextTick().then(() => {
|
nextTick().then(() => {
|
||||||
if (vm.refs.input && vm.refs.input.$el) {
|
if (inputRef.value && inputRef.value.$el) {
|
||||||
getInputElement().focus()
|
getInputElement().focus()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -249,98 +287,66 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
vm = getCurrentInstance()
|
|
||||||
vm.setupInstall = {
|
|
||||||
state,
|
|
||||||
doClose,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
if (props.closeOnHashChange) {
|
if (props.closeOnHashChange) {
|
||||||
on(window, 'hashchange', popup.close)
|
on(window, 'hashchange', doClose)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (props.closeOnHashChange) {
|
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() {
|
function doClose() {
|
||||||
if (!state.visible) return
|
if (!visible.value) return
|
||||||
state.visible = false
|
visible.value = false
|
||||||
popup.updateClosingFlag(true)
|
nextTick(() => {
|
||||||
dialog.closeDialog()
|
if (state.action) emit('action', state.action)
|
||||||
if (props.lockScroll) {
|
|
||||||
setTimeout(popup.restoreBodyStyle, 200)
|
|
||||||
}
|
|
||||||
popup.state.opened = false
|
|
||||||
popup.doAfterClose()
|
|
||||||
setTimeout(() => {
|
|
||||||
if (state.action) state.callback(state.action, state)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = () => {
|
const handleWrapperClick = () => {
|
||||||
if (props.closeOnClickModal) {
|
if (props.closeOnClickModal) {
|
||||||
handleAction(state.distinguishCancelAndClose ? 'close' : 'cancel')
|
handleAction(props.distinguishCancelAndClose ? 'close' : 'cancel')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleInputEnter = () => {
|
const handleInputEnter = () => {
|
||||||
if (state.inputType !== 'textarea') {
|
if (props.inputType !== 'textarea') {
|
||||||
return handleAction('confirm')
|
return handleAction('confirm')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAction = action => {
|
const handleAction = (action: Action) => {
|
||||||
if (state.type$ === 'prompt' && action === 'confirm' && !validate()) {
|
if (props.type === 'prompt' && action === 'confirm' && !validate()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
state.action = action
|
state.action = action
|
||||||
if (typeof vm.setupInstall.state.beforeClose === 'function') {
|
|
||||||
vm.setupInstall.state.close = getSafeClose()
|
if (props.beforeClose) {
|
||||||
vm.setupInstall.state.beforeClose(action, state, popup.close)
|
props.beforeClose?.(action, state, doClose)
|
||||||
} else {
|
} else {
|
||||||
doClose()
|
doClose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const validate = () => {
|
const validate = () => {
|
||||||
if (state.type$ === 'prompt') {
|
if (props.type === 'prompt') {
|
||||||
const inputPattern = state.inputPattern
|
const inputPattern = props.inputPattern
|
||||||
if (inputPattern && !inputPattern.test(state.inputValue || '')) {
|
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
|
state.validateError = true
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
const inputValidator = state.inputValidator
|
const inputValidator = props.inputValidator
|
||||||
if (typeof inputValidator === 'function') {
|
if (typeof inputValidator === 'function') {
|
||||||
const validateResult = inputValidator(state.inputValue)
|
const validateResult = inputValidator(state.inputValue)
|
||||||
if (validateResult === false) {
|
if (validateResult === false) {
|
||||||
state.editorErrorMessage = state.inputErrorMessage || t('el.messagebox.error')
|
state.editorErrorMessage = props.inputErrorMessage || t('el.messagebox.error')
|
||||||
state.validateError = true
|
state.validateError = true
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -357,25 +363,50 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getInputElement = () => {
|
const getInputElement = () => {
|
||||||
const inputRefs = vm.refs.input.$refs
|
const inputRefs = inputRef.value.$refs
|
||||||
return inputRefs.input || inputRefs.textarea
|
return (inputRefs.input || inputRefs.textarea) as HTMLElement
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
handleAction('close')
|
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 {
|
return {
|
||||||
...toRefs(state),
|
state,
|
||||||
|
visible,
|
||||||
hasMessage,
|
hasMessage,
|
||||||
icon,
|
icon,
|
||||||
confirmButtonClasses,
|
confirmButtonClasses,
|
||||||
|
inputRef,
|
||||||
|
confirmRef,
|
||||||
|
doClose, // for outside usage
|
||||||
|
handleClose, // for out side usage
|
||||||
handleWrapperClick,
|
handleWrapperClick,
|
||||||
handleInputEnter,
|
handleInputEnter,
|
||||||
handleAction,
|
handleAction,
|
||||||
handleClose,
|
|
||||||
t,
|
t,
|
||||||
doClose,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -1,17 +1,31 @@
|
|||||||
import { VNode } from 'vue'
|
import type { VNode } from 'vue'
|
||||||
export type MessageType = 'success' | 'warning' | 'info' | 'error'
|
|
||||||
export type MessageBoxCloseAction = 'confirm' | 'cancel' | 'close'
|
|
||||||
export type MessageBoxData = MessageBoxInputData
|
|
||||||
|
|
||||||
|
export type Action = 'confirm' | 'close' | 'cancel'
|
||||||
|
export type MessageType = 'success' | 'warning' | 'info' | 'error'
|
||||||
|
export type MessageBoxData = MessageBoxInputData & Action
|
||||||
export interface MessageBoxInputData {
|
export interface MessageBoxInputData {
|
||||||
value: string
|
value: string
|
||||||
action: MessageBoxCloseAction
|
action: Action
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageBoxInputValidator {
|
export interface MessageBoxInputValidator {
|
||||||
(value: string): boolean | string
|
(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 {
|
export declare class ElMessageBoxComponent {
|
||||||
title: string
|
title: string
|
||||||
message: string
|
message: string
|
||||||
@ -28,7 +42,7 @@ export declare class ElMessageBoxComponent {
|
|||||||
inputErrorMessage: string
|
inputErrorMessage: string
|
||||||
showConfirmButton: boolean
|
showConfirmButton: boolean
|
||||||
showCancelButton: boolean
|
showCancelButton: boolean
|
||||||
action: MessageBoxCloseAction
|
action: Action
|
||||||
dangerouslyUseHTMLString: boolean
|
dangerouslyUseHTMLString: boolean
|
||||||
confirmButtonText: string
|
confirmButtonText: string
|
||||||
cancelButtonText: string
|
cancelButtonText: string
|
||||||
@ -42,40 +56,25 @@ export declare class ElMessageBoxComponent {
|
|||||||
close(): any
|
close(): any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Callback =
|
||||||
|
| ((value: string, action: Action) => any)
|
||||||
|
| ((action: Action) => any)
|
||||||
|
|
||||||
/** Options used in MessageBox */
|
/** Options used in MessageBox */
|
||||||
export interface ElMessageBoxOptions {
|
export interface ElMessageBoxOptions {
|
||||||
/** Title of the MessageBox */
|
|
||||||
title?: string
|
|
||||||
|
|
||||||
/** Content of the MessageBox */
|
/** Callback before MessageBox closes, and it will prevent MessageBox from closing */
|
||||||
message?: string | VNode
|
beforeClose?: (
|
||||||
|
action: Action,
|
||||||
/** Message type, used for icon display */
|
instance: ElMessageBoxComponent,
|
||||||
type?: MessageType
|
done: () => void,
|
||||||
|
) => void
|
||||||
/** Custom icon's class */
|
|
||||||
iconClass?: string
|
|
||||||
|
|
||||||
/** Custom class name for MessageBox */
|
/** Custom class name for MessageBox */
|
||||||
customClass?: string
|
customClass?: string
|
||||||
|
|
||||||
/** MessageBox closing callback if you don't prefer Promise */
|
/** MessageBox closing callback if you don't prefer Promise */
|
||||||
callback?: (action: MessageBoxCloseAction, instance: ElMessageBoxComponent) => void
|
callback?: Callback
|
||||||
|
|
||||||
/** 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
|
|
||||||
|
|
||||||
/** Text content of cancel button */
|
/** Text content of cancel button */
|
||||||
cancelButtonText?: string
|
cancelButtonText?: string
|
||||||
@ -92,9 +91,36 @@ export interface ElMessageBoxOptions {
|
|||||||
/** Whether to align the content in center */
|
/** Whether to align the content in center */
|
||||||
center?: boolean
|
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 */
|
/** Whether message is treated as HTML string */
|
||||||
dangerouslyUseHTMLString?: boolean
|
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 */
|
/** Whether to use round button */
|
||||||
roundButton?: boolean
|
roundButton?: boolean
|
||||||
|
|
||||||
@ -128,18 +154,22 @@ export interface ElMessageBoxOptions {
|
|||||||
/** Error message when validation fails */
|
/** Error message when validation fails */
|
||||||
inputErrorMessage?: string
|
inputErrorMessage?: string
|
||||||
|
|
||||||
/** Whether to distinguish canceling and closing */
|
|
||||||
distinguishCancelAndClose?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ElMessageBoxShortcutMethod {
|
export type ElMessageBoxShortcutMethod =
|
||||||
(message: string, title: string, options?: ElMessageBoxOptions): Promise<MessageBoxData>
|
((
|
||||||
(message: string, options?: ElMessageBoxOptions): Promise<MessageBoxData>
|
message: string,
|
||||||
}
|
title: string,
|
||||||
|
options?: ElMessageBoxOptions,
|
||||||
|
) => Promise<MessageBoxData>)
|
||||||
|
& ((
|
||||||
|
message: string,
|
||||||
|
options?: ElMessageBoxOptions,
|
||||||
|
) => Promise<MessageBoxData>)
|
||||||
|
|
||||||
export interface ElMessageBox {
|
export interface ElMessageBox {
|
||||||
/** Show a message box */
|
/** Show a message box */
|
||||||
(message: string, title?: string, type?: string): Promise<MessageBoxData>
|
// (message: string, title?: string, type?: string): Promise<MessageBoxData>
|
||||||
|
|
||||||
/** Show a message box */
|
/** Show a message box */
|
||||||
(options: ElMessageBoxOptions): Promise<MessageBoxData>
|
(options: ElMessageBoxOptions): Promise<MessageBoxData>
|
||||||
@ -153,9 +183,6 @@ export interface ElMessageBox {
|
|||||||
/** Show a prompt message box */
|
/** Show a prompt message box */
|
||||||
prompt: ElMessageBoxShortcutMethod
|
prompt: ElMessageBoxShortcutMethod
|
||||||
|
|
||||||
/** Set default options of message boxes */
|
|
||||||
setDefaults (defaults: ElMessageBoxOptions): void
|
|
||||||
|
|
||||||
/** Close current message box */
|
/** 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 MessageBoxConstructor from './index.vue'
|
||||||
import isServer from '@element-plus/utils/isServer'
|
import isServer from '@element-plus/utils/isServer'
|
||||||
import { isVNode } from '../../utils/util'
|
import { isVNode, isString } from '@element-plus/utils/util'
|
||||||
import { ElMessageBoxOptions } from './message-box.type'
|
|
||||||
|
|
||||||
let currentMsg, instance
|
import type { ComponentPublicInstance, VNode } from 'vue'
|
||||||
|
import type {
|
||||||
// component default props
|
Action,
|
||||||
const PROP_KEYS = [
|
Callback,
|
||||||
'lockScroll',
|
MessageBoxState,
|
||||||
'showClose',
|
ElMessageBox,
|
||||||
'closeOnClickModal',
|
ElMessageBoxOptions,
|
||||||
'closeOnPressEscape',
|
MessageBoxData,
|
||||||
'closeOnHashChange',
|
} from './message-box.type'
|
||||||
'center',
|
|
||||||
'roundButton',
|
|
||||||
'closeDelay',
|
|
||||||
'zIndex',
|
|
||||||
'modal',
|
|
||||||
'modalFade',
|
|
||||||
'modalClass',
|
|
||||||
'modalAppendToBody',
|
|
||||||
'lockScroll',
|
|
||||||
]
|
|
||||||
|
|
||||||
// component default merge props & data
|
// 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 messageInstance = new Map<
|
||||||
|
ComponentPublicInstance<{ doClose: () => void; }>, // marking doClose as function
|
||||||
const defaultCallback = (action, ctx) => {
|
{
|
||||||
if (currentMsg) {
|
options: any
|
||||||
const callback = currentMsg.callback
|
callback: Callback
|
||||||
if (typeof callback === 'function') {
|
resolve: (res: any) => void
|
||||||
if (ctx.showInput) {
|
reject: (reason?: any) => void
|
||||||
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 initInstance = () => {
|
|
||||||
const container = document.createElement('div')
|
const initInstance = (props: any, container: HTMLElement) => {
|
||||||
const vnode = createVNode(MessageBoxConstructor)
|
const vnode = h(MessageBoxConstructor, props)
|
||||||
render(vnode, container)
|
render(vnode, container)
|
||||||
instance = vnode.component
|
document.body.appendChild(container.firstElementChild)
|
||||||
|
return vnode.component
|
||||||
}
|
}
|
||||||
|
|
||||||
const showNextMsg = async () => {
|
const genContainer = () => {
|
||||||
if (!instance) {
|
return document.createElement('div')
|
||||||
initInstance()
|
}
|
||||||
|
|
||||||
|
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) {
|
options.onAction = (action: Action) => {
|
||||||
const props = {}
|
|
||||||
const state = {}
|
const currentMsg = messageInstance.get(vm)
|
||||||
currentMsg = msgQueue.shift()
|
let resolve: Action | { value: string; action: Action; }
|
||||||
const options = currentMsg.options
|
if (options.showInput) {
|
||||||
Object.keys(options).forEach(key => {
|
resolve = { value: vm.state.inputValue, action }
|
||||||
if (PROP_KEYS.includes(key)) {
|
} else {
|
||||||
props[key] = options[key]
|
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 {
|
} else {
|
||||||
state[key] = options[key]
|
currentMsg.resolve(resolve)
|
||||||
}
|
|
||||||
})
|
|
||||||
// update props to instance/**/
|
|
||||||
const vmProps = instance.props
|
|
||||||
for (const prop in props) {
|
|
||||||
if (props.hasOwnProperty(prop)) {
|
|
||||||
vmProps[prop] = props[prop]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 (isServer) return
|
||||||
if (typeof options === 'string' || isVNode(options)) {
|
let callback
|
||||||
|
if (isString(options) || isVNode(options)) {
|
||||||
options = {
|
options = {
|
||||||
message: options,
|
message: options,
|
||||||
}
|
}
|
||||||
if (typeof callback === 'string') {
|
} else {
|
||||||
options.title = callback
|
|
||||||
}
|
|
||||||
} else if (options.callback && !callback) {
|
|
||||||
callback = options.callback
|
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') {
|
if (typeof title === 'object') {
|
||||||
options = title
|
options = title
|
||||||
title = ''
|
title = ''
|
||||||
@ -181,51 +132,78 @@ MessageBox.alert = (message, title, options?: ElMessageBoxOptions) => {
|
|||||||
title = ''
|
title = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
return MessageBox(Object.assign({
|
return MessageBox(
|
||||||
title: title,
|
Object.assign(
|
||||||
message: message,
|
{
|
||||||
type$: 'alert',
|
title: title,
|
||||||
closeOnPressEscape: false,
|
message: message,
|
||||||
closeOnClickModal: false,
|
type: 'alert',
|
||||||
}, options))
|
closeOnPressEscape: false,
|
||||||
|
closeOnClickModal: false,
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageBox.confirm = (message, title, options?: ElMessageBoxOptions) => {
|
MessageBox.confirm = (
|
||||||
|
message: string,
|
||||||
|
title: string,
|
||||||
|
options?: ElMessageBoxOptions,
|
||||||
|
) => {
|
||||||
if (typeof title === 'object') {
|
if (typeof title === 'object') {
|
||||||
options = title
|
options = title
|
||||||
title = ''
|
title = ''
|
||||||
} else if (title === undefined) {
|
} else if (title === undefined) {
|
||||||
title = ''
|
title = ''
|
||||||
}
|
}
|
||||||
return MessageBox(Object.assign({
|
return MessageBox(
|
||||||
title: title,
|
Object.assign(
|
||||||
message: message,
|
{
|
||||||
type$: 'confirm',
|
title: title,
|
||||||
showCancelButton: true,
|
message: message,
|
||||||
}, options))
|
type: 'confirm',
|
||||||
|
showCancelButton: true,
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageBox.prompt = (message, title, options?: ElMessageBoxOptions) => {
|
MessageBox.prompt = (
|
||||||
|
message: string,
|
||||||
|
title: string,
|
||||||
|
options?: ElMessageBoxOptions,
|
||||||
|
) => {
|
||||||
if (typeof title === 'object') {
|
if (typeof title === 'object') {
|
||||||
options = title
|
options = title
|
||||||
title = ''
|
title = ''
|
||||||
} else if (title === undefined) {
|
} else if (title === undefined) {
|
||||||
title = ''
|
title = ''
|
||||||
}
|
}
|
||||||
return MessageBox(Object.assign({
|
return MessageBox(
|
||||||
title: title,
|
Object.assign(
|
||||||
message: message,
|
{
|
||||||
showCancelButton: true,
|
title: title,
|
||||||
showInput: true,
|
message: message,
|
||||||
type$: 'prompt',
|
showCancelButton: true,
|
||||||
}, options))
|
showInput: true,
|
||||||
|
type: 'prompt',
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageBox.close = () => {
|
MessageBox.close = () => {
|
||||||
instance.setupInstall.doClose()
|
// instance.setupInstall.doClose()
|
||||||
instance.setupInstall.state.visible = false
|
// instance.setupInstall.state.visible = false
|
||||||
msgQueue = []
|
|
||||||
currentMsg = null
|
messageInstance.forEach((_, vm) => {
|
||||||
|
vm.doClose()
|
||||||
|
})
|
||||||
|
|
||||||
|
messageInstance.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MessageBox
|
export default MessageBox as ElMessageBox
|
||||||
|
@ -18,9 +18,22 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
emits: ['click'],
|
emits: ['click'],
|
||||||
setup(props, { slots, emit }) {
|
setup(props, { slots, emit }) {
|
||||||
const onMaskClick = () => {
|
let mousedownTarget = false
|
||||||
emit('click')
|
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
|
// init here
|
||||||
return () => {
|
return () => {
|
||||||
// when the vnode meets the same structure but with different change trigger
|
// when the vnode meets the same structure but with different change trigger
|
||||||
@ -34,21 +47,36 @@ export default defineComponent({
|
|||||||
zIndex: props.zIndex,
|
zIndex: props.zIndex,
|
||||||
},
|
},
|
||||||
onClick: onMaskClick,
|
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')],
|
[renderSlot(slots, 'default')],
|
||||||
PatchFlags.STYLE | PatchFlags.CLASS | PatchFlags.PROPS,
|
PatchFlags.STYLE | PatchFlags.CLASS | PatchFlags.PROPS,
|
||||||
['onClick'],
|
['onClick', 'onMouseup', 'onMousedown'],
|
||||||
)
|
)
|
||||||
: h('div', {
|
: h(
|
||||||
style: {
|
'div',
|
||||||
zIndex: props.zIndex,
|
{
|
||||||
position: 'fixed',
|
style: {
|
||||||
top: '0px',
|
zIndex: props.zIndex,
|
||||||
right: '0px',
|
position: 'fixed',
|
||||||
bottom: '0px',
|
top: '0px',
|
||||||
left: '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 => {
|
return new Promise(res => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
requestAnimationFrame(async () => {
|
requestAnimationFrame(async () => {
|
||||||
res()
|
res(null)
|
||||||
await nextTick()
|
await nextTick()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
@import "mixins/mixins";
|
@import 'mixins/mixins';
|
||||||
@import "common/var";
|
@import 'common/var';
|
||||||
@import "./overlay.scss";
|
@import './overlay.scss';
|
||||||
|
|
||||||
@keyframes el-drawer-fade-in {
|
@keyframes el-drawer-fade-in {
|
||||||
0% {
|
0% {
|
||||||
@ -12,54 +12,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@mixin drawer-animation($direction) {
|
@mixin drawer-animation($direction) {
|
||||||
|
@keyframes #{$direction}-drawer-animation {
|
||||||
@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 {
|
|
||||||
0% {
|
0% {
|
||||||
@if $direction == ltr {
|
@if $direction == ltr {
|
||||||
transform: translate(0px, 0px);
|
transform: translate(0px, 0px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@if $direction == rtl {
|
@if $direction == rtl {
|
||||||
transform: translate(0px, 0px);;
|
transform: translate(0px, 0px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@if $direction == ttb {
|
@if $direction == ttb {
|
||||||
@ -67,7 +27,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@if $direction == btt {
|
@if $direction == btt {
|
||||||
transform: translate(0px, 0);
|
transform: translate(0px, 0px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,14 +52,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@mixin animation-in($direction) {
|
@mixin animation-in($direction) {
|
||||||
.el-drawer__open &.#{$direction} {
|
&.#{$direction} {
|
||||||
animation: #{$direction}-drawer-in .3s 1ms;
|
animation: #{$direction}-drawer-animation 0.3s linear reverse;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin animation-out($direction) {
|
@mixin animation-out($direction) {
|
||||||
&.#{$direction} {
|
&.#{$direction} {
|
||||||
animation: #{$direction}-drawer-out .3s;
|
animation: #{$direction}-drawer-animation 0.3s linear;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,29 +71,23 @@
|
|||||||
$directions: rtl, ltr, ttb, btt;
|
$directions: rtl, ltr, ttb, btt;
|
||||||
|
|
||||||
@include b(drawer) {
|
@include b(drawer) {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background-color: $--dialog-background-color;
|
background-color: $--dialog-background-color;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
box-shadow: 0 8px 10px -5px rgba(0, 0, 0, 0.2),
|
box-shadow: 0 8px 10px -5px rgba(0, 0, 0, 0.2),
|
||||||
0 16px 24px 2px rgba(0, 0, 0, 0.14),
|
0 16px 24px 2px rgba(0, 0, 0, 0.14), 0 6px 30px 5px rgba(0, 0, 0, 0.12);
|
||||||
0 6px 30px 5px rgba(0, 0, 0, 0.12);
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
@each $direction in $directions {
|
@each $direction in $directions {
|
||||||
@include animation-out($direction);
|
.el-drawer-fade-enter-active & {
|
||||||
@include animation-in($direction);
|
@include animation-in($direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__wrapper {
|
.el-drawer-fade-leave-active & {
|
||||||
position: fixed;
|
@include animation-out($direction);
|
||||||
top: 0;
|
}
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__header {
|
&__header {
|
||||||
@ -170,13 +124,15 @@ $directions: rtl, ltr, ttb, btt;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.ltr, &.rtl {
|
&.ltr,
|
||||||
|
&.rtl {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.ttb, &.btt {
|
&.ttb,
|
||||||
|
&.btt {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 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 {
|
.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 {
|
.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;
|
overflow: hidden;
|
||||||
backface-visibility: 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) {
|
@include e(header) {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: $--msgbox-padding-primary;
|
padding: $--msgbox-padding-primary;
|
||||||
@ -195,12 +178,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.msgbox-fade-enter-active {
|
.fade-in-linear-enter-active {
|
||||||
animation: msgbox-fade-in .3s;
|
.el-message-box {
|
||||||
|
animation: msgbox-fade-in .3s;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.msgbox-fade-leave-active {
|
.fade-in-linear-leave-active {
|
||||||
animation: msgbox-fade-out .3s;
|
.el-message-box {
|
||||||
|
animation: msgbox-fade-in .3s reverse;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes msgbox-fade-in {
|
@keyframes msgbox-fade-in {
|
||||||
|
@ -12,6 +12,15 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
overflow: auto;
|
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,
|
element: HTMLElement | Document | Window,
|
||||||
event: string,
|
event: string,
|
||||||
handler: EventListenerOrEventListenerObject,
|
handler: EventListenerOrEventListenerObject,
|
||||||
|
useCapture = false,
|
||||||
): void {
|
): void {
|
||||||
if (element && event && handler) {
|
if (element && event && handler) {
|
||||||
element.removeEventListener(event, handler, false)
|
element.removeEventListener(event, handler, useCapture)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user