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`] = `
+
+
+
+
+
+
+ + + + + + + + + + +
+
+
+
+
+
+
+ 内容 1 +
+
+ 内容 2 +
+
+ 内容 3 +
+
+ 内容 4 +
+
+ 内容 5 +
+
+ 内容 6 +
+
+ 内容 7 +
+
+ 内容 8 +
+
+ 内容 9 +
+
+ 内容 10 +
+
+
+
`; diff --git a/src/tab/test/__snapshots__/index.spec.js.snap b/src/tab/test/__snapshots__/index.spec.js.snap index 67e3bbd97..85aa995d3 100644 --- a/src/tab/test/__snapshots__/index.spec.js.snap +++ b/src/tab/test/__snapshots__/index.spec.js.snap @@ -223,6 +223,50 @@ exports[`render nav-left & nav-right slot 1`] = ` `; +exports[`scrollspy 1`] = ` +
+
+
+
+
+ + + +
+
+
+
+
+
+
Text
+
Text
+
Text
+
+
+`; + +exports[`scrollspy 2`] = ` +
+
+
+
+
+ + + +
+
+
+
+
+
+
Text
+
Text
+
Text
+
+
+`; + exports[`swipe to switch tab 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; +}