Files
op-pedal/vite/src/pipedal/ToolTipEx.tsx
T
2025-06-29 07:44:39 -04:00

242 lines
9.1 KiB
TypeScript

/*
* 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<typeof Tooltip> {
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<number>(0);
let [timeoutInstance, setTimeoutInstance] = React.useState<number>(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<HTMLDivElement>) {
setOpen(false);
startTimeout(hoverTimeout);
}
function handleMouseLeave(event: React.MouseEvent<HTMLDivElement>) {
setOpen(false);
stopTimeout();
setLongPressLeaving(false);
}
function handlePointerDownCapture(event: React.PointerEvent<HTMLDivElement>) {
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<HTMLDivElement>) {
setOpen(false);
setIsLongPress(false);
stopTimeout();
setPointerdownPoint(null); // Reset the mouse down point
}
function handlePointerMoveCapture(event: React.PointerEvent<HTMLDivElement>) {
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<HTMLDivElement>) {
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<HTMLDivElement>) {
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<HTMLDivElement>) {
setLongPressLeaving(false);
setIsLongPress(false);
setOpen(false);
stopTimeout(); // Reset hover timeout on click
}
return (
<div style={{ display: 'inline-block' }}
onClickCapture={(e) => {
// 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);
}}
>
<Tooltip {...extras}
open={open || isLongPress || valueTooltip !== undefined}
disableInteractive={true}
disableTouchListener={true}
disableHoverListener={true}
disableFocusListener={true}
title={effectiveTitle}
placement={ placement}
arrow enterDelay={1500} enterNextDelay={1500}
slotProps={{
transition: {
timeout: 0 // Disable transition for the tooltip
}
}}
/>
</div>
)
}
export default ToolTipEx;