mirror of
https://gitee.com/vant-contrib/vant.git
synced 2025-04-06 03:57:59 +08:00
[new feature] Tab support sticky (#382)
This commit is contained in:
parent
beb3ed141d
commit
80c2fdc18f
@ -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;
|
||||||
|
@ -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 |
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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() {
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user