Skip to content

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

Open
wants to merge 5 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/sites/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import './assets/index.less'
import './style.css'

// 覆盖默认的github markdown样式
import './assets/custom-markdown.css'
import './assets/custom-markdown.less'
import './assets/custom-block.less'
import './assets/md-preview.less'

Expand Down
48 changes: 48 additions & 0 deletions packages/renderless/src/tooltip/new-index.ts
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

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.

if (scrollWidth <= clientWidth) {
return
}
}

state.showPopper = isShow
if (props.manual) {
emit('update:modelValue', isShow)
}

// 自动隐藏: 如果显示,且要自动隐藏,则延时后关闭
if (!props.manual && props.hideAfter && isShow) {
api.delayHideAfter()
}
}
75 changes: 75 additions & 0 deletions packages/renderless/src/tooltip/new-vue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

toggleShow omitted from the public API list

tooltip.vue relies on the list in export const api to expose methods returned by
setup(...). toggleShow is created on line 52 but not exported in the array
declared on line 5, making it inaccessible to downstream consumers.

-export const api = ['state', 'handlePopEvent', 'handleRefEvent']
+export const api = [
+  'state',
+  'handlePopEvent',
+  'handleRefEvent',
+  'toggleShow'
+]

Also applies to: 43-53

🤖 Prompt for AI Agents
In packages/renderless/src/tooltip/new-vue.ts around lines 5 to 6 and 43 to 53,
the method toggleShow is defined but not included in the exported api array,
which limits its accessibility to downstream consumers. To fix this, add
'toggleShow' to the api array declared on line 5 so it is properly exposed as
part of the public API.

export const renderless = (
props,
{ ref, 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 } = userPopper({
emit,
props,
nextTick,
toRefs,
reactive,
parent: parent.$parent,
vm,
slots,
onBeforeUnmount,
onDeactivated,
watch,
popperVmRef
} as any)

showPopper.value = false // 初始为false
const state = reactive({
showPopper,
popperElm,
referenceElm,
tooltipId: guid('tiny-tooltip-', 4),
showContent: inject('showContent', null),
tipsMaxWidth: inject('tips-max-width', null)
})

const useTimerFn = useTimer({ onUnmounted, ref })
const toggleShowFn = toggleShow({ state, props, emit, api })
const { start: delayShow, clear: cancelDelayShow } = useTimerFn(() => toggleShowFn(true), toRef(props, 'openDelay'))
const { start: delayHide, clear: cancelDelayHide } = useTimerFn(() => toggleShowFn(false), toRef(props, 'closeDelay'))
const { start: delayHideAfter } = useTimerFn(() => toggleShowFn(false), toRef(props, 'hideAfter'))

Object.assign(api, {
state,
delayShow,
cancelDelayShow,
delayHide,
cancelDelayHide,
delayHideAfter,
handlePopEvent: handlePopEvent({ props, api }),
handleRefEvent: handleRefEvent({ props, api })
})
Comment on lines +45 to +54
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider including toggleShow in the API object

While toggleShowFn is used internally to handle visibility, it's not exposed in the API object, limiting component flexibility.

Object.assign(api, {
  state,
  delayShow,
  cancelDelayShow,
  delayHide,
  cancelDelayHide,
  delayHideAfter,
+ toggleShow: toggleShowFn,
  handlePopEvent: handlePopEvent({ props, api }),
  handleRefEvent: handleRefEvent({ props, api })
})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Object.assign(api, {
state,
delayShow,
cancelDelayShow,
delayHide,
cancelDelayHide,
delayHideAfter,
handlePopEvent: handlePopEvent({ props, api }),
handleRefEvent: handleRefEvent({ props, api })
})
Object.assign(api, {
state,
delayShow,
cancelDelayShow,
delayHide,
cancelDelayHide,
delayHideAfter,
toggleShow: toggleShowFn,
handlePopEvent: handlePopEvent({ props, api }),
handleRefEvent: handleRefEvent({ props, api })
})
🤖 Prompt for AI Agents
In packages/renderless/src/tooltip/new-vue.ts around lines 45 to 54, the
toggleShowFn function is used internally but not included in the exported API
object, which limits external control over tooltip visibility. To fix this, add
toggleShowFn to the Object.assign call that defines the api object properties,
exposing it as toggleShow or a similarly named property to allow components
using this API to toggle visibility directly.

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())

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure updatePopper() is a function and is correctly invoked to avoid potential runtime errors.


return api
}
1 change: 1 addition & 0 deletions packages/vue-hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ export { useUserAgent } from './src/useUserAgent'
export { useWindowSize } from './src/useWindowSize'
export { userPopper } from './src/vue-popper'
export { usePopup } from './src/vue-popup'
export { useTimer } from './src/useTimer'
36 changes: 36 additions & 0 deletions packages/vue-hooks/src/useTimer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/** 延时触发的通用定时器。 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 const useTimer =
({ onUnmounted, ref }) =>
(cb: (...args: any[]) => void, delay: any) => {
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 }
}
8 changes: 7 additions & 1 deletion packages/vue/src/tooltip/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { $props, $prefix, $setup, defineComponent } from '@opentiny/vue-common'
import type { ITooltipApi } from '@opentiny/vue-renderless/types/tooltip.type'
import TooltipPc from './tooltip.vue'
import TooltipMf from './mobile-first.vue'

import template from 'virtual-template?pc|mobile-first'
// 切换下面,以发布不同的tooltip组件

// import template // from 'virtual-template?pc|mobile-first'

const template = (mode) => (mode === 'mobile-first' ? TooltipMf : TooltipPc) // 新PC, 老 MF

export const tooltipProps = {
...$props,
Expand Down
155 changes: 155 additions & 0 deletions packages/vue/src/tooltip/src/tooltip.vue
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')"
>
<slot></slot>
<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>
</div>
</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))
Copy link
Member

Choose a reason for hiding this comment

The 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>
Loading