/* * Copyright (c) 2025 Robin E. R. Davies * All rights reserved. * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ import React from 'react'; import Tooltip from '@mui/material/Tooltip'; export interface ToolTipExProps extends React.ComponentProps { valueTooltip?: React.ReactNode; // For use with value prop, to show a tooltip on the value. } /* ToolTipEx: a reimplementation of the MUI Tooltip that works with pointer events and long presses. */ function ToolTipEx(props: ToolTipExProps) { let {title, valueTooltip, ...extras} = props; let [open,setOpen]= React.useState(false); let [isLongPress, setIsLongPress] = React.useState(false); let [longPressLeaving, setLongPressLeaving] = React.useState(false); let [timeout, setTimeout] = React.useState(0); let [timeoutInstance, setTimeoutInstance] = React.useState(0); let [pointerDownPoint, setPointerdownPoint] = React.useState<{x: number, y: number} | null>(null); const hoverTimeout = 1250; const longpressTimeout = 500; const longpressLeaveTimeout = 1500; function startTimeout(timeout: number) { setTimeout(timeout); setTimeoutInstance(timeoutInstance + 1); // make useeffect run. } function stopTimeout() { setTimeout(0); setTimeoutInstance(0); } function handleMouseEnter(event: React.MouseEvent) { setOpen(false); startTimeout(hoverTimeout); } function handleMouseLeave(event: React.MouseEvent) { setOpen(false); stopTimeout(); setLongPressLeaving(false); } function handlePointerDownCapture(event: React.PointerEvent) { let pointerType = (event as any).pointerType || "n/a"; setIsLongPress(false); if (pointerType === "mouse") { setOpen(false); stopTimeout(); // Reset hover timeout for mouse } else { // pen, or touch. setTimeout(longpressTimeout); setOpen(false); setPointerdownPoint({x: event.clientX, y: event.clientY}); } } React.useEffect(()=> { let t = timeout; let handle: number | null = null; if (valueTooltip === undefined) // no timeout if there's a value tooltip { if (t > 0) { // console.log("ToolTipEx: starting timeout for ", t); handle = window.setTimeout(() => { setOpen(true); },t); } } return () => { if (handle !== null) { // console.log("ToolTipEx: clearing timeout for ", t); window.clearTimeout(handle); } }; },[timeoutInstance]); React.useEffect(()=> { if (longPressLeaving) { let handle: number | null = null; handle = window.setTimeout(() => { setIsLongPress(false); setLongPressLeaving(false); }, longpressLeaveTimeout); return () => { if (handle !== null) { window.clearTimeout(handle); } }; } else { return () => { }; } },[longPressLeaving]); function handlePointerCancel(event: React.PointerEvent) { setOpen(false); setIsLongPress(false); stopTimeout(); setPointerdownPoint(null); // Reset the mouse down point } function handlePointerMoveCapture(event: React.PointerEvent) { let pointerType = (event as any).pointerType || "n/a"; if (pointerType === "mouse") { setOpen(false); startTimeout(hoverTimeout); // Reset hover timeout for mouse } else { // pen, or touch. if (pointerDownPoint) { const dx = event.clientX - pointerDownPoint.x; const dy = event.clientY - pointerDownPoint.y; if (Math.abs(dx) > 5 || Math.abs(dy) > 5) { // If the mouse has moved more than 5 pixels, re-enable the tooltip setOpen(false); setIsLongPress(false); stopTimeout(); setPointerdownPoint(null); // Reset the mouse down point } } } } function handleConextMenu(event: React.MouseEvent) { event.preventDefault(); // Prevent the default context menu from appearing event.stopPropagation(); // Prevent the default context menu from appearing if (pointerDownPoint) // touch sequence. This is a long press. { setIsLongPress(true); setOpen(true); stopTimeout(); } } // Reset hover timeout for context menu function handlePointerUpCapture(event: React.PointerEvent) { let pointerType = (event as any).pointerType || "n/a"; if (pointerType === "mouse") { setOpen(false); stopTimeout(); // Reset hover timeout for mouse } else { // pen, or touch. stopTimeout(); setOpen(false); if (isLongPress) { // If this is a long press, we want to leave the tooltip open for a while. setLongPressLeaving(true); } } } let effectiveTitle: React.ReactNode | null = null; let placement: "top-start" | "right" = "top-start"; if (valueTooltip !== undefined && !isLongPress) { effectiveTitle = valueTooltip; // Show value tooltip if available placement = "right"; } else { if (open || isLongPress) { effectiveTitle = title; // Show regular title if no value tooltip } } function handleClickCapture(event: React.MouseEvent) { setLongPressLeaving(false); setIsLongPress(false); setOpen(false); stopTimeout(); // Reset hover timeout on click } return (
{ // console.log("ToolTipEx: onClickCapture"); handleClickCapture(e); }} onMouseEnter={(e)=> { // console.log("ToolTipEx: onMouseEnter"); if (!longPressLeaving) {// Don't handle mouse enter if we're in a long press leaving state handleMouseEnter(e); } }} onMouseLeave={(e)=> { // console.log("ToolTipEx: onMouseLeave"); handleMouseLeave(e); }} onPointerCancelCapture={(e) => { // console.log("ToolTipEx: onPointerCancelCapture"); handlePointerCancel(e); }} onContextMenuCapture={(e) => { // console.log("ToolTipEx: onContextMenuCapture"); handleConextMenu(e); return false; }} onPointerDownCapture={(e) => { // console.log("ToolTipEx: onPointerDownCapture"); handlePointerDownCapture(e); }} onPointerMoveCapture={(e) => { // console.log("ToolTipEx: onPointerMoveCapture"); handlePointerMoveCapture(e); }} onPointerUpCapture={(e) => { // console.log("ToolTipEx: onPointerUpCapture"); handlePointerUpCapture(e); }} >
) } export default ToolTipEx;