fix(popper): remove clickouside listener when manual enabled (#450)

This commit is contained in:
jeremywu 2020-10-22 14:00:33 +08:00 committed by GitHub
parent 8d2d6be085
commit 8e95db293c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 175 additions and 106 deletions

View File

@ -97,7 +97,7 @@ describe('Dialog.vue', () => {
test('should not have overlay mask when mask is false', () => {
const wrapper = _mount({
props: {
mask: false,
modal: false,
},
})

View File

@ -1,14 +1,21 @@
import {
createVNode,
defineComponent,
Fragment,
Transition,
Teleport,
h,
withDirectives,
vShow,
toDisplayString,
renderSlot,
withCtx,
} from 'vue'
import { TrapFocus } from '@element-plus/directives'
import { stop } from '@element-plus/utils/dom'
import { isValidWidthUnit } from '@element-plus/utils/validators'
import { PatchFlags, renderBlock, renderIf } from '@element-plus/utils/vnode'
import ElOverlay from '@element-plus/overlay'
import {
@ -22,6 +29,13 @@ import {
import type { PropType, SetupContext } from 'vue'
const closeIcon = createVNode('i', { class: 'el-dialog__close el-icon el-icon-close' }, null, PatchFlags.HOISTED)
const headerKls = { class: 'el-dialog__header' }
const bodyKls = { class: 'el-dialog__body' }
const titleKls = { class: 'el-dialog__title' }
const footerKls = { class: 'el-dialog__footer', key: 0 }
export default defineComponent({
name: 'ElDialog',
props: {
@ -114,44 +128,38 @@ export default defineComponent({
return null
}
const { $slots } = this
const closeBtn = this.showClose
? h(
'button',
{
type: 'button',
class: 'el-dialog__headerbtn',
ariaLabel: 'close',
onClick: this.handleClose,
},
h('i', { class: 'el-dialog__close el-icon el-icon-close' }),
)
: null
const header = h(
'div',
const closeBtn = renderIf(this.showClose, 'button',
{
class: 'el-dialog__header',
type: 'button',
class: 'el-dialog__headerbtn',
ariaLabel: 'close',
onClick: this.handleClose,
},
[closeIcon],
PatchFlags.PROPS,
['onClick'],
)
const header = createVNode(
'div',
headerKls,
[
$slots.header
? $slots.header()
: h('span', { class: 'el-dialog__title' }, this.title),
renderSlot($slots, 'header', {}, () =>
[createVNode('span', titleKls, toDisplayString(this.title), PatchFlags.TEXT)],
),
closeBtn,
],
)
const body = h(
const body = createVNode(
'div',
{
class: 'el-dialog__body',
},
$slots.default?.(),
bodyKls,
[renderSlot($slots, 'default')],
)
const footer = $slots.footer
? h('div', { class: 'el-dialog__footer' }, $slots.footer())
: null
const footer = renderIf(!!$slots.footer, 'div', footerKls, [renderSlot($slots, 'footer')])
const dialog = h(
const dialog = createVNode(
'div',
{
ariaModal: true,
@ -167,14 +175,16 @@ export default defineComponent({
ref: 'dialogRef',
role: 'dialog',
style: this.style,
onClick: (e: MouseEvent) => e.stopPropagation(),
onClick: stop,
},
[header, body, footer],
PatchFlags.STYLE | PatchFlags.CLASS | PatchFlags.PROPS,
['ariaLabel', 'onClick'],
)
const trappedDialog = withDirectives(dialog, [[TrapFocus]])
const overlay = withDirectives(
h(
createVNode(
ElOverlay,
{
mask: this.modal,
@ -182,33 +192,31 @@ export default defineComponent({
zIndex: this.zIndex,
},
{
default: () => trappedDialog,
default: withCtx(() => [trappedDialog]),
},
),
[[vShow, this.visible]],
)
PatchFlags.PROPS,
['mask', 'onClick', 'zIndex'],
), [[vShow, this.visible]])
const renderer = h(
const renderer = createVNode(
Transition,
{
name: 'dialog-fade',
onAfterEnter: this.afterEnter,
onAfterLeave: this.afterLeave,
'onAfter-enter': this.afterEnter,
'onAfter-leave': this.afterLeave,
},
{
default: () => overlay,
default: () => [overlay],
},
PatchFlags.PROPS,
['onAfter-enter', 'onAfter-leave'],
)
if (this.appendToBody) {
return h(
Teleport,
{
to: 'body',
},
renderer,
)
}
return renderer
return renderBlock(Fragment, null, [
this.appendToBody
? h(Teleport, { key: 0, to: 'body' }, [renderer])
: h(Fragment, { key: 1 }, [renderer]),
])
},
})

View File

@ -1,5 +1,7 @@
<script lang='ts'>
import { defineComponent, h } from 'vue'
<script lang="ts">
import { createVNode, defineComponent, renderSlot } from 'vue'
import { PatchFlags } from '@element-plus/utils/vnode'
export default defineComponent({
name: 'ElOverlay',
props: {
@ -22,7 +24,7 @@ export default defineComponent({
// init here
return () => {
return props.mask
? h(
? createVNode(
'div',
{
class: ['el-overlay', props.overlayClass],
@ -31,9 +33,11 @@ export default defineComponent({
},
onClick: onMaskClick,
},
slots.default?.(),
[renderSlot(slots, 'default')],
PatchFlags.STYLE | PatchFlags.CLASS | PatchFlags.PROPS,
['onClick'],
)
: slots.default?.()
: renderSlot(slots, 'default')
}
},
})
@ -51,7 +55,6 @@ export default defineComponent({
left: 0;
z-index: 2000;
height: 100%;
background-color: rgba(0,0,0,.5);
background-color: rgba(0, 0, 0, 0.5);
}
</style>

View File

@ -1,28 +1,26 @@
<script lang="ts">
import {
createVNode,
defineComponent,
h,
Fragment,
Teleport,
onMounted,
onBeforeUnmount,
onDeactivated,
onActivated,
renderSlot,
toDisplayString,
withCtx,
} from 'vue'
import { ClickOutside } from '@element-plus/directives'
import throwError from '@element-plus/utils/error'
import { stop } from '@element-plus/utils/dom'
import { renderBlock, PatchFlags } from '@element-plus/utils/vnode'
import usePopper from './use-popper/index'
import defaultProps from './use-popper/defaults'
import {
renderMask,
renderPopper,
renderTrigger,
renderArrow,
} from './renderers'
import { Mask, renderPopper, renderTrigger, renderArrow } from './renderers'
const compName = 'ElPopper'
const UPDATE_VISIBLE_EVENT = 'update:visible'
@ -31,9 +29,6 @@ const emits = [UPDATE_VISIBLE_EVENT, 'after-enter', 'after-leave']
export default defineComponent({
name: compName,
directives: {
ClickOutside,
},
props: defaultProps,
emits,
setup(props, ctx) {
@ -90,11 +85,9 @@ export default defineComponent({
visibility,
},
[
$slots.default ? h(
Fragment,
null,
$slots.default(),
) : this.content,
renderSlot($slots, 'default', {}, () => {
return [toDisplayString(this.content)]
}),
arrow,
],
)
@ -109,19 +102,31 @@ export default defineComponent({
...this.events,
})
return h(Fragment, null, [
return renderBlock(Fragment, null, [
trigger,
appendToBody
? h(
? createVNode(
Teleport,
{
to: 'body',
key: 0,
},
renderMask(popper, {
hide,
}),
[
createVNode(
Mask,
{
hide,
isManualMode: this.isManualMode(),
},
{
default: withCtx(() => [popper]),
},
PatchFlags.PROPS,
['hide', 'isManualMode'],
),
],
)
: popper,
: renderBlock(Fragment, { key: 1 }, [popper]),
])
},
})

View File

@ -1,4 +1,4 @@
export { default as renderMask } from './mask'
export { default as Mask } from './mask'
export { default as renderPopper } from './popper'
export { default as renderTrigger } from './trigger'
export { default as renderArrow } from './arrow'

View File

@ -1,18 +1,61 @@
import { h, withDirectives } from 'vue'
import type { VNode } from 'vue'
import { withDirectives, renderSlot, createVNode } from 'vue'
import { ClickOutside } from '@element-plus/directives'
interface IRenderMaskProps {
hide: () => void
manualMode: boolean
}
export default function renderMask(popper: VNode, { hide }: IRenderMaskProps): VNode {
return withDirectives(
h('div', {
class: 'el-popper__mask',
}, popper),
// marking excludes as any due to the current version of Vue's definition file
// DOES NOT support types other than string as arguments
[[ClickOutside, hide]],
)
const _hoist1 = {
key: 0,
class: 'el-popper__mask',
}
const _hoist2 = {
key: 1,
class: 'el-popper__mask',
}
// export default function renderMask(popper: VNode, { hide, manualMode }: IRenderMaskProps): VNode {
// return manualMode ? withDirectives(
// renderBlock('div', _hoist1, [popper]),
// // marking excludes as any due to the current version of Vue's definition file
// // DOES NOT support types other than string as arguments
// [[ClickOutside, hide]],
// ) : renderBlock('div', _hoist2, [popper])
// }
export default ({
hide,
manualMode,
}: IRenderMaskProps, { slots }) => {
const children = renderSlot(slots, 'default')
return manualMode
? withDirectives(
createVNode('div', _hoist1, [ children ]), [[ClickOutside, hide]],
)
: createVNode('div', _hoist2, [ children ])
}
// defineComponent({
// template: `
// <div v-if="!manualMode" v-click-outside="hide">
// <slot />
// </div>
// <div v-else>
// <slot />
// </div>
// `,
// directives: {
// ClickOutside,
// },
// props: {
// hide: {
// type: Function as PropType<() => void>,
// },
// manualMode: {
// type: Boolean,
// },
// },
// })

View File

@ -263,6 +263,7 @@ export default function (props: IPopperOptions, { emit }: SetupContext<string[]>
emit('after-leave')
},
initializePopper,
isManualMode,
arrowRef,
events,
popperId,

View File

@ -13,14 +13,16 @@ Dialog pops up a dialog box, and it's quite customizable.
<el-dialog
title="Tips"
:visible.sync="dialogVisible"
v-model="dialogVisible"
width="30%"
:before-close="handleClose">
<span>This is a message</span>
<span slot="footer" class="dialog-footer">
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">Cancel</el-button>
<el-button type="primary" @click="dialogVisible = false">Confirm</el-button>
</span>
</template>
</el-dialog>
<script>
@ -35,6 +37,7 @@ Dialog pops up a dialog box, and it's quite customizable.
this.$confirm('Are you sure to close this dialog?')
.then(_ => {
done();
this.dialogVisible = false
})
.catch(_ => {});
}
@ -58,7 +61,7 @@ The content of Dialog can be anything, even a table or a form. This example show
<!-- Table -->
<el-button type="text" @click="dialogTableVisible = true">open a Table nested Dialog</el-button>
<el-dialog title="Shipping address" :visible.sync="dialogTableVisible">
<el-dialog title="Shipping address" v-model="dialogTableVisible">
<el-table :data="gridData">
<el-table-column property="date" label="Date" width="150"></el-table-column>
<el-table-column property="name" label="Name" width="200"></el-table-column>
@ -69,7 +72,7 @@ The content of Dialog can be anything, even a table or a form. This example show
<!-- Form -->
<el-button type="text" @click="dialogFormVisible = true">open a Form nested Dialog</el-button>
<el-dialog title="Shipping address" :visible.sync="dialogFormVisible">
<el-dialog title="Shipping address" v-model="dialogFormVisible">
<el-form :model="form">
<el-form-item label="Promotion name" :label-width="formLabelWidth">
<el-input v-model="form.name" autocomplete="off"></el-input>
@ -135,17 +138,21 @@ If a Dialog is nested in another Dialog, `append-to-body` is required.
<template>
<el-button type="text" @click="outerVisible = true">open the outer Dialog</el-button>
<el-dialog title="Outer Dialog" :visible.sync="outerVisible">
<el-dialog
width="30%"
title="Inner Dialog"
:visible.sync="innerVisible"
append-to-body>
</el-dialog>
<div slot="footer" class="dialog-footer">
<el-dialog title="Outer Dialog" v-model="outerVisible">
<template #default>
<el-dialog
width="30%"
title="Inner Dialog"
v-model="innerVisible"
append-to-body>
</el-dialog>
</template>
<template #footer>
<div class="dialog-footer">
<el-button @click="outerVisible = false">Cancel</el-button>
<el-button type="primary" @click="innerVisible = true">open the inner Dialog</el-button>
</div>
</template>
</el-dialog>
</template>
@ -172,14 +179,16 @@ Dialog's content can be centered.
<el-dialog
title="Warning"
:visible.sync="centerDialogVisible"
v-model="centerDialogVisible"
width="30%"
center>
<span>It should be noted that the content will not be aligned in center by default</span>
<span slot="footer" class="dialog-footer">
<el-button @click="centerDialogVisible = false">Cancel</el-button>
<el-button type="primary" @click="centerDialogVisible = false">Confirm</el-button>
</span>
<template #footer>
<span class="dialog-footer">
<el-button @click="centerDialogVisible = false">Cancel</el-button>
<el-button type="primary" @click="centerDialogVisible = false">Confirm</el-button>
</span>
</footer>
</el-dialog>
<script>