diff --git a/src/index-bar/test/__snapshots__/demo.spec.js.snap b/src/index-bar/test/__snapshots__/demo.spec.js.snap
index cc9e1bd60..6be943e5e 100644
--- a/src/index-bar/test/__snapshots__/demo.spec.js.snap
+++ b/src/index-bar/test/__snapshots__/demo.spec.js.snap
@@ -5,8 +5,8 @@ exports[`renders demo correctly 1`] = `
-
基础用法
-
自定义索引列表
+
基础用法
+
自定义索引列表
diff --git a/src/list/test/__snapshots__/demo.spec.js.snap b/src/list/test/__snapshots__/demo.spec.js.snap
index bf71907e7..41e123a54 100644
--- a/src/list/test/__snapshots__/demo.spec.js.snap
+++ b/src/list/test/__snapshots__/demo.spec.js.snap
@@ -5,8 +5,8 @@ exports[`renders demo correctly 1`] = `
-
基础用法
-
错误提示
+
基础用法
+
错误提示
diff --git a/src/tab/README.md b/src/tab/README.md
index 5748ec380..0538f36a9 100644
--- a/src/tab/README.md
+++ b/src/tab/README.md
@@ -170,6 +170,18 @@ In swipeable mode, you can switch tabs with swipe gestrue in the content
```
+### Scrollspy
+
+In scrollspy mode, the list of content will be tiled
+
+```html
+
+
+ content {{ index }}
+
+
+```
+
## API
### Tabs Props
@@ -193,6 +205,7 @@ In swipeable mode, you can switch tabs with swipe gestrue in the content
| sticky | Whether to use sticky mode | *boolean* | `false` | - |
| swipeable | Whether to switch tabs with swipe gestrue in the content | *boolean* | `false` | - |
| lazy-render | Whether to enable tab content lazy render | *boolean* | `true` | - |
+| scrollspy | Whether to use scrollspy mode | *boolean* | `false` | - |
### Tab Props
diff --git a/src/tab/README.zh-CN.md b/src/tab/README.zh-CN.md
index e316d1ef7..32aba02f6 100644
--- a/src/tab/README.zh-CN.md
+++ b/src/tab/README.zh-CN.md
@@ -174,6 +174,18 @@ export default {
```
+### 滚动导航
+
+通过`scrollspy`属性可以开启滚动导航模式,该模式下,内容将会平铺展示
+
+```html
+
+
+ 内容 {{ index }}
+
+
+```
+
## API
### Tabs Props
@@ -197,6 +209,7 @@ export default {
| sticky | 是否使用粘性定位布局 | *boolean* | `false` | - |
| swipeable | 是否开启手势滑动切换 | *boolean* | `false` | - |
| lazy-render | 是否开启标签页内容延迟渲染 | *boolean* | `true` | - |
+| scrollspy | 是否开启滚动导航 | *boolean* | `false` | - |
### Tab Props
diff --git a/src/tab/demo/index.vue b/src/tab/demo/index.vue
index 9bd3c8d3c..73a03ca3d 100644
--- a/src/tab/demo/index.vue
+++ b/src/tab/demo/index.vue
@@ -93,6 +93,14 @@
+
+
+
+
+ {{ $t('content') }} {{ index }}
+
+
+
@@ -109,6 +117,7 @@ export default {
title7: '自定义标签',
title8: '切换动画',
title9: '滑动切换',
+ title10: '滚动导航',
disabled: ' 已被禁用',
matchByName: '通过名称匹配'
},
@@ -123,6 +132,7 @@ export default {
title7: 'Custom Tab',
title8: 'Switch Animation',
title9: 'Swipeable',
+ title10: 'Scrollspy',
disabled: ' is disabled',
matchByName: 'Match By Name'
}
diff --git a/src/tab/index.js b/src/tab/index.js
index 7ddc50834..af73128c9 100644
--- a/src/tab/index.js
+++ b/src/tab/index.js
@@ -46,7 +46,8 @@ export default createComponent({
render(h) {
const { slots, isActive } = this;
- const shouldRender = this.inited || !this.parent.lazyRender;
+ const shouldRender = this.inited || this.parent.scrollspy || !this.parent.lazyRender;
+ const show = this.parent.scrollspy || isActive;
const Content = shouldRender ? slots() : h();
if (this.parent.animated) {
@@ -62,7 +63,7 @@ export default createComponent({
}
return (
-
+
{Content}
);
diff --git a/src/tab/test/__snapshots__/demo.spec.js.snap b/src/tab/test/__snapshots__/demo.spec.js.snap
index 8247d22c4..8362244ea 100644
--- a/src/tab/test/__snapshots__/demo.spec.js.snap
+++ b/src/tab/test/__snapshots__/demo.spec.js.snap
@@ -275,5 +275,60 @@ exports[`renders demo correctly 1`] = `
+
diff --git a/src/tab/test/index.spec.js b/src/tab/test/index.spec.js
index 02a66f49b..a7471ebf1 100644
--- a/src/tab/test/index.spec.js
+++ b/src/tab/test/index.spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import Tab from '..';
import Tabs from '../../tabs';
-import { mount, later, triggerDrag } from '../../../test';
+import { mount, later, triggerDrag, mockScrollTop } from '../../../test';
Vue.use(Tab);
Vue.use(Tabs);
@@ -263,3 +263,33 @@ test('info prop', () => {
expect(wrapper).toMatchSnapshot();
});
+
+test('scrollspy', async () => {
+ const onChange = jest.fn();
+ window.scrollTo = jest.fn();
+
+ const wrapper = mount({
+ template: `
+
+ Text
+ Text
+ Text
+
+ `,
+ methods: {
+ onChange
+ }
+ });
+
+ await later();
+ expect(wrapper).toMatchSnapshot();
+
+ const tabs = wrapper.findAll('.van-tab');
+ tabs.at(2).trigger('click');
+ expect(onChange).toHaveBeenCalledWith('c', 'title3');
+
+ await later();
+ mockScrollTop(100);
+ expect(wrapper).toMatchSnapshot();
+ expect(onChange).toHaveBeenCalledWith('c', 'title3');
+});
diff --git a/src/tabs/index.js b/src/tabs/index.js
index 1a5eac26e..bc7789fd8 100644
--- a/src/tabs/index.js
+++ b/src/tabs/index.js
@@ -1,11 +1,12 @@
import { createNamespace, isDef, addUnit } from '../utils';
-import { scrollLeftTo } from './utils';
+import { scrollLeftTo, scrollTopTo } from './utils';
import { route } from '../utils/router';
import { isHidden } from '../utils/dom/style';
+import { on, off } from '../utils/dom/event';
import { ParentMixin } from '../mixins/relation';
import { BindEventMixin } from '../mixins/bind-event';
import { BORDER_TOP_BOTTOM } from '../utils/constant';
-import { setRootScrollTop, getElementTop } from '../utils/dom/scroll';
+import { setRootScrollTop, getElementTop, getVisibleHeight, getVisibleTop } from '../utils/dom/scroll';
import Title from './Title';
import Content from './Content';
import Sticky from '../sticky';
@@ -17,6 +18,9 @@ export default createComponent({
ParentMixin('vanTabs'),
BindEventMixin(function(bind) {
bind(window, 'resize', this.resize, true);
+ if (this.scrollspy) {
+ bind(window, 'scroll', this.onScrollspyScroll, true);
+ }
})
],
@@ -29,6 +33,7 @@ export default createComponent({
sticky: Boolean,
animated: Boolean,
swipeable: Boolean,
+ scrollspy: Boolean,
background: String,
lineWidth: [Number, String],
lineHeight: [Number, String],
@@ -97,6 +102,13 @@ export default createComponent({
if (activeTab) {
return activeTab.computedName;
}
+ },
+
+ scrollOffset() {
+ if (this.sticky) {
+ return this.offsetTop + this.tabHeight;
+ }
+ return 0;
}
},
@@ -123,9 +135,17 @@ export default createComponent({
this.setLine();
// scroll to correct position
- if (this.stickyFixed) {
+ if (this.stickyFixed && !this.scrollspy) {
setRootScrollTop(Math.ceil(getElementTop(this.$el) - this.offsetTop));
}
+ },
+
+ scrollspy(val) {
+ if (val) {
+ on(window, 'scroll', this.onScrollspyScroll, true);
+ } else {
+ off(window, 'scroll', this.onScrollspyScroll);
+ }
}
},
@@ -147,6 +167,7 @@ export default createComponent({
onShow() {
this.$nextTick(() => {
this.inited = true;
+ this.tabHeight = getVisibleHeight(this.$refs.wrap);
this.scrollIntoView(true);
});
},
@@ -236,6 +257,7 @@ export default createComponent({
this.$emit('disabled', computedName, title);
} else {
this.setCurrentIndex(index);
+ this.scrollToCurrentContent();
this.$emit('click', computedName, title);
}
},
@@ -258,6 +280,44 @@ export default createComponent({
onScroll(params) {
this.stickyFixed = params.isFixed;
this.$emit('scroll', params);
+ },
+
+ scrollToCurrentContent() {
+ if (this.scrollspy) {
+ this.clickedScroll = true;
+ const instance = this.children[this.currentIndex];
+ const el = instance && instance.$el;
+ if (el) {
+ const to = Math.ceil(getElementTop(el)) - this.scrollOffset;
+ scrollTopTo(to, this.duration, () => {
+ this.clickedScroll = false;
+ });
+ }
+ }
+ },
+
+ onScrollspyScroll() {
+ if (this.scrollspy && !this.clickedScroll) {
+ const index = this.getCurrentIndexOnScroll();
+ this.setCurrentIndex(index);
+ }
+ },
+
+ getCurrentIndexOnScroll() {
+ let i;
+
+ for (i = 0; i < this.children.length; i++) {
+ const top = getVisibleTop(this.children[i].$el);
+
+ if (top > this.scrollOffset) {
+ if (i === 0) {
+ return 0;
+ }
+ return i - 1;
+ }
+ }
+
+ return i - 1;
}
},
diff --git a/src/tabs/utils.ts b/src/tabs/utils.ts
index 2b4e96e99..496ce2172 100644
--- a/src/tabs/utils.ts
+++ b/src/tabs/utils.ts
@@ -1,4 +1,5 @@
import { raf } from '../utils/dom/raf';
+import { getRootScrollTop, setRootScrollTop } from '../utils/dom/scroll';
export function scrollLeftTo(el: HTMLElement, to: number, duration: number) {
let count = 0;
@@ -15,3 +16,28 @@ export function scrollLeftTo(el: HTMLElement, to: number, duration: number) {
animate();
}
+
+export function scrollTopTo(to: number, duration: number, cb: Function) {
+ let current = getRootScrollTop();
+ const toDown = current < to;
+ const frames = duration === 0 ? 1 : Math.round((duration * 1000) / 16);
+ const pxPerFrames = (to - current) / frames;
+
+ function animate() {
+ current += pxPerFrames;
+
+ if ((toDown && current > to) || (!toDown && current < to)) {
+ current = to;
+ }
+
+ setRootScrollTop(current);
+
+ if ((toDown && current < to) || (!toDown && current > to)) {
+ raf(animate);
+ } else {
+ cb && cb();
+ }
+ }
+
+ animate();
+}
diff --git a/src/utils/dom/scroll.ts b/src/utils/dom/scroll.ts
index ea47358e8..544dfae1e 100644
--- a/src/utils/dom/scroll.ts
+++ b/src/utils/dom/scroll.ts
@@ -59,3 +59,7 @@ export function getVisibleHeight(element: ScrollElement) {
? element.innerHeight
: (element).getBoundingClientRect().height;
}
+
+export function getVisibleTop(element: ScrollElement) {
+ return element === window ? 0 : (element).getBoundingClientRect().top;
+}