diff --git a/docs/examples-docs/pull-refresh.md b/docs/examples-docs/pull-refresh.md
new file mode 100644
index 000000000..46d9c1288
--- /dev/null
+++ b/docs/examples-docs/pull-refresh.md
@@ -0,0 +1,115 @@
+
+
+
+
+## PullRefresh 下拉刷新
+
+### 使用指南
+``` javascript
+import { PullRefresh } from 'vant';
+
+Vue.component(PullRefresh.name, PullRefresh);
+```
+
+### 代码演示
+
+:::demo
+```html
+
+
+ 刷新次数: {{ count }}
+
+```
+
+```javascript
+export default {
+ data() {
+ return {
+ count: 0,
+ isLoading: false
+ }
+ },
+
+ watch: {
+ isLoading() {
+ if (this.isLoading) {
+ setTimeout(() => {
+ Toast('刷新成功');
+ this.isLoading = false;
+ this.count++;
+ }, 500);
+ }
+ }
+ }
+}
+```
+:::
+
+### API
+
+| 参数 | 说明 | 类型 | 默认值 | 可选值 |
+|-----------|-----------|-----------|-------------|-------------|
+| v-model | 是否在加载中 | `Boolean` | - | - |
+| pullingText | 下拉过程中顶部文案 | `String` | `下拉即可刷新...` | - |
+| loosingText | 释放过程中顶部文案 | `String` | `释放即可刷新...` | - |
+| loadingText | 加载过程中顶部文案 | `String` | `加载中...` | - |
+| animationDuration | 动画时长 | `Number` | `300` | - |
+| headHeight | 顶部内容高度 | `Number` | `50` | - |
+
+### Slot
+
+| name | 描述 |
+|-----------|-----------|
+| - | 自定义内容 |
+| normal | 非下拉状态时顶部内容 |
+| pulling | 下拉过程中顶部内容 |
+| loosing | 释放过程中顶部内容 |
+| loading | 加载过程中顶部内容 |
diff --git a/docs/src/doc.config.js b/docs/src/doc.config.js
index 9f86fe1f1..b51fa8856 100644
--- a/docs/src/doc.config.js
+++ b/docs/src/doc.config.js
@@ -176,6 +176,10 @@ module.exports = {
"path": "/picker",
"title": "Picker 选择器"
},
+ {
+ "path": "/pull-refresh",
+ "title": "PullRefresh 下拉刷新"
+ },
{
"path": "/toast",
"title": "Toast 轻提示"
diff --git a/docs/src/iframe-router.js b/docs/src/iframe-router.js
index fa3015314..f02baddaa 100644
--- a/docs/src/iframe-router.js
+++ b/docs/src/iframe-router.js
@@ -23,18 +23,19 @@ window.changePath = function(path) {
function iframeReady(iframe, callback) {
const doc = iframe.contentDocument || iframe.contentWindow.document;
+ const interval = () => {
+ if (iframe.contentWindow.changePath) {
+ callback();
+ } else {
+ setTimeout(() => {
+ interval();
+ }, 50);
+ }
+ };
+
if (doc.readyState === 'complete') {
- callback();
+ interval();
} else {
- const interval = () => {
- if (iframe.contentWindow.changePath) {
- callback();
- } else {
- setTimeout(() => {
- interval();
- }, 50);
- }
- };
iframe.onload = interval;
}
}
diff --git a/packages/index.js b/packages/index.js
index 4028db500..02fff5ae4 100644
--- a/packages/index.js
+++ b/packages/index.js
@@ -32,6 +32,7 @@ import PayOrder from './pay-order';
import Picker from './picker';
import Popup from './popup';
import Progress from './progress';
+import PullRefresh from './pull-refresh';
import Quantity from './quantity';
import Radio from './radio';
import RadioGroup from './radio-group';
@@ -83,6 +84,7 @@ const components = [
Picker,
Popup,
Progress,
+ PullRefresh,
Quantity,
Radio,
RadioGroup,
@@ -150,6 +152,7 @@ export {
Picker,
Popup,
Progress,
+ PullRefresh,
Quantity,
Radio,
RadioGroup,
diff --git a/packages/pull-refresh/index.vue b/packages/pull-refresh/index.vue
new file mode 100644
index 000000000..ddc10158b
--- /dev/null
+++ b/packages/pull-refresh/index.vue
@@ -0,0 +1,176 @@
+
+
+
+
+
+ {{ pullingText }}
+
+
+ {{ loosingText }}
+
+
+
+
+ {{ loadingText }}
+
+
+
+
+
+
+
+
diff --git a/packages/vant-css/src/index.css b/packages/vant-css/src/index.css
index af2da423d..4c8d5576f 100644
--- a/packages/vant-css/src/index.css
+++ b/packages/vant-css/src/index.css
@@ -38,6 +38,7 @@
@import './actionsheet.css';
@import './dialog.css';
@import './picker.css';
+@import './pull-refresh.css';
@import './toast.css';
/* business components */
diff --git a/packages/vant-css/src/pull-refresh.css b/packages/vant-css/src/pull-refresh.css
new file mode 100644
index 000000000..f2514c121
--- /dev/null
+++ b/packages/vant-css/src/pull-refresh.css
@@ -0,0 +1,37 @@
+@import './common/var.css';
+
+.van-pull-refresh {
+ user-select: none;
+ position: relative;
+
+ &__head {
+ width: 100%;
+ height: 50px;
+ left: 0;
+ overflow: hidden;
+ position: absolute;
+ text-align: center;
+ top: -50px;
+ font-size: 14px;
+ color: $gray-dark;
+ line-height: 50px;
+ }
+
+ &__loading {
+ .van-loading {
+ width: 16px;
+ height: 16px;
+ display: inline-block;
+ margin-right: 10px;
+ }
+
+ span,
+ .van-loading {
+ vertical-align: middle;
+ }
+ }
+
+ &__text {
+ display: block;
+ }
+}
\ No newline at end of file
diff --git a/test/unit/specs/pull-refresh.spec.js b/test/unit/specs/pull-refresh.spec.js
new file mode 100644
index 000000000..89548dc9f
--- /dev/null
+++ b/test/unit/specs/pull-refresh.spec.js
@@ -0,0 +1,129 @@
+import PullRefresh from 'packages/pull-refresh';
+import { mount } from 'avoriaz';
+import { triggerTouch } from '../utils';
+
+describe('PullRefresh', () => {
+ let wrapper;
+ afterEach(() => {
+ wrapper && wrapper.destroy();
+ });
+
+ it('create a PullRefresh', () => {
+ wrapper = mount(PullRefresh, {
+ propsData: {
+ value: false
+ }
+ });
+
+ expect(wrapper.hasClass('van-pull-refresh')).to.be.true;
+ });
+
+ it('change head content when pulling down', (done) => {
+ wrapper = mount(PullRefresh, {
+ propsData: {
+ value: false
+ }
+ });
+
+ triggerTouch(wrapper, 'touchstart', 0, 0);
+ triggerTouch(wrapper, 'touchmove', 0, 10);
+
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.find('.van-pull-refresh__text')[0].text()).to.equal('下拉即可刷新...');
+
+ triggerTouch(wrapper, 'touchmove', 0, 30);
+ triggerTouch(wrapper, 'touchmove', 0, 60);
+ triggerTouch(wrapper, 'touchmove', 0, 100);
+
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.find('.van-pull-refresh__text')[0].text()).to.equal('释放即可刷新...');
+
+ triggerTouch(wrapper, 'touchend', 0, 100);
+
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.find('.van-pull-refresh__loading span')[1].text()).to.equal('加载中...');
+ done();
+ });
+ });
+ });
+ });
+
+ it('change loading status when pulling down', (done) => {
+ wrapper = mount(PullRefresh, {
+ propsData: {
+ value: false
+ }
+ });
+
+ wrapper.vm.$on('input', value => {
+ wrapper.vm.value = value;
+
+ setTimeout(() => {
+ wrapper.vm.value = false;
+ setTimeout(() => {
+ expect(wrapper.vm.status).to.equal('normal');
+ done();
+ }, 0);
+ }, 30);
+ });
+
+ triggerTouch(wrapper, 'touchstart', 0, 0);
+ triggerTouch(wrapper, 'touchmove', 0, 100);
+ triggerTouch(wrapper, 'touchend', 0, 100);
+
+ expect(wrapper.vm.value).to.be.true;
+ expect(wrapper.vm.status).to.equal('loading');
+
+ // ignore touch event when loading
+ triggerTouch(wrapper, 'touchstart', 0, 0);
+ triggerTouch(wrapper, 'touchmove', 0, 100);
+ triggerTouch(wrapper, 'touchend', 0, 100);
+ });
+
+ it('pull a short distance', () => {
+ wrapper = mount(PullRefresh, {
+ propsData: {
+ value: false
+ }
+ });
+
+ triggerTouch(wrapper, 'touchstart', 0, 0);
+ triggerTouch(wrapper, 'touchmove', 0, 10);
+ triggerTouch(wrapper, 'touchend', 0, 10);
+
+ expect(wrapper.vm.value).to.be.false;
+ expect(wrapper.vm.status).to.equal('normal');
+ });
+
+ it('not in page top', () => {
+ wrapper = mount(PullRefresh, {
+ propsData: {
+ value: false
+ }
+ });
+
+ window.scrollY = 100;
+
+ // ignore touch event when not at page top
+ triggerTouch(wrapper, 'touchstart', 0, 0);
+ triggerTouch(wrapper, 'touchmove', 0, 100);
+ triggerTouch(wrapper, 'touchend', 0, 100);
+ expect(wrapper.vm.ceiling).to.be.false;
+
+ window.scrollY = 0;
+ triggerTouch(wrapper, 'touchmove', 0, 100);
+ expect(wrapper.vm.ceiling).to.be.true;
+ });
+
+ it('horizontal direction', () => {
+ wrapper = mount(PullRefresh, {
+ propsData: {
+ value: false
+ }
+ });
+ triggerTouch(wrapper, 'touchstart', 0, 0);
+ triggerTouch(wrapper, 'touchmove', 10, 0);
+ triggerTouch(wrapper, 'touchend', 10, 0);
+ expect(wrapper.vm.direction).to.equal('horizontal');
+ });
+});
diff --git a/test/unit/specs/tag.spec.js b/test/unit/specs/tag.spec.js
index 984beab2e..8dc6ca4e0 100644
--- a/test/unit/specs/tag.spec.js
+++ b/test/unit/specs/tag.spec.js
@@ -16,6 +16,6 @@ describe('Tag', () => {
propsData: {
type: 'primary'
}
- })
+ });
});
});