Skip to content

Instantly share code, notes, and snippets.

@mrchaofan
Created June 9, 2022 11:25
Show Gist options
  • Save mrchaofan/d6ff24368711927c958cbbae94045d73 to your computer and use it in GitHub Desktop.
Save mrchaofan/d6ff24368711927c958cbbae94045d73 to your computer and use it in GitHub Desktop.
// https://codesandbox.io/s/pull-to-refresh-ljetin
import React from "react";
import ReactDOM from "react-dom";
import { Motion, spring, presets } from "react-motion";
function getScrollElement(el: Element): Window | Element {
let ret: Window | Element = window;
let current = el;
while (current && current !== document.body) {
const { overflowY } = window.getComputedStyle(current);
if (overflowY === "scroll" || overflowY === "auto") {
ret = current;
break;
}
current = current.parentElement;
}
return ret;
}
function getScrollTop(e: Element | Window) {
return e === window ? e.pageYOffset : (e as Element).scrollTop;
}
export interface IPullToRefreshProps {
isRefreshing: boolean;
onRefresh(): void;
renderPullView: (
pullInfo: { progress: number },
ref: React.RefCallback<HTMLElement>
) => React.ReactElement;
children?: React.ReactNode;
}
interface IPullInfo {
lastPull: number;
moved: boolean;
startY: number | null;
}
function createScheduler() {
let sched: number;
function cancel() {
clearTimeout(sched);
}
function schedule(fn: () => void, t: number) {
cancel();
sched = setTimeout(fn, t);
}
return {
schedule,
cancel
};
}
export default class PullToRefresh extends React.Component<
IPullToRefreshProps,
{
pullDistance: number;
}
> {
listView?: HTMLElement;
pullView?: HTMLElement;
pullHeight?: number;
scrollParent?: Element | Window;
unmounted = false;
inPTR = false;
pullInfo: IPullInfo = {
lastPull: 0,
moved: false,
startY: null
};
state = {
pullDistance: 0
};
scheduler = createScheduler();
getListViewRef: React.RefCallback<HTMLElement> = (listView) => {
this.listView = listView;
};
getPullViewRef: React.RefCallback<HTMLElement> = (pullView) => {
this.pullView = pullView;
};
handleScroll = () =>
window.requestAnimationFrame(() => {
if (this.getSrollTop() === 0) {
this.enablePTR();
} else {
this.disablePTR();
}
});
getScrollElement = () => {
return (
this.scrollParent ||
(this.scrollParent = getScrollElement(
ReactDOM.findDOMNode(this) as Element
))
);
};
enablePTRIfNeeded = () => {
if (!this.unmounted) {
const scrollTop = this.getSrollTop();
if (scrollTop <= 0) {
this.enablePTR();
}
}
};
enablePTR = () => {
if (!this.inPTR) {
this.inPTR = true;
const el = this.listView;
if (el) {
el.addEventListener("touchstart", this.handleTouchStart);
el.addEventListener("touchmove", this.handleTouchMove);
el.addEventListener("touchend", this.handleTouchEnd);
el.style.overflow = "visible";
}
}
};
disablePTR = () => {
this.pullInfo.moved = false;
if (this.inPTR) {
this.inPTR = false;
const el = this.listView;
if (el) {
el.removeEventListener("touchstart", this.handleTouchStart);
el.removeEventListener("touchmove", this.handleTouchMove);
el.removeEventListener("touchend", this.handleTouchEnd);
}
this.setState({
pullDistance: 0
});
}
};
handleTouchStart = (event: TouchEvent) => {
this.pullInfo.startY = event.touches[0].clientY;
};
handleTouchMove = (event: TouchEvent) => {
if (this.pullInfo.startY == null || event.defaultPrevented) {
return;
}
const clientY = event.touches[0].clientY;
const pullDistance = Math.round(0.4 * (clientY - this.pullInfo.startY));
if (pullDistance <= 0 && this.state.pullDistance === 0) {
return;
}
if (pullDistance > 0) {
event.preventDefault();
}
this.updatePullPosition(pullDistance);
};
handleTouchEnd = (event: TouchEvent) => {
if (this.pullInfo.moved) {
if (this.state.pullDistance > this.pullHeight) {
event.preventDefault();
this.startRefreshing();
} else {
this.resetPullInfo();
}
}
};
startRefreshing = () => {
if (this.state.pullDistance > this.pullHeight) {
this.disablePTR();
if (!this.props.isRefreshing) {
this.props.onRefresh();
}
this.setState({
pullDistance: this.pullHeight
});
this.scheduler.schedule(this.stopRefresing, 1000);
}
};
stopRefresing = () => {
this.disablePTR();
this.resetPullInfo();
requestAnimationFrame(this.enablePTRIfNeeded);
};
getSrollTop = () => {
const el = this.getScrollElement();
return el ? getScrollTop(el) : 0;
};
resetPullInfo = () => {
this.pullInfo = {
lastPull: 0,
moved: false,
startY: null
};
this.setState({
pullDistance: 0
});
};
updatePullPosition = (pullDistance: number) => {
this.pullInfo.moved = true;
this.setState({
pullDistance
});
};
render() {
window["ptr"] = this;
const progress =
this.pullHeight != null ? this.state.pullDistance / this.pullHeight : 0;
return (
<Motion
style={{
pullDistance: spring(this.state.pullDistance, presets.stiff)
}}
>
{({ pullDistance }) => {
const pullView = this.props.renderPullView(
{
progress
},
this.getPullViewRef
);
return (
<div
className="ptr-container"
ref={this.getListViewRef}
style={
pullDistance > 0
? {
transform: `translate3d(0,${pullDistance}px,0)`
}
: {}
}
>
{pullView}
{this.props.children}
</div>
);
}}
</Motion>
);
}
componentDidMount() {
this.pullHeight = this.pullView.getBoundingClientRect().height;
this.resetPullInfo();
const el = this.getScrollElement();
if (el) {
el.addEventListener("scroll", this.handleScroll);
}
requestAnimationFrame(this.enablePTRIfNeeded);
}
componentWillReceiveProps(nextProps: IPullToRefreshProps) {
if (!this.props.isRefreshing && nextProps.isRefreshing) {
this.scheduler.cancel();
} else if (this.props.isRefreshing && !nextProps.isRefreshing) {
this.stopRefresing();
}
}
componentWillUnmount() {
this.unmounted = true;
const el = this.getScrollElement();
if (el) {
el.removeEventListener("scroll", this.handleScroll);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment