Files

75 lines
2.1 KiB
TypeScript

/**
* Minimal touch pull-to-refresh: track a downward drag from the top of the
* page, fire the reload past a 60px threshold, and expose the drag distance
* for a visual hint. Deliberately hand-rolled — no gesture library.
*/
export interface PullToRefresh {
/** Current damped drag distance in px; drive a translateY hint with it. */
pullDistance: Readonly<Ref<number>>;
/** True past the threshold while the finger is still down. */
armed: Readonly<Ref<boolean>>;
/** True while the reload callback runs. */
refreshing: Readonly<Ref<boolean>>;
onTouchStart: (event: TouchEvent) => void;
onTouchMove: (event: TouchEvent) => void;
onTouchEnd: () => void;
}
const THRESHOLD_PX = 60;
const MAX_PX = 96;
export function usePullToRefresh(onRefresh: () => Promise<void> | void): PullToRefresh {
const pullDistance = ref(0);
const armed = ref(false);
const refreshing = ref(false);
let startY = 0;
let tracking = false;
function touchStart(event: TouchEvent): void {
if (refreshing.value) return;
// Only pull when content is already scrolled to the top.
if (import.meta.client && window.scrollY > 0) return;
tracking = true;
startY = event.touches[0]?.clientY ?? 0;
armed.value = false;
}
function touchMove(event: TouchEvent): void {
if (!tracking || refreshing.value) return;
const delta = (event.touches[0]?.clientY ?? 0) - startY;
if (delta <= 0) {
pullDistance.value = 0;
armed.value = false;
return;
}
// Damp the drag so the content trails the finger.
pullDistance.value = Math.min(delta / 2, MAX_PX);
armed.value = pullDistance.value >= THRESHOLD_PX;
}
async function touchEnd(): Promise<void> {
if (!tracking) return;
tracking = false;
const trigger = armed.value;
armed.value = false;
pullDistance.value = 0;
if (!trigger) return;
refreshing.value = true;
try {
await onRefresh();
} finally {
refreshing.value = false;
}
}
return {
pullDistance,
armed,
refreshing,
onTouchStart: touchStart,
onTouchMove: touchMove,
onTouchEnd: () => void touchEnd(),
};
}