refactor(Tab): refactor component with Sticky (#2285)

fix #2253 #2122

Tab
support css variables
This commit is contained in:
rex 2019-11-12 14:32:36 +08:00 committed by GitHub
parent 13b05cfa9c
commit 66fbfacf19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 241 additions and 357 deletions

View File

@ -57,7 +57,7 @@
</demo-block>
<demo-block title="样式风格">
<van-tabs type="card" tab-class="tab-class" tab-active-class="tab-active-class">
<van-tabs type="card" tab-class="tab-class">
<van-tab
wx:for="123"
wx:key="index"

View File

@ -14,6 +14,7 @@ page {
.right-nav {
padding: 0 10px;
line-height: 44px !important;
background-color: #fff;
}
.tab-class {

View File

@ -442,6 +442,20 @@
@tabbar-item-icon-size: 18px;
@tabbar-item-margin-bottom: 5px;
// Tab
@tab-text-color: @gray-darker;
@tab-active-text-color: @text-color;
@tab-disabled-text-color: @gray;
@tab-font-size: @font-size-md;
// Tabs
@tabs-default-color: @red;
@tabs-line-height: 44px;
@tabs-card-height: 30px;
@tabs-nav-background-color: @white;
@tabs-bottom-bar-height: 3px;
@tabs-bottom-bar-color: @tabs-default-color;
// Tag
@tag-padding: .2em .5em;
@tag-font-size: @font-size-xs;

View File

@ -1,3 +1,11 @@
.van-sticky {
position: relative;
&-wrap {
&--fixed {
position: fixed;
right: 0;
left: 0;
}
}
}

View File

@ -1,7 +1,4 @@
import { VantComponent } from '../common/component';
import { nextTick } from '../common/utils';
type Position = 'top' | 'bottom' | '';
VantComponent({
props: {
@ -12,131 +9,69 @@ VantComponent({
offsetTop: {
type: Number,
value: 0
}
},
disabled: Boolean
},
data: {
position: '', // 当前定位
height: 0,
wrapStyle: '',
containerStyle: ''
},
methods: {
setWrapStyle() {
const { offsetTop, position } = this.data;
let wrapStyle: string;
let containerStyle: string;
setStyle() {
const { offsetTop, height, fixed, zIndex } = this.data;
switch (position) {
case 'top':
wrapStyle = `
top: ${offsetTop}px;
position: fixed;
`;
containerStyle = `height: ${this.itemHeight}px;`;
break;
case 'bottom':
wrapStyle = `
top: auto;
bottom: 0;
`;
containerStyle = '';
break;
default:
wrapStyle = '';
containerStyle = '';
}
const data: Record<string, string> = {};
if (wrapStyle !== this.data.wrapStyle) {
data.wrapStyle = wrapStyle;
}
if (containerStyle !== this.data.containerStyle) {
data.containerStyle = containerStyle;
}
if (JSON.stringify(data) !== '{}') {
this.setData(data);
}
},
setPosition(position: Position) {
if (position !== this.data.position) {
this.setData({ position });
nextTick(() => {
this.setWrapStyle();
if (fixed) {
this.setData({
wrapStyle: `top: ${offsetTop}px;`,
containerStyle: `height: ${height}px; z-index: ${zIndex};`
});
} else {
this.setData({
wrapStyle: '',
containerStyle: ''
});
}
},
observerContentScroll() {
const { offsetTop = 0 } = this.data;
const { windowHeight } = wx.getSystemInfoSync();
this.createIntersectionObserver({}).disconnect();
// @ts-ignore
this.createIntersectionObserver()
.relativeToViewport({ top: -(this.itemHeight + offsetTop) })
.observe(
const { offsetTop } = this.data;
const intersectionObserver = this.createIntersectionObserver({
thresholds: [0, 1]
});
this.intersectionObserver = intersectionObserver;
intersectionObserver.relativeToViewport({ top: -offsetTop });
intersectionObserver.observe(
'.van-sticky',
(res: WechatMiniprogram.ObserveCallbackResult) => {
const { top } = res.boundingClientRect;
if (top > offsetTop) {
(res) => {
if (this.data.disabled) {
return;
}
const position: Position = 'top';
this.$emit('scroll', {
scrollTop: top + offsetTop,
isFixed: true
});
this.setPosition(position);
}
);
// @ts-ignore
this.createIntersectionObserver()
.relativeToViewport({ bottom: -(windowHeight - 1 - offsetTop) })
.observe(
'.van-sticky',
(res: WechatMiniprogram.ObserveCallbackResult) => {
const { top, bottom } = res.boundingClientRect;
if (bottom <= this.itemHeight - 1) {
return;
}
const position: Position = res.intersectionRatio > 0 ? 'top' : '';
const { top, height } = res.boundingClientRect;
const fixed = top <= offsetTop;
this.$emit('scroll', {
scrollTop: top + offsetTop,
isFixed: position === 'top'
scrollTop: top,
isFixed: fixed
});
this.setPosition(position);
this.setData({ fixed, height });
wx.nextTick(() => {
this.setStyle();
});
}
);
}
},
mounted() {
this.getRect('.van-sticky').then(
(rect: WechatMiniprogram.BoundingClientRectCallbackResult) => {
this.itemHeight = rect.height;
this.itemTop = rect.top;
this.observerContentScroll();
}
);
},
destroyed() {
this.createIntersectionObserver({}).disconnect();
this.intersectionObserver.disconnect();
}
});

View File

@ -1,5 +1,7 @@
<view class="custom-class van-sticky" style="z-index: {{ zIndex }}; {{ containerStyle }}">
<view class="van-sticky-wrap" style="{{ wrapStyle }}">
<wxs src="../wxs/utils.wxs" module="utils" />
<view class="custom-class van-sticky }}" style="{{ containerStyle }}">
<view class="{{ utils.bem('sticky-wrap', { fixed }) }}" style="{{ wrapStyle }}">
<slot />
</view>
</view>

View File

@ -3,7 +3,13 @@ import { VantComponent } from '../common/component';
VantComponent({
relation: {
name: 'tabs',
type: 'ancestor'
type: 'ancestor',
linked(target) {
this.parent = target;
},
unlinked() {
this.parent = null;
}
},
props: {
@ -15,7 +21,6 @@ VantComponent({
name: {
type: [Number, String],
value: '',
observer: 'setComputedName'
}
},
@ -39,10 +44,16 @@ VantComponent({
this.computedName = this.data.name || this.index;
},
getComputedName() {
if (this.data.name !== '') {
return this.data.name;
}
return this.index;
},
update() {
const parent = this.getRelationNodes('../tabs/index')[0];
if (parent) {
parent.updateTabs();
if (this.parent) {
this.parent.updateTabs();
}
}
}

View File

@ -1,6 +1,7 @@
{
"component": true,
"usingComponents": {
"van-info": "../info/index"
"van-info": "../info/index",
"van-sticky": "../sticky/index"
}
}

View File

@ -1,29 +1,13 @@
@import '../common/style/var.less';
@import '../common/style/theme.less';
@tabs-line-height: 44px;
@tabs-card-height: 30px;
.van-tabs {
position: relative;
-webkit-tap-highlight-color: transparent;
&__wrap {
position: absolute;
top: 0;
right: 0;
left: 0;
display: flex;
.theme(background-color, '@white');
&--page-top {
position: fixed;
}
&--content-bottom {
top: auto;
bottom: 0;
}
overflow: hidden;
&--scrollable {
.van-tab {
@ -32,9 +16,17 @@
}
}
&__scroll--card {
border-radius: 2px;
.theme(border, '1px solid @red');
&__scroll {
.theme(background-color, '@tabs-nav-background-color');
&--line {
box-sizing: content-box;
height: calc(100% + 15px); /* 15px padding to hide scrollbar in mobile safari */
}
&--card {
.theme(margin, '0 @padding-md');
}
}
&__nav {
@ -42,17 +34,16 @@
display: flex;
user-select: none;
&--line {
height: 100%;
}
&--card {
box-sizing: border-box;
.theme(height, '@tabs-card-height');
.theme(border, '@border-width-base solid @tabs-default-color');
.theme(border-radius, '@border-radius-sm');
.van-tab {
.theme(color, '@red');
.theme(line-height, '@tabs-card-height');
.theme(border-right, '1px solid @red');
.theme(color, '@tabs-default-color');
.theme(line-height, 'calc(@tabs-card-height - 2 * @border-width-base)');
.theme(border-right, '@border-width-base solid @tabs-default-color');
&:last-child {
border-right: none;
@ -60,7 +51,11 @@
&.van-tab--active {
.theme(color, '@white');
.theme(background-color, '@red');
.theme(background-color, '@tabs-default-color');
}
&--disabled {
.theme(color, '@tab-disabled-text-color');
}
}
}
@ -71,34 +66,29 @@
bottom: 0;
left: 0;
z-index: 1;
height: 3px;
border-radius: 3px;
.theme(background-color, '@red');
.theme(height, '@tabs-bottom-bar-height');
.theme(border-radius, '@tabs-bottom-bar-height');
.theme(background-color, '@tabs-bottom-bar-color');
}
&--line {
.theme(padding-top, '@tabs-line-height');
.van-tabs__wrap {
.theme(height, '@tabs-line-height');
}
}
&--card {
margin: 0 15px;
.theme(padding-top, '@tabs-card-height');
.van-tabs__wrap {
.theme(height, '@tabs-card-height');
}
&__track {
position: relative;
}
&__content {
overflow: hidden;
}
&__track {
position: relative;
&--line {
.van-tabs__wrap {
.theme(height, '@tabs-line-height');
}
}
&--card {
.van-tabs__wrap {
.theme(height, '@tabs-card-height');
}
}
}
@ -108,19 +98,19 @@
box-sizing: border-box;
min-width: 0; /* hack for flex ellipsis */
padding: 0 5px;
font-size: 14px;
text-align: center;
cursor: pointer;
.theme(color, '@gray-darker');
.theme(color, '@tab-text-color');
.theme(font-size, '@tab-font-size');
.theme(line-height, '@tabs-line-height');
&--active {
font-weight: 500;
.theme(color, '@text-color');
.theme(font-weight, '@font-weight-bold');
.theme(color, '@tab-active-text-color');
}
&--disabled {
.theme(color, '@gray');
.theme(color, '@tab-disabled-text-color');
}
&__title {

View File

@ -4,15 +4,13 @@ import { Weapp } from 'definitions/weapp';
import { nextTick, isDef, addUnit } from '../common/utils';
type TabItemData = {
width?: number
active: boolean
inited?: boolean
animated?: boolean
name?: string | number
width?: number;
active: boolean;
inited?: boolean;
animated?: boolean;
name?: string | number;
};
type Position = 'top' | 'bottom' | '';
VantComponent({
mixins: [touch],
@ -23,7 +21,6 @@ VantComponent({
type: 'descendant',
linked(child) {
child.index = this.children.length;
child.setComputedName();
this.children.push(child);
this.updateTabs(this.data.tabs.concat(child.data));
},
@ -37,7 +34,6 @@ VantComponent({
while (i >= 0 && i < this.children.length) {
const currentChild = this.children[i];
currentChild.index--;
currentChild.setComputedName();
i++;
}
@ -46,21 +42,33 @@ VantComponent({
},
props: {
color: String,
color: {
type: String,
observer: 'setLine'
},
sticky: Boolean,
animated: Boolean,
animated: {
type: Boolean,
observer: 'setTrack'
},
swipeable: Boolean,
lineWidth: {
type: [String, Number],
value: -1
value: -1,
observer: 'setLine'
},
lineHeight: {
type: [String, Number],
value: -1
value: -1,
observer: 'setLine'
},
active: {
type: [String, Number],
value: 0,
observer(value) {
this.currentName = value;
this.setActiveTab();
}
},
type: {
type: String,
@ -80,7 +88,12 @@ VantComponent({
},
swipeThreshold: {
type: Number,
value: 4
value: 4,
observer() {
this.setData({
scrollable: this.children.length > this.data.swipeThreshold
});
}
},
offsetTop: {
type: Number,
@ -96,21 +109,7 @@ VantComponent({
trackStyle: '',
wrapStyle: '',
position: '',
currentIndex: 0,
},
watch: {
swipeThreshold() {
this.setData({
scrollable: this.children.length > this.data.swipeThreshold
});
},
color: 'setLine',
lineWidth: 'setLine',
lineHeight: 'setLine',
active: 'setActiveTab',
animated: 'setTrack',
offsetTop: 'setWrapStyle'
currentIndex: 0
},
beforeCreate() {
@ -121,17 +120,6 @@ VantComponent({
this.setLine(true);
this.setTrack();
this.scrollIntoView();
this.getRect('.van-tabs__wrap').then(
(rect: WechatMiniprogram.BoundingClientRectCallbackResult) => {
this.navHeight = rect.height;
this.observerContentScroll();
}
);
},
destroyed() {
// @ts-ignore
this.createIntersectionObserver().disconnect();
},
methods: {
@ -156,18 +144,20 @@ VantComponent({
onTap(event: Weapp.Event) {
const { index } = event.currentTarget.dataset;
const child = this.children[index];
const computedName = child.getComputedName();
if (this.data.tabs[index].disabled) {
this.trigger('disabled', child.computedName);
this.trigger('disabled', computedName);
} else {
this.trigger('click', child.computedName);
this.setActive(child.computedName);
this.trigger('click', computedName);
this.setActive(computedName);
}
},
setActive(computedName) {
if (computedName !== this.currentName) {
this.currentName = computedName;
this.trigger('change', computedName);
setActive(name) {
if (name !== this.currentName) {
this.currentName = name;
this.trigger('change', name);
this.setActiveTab();
}
},
@ -177,13 +167,22 @@ VantComponent({
return;
}
const { color, duration, currentIndex, lineWidth, lineHeight } = this.data;
const {
color,
duration,
currentIndex,
lineWidth,
lineHeight
} = this.data;
this.getRect('.van-tab', true).then(
(rects: WechatMiniprogram.BoundingClientRectCallbackResult[]) => {
const rect = rects[currentIndex];
const width = lineWidth !== -1 ? lineWidth : rect.width / 2;
const height = lineHeight !== -1 ? `height: ${addUnit(lineHeight)}; border-radius: ${addUnit(lineHeight)};` : '';
const height =
lineHeight !== -1
? `height: ${addUnit(lineHeight)}; border-radius: ${addUnit(lineHeight)};`
: '';
let left = rects
.slice(0, currentIndex)
@ -230,34 +229,42 @@ VantComponent({
const data = { width, animated };
this.children.forEach((item: WechatMiniprogram.Component.TrivialInstance) => {
this.children.forEach(
(item: WechatMiniprogram.Component.TrivialInstance) => {
item.setData(data);
});
}
);
}
);
},
setActiveTab() {
if (!isDef(this.currentName)) {
this.currentName = this.data.active || (this.children[0] || {}).computedName;
const { active } = this.data;
const { children = [] } = this;
this.currentName =
active === '' && children.length
? children[0].getComputedName()
: active;
}
this.children.forEach((item: WechatMiniprogram.Component.TrivialInstance, index: number) => {
this.children.forEach(
(item: WechatMiniprogram.Component.TrivialInstance, index: number) => {
const data: TabItemData = {
active: item.computedName === this.currentName
active: item.getComputedName() === this.currentName
};
if (data.active) {
this.setData({
currentIndex: index
});
this.setData({ currentIndex: index });
data.inited = true;
}
if (data.active !== item.data.active) {
item.setData(data);
}
});
}
);
nextTick(() => {
this.setLine();
@ -317,101 +324,13 @@ VantComponent({
if (direction === 'horizontal' && offsetX >= minSwipeDistance) {
if (deltaX > 0 && currentIndex !== 0) {
this.setActive(this.children[currentIndex - 1].computedName);
const child = this.children[currentIndex - 1];
this.setActive(child.getComputedName());
} else if (deltaX < 0 && currentIndex !== tabs.length - 1) {
this.setActive(this.children[currentIndex + 1].computedName);
const child = this.children[currentIndex - 1];
this.setActive(child.getComputedName());
}
}
},
setWrapStyle() {
const { offsetTop, position } = this.data as {
offsetTop: number
position: Position
};
let wrapStyle: string;
switch (position) {
case 'top':
wrapStyle = `
top: ${offsetTop}px;
position: fixed;
`;
break;
case 'bottom':
wrapStyle = `
top: auto;
bottom: 0;
`;
break;
default:
wrapStyle = '';
}
if (wrapStyle !== this.data.wrapStyle) {
this.setData({ wrapStyle });
}
},
observerContentScroll() {
if (!this.data.sticky) {
return;
}
const { offsetTop } = this.data;
const { windowHeight } = wx.getSystemInfoSync();
// @ts-ignore
this.createIntersectionObserver().disconnect();
// @ts-ignore
this.createIntersectionObserver()
.relativeToViewport({ top: -(this.navHeight + offsetTop) })
.observe('.van-tabs', (res: WechatMiniprogram.ObserveCallbackResult) => {
const { top } = res.boundingClientRect;
if (top > offsetTop) {
return;
}
const position: Position =
res.intersectionRatio > 0 ? 'top' : 'bottom';
this.$emit('scroll', {
scrollTop: top + offsetTop,
isFixed: position === 'top'
});
this.setPosition(position);
});
// @ts-ignore
this.createIntersectionObserver()
.relativeToViewport({ bottom: -(windowHeight - 1 - offsetTop) })
.observe('.van-tabs', (res: WechatMiniprogram.ObserveCallbackResult) => {
const { top, bottom } = res.boundingClientRect;
if (bottom < this.navHeight) {
return;
}
const position: Position = res.intersectionRatio > 0 ? 'top' : '';
this.$emit('scroll', {
scrollTop: top + offsetTop,
isFixed: position === 'top'
});
this.setPosition(position);
});
},
setPosition(position: Position) {
if (position !== this.data.position) {
this.set({ position }).then(() => {
this.setWrapStyle();
});
}
}
}
});

View File

@ -1,14 +1,15 @@
<wxs src="../wxs/utils.wxs" module="utils" />
<view class="custom-class {{ utils.bem('tabs', [type]) }}">
<view style="z-index: {{ zIndex }}; {{ wrapStyle }}" class="{{ utils.bem('tabs__wrap', { scrollable }) }} {{ type === 'line' && border ? 'van-hairline--top-bottom' : '' }}">
<van-sticky disabled="{{ !sticky }}" z-index="{{ zIndex }}" offset-top="{{ offsetTop }}">
<view class="{{ utils.bem('tabs__wrap', { scrollable }) }} {{ type === 'line' && border ? 'van-hairline--top-bottom' : '' }}">
<slot name="nav-left" />
<scroll-view
scroll-x="{{ scrollable }}"
scroll-with-animation
scroll-left="{{ scrollLeft }}"
class="van-tabs__scroll--{{ type }}"
class="{{ utils.bem('tabs__scroll', [type]) }}"
style="{{ color ? 'border-color: ' + color : '' }}"
>
<view class="{{ utils.bem('tabs__nav', [type]) }} nav-class">
@ -36,6 +37,8 @@
<slot name="nav-right" />
</view>
</van-sticky>
<view
class="van-tabs__content"
bind:touchstart="onTouchStart"