Skip to content
This repository was archived by the owner on Mar 25, 2025. It is now read-only.

Commit 06d200f

Browse files
danielkaxisboilund
authored andcommitted
feat(tooltip): remove tooltip on touch scroll
Remove tooltip on touch-devices if user scrolls more than 150 pixels.
1 parent b19437f commit 06d200f

File tree

4 files changed

+137
-1
lines changed

4 files changed

+137
-1
lines changed

packages/core/src/Tooltip/index.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { Typography, TypographyProps } from '../Typography'
1616
import { PopOver, PopOverProps } from '../PopOver'
1717
import { shape, spacing, componentSize } from '../designparams'
1818
import { font } from '../theme'
19+
import { useTouchScrollDistance } from './utils'
1920

2021
/**
2122
* Tooltip
@@ -252,7 +253,6 @@ export const Tooltip: FC<TooltipProps | ExpandedTooltipProps> = ({
252253
(props.variant === 'expanded' ? props.placement : undefined) ?? 'up-down'
253254
const child = Children.only(children) as ReactElement
254255
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
255-
256256
// State for click
257257
const [visibleByClick, showByClick] = useState(false)
258258
// Delayed state for pointer
@@ -265,6 +265,8 @@ export const Tooltip: FC<TooltipProps | ExpandedTooltipProps> = ({
265265
// If tooltip should be shown
266266
const visible = visibleByClick || debouncedVisible
267267

268+
const touchScrollDistance = useTouchScrollDistance()
269+
268270
const toggle = useCallback(
269271
(event: PointerEvent) => {
270272
// When using touch instead of mouse, we have to toggle the tooltip
@@ -278,6 +280,20 @@ export const Tooltip: FC<TooltipProps | ExpandedTooltipProps> = ({
278280
[showByClick]
279281
)
280282

283+
/**
284+
* If the delta for any axis is larger than 150 pixels,
285+
* remove the tooltip from the screen.
286+
*/
287+
useLayoutEffect(() => {
288+
if (!visible) {
289+
return
290+
}
291+
const { x, y } = touchScrollDistance
292+
if (Math.max(Math.abs(x), Math.abs(y)) > 150) {
293+
showByClick(false)
294+
}
295+
}, [touchScrollDistance])
296+
281297
useEffect(() => {
282298
const delayVisible = () => setDebouncedVisible(visibleDelayed)
283299
const delayed = setTimeout(delayVisible, TOOLTIP_DELAY_MS)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './useTouchScrollDistance'
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { useState, useEffect, useLayoutEffect } from 'react'
2+
3+
export const useTouchScrollDistance = () => {
4+
const [origin, setOrigin] = useState<TouchList | null>(null)
5+
const [touches, setTouches] = useState<TouchList | null>(null)
6+
/**
7+
* The distance between touch origin and touch current for both
8+
* x-axis and y-axis
9+
*/
10+
const [touchScrollDistance, setTouchScrollDistance] = useState({ x: 0, y: 0 })
11+
12+
useEffect(() => {
13+
const touchStartHandler = (event: TouchEvent) => {
14+
if (origin === null) {
15+
setOrigin(event.touches)
16+
}
17+
}
18+
19+
const touchMoveHandler = (event: TouchEvent) =>
20+
setTouches(event.changedTouches)
21+
22+
const touchEndHandler = (event: TouchEvent) => {
23+
if (event.touches.length === 0) {
24+
setOrigin(null)
25+
setTouches(null)
26+
setTouchScrollDistance({ x: 0, y: 0 })
27+
}
28+
}
29+
30+
const touchCancelHandler = () => {
31+
setOrigin(null)
32+
setTouches(null)
33+
setTouchScrollDistance({ x: 0, y: 0 })
34+
}
35+
36+
document.addEventListener('touchstart', touchStartHandler)
37+
document.addEventListener('touchmove', touchMoveHandler)
38+
document.addEventListener('touchend', touchEndHandler)
39+
document.addEventListener('touchcancel', touchCancelHandler)
40+
41+
return () => {
42+
document.removeEventListener('touchstart', touchStartHandler)
43+
document.removeEventListener('touchmove', touchMoveHandler)
44+
document.removeEventListener('touchend', touchEndHandler)
45+
document.removeEventListener('touchcancel', touchCancelHandler)
46+
}
47+
}, [origin])
48+
49+
/**
50+
* Calculates the distance in pixels between the origin of
51+
* a touch event and position updates to that touch event.
52+
*/
53+
useLayoutEffect(() => {
54+
if (origin === null || touches === null) {
55+
return
56+
}
57+
58+
// User is not scrolling
59+
if (touches.length > 1) {
60+
return
61+
}
62+
63+
const deltaX = touches[0].clientX - origin[0].clientX
64+
const deltaY = touches[0].clientY - origin[0].clientY
65+
66+
setTouchScrollDistance({ x: deltaX, y: deltaY })
67+
}, [origin, touches])
68+
69+
return touchScrollDistance
70+
}

packages/ui-tests/src/coreComponents/Tooltip.spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,52 @@ context('Tooltip', () => {
6060
})
6161
})
6262
})
63+
64+
context('Tooltip mobile device', () => {
65+
before(() => {
66+
cy.viewport('ipad-2')
67+
cy.visit('http://localhost:9009/#/components/tooltip')
68+
})
69+
70+
const data = {
71+
textDataCy: 'expandedTooltipBottomLeftRightText',
72+
tooltipDataCy: 'expandedTooltipBottomLeftRight',
73+
}
74+
75+
it(`Tooltip ${data.tooltipDataCy} should appear, and should hide`, () => {
76+
// Touch to show tooltip
77+
cy.get(`[data-cy=${data.textDataCy}]`).should('exist')
78+
cy.get(`[data-cy=${data.textDataCy}]`).trigger('pointerdown')
79+
cy.get(`[data-cy=${data.tooltipDataCy}]`)
80+
.should('exist')
81+
.should('be.visible')
82+
83+
// Touch to hide tooltip
84+
cy.get(`[data-cy=${data.textDataCy}]`).trigger('pointerdown')
85+
cy.get(`[data-cy=${data.tooltipDataCy}]`).should('not.exist')
86+
})
87+
88+
it(`Tooltip ${data.tooltipDataCy} should hide when client touch move more than 150 pixels`, () => {
89+
// Touch to show tooltip
90+
cy.get(`[data-cy=${data.textDataCy}]`).trigger('pointerdown')
91+
cy.get(`[data-cy=${data.tooltipDataCy}]`)
92+
.should('exist')
93+
.should('be.visible')
94+
95+
// Touch move 151 pixels and hide tooltip
96+
cy.get(`[data-cy=${data.textDataCy}]`)
97+
.parent()
98+
.trigger('touchstart', {
99+
touches: [{ clientX: 0, clientY: 0, identifier: 0 }],
100+
})
101+
102+
cy.get(`[data-cy=${data.textDataCy}]`)
103+
.parent()
104+
.trigger('touchmove', {
105+
changedTouches: [{ clientX: 151, clientY: 151, identifier: 0 }],
106+
})
107+
108+
// Tooltip is not shown
109+
cy.get(`[data-cy=${data.tooltipDataCy}]`).should('not.exist')
110+
})
111+
})

0 commit comments

Comments
 (0)