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

@ -3,9 +3,12 @@
* 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 { usePopperTooltip } from "react-popper-tooltip";
import { Placement } from '@popperjs/core';
import { classNames } from "@utils";
@ -21,6 +24,7 @@ interface TooltipProps {
// NOTE(stacksmash76): onClick is not propagated
// to the label (always-visible) component, so you will have
// to use the `onLabelClick` prop in this case.
export const Tooltip = ({
label,
onLabelClick,
@ -29,22 +33,75 @@ export const Tooltip = ({
requiresClick,
maxWidth = "max-w-sm"
}: 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 {
// TODO?: getArrowProps,
getTooltipProps,
setTooltipRef,
setTriggerRef,
setTooltipRef: popperSetTooltipRef,
setTriggerRef: popperSetTriggerRef,
visible
} = usePopperTooltip({
trigger: requiresClick ? ["click"] : ["click", "hover"],
trigger: isTouchDevice() ? [] : (requiresClick ? 'click' : ['click', 'hover']),
interactive: true,
delayHide: 200,
placement: "right"
placement,
});
if (!children || Array.isArray(children) && !children.length) {
return null;
}
const handleTouch = (e: React.TouchEvent<HTMLDivElement>) => {
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 (
<>
@ -52,17 +109,16 @@ export const Tooltip = ({
ref={setTriggerRef}
className="truncate"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
onLabelClick?.(e);
if (!isTouchDevice() && !visible) {
onLabelClick?.(e);
}
}}
onTouchStart={isTouchDevice() ? handleTouch : undefined}
>
{label}
</div>
<Transition
show={visible}
show={isTouchDevice() ? isTooltipVisible : visible}
className="z-10"
enter="transition duration-200 ease-out"
enterFrom="opacity-0"
@ -77,7 +133,8 @@ export const Tooltip = ({
className: classNames(
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"
)
),
onClick: (e: React.MouseEvent) => e.stopPropagation()
})}
>
{title ? (