From 89390f0b73f98d7121add0fd1e181177a41c6123 Mon Sep 17 00:00:00 2001 From: J-Sek Date: Sun, 3 Aug 2025 22:21:26 +0200 Subject: [PATCH] feat(VTooltip): open on touch hold --- .../src/components/VOverlay/useActivator.tsx | 43 ++++++++++++++++++- .../src/components/VTooltip/VTooltip.tsx | 1 + 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/vuetify/src/components/VOverlay/useActivator.tsx b/packages/vuetify/src/components/VOverlay/useActivator.tsx index bb216f85848..97c37b57882 100644 --- a/packages/vuetify/src/components/VOverlay/useActivator.tsx +++ b/packages/vuetify/src/components/VOverlay/useActivator.tsx @@ -42,6 +42,7 @@ interface ActivatorProps extends DelayProps { activatorProps: Record openOnClick: boolean | undefined + openOnTouchHold: boolean | undefined openOnHover: boolean openOnFocus: boolean | undefined @@ -60,6 +61,7 @@ export const makeActivatorProps = propsFactory({ type: Boolean, default: undefined, }, + openOnTouchHold: Boolean, openOnHover: Boolean, openOnFocus: { type: Boolean, @@ -83,16 +85,18 @@ export function useActivator ( const activatorEl = ref() let isHovered = false + let isTouched = false let isFocused = false let firstEnter = true const openOnFocus = computed(() => props.openOnFocus || (props.openOnFocus == null && props.openOnHover)) const openOnClick = computed(() => props.openOnClick || (props.openOnClick == null && !props.openOnHover && !openOnFocus.value)) - const { runOpenDelay, runCloseDelay } = useDelay(props, value => { + const { clearDelay, runOpenDelay, runCloseDelay } = useDelay(props, value => { if ( value === ( (props.openOnHover && isHovered) || + (props.openOnTouchHold && isTouched) || (openOnFocus.value && isFocused) ) && !(props.openOnHover && isActive.value && !isTop.value) ) { @@ -113,6 +117,37 @@ export function useActivator ( } isActive.value = !isActive.value }, + oncontextmenu: (e: Event) => { + e.preventDefault() + }, + onTouchstart: (e: TouchEvent) => { + if (!e.touches) return + e.stopPropagation() + clearDelay() + activatorEl.value = (e.currentTarget || e.target) as HTMLElement + if (!isActive.value) { + const { clientX, clientY } = e.touches[0] + cursorTarget.value = [clientX, clientY] + } + isTouched = true + runOpenDelay() + }, + onTouchmove: (e: TouchEvent) => { + if (!e.touches || !cursorTarget.value) return + e.stopPropagation() + + const sensitivity = 7 + const [x, y] = cursorTarget.value + const { clientX, clientY } = e.touches[0] + if (Math.abs(x - clientX) > sensitivity || Math.abs(y - clientY) > sensitivity) { + isTouched = false + clearDelay() + } + }, + onTouchend: (e: TouchEvent) => { + isTouched = false + clearDelay() + }, onMouseenter: (e: MouseEvent) => { if (e.sourceCapabilities?.firesTouchEvents) return @@ -147,6 +182,12 @@ export function useActivator ( if (openOnClick.value) { events.onClick = availableEvents.onClick } + if (props.openOnTouchHold) { + events.onTouchstart = availableEvents.onTouchstart + events.onTouchmove = availableEvents.onTouchmove + events.onTouchend = availableEvents.onTouchend + events.oncontextmenu = availableEvents.oncontextmenu + } if (props.openOnHover) { events.onMouseenter = availableEvents.onMouseenter events.onMouseleave = availableEvents.onMouseleave diff --git a/packages/vuetify/src/components/VTooltip/VTooltip.tsx b/packages/vuetify/src/components/VTooltip/VTooltip.tsx index 29aae2b94ec..ce3c584b8d8 100644 --- a/packages/vuetify/src/components/VTooltip/VTooltip.tsx +++ b/packages/vuetify/src/components/VTooltip/VTooltip.tsx @@ -110,6 +110,7 @@ export const VTooltip = genericComponent()({ role="tooltip" activatorProps={ activatorProps.value } _disableGlobalStack + onClick:outside={ () => isActive.value = false } { ...scopeId } > {{