[new feature] Tab: refactor component & add new slot

支持 #1316

pull request 改动点:

* refactor sticky implementation
* improve performance
* add new slot left-nav & right-nav
This commit is contained in:
rex 2019-02-25 10:11:35 +08:00 committed by GitHub
parent 082fcbb0db
commit 74cb663c85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 211 additions and 129 deletions

View File

@ -20,6 +20,13 @@ Page({
}); });
}, },
onClickNavRight() {
wx.showToast({
title: '点击right nav',
icon: 'none'
});
},
onClick(event) { onClick(event) {
wx.showToast({ wx.showToast({
title: `点击标签 ${event.detail.index + 1}`, title: `点击标签 ${event.detail.index + 1}`,

View File

@ -14,6 +14,12 @@
<demo-block title="自定义标题"> <demo-block title="自定义标题">
<van-tabs active="{{ 1 }}" bind:change="onChange"> <van-tabs active="{{ 1 }}" bind:change="onChange">
<van-icon
slot="nav-right"
name="search"
custom-class="right-nav"
bind:click="onClickNavRight"
/>
<van-tab <van-tab
wx:for="1234" wx:for="1234"
wx:key="index" wx:key="index"
@ -87,7 +93,7 @@
</demo-block> </demo-block>
<demo-block title="粘性布局"> <demo-block title="粘性布局">
<van-tabs sticky scroll-top="{{ scrollTop }}"> <van-tabs sticky>
<van-tab <van-tab
wx:for="1234" wx:for="1234"
wx:key="index" wx:key="index"

View File

@ -5,4 +5,9 @@
.content-2 { .content-2 {
padding: 20px; padding: 20px;
} }
.right-nav {
padding: 0 10px;
line-height: 44px !important;
}

View File

@ -4,7 +4,7 @@ export const basic = Behavior({
this.triggerEvent.apply(this, arguments); this.triggerEvent.apply(this, arguments);
}, },
getRect(selector, all) { getRect(selector: string, all: boolean) {
return new Promise(resolve => { return new Promise(resolve => {
wx.createSelectorQuery() wx.createSelectorQuery()
.in(this)[all ? 'selectAll' : 'select'](selector) .in(this)[all ? 'selectAll' : 'select'](selector)

View File

@ -116,7 +116,7 @@ Page({
通过`sticky`属性可以开启粘性布局,粘性布局下,当 Tab 滚动到顶部时会自动吸顶 通过`sticky`属性可以开启粘性布局,粘性布局下,当 Tab 滚动到顶部时会自动吸顶
```html ```html
<van-tabs sticky scroll-top="{{ scrollTop }}"> <van-tabs sticky>
<van-tab title="标签 1">内容 1</van-tab> <van-tab title="标签 1">内容 1</van-tab>
<van-tab title="标签 2">内容 2</van-tab> <van-tab title="标签 2">内容 2</van-tab>
<van-tab title="标签 3">内容 3</van-tab> <van-tab title="标签 3">内容 3</van-tab>
@ -124,19 +124,6 @@ Page({
</van-tabs> </van-tabs>
``` ```
```javascript
Page({
data: {
scrollTop: 0
},
onPageScroll(event) {
this.setData({
scrollTop: event.scrollTop
});
}
});
```
#### 切换动画 #### 切换动画
可以通过`animated`来设置是否启用切换tab时的动画。 可以通过`animated`来设置是否启用切换tab时的动画。
@ -178,7 +165,6 @@ Page({
| animated | 是否使用动画切换 Tabs | `Boolean` | `false` | | animated | 是否使用动画切换 Tabs | `Boolean` | `false` |
| swipeable | 是否开启手势滑动切换 | `Boolean` | `false` | | swipeable | 是否开启手势滑动切换 | `Boolean` | `false` |
| sticky | 是否使用粘性定位布局 | `Boolean` | `false` | | sticky | 是否使用粘性定位布局 | `Boolean` | `false` |
| offset-top | 粘性定位布局下与顶部的最小距离,单位 px | `Number` | `0` |
### Tab API ### Tab API
@ -190,6 +176,13 @@ Page({
| info | 图标右上角提示信息 | `String | Number` | - | | info | 图标右上角提示信息 | `String | Number` | - |
| title-style | 自定义标题样式 | `String` | - | | title-style | 自定义标题样式 | `String` | - |
### Tabs Slot
| 名称 | 说明 |
|-----------|-----------|
| nav-left | 标题左侧内容 |
| nav-right | 标题右侧内容 |
### Tab Slot ### Tab Slot
| 名称 | 说明 | | 名称 | 说明 |
@ -210,15 +203,3 @@ Page({
| 类名 | 说明 | | 类名 | 说明 |
|-----------|-----------| |-----------|-----------|
| custom-class | 根节点样式类 | | custom-class | 根节点样式类 |
### 更新日志
| 版本 | 类型 | 内容 |
|-----------|-----------|-----------|
| 0.3.0 | feature | 新增组件 |
| 0.3.2 | bugfix | 修复部分情况下代码报错的问题 |
| 0.3.2 | bugfix | 修复 color 属性会改变未激活标签的颜色的问题 |
| 0.3.3 | feature | 新增 border 属性 |
| 0.3.3 | feature | 支持传入外部样式类 |
| 0.3.5 | bugfix | 修复 active 属性默认值错误的问题 |
| 0.3.7 | feature | 新增 z-index 属性 |

View File

@ -12,6 +12,8 @@
top: 0; top: 0;
right: 0; right: 0;
left: 0; left: 0;
display: flex;
background-color: @white;
&--page-top { &--page-top {
position: fixed; position: fixed;
@ -37,7 +39,6 @@
&__nav { &__nav {
position: relative; position: relative;
display: flex; display: flex;
background-color: @white;
user-select: none; user-select: none;
&--line { &--line {

View File

@ -2,12 +2,14 @@ import { VantComponent } from '../common/component';
import { touch } from '../mixins/touch'; import { touch } from '../mixins/touch';
type TabItemData = { type TabItemData = {
width?: number; width?: number
active: boolean; active: boolean
inited?: boolean; inited?: boolean
animated?: boolean; animated?: boolean
}; };
type Position = 'top' | 'bottom' | '';
VantComponent({ VantComponent({
mixins: [touch], mixins: [touch],
@ -67,10 +69,6 @@ VantComponent({
offsetTop: { offsetTop: {
type: Number, type: Number,
value: 0 value: 0
},
scrollTop: {
type: Number,
value: 0
} }
}, },
@ -95,7 +93,6 @@ VantComponent({
lineHeight: 'setLine', lineHeight: 'setLine',
active: 'setActiveTab', active: 'setActiveTab',
animated: 'setTrack', animated: 'setTrack',
scrollTop: 'onScroll',
offsetTop: 'setWrapStyle' offsetTop: 'setWrapStyle'
}, },
@ -107,6 +104,11 @@ VantComponent({
this.setLine(true); this.setLine(true);
this.setTrack(); this.setTrack();
this.scrollIntoView(); this.scrollIntoView();
this.getRect('.van-tabs__wrap').then((rect: BoundingClientRect) => {
this.navHeight = rect.height;
this.observerContentScroll();
});
}, },
destroyed() { destroyed() {
@ -114,7 +116,7 @@ VantComponent({
}, },
methods: { methods: {
updateTabs(tabs) { updateTabs(tabs: TabItemData[]) {
tabs = tabs || this.data.tabs; tabs = tabs || this.data.tabs;
this.set({ this.set({
tabs, tabs,
@ -153,17 +155,11 @@ VantComponent({
return; return;
} }
const { const { color, active, duration, lineWidth, lineHeight } = this.data;
color,
active,
duration,
lineWidth,
lineHeight
} = this.data;
this.getRect('.van-tab', true).then(rects => { this.getRect('.van-tab', true).then((rects: BoundingClientRect[]) => {
const rect = rects[active]; const rect = rects[active];
const width = (lineWidth !== -1) ? lineWidth : rect.width / 2; const width = lineWidth !== -1 ? lineWidth : rect.width / 2;
const height = lineHeight !== -1 ? `height: ${lineHeight}px;` : ''; const height = lineHeight !== -1 ? `height: ${lineHeight}px;` : '';
let left = rects let left = rects
@ -172,7 +168,9 @@ VantComponent({
left += (rect.width - width) / 2; left += (rect.width - width) / 2;
const transition = skipTransition ? '' : `transition-duration: ${duration}s; -webkit-transition-duration: ${duration}s;`; const transition = skipTransition
? ''
: `transition-duration: ${duration}s; -webkit-transition-duration: ${duration}s;`;
this.set({ this.set({
lineStyle: ` lineStyle: `
@ -188,15 +186,11 @@ VantComponent({
}, },
setTrack() { setTrack() {
const { const { animated, active, duration } = this.data;
animated,
active,
duration
} = this.data;
if (!animated) return ''; if (!animated) return '';
this.getRect('.van-tabs__content').then(rect => { this.getRect('.van-tabs__content').then((rect: BoundingClientRect) => {
const { width } = rect; const { width } = rect;
this.set({ this.set({
@ -204,24 +198,21 @@ VantComponent({
width: ${width * this.child.length}px; width: ${width * this.child.length}px;
left: ${-1 * active * width}px; left: ${-1 * active * width}px;
transition: left ${duration}s; transition: left ${duration}s;
display: -webkit-box;
display: flex; display: flex;
` `
}); });
this.setTabsProps({
width,
animated
})
})
},
setTabsProps(props) { const props = { width, animated };
this.child.forEach(item => {
item.set(props); this.child.forEach((item: Weapp.Component) => {
item.set(props);
});
}); });
}, },
setActiveTab() { setActiveTab() {
this.child.forEach((item, index) => { this.child.forEach((item: Weapp.Component, index: number) => {
const data: TabItemData = { const data: TabItemData = {
active: index === this.data.active active: index === this.data.active
}; };
@ -244,24 +235,27 @@ VantComponent({
// scroll active tab into view // scroll active tab into view
scrollIntoView() { scrollIntoView() {
if (!this.data.scrollable) { const { active, scrollable } = this.data;
if (!scrollable) {
return; return;
} }
this.getRect('.van-tab', true).then(tabRects => { Promise.all([
const tabRect = tabRects[this.data.active]; this.getRect('.van-tab', true),
const offsetLeft = tabRects this.getRect('.van-tabs__nav')
.slice(0, this.data.active) ]).then(
.reduce((prev, curr) => prev + curr.width, 0); ([tabRects, navRect]: [BoundingClientRect[], BoundingClientRect]) => {
const tabWidth = tabRect.width; const tabRect = tabRects[active];
const offsetLeft = tabRects
.slice(0, active)
.reduce((prev, curr) => prev + curr.width, 0);
this.getRect('.van-tabs__nav').then(navRect => {
const navWidth = navRect.width;
this.set({ this.set({
scrollLeft: offsetLeft - (navWidth - tabWidth) / 2 scrollLeft: offsetLeft - (navRect.width - tabRect.width) / 2
}); });
}); }
}); );
}, },
onTouchStart(event: Weapp.TouchEvent) { onTouchStart(event: Weapp.TouchEvent) {
@ -295,8 +289,11 @@ VantComponent({
}, },
setWrapStyle() { setWrapStyle() {
const { offsetTop, position } = this.data; const { offsetTop, position } = this.data as {
let wrapStyle; offsetTop: number
position: Position
};
let wrapStyle: string;
switch (position) { switch (position) {
case 'top': case 'top':
@ -318,44 +315,63 @@ VantComponent({
// cut down `set` // cut down `set`
if (wrapStyle === this.data.wrapStyle) return; if (wrapStyle === this.data.wrapStyle) return;
this.set({ this.set({ wrapStyle });
wrapStyle
});
}, },
// adjust tab position observerContentScroll() {
onScroll(scrollTop) { if (!this.data.sticky) {
if (!this.data.sticky) return; return;
}
const { offsetTop } = this.data; const { offsetTop } = this.data;
const { windowHeight } = wx.getSystemInfoSync();
this.getRect('.van-tabs').then(rect => { wx.createIntersectionObserver(this)
const { top, height } = rect; .relativeToViewport({ top: -this.navHeight })
.observe('.van-tabs', res => {
const { top } = res.boundingClientRect;
this.getRect('.van-tabs__wrap').then(rect => { if (top > 0) {
const { height: wrapHeight } = rect; return;
let position = '';
if (offsetTop > top + height - wrapHeight) {
position = 'bottom';
} else if (offsetTop > top) {
position = 'top';
} }
const position: Position =
res.intersectionRatio > 0 ? 'top' : 'bottom';
this.$emit('scroll', { this.$emit('scroll', {
scrollTop: scrollTop + offsetTop, scrollTop: top + offsetTop,
isFixed: position === 'top' isFixed: position === 'top'
}); });
if (position !== this.data.position) { this.setPosition(position);
this.set({
position
}, () => {
this.setWrapStyle();
});
}
}); });
});
wx.createIntersectionObserver(this)
.relativeToViewport({ bottom: -(windowHeight - 1) })
.observe('.van-tabs', res => {
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,7 +1,9 @@
<wxs src="../wxs/utils.wxs" module="utils" /> <wxs src="../wxs/utils.wxs" module="utils" />
<view class="custom-class van-tabs van-tabs--{{ type }}"> <view class="custom-class {{ utils.bem('tabs', [type]) }}">
<view style="z-index: {{ zIndex }}; {{ wrapStyle }}" class="van-tabs__wrap {{ scrollable ? 'van-tabs__wrap--scrollable' : '' }} {{ type === 'line' && border ? 'van-hairline--top-bottom' : '' }}"> <view style="z-index: {{ zIndex }}; {{ wrapStyle }}" class="{{ utils.bem('tabs__wrap', { scrollable }) }} {{ type === 'line' && border ? 'van-hairline--top-bottom' : '' }}">
<slot name="nav-left" />
<scroll-view <scroll-view
scroll-x="{{ scrollable }}" scroll-x="{{ scrollable }}"
scroll-with-animation scroll-with-animation
@ -30,6 +32,8 @@
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
<slot name="nav-right" />
</view> </view>
<view <view
class="van-tabs__content" class="van-tabs__content"

104
types/wx.d.ts vendored
View File

@ -74,19 +74,19 @@ interface AnimationOptions {
/** /**
* ms * ms
*/ */
duration?: number duration: number
/** /**
* *
*/ */
timingFunction?: TimingFunction timingFunction: TimingFunction
/** /**
* ms * ms
*/ */
delay?: number delay: number
/** /**
* transform-origin * transform-origin
*/ */
transformOrigin?: string transformOrigin: string
} }
interface Animation { interface Animation {
@ -98,7 +98,7 @@ interface Animation {
/** /**
* *
*/ */
step(object: AnimationOptions): void step(object: Partial<AnimationOptions>): void
/** /**
* 0~1 * 0~1
@ -226,27 +226,51 @@ interface FieldsOptions {
/** /**
* id * id
*/ */
id?: boolean id: boolean
/** /**
* dataset * dataset
*/ */
dataset?: boolean dataset: boolean
rect?: boolean rect: boolean
size?: boolean size: boolean
scrollOffset?: boolean scrollOffset: boolean
properties?: string[] properties: string[]
computedStyle?: string[] computedStyle: string[]
context?: boolean context: boolean
} }
interface BoundingClientRect { interface BoundingClientRect {
/**
* ID
*/
id: string id: string
/**
* dataset
*/
dataset: object dataset: object
/**
*
*/
left: number left: number
/**
*
*/
right: number right: number
/**
*
*/
top: number top: number
/**
*
*/
bottom: number bottom: number
/**
*
*/
width: number width: number
/**
*
*/
height: number height: number
} }
@ -292,7 +316,7 @@ interface NodesRef {
/** /**
* fields中指定 nodesRef selectorQuery * fields中指定 nodesRef selectorQuery
*/ */
fields(object: FieldsOptions): object fields(object: Partial<FieldsOptions>): object
/** /**
* DOM getBoundingClientRect NodesRef SelectorQuery * DOM getBoundingClientRect NodesRef SelectorQuery
@ -342,15 +366,15 @@ interface ObserverOptions {
/** /**
* *
*/ */
thresholds?: number[] thresholds: number[]
/** /**
* *
*/ */
initialRatio?: number initialRatio: number
/** /**
* true observe targetSelector * true observe targetSelector
*/ */
observeAll?: boolean observeAll: boolean
} }
interface Margins { interface Margins {
@ -381,12 +405,12 @@ interface IntersectionObserver {
/** /**
* 使 * 使
*/ */
relativeTo(selector: string, object: Margins): IntersectionObserver relativeTo(selector: string, object: Partial<Margins>): IntersectionObserver
/** /**
* *
*/ */
relativeToViewport(object: Margins): IntersectionObserver relativeToViewport(object?: Partial<Margins>): IntersectionObserver
/** /**
* *
@ -438,7 +462,7 @@ interface Wx {
/** /**
* animation export animation * animation export animation
*/ */
createAnimation(object: AnimationOptions): Animation createAnimation(object: Partial<AnimationOptions>): Animation
/** /**
* SelectorQuery 使 this.createSelectorQuery() * SelectorQuery 使 this.createSelectorQuery()
@ -447,8 +471,46 @@ interface Wx {
createIntersectionObserver( createIntersectionObserver(
component: Weapp.Component, component: Weapp.Component,
options: ObserverOptions options?: Partial<ObserverOptions>
): IntersectionObserver ): IntersectionObserver
/**
*
*/
showToast(options: {
/**
*
*/
title: string
/**
*
*/
icon?: 'success' | 'loading' | 'none'
/**
* image icon
*/
image?: string
/**
*
*/
duration?: number
/**
* 穿
*/
mask?: boolean
/**
*
*/
success?: () => void
/**
*
*/
fail?: () => void
/**
*
*/
complete?: () => void
}): void
} }
declare const wx: Wx; declare const wx: Wx;