-
Notifications
You must be signed in to change notification settings - Fork 302
feat(tooltip): use the template syntax and rewrite the tooltip component. #3449
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
export const handleRefEvent = | ||
({ props, api }) => | ||
(type: string) => { | ||
if (props.manual) return | ||
|
||
if (type === 'mouseenter') { | ||
api.cancelDelayHide() | ||
api.delayShow() | ||
} else if (type === 'mouseleave') { | ||
api.cancelDelayShow() | ||
api.delayHide() | ||
} | ||
} | ||
|
||
export const handlePopEvent = | ||
({ props, api }) => | ||
(type: string) => { | ||
if (props.manual) return | ||
if (!props.enterable) return | ||
|
||
if (type === 'mouseenter') { | ||
api.cancelDelayHide() | ||
} else if (type === 'mouseleave') { | ||
api.delayHide() | ||
} | ||
} | ||
|
||
export const toggleShow = | ||
({ state, props, emit, api }) => | ||
(isShow: boolean) => { | ||
// 智能识别模式 | ||
if (props.visible === 'auto' && state.referenceElm) { | ||
const { clientWidth, scrollWidth } = state.referenceElm.firstElementChild | ||
if (scrollWidth <= clientWidth) { | ||
return | ||
} | ||
} | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
state.showPopper = isShow | ||
if (props.manual) { | ||
emit('update:modelValue', isShow) | ||
} | ||
|
||
// 自动隐藏: 如果显示,且要自动隐藏,则延时后关闭 | ||
if (!props.manual && props.hideAfter && isShow) { | ||
api.delayHideAfter() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import { handlePopEvent, handleRefEvent, toggleShow } from './new-index' | ||
import { userPopper, useTimer } from '@opentiny/vue-hooks' | ||
import { guid } from '@opentiny/utils' | ||
|
||
export const api = ['state', 'handlePopEvent', 'handleRefEvent'] | ||
|
||
Comment on lines
+5
to
+6
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion
-export const api = ['state', 'handlePopEvent', 'handleRefEvent']
+export const api = [
+ 'state',
+ 'handlePopEvent',
+ 'handleRefEvent',
+ 'toggleShow'
+] Also applies to: 43-53 🤖 Prompt for AI Agents
|
||
export const renderless = ( | ||
props, | ||
{ watch, toRefs, toRef, reactive, onBeforeUnmount, onDeactivated, onMounted, onUnmounted, inject }, | ||
{ vm, emit, slots, nextTick, parent } | ||
) => { | ||
const api = {} as any | ||
const popperVmRef = {} | ||
const { showPopper, updatePopper, popperElm, referenceElm, doDestroy, popperJS } = userPopper({ | ||
emit, | ||
props, | ||
nextTick, | ||
toRefs, | ||
reactive, | ||
parent: parent.$parent, | ||
vm, | ||
slots, | ||
onBeforeUnmount, | ||
onDeactivated, | ||
watch, | ||
popperVmRef | ||
} as any) | ||
|
||
const state = reactive({ | ||
showPopper, | ||
popperElm, | ||
referenceElm, | ||
tooltipId: guid('tiny-tooltip-', 4), | ||
showContent: inject('showContent', null), | ||
tipsMaxWidth: inject('tips-max-width', null) | ||
}) | ||
state.showPopper = false // 初始为false | ||
|
||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const { start: delayShow, clear: cancelDelayShow } = useTimer(() => api.toggleShow(true), toRef(props, 'openDelay')) | ||
const { start: delayHide, clear: cancelDelayHide } = useTimer(() => api.toggleShow(false), toRef(props, 'closeDelay')) | ||
const { start: delayHideAfter } = useTimer(() => api.toggleShow(false), toRef(props, 'hideAfter')) | ||
|
||
Object.assign(api, { | ||
state, | ||
delayShow, | ||
cancelDelayShow, | ||
delayHide, | ||
cancelDelayHide, | ||
delayHideAfter, | ||
handlePopEvent: handlePopEvent({ props, api }), | ||
handleRefEvent: handleRefEvent({ props, api }), | ||
toggleShow: toggleShow({ state, props, emit, api }) | ||
}) | ||
watch( | ||
() => props.modelValue, | ||
(val) => { | ||
if (props.manual) { | ||
val ? delayShow() : delayHide() | ||
} | ||
} | ||
) | ||
onMounted(() => { | ||
state.popperElm = vm.$refs.popperRef | ||
state.referenceElm = vm.$refs.referenceRef | ||
// 初始显示 | ||
if (props.manual && props.modelValue) { | ||
state.showPopper = true | ||
} | ||
}) | ||
|
||
vm.$on('tooltip-update', updatePopper()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
onUnmounted(() => {}) | ||
|
||
return api | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { onUnmounted, ref, type MaybeRef } from 'vue' | ||
|
||
/** 延时触发的通用定时器。 setTimeout/ debounce 的地方均由该函数代替。 | ||
* 比如按钮禁用, 1秒后修改disable为false | ||
* 1、 防止连续触发 | ||
* 2、 组件卸载时,自动取消 | ||
* @example | ||
* const {start: resetDisabled } = useTimer(()=> state.disabled=false, 1000) | ||
* resetDisabled(); | ||
* | ||
* const {start: debounceQuery } = useTimer((page)=> grid.query(page), 500) | ||
* debounceQuery(1); | ||
* debounceQuery(2); // 仅请求第2页 | ||
*/ | ||
export function useTimer(cb: (...args: any[]) => void, delay: MaybeRef<number>) { | ||
let timerId = 0 | ||
const $delay = ref(delay) | ||
|
||
function start(...args: any[]) { | ||
clear() | ||
timerId = setTimeout(() => { | ||
cb(...args) | ||
timerId = 0 | ||
}, $delay.value) | ||
} | ||
function clear() { | ||
if (timerId) { | ||
clearTimeout(timerId) | ||
timerId = 0 | ||
} | ||
} | ||
|
||
onUnmounted(() => clear()) | ||
|
||
return { start, clear, delay: $delay } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
<template> | ||
<div | ||
ref="referenceRef" | ||
class="tiny-tooltip" | ||
v-bind="$attrs" | ||
style="display: inline-block" | ||
:tabindex="tabindex" | ||
:aria-describeby="state.tooltipId" | ||
@mouseenter="handleRefEvent('mouseenter')" | ||
@mouseleave="handleRefEvent('mouseleave')" | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
> | ||
<slot></slot> | ||
</div> | ||
<Transition :name="transition"> | ||
<div | ||
ref="popperRef" | ||
v-show="!disabled && state.showPopper" | ||
:id="state.tooltipId" | ||
class="tiny-tooltip tiny-tooltip__popper" | ||
:class="['is-' + (type || effect || 'dark'), popperClass, state.showContent ? 'tiny-tooltip__show-tips' : '']" | ||
:style="{ ['max-width']: state.tipsMaxWidth }" | ||
@mouseenter="handlePopEvent('mouseenter')" | ||
@mouseleave="handlePopEvent('mouseleave')" | ||
> | ||
<slot name="content"> | ||
<template v-if="renderContent"> | ||
<render-content-node :renderContent="renderContent" :content="content" /> | ||
</template> | ||
<template v-else> | ||
<div v-if="!pre" class="tiny-tooltip__content-wrapper" :style="{ ['max-height']: contentMaxHeight }"> | ||
{{ content }} | ||
</div> | ||
<pre v-else>{{ content }}</pre> | ||
</template> | ||
</slot> | ||
</div> | ||
</Transition> | ||
</template> | ||
|
||
<script lang="tsx"> | ||
import { renderless, api } from '@opentiny/vue-renderless/tooltip/new-vue' | ||
import { $prefix, setup, defineComponent, $props, h } from '@opentiny/vue-common' | ||
import '@opentiny/vue-theme/tooltip/index.less' | ||
|
||
export default defineComponent({ | ||
name: $prefix + 'Tooltip', | ||
componentName: 'Tooltip', | ||
components: { | ||
RenderContentNode: { | ||
props: ['renderContent', 'content'], | ||
render() { | ||
return this.renderContent(h, this.content) | ||
} | ||
} | ||
}, | ||
props: { | ||
...$props, | ||
visible: { | ||
type: String, | ||
default: () => 'always', | ||
validator: (value: string) => ['always', 'auto'].includes(value) | ||
}, | ||
// 原来未暴露的属性, 自动传入vue-popper | ||
adjustArrow: { | ||
type: Boolean, | ||
default: () => false | ||
}, | ||
// 自动传入vue-popper | ||
appendToBody: { | ||
type: Boolean, | ||
default: () => true | ||
}, | ||
// 原来未暴露的属性, 自动传入vue-popper | ||
arrowOffset: { | ||
type: Number, | ||
default: () => 0 | ||
}, | ||
// 原来未暴露的属性, 未入 vue-popper, 可能bug | ||
boundariesPadding: { | ||
type: Number, | ||
default: () => 5 | ||
}, | ||
closeDelay: { | ||
type: Number, | ||
default: () => 300 | ||
}, | ||
content: { type: [String, Object] }, | ||
disabled: { type: Boolean }, | ||
effect: { | ||
type: String, | ||
default: () => 'dark' | ||
}, | ||
enterable: { | ||
type: Boolean, | ||
default: () => true | ||
}, | ||
hideAfter: { | ||
type: Number, | ||
default: () => 0 | ||
}, | ||
manual: { type: Boolean }, | ||
modelValue: { type: Boolean }, | ||
// 自动传入vue-popper | ||
offset: { | ||
default: () => 0 | ||
}, | ||
openDelay: { | ||
type: Number, | ||
default: () => 0 | ||
}, | ||
placement: { | ||
type: String, | ||
default: () => 'bottom' | ||
}, | ||
popperClass: { type: String }, | ||
popperOptions: { | ||
default: () => ({ gpuAcceleration: false, boundariesPadding: 10 }) | ||
}, | ||
pre: { type: Boolean }, | ||
// 原来未暴露的属性, 不明确作用 | ||
reference: {}, | ||
popper: {}, | ||
|
||
renderContent: { type: Function }, | ||
tabindex: { | ||
type: Number, | ||
default: () => 0 | ||
}, | ||
transition: { | ||
type: String, | ||
default: () => 'tiny-fade-in-linear' | ||
}, | ||
// 优先级 > effect | ||
type: { | ||
type: String, | ||
validator: (value: string) => Boolean(~['normal', 'warning', 'error', 'info', 'success'].indexOf(value)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这里用includes代替下吧 |
||
}, | ||
visibleArrow: { | ||
type: Boolean, | ||
default: () => true | ||
}, | ||
// 原来未暴露的属性 | ||
zIndex: { | ||
type: String, | ||
default: () => 'next' | ||
}, | ||
contentMaxHeight: { | ||
type: String | ||
} | ||
}, | ||
setup(props, context) { | ||
return setup({ props, context, renderless, api }) | ||
} | ||
}) | ||
</script> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ensure
state.referenceElm.firstElementChild
is not null before accessing its properties to avoid potential runtime errors.