feat(components): [tooltip-v2] trigger implementation (#6844)

- Implement TooltipV2Trigger
- Implement TooltipV2Root
- Fix a potential mem leak issue in OnlyChild
- Add token for tooltip v2
This commit is contained in:
JeremyWuuuuu 2022-03-26 18:53:56 +08:00 committed by GitHub
parent f8c6a9ba62
commit 7b166ed7fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 235 additions and 4 deletions

View File

@ -25,7 +25,13 @@ export default defineComponent({
// vue fragments is represented as a text element.
// The first element sibling should be the first element children of fragment.
// This is how we get the element.
props.setRef((el as HTMLElement).nextElementSibling as HTMLElement | null)
if (el) {
props.setRef(
(el as HTMLElement).nextElementSibling as HTMLElement | null
)
} else {
props.setRef(null)
}
})
return () => {
const [firstChild] = slots.default?.() || []

View File

@ -0,0 +1,25 @@
import { buildProps, definePropType } from '@element-plus/utils'
import type { ExtractPropTypes } from 'vue'
type StateUpdater = (state: boolean) => void
export const tooltipV2RootProps = buildProps({
delayDuration: {
type: Number,
default: 300,
},
defaultOpen: Boolean,
open: {
type: Boolean,
default: undefined,
},
onOpenChange: {
type: definePropType<StateUpdater>(Function),
},
'onUpdate:open': {
type: definePropType<StateUpdater>(Function),
},
} as const)
export type TooltipV2RootProps = ExtractPropTypes<typeof tooltipV2RootProps>

View File

@ -0,0 +1,104 @@
<template>
<slot />
</template>
<script setup lang="ts">
import {
computed,
onBeforeUnmount,
onMounted,
provide,
ref,
unref,
watch,
} from 'vue'
import { useTimeoutFn } from '@vueuse/core'
import { useId } from '@element-plus/hooks'
import { isNumber, isPropAbsent } from '@element-plus/utils'
import { TOOLTIP_V2_OPEN, tooltipV2RootKey } from '@element-plus/tokens'
import { tooltipV2RootProps } from './root'
const props = defineProps(tooltipV2RootProps)
const _open = ref(props.defaultOpen)
const open = computed<boolean>({
get: () => (isPropAbsent(props.open) ? _open.value : props.open),
set: (open) => {
_open.value = open
props['onUpdate:open']?.(open)
},
})
const isOpenDelayed = computed(
() => isNumber(props.delayDuration) && props.delayDuration > 0
)
const { start: onDelayedOpen, stop: clearTimer } = useTimeoutFn(
() => {
open.value = true
},
computed(() => props.delayDuration)
)
const contentId = useId()
const onNormalOpen = () => {
clearTimer()
open.value = true
}
const onDelayOpen = () => {
unref(isOpenDelayed) ? onDelayedOpen() : onNormalOpen()
}
const onOpen = onNormalOpen
const onClose = () => {
clearTimer()
open.value = false
}
const onChange = (open: boolean) => {
//
if (open) {
document.dispatchEvent(new CustomEvent(TOOLTIP_V2_OPEN))
onOpen()
}
props.onOpenChange?.(open)
}
watch(open, onChange)
onMounted(() => {
// Keeps only 1 tooltip open at a time
document.addEventListener(TOOLTIP_V2_OPEN, onClose)
})
onBeforeUnmount(() => {
clearTimer()
document.removeEventListener(TOOLTIP_V2_OPEN, onClose)
})
provide(tooltipV2RootKey, {
contentId,
onClose,
onDelayOpen,
onOpen,
})
defineExpose({
/**
* @description open tooltip programmatically
*/
onOpen,
/**
* @description close tooltip programmatically
*/
onClose,
})
</script>

View File

@ -1,9 +1,19 @@
import { buildProps } from '@element-plus/utils'
import { buildProps, definePropType } from '@element-plus/utils'
import type { ExtractPropTypes } from 'vue'
const EventHandler = {
type: definePropType<(e: Event) => boolean | void>(Function),
} as const
export const tooltipTriggerV2Props = buildProps({
asChild: Boolean,
onBlur: EventHandler,
onClick: EventHandler,
onFocus: EventHandler,
onMouseDown: EventHandler,
onMouseEnter: EventHandler,
onMouseLeave: EventHandler,
} as const)
export type TooltipTriggerV2Props = ExtractPropTypes<

View File

@ -8,13 +8,80 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { inject, onBeforeUnmount, ref, watch } from 'vue'
import { composeEventHandlers } from '@element-plus/utils'
import { tooltipV2RootKey } from '@element-plus/tokens'
import OnlyChild from './only-child'
import { tooltipTriggerV2Props } from './trigger'
defineProps(tooltipTriggerV2Props)
const props = defineProps(tooltipTriggerV2Props)
/**
* onOpen opens the tooltip instantly, onTrigger acts a lil bit differently,
* it will check if delayDuration is set to greater than 0 and based on that result,
* if true, it opens the tooltip after delayDuration, otherwise it opens it instantly.
*/
const { onClose, onOpen, onDelayOpen } = inject(tooltipV2RootKey)!
let isMousedown = false
const triggerRef = ref<HTMLElement | null>(null)
const setTriggerRef = (el: HTMLElement | null) => {
triggerRef.value = el
}
const onMouseenter = composeEventHandlers(props.onMouseEnter, onDelayOpen)
const onMouseleave = composeEventHandlers(props.onMouseLeave, onClose)
const onMouseup = () => {
isMousedown = false
}
const onMousedown = composeEventHandlers(props.onMouseDown, () => {
onClose()
isMousedown = true
document.addEventListener('mouseup', onMouseup, { once: true })
})
const onFocus = composeEventHandlers(props.onFocus, () => {
if (!isMousedown) onOpen()
})
const onBlur = composeEventHandlers(props.onBlur, onClose)
const onClick = composeEventHandlers(props.onClick, (e) => {
if ((e as MouseEvent).detail === 0) onClose()
})
const events = {
blur: onBlur,
click: onClick,
focus: onFocus,
mousedown: onMousedown,
mouseenter: onMouseenter,
mouseleave: onMouseleave,
}
const setEvents = <T extends (e: Event) => void>(
el: HTMLElement | null | undefined,
events: Record<string, T>,
type: 'addEventListener' | 'removeEventListener'
) => {
if (el) {
Object.entries(events).forEach(([name, handler]) => {
el[type](name, handler)
})
}
}
watch(triggerRef, (triggerEl, previousTriggerEl) => {
setEvents(triggerEl, events, 'addEventListener')
setEvents(previousTriggerEl, events, 'removeEventListener')
})
onBeforeUnmount(() => {
setEvents(triggerRef.value, events, 'removeEventListener')
document.removeEventListener('mouseup', onMouseup)
})
</script>

View File

@ -13,3 +13,4 @@ export * from './tabs'
export * from './upload'
export * from './experimental-features'
export * from './popper'
export * from './tooltip-v2'

View File

@ -0,0 +1,13 @@
import type { InjectionKey, Ref } from 'vue'
export type TooltipV2Context = {
onClose: () => void
onDelayOpen: () => void
onOpen: () => void
contentId: Ref<string>
}
export const tooltipV2RootKey: InjectionKey<TooltipV2Context> =
Symbol('tooltipV2')
export const TOOLTIP_V2_OPEN = 'tooltip_v2.open'

View File

@ -1,4 +1,5 @@
import { isArray, isObject } from '@vue/shared'
import { isNil } from 'lodash-unified'
export {
isArray,
@ -23,3 +24,7 @@ export const isElement = (e: unknown): e is Element => {
if (typeof Element === 'undefined') return false
return e instanceof Element
}
export const isPropAbsent = (prop: unknown): prop is null | undefined => {
return isNil(prop)
}