diff --git a/web/package.json b/web/package.json index 92eb403..893a893 100644 --- a/web/package.json +++ b/web/package.json @@ -34,6 +34,7 @@ "@headlessui/react": "^1.7.17", "@heroicons/react": "^2.0.18", "@hookform/error-message": "^2.0.1", + "@popperjs/core": "^2.11.8", "@tailwindcss/forms": "^0.5.7", "@tanstack/react-query": "^4.36.1", "@tanstack/react-query-devtools": "^4.36.1", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index c7d160a..98bf758 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -17,6 +17,9 @@ dependencies: '@hookform/error-message': specifier: ^2.0.1 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': specifier: ^0.5.7 version: 0.5.7(tailwindcss@3.3.5) diff --git a/web/src/components/tooltips/Tooltip.tsx b/web/src/components/tooltips/Tooltip.tsx index 7150aa5..281b694 100644 --- a/web/src/components/tooltips/Tooltip.tsx +++ b/web/src/components/tooltips/Tooltip.tsx @@ -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(null); + const [triggerNode, setTriggerNode] = useState(null); + const isTouchDevice = () => { + return 'ontouchstart' in window || navigator.maxTouchPoints > 0; + }; + + // default tooltip placement to right + const [placement, setPlacement] = useState('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) => { + 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} e.stopPropagation() })} > {title ? (