mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-12-03 03:38:41 +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', () => {
|
||||
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,
|
||||
})
|
||||
|
@ -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>
|
||||
|
@ -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
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
|
||||
|
||||
/**
|
||||
* 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'
|
||||
|
Loading…
Reference in New Issue
Block a user