75 lines
2.1 KiB
TypeScript
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(),
|
|
};
|
|
}
|