-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathusePullToRefresh.ts
More file actions
88 lines (78 loc) · 2.71 KB
/
usePullToRefresh.ts
File metadata and controls
88 lines (78 loc) · 2.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { useState, useEffect, useRef, useCallback } from 'react';
export interface PullToRefreshOptions {
/** Callback when pull-to-refresh is triggered */
onRefresh: () => Promise<void>;
/** Minimum pull distance to trigger refresh (pixels) */
threshold?: number;
/** Whether pull-to-refresh is enabled */
enabled?: boolean;
}
/**
* Hook for implementing pull-to-refresh behavior.
* Returns a ref to attach to the scrollable container.
*/
export function usePullToRefresh<T extends HTMLElement = HTMLElement>(
options: PullToRefreshOptions,
) {
const { onRefresh, threshold = 80, enabled = true } = options;
const ref = useRef<T>(null);
const [isRefreshing, setIsRefreshing] = useState(false);
const [pullDistance, setPullDistance] = useState(0);
const startYRef = useRef(0);
const handleTouchStart = useCallback(
(e: TouchEvent) => {
if (!enabled || isRefreshing) return;
const el = ref.current;
if (el && el.scrollTop === 0) {
startYRef.current = e.touches[0].clientY;
}
},
[enabled, isRefreshing],
);
const handleTouchMove = useCallback(
(e: TouchEvent) => {
if (!enabled || isRefreshing || !startYRef.current) return;
const currentY = e.touches[0].clientY;
const diff = currentY - startYRef.current;
if (diff > 0) {
setPullDistance(Math.min(diff, threshold * 1.5));
}
},
[enabled, isRefreshing, threshold],
);
const handleTouchEnd = useCallback(async () => {
if (!enabled || isRefreshing) return;
// Capture distance and reset UI immediately to prevent lock during async refresh
const distance = pullDistance;
setPullDistance(0);
startYRef.current = 0;
if (distance >= threshold) {
setIsRefreshing(true);
try {
await onRefresh();
} finally {
setIsRefreshing(false);
}
}
}, [enabled, isRefreshing, pullDistance, threshold, onRefresh]);
useEffect(() => {
const el = ref.current;
if (!el || !enabled) return;
el.addEventListener('touchstart', handleTouchStart, { passive: true });
el.addEventListener('touchmove', handleTouchMove, { passive: true });
el.addEventListener('touchend', handleTouchEnd, { passive: true });
return () => {
el.removeEventListener('touchstart', handleTouchStart);
el.removeEventListener('touchmove', handleTouchMove);
el.removeEventListener('touchend', handleTouchEnd);
};
}, [handleTouchStart, handleTouchMove, handleTouchEnd, enabled]);
return { ref, isRefreshing, pullDistance };
}