37 lines
1.5 KiB
TypeScript
37 lines
1.5 KiB
TypeScript
import { RefObject, useCallback, useEffect, useRef } from 'react';
|
|||
|
|
import { useMediaQuery } from './useMediaQuery';
|
||
|
|
|
||
|
|
const RECENT_MS = 700;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* [Gitea #147] Tells whether a click was produced by a finger. Records the
|
||
|
|
* last touch inside `ref`; `wasTouch()` is true for a short window after it.
|
||
|
|
* Keyboard activation and screen-reader activation (TalkBack/VoiceOver send a
|
||
|
|
* bare click, no touch events) report false, so they keep one-step behaviour.
|
||
|
|
* Always false on devices without a coarse pointer.
|
||
|
|
*/
|
||
|
|
export function useRecentTouch(ref: RefObject<HTMLElement | null>) {
|
||
|
|
const coarse = useMediaQuery('(pointer: coarse)');
|
||
|
|
const last = useRef(0);
|
||
|
|
|
||
|
|
// Listen on the document (the ref'd element may be remounted, e.g. on a tab
|
||
|
|
// switch) and only count touches that land inside it.
|
||
|
|
useEffect(() => {
|
||
|
|
if (!coarse) return undefined;
|
||
|
|
const mark = (evt: TouchEvent) => {
|
||
|
|
const el = ref.current;
|
||
|
|
if (el && evt.target instanceof Node && !el.contains(evt.target)) return;
|
||
|
|
last.current = Date.now();
|
||
|
|
};
|
||
|
|
document.addEventListener('touchstart', mark, { passive: true, capture: true });
|
||
|
|
document.addEventListener('touchend', mark, { passive: true, capture: true });
|
||
|
|
return () => {
|
||
|
|
document.removeEventListener('touchstart', mark, { capture: true });
|
||
|
|
document.removeEventListener('touchend', mark, { capture: true });
|
||
|
|
};
|
||
|
|
}, [ref, coarse]);
|
||
|
|
|
||
|
|
const wasTouch = useCallback(() => coarse && Date.now() - last.current < RECENT_MS, [coarse]);
|
||
|
|
return { coarse, wasTouch };
|
||
|
|
}
|