feat(Tab): add scrollspy prop (#5273)

This commit is contained in:
健忘症患者丶 2019-12-16 20:17:09 +08:00 committed by neverland
parent 9cd06f3b20
commit 4603e1633c
12 changed files with 266 additions and 10 deletions

View File

@ -5,8 +5,8 @@ exports[`renders demo correctly 1`] = `
<div class="van-tabs van-tabs--line"> <div class="van-tabs van-tabs--line">
<div class="van-tabs__wrap van-hairline--top-bottom"> <div class="van-tabs__wrap van-hairline--top-bottom">
<div role="tablist" class="van-tabs__nav van-tabs__nav--line" style="border-color: #1989fa;"> <div role="tablist" class="van-tabs__nav van-tabs__nav--line" style="border-color: #1989fa;">
<div role="tab" aria-selected="true" class="van-tab van-tab--active"><span class="van-ellipsis">基础用法</span></div> <div role="tab" aria-selected="true" class="van-tab van-tab--active van-ellipsis"><span class="van-tab__text">基础用法<!----></span></div>
<div role="tab" class="van-tab"><span class="van-ellipsis">自定义索引列表</span></div> <div role="tab" class="van-tab van-ellipsis"><span class="van-tab__text">自定义索引列表<!----></span></div>
<div class="van-tabs__line" style="background-color: rgb(25, 137, 250); width: 0px; transform: translateX(0px) translateX(-50%);"></div> <div class="van-tabs__line" style="background-color: rgb(25, 137, 250); width: 0px; transform: translateX(0px) translateX(-50%);"></div>
</div> </div>
</div> </div>

View File

@ -5,8 +5,8 @@ exports[`renders demo correctly 1`] = `
<div class="van-tabs van-tabs--line"> <div class="van-tabs van-tabs--line">
<div class="van-tabs__wrap van-hairline--top-bottom"> <div class="van-tabs__wrap van-hairline--top-bottom">
<div role="tablist" class="van-tabs__nav van-tabs__nav--line"> <div role="tablist" class="van-tabs__nav van-tabs__nav--line">
<div role="tab" aria-selected="true" class="van-tab van-tab--active"><span class="van-ellipsis">基础用法</span></div> <div role="tab" aria-selected="true" class="van-tab van-tab--active van-ellipsis"><span class="van-tab__text">基础用法<!----></span></div>
<div role="tab" class="van-tab"><span class="van-ellipsis">错误提示</span></div> <div role="tab" class="van-tab van-ellipsis"><span class="van-tab__text">错误提示<!----></span></div>
<div class="van-tabs__line" style="width: 0px; transform: translateX(0px) translateX(-50%);"></div> <div class="van-tabs__line" style="width: 0px; transform: translateX(0px) translateX(-50%);"></div>
</div> </div>
</div> </div>

View File

@ -170,6 +170,18 @@ In swipeable mode, you can switch tabs with swipe gestrue in the content
</van-tabs> </van-tabs>
``` ```
### Scrollspy
In scrollspy mode, the list of content will be tiled
```html
<van-tabs v-model="active" scrollspy sticky>
<van-tab v-for="index in 10" :title="'tab ' + index">
content {{ index }}
</van-tab>
</van-tabs>
```
## API ## API
### Tabs Props ### 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` | - | | sticky | Whether to use sticky mode | *boolean* | `false` | - |
| swipeable | Whether to switch tabs with swipe gestrue in the content | *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` | - | | lazy-render | Whether to enable tab content lazy render | *boolean* | `true` | - |
| scrollspy | Whether to use scrollspy mode | *boolean* | `false` | - |
### Tab Props ### Tab Props

View File

@ -174,6 +174,18 @@ export default {
</van-tabs> </van-tabs>
``` ```
### 滚动导航
通过`scrollspy`属性可以开启滚动导航模式,该模式下,内容将会平铺展示
```html
<van-tabs v-model="active" scrollspy sticky>
<van-tab v-for="index in 10" :title="'选项 ' + index">
内容 {{ index }}
</van-tab>
</van-tabs>
```
## API ## API
### Tabs Props ### Tabs Props
@ -197,6 +209,7 @@ export default {
| sticky | 是否使用粘性定位布局 | *boolean* | `false` | - | | sticky | 是否使用粘性定位布局 | *boolean* | `false` | - |
| swipeable | 是否开启手势滑动切换 | *boolean* | `false` | - | | swipeable | 是否开启手势滑动切换 | *boolean* | `false` | - |
| lazy-render | 是否开启标签页内容延迟渲染 | *boolean* | `true` | - | | lazy-render | 是否开启标签页内容延迟渲染 | *boolean* | `true` | - |
| scrollspy | 是否开启滚动导航 | *boolean* | `false` | - |
### Tab Props ### Tab Props

View File

@ -93,6 +93,14 @@
</van-tab> </van-tab>
</van-tabs> </van-tabs>
</demo-block> </demo-block>
<demo-block :title="$t('title10')">
<van-tabs scrollspy sticky>
<van-tab :title="$t('tab') + index" v-for="index in 10" :key="index">
{{ $t('content') }} {{ index }}
</van-tab>
</van-tabs>
</demo-block>
</demo-section> </demo-section>
</template> </template>
@ -109,6 +117,7 @@ export default {
title7: '自定义标签', title7: '自定义标签',
title8: '切换动画', title8: '切换动画',
title9: '滑动切换', title9: '滑动切换',
title10: '滚动导航',
disabled: ' 已被禁用', disabled: ' 已被禁用',
matchByName: '通过名称匹配' matchByName: '通过名称匹配'
}, },
@ -123,6 +132,7 @@ export default {
title7: 'Custom Tab', title7: 'Custom Tab',
title8: 'Switch Animation', title8: 'Switch Animation',
title9: 'Swipeable', title9: 'Swipeable',
title10: 'Scrollspy',
disabled: ' is disabled', disabled: ' is disabled',
matchByName: 'Match By Name' matchByName: 'Match By Name'
} }

View File

@ -46,7 +46,8 @@ export default createComponent({
render(h) { render(h) {
const { slots, isActive } = this; 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(); const Content = shouldRender ? slots() : h();
if (this.parent.animated) { if (this.parent.animated) {
@ -62,7 +63,7 @@ export default createComponent({
} }
return ( return (
<div vShow={isActive} role="tabpanel" class={bem('pane')}> <div vShow={show} role="tabpanel" class={bem('pane')}>
{Content} {Content}
</div> </div>
); );

View File

@ -275,5 +275,60 @@ exports[`renders demo correctly 1`] = `
</div> </div>
</div> </div>
</div> </div>
<div>
<div class="van-tabs van-tabs--line">
<div>
<div class="van-sticky">
<div class="van-tabs__wrap van-tabs__wrap--scrollable van-hairline--top-bottom">
<div role="tablist" class="van-tabs__nav van-tabs__nav--line">
<div role="tab" aria-selected="true" class="van-tab van-tab--active van-ellipsis" style="flex-basis: 22%;"><span class="van-tab__text">标签 1<!----></span></div>
<div role="tab" class="van-tab van-ellipsis" style="flex-basis: 22%;"><span class="van-tab__text">标签 2<!----></span></div>
<div role="tab" class="van-tab van-ellipsis" style="flex-basis: 22%;"><span class="van-tab__text">标签 3<!----></span></div>
<div role="tab" class="van-tab van-ellipsis" style="flex-basis: 22%;"><span class="van-tab__text">标签 4<!----></span></div>
<div role="tab" class="van-tab van-ellipsis" style="flex-basis: 22%;"><span class="van-tab__text">标签 5<!----></span></div>
<div role="tab" class="van-tab van-ellipsis" style="flex-basis: 22%;"><span class="van-tab__text">标签 6<!----></span></div>
<div role="tab" class="van-tab van-ellipsis" style="flex-basis: 22%;"><span class="van-tab__text">标签 7<!----></span></div>
<div role="tab" class="van-tab van-ellipsis" style="flex-basis: 22%;"><span class="van-tab__text">标签 8<!----></span></div>
<div role="tab" class="van-tab van-ellipsis" style="flex-basis: 22%;"><span class="van-tab__text">标签 9<!----></span></div>
<div role="tab" class="van-tab van-ellipsis" style="flex-basis: 22%;"><span class="van-tab__text">标签 10<!----></span></div>
<div class="van-tabs__line" style="width: 0px; transform: translateX(0px) translateX(-50%);"></div>
</div>
</div>
</div>
</div>
<div class="van-tabs__content">
<div role="tabpanel" class="van-tab__pane">
内容 1
</div>
<div role="tabpanel" class="van-tab__pane">
内容 2
</div>
<div role="tabpanel" class="van-tab__pane">
内容 3
</div>
<div role="tabpanel" class="van-tab__pane">
内容 4
</div>
<div role="tabpanel" class="van-tab__pane">
内容 5
</div>
<div role="tabpanel" class="van-tab__pane">
内容 6
</div>
<div role="tabpanel" class="van-tab__pane">
内容 7
</div>
<div role="tabpanel" class="van-tab__pane">
内容 8
</div>
<div role="tabpanel" class="van-tab__pane">
内容 9
</div>
<div role="tabpanel" class="van-tab__pane">
内容 10
</div>
</div>
</div>
</div>
</div> </div>
`; `;

View File

@ -223,6 +223,50 @@ exports[`render nav-left & nav-right slot 1`] = `
</div> </div>
`; `;
exports[`scrollspy 1`] = `
<div class="van-tabs van-tabs--line">
<div>
<div class="van-sticky">
<div class="van-tabs__wrap van-hairline--top-bottom">
<div role="tablist" class="van-tabs__nav van-tabs__nav--line">
<div role="tab" aria-selected="true" class="van-tab van-tab--active van-ellipsis"><span class="van-tab__text">title1<!----></span></div>
<div role="tab" class="van-tab van-ellipsis"><span class="van-tab__text">title2<!----></span></div>
<div role="tab" class="van-tab van-ellipsis"><span class="van-tab__text">title3<!----></span></div>
<div class="van-tabs__line" style="width: 0px; transform: translateX(0px) translateX(-50%);"></div>
</div>
</div>
</div>
</div>
<div class="van-tabs__content">
<div role="tabpanel" class="van-tab__pane">Text</div>
<div role="tabpanel" class="van-tab__pane">Text</div>
<div role="tabpanel" class="van-tab__pane">Text</div>
</div>
</div>
`;
exports[`scrollspy 2`] = `
<div class="van-tabs van-tabs--line">
<div>
<div class="van-sticky">
<div class="van-tabs__wrap van-hairline--top-bottom">
<div role="tablist" class="van-tabs__nav van-tabs__nav--line">
<div role="tab" class="van-tab van-ellipsis"><span class="van-tab__text">title1<!----></span></div>
<div role="tab" class="van-tab van-ellipsis"><span class="van-tab__text">title2<!----></span></div>
<div role="tab" class="van-tab van-tab--active van-ellipsis" aria-selected="true"><span class="van-tab__text">title3<!----></span></div>
<div class="van-tabs__line" style="width: 0px; transform: translateX(0px) translateX(-50%); transition-duration: 0.3s;"></div>
</div>
</div>
</div>
</div>
<div class="van-tabs__content">
<div role="tabpanel" class="van-tab__pane">Text</div>
<div role="tabpanel" class="van-tab__pane">Text</div>
<div role="tabpanel" class="van-tab__pane">Text</div>
</div>
</div>
`;
exports[`swipe to switch tab 1`] = ` exports[`swipe to switch tab 1`] = `
<div class="van-tabs van-tabs--line"> <div class="van-tabs van-tabs--line">
<div class="van-tabs__wrap van-hairline--top-bottom"> <div class="van-tabs__wrap van-hairline--top-bottom">

View File

@ -1,7 +1,7 @@
import Vue from 'vue'; import Vue from 'vue';
import Tab from '..'; import Tab from '..';
import Tabs from '../../tabs'; import Tabs from '../../tabs';
import { mount, later, triggerDrag } from '../../../test'; import { mount, later, triggerDrag, mockScrollTop } from '../../../test';
Vue.use(Tab); Vue.use(Tab);
Vue.use(Tabs); Vue.use(Tabs);
@ -263,3 +263,33 @@ test('info prop', () => {
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
test('scrollspy', async () => {
const onChange = jest.fn();
window.scrollTo = jest.fn();
const wrapper = mount({
template: `
<van-tabs scrollspy sticky @change="onChange">
<van-tab name="a" title="title1">Text</van-tab>
<van-tab name="b" title="title2">Text</van-tab>
<van-tab name="c" title="title3">Text</van-tab>
</van-tabs>
`,
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');
});

View File

@ -1,11 +1,12 @@
import { createNamespace, isDef, addUnit } from '../utils'; import { createNamespace, isDef, addUnit } from '../utils';
import { scrollLeftTo } from './utils'; import { scrollLeftTo, scrollTopTo } from './utils';
import { route } from '../utils/router'; import { route } from '../utils/router';
import { isHidden } from '../utils/dom/style'; import { isHidden } from '../utils/dom/style';
import { on, off } from '../utils/dom/event';
import { ParentMixin } from '../mixins/relation'; import { ParentMixin } from '../mixins/relation';
import { BindEventMixin } from '../mixins/bind-event'; import { BindEventMixin } from '../mixins/bind-event';
import { BORDER_TOP_BOTTOM } from '../utils/constant'; 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 Title from './Title';
import Content from './Content'; import Content from './Content';
import Sticky from '../sticky'; import Sticky from '../sticky';
@ -17,6 +18,9 @@ export default createComponent({
ParentMixin('vanTabs'), ParentMixin('vanTabs'),
BindEventMixin(function(bind) { BindEventMixin(function(bind) {
bind(window, 'resize', this.resize, true); bind(window, 'resize', this.resize, true);
if (this.scrollspy) {
bind(window, 'scroll', this.onScrollspyScroll, true);
}
}) })
], ],
@ -29,6 +33,7 @@ export default createComponent({
sticky: Boolean, sticky: Boolean,
animated: Boolean, animated: Boolean,
swipeable: Boolean, swipeable: Boolean,
scrollspy: Boolean,
background: String, background: String,
lineWidth: [Number, String], lineWidth: [Number, String],
lineHeight: [Number, String], lineHeight: [Number, String],
@ -97,6 +102,13 @@ export default createComponent({
if (activeTab) { if (activeTab) {
return activeTab.computedName; return activeTab.computedName;
} }
},
scrollOffset() {
if (this.sticky) {
return this.offsetTop + this.tabHeight;
}
return 0;
} }
}, },
@ -123,9 +135,17 @@ export default createComponent({
this.setLine(); this.setLine();
// scroll to correct position // scroll to correct position
if (this.stickyFixed) { if (this.stickyFixed && !this.scrollspy) {
setRootScrollTop(Math.ceil(getElementTop(this.$el) - this.offsetTop)); 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() { onShow() {
this.$nextTick(() => { this.$nextTick(() => {
this.inited = true; this.inited = true;
this.tabHeight = getVisibleHeight(this.$refs.wrap);
this.scrollIntoView(true); this.scrollIntoView(true);
}); });
}, },
@ -236,6 +257,7 @@ export default createComponent({
this.$emit('disabled', computedName, title); this.$emit('disabled', computedName, title);
} else { } else {
this.setCurrentIndex(index); this.setCurrentIndex(index);
this.scrollToCurrentContent();
this.$emit('click', computedName, title); this.$emit('click', computedName, title);
} }
}, },
@ -258,6 +280,44 @@ export default createComponent({
onScroll(params) { onScroll(params) {
this.stickyFixed = params.isFixed; this.stickyFixed = params.isFixed;
this.$emit('scroll', params); 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;
} }
}, },

View File

@ -1,4 +1,5 @@
import { raf } from '../utils/dom/raf'; import { raf } from '../utils/dom/raf';
import { getRootScrollTop, setRootScrollTop } from '../utils/dom/scroll';
export function scrollLeftTo(el: HTMLElement, to: number, duration: number) { export function scrollLeftTo(el: HTMLElement, to: number, duration: number) {
let count = 0; let count = 0;
@ -15,3 +16,28 @@ export function scrollLeftTo(el: HTMLElement, to: number, duration: number) {
animate(); 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();
}

View File

@ -59,3 +59,7 @@ export function getVisibleHeight(element: ScrollElement) {
? element.innerHeight ? element.innerHeight
: (<HTMLElement>element).getBoundingClientRect().height; : (<HTMLElement>element).getBoundingClientRect().height;
} }
export function getVisibleTop(element: ScrollElement) {
return element === window ? 0 : (<HTMLElement>element).getBoundingClientRect().top;
}