fix(web): improve tooltip responsiveness and click-through (#1314)

* fix(web): prevent tooltip click-through on mobile

* fix: adjust tooltip placement based on screen size
This commit is contained in:
soup 2023-12-25 13:38:29 +01:00 committed by GitHub
parent 6815c67e0c
commit 3fd939b531
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 77 additions and 16 deletions

View file

@ -34,6 +34,7 @@
"@headlessui/react": "^1.7.17", "@headlessui/react": "^1.7.17",
"@heroicons/react": "^2.0.18", "@heroicons/react": "^2.0.18",
"@hookform/error-message": "^2.0.1", "@hookform/error-message": "^2.0.1",
"@popperjs/core": "^2.11.8",
"@tailwindcss/forms": "^0.5.7", "@tailwindcss/forms": "^0.5.7",
"@tanstack/react-query": "^4.36.1", "@tanstack/react-query": "^4.36.1",
"@tanstack/react-query-devtools": "^4.36.1", "@tanstack/react-query-devtools": "^4.36.1",

3
web/pnpm-lock.yaml generated
View file

@ -17,6 +17,9 @@ dependencies:
'@hookform/error-message': '@hookform/error-message':
specifier: ^2.0.1 specifier: ^2.0.1
version: 2.0.1(react-dom@18.2.0)(react-hook-form@7.48.2)(react@18.2.0) version: 2.0.1(react-dom@18.2.0)(react-hook-form@7.48.2)(react@18.2.0)
'@popperjs/core':
specifier: ^2.11.8
version: 2.11.8
'@tailwindcss/forms': '@tailwindcss/forms':
specifier: ^0.5.7 specifier: ^0.5.7
version: 0.5.7(tailwindcss@3.3.5) version: 0.5.7(tailwindcss@3.3.5)

View file

@ -3,9 +3,12 @@
* SPDX-License-Identifier: GPL-2.0-or-later * SPDX-License-Identifier: GPL-2.0-or-later
*/ */
import type { ReactNode } from "react"; import React, { useState, useCallback, useEffect } from 'react';
import type { ReactNode } from 'react';
import { Transition } from "@headlessui/react"; import { Transition } from "@headlessui/react";
import { usePopperTooltip } from "react-popper-tooltip"; import { usePopperTooltip } from "react-popper-tooltip";
import { Placement } from '@popperjs/core';
import { classNames } from "@utils"; import { classNames } from "@utils";
@ -21,6 +24,7 @@ interface TooltipProps {
// NOTE(stacksmash76): onClick is not propagated // NOTE(stacksmash76): onClick is not propagated
// to the label (always-visible) component, so you will have // to the label (always-visible) component, so you will have
// to use the `onLabelClick` prop in this case. // to use the `onLabelClick` prop in this case.
export const Tooltip = ({ export const Tooltip = ({
label, label,
onLabelClick, onLabelClick,
@ -29,22 +33,75 @@ export const Tooltip = ({
requiresClick, requiresClick,
maxWidth = "max-w-sm" maxWidth = "max-w-sm"
}: TooltipProps) => { }: TooltipProps) => {
const [isTooltipVisible, setIsTooltipVisible] = useState(false);
const [tooltipNode, setTooltipNode] = useState<HTMLDivElement | null>(null);
const [triggerNode, setTriggerNode] = useState<HTMLDivElement | null>(null);
const isTouchDevice = () => {
return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
};
// default tooltip placement to right
const [placement, setPlacement] = useState<Placement>('right');
// check screen size and update placement if needed
useEffect(() => {
const updatePlacementForScreenSize = () => {
const screenWidth = window.innerWidth;
if (screenWidth < 640) { // tailwind's sm breakpoint
setPlacement('top');
} else {
setPlacement('right');
}
};
updatePlacementForScreenSize();
window.addEventListener('resize', updatePlacementForScreenSize);
return () => {
window.removeEventListener('resize', updatePlacementForScreenSize);
};
}, []);
const { const {
// TODO?: getArrowProps,
getTooltipProps, getTooltipProps,
setTooltipRef, setTooltipRef: popperSetTooltipRef,
setTriggerRef, setTriggerRef: popperSetTriggerRef,
visible visible
} = usePopperTooltip({ } = usePopperTooltip({
trigger: requiresClick ? ["click"] : ["click", "hover"], trigger: isTouchDevice() ? [] : (requiresClick ? 'click' : ['click', 'hover']),
interactive: true, interactive: true,
delayHide: 200, delayHide: 200,
placement: "right" placement,
}); });
if (!children || Array.isArray(children) && !children.length) { const handleTouch = (e: React.TouchEvent<HTMLDivElement>) => {
return null; e.preventDefault();
} setIsTooltipVisible(!isTooltipVisible);
};
const setTooltipRef = (node: HTMLDivElement | null) => {
popperSetTooltipRef(node);
setTooltipNode(node);
};
const setTriggerRef = (node: HTMLDivElement | null) => {
popperSetTriggerRef(node);
setTriggerNode(node);
};
const handleClickOutside = useCallback((event: TouchEvent) => {
if (tooltipNode && !tooltipNode.contains(event.target as Node) && triggerNode && !triggerNode.contains(event.target as Node)) {
setIsTooltipVisible(false);
}
}, [tooltipNode, triggerNode]);
useEffect(() => {
document.addEventListener('touchstart', handleClickOutside, true);
return () => {
document.removeEventListener('touchstart', handleClickOutside, true);
};
}, [handleClickOutside, tooltipNode, triggerNode]);
return ( return (
<> <>
@ -52,17 +109,16 @@ export const Tooltip = ({
ref={setTriggerRef} ref={setTriggerRef}
className="truncate" className="truncate"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); if (!isTouchDevice() && !visible) {
e.stopPropagation(); onLabelClick?.(e);
e.nativeEvent.stopImmediatePropagation(); }
onLabelClick?.(e);
}} }}
onTouchStart={isTouchDevice() ? handleTouch : undefined}
> >
{label} {label}
</div> </div>
<Transition <Transition
show={visible} show={isTouchDevice() ? isTooltipVisible : visible}
className="z-10" className="z-10"
enter="transition duration-200 ease-out" enter="transition duration-200 ease-out"
enterFrom="opacity-0" enterFrom="opacity-0"
@ -77,7 +133,8 @@ export const Tooltip = ({
className: classNames( className: classNames(
maxWidth, maxWidth,
"rounded-md border border-gray-300 text-black text-xs normal-case tracking-normal font-normal shadow-lg dark:text-white dark:border-gray-700 dark:shadow-2xl" "rounded-md border border-gray-300 text-black text-xs normal-case tracking-normal font-normal shadow-lg dark:text-white dark:border-gray-700 dark:shadow-2xl"
) ),
onClick: (e: React.MouseEvent) => e.stopPropagation()
})} })}
> >
{title ? ( {title ? (