mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-11-30 02:08:12 +08:00
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:
parent
f8c6a9ba62
commit
7b166ed7fd
@ -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?.() || []
|
||||
|
@ -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>
|
@ -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>
|
@ -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<
|
||||
|
@ -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>
|
||||
|
@ -13,3 +13,4 @@ export * from './tabs'
|
||||
export * from './upload'
|
||||
export * from './experimental-features'
|
||||
export * from './popper'
|
||||
export * from './tooltip-v2'
|
||||
|
13
packages/tokens/tooltip-v2.ts
Normal file
13
packages/tokens/tooltip-v2.ts
Normal 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'
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user