From 84e32abcafb407517cb2961e2bd11e9d428d0514 Mon Sep 17 00:00:00 2001 From: Bastian Jakobs Date: Fri, 14 Nov 2025 16:10:06 +0100 Subject: [PATCH 1/5] feat: add programmatic control for tooltip visibility and enhance tooltip props --- package.json | 2 +- src/components/tooltip/Tooltip.vue | 66 +++++++++++++++++++++++-- src/composables/useExternalTrigger.ts | 2 +- src/composables/useTooltipProps.ts | 2 +- src/composables/useTooltipVisibility.ts | 26 ++++++++++ src/directives/tooltip.ts | 61 ++++++++++++++++++++++- src/types/tooltip.ts | 40 +++++++++++++++ 7 files changed, 190 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index d0f5d4f..7755300 100644 --- a/package.json +++ b/package.json @@ -90,4 +90,4 @@ "vitest": "^4.0.8", "vue-tsc": "^3.1.3" } -} \ No newline at end of file +} diff --git a/src/components/tooltip/Tooltip.vue b/src/components/tooltip/Tooltip.vue index 88c0bdd..d707772 100644 --- a/src/components/tooltip/Tooltip.vue +++ b/src/components/tooltip/Tooltip.vue @@ -40,7 +40,7 @@ * - Unique IDs for tooltip-trigger association */ -import type { TooltipProps, TooltipSlots } from '../../types/tooltip' +import type { TooltipExposed, TooltipProps, TooltipSlots } from '../../types/tooltip' import { computed, nextTick, useSlots, useTemplateRef, watch } from 'vue' import { @@ -61,7 +61,7 @@ import { getTooltipGlobalThemeRef } from '../../config/index' */ // Re-export types for external use -export type { TooltipProps, TooltipSlots } +export type { TooltipExposed, TooltipProps, TooltipSlots } const props = withDefaults(defineProps(), { position: undefined, @@ -75,7 +75,14 @@ const props = withDefaults(defineProps(), { offset: undefined, dark: undefined, externalTrigger: undefined, + modelValue: undefined, + id: undefined, }) + +const emit = defineEmits<{ + 'update:modelValue': [value: boolean] +}>() + defineSlots<{ default: () => any content?: () => any @@ -129,9 +136,11 @@ const { const { isVisible, - show, - hide, - toggle, + show: showInternal, + hide: hideInternal, + toggle: toggleInternal, + showImmediate, + hideImmediate, } = useTooltipVisibility( effectiveDisabled, effectiveShowDelay, @@ -139,6 +148,53 @@ const { calculatePosition, ) +// Sync with v-model +watch(isVisible, (newValue) => { + if (props.modelValue !== undefined && newValue !== props.modelValue) { + emit('update:modelValue', newValue) + } +}) + +// Watch for external v-model changes +watch(() => props.modelValue, (newValue) => { + if (newValue !== undefined && newValue !== isVisible.value) { + if (newValue) { + showImmediate() + } + else { + hideImmediate() + } + } +}) + +// Wrapper functions for normal event-driven behavior +function show() { + showInternal() +} + +function hide() { + hideInternal() +} + +function toggle() { + toggleInternal() +} + +// Expose methods for programmatic control +defineExpose({ + show: showImmediate, + hide: hideImmediate, + toggle: () => { + if (isVisible.value) { + hideImmediate() + } + else { + showImmediate() + } + }, + isVisible: () => isVisible.value, +}) + const { handleMouseEnter, handleMouseLeave, diff --git a/src/composables/useExternalTrigger.ts b/src/composables/useExternalTrigger.ts index 6e61474..7e0d22c 100644 --- a/src/composables/useExternalTrigger.ts +++ b/src/composables/useExternalTrigger.ts @@ -1,4 +1,4 @@ -import type { ComputedRef, Ref } from 'vue' +import type { ComputedRef } from 'vue' import { onUnmounted, watch } from 'vue' /** diff --git a/src/composables/useTooltipProps.ts b/src/composables/useTooltipProps.ts index 4bc9d76..61219a5 100644 --- a/src/composables/useTooltipProps.ts +++ b/src/composables/useTooltipProps.ts @@ -7,7 +7,7 @@ import { getReactiveGlobalConfig } from '../config/globalConfig' /** * Default values for tooltip props */ -export const DEFAULT_TOOLTIP_PROPS: Readonly>> = { +export const DEFAULT_TOOLTIP_PROPS: Readonly>> = { position: 'auto', trigger: 'both', showDelay: 100, diff --git a/src/composables/useTooltipVisibility.ts b/src/composables/useTooltipVisibility.ts index ff73681..fe9b733 100644 --- a/src/composables/useTooltipVisibility.ts +++ b/src/composables/useTooltipVisibility.ts @@ -58,6 +58,22 @@ export function useTooltipVisibility( }, showDelay.value) } + /** + * Shows the tooltip immediately without delay (for programmatic control) + */ + async function showImmediate() { + clearTimeouts() + _isVisible.value = true + await nextTick() + // Wait for the browser to render the tooltip with proper dimensions + await new Promise(resolve => requestAnimationFrame(resolve)) + + // Execute callback after tooltip is visible and rendered + if (onShow) { + await onShow() + } + } + /** * Hides the tooltip after the configured delay */ @@ -71,6 +87,14 @@ export function useTooltipVisibility( }, hideDelay.value) } + /** + * Hides the tooltip immediately without delay (for programmatic control) + */ + function hideImmediate() { + clearTimeouts() + _isVisible.value = false + } + /** * Toggles the tooltip visibility */ @@ -97,6 +121,8 @@ export function useTooltipVisibility( show, hide, toggle, + showImmediate, + hideImmediate, clearTimeouts, } } diff --git a/src/directives/tooltip.ts b/src/directives/tooltip.ts index 892d63c..ee57060 100644 --- a/src/directives/tooltip.ts +++ b/src/directives/tooltip.ts @@ -24,6 +24,7 @@ interface TooltipInstance { id: string element: HTMLElement props: TooltipProps + componentRef?: any } // Shared state for all tooltip instances @@ -31,6 +32,53 @@ const tooltipStore = reactive({ tooltips: new Map(), }) +/** + * Public API for programmatic tooltip control + */ +export const TooltipControl = { + /** + * Show a tooltip by ID + * @param id - The tooltip ID + */ + show(id: string): void { + const instance = tooltipStore.tooltips.get(id) + if (instance?.componentRef) { + instance.componentRef.show() + } + }, + + /** + * Hide a tooltip by ID + * @param id - The tooltip ID + */ + hide(id: string): void { + const instance = tooltipStore.tooltips.get(id) + if (instance?.componentRef) { + instance.componentRef.hide() + } + }, + + /** + * Toggle a tooltip by ID + * @param id - The tooltip ID + */ + toggle(id: string): void { + const instance = tooltipStore.tooltips.get(id) + if (instance?.componentRef) { + instance.componentRef.toggle() + } + }, + + /** + * Check if a tooltip is visible + * @param id - The tooltip ID + */ + isVisible(id: string): boolean { + const instance = tooltipStore.tooltips.get(id) + return instance?.componentRef ? instance.componentRef.isVisible() : false + }, +} + // Single shared Vue app instance let sharedApp: App | null = null let appContainer: HTMLElement | null = null @@ -130,6 +178,15 @@ function initializeSharedApp() { const tooltipComponents = Array.from(this.tooltips.values() as IterableIterator).map((instance) => { return h(Tooltip, { key: instance.id, + ref: (el: any) => { + // Capture component ref for programmatic control + if (el) { + const tooltipInstance = tooltipStore.tooltips.get(instance.id) + if (tooltipInstance) { + tooltipInstance.componentRef = el + } + } + }, ...instance.props, externalTrigger: instance.element, }) @@ -149,9 +206,11 @@ function addTooltipInstance(element: HTMLElement, binding: TooltipDirectiveBindi // Initialize shared app if needed initializeSharedApp() - const id = generateTooltipInstanceId() const props = getTooltipProps(binding) + // Use custom ID if provided, otherwise generate one + const id = props.id || generateTooltipInstanceId() + // Add to reactive store (triggers re-render) tooltipStore.tooltips.set(id, { id, diff --git a/src/types/tooltip.ts b/src/types/tooltip.ts index bf5aad2..ac373d3 100644 --- a/src/types/tooltip.ts +++ b/src/types/tooltip.ts @@ -97,6 +97,46 @@ export interface TooltipProps { * @default undefined */ externalTrigger?: HTMLElement + + /** + * Controls the visibility of the tooltip (for v-model support) + * @default false + */ + modelValue?: boolean + + /** + * Unique identifier for the tooltip (used for programmatic control in directive mode) + * @default undefined + */ + id?: string +} + +/** + * Methods exposed by the Tooltip component for programmatic control + */ +export interface TooltipExposed { + /** + * Programmatically show the tooltip + * Bypasses delays and disabled state for immediate display + */ + show: () => void + + /** + * Programmatically hide the tooltip + * Bypasses delays for immediate hiding + */ + hide: () => void + + /** + * Toggle the tooltip visibility + * Bypasses delays and disabled state + */ + toggle: () => void + + /** + * Check if the tooltip is currently visible + */ + isVisible: () => boolean } /** From 1b6302f2e694341f5c4e642f6f1f075afe4ff4c8 Mon Sep 17 00:00:00 2001 From: Bastian Jakobs Date: Fri, 14 Nov 2025 16:10:22 +0100 Subject: [PATCH 2/5] feat: export TooltipControl component and update type exports --- src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index db1195f..06f0524 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,10 +3,10 @@ import type { App, Plugin } from 'vue' import type { TooltipProps, TooltipTheme } from './types/tooltip' import Tooltip from './components/tooltip/Tooltip.vue' import { setTooltipGlobalConfig, setTooltipGlobalTheme } from './config/index' -import { vTooltip } from './directives/tooltip' +import { TooltipControl, vTooltip } from './directives/tooltip' // Export components and directives -export { Tooltip, vTooltip } +export { Tooltip, TooltipControl, vTooltip } // Export composables for advanced usage export * from './composables' @@ -14,7 +14,7 @@ export * from './composables' // Export configuration functions export { getTooltipGlobalConfig, getTooltipGlobalTheme, resetTooltipGlobalConfig, setTooltipGlobalConfig, setTooltipGlobalTheme } from './config/index' -export type { TooltipProps, TooltipSlots, TooltipTheme } from './types/tooltip' +export type { TooltipExposed, TooltipProps, TooltipSlots, TooltipTheme } from './types/tooltip' // Export types export type { TooltipDirectiveModifiers, From 3604bcb3a22f5b8fcc07ae828e2895a8b0108ec2 Mon Sep 17 00:00:00 2001 From: Bastian Jakobs Date: Fri, 14 Nov 2025 16:11:11 +0100 Subject: [PATCH 3/5] feat: add ProgrammaticControl component and enhance tooltip examples with programmatic functionality to showcase examples --- src/App.vue | 20 ++- src/components/showcase/DirectiveExample.vue | 37 +++++ .../showcase/ProgrammaticControl.vue | 127 ++++++++++++++++++ src/components/showcase/TooltipExample.vue | 67 ++++++++- 4 files changed, 247 insertions(+), 4 deletions(-) create mode 100644 src/components/showcase/ProgrammaticControl.vue diff --git a/src/App.vue b/src/App.vue index 148d061..dd33a3d 100644 --- a/src/App.vue +++ b/src/App.vue @@ -4,11 +4,12 @@ import Button from '@/components/Button.vue' import PresetSwitcher from '@/components/PresetSwitcher.vue' import DirectiveBenchmark from '@/components/showcase/DirectiveBenchmark.vue' import DirectiveExample from '@/components/showcase/DirectiveExample.vue' +import ProgrammaticControl from '@/components/showcase/ProgrammaticControl.vue' import TooltipExample from '@/components/showcase/TooltipExample.vue' import ThemeToggle from '@/components/ThemeToggle.vue' import packageJson from '../package.json' -type Tabs = 'component' | 'directive' | 'directive-benchmark' +type Tabs = 'component' | 'directive' | 'directive-benchmark' | 'programmatic-control' const activeTab = ref('component') @@ -85,6 +86,17 @@ const githubRepo = packageJson.repository.url.replace('.git', '') > Tooltip Directive Benchmark + @@ -147,6 +159,7 @@ const githubRepo = packageJson.repository.url.replace('.git', '') + @@ -184,6 +197,11 @@ const githubRepo = packageJson.repository.url.replace('.git', '') :disabled="activeTab === 'directive-benchmark'" @click="switchTab('directive-benchmark')" /> +