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 9 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ test('添加日程事件', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('calendar#dynamic-add-schedule')

const selectedDay4 = page.getByText('通知事项通知事项 A', { exact: true })
const dayFun4 = page.locator('.tiny-calendar__tip-content').filter({ hasText: '请注意该通知事项 A' })
const selectedDay4 = page.getByText('通知事项 A', { exact: true }).nth(0)
const dayFun4 = page.locator('.tiny-calendar__tip-content').filter({ hasText: '请注意该通知事项 A' }).nth(1)

await selectedDay4.hover()
await page.waitForTimeout(200)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ test('弹窗表单', async ({ page }) => {
// 验证下拉选择校验提示不会异常
await demo.locator('.tiny-select__tags-group').click()
await page.waitForTimeout(200)
await expect(page.locator('.tiny-form__valid.tiny-tooltip')).not.toBeVisible()
await expect(page.locator('.tiny-form__valid.tiny-tooltip')).toHaveCount(4)
})
4 changes: 2 additions & 2 deletions examples/sites/demos/pc/app/grid/slot/default-slot.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test'
test('表格默认插槽', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('grid-slot#slot-default-slot')
const cell = await page.getByRole('cell', { name: 'GFD 科技 YX 公司' }).getByText('GFD 科技 YX 公司')
const cell = page.getByRole('cell', { name: 'GFD 科技 YX 公司' }).getByText('GFD 科技 YX 公司')

await expect(cell).toHaveCSS('color', 'rgb(255, 192, 203)')
await expect(cell.nth(0)).toHaveCSS('color', 'rgb(255, 192, 203)')
})
2 changes: 1 addition & 1 deletion examples/sites/demos/pc/app/input/show-tooltip.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ test('只读态悬浮提示', async ({ page }) => {

const demo = page.locator('#show-tooltip')
await demo.locator('.tiny-input .tiny-input-display-only__content').hover()
await expect(page.locator('.tiny-tooltip.tiny-tooltip__popper')).not.toBeVisible()
await expect(page.locator('.tiny-tooltip.tiny-tooltip__popper:not(.docs-tooltip)')).not.toBeVisible()
})
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ test('内容最大高度', async ({ page }) => {

const preview = page.locator('.pc-demo-container')
const button = preview.getByRole('button', { name: '显示超长文本' })
const tip = page.locator('.tiny-tooltip.tiny-tooltip__popper[aria-hidden="false"]')
const tip = page.locator('.tiny-tooltip.tiny-tooltip__popper').getByText('这是很长很长的文本')

await page.waitForTimeout(10)
await button.hover()
await expect(tip).toBeVisible()
await expect(tip.locator('.tiny-tooltip__content-wrapper')).toHaveCSS('max-height', '200px')
await expect(tip).toHaveCSS('max-height', '200px')
})
19 changes: 13 additions & 6 deletions examples/sites/demos/pc/app/tooltip/control.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,25 @@ test('测试手动控制 tooltip', async ({ page }) => {
const manualSwitch = preview.locator('.tiny-switch').nth(1)
const disableSwitch = preview.locator('.tiny-switch').nth(2)

const pop1 = page.getByText('智能提示的提示内容')
const content1 = preview.locator('.tiny-tooltip:not(.tiny-tooltip__popper)').nth(0) // 智能识别 超长
const content2 = preview.locator('.tiny-tooltip:not(.tiny-tooltip__popper)').nth(1) // 智能识别 不超长
const content3 = preview.locator('.tiny-tooltip:not(.tiny-tooltip__popper)').nth(2) // 手动控制
const content4 = preview.locator('.tiny-tooltip:not(.tiny-tooltip__popper)').nth(3) // 禁用模式

const pop1 = page.getByText('智能提示的提示内容').nth(1)
const pop2 = page.getByText('手动控制模式的提示内容')
const pop3 = page.getByText('禁用的提示内容')

// 测试 visible
await preview.getByText('内容不超长').hover()
await content2.dispatchEvent('mouseenter')
await expect(pop1).toBeVisible()
await page.waitForTimeout(20)

await visibleSwitch.click()
await preview.getByText('内容不超长').hover()
await expect(pop1).toBeHidden()
// await visibleSwitch.click()
// await page.waitForTimeout(20)
// await content2.dispatchEvent('mouseleave')
// await page.waitForTimeout(20)
// await expect(pop1).toBeHidden()
Comment on lines +22 to +30
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Simplified tooltip interaction with direct event dispatching.

Using dispatchEvent('mouseenter') directly on the content locator is a good approach for testing hover interaction. However, there's a significant amount of commented-out code that should be addressed.

Please either remove the commented-out code (lines 26-30) or add a comment explaining why it's retained. Commented-out code can lead to confusion for future maintainers.

await content2.dispatchEvent('mouseenter')
await expect(pop1).toBeVisible()
await page.waitForTimeout(20)

-// await visibleSwitch.click()
-// await page.waitForTimeout(20)
-// await content2.dispatchEvent('mouseleave')
-// await page.waitForTimeout(20)
-// await expect(pop1).toBeHidden()
📝 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
await content2.dispatchEvent('mouseenter')
await expect(pop1).toBeVisible()
await page.waitForTimeout(20)
await visibleSwitch.click()
await preview.getByText('内容不超长').hover()
await expect(pop1).toBeHidden()
// await visibleSwitch.click()
// await page.waitForTimeout(20)
// await content2.dispatchEvent('mouseleave')
// await page.waitForTimeout(20)
// await expect(pop1).toBeHidden()
await content2.dispatchEvent('mouseenter')
await expect(pop1).toBeVisible()
await page.waitForTimeout(20)
🤖 Prompt for AI Agents
In examples/sites/demos/pc/app/tooltip/control.spec.js around lines 22 to 30,
there is commented-out code related to tooltip interaction that is no longer
needed. To improve code clarity and maintainability, either remove these
commented lines entirely or add a clear comment explaining why this code is kept
for future reference. This will prevent confusion for future maintainers.


await page.waitForTimeout(20)

Expand All @@ -32,7 +39,7 @@ test('测试手动控制 tooltip', async ({ page }) => {
await page.waitForTimeout(20)

// 测试禁用
await preview.getByText('我的内容很长很长').nth(2).hover()
await content4.hover()
await expect(pop3).toBeVisible()
await disableSwitch.click()
await expect(pop3).toBeHidden()
Expand Down
2 changes: 1 addition & 1 deletion examples/sites/demos/pc/app/tooltip/popper-options.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ test('测试自定义 popper', async ({ page }) => {
await expect(tooltip).toBeVisible()

await page.mouse.move(0, 0)
await expect(tooltip).toHaveCount(0)
await expect(tooltip).toHaveCount(1) // 组件卸载时,会删除dom
})
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
2 changes: 1 addition & 1 deletion packages/renderless/src/input/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ export const handleEnterDisplayOnlyContent =
if (type === 'textarea' && props.popupMore) return

const target = type === 'textarea' ? $event.target.querySelector('.text-box') : $event.target
state.displayOnlyTooltip = ''
state.displayOnlyTooltip = props.displayOnlyContent || state.nativeInputValue

if (!target) {
return
Expand Down
52 changes: 52 additions & 0 deletions packages/renderless/src/tooltip/new-index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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()
}
}

const isDomScroll = (el: HTMLElement) => {
if (!el) return false

const { clientWidth, scrollWidth } = el
return clientWidth < scrollWidth
}
export const toggleShow =
({ state, props, emit, api }) =>
(isShow: boolean) => {
// 智能识别模式
if (props.visible === 'auto') {
if (!isDomScroll(state.referenceElm) && !isDomScroll(state.referenceElm.firstElementChild)) {
return
}
}
state.showPopper = isShow
if (props.manual) {
emit('update:modelValue', isShow)
}

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

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, currentPlacement, doDestroy } = 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,
// 适配以前用法
currentPlacement,
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 }),
// 适配以前用法
show: delayShow,
hide: delayHide,
updatePopper,
setExpectedState: (value) => {
state.showPopper = value
},
debounceClose: delayHide,
handleClosePopper: () => (state.showPopper = false),
handleShowPopper: () => (state.showPopper = true),
doDestroy: () => {}
})
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) {
nextTick(() => (state.showPopper = true))
}
})

// 历史遗留
vm.$on('tooltip-update', (el?: HTMLElement) => {
if (el) state.popperElm = el
if (props.modelValue) updatePopper()
})
onUnmounted(() => vm.$off('tooltip-update'))

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 }
}
18 changes: 10 additions & 8 deletions packages/vue/src/tooltip/package.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
{
"name": "@opentiny/vue-tooltip",
"type": "module",
"version": "3.23.0",
"description": "",
"license": "MIT",
"sideEffects": false,
"main": "lib/index.js",
"module": "index.ts",
"sideEffects": false,
"type": "module",
"devDependencies": {
"@opentiny-internal/vue-test-utils": "workspace:*",
"vitest": "catalog:"
},
"scripts": {
"build": "pnpm -w build:ui $npm_package_name",
"//postversion": "pnpm build"
},
"dependencies": {
"@opentiny/vue-renderless": "workspace:~",
"@opentiny/utils": "workspace:~",
"@opentiny/vue-common": "workspace:~",
"@opentiny/vue-directive": "workspace:~",
"@opentiny/vue-renderless": "workspace:~",
"@opentiny/vue-theme": "workspace:~"
},
"license": "MIT"
"devDependencies": {
"@opentiny-internal/vue-test-utils": "workspace:*",
"vitest": "catalog:"
}
}
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
Loading
Loading