mirror of
https://github.com/element-plus/element-plus.git
synced 2025-01-30 11:16:12 +08:00
feat(tooltip): tooltip component
This commit is contained in:
parent
1584f07f0c
commit
af45dbd070
@ -1,5 +1,2 @@
|
||||
import ClickOutside from './click-outside/index'
|
||||
export { default as ClickOutside } from './click-outside'
|
||||
|
||||
export {
|
||||
ClickOutside,
|
||||
}
|
||||
|
1
packages/hooks/index.ts
Normal file
1
packages/hooks/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as useEvents } from './use-events'
|
12
packages/hooks/package.json
Normal file
12
packages/hooks/package.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@element-plus/hooks",
|
||||
"version": "0.0.0",
|
||||
"main": "dist/index.js",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0-rc.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/test-utils": "^2.0.0-beta.0"
|
||||
}
|
||||
}
|
23
packages/hooks/use-events/index.ts
Normal file
23
packages/hooks/use-events/index.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { watch } from 'vue'
|
||||
import { on, off } from '@element-plus/utils/dom'
|
||||
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
type Events = Array<{
|
||||
name: string
|
||||
handler: (...args: any[]) => any
|
||||
}>
|
||||
|
||||
export default (el: Ref<HTMLElement>, events: Events) => {
|
||||
events.map(({ name, handler }) => {
|
||||
on(el.value, name, handler)
|
||||
})
|
||||
|
||||
watch(el, (_, __, onCleanup) => {
|
||||
onCleanup(() => {
|
||||
events.map(({ name, handler }) => {
|
||||
off(el.value, name, handler)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
@ -83,8 +83,14 @@ describe('Popper.vue', () => {
|
||||
let wrapper = _mount()
|
||||
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)
|
||||
/**
|
||||
* Current layout of `ElPopper`
|
||||
* --> Teleport
|
||||
* --> mask
|
||||
* --> transition
|
||||
* --> popper
|
||||
*/
|
||||
|
||||
wrapper = _mount({
|
||||
appendToBody: false,
|
||||
})
|
||||
|
@ -1,14 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
h,
|
||||
ref,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
onUpdated,
|
||||
watch,
|
||||
Fragment,
|
||||
Teleport,
|
||||
Transition,
|
||||
@ -16,47 +9,32 @@ import {
|
||||
vShow,
|
||||
} from 'vue'
|
||||
import { isArray } from '@vue/shared'
|
||||
import { debounce } from 'lodash'
|
||||
import { createPopper } from '@popperjs/core'
|
||||
|
||||
import { generateId } from '@element-plus/utils/util'
|
||||
import { on, off } from '@element-plus/utils/dom'
|
||||
import throwError from '@element-plus/utils/error'
|
||||
import { UPDATE_MODEL_EVENT } from '@element-plus/utils/constants'
|
||||
import { ClickOutside } from '@element-plus/directives'
|
||||
import { default as usePopper, DEFAULT_TRIGGER, UPDATE_VALUE_EVENT } from './usePopper'
|
||||
|
||||
import useModifier from './useModifier'
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import type { PropType, Ref } from 'vue'
|
||||
|
||||
import type { Effect, Offset, Placement, PopperInstance, PositioningStrategy, RefElement, Options } from './popper'
|
||||
import type {
|
||||
Effect,
|
||||
Offset,
|
||||
Options,
|
||||
Placement,
|
||||
PositioningStrategy,
|
||||
TriggerType,
|
||||
IPopperOptions,
|
||||
} from './popper'
|
||||
|
||||
const stop = (e: Event) => e.stopPropagation()
|
||||
|
||||
const getTrigger = () => {
|
||||
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'
|
||||
|
||||
export default defineComponent({
|
||||
name: compName,
|
||||
directives: {
|
||||
ClickOutside,
|
||||
},
|
||||
props: {
|
||||
arrowOffset: {
|
||||
type: Number,
|
||||
@ -88,7 +66,7 @@ export default defineComponent({
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
defualt: false,
|
||||
default: false,
|
||||
},
|
||||
effect: {
|
||||
type: String as PropType<Effect>,
|
||||
@ -154,216 +132,50 @@ export default defineComponent({
|
||||
type: String,
|
||||
default: 'el-fade-in-linear',
|
||||
},
|
||||
trigger: {
|
||||
type: [String, Array] as PropType<TriggerType | Array<TriggerType>>,
|
||||
default: DEFAULT_TRIGGER,
|
||||
},
|
||||
tabIndex: {
|
||||
type: String,
|
||||
default: '0',
|
||||
},
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
validator: val => typeof val === 'boolean',
|
||||
},
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: [UPDATE_MODEL_EVENT],
|
||||
setup(props, { slots, emit }) {
|
||||
const popperRef = ref<RefElement>(null)
|
||||
const arrowRef = ref<RefElement>(null)
|
||||
const trigger = ref<RefElement>(null)
|
||||
|
||||
const exceptionState = ref(false)
|
||||
const show = ref(false)
|
||||
const popperId = ref(`el-tooltip-${generateId()}`)
|
||||
|
||||
const popperInstance = ref<Nullable<PopperInstance>>(null)
|
||||
const timeout = ref<NodeJS.Timeout>(null)
|
||||
const timeoutPending = ref<NodeJS.Timeout>(null)
|
||||
|
||||
const visible = computed(() => {
|
||||
return props.manualMode ? props.value : !props.disabled && show.value
|
||||
})
|
||||
|
||||
const popperOptions = computed(() => {
|
||||
return {
|
||||
modifierOptions: {
|
||||
arrowOffset: props.arrowOffset,
|
||||
arrowRef: arrowRef.value,
|
||||
boundariesPadding: props.boundariesPadding,
|
||||
cutoff: props.cutoff,
|
||||
offset: props.offset,
|
||||
showArrow: props.showArrow,
|
||||
fallbackOptions: props.flip ? {} : { fallbackPlacements: [] },
|
||||
},
|
||||
placement: props.placement,
|
||||
strategy: props.strategy,
|
||||
}
|
||||
})
|
||||
|
||||
const clearTimer = (timer: Ref<Nullable<NodeJS.Timeout>>) => {
|
||||
clearTimeout(timer.value)
|
||||
timer.value = null
|
||||
}
|
||||
if (!slots.trigger) {
|
||||
emits: [UPDATE_VALUE_EVENT],
|
||||
setup(props, ctx) {
|
||||
if (!ctx.slots.trigger) {
|
||||
throwError(compName, 'Trigger must be provided')
|
||||
}
|
||||
// this is a reference that we need to pass down to child component
|
||||
// to obtain the child instance
|
||||
function doDestroy(forceDestroy: boolean) {
|
||||
/* istanbul ignore if */
|
||||
if (!popperInstance.value || (visible.value && !forceDestroy)) return
|
||||
detach()
|
||||
}
|
||||
|
||||
function detach() {
|
||||
popperInstance.value.destroy()
|
||||
popperInstance.value = null
|
||||
const _trigger = trigger.value
|
||||
off(_trigger, 'mouseenter', _show)
|
||||
off(_trigger, 'mouseleave', _hide)
|
||||
off(_trigger, 'focus', handleFocus)
|
||||
off(_trigger, 'blur', handleBlur)
|
||||
trigger.value = null
|
||||
}
|
||||
|
||||
const showPopper = () => {
|
||||
if (!exceptionState.value || props.manualMode || props.disabled) return
|
||||
clearTimer(timeout)
|
||||
const handleHideAfter = () => {
|
||||
if (props.hideAfter > 0) {
|
||||
timeoutPending.value = setTimeout(() => {
|
||||
show.value = false
|
||||
}, props.hideAfter)
|
||||
}
|
||||
}
|
||||
if (props.showAfter === 0) {
|
||||
show.value = true
|
||||
handleHideAfter()
|
||||
} else {
|
||||
timeout.value = setTimeout(() => {
|
||||
show.value = true
|
||||
handleHideAfter()
|
||||
}, props.showAfter)
|
||||
}
|
||||
}
|
||||
|
||||
const debouncedClose = debounce(() => {
|
||||
if (props.enterable && exceptionState.value) return
|
||||
clearTimer(timeout)
|
||||
if (timeoutPending.value !== null) {
|
||||
clearTimer(timeoutPending)
|
||||
}
|
||||
show.value = false
|
||||
if (props.disabled) {
|
||||
doDestroy(true)
|
||||
}
|
||||
}, props.closeDelay)
|
||||
|
||||
function setExpectionState(state: boolean) {
|
||||
if (!state) {
|
||||
clearTimer(timeoutPending)
|
||||
}
|
||||
exceptionState.value = state
|
||||
}
|
||||
|
||||
function _show() {
|
||||
setExpectionState(true)
|
||||
showPopper()
|
||||
}
|
||||
|
||||
function _hide() {
|
||||
setExpectionState(false)
|
||||
debouncedClose()
|
||||
}
|
||||
|
||||
function handleBlur() {
|
||||
_hide()
|
||||
}
|
||||
|
||||
function handleFocus() {
|
||||
_show()
|
||||
}
|
||||
|
||||
function initializePopper() {
|
||||
const _trigger = getTrigger()
|
||||
|
||||
|
||||
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)
|
||||
_trigger.classList.add(props.class)
|
||||
on(_trigger, 'mouseenter', _show)
|
||||
on(_trigger, 'mouseleave', _hide)
|
||||
on(_trigger, 'focus', handleFocus)
|
||||
on(_trigger, 'blur', handleBlur)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
val => {
|
||||
if (popperInstance.value) {
|
||||
popperInstance.value.update()
|
||||
} else {
|
||||
initializePopper()
|
||||
}
|
||||
if (props.manualMode) {
|
||||
emit(UPDATE_MODEL_EVENT, val)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
watch(() => popperOptions.value, val => {
|
||||
if (!popperInstance.value) return
|
||||
popperInstance.value.setOptions({
|
||||
placement: val.placement,
|
||||
strategy: val.strategy,
|
||||
modifiers: useModifier(val.modifierOptions),
|
||||
})
|
||||
popperInstance.value.update()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initializePopper()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
doDestroy(true)
|
||||
})
|
||||
|
||||
onUpdated(() => {
|
||||
const _trigger = getTrigger()
|
||||
if (_trigger !== trigger.value && popperInstance.value) {
|
||||
detach()
|
||||
}
|
||||
if (popperInstance.value) {
|
||||
popperInstance.value.update()
|
||||
} else {
|
||||
initializePopper()
|
||||
}
|
||||
})
|
||||
const {
|
||||
arrowRef,
|
||||
clickMask,
|
||||
doDestroy,
|
||||
onShow,
|
||||
onHide,
|
||||
popperInstance,
|
||||
popperId,
|
||||
popperRef,
|
||||
initializePopper,
|
||||
visible,
|
||||
} = usePopper(props as IPopperOptions, ctx)
|
||||
|
||||
return {
|
||||
arrowRef,
|
||||
clickMask,
|
||||
popperId,
|
||||
doDestroy,
|
||||
_show,
|
||||
_hide,
|
||||
onShow,
|
||||
onHide,
|
||||
popperRef,
|
||||
popperInstance,
|
||||
initializePopper,
|
||||
visible,
|
||||
}
|
||||
},
|
||||
@ -408,8 +220,8 @@ export default defineComponent({
|
||||
id: this.popperId,
|
||||
ref: 'popperRef',
|
||||
role: 'tooltip',
|
||||
onMouseEnter: this._show,
|
||||
onMouseLeave: this._hide,
|
||||
onMouseEnter: this.onShow,
|
||||
onMouseLeave: this.onHide,
|
||||
onClick: stop,
|
||||
},
|
||||
[
|
||||
@ -434,7 +246,17 @@ export default defineComponent({
|
||||
{
|
||||
to: 'body',
|
||||
},
|
||||
popper,
|
||||
withDirectives(
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: 'el-popper__mask',
|
||||
onClick: this.clickMask,
|
||||
},
|
||||
popper,
|
||||
),
|
||||
[[ClickOutside, () => this.$emit(UPDATE_VALUE_EVENT, false)]],
|
||||
),
|
||||
)
|
||||
: popper,
|
||||
],
|
||||
@ -444,6 +266,18 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
.el-popper__mask {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
position: fixed;
|
||||
z-index: 1000000;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
visibility: hidden;
|
||||
}
|
||||
.el-popper {
|
||||
position: absolute;
|
||||
border-radius: 4px;
|
||||
@ -453,6 +287,7 @@ export default defineComponent({
|
||||
line-height: 1.2;
|
||||
min-width: 10px;
|
||||
word-wrap: break-word;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.el-popper__arrow,
|
||||
|
31
packages/popper/src/popper.d.ts
vendored
31
packages/popper/src/popper.d.ts
vendored
@ -1,10 +1,33 @@
|
||||
|
||||
import type { Placement, PositioningStrategy, Instance as PopperInstance, Options } from '@popperjs/core'
|
||||
|
||||
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'
|
||||
|
||||
export { Placement, PositioningStrategy, PopperInstance, Options }
|
||||
|
||||
export type TriggerType = 'click' | 'hover' | 'focus' | 'contextMenu'
|
||||
|
||||
export type IPopperOptions = {
|
||||
arrowOffset: number
|
||||
boundariesPadding: number
|
||||
class: string
|
||||
closeDelay: number
|
||||
cutoff: boolean
|
||||
disabled: boolean
|
||||
enterable: boolean
|
||||
flip: boolean
|
||||
hideAfter: number
|
||||
manualMode: boolean
|
||||
offset: number
|
||||
placement: Placement
|
||||
popperOptions: Options
|
||||
showAfter: number
|
||||
showArrow: boolean
|
||||
strategy: PositioningStrategy
|
||||
tabIndex: string
|
||||
value: boolean
|
||||
}
|
||||
|
243
packages/popper/src/usePopper.ts
Normal file
243
packages/popper/src/usePopper.ts
Normal file
@ -0,0 +1,243 @@
|
||||
import { computed, Fragment, getCurrentInstance, ref, onMounted, onBeforeUnmount, onUpdated, watch } from 'vue'
|
||||
import { debounce } from 'lodash'
|
||||
import { createPopper } from '@popperjs/core'
|
||||
|
||||
import { generateId } from '@element-plus/utils/util'
|
||||
import { off, addClass } from '@element-plus/utils/dom'
|
||||
import throwError from '@element-plus/utils/error'
|
||||
|
||||
import useEvents from '@element-plus/hooks/use-events'
|
||||
|
||||
import useModifier from './useModifier'
|
||||
|
||||
import type { Ref } from 'vue'
|
||||
import type { SetupContext } from '@vue/runtime-core'
|
||||
import type { IPopperOptions, RefElement, PopperInstance } from './popper'
|
||||
|
||||
export const DEFAULT_TRIGGER = ['click', 'hover', 'focus', 'contextMenu']
|
||||
export const UPDATE_VALUE_EVENT = 'updateValue'
|
||||
|
||||
const clearTimer = (timer: Ref<Nullable<NodeJS.Timeout>>) => {
|
||||
clearTimeout(timer.value)
|
||||
timer.value = null
|
||||
}
|
||||
|
||||
const getTrigger = () => {
|
||||
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('ElPopper', 'Cannot find referrer to attach popper to')
|
||||
}
|
||||
return trigger
|
||||
}
|
||||
|
||||
export default <T extends IPopperOptions>(props: T, ctx: SetupContext) => {
|
||||
const arrowRef = ref<RefElement>(null)
|
||||
const trigger = ref<RefElement>(null)
|
||||
const exceptionState = ref(false)
|
||||
const popperInstance = ref<Nullable<PopperInstance>>(null)
|
||||
const popperId = ref(`el-popper-${generateId()}`)
|
||||
const popperRef = ref<RefElement>(null)
|
||||
const show = ref(false)
|
||||
const timeout = ref<NodeJS.Timeout>(null)
|
||||
const timeoutPending = ref<NodeJS.Timeout>(null)
|
||||
|
||||
const popperOptions = computed(() => {
|
||||
return {
|
||||
modifierOptions: {
|
||||
arrowOffset: props.arrowOffset,
|
||||
arrowRef: arrowRef.value,
|
||||
boundariesPadding: props.boundariesPadding,
|
||||
cutoff: props.cutoff,
|
||||
offset: props.offset,
|
||||
showArrow: props.showArrow,
|
||||
fallbackOptions: props.flip ? {} : { fallbackPlacements: [] },
|
||||
},
|
||||
placement: props.placement,
|
||||
strategy: props.strategy,
|
||||
}
|
||||
})
|
||||
|
||||
const visible = computed(() => {
|
||||
return props.manualMode ? props.value : !props.disabled && show.value
|
||||
})
|
||||
|
||||
const showPopper = () => {
|
||||
if (!exceptionState.value || props.manualMode || props.disabled) return
|
||||
clearTimer(timeout)
|
||||
const handleHideAfter = () => {
|
||||
if (props.hideAfter > 0) {
|
||||
timeoutPending.value = setTimeout(() => {
|
||||
show.value = false
|
||||
}, props.hideAfter)
|
||||
}
|
||||
}
|
||||
if (props.showAfter === 0) {
|
||||
show.value = true
|
||||
handleHideAfter()
|
||||
} else {
|
||||
timeout.value = setTimeout(() => {
|
||||
show.value = true
|
||||
handleHideAfter()
|
||||
}, props.showAfter)
|
||||
}
|
||||
}
|
||||
|
||||
const closePopper = debounce(() => {
|
||||
if (props.enterable && exceptionState.value) return
|
||||
clearTimer(timeout)
|
||||
if (timeoutPending.value !== null) {
|
||||
clearTimer(timeoutPending)
|
||||
}
|
||||
show.value = false
|
||||
if (props.disabled) {
|
||||
doDestroy(true)
|
||||
}
|
||||
}, props.closeDelay)
|
||||
|
||||
function onShow() {
|
||||
setExpectionState(true)
|
||||
showPopper()
|
||||
}
|
||||
|
||||
function onHide() {
|
||||
setExpectionState(false)
|
||||
closePopper()
|
||||
}
|
||||
|
||||
function setExpectionState(state: boolean) {
|
||||
if (!state) {
|
||||
clearTimer(timeoutPending)
|
||||
}
|
||||
exceptionState.value = state
|
||||
}
|
||||
|
||||
function initializePopper() {
|
||||
const _trigger = getTrigger()
|
||||
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)
|
||||
addClass(_trigger, props.class)
|
||||
|
||||
const events = [
|
||||
{
|
||||
name: 'mouseenter',
|
||||
handler: onShow,
|
||||
},
|
||||
{
|
||||
name: 'mouseleave',
|
||||
handler: onHide,
|
||||
},
|
||||
{
|
||||
name: 'focus',
|
||||
handler: onShow,
|
||||
},
|
||||
{
|
||||
name: 'blur',
|
||||
handler: onHide,
|
||||
},
|
||||
]
|
||||
useEvents(trigger, events)
|
||||
}
|
||||
|
||||
function doDestroy(forceDestroy: boolean) {
|
||||
/* istanbul ignore if */
|
||||
if (!popperInstance.value || (visible.value && !forceDestroy)) return
|
||||
detachPopper()
|
||||
}
|
||||
|
||||
function clickMask() {
|
||||
if (props.manualMode) {
|
||||
ctx.emit(UPDATE_VALUE_EVENT, false)
|
||||
}
|
||||
}
|
||||
|
||||
function detachPopper() {
|
||||
popperInstance.value.destroy()
|
||||
popperInstance.value = null
|
||||
const _trigger = trigger.value
|
||||
off(_trigger, 'mouseenter', onShow)
|
||||
off(_trigger, 'mouseleave', onHide)
|
||||
off(_trigger, 'focus', onShow)
|
||||
off(_trigger, 'blur', onHide)
|
||||
trigger.value = null
|
||||
}
|
||||
|
||||
watch(popperOptions, val => {
|
||||
if (!popperInstance.value) return
|
||||
popperInstance.value.setOptions({
|
||||
placement: val.placement,
|
||||
strategy: val.strategy,
|
||||
modifiers: useModifier(val.modifierOptions),
|
||||
})
|
||||
popperInstance.value.update()
|
||||
})
|
||||
|
||||
watch(visible,
|
||||
() => {
|
||||
if (popperInstance.value) {
|
||||
popperInstance.value.update()
|
||||
} else {
|
||||
initializePopper()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
initializePopper()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
doDestroy(true)
|
||||
})
|
||||
|
||||
onUpdated(() => {
|
||||
const _trigger = getTrigger()
|
||||
if (_trigger !== trigger.value && popperInstance.value) {
|
||||
detachPopper()
|
||||
}
|
||||
if (popperInstance.value) {
|
||||
popperInstance.value.update()
|
||||
} else {
|
||||
initializePopper()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
clickMask,
|
||||
doDestroy,
|
||||
onShow,
|
||||
onHide,
|
||||
initializePopper,
|
||||
arrowRef,
|
||||
popperId,
|
||||
popperInstance,
|
||||
popperRef,
|
||||
visible,
|
||||
}
|
||||
}
|
@ -1,15 +1,42 @@
|
||||
import { h, nextTick } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Tooltip from '../src/index.vue'
|
||||
|
||||
import type { VNode } from 'vue'
|
||||
|
||||
const AXIOM = 'Rem is the best girl'
|
||||
|
||||
const _mount = (props: any = {}, content: string | VNode = '') => mount(Tooltip, {
|
||||
slots: {
|
||||
default: () => h('div', AXIOM),
|
||||
content: () => content,
|
||||
},
|
||||
props,
|
||||
})
|
||||
|
||||
const selector = '.el-popper'
|
||||
describe('Tooltip.vue', () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
test('render test', () => {
|
||||
const wrapper = mount(Tooltip, {
|
||||
slots: {
|
||||
default: AXIOM,
|
||||
},
|
||||
const wrapper = _mount(undefined, AXIOM)
|
||||
expect(wrapper.html()).toContain(AXIOM)
|
||||
})
|
||||
|
||||
test('manual mode', async () => {
|
||||
const wrapper = _mount({
|
||||
manual: true,
|
||||
modelValue: false,
|
||||
}, AXIOM)
|
||||
// since VTU does not provide any functionality for testing teleporting components
|
||||
expect(document.querySelector(selector).getAttribute('style')).toContain('display: none')
|
||||
|
||||
await wrapper.setProps({
|
||||
modelValue: true,
|
||||
})
|
||||
expect(wrapper.text()).toEqual(AXIOM)
|
||||
|
||||
await nextTick()
|
||||
expect(document.querySelector(selector).getAttribute('style')).not.toContain('display: none')
|
||||
})
|
||||
})
|
||||
|
@ -2,10 +2,12 @@
|
||||
<div class="box">
|
||||
<div class="top">
|
||||
<el-tooltip
|
||||
v-model="model"
|
||||
class="item"
|
||||
effect="dark"
|
||||
content="Top Left prompts info"
|
||||
placement="top-start"
|
||||
:manual="true"
|
||||
>
|
||||
<el-button>top-start</el-button>
|
||||
</el-tooltip>
|
||||
@ -107,6 +109,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import ClickOutside from '@element-plus/directives/click-outside'
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
ClickOutside,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
model: true,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.box {
|
||||
width: 400px;
|
||||
|
@ -1,7 +1,8 @@
|
||||
<script lang='ts'>
|
||||
import { computed, defineComponent, h, watch } from 'vue'
|
||||
import { defineComponent, h } from 'vue'
|
||||
import { Popper as ElPopper } from '@element-plus/popper'
|
||||
import { UPDATE_MODEL_EVENT } from '@element-plus/utils/constants'
|
||||
import throwError from '@element-plus/utils/error'
|
||||
|
||||
import type { PropType } from 'vue'
|
||||
import type {
|
||||
@ -86,11 +87,6 @@ export default defineComponent({
|
||||
type: String,
|
||||
default: 'el-fade-in-linear',
|
||||
},
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
validator: val => typeof val === 'boolean',
|
||||
},
|
||||
visibleArrow: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
@ -99,21 +95,18 @@ export default defineComponent({
|
||||
emits: [UPDATE_MODEL_EVENT],
|
||||
setup(props, ctx) {
|
||||
// init here
|
||||
const shouldShow = computed(() => {
|
||||
return props.manual
|
||||
? props.modelValue !== undefined
|
||||
? props.modelValue
|
||||
: props.value
|
||||
: null
|
||||
})
|
||||
|
||||
watch(shouldShow, (val, preVal) => {
|
||||
if (val !== preVal) {
|
||||
ctx.emit(UPDATE_MODEL_EVENT, val)
|
||||
}
|
||||
})
|
||||
// when manual mode is true, v-model must be passed down
|
||||
if (props.manual && typeof props.modelValue === 'undefined') {
|
||||
throwError('[ElTooltip]', 'You need to pass a v-model to el-tooltip when `manual` is true')
|
||||
}
|
||||
|
||||
const onUpdateValue = val => {
|
||||
ctx.emit(UPDATE_MODEL_EVENT, val)
|
||||
}
|
||||
|
||||
return {
|
||||
shouldShow,
|
||||
onUpdateValue,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
@ -127,10 +120,10 @@ export default defineComponent({
|
||||
manual,
|
||||
offset,
|
||||
openDelay,
|
||||
onUpdateValue,
|
||||
placement,
|
||||
popperOptions,
|
||||
showAfter,
|
||||
shouldShow,
|
||||
transition,
|
||||
tabindex,
|
||||
visibleArrow,
|
||||
@ -144,7 +137,6 @@ export default defineComponent({
|
||||
enterable,
|
||||
hideAfter,
|
||||
manualMode: manual,
|
||||
modelValue: shouldShow,
|
||||
offset,
|
||||
placement,
|
||||
showAfter: openDelay || showAfter, // this is for mapping API due to we decided to rename the current openDelay API to showAfter for better readability,
|
||||
@ -152,6 +144,8 @@ export default defineComponent({
|
||||
transition,
|
||||
tabIndex: String(tabindex),
|
||||
popperOptions, // Breakings!: Once popperOptions is provided, the whole popper is under user's control, ElPopper nolonger generates the default options for popper, this is by design if the user wants the full contorl on @PopperJS, read the doc @https://popper.js.org/docs/v2/
|
||||
value: this.modelValue,
|
||||
onUpdateValue,
|
||||
},
|
||||
{
|
||||
default: () => ($slots.content ? $slots.content() : content),
|
||||
|
@ -1,20 +1,10 @@
|
||||
import isServer from './isServer'
|
||||
|
||||
const SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g
|
||||
const MOZ_HACK_REGEXP = /^moz([A-Z])/
|
||||
import { camelize } from './util'
|
||||
|
||||
/* istanbul ignore next */
|
||||
const trim = function(s: string) {
|
||||
return (s || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, '')
|
||||
}
|
||||
/* istanbul ignore next */
|
||||
const camelCase = function(name: string) {
|
||||
return name
|
||||
.replace(SPECIAL_CHARS_REGEXP, function(_, __, letter, offset) {
|
||||
return offset ? letter.toUpperCase() : letter
|
||||
})
|
||||
.replace(MOZ_HACK_REGEXP, 'Moz$1')
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export const on = function(
|
||||
@ -110,7 +100,7 @@ export function removeClass(el: HTMLElement, cls: string): void {
|
||||
/* istanbul ignore next */
|
||||
// Here I want to use the type CSSStyleDeclaration, but the definition for CSSStyleDeclaration
|
||||
// has { [index: number]: string } in its type annotation, which does not satisfy the method
|
||||
// camelCase(s: string)
|
||||
// camelize(s: string)
|
||||
// Same as the return type
|
||||
export const getStyle = function(
|
||||
element: HTMLElement,
|
||||
@ -118,7 +108,7 @@ export const getStyle = function(
|
||||
): string {
|
||||
if (isServer) return
|
||||
if (!element || !styleName) return null
|
||||
styleName = camelCase(styleName)
|
||||
styleName = camelize(styleName)
|
||||
if (styleName === 'float') {
|
||||
styleName = 'cssFloat'
|
||||
}
|
||||
@ -145,7 +135,7 @@ export function setStyle(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
styleName = camelCase(styleName)
|
||||
styleName = camelize(styleName)
|
||||
|
||||
element.style[styleName] = value
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import isServer from './isServer'
|
||||
import { isObject, capitalize, hyphenate, looseEqual, extend } from '@vue/shared'
|
||||
import { isObject, capitalize, hyphenate, looseEqual, extend, camelize } from '@vue/shared'
|
||||
import { isEmpty, castArray, isEqual } from 'lodash'
|
||||
|
||||
import type { AnyFunction } from './types'
|
||||
@ -108,6 +108,7 @@ export {
|
||||
isEqual,
|
||||
isObject,
|
||||
capitalize,
|
||||
camelize,
|
||||
looseEqual,
|
||||
extend,
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user