Skip to content

Commit

Permalink
feat(pullrefresh): pullRefresh component
Browse files Browse the repository at this point in the history
  • Loading branch information
webyom committed Sep 14, 2020
1 parent 202b417 commit 8110db0
Show file tree
Hide file tree
Showing 8 changed files with 385 additions and 7 deletions.
2 changes: 2 additions & 0 deletions src/_site/scripts/root-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { OverlayRouteComponent } from '../../overlay/demo';
import { PasswordInputRouteComponent } from '../../password-input/demo';
import { PickerRouteComponent } from '../../picker/demo';
import { PopupRouteComponent } from '../../popup/demo';
import { PullRefreshRouteComponent } from '../../pull-refresh/demo';
import { RadioRouteComponent } from '../../radio-group/demo';
import { SkeletonRouteComponent } from '../../skeleton/demo';
import { StickyRouteComponent } from '../../sticky/demo';
Expand Down Expand Up @@ -65,6 +66,7 @@ export class RootComponent extends preact.Component {
<PasswordInputRouteComponent path="/password-input/" />
<PickerRouteComponent path="/picker/" />
<PopupRouteComponent path="/popup/" />
<PullRefreshRouteComponent path="/pull-refresh/" />
<RadioRouteComponent path="/radio/" />
<SkeletonRouteComponent path="/skeleton/" />
<StickyRouteComponent path="/sticky/" />
Expand Down
3 changes: 3 additions & 0 deletions src/_site/scripts/routes/home/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ export class HomeRouteComponent extends preact.Component {
<Link href="/overlay/">
Overlay <Arrow />
</Link>
<Link href="/pull-refresh/">
PullRefresh <Arrow />
</Link>
<Link href="/toast/">
Toast <Arrow />
</Link>
Expand Down
18 changes: 18 additions & 0 deletions src/pull-refresh/demo/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@import '../../styles/var.scss';

.demo-pull-refresh {
padding: $padding-md 0;
.pant-pull-refresh {
min-height: calc(100vh - 116px);
p {
margin: 0;
padding: 20px;
}
.doge {
width: 140px;
height: 72px;
margin-top: 8px;
border-radius: 4px;
}
}
}
89 changes: 89 additions & 0 deletions src/pull-refresh/demo/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import * as preact from 'preact';
import { toast } from '../../toast';
import { Tabs, Tab } from '../../tab';
import { PullRefresh } from '../../pull-refresh';
import { createBEM } from '../../utils/bem';
import { NavBar } from '../../_site/scripts/components/nav-bar';
import './index.scss';

const bem = createBEM('demo-pull-refresh');

const PullingDoge: preact.FunctionalComponent<{ distance?: number }> = props => (
<img
class="doge"
src="https://b.yzcdn.cn/vant/doge.png"
style={{ transform: `scale(${(props.distance || 80) / 80})` }}
/>
);

export class PullRefreshRouteComponent extends preact.Component<{}, { count: number }> {
state = {
count: 0,
};

render(): preact.JSX.Element {
return (
<preact.Fragment>
<NavBar title="PullRefresh" type="pull-refresh" />
<div className={bem()}>
<Tabs scrollable>
<Tab title="Basic Usage">
<PullRefresh
onRefresh={(): Promise<void> => {
return new Promise(resolve => {
setTimeout(() => {
this.setState({ count: this.state.count + 1 });
toast('Refresh success');
resolve();
}, 2000);
});
}}
>
<p>Refresh Count: {this.state.count}</p>
</PullRefresh>
</Tab>
<Tab title="Success Tip">
<PullRefresh
successText="Refresh success"
onRefresh={(): Promise<void> => {
return new Promise(resolve => {
setTimeout(() => {
this.setState({ count: this.state.count + 1 });
resolve();
}, 2000);
});
}}
>
<p>Refresh Count: {this.state.count}</p>
</PullRefresh>
</Tab>
<Tab title="Custom Tips">
<PullRefresh
headHeight="80"
pullingNode={<PullingDoge />}
loosingNode={<PullingDoge />}
loadingNode={<img src="https://b.yzcdn.cn/vant/doge-fire.jpg" class="doge" />}
onRefresh={(): Promise<void> => {
return new Promise(resolve => {
setTimeout(() => {
this.setState({ count: this.state.count + 1 });
toast('Refresh success');
resolve();
}, 2000);
});
}}
>
<p>Refresh Count: {this.state.count}</p>
</PullRefresh>
</Tab>
<Tab title="Disabled">
<PullRefresh disabled>
<p>Refresh Count: {this.state.count}</p>
</PullRefresh>
</Tab>
</Tabs>
</div>
</preact.Fragment>
);
}
}
25 changes: 25 additions & 0 deletions src/pull-refresh/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
@import '../styles/var.scss';

.pant-pull-refresh {
overflow: hidden;
user-select: none;

&__track {
position: relative;
height: 100%;
transition-property: transform;
}

&__head {
position: absolute;
left: 0;
width: 100%;
height: $pull-refresh-head-height;
overflow: hidden;
color: $pull-refresh-head-text-color;
font-size: $pull-refresh-head-font-size;
line-height: $pull-refresh-head-height;
text-align: center;
transform: translateY(-100%);
}
}
216 changes: 216 additions & 0 deletions src/pull-refresh/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import * as preact from 'preact';
import { addUnit } from '../utils';
import { preventDefault } from '../utils/event';
import { createBEM } from '../utils/bem';
import { getScroller, getScrollTop, ScrollElement } from '../utils/scroll';
import { TouchHandler } from '../utils/touch-handler';
import { Loading } from '../loading';
import './index.scss';

const DEFAULT_HEAD_HEIGHT = 50;

export type PullRefreshProps = {
disabled?: boolean;
successText?: string;
pullingText?: string;
loosingText?: string;
loadingText?: string;
successNode?: preact.VNode;
pullingNode?: preact.VNode;
loosingNode?: preact.VNode;
loadingNode?: preact.VNode;
successDuration?: number | string;
animationDuration?: number | string;
headHeight?: number | string;
onRefresh?(): Promise<void>;
};

type PullRefreshStatus = 'normal' | 'pulling' | 'loosing' | 'loading' | 'success';

type PullRefreshState = {
status: PullRefreshStatus;
distance: number;
};

const bem = createBEM('pant-pull-refresh');

export class PullRefresh extends preact.Component<PullRefreshProps, PullRefreshState> {
private containerRef = preact.createRef<HTMLDivElement>();
private ceiling = false;
private scroller: ScrollElement;
private touchHandler: TouchHandler;

constructor(props: PullRefreshProps) {
super(props);
this.state = {
status: 'normal',
distance: 0,
};
}

componentDidMount(): void {
this.scroller = getScroller(this.containerRef.current);
this.touchHandler = new TouchHandler(this.containerRef.current, {
onBeforeTouchStart: this.onBeforeTouchStart.bind(this),
onTouchMove: this.onTouchMove.bind(this),
onBeforeTouchMove: this.onBeforeTouchMove.bind(this),
onTouchEnd: this.onTouchEnd.bind(this),
});
}

componentWillUnmount(): void {
this.touchHandler.destroy();
this.touchHandler = null;
this.scroller = null;
}

private get touchable(): boolean {
const { status } = this.state;
return status !== 'loading' && status !== 'success' && !this.props.disabled;
}

private checkPullStart(event: TouchEvent): void {
this.ceiling = getScrollTop(this.scroller) === 0;

if (this.ceiling) {
this.touchHandler.touchStart(event);
}
}

private onBeforeTouchStart(event: TouchEvent): boolean | void {
if (!this.touchable) {
return false;
}
this.checkPullStart(event);
return false;
}

private onBeforeTouchMove(event: TouchEvent): boolean | void {
if (!this.touchable) {
return false;
}
if (!this.ceiling) {
this.checkPullStart(event);
}
return this.ceiling;
}

private onTouchMove(): void {
const { deltaY, direction } = this.touchHandler.state;
if (deltaY >= 0 && direction === 'vertical') {
preventDefault(event);
this.setStatus(this.ease(deltaY));
}
}

private onTouchEnd(): void {
const { status } = this.state;
const props = this.props;
const { headHeight, onRefresh, successDuration } = props;
const { deltaY } = this.touchHandler.state;
if (this.touchable && this.ceiling && deltaY) {
if (status === 'loosing' && onRefresh) {
this.setStatus(+headHeight, true);
onRefresh().finally(() => {
if (props.successNode || props.successText) {
this.setState({ status: 'success' }, () => {
setTimeout(() => {
this.setStatus(0);
}, +successDuration);
});
} else {
this.setStatus(0);
}
});
} else {
this.setStatus(0);
}
}
}

private ease(distance: number): number {
const headHeight = +this.props.headHeight;

if (distance > headHeight) {
if (distance < headHeight * 2) {
distance = headHeight + (distance - headHeight) / 2;
} else {
distance = headHeight * 1.5 + (distance - headHeight * 2) / 4;
}
}

return Math.round(distance);
}

private setStatus(distance: number, isLoading?: boolean): void {
let status: PullRefreshStatus;
if (isLoading) {
this.ceiling = false;
status = 'loading';
} else if (distance === 0) {
this.ceiling = false;
status = 'normal';
} else {
status = distance < this.props.headHeight ? 'pulling' : 'loosing';
}
this.setState({ status, distance });
}

private genStatus(): preact.VNode {
const props = this.props;
const { status, distance } = this.state;

if (status === 'success') {
return props.successNode || <div class={bem('text')}>{props.successText}</div>;
}
if (status === 'loading') {
return props.loadingNode || <Loading size="16">{props.loadingText}</Loading>;
}
if (status === 'loosing') {
return props.loosingNode || <div class={bem('text')}>{props.loosingText}</div>;
}
if (status === 'pulling') {
return (
(props.pullingNode && preact.cloneElement(props.pullingNode, { distance })) || (
<div class={bem('text')}>{props.pullingText}</div>
)
);
}
}

render(): preact.JSX.Element {
const { animationDuration, headHeight, children } = this.props;
const { distance } = this.state;
const duration = this.ceiling ? 0 : animationDuration;
const trackStyle = {
transitionDuration: `${duration}ms`,
transform: distance ? `translate3d(0,${distance}px, 0)` : '',
};
const headStyle =
headHeight !== DEFAULT_HEAD_HEIGHT
? {
height: addUnit(headHeight),
}
: null;

return (
<div ref={this.containerRef} class={bem()}>
<div class={bem('track')} style={trackStyle}>
<div class={bem('head')} style={headStyle}>
{this.genStatus()}
</div>
{children}
</div>
</div>
);
}
}

PullRefresh.defaultProps = {
pullingText: 'Pull to refresh...',
loosingText: 'Loose to refresh...',
loadingText: 'Loading...',
successDuration: 500,
animationDuration: 300,
headHeight: DEFAULT_HEAD_HEIGHT,
};
7 changes: 6 additions & 1 deletion src/styles/var.scss
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ $password-input-error-info-color: $red;
$password-input-dot-size: 10px;
$password-input-dot-color: $black;

// picker
// Picker
$picker-background-color: $white;
$picker-toolbar-height: 44px;
$picker-title-font-size: $font-size-lg;
Expand All @@ -314,6 +314,11 @@ $popup-close-icon-active-color: $gray-6;
$popup-close-icon-margin: 16px;
$popup-close-icon-z-index: 1;

// PullRefresh
$pull-refresh-head-height: 50px;
$pull-refresh-head-font-size: $font-size-md;
$pull-refresh-head-text-color: $gray-6;

// Skeleton
$skeleton-row-height: 16px;
$skeleton-row-background-color: $active-color;
Expand Down
Loading

0 comments on commit 8110db0

Please sign in to comment.