fix(popper): fix popper's mechanism (#177)

- Rewrite popper's render method \
This commit is contained in:
jeremywu 2020-08-24 00:00:20 +08:00 committed by GitHub
parent 24f2a83680
commit 44d7cc2426
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 193 additions and 111 deletions

View File

@ -80,16 +80,11 @@ describe('Popper.vue', () => {
})
test('append to body', () => {
const { appendChild } = document.body
document.body.appendChild = jest.fn(child => {
return appendChild.call(document.body, child)
})
let wrapper = _mount()
expect(wrapper.find('[role="tooltip"]').exists()).toBe(false)
expect(document.body.appendChild).toHaveBeenCalled()
document.body.appendChild = appendChild
const selector = '[role="tooltip"]'
expect(wrapper.find(selector).exists()).toBe(false)
// Due to the parent node of popper is Transition so we should match the grandparent
expect(document.querySelector(selector).parentElement.parentElement).toBe(document.body)
wrapper = _mount({
appendToBody: false,
})

View File

@ -1,7 +1,10 @@
<template>
<div>
<div>referrer</div>
<el-popper :placement="placement" :disabled="disabled">
<div class="popper-container">
<el-popper
:placement="placement"
:disabled="disabled"
effect="light"
>
<template #default>content</template>
<template #trigger>
<el-button v-if="showButton" @click="disabled = !disabled">
@ -21,6 +24,7 @@
<script lang="ts">
import { ref } from 'vue'
import { getRandomInt } from '@element-plus/utils/util'
const placements = ['top','top-start','top-end','bottom','bottom-start','bottom-end','left','left-start','left-end','right','right-start', 'right-end']
export default {
@ -32,10 +36,19 @@ export default {
referrer: ref(null),
placement,
toggle: () => {
const random = Math.floor(Math.random() * 13)
const random = getRandomInt(12) // range [0, 11]
placement.value = placements[random]
},
}
},
}
</script>
<style scoped>
.popper-container {
width: 100%;
height: 100%;
padding: 150px;
}
</style>

View File

@ -1,41 +1,19 @@
<template>
<slot name="trigger"></slot>
<transition :name="transition">
<div
v-show="visible"
:id="popperId"
ref="popperRef"
role="tooltip"
:aria-hidden="visible ? 'false' : 'true'"
:class="['el-popper', 'is-' + effect, popperClass]"
@mouseenter="_show"
@mouseleave="_hide"
>
<!-- mark this parent container un-indexable to disable it from being indexed -->
<slot>
<div>{{ content }}</div>
</slot>
<div
v-if="showArrow"
ref="arrowRef"
class="el-popper__arrow"
data-popper-arrow
></div>
</div>
</transition>
</template>
<script lang="ts">
import {
computed,
defineComponent,
getCurrentInstance,
h,
ref,
onBeforeUnmount,
onMounted,
onUpdated,
watch,
Fragment,
Teleport,
Transition,
withDirectives,
vShow,
} from 'vue'
import { isArray } from '@vue/shared'
import { debounce } from 'lodash'
@ -45,26 +23,34 @@ import { generateId } from '@element-plus/utils/util'
import { on, off } from '@element-plus/utils/dom'
import throwError from '@element-plus/utils/error'
import useModifer from './useModifier'
import type { PropType, Ref } from 'vue'
import type {
Placement,
Instance as PopperInstance,
PositioningStrategy,
} from '@popperjs/core'
import useModifier from './useModifier'
type Effect = 'dark' | 'light';
type RefElement = Nullable<HTMLElement>
import type { PropType, Ref } from 'vue'
import type { Effect, Offset, Placement, PopperInstance, PositioningStrategy, RefElement, Options } from './popper'
const stop = (e: Event) => e.stopPropagation()
const getTrigger = () => {
const {
subTree: { dynamicChildren },
} = getCurrentInstance()
return dynamicChildren[0].children
const { subTree: { children } } = getCurrentInstance()
// SubTree is formed by <slot name="trigger"/><popper />
// So that the trigger element is within the slot, we need to take it out of the slot in order to attach
// events on top of it
const targetSlot = children[0]
if (targetSlot.length > 1) {
console.warn('Popper will only be attached to the first child')
}
// This indicates if the slot is rendered with directives (e.g. v-if) or templates (e.g. <template />)
// if it's true, then the children needs to be taken by accessing targetSlots.children to get it
const trigger: HTMLElement = targetSlot.type === Fragment
? targetSlot.children[0].el
: targetSlot.el
if (!trigger) {
throwError(compName, 'Cannot find referrer to attach popper to')
}
return trigger
}
const compName = 'ElPopper'
@ -124,9 +110,9 @@ export default defineComponent({
default: 0,
},
offset: {
type: [Number, Array] as PropType<[number, number] | number>,
default: [0, 12] as [number, number],
validator: (val: [number, number] | number): boolean => {
type: [Number, Array] as PropType<Offset>,
default: [0, 12] as Offset,
validator: (val: Offset): boolean => {
return (isArray(val) && val.length === 2) || typeof val === 'number'
},
},
@ -138,6 +124,15 @@ export default defineComponent({
type: String,
default: '',
},
pure: {
type: Boolean,
default: false,
},
// Once this option were given, the entire popper is under the users' control, top priority
popperOptions: {
type: Object as PropType<Options>,
default: () => null,
},
referrer: {
type: HTMLElement as PropType<Nullable<HTMLElement>>,
default: null as Nullable<HTMLElement>,
@ -160,7 +155,7 @@ export default defineComponent({
},
value: {
type: Boolean,
default: true,
default: false,
},
},
setup(props, { slots }) {
@ -209,10 +204,6 @@ export default defineComponent({
/* istanbul ignore if */
if (!popperInstance.value || (visible.value && !forceDestroy)) return
detach()
if (popperRef.value && props.appendToBody) {
off(popperRef.value, 'click', stop)
document.body.removeChild(popperRef.value)
}
}
function detach() {
@ -265,6 +256,7 @@ export default defineComponent({
}
exceptionState.value = state
}
function _show() {
setExpectionState(true)
showPopper()
@ -284,38 +276,28 @@ export default defineComponent({
}
function initializePopper() {
const subTree = getTrigger()
const _trigger = getTrigger()
if (subTree.length > 1) {
console.warn('Popper will only be attached to the first child')
}
let referenceElement: HTMLElement
if (subTree[0].type === Fragment) {
referenceElement = subTree[0].children[0].el
} else {
referenceElement = subTree[0].el
}
if (!referenceElement) {
throwError(compName, 'Cannot find referrer to attach popper to')
}
trigger.value = referenceElement
const modifiers = useModifer(popperOptions.value.modifierOptions)
popperInstance.value = createPopper(referenceElement, popperRef.value, {
placement: popperOptions.value.placement,
onFirstUpdate: () => {
popperInstance.value.forceUpdate()
},
strategy: popperOptions.value.strategy,
modifiers,
})
referenceElement.setAttribute('aria-describedby', popperId.value)
referenceElement.setAttribute('tabindex', props.tabIndex)
on(referenceElement, 'mouseenter', _show)
on(referenceElement, 'mouseleave', _hide)
on(referenceElement, 'focus', handleFocus)
on(referenceElement, 'blur', handleBlur)
on(popperRef.value, 'click', stop)
trigger.value = _trigger
popperInstance.value = createPopper(_trigger, popperRef.value,
props.popperOptions !== null
? props.popperOptions
: {
placement: popperOptions.value.placement,
onFirstUpdate: () => {
popperInstance.value.forceUpdate()
},
strategy: popperOptions.value.strategy,
modifiers: useModifier(popperOptions.value.modifierOptions),
})
_trigger.setAttribute('aria-describedby', popperId.value)
_trigger.setAttribute('tabindex', props.tabIndex)
on(_trigger, 'mouseenter', _show)
on(_trigger, 'mouseleave', _hide)
on(_trigger, 'focus', handleFocus)
on(_trigger, 'blur', handleBlur)
}
watch(
@ -330,6 +312,7 @@ export default defineComponent({
)
watch(() => popperOptions.value, val => {
if (!popperInstance.value) return
popperInstance.value.setOptions({
placement: val.placement,
strategy: val.strategy,
@ -340,9 +323,6 @@ export default defineComponent({
onMounted(() => {
initializePopper()
if (props.appendToBody && popperRef.value) {
document.body.appendChild(popperRef.value)
}
})
onBeforeUnmount(() => {
@ -350,8 +330,8 @@ export default defineComponent({
})
onUpdated(() => {
const subTree = getTrigger()
if (subTree[0].el !== trigger.value && popperInstance.value) {
const _trigger = getTrigger()
if (_trigger !== trigger.value && popperInstance.value) {
detach()
}
if (popperInstance.value) {
@ -378,6 +358,73 @@ export default defineComponent({
activated() {
this.initializePopper()
},
render() {
const arrow = this.showArrow
? h(
'div',
{
ref: 'arrowRef',
class: 'el-popper__arrow',
'data-popper-arrow': '',
},
)
: null
const popper = h(
Transition,
{
name: this.transition,
},
{
default: () =>
withDirectives(
h(
'div',
{
ariaHidden: this.visible ? 'false' : 'true',
class: [
'el-popper',
'is-' + this.effect,
this.popperClass,
this.pure
? 'el-popper__pure'
: '',
],
id: this.popperId,
ref: 'popperRef',
role: 'tooltip',
onMouseEnter: this._show,
onMouseLeave: this._hide,
onClick: stop,
},
[
this.$slots.default ? this.$slots.default() : this.content,
arrow,
],
),
[
[vShow, this.visible],
],
),
},
)
return h(
Fragment,
null,
[
this.$slots.trigger?.(),
this.appendToBody
? h(
Teleport,
{
to: 'body',
},
popper,
)
: popper,
],
)
},
})
</script>
@ -396,8 +443,8 @@ export default defineComponent({
.el-popper__arrow,
.el-popper__arrow::before {
position: absolute;
width: 8px;
height: 8px;
width: 10px;
height: 10px;
z-index: -1;
}
@ -409,19 +456,19 @@ export default defineComponent({
}
.el-popper[data-popper-placement^="top"] > .el-popper__arrow {
bottom: -4px;
bottom: -5px;
}
.el-popper[data-popper-placement^="bottom"] > .el-popper__arrow {
top: -4px;
top: -5px;
}
.el-popper[data-popper-placement^="left"] > .el-popper__arrow {
right: -4px;
right: -5px;
}
.el-popper[data-popper-placement^="right"] > .el-popper__arrow {
left: -4px;
left: -5px;
}
.el-popper.is-dark {
@ -443,22 +490,31 @@ export default defineComponent({
}
.el-popper.is-light[data-popper-placement^="top"] .el-popper__arrow::before {
border-top: none;
border-left: none;
border-top-color: transparent;
border-left-color: transparent;
}
.el-popper.is-light[data-popper-placement^="bottom"] .el-popper__arrow::before {
border-top: none;
border-left: none;
border-bottom-color: transparent;
border-right-color: transparent;
}
.el-popper.is-light[data-popper-placement^="left"] .el-popper__arrow::before {
border-left: none;
border-bottom: none;
border-left-color: transparent;
border-bottom-color: transparent;
}
.el-popper.is-light[data-popper-placement^="right"] .el-popper__arrow::before {
border-top: none;
border-right: none;
border-top-color: transparent;
border-right-color: transparent;
}
.el-popper.el-popper__pure {
padding: 0;
border: none;
}
.el-popper.el-popper__pure .el-popper__arrow::before {
border: none;
}
</style>

10
packages/popper/src/popper.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
export type Effect = 'dark' | 'light';
export type RefElement = Nullable<HTMLElement>
export type Offset = [number, number] | number
export type {
Placement,
Instance as PopperInstance,
PositioningStrategy,
Options,
} from '@popperjs/core'

View File

@ -147,4 +147,12 @@ export function rafThrottle(fn: (args: Record<string, unknown>) => unknown): (..
export const objToArray = castArray
/**
* Generating a random int in range (0, max - 1)
* @param max {number}
*/
export function getRandomInt(max: number) {
return Math.floor(Math.random() * Math.floor(max))
}
export { isVNode } from 'vue'