-
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 all 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?.firstElementChild) { | ||
const { clientWidth, scrollWidth } = state.referenceElm.firstElementChild | ||
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 |
||
if (scrollWidth <= clientWidth) { | ||
return | ||
} | ||
} | ||
|
||
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,80 @@ | ||||||||||||||||||||||||||||||||||||||||||||
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, | ||||||||||||||||||||||||||||||||||||||||||||
{ 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
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 Consider including While Object.assign(api, {
state,
delayShow,
cancelDelayShow,
delayHide,
cancelDelayHide,
delayHideAfter,
+ toggleShow: toggleShowFn,
handlePopEvent: handlePopEvent({ props, api }),
handleRefEvent: handleRefEvent({ props, api })
}) 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||
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 | ||||||||||||||||||||||||||||||||||||||||||||
} |
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 } | ||
} |
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:" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
/** | ||
* Copyright (c) 2022 - present TinyVue Authors. | ||
* Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. | ||
* | ||
* Use of this source code is governed by an MIT-style license. | ||
* | ||
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, | ||
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR | ||
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. | ||
* | ||
*/ | ||
|
||
import { on, isServer } from '@opentiny/utils' | ||
|
||
const nodeList = [] | ||
const nameSpace = '@@clickoutsideContext' | ||
let startClick | ||
let seed = 0 | ||
|
||
if (!isServer) { | ||
on(document, 'mousedown', (event) => { | ||
startClick = event | ||
nodeList | ||
.filter((node) => node[nameSpace].mousedownTrigger) | ||
.forEach((node) => node[nameSpace].documentHandler(event, startClick)) | ||
}) | ||
|
||
on(document, 'mouseup', (event) => { | ||
nodeList | ||
.filter((node) => !node[nameSpace].mousedownTrigger) | ||
.forEach((node) => node[nameSpace].documentHandler(event, node[nameSpace]?.mouseupTrigger ? event : startClick)) | ||
startClick = null | ||
}) | ||
} | ||
Comment on lines
+13
to
+34
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 Ensure document event listeners are properly cleaned up The implementation attaches global document event listeners, but there's no mechanism to remove these listeners when they're no longer needed. This could lead to memory leaks if the application repeatedly mounts and unmounts components using this directive. Consider adding a mechanism to remove document event listeners when no elements are using the directive: if (!isServer) {
+ let hasAttachedEvents = false;
+ const attachEvents = () => {
+ if (hasAttachedEvents) return;
+ hasAttachedEvents = true;
on(document, 'mousedown', (event) => {
startClick = event
nodeList
.filter((node) => node[nameSpace].mousedownTrigger)
.forEach((node) => node[nameSpace].documentHandler(event, startClick))
})
on(document, 'mouseup', (event) => {
nodeList
.filter((node) => !node[nameSpace].mousedownTrigger)
.forEach((node) => node[nameSpace].documentHandler(event, node[nameSpace]?.mouseupTrigger ? event : startClick))
startClick = null
})
+ }
+
+ const detachEvents = () => {
+ if (!hasAttachedEvents || nodeList.length > 0) return;
+ hasAttachedEvents = false;
+ document.removeEventListener('mousedown', /* stored handler reference */);
+ document.removeEventListener('mouseup', /* stored handler reference */);
+ }
+
+ attachEvents();
} Then call
🤖 Prompt for AI Agents
|
||
|
||
const createDocumentHandler = (el, binding, vnode) => | ||
function (mouseup = {}, mousedown = {}) { | ||
let popperElm = vnode.context.popperElm || (vnode.context.state && vnode.context.state.popperElm) | ||
|
||
if ( | ||
!mouseup?.target || | ||
!mousedown?.target || | ||
el.contains(mouseup.target) || | ||
el.contains(mousedown.target) || | ||
el === mouseup.target || | ||
(popperElm && (popperElm.contains(mouseup.target) || popperElm.contains(mousedown.target))) | ||
) { | ||
return | ||
} | ||
|
||
if (binding.expression && el[nameSpace].methodName && vnode.context[el[nameSpace].methodName]) { | ||
vnode.context[el[nameSpace].methodName]() | ||
} else { | ||
el[nameSpace].bindingFn && el[nameSpace].bindingFn() | ||
} | ||
} | ||
|
||
/** | ||
* v-clickoutside | ||
* @desc 点击元素外面才会触发的事件 | ||
* @example | ||
* 两个修饰符,mousedown、mouseup | ||
* 当没有修饰符时,需要同时满足在目标元素外同步按下和释放鼠标才会触发回调。 | ||
* ```html | ||
* <div v-clickoutside="handleClose"> // 在元素外部点击时触发 | ||
* <div v-clickoutside.mousedown="handleClose"> // 在元素外部按下鼠标时触发 | ||
* <div v-clickoutside.mouseup="handleClose"> // 在元素外部松开鼠标时触发 | ||
* ``` | ||
*/ | ||
export default { | ||
bind: (el, binding, vnode) => { | ||
nodeList.push(el) | ||
const id = seed++ | ||
const { modifiers, expression, value } = binding | ||
const { mousedown = false, mouseup = false } = modifiers || {} | ||
el[nameSpace] = { | ||
id, | ||
documentHandler: createDocumentHandler(el, binding, vnode), | ||
methodName: expression, | ||
bindingFn: value, | ||
mousedownTrigger: mousedown, | ||
mouseupTrigger: mouseup | ||
} | ||
}, | ||
|
||
update: (el, binding, vnode) => { | ||
const { modifiers, expression, value } = binding | ||
const { mousedown = false, mouseup = false } = modifiers || {} | ||
el[nameSpace].documentHandler = createDocumentHandler(el, binding, vnode) | ||
el[nameSpace].methodName = expression | ||
el[nameSpace].bindingFn = value | ||
el[nameSpace].mousedownTrigger = mousedown | ||
el[nameSpace].mouseupTrigger = mouseup | ||
}, | ||
|
||
unbind: (el) => { | ||
if (el.nodeType !== Node.ELEMENT_NODE) { | ||
return | ||
} | ||
|
||
let len = nodeList.length | ||
|
||
for (let i = 0; i < len; i++) { | ||
if (nodeList[i][nameSpace].id === el[nameSpace].id) { | ||
nodeList.splice(i, 1) | ||
break | ||
} | ||
} | ||
|
||
if (nodeList.length === 0 && startClick) { | ||
startClick = null | ||
} | ||
|
||
delete el[nameSpace] | ||
} | ||
} |
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.
🛠️ 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.
📝 Committable suggestion
🤖 Prompt for AI Agents