mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-12-04 04:08:34 +08:00
fix(popper): fix popper's mechanism (#177)
- Rewrite popper's render method \
This commit is contained in:
parent
24f2a83680
commit
44d7cc2426
@ -80,16 +80,11 @@ describe('Popper.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('append to body', () => {
|
test('append to body', () => {
|
||||||
const { appendChild } = document.body
|
|
||||||
document.body.appendChild = jest.fn(child => {
|
|
||||||
return appendChild.call(document.body, child)
|
|
||||||
})
|
|
||||||
|
|
||||||
let wrapper = _mount()
|
let wrapper = _mount()
|
||||||
expect(wrapper.find('[role="tooltip"]').exists()).toBe(false)
|
const selector = '[role="tooltip"]'
|
||||||
expect(document.body.appendChild).toHaveBeenCalled()
|
expect(wrapper.find(selector).exists()).toBe(false)
|
||||||
|
// Due to the parent node of popper is Transition so we should match the grandparent
|
||||||
document.body.appendChild = appendChild
|
expect(document.querySelector(selector).parentElement.parentElement).toBe(document.body)
|
||||||
wrapper = _mount({
|
wrapper = _mount({
|
||||||
appendToBody: false,
|
appendToBody: false,
|
||||||
})
|
})
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="popper-container">
|
||||||
<div>referrer</div>
|
<el-popper
|
||||||
<el-popper :placement="placement" :disabled="disabled">
|
:placement="placement"
|
||||||
|
:disabled="disabled"
|
||||||
|
effect="light"
|
||||||
|
>
|
||||||
<template #default>content</template>
|
<template #default>content</template>
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<el-button v-if="showButton" @click="disabled = !disabled">
|
<el-button v-if="showButton" @click="disabled = !disabled">
|
||||||
@ -21,6 +24,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ref } from 'vue'
|
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']
|
const placements = ['top','top-start','top-end','bottom','bottom-start','bottom-end','left','left-start','left-end','right','right-start', 'right-end']
|
||||||
export default {
|
export default {
|
||||||
@ -32,10 +36,19 @@ export default {
|
|||||||
referrer: ref(null),
|
referrer: ref(null),
|
||||||
placement,
|
placement,
|
||||||
toggle: () => {
|
toggle: () => {
|
||||||
const random = Math.floor(Math.random() * 13)
|
const random = getRandomInt(12) // range [0, 11]
|
||||||
placement.value = placements[random]
|
placement.value = placements[random]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.popper-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
@ -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">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
defineComponent,
|
defineComponent,
|
||||||
getCurrentInstance,
|
getCurrentInstance,
|
||||||
|
h,
|
||||||
ref,
|
ref,
|
||||||
onBeforeUnmount,
|
onBeforeUnmount,
|
||||||
onMounted,
|
onMounted,
|
||||||
onUpdated,
|
onUpdated,
|
||||||
watch,
|
watch,
|
||||||
Fragment,
|
Fragment,
|
||||||
|
Teleport,
|
||||||
|
Transition,
|
||||||
|
withDirectives,
|
||||||
|
vShow,
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
import { isArray } from '@vue/shared'
|
import { isArray } from '@vue/shared'
|
||||||
import { debounce } from 'lodash'
|
import { debounce } from 'lodash'
|
||||||
@ -45,26 +23,34 @@ import { generateId } from '@element-plus/utils/util'
|
|||||||
import { on, off } from '@element-plus/utils/dom'
|
import { on, off } from '@element-plus/utils/dom'
|
||||||
import throwError from '@element-plus/utils/error'
|
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'
|
import useModifier from './useModifier'
|
||||||
|
|
||||||
type Effect = 'dark' | 'light';
|
import type { PropType, Ref } from 'vue'
|
||||||
type RefElement = Nullable<HTMLElement>
|
|
||||||
|
import type { Effect, Offset, Placement, PopperInstance, PositioningStrategy, RefElement, Options } from './popper'
|
||||||
|
|
||||||
const stop = (e: Event) => e.stopPropagation()
|
const stop = (e: Event) => e.stopPropagation()
|
||||||
|
|
||||||
const getTrigger = () => {
|
const getTrigger = () => {
|
||||||
const {
|
const { subTree: { children } } = getCurrentInstance()
|
||||||
subTree: { dynamicChildren },
|
// SubTree is formed by <slot name="trigger"/><popper />
|
||||||
} = getCurrentInstance()
|
// So that the trigger element is within the slot, we need to take it out of the slot in order to attach
|
||||||
return dynamicChildren[0].children
|
// 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'
|
const compName = 'ElPopper'
|
||||||
@ -124,9 +110,9 @@ export default defineComponent({
|
|||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
offset: {
|
offset: {
|
||||||
type: [Number, Array] as PropType<[number, number] | number>,
|
type: [Number, Array] as PropType<Offset>,
|
||||||
default: [0, 12] as [number, number],
|
default: [0, 12] as Offset,
|
||||||
validator: (val: [number, number] | number): boolean => {
|
validator: (val: Offset): boolean => {
|
||||||
return (isArray(val) && val.length === 2) || typeof val === 'number'
|
return (isArray(val) && val.length === 2) || typeof val === 'number'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -138,6 +124,15 @@ export default defineComponent({
|
|||||||
type: String,
|
type: String,
|
||||||
default: '',
|
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: {
|
referrer: {
|
||||||
type: HTMLElement as PropType<Nullable<HTMLElement>>,
|
type: HTMLElement as PropType<Nullable<HTMLElement>>,
|
||||||
default: null as Nullable<HTMLElement>,
|
default: null as Nullable<HTMLElement>,
|
||||||
@ -160,7 +155,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup(props, { slots }) {
|
setup(props, { slots }) {
|
||||||
@ -209,10 +204,6 @@ export default defineComponent({
|
|||||||
/* istanbul ignore if */
|
/* istanbul ignore if */
|
||||||
if (!popperInstance.value || (visible.value && !forceDestroy)) return
|
if (!popperInstance.value || (visible.value && !forceDestroy)) return
|
||||||
detach()
|
detach()
|
||||||
if (popperRef.value && props.appendToBody) {
|
|
||||||
off(popperRef.value, 'click', stop)
|
|
||||||
document.body.removeChild(popperRef.value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function detach() {
|
function detach() {
|
||||||
@ -265,6 +256,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
exceptionState.value = state
|
exceptionState.value = state
|
||||||
}
|
}
|
||||||
|
|
||||||
function _show() {
|
function _show() {
|
||||||
setExpectionState(true)
|
setExpectionState(true)
|
||||||
showPopper()
|
showPopper()
|
||||||
@ -284,38 +276,28 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initializePopper() {
|
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, {
|
trigger.value = _trigger
|
||||||
placement: popperOptions.value.placement,
|
|
||||||
onFirstUpdate: () => {
|
popperInstance.value = createPopper(_trigger, popperRef.value,
|
||||||
popperInstance.value.forceUpdate()
|
props.popperOptions !== null
|
||||||
},
|
? props.popperOptions
|
||||||
strategy: popperOptions.value.strategy,
|
: {
|
||||||
modifiers,
|
placement: popperOptions.value.placement,
|
||||||
})
|
onFirstUpdate: () => {
|
||||||
referenceElement.setAttribute('aria-describedby', popperId.value)
|
popperInstance.value.forceUpdate()
|
||||||
referenceElement.setAttribute('tabindex', props.tabIndex)
|
},
|
||||||
on(referenceElement, 'mouseenter', _show)
|
strategy: popperOptions.value.strategy,
|
||||||
on(referenceElement, 'mouseleave', _hide)
|
modifiers: useModifier(popperOptions.value.modifierOptions),
|
||||||
on(referenceElement, 'focus', handleFocus)
|
})
|
||||||
on(referenceElement, 'blur', handleBlur)
|
_trigger.setAttribute('aria-describedby', popperId.value)
|
||||||
on(popperRef.value, 'click', stop)
|
_trigger.setAttribute('tabindex', props.tabIndex)
|
||||||
|
on(_trigger, 'mouseenter', _show)
|
||||||
|
on(_trigger, 'mouseleave', _hide)
|
||||||
|
on(_trigger, 'focus', handleFocus)
|
||||||
|
on(_trigger, 'blur', handleBlur)
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@ -330,6 +312,7 @@ export default defineComponent({
|
|||||||
)
|
)
|
||||||
|
|
||||||
watch(() => popperOptions.value, val => {
|
watch(() => popperOptions.value, val => {
|
||||||
|
if (!popperInstance.value) return
|
||||||
popperInstance.value.setOptions({
|
popperInstance.value.setOptions({
|
||||||
placement: val.placement,
|
placement: val.placement,
|
||||||
strategy: val.strategy,
|
strategy: val.strategy,
|
||||||
@ -340,9 +323,6 @@ export default defineComponent({
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initializePopper()
|
initializePopper()
|
||||||
if (props.appendToBody && popperRef.value) {
|
|
||||||
document.body.appendChild(popperRef.value)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@ -350,8 +330,8 @@ export default defineComponent({
|
|||||||
})
|
})
|
||||||
|
|
||||||
onUpdated(() => {
|
onUpdated(() => {
|
||||||
const subTree = getTrigger()
|
const _trigger = getTrigger()
|
||||||
if (subTree[0].el !== trigger.value && popperInstance.value) {
|
if (_trigger !== trigger.value && popperInstance.value) {
|
||||||
detach()
|
detach()
|
||||||
}
|
}
|
||||||
if (popperInstance.value) {
|
if (popperInstance.value) {
|
||||||
@ -378,6 +358,73 @@ export default defineComponent({
|
|||||||
activated() {
|
activated() {
|
||||||
this.initializePopper()
|
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>
|
</script>
|
||||||
|
|
||||||
@ -396,8 +443,8 @@ export default defineComponent({
|
|||||||
.el-popper__arrow,
|
.el-popper__arrow,
|
||||||
.el-popper__arrow::before {
|
.el-popper__arrow::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 8px;
|
width: 10px;
|
||||||
height: 8px;
|
height: 10px;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -409,19 +456,19 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.el-popper[data-popper-placement^="top"] > .el-popper__arrow {
|
.el-popper[data-popper-placement^="top"] > .el-popper__arrow {
|
||||||
bottom: -4px;
|
bottom: -5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-popper[data-popper-placement^="bottom"] > .el-popper__arrow {
|
.el-popper[data-popper-placement^="bottom"] > .el-popper__arrow {
|
||||||
top: -4px;
|
top: -5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-popper[data-popper-placement^="left"] > .el-popper__arrow {
|
.el-popper[data-popper-placement^="left"] > .el-popper__arrow {
|
||||||
right: -4px;
|
right: -5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-popper[data-popper-placement^="right"] > .el-popper__arrow {
|
.el-popper[data-popper-placement^="right"] > .el-popper__arrow {
|
||||||
left: -4px;
|
left: -5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-popper.is-dark {
|
.el-popper.is-dark {
|
||||||
@ -443,22 +490,31 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.el-popper.is-light[data-popper-placement^="top"] .el-popper__arrow::before {
|
.el-popper.is-light[data-popper-placement^="top"] .el-popper__arrow::before {
|
||||||
border-top: none;
|
border-top-color: transparent;
|
||||||
border-left: none;
|
border-left-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-popper.is-light[data-popper-placement^="bottom"] .el-popper__arrow::before {
|
.el-popper.is-light[data-popper-placement^="bottom"] .el-popper__arrow::before {
|
||||||
border-top: none;
|
border-bottom-color: transparent;
|
||||||
border-left: none;
|
border-right-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-popper.is-light[data-popper-placement^="left"] .el-popper__arrow::before {
|
.el-popper.is-light[data-popper-placement^="left"] .el-popper__arrow::before {
|
||||||
border-left: none;
|
border-left-color: transparent;
|
||||||
border-bottom: none;
|
border-bottom-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-popper.is-light[data-popper-placement^="right"] .el-popper__arrow::before {
|
.el-popper.is-light[data-popper-placement^="right"] .el-popper__arrow::before {
|
||||||
border-top: none;
|
border-top-color: transparent;
|
||||||
border-right: none;
|
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>
|
</style>
|
||||||
|
10
packages/popper/src/popper.d.ts
vendored
Normal file
10
packages/popper/src/popper.d.ts
vendored
Normal 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'
|
@ -147,4 +147,12 @@ export function rafThrottle(fn: (args: Record<string, unknown>) => unknown): (..
|
|||||||
|
|
||||||
export const objToArray = castArray
|
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'
|
export { isVNode } from 'vue'
|
||||||
|
Loading…
Reference in New Issue
Block a user