From 2842e1c65579aa36466fc987478cb985cf6c1a42 Mon Sep 17 00:00:00 2001 From: Daniel Karlsson Date: Tue, 14 Mar 2023 15:04:03 +0100 Subject: [PATCH 1/2] feat(popover): reposition on scroll event Keep popover at anchor element when user is scrolling --- packages/core/src/PopOver/index.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/core/src/PopOver/index.tsx b/packages/core/src/PopOver/index.tsx index 75520b17..c379def8 100644 --- a/packages/core/src/PopOver/index.tsx +++ b/packages/core/src/PopOver/index.tsx @@ -136,14 +136,10 @@ export const PopOver: FC = ({ popOverContainer, ]) - useOnScrollEffect( - anchorEl, - onScroll !== undefined - ? onScroll - : () => { - /** */ - } - ) + useOnScrollEffect(anchorEl, () => { + onScroll?.() + position() + }) // Used when resizing the parent anchorEl useOnResizeParentEffect(anchorEl, position) From d2c218d5a1cc7b8bfbd1c3da3583677b8c5ab001 Mon Sep 17 00:00:00 2001 From: Daniel Karlsson Date: Tue, 14 Mar 2023 15:28:27 +0100 Subject: [PATCH 2/2] feat(tooltip): remove tooltip on touch scroll Remove tooltip on touch-devices if user scrolls more than 150 pixels. --- packages/core/src/Tooltip/index.tsx | 18 ++++- packages/core/src/Tooltip/utils/index.ts | 1 + .../Tooltip/utils/useTouchScrollDistance.ts | 70 +++++++++++++++++++ .../src/coreComponents/Tooltip.spec.ts | 49 +++++++++++++ 4 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/Tooltip/utils/index.ts create mode 100644 packages/core/src/Tooltip/utils/useTouchScrollDistance.ts diff --git a/packages/core/src/Tooltip/index.tsx b/packages/core/src/Tooltip/index.tsx index b4080c4d..e29ac9b5 100644 --- a/packages/core/src/Tooltip/index.tsx +++ b/packages/core/src/Tooltip/index.tsx @@ -16,6 +16,7 @@ import { Typography, TypographyProps } from '../Typography' import { PopOver, PopOverProps } from '../PopOver' import { shape, spacing, componentSize } from '../designparams' import { font } from '../theme' +import { useTouchScrollDistance } from './utils' /** * Tooltip @@ -252,7 +253,6 @@ export const Tooltip: FC = ({ (props.variant === 'expanded' ? props.placement : undefined) ?? 'up-down' const child = Children.only(children) as ReactElement const [anchorEl, setAnchorEl] = useState(null) - // State for click const [visibleByClick, showByClick] = useState(false) // Delayed state for pointer @@ -265,6 +265,8 @@ export const Tooltip: FC = ({ // If tooltip should be shown const visible = visibleByClick || debouncedVisible + const touchScrollDistance = useTouchScrollDistance() + const toggle = useCallback( (event: PointerEvent) => { // When using touch instead of mouse, we have to toggle the tooltip @@ -278,6 +280,20 @@ export const Tooltip: FC = ({ [showByClick] ) + /** + * If the delta for any axis is larger than 150 pixels, + * remove the tooltip from the screen. + */ + useLayoutEffect(() => { + if (!visible) { + return + } + const { x, y } = touchScrollDistance + if (Math.max(Math.abs(x), Math.abs(y)) > 150) { + showByClick(false) + } + }, [touchScrollDistance]) + useEffect(() => { const delayVisible = () => setDebouncedVisible(visibleDelayed) const delayed = setTimeout(delayVisible, TOOLTIP_DELAY_MS) diff --git a/packages/core/src/Tooltip/utils/index.ts b/packages/core/src/Tooltip/utils/index.ts new file mode 100644 index 00000000..bd1fd1e8 --- /dev/null +++ b/packages/core/src/Tooltip/utils/index.ts @@ -0,0 +1 @@ +export * from './useTouchScrollDistance' diff --git a/packages/core/src/Tooltip/utils/useTouchScrollDistance.ts b/packages/core/src/Tooltip/utils/useTouchScrollDistance.ts new file mode 100644 index 00000000..cf327edf --- /dev/null +++ b/packages/core/src/Tooltip/utils/useTouchScrollDistance.ts @@ -0,0 +1,70 @@ +import { useState, useEffect, useLayoutEffect } from 'react' + +export const useTouchScrollDistance = () => { + const [origin, setOrigin] = useState(null) + const [touches, setTouches] = useState(null) + /** + * The distance between touch origin and touch current for both + * x-axis and y-axis + */ + const [touchScrollDistance, setTouchScrollDistance] = useState({ x: 0, y: 0 }) + + useEffect(() => { + const touchStartHandler = (event: TouchEvent) => { + if (origin === null) { + setOrigin(event.touches) + } + } + + const touchMoveHandler = (event: TouchEvent) => + setTouches(event.changedTouches) + + const touchEndHandler = (event: TouchEvent) => { + if (event.touches.length === 0) { + setOrigin(null) + setTouches(null) + setTouchScrollDistance({ x: 0, y: 0 }) + } + } + + const touchCancelHandler = () => { + setOrigin(null) + setTouches(null) + setTouchScrollDistance({ x: 0, y: 0 }) + } + + document.addEventListener('touchstart', touchStartHandler) + document.addEventListener('touchmove', touchMoveHandler) + document.addEventListener('touchend', touchEndHandler) + document.addEventListener('touchcancel', touchCancelHandler) + + return () => { + document.removeEventListener('touchstart', touchStartHandler) + document.removeEventListener('touchmove', touchMoveHandler) + document.removeEventListener('touchend', touchEndHandler) + document.removeEventListener('touchcancel', touchCancelHandler) + } + }, [origin]) + + /** + * Calculates the distance in pixels between the origin of + * a touch event and position updates to that touch event. + */ + useLayoutEffect(() => { + if (origin === null || touches === null) { + return + } + + // User is not scrolling + if (touches.length > 1) { + return + } + + const deltaX = touches[0].clientX - origin[0].clientX + const deltaY = touches[0].clientY - origin[0].clientY + + setTouchScrollDistance({ x: deltaX, y: deltaY }) + }, [origin, touches]) + + return touchScrollDistance +} diff --git a/packages/ui-tests/src/coreComponents/Tooltip.spec.ts b/packages/ui-tests/src/coreComponents/Tooltip.spec.ts index 42d330b3..2e75e136 100644 --- a/packages/ui-tests/src/coreComponents/Tooltip.spec.ts +++ b/packages/ui-tests/src/coreComponents/Tooltip.spec.ts @@ -60,3 +60,52 @@ context('Tooltip', () => { }) }) }) + +context('Tooltip mobile device', () => { + before(() => { + cy.viewport('ipad-2') + cy.visit('http://localhost:9009/#/components/tooltip') + }) + + const data = { + textDataCy: 'expandedTooltipBottomLeftRightText', + tooltipDataCy: 'expandedTooltipBottomLeftRight', + } + + it(`Tooltip ${data.tooltipDataCy} should appear, and should hide`, () => { + // Touch to show tooltip + cy.get(`[data-cy=${data.textDataCy}]`).should('exist') + cy.get(`[data-cy=${data.textDataCy}]`).trigger('pointerdown') + cy.get(`[data-cy=${data.tooltipDataCy}]`) + .should('exist') + .should('be.visible') + + // Touch to hide tooltip + cy.get(`[data-cy=${data.textDataCy}]`).trigger('pointerdown') + cy.get(`[data-cy=${data.tooltipDataCy}]`).should('not.exist') + }) + + it(`Tooltip ${data.tooltipDataCy} should hide when client touch move more than 150 pixels`, () => { + // Touch to show tooltip + cy.get(`[data-cy=${data.textDataCy}]`).trigger('pointerdown') + cy.get(`[data-cy=${data.tooltipDataCy}]`) + .should('exist') + .should('be.visible') + + // Touch move 151 pixels and hide tooltip + cy.get(`[data-cy=${data.textDataCy}]`) + .parent() + .trigger('touchstart', { + touches: [{ clientX: 0, clientY: 0, identifier: 0 }], + }) + + cy.get(`[data-cy=${data.textDataCy}]`) + .parent() + .trigger('touchmove', { + changedTouches: [{ clientX: 151, clientY: 151, identifier: 0 }], + }) + + // Tooltip is not shown + cy.get(`[data-cy=${data.tooltipDataCy}]`).should('not.exist') + }) +})