feat: multi-module iteration
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
import { useCallback, useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react';
|
||||
import './pull-to-refresh.css';
|
||||
|
||||
const PULL_THRESHOLD = 64;
|
||||
const MAX_PULL = 96;
|
||||
|
||||
type PullToRefreshProps = {
|
||||
onRefresh: () => void | Promise<void>;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
function getScrollTop() {
|
||||
return window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0;
|
||||
}
|
||||
|
||||
export default function PullToRefresh({
|
||||
onRefresh,
|
||||
children,
|
||||
className,
|
||||
style,
|
||||
disabled = false,
|
||||
}: PullToRefreshProps) {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const [pull, setPull] = useState(0);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const startY = useRef(0);
|
||||
const pulling = useRef(false);
|
||||
const busy = useRef(false);
|
||||
const pullRef = useRef(0);
|
||||
const disabledRef = useRef(disabled);
|
||||
const onRefreshRef = useRef(onRefresh);
|
||||
|
||||
disabledRef.current = disabled;
|
||||
onRefreshRef.current = onRefresh;
|
||||
pullRef.current = pull;
|
||||
|
||||
const finishRefresh = useCallback(async () => {
|
||||
if (busy.current) return;
|
||||
busy.current = true;
|
||||
setRefreshing(true);
|
||||
setPull(40);
|
||||
pullRef.current = 40;
|
||||
try {
|
||||
await onRefreshRef.current();
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
setPull(0);
|
||||
pullRef.current = 0;
|
||||
busy.current = false;
|
||||
pulling.current = false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const el = rootRef.current;
|
||||
if (!el) return;
|
||||
|
||||
const onTouchStart = (e: TouchEvent) => {
|
||||
if (disabledRef.current || busy.current) return;
|
||||
if (getScrollTop() > 2) return;
|
||||
startY.current = e.touches[0]?.clientY ?? 0;
|
||||
pulling.current = true;
|
||||
};
|
||||
|
||||
const onTouchMove = (e: TouchEvent) => {
|
||||
if (!pulling.current || disabledRef.current || busy.current) return;
|
||||
if (getScrollTop() > 2) {
|
||||
pulling.current = false;
|
||||
setPull(0);
|
||||
pullRef.current = 0;
|
||||
return;
|
||||
}
|
||||
const y = e.touches[0]?.clientY ?? 0;
|
||||
const delta = y - startY.current;
|
||||
if (delta <= 0) {
|
||||
setPull(0);
|
||||
pullRef.current = 0;
|
||||
return;
|
||||
}
|
||||
const next = Math.min(MAX_PULL, delta * 0.45);
|
||||
setPull(next);
|
||||
pullRef.current = next;
|
||||
if (next > 8 && e.cancelable) e.preventDefault();
|
||||
};
|
||||
|
||||
const onTouchEnd = () => {
|
||||
if (!pulling.current || disabledRef.current) return;
|
||||
pulling.current = false;
|
||||
if (pullRef.current >= PULL_THRESHOLD) {
|
||||
void finishRefresh();
|
||||
return;
|
||||
}
|
||||
setPull(0);
|
||||
pullRef.current = 0;
|
||||
};
|
||||
|
||||
el.addEventListener('touchstart', onTouchStart, { passive: true });
|
||||
el.addEventListener('touchmove', onTouchMove, { passive: false });
|
||||
el.addEventListener('touchend', onTouchEnd);
|
||||
el.addEventListener('touchcancel', onTouchEnd);
|
||||
return () => {
|
||||
el.removeEventListener('touchstart', onTouchStart);
|
||||
el.removeEventListener('touchmove', onTouchMove);
|
||||
el.removeEventListener('touchend', onTouchEnd);
|
||||
el.removeEventListener('touchcancel', onTouchEnd);
|
||||
};
|
||||
}, [finishRefresh]);
|
||||
|
||||
const showHint = pull > 8 || refreshing;
|
||||
const ready = pull >= PULL_THRESHOLD;
|
||||
const pad = showHint ? Math.max(pull, refreshing ? 40 : 0) : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={rootRef}
|
||||
className={['dukang-ptr', className].filter(Boolean).join(' ')}
|
||||
style={{ ...style, paddingTop: pad || undefined }}
|
||||
>
|
||||
<div
|
||||
className={`dukang-ptr-indicator${showHint ? ' dukang-ptr-indicator--visible' : ''}`}
|
||||
style={{ height: pad }}
|
||||
aria-live="polite"
|
||||
>
|
||||
<span className="dukang-ptr-text">
|
||||
{refreshing ? '刷新中…' : ready ? '松开刷新' : '下拉刷新'}
|
||||
</span>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user