[new feature] Tab support sticky (#382)

This commit is contained in:
neverland 2017-12-06 14:19:08 +08:00 committed by GitHub
parent beb3ed141d
commit 80c2fdc18f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 148 additions and 33 deletions

View File

@ -39,6 +39,14 @@
</van-tab> </van-tab>
</van-tabs> </van-tabs>
</demo-block> </demo-block>
<demo-block :title="$t('title6')">
<van-tabs :active="active" sticky>
<van-tab :title="$t('tab') + index" v-for="index in tabs" :key="index">
{{ $t('content') }} {{ index }}
</van-tab>
</van-tabs>
</demo-block>
</demo-section> </demo-section>
</template> </template>
@ -50,7 +58,8 @@ export default {
title2: '横向滚动', title2: '横向滚动',
title3: '禁用标签', title3: '禁用标签',
title4: '样式风格', title4: '样式风格',
title5: '点击事件' title5: '点击事件',
title6: '粘性布局'
}, },
'en-US': { 'en-US': {
tab: 'Tab ', tab: 'Tab ',
@ -58,7 +67,8 @@ export default {
title2: 'Swipe Tabs', title2: 'Swipe Tabs',
title3: 'Disabled Tab', title3: 'Disabled Tab',
title4: 'Card Style', title4: 'Card Style',
title5: 'Click Event' title5: 'Click Event',
title6: 'Sticky'
} }
}, },
@ -89,6 +99,8 @@ export default {
<style lang="postcss"> <style lang="postcss">
.demo-tab { .demo-tab {
margin-bottom: 1000px;
.van-tab__pane { .van-tab__pane {
background-color: #fff; background-color: #fff;
padding: 20px; padding: 20px;

View File

@ -100,6 +100,17 @@ export default {
}; };
``` ```
#### Sticky
In sticky mode, the tab will be fixed to top when scroll to top
```html
<van-tabs :active="active" sticky>
<van-tab v-for="index in 4" :title="'选项 ' + index">
内容 {{ index }}
</van-tab>
</van-tabs>
```
### Tabs API ### Tabs API
| Attribute | Description | Type | Default | Accepted Values | | Attribute | Description | Type | Default | Accepted Values |

View File

@ -100,14 +100,26 @@ export default {
}; };
``` ```
#### 粘性布局
通过`sticky`属性可以开启粘性布局,粘性布局下,当 Tab 滚动到顶部时会自动吸顶
```html
<van-tabs :active="active" sticky>
<van-tab v-for="index in 4" :title="'选项 ' + index">
内容 {{ index }}
</van-tab>
</van-tabs>
```
### Tabs API ### Tabs API
| 参数 | 说明 | 类型 | 默认值 | 可选 | | 参数 | 说明 | 类型 | 默认值 | 可选 |
|-----------|-----------|-----------|-------------|-------------| |-----------|-----------|-----------|-------------|-------------|
| type | Tab 样式类型 | `String` | `line` | `card` | | type | Tab 样式类型 | `String` | `line` | `card` |
| active | 默认激活的 tab | `String` `Number` | `0` | - | | active | 默认激活的 tab | `String` `Number` | `0` | - |
| duration | 切换 tab 的动画时间 | `Number` | `0.2` | - | - | | duration | 切换 tab 的动画时间 | `Number` | `0.2` | - |
| swipeThreshold | 滚动阀值,设置 Tab 超过多少个可滚动 | `Number` | `4` | - | - | | swipeThreshold | 滚动阀值,设置 Tab 超过多少个可滚动 | `Number` | `4` | - |
| sticky | 是否使用粘性定位布局 | `Boolean` | `false` | - |
### Tab API ### Tab API

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="van-tabs" :class="`van-tabs--${type}`"> <div class="van-tabs" :class="[`van-tabs--${type}`, { 'van-tabs--fixed': fixed }]">
<div :class="{ 'van-tabs__swipe': scrollable, 'van-hairline--top-bottom': type === 'line' }"> <div class="van-tabs__wrap" :class="{ 'van-tabs--scrollbale': scrollable, 'van-hairline--top-bottom': type === 'line' }">
<div class="van-tabs__nav" :class="`van-tabs__nav--${type}`" ref="nav"> <div class="van-tabs__nav" :class="`van-tabs__nav--${type}`" ref="nav">
<div v-if="type === 'line'" class="van-tabs__nav-bar" :style="navBarStyle" /> <div v-if="type === 'line'" class="van-tabs__nav-bar" :style="navBarStyle" />
<div <div
@ -26,6 +26,7 @@
<script> <script>
import { raf } from '../utils/raf'; import { raf } from '../utils/raf';
import scrollUtils from '../utils/scroll';
export default { export default {
name: 'van-tabs', name: 'van-tabs',
@ -46,14 +47,17 @@ export default {
swipeThreshold: { swipeThreshold: {
type: Number, type: Number,
default: 4 default: 4
} },
sticky: Boolean
}, },
data() { data() {
/* istanbul ignore next */
this.winWidth = this.$isServer ? 0 : window.innerWidth; this.winWidth = this.$isServer ? 0 : window.innerWidth;
return { return {
tabs: [], tabs: [],
fixed: false,
curActive: 0, curActive: 0,
navBarStyle: {} navBarStyle: {}
}; };
@ -72,23 +76,56 @@ export default {
curActive() { curActive() {
this.scrollIntoView(); this.scrollIntoView();
this.setNavBar(); this.setNavBar();
},
sticky(isSticky) {
this.scrollHandler(isSticky);
} }
}, },
mounted() { mounted() {
this.correctActive(this.active); this.correctActive(this.active);
this.setNavBar(); this.setNavBar();
if (this.sticky) {
this.scrollHandler(true);
}
},
destoryed() {
/* istanbul ignore next */
if (this.sticky) {
this.scrollHandler(false);
}
}, },
computed: { computed: {
// whether the nav is scrollable
scrollable() { scrollable() {
return this.tabs.length > this.swipeThreshold; return this.tabs.length > this.swipeThreshold;
} }
}, },
methods: { methods: {
// whether to bind sticky listener
scrollHandler(init) {
this.scrollEl = this.scrollEl || scrollUtils.getScrollEventTarget(this.$el);
this.scrollEl[init ? 'addEventListener' : 'removeEventListener']('scroll', this.onScroll);
this.onScroll();
},
// fixed tab when scrollTop > distance to top
onScroll() {
this.fixed = scrollUtils.getScrollTop(this.scrollEl) > scrollUtils.getElementTop(this.$el);
},
// update nav bar style
setNavBar() { setNavBar() {
this.$nextTick(() => { this.$nextTick(() => {
if (!this.$refs.tabs) {
return;
}
const tab = this.$refs.tabs[this.curActive]; const tab = this.$refs.tabs[this.curActive];
this.navBarStyle = { this.navBarStyle = {
width: `${tab.offsetWidth || 0}px`, width: `${tab.offsetWidth || 0}px`,
@ -98,12 +135,15 @@ export default {
}); });
}, },
// correct the value of active
correctActive(active) { correctActive(active) {
active = +active; active = +active;
const exist = this.tabs.some(tab => tab.index === active); const exist = this.tabs.some(tab => tab.index === active);
this.curActive = exist ? active : (this.tabs[0].index || 0); const defaultActive = (this.tabs[0] || {}).index || 0;
this.curActive = exist ? active : defaultActive;
}, },
// emit event when clicked
onClick(index) { onClick(index) {
if (this.tabs[index].disabled) { if (this.tabs[index].disabled) {
this.$emit('disabled', index); this.$emit('disabled', index);
@ -113,8 +153,9 @@ export default {
} }
}, },
// scroll active tab into view
scrollIntoView() { scrollIntoView() {
if (!this.scrollable) { if (!this.scrollable || !this.$refs.tabs) {
return; return;
} }
@ -125,6 +166,7 @@ export default {
const { offsetLeft, offsetWidth: tabWidth } = tab; const { offsetLeft, offsetWidth: tabWidth } = tab;
// out of right side // out of right side
/* istanbul ignore next */
if ((winWidth + scrollLeft - offsetLeft - tabWidth * 1.8) < 0) { if ((winWidth + scrollLeft - offsetLeft - tabWidth * 1.8) < 0) {
this.scrollTo(nav, scrollLeft, offsetLeft + tabWidth * 1.8 - winWidth); this.scrollTo(nav, scrollLeft, offsetLeft + tabWidth * 1.8 - winWidth);
} }
@ -134,11 +176,13 @@ export default {
} }
}, },
// animate the scrollLeft of nav
scrollTo(el, from, to) { scrollTo(el, from, to) {
let count = 0; let count = 0;
const frames = Math.round(this.duration * 1000 / 16); const frames = Math.round(this.duration * 1000 / 16);
const animate = () => { const animate = () => {
el.scrollLeft += (to - from) / frames; el.scrollLeft += (to - from) / frames;
/* istanbul ignore next */
if (++count < frames) { if (++count < frames) {
raf(animate); raf(animate);
} }

View File

@ -1,45 +1,31 @@
@import './common/var.css'; @import './common/var.css';
@import './mixins/ellipsis.css'; @import './mixins/ellipsis.css';
$van-tabs-line-height: 44px;
$van-tabs-card-height: 28px;
.van-tabs { .van-tabs {
position: relative; position: relative;
&__swipe {
user-select: none;
.van-tab {
flex: 0 0 22%;
}
.van-tabs__nav {
overflow-y: auto;
-webkit-overflow-scrolling: touch;
&::-webkit-scrollbar {
display: none;
background-color: transparent;
}
}
}
&__nav { &__nav {
display: flex; display: flex;
position: relative; position: relative;
user-select: none;
background-color: $white; background-color: $white;
&--line { &--line {
height: 44px; height: $van-tabs-line-height;
} }
&--card { &--card {
height: 28px;
margin: 0 15px; margin: 0 15px;
border-radius: 2px; border-radius: 2px;
height: $van-tabs-card-height;
border: 1px solid $gray-darker; border: 1px solid $gray-darker;
.van-tab { .van-tab {
color: $gray-darker; color: $gray-darker;
line-height: 28px; line-height: $van-tabs-card-height;
border-right: 1px solid $gray-darker; border-right: 1px solid $gray-darker;
&:last-child { &:last-child {
@ -62,6 +48,39 @@
position: absolute; position: absolute;
background-color: $red; background-color: $red;
} }
&--scrollbale {
.van-tab {
flex: 0 0 22%;
}
.van-tabs__nav {
overflow-y: auto;
-webkit-overflow-scrolling: touch;
&::-webkit-scrollbar {
display: none;
background-color: transparent;
}
}
}
&--fixed {
.van-tabs__wrap {
top: 0;
left: 0;
right: 0;
position: fixed;
}
&.van-tabs--line {
padding-top: $van-tabs-line-height;
}
&.van-tabs--card {
padding-top: $van-tabs-card-height;
}
}
} }
.van-tab { .van-tab {
@ -71,7 +90,7 @@
font-size: 14px; font-size: 14px;
position: relative; position: relative;
color: $text-color; color: $text-color;
line-height: 44px; line-height: $van-tabs-line-height;
text-align: center; text-align: center;
box-sizing: border-box; box-sizing: border-box;
background-color: $white; background-color: $white;

View File

@ -1,5 +1,5 @@
<template> <template>
<van-tabs :active="active" :duration="duration" @click="handleTabClick" @disabled="handleTabDisabledClick"> <van-tabs :active="active" :duration="duration" @click="handleTabClick" @disabled="handleTabDisabledClick" :sticky="sticky">
<van-tab :title="firstTabTitle" :disabled="firstTabDisabled">内容一</van-tab> <van-tab :title="firstTabTitle" :disabled="firstTabDisabled">内容一</van-tab>
<van-tab title="选项二">内容二</van-tab> <van-tab title="选项二">内容二</van-tab>
<van-tab title="选项三" disabled>内容三</van-tab> <van-tab title="选项三" disabled>内容三</van-tab>
@ -24,7 +24,8 @@ export default {
}, },
firstTabDisabled: { firstTabDisabled: {
type: Boolean type: Boolean
} },
sticky: Boolean
}, },
data() { data() {

View File

@ -113,4 +113,20 @@ describe('Tabs', () => {
done(); done();
}); });
}); });
it('sticky', (done) => {
wrapper = mount(TabsTestComponent, {
attachToDocument: true,
propsData: {
sticky: true
}
});
wrapper.vm.sticky = false;
setTimeout(() => {
expect(wrapper.vm.$children[0].fixed).to.be.false;
done();
}, 30);
});
}); });