diff --git a/examples/sites/demos/pc/app/tooltip/content-max-height.spec.js b/examples/sites/demos/pc/app/tooltip/content-max-height.spec.js index b12ef14d92..ad985d13da 100644 --- a/examples/sites/demos/pc/app/tooltip/content-max-height.spec.js +++ b/examples/sites/demos/pc/app/tooltip/content-max-height.spec.js @@ -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') }) diff --git a/examples/sites/demos/pc/app/tooltip/control.spec.js b/examples/sites/demos/pc/app/tooltip/control.spec.js index cdf9f51109..8d29f6d519 100644 --- a/examples/sites/demos/pc/app/tooltip/control.spec.js +++ b/examples/sites/demos/pc/app/tooltip/control.spec.js @@ -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() await page.waitForTimeout(20) @@ -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() diff --git a/examples/sites/demos/pc/app/tooltip/popper-options.spec.js b/examples/sites/demos/pc/app/tooltip/popper-options.spec.js index d5c2de8a9f..47d8220b56 100644 --- a/examples/sites/demos/pc/app/tooltip/popper-options.spec.js +++ b/examples/sites/demos/pc/app/tooltip/popper-options.spec.js @@ -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 }) diff --git a/examples/sites/src/assets/custom-markdown.css b/examples/sites/src/assets/custom-markdown.less similarity index 100% rename from examples/sites/src/assets/custom-markdown.css rename to examples/sites/src/assets/custom-markdown.less diff --git a/examples/sites/src/main.js b/examples/sites/src/main.js index d009daa8d5..393e910265 100644 --- a/examples/sites/src/main.js +++ b/examples/sites/src/main.js @@ -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' diff --git a/packages/renderless/src/tooltip/new-index.ts b/packages/renderless/src/tooltip/new-index.ts new file mode 100644 index 0000000000..4000be0929 --- /dev/null +++ b/packages/renderless/src/tooltip/new-index.ts @@ -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 + if (scrollWidth <= clientWidth) { + return + } + } + + state.showPopper = isShow + if (props.manual) { + emit('update:modelValue', isShow) + } + + // 自动隐藏: 如果显示,且要自动隐藏,则延时后关闭 + if (!props.manual && props.hideAfter && isShow) { + api.delayHideAfter() + } + } diff --git a/packages/renderless/src/tooltip/new-vue.ts b/packages/renderless/src/tooltip/new-vue.ts new file mode 100644 index 0000000000..25a33b1d7e --- /dev/null +++ b/packages/renderless/src/tooltip/new-vue.ts @@ -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'] + +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 }) + }) + 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 +} diff --git a/packages/vue-hooks/index.ts b/packages/vue-hooks/index.ts index 4c8e1bd425..36c82f10fb 100644 --- a/packages/vue-hooks/index.ts +++ b/packages/vue-hooks/index.ts @@ -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' diff --git a/packages/vue-hooks/src/useTimer.ts b/packages/vue-hooks/src/useTimer.ts new file mode 100644 index 0000000000..02dcb366ef --- /dev/null +++ b/packages/vue-hooks/src/useTimer.ts @@ -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 } + } diff --git a/packages/vue/src/tooltip/package.json b/packages/vue/src/tooltip/package.json index 33b3029b26..efbc906298 100644 --- a/packages/vue/src/tooltip/package.json +++ b/packages/vue/src/tooltip/package.json @@ -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:" + } } diff --git a/packages/vue/src/tooltip/src/clickoutside.ts b/packages/vue/src/tooltip/src/clickoutside.ts new file mode 100644 index 0000000000..e87cac6877 --- /dev/null +++ b/packages/vue/src/tooltip/src/clickoutside.ts @@ -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 + }) +} + +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 + *
// 在元素外部点击时触发 + *
// 在元素外部按下鼠标时触发 + *
// 在元素外部松开鼠标时触发 + * ``` + */ +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] + } +} diff --git a/packages/vue/src/tooltip/src/index.ts b/packages/vue/src/tooltip/src/index.ts index 4c5ffa7456..f57e75600e 100644 --- a/packages/vue/src/tooltip/src/index.ts +++ b/packages/vue/src/tooltip/src/index.ts @@ -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, diff --git a/packages/vue/src/tooltip/src/tooltip.vue b/packages/vue/src/tooltip/src/tooltip.vue new file mode 100644 index 0000000000..f64867b52f --- /dev/null +++ b/packages/vue/src/tooltip/src/tooltip.vue @@ -0,0 +1,160 @@ + + +