feat(FN-5482): suppress touch-synthesized mouse events on overlay dismiss
Adds a `useOverlayDismiss` hook that suppresses touch-synthesized mouse events on modal/dropdown overlays to prevent unintended close behavior on touch devices, with tests covering TaskCard dismissal and overlay interaction edge cases. Documentation updates in AGENTS.md and docs/architecture.md capt Fusion-Task-Id: FN-5482 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> Fusion-Task-Id: FN-5482
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useRef } from "react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
/**
|
||||
* Returns props for a modal-overlay element that dismisses only when a real
|
||||
@@ -18,13 +18,42 @@ import { useCallback, useRef } from "react";
|
||||
export function useOverlayDismiss(onClose: () => void): {
|
||||
onMouseDown: (e: React.MouseEvent) => void;
|
||||
onMouseUp: (e: React.MouseEvent) => void;
|
||||
onTouchStart: () => void;
|
||||
onTouchEnd: () => void;
|
||||
} {
|
||||
const startedOnOverlayRef = useRef(false);
|
||||
const lastTouchAtRef = useRef(0);
|
||||
|
||||
const markTouch = useCallback(() => {
|
||||
lastTouchAtRef.current = Date.now();
|
||||
}, []);
|
||||
|
||||
const onMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
// Android/webview may emit compatibility mouse events right after touchend.
|
||||
// Ignore those so a newly-mounted overlay is not dismissed immediately.
|
||||
if (Date.now() - lastTouchAtRef.current < 500) {
|
||||
startedOnOverlayRef.current = false;
|
||||
return;
|
||||
}
|
||||
startedOnOverlayRef.current = e.target === e.currentTarget;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof document === "undefined") return;
|
||||
|
||||
const handleDocumentTouch = () => {
|
||||
lastTouchAtRef.current = Date.now();
|
||||
};
|
||||
|
||||
document.addEventListener("touchstart", handleDocumentTouch, { passive: true });
|
||||
document.addEventListener("touchend", handleDocumentTouch, { passive: true });
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("touchstart", handleDocumentTouch);
|
||||
document.removeEventListener("touchend", handleDocumentTouch);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onMouseUp = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
const shouldClose = startedOnOverlayRef.current && e.target === e.currentTarget;
|
||||
@@ -34,5 +63,5 @@ export function useOverlayDismiss(onClose: () => void): {
|
||||
[onClose],
|
||||
);
|
||||
|
||||
return { onMouseDown, onMouseUp };
|
||||
return { onMouseDown, onMouseUp, onTouchStart: markTouch, onTouchEnd: markTouch };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user