[new feature] Tab: add name prop (#3762)

This commit is contained in:
neverland 2019-07-05 14:14:20 +08:00 committed by GitHub
parent 2389ac06ba
commit b802047e24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 243 additions and 84 deletions

View File

@ -32,6 +32,26 @@ export default {
}
```
### Match By Name
```html
<van-tabs v-model="activeName">
<van-tab title="tab 1" name="a">content of tab 1</van-tab>
<van-tab title="tab 2" name="b">content of tab 2</van-tab>
<van-tab title="tab 3" name="c">content of tab 3</van-tab>
</van-tabs>
```
```js
export default {
data() {
return {
activeName: 'a'
};
}
}
```
### Swipe Tabs
By default more than 4 tabs, you can scroll through the tabs. You can set `swipe-threshold` attribute to customize threshold number.
@ -59,7 +79,7 @@ You can set `disabled` attribute on the corresponding `van-tab`.
```javascript
export default {
methods: {
onClickDisabled(index, title) {
onClickDisabled(name, title) {
this.$toast(title + ' is disabled');
}
}
@ -91,7 +111,7 @@ Tabs styled as cards.
```javascript
export default {
methods: {
onClick(index, title) {
onClick(name, title) {
this.$toast(title);
}
}
@ -155,7 +175,7 @@ In swipeable mode, you can switch tabs with swipe gestrue in the content
| Attribute | Description | Type | Default |
|------|------|------|------|
| v-model | Index of active tab | `String` `Number` | `0` |
| v-model | Index of active tab | `String | Number` | `0` |
| type | Can be set to `line` `card` | `String` | `line` |
| duration | Toggle tab's animation time | `Number` | `0.3` | - |
| background | Background color | `String` | `white` |
@ -177,6 +197,7 @@ In swipeable mode, you can switch tabs with swipe gestrue in the content
| Attribute | Description | Type | Default |
|------|------|------|------|
| name | Identifier | `String | Number` | Index of tab |
| title | Title | `String` | - |
| disabled | Whether to disable tab | `Boolean` | `false` |
@ -198,7 +219,7 @@ In swipeable mode, you can switch tabs with swipe gestrue in the content
| Event | Description | Arguments |
|------|------|------|
| click | Triggered when click tab | indexindex of current tabtitle: tab title |
| change | Triggered when active tab changed | indexindex of current tabtitle: tab title |
| disabled | Triggered when click disabled tab | indexindex of current tab, title: tab title |
| click | Triggered when click tab | namename of current tabtitle: tab title |
| change | Triggered when active tab changed | namename of current tabtitle: tab title |
| disabled | Triggered when click disabled tab | namename of current tab, title: tab title |
| scroll | Triggered when tab scroll in sticky mode | Object: { scrollTop, isFixed } |

View File

@ -12,7 +12,7 @@ Vue.use(Tab).use(Tabs);
### 基础用法
默认情况下启用第一个标签,可以通过`v-model`绑定当前激活标签索引
通过`v-model`绑定当前激活标签对应的索引值,默认情况下启用第一个标签
```html
<van-tabs v-model="active">
@ -33,9 +33,31 @@ export default {
}
```
### 横向滚动
### 通过名称匹配
多于 4 个标签时Tab 可以横向滚动
在标签指定`name`属性的情况下,`v-model`的值为当前标签的`name`
```html
<van-tabs v-model="activeName">
<van-tab title="标签 1" name="a">内容 1</van-tab>
<van-tab title="标签 2" name="b">内容 2</van-tab>
<van-tab title="标签 3" name="c">内容 3</van-tab>
</van-tabs>
```
```js
export default {
data() {
return {
activeName: 'a'
};
}
}
```
### 标签栏滚动
标签数量超过 4 个时,标签栏可以在水平方向上滚动,切换时会自动将当前标签居中
```html
<van-tabs>
@ -60,8 +82,8 @@ export default {
```javascript
export default {
methods: {
onClickDisabled(index, title) {
this.$toast(title + '已被禁用');
onClickDisabled(name, title) {
this.$toast(name + '已被禁用');
}
}
};
@ -93,7 +115,7 @@ export default {
```javascript
export default {
methods: {
onClick(index, title) {
onClick(name, title) {
this.$toast(title);
}
}
@ -157,7 +179,7 @@ export default {
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|------|------|------|------|------|
| v-model | 当前标签的索引 | `String` `Number` | `0` | - |
| v-model | 绑定当前选中标签的标识符 | `String | Number` | `0` | - |
| type | 样式类型,可选值为`card` | `String` | `line` | - |
| duration | 动画时间,单位秒 | `Number` | `0.3` | - |
| background | 标签栏背景色 | `String` | `white` | 1.6.5 |
@ -179,6 +201,7 @@ export default {
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|------|------|------|------|------|
| name | 标签名称,作为匹配的标识符 | `String | Number` | 标签的索引值 | 2.0.6 |
| title | 标题 | `String` | - | - |
| disabled | 是否禁用标签 | `Boolean` | `false` | - |
@ -200,7 +223,7 @@ export default {
| 事件名 | 说明 | 回调参数 |
|------|------|------|
| click | 点击标签时触发 | index标签索引title标题 |
| change | 当前激活的标签改变时触发 | index标签索引title标题 |
| disabled | 点击被禁用的标签时触发 | index标签索引title标题 |
| click | 点击标签时触发 | name标签标识符title标题 |
| change | 当前激活的标签改变时触发 | name标签标识符title标题 |
| disabled | 点击被禁用的标签时触发 | name标签标识符title标题 |
| scroll | 滚动时触发,仅在 sticky 模式下生效 | { scrollTop: 距离顶部位置, isFixed: 是否吸顶 } |

View File

@ -12,8 +12,31 @@
</van-tabs>
</demo-block>
<demo-block :title="$t('matchByName')">
<van-tabs v-model="activeName">
<van-tab
name="a"
:title="$t('tab') + 1"
>
{{ $t('content') }} 1
</van-tab>
<van-tab
name="b"
:title="$t('tab') + 2"
>
{{ $t('content') }} 2
</van-tab>
<van-tab
name="c"
:title="$t('tab') + 3"
>
{{ $t('content') }} 3
</van-tab>
</van-tabs>
</demo-block>
<demo-block :title="$t('title2')">
<van-tabs @scroll="onScroll">
<van-tabs>
<van-tab
v-for="index in 8"
:title="$t('tab') + index"
@ -124,7 +147,7 @@ export default {
i18n: {
'zh-CN': {
tab: '标签 ',
title2: '横向滚动',
title2: '标签栏滚动',
title3: '禁用标签',
title4: '样式风格',
title5: '点击事件',
@ -132,7 +155,8 @@ export default {
title7: '自定义标签',
title8: '切换动画',
title9: '滑动切换',
disabled: ' 已被禁用'
disabled: ' 已被禁用',
matchByName: '通过名称匹配'
},
'en-US': {
tab: 'Tab ',
@ -145,13 +169,15 @@ export default {
title7: 'Custom Tab',
title8: 'Switch Animation',
title9: 'Swipeable',
disabled: ' is disabled'
disabled: ' is disabled',
matchByName: 'Match By Name'
}
},
data() {
return {
active: 2,
activeName: 'b',
tabs: [1, 2, 3, 4]
};
},
@ -163,10 +189,6 @@ export default {
onClick(index, title) {
this.$toast(title);
},
onScroll(e) {
console.log(e);
}
}
};

View File

@ -8,6 +8,7 @@ export default createComponent({
mixins: [ChildrenMixin('vanTabs')],
props: {
name: [String, Number],
title: String,
disabled: Boolean
},
@ -19,14 +20,18 @@ export default createComponent({
},
computed: {
selected() {
return this.index === this.parent.curActive;
computedName() {
return this.name || this.index;
},
isActive() {
return this.computedName === this.parent.currentName;
}
},
watch: {
'parent.curActive'() {
this.inited = this.inited || this.selected;
'parent.currentIndex'() {
this.inited = this.inited || this.isActive;
},
title() {
@ -41,7 +46,7 @@ export default createComponent({
},
render(h) {
const { slots, selected } = this;
const { slots, isActive } = this;
const shouldRender = this.inited || !this.parent.lazyRender;
const Content = [shouldRender ? slots() : h()];
@ -53,8 +58,8 @@ export default createComponent({
return (
<div
role="tabpanel"
aria-hidden={!selected}
class={bem('pane-wrapper', { inactive: !selected })}
aria-hidden={!isActive}
class={bem('pane-wrapper', { inactive: !isActive })}
>
<div class={bem('pane')}>{Content}</div>
</div>
@ -62,7 +67,7 @@ export default createComponent({
}
return (
<div vShow={selected} role="tabpanel" class={bem('pane')}>
<div vShow={isActive} role="tabpanel" class={bem('pane')}>
{Content}
</div>
);

View File

@ -29,6 +29,29 @@ exports[`renders demo correctly 1`] = `
</div>
</div>
</div>
<div>
<div class="van-tabs van-tabs--line">
<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"><span class="van-ellipsis">标签 1</span></div>
<div role="tab" aria-selected="true" class="van-tab van-tab--active"><span class="van-ellipsis">标签 2</span></div>
<div role="tab" class="van-tab"><span class="van-ellipsis">标签 3</span></div>
<div class="van-tabs__line" style="width: 0px; transform: translateX(0px) translateX(-50%);"></div>
</div>
</div>
<div class="van-tabs__content">
<div role="tabpanel" class="van-tab__pane" style="display: none;">
<!---->
</div>
<div role="tabpanel" class="van-tab__pane" style="">
内容 2
</div>
<div role="tabpanel" class="van-tab__pane" style="display: none;">
<!---->
</div>
</div>
</div>
</div>
<div>
<div class="van-tabs van-tabs--line">
<div class="van-tabs__wrap van-tabs__wrap--scrollable van-hairline--top-bottom">

View File

@ -136,6 +136,28 @@ exports[`lazy render 2`] = `
</div>
`;
exports[`name prop 1`] = `
<div class="van-tabs van-tabs--line">
<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"><span class="van-ellipsis">title1</span></div>
<div role="tab" class="van-tab"><span class="van-ellipsis">title2</span></div>
<div role="tab" class="van-tab van-tab--disabled"><span class="van-ellipsis">title3</span></div>
<div class="van-tabs__line"></div>
</div>
</div>
<div class="van-tabs__content">
<div role="tabpanel" class="van-tab__pane">Text</div>
<div role="tabpanel" class="van-tab__pane" style="display: none;">
<!---->
</div>
<div role="tabpanel" class="van-tab__pane" style="display: none;">
<!---->
</div>
</div>
</div>
`;
exports[`render nav-left & nav-right slot 1`] = `
<div class="van-tabs van-tabs--line">
<div class="van-tabs__wrap van-hairline--top-bottom">

View File

@ -145,3 +145,38 @@ test('border props', async () => {
expect(wrapper).toMatchSnapshot();
});
test('name prop', async () => {
const onClick = jest.fn();
const onChange = jest.fn();
const onDisabled = jest.fn();
const wrapper = mount({
template: `
<van-tabs @click="onClick" @disabled="onDisabled" @change="onChange">
<van-tab title="title1" name="a">Text</van-tab>
<van-tab title="title2" name="b">Text</van-tab>
<van-tab title="title3" name="c" disabled>Text</van-tab>
</van-tabs>
`,
methods: {
onClick,
onChange,
onDisabled
}
});
await later();
expect(wrapper).toMatchSnapshot();
const tabs = wrapper.findAll('.van-tab');
tabs.at(1).trigger('click');
expect(onClick).toHaveBeenCalledWith('b', 'title2');
expect(onChange).toHaveBeenCalledWith('b', 'title2');
expect(onChange).toHaveBeenCalledTimes(1);
tabs.at(2).trigger('click');
expect(onDisabled).toHaveBeenCalledWith('c', 'title3');
expect(onChange).toHaveBeenCalledTimes(1);
});

View File

@ -31,7 +31,7 @@ export default {
}
```
### Item Name
### Match by name
```html
<van-tabbar v-model="active">

View File

@ -9,7 +9,7 @@
</van-tabbar>
</demo-block>
<demo-block :title="$t('itemName')">
<demo-block :title="$t('matchByName')">
<van-tabbar v-model="activeName">
<van-tabbar-item
name="home"
@ -97,13 +97,13 @@ export default {
badge: '显示徽标',
customIcon: '自定义图标',
customColor: '自定义颜色',
itemName: '通过名称匹配'
matchByName: '通过名称匹配'
},
'en-US': {
badge: 'Show Badge',
customIcon: 'Custom Icon',
customColor: 'Custom Color',
itemName: 'Item Name'
matchByName: 'Match by name'
}
},

View File

@ -9,17 +9,17 @@ export default createComponent({
props: {
count: Number,
active: Number,
duration: Number,
animated: Boolean,
swipeable: Boolean
swipeable: Boolean,
currentIndex: Number
},
computed: {
style() {
if (this.animated) {
return {
transform: `translate3d(${-1 * this.active * 100}%, 0, 0)`,
transform: `translate3d(${-1 * this.currentIndex * 100}%, 0, 0)`,
transitionDuration: `${this.duration}s`
};
}
@ -40,15 +40,15 @@ export default createComponent({
methods: {
// watch swipe touch end
onTouchEnd() {
const { direction, deltaX, active } = this;
const { direction, deltaX, currentIndex } = this;
/* istanbul ignore else */
if (direction === 'horizontal' && this.offsetX >= MIN_SWIPE_DISTANCE) {
/* istanbul ignore else */
if (deltaX > 0 && active !== 0) {
this.$emit('change', active - 1);
} else if (deltaX < 0 && active !== this.count - 1) {
this.$emit('change', active + 1);
if (deltaX > 0 && currentIndex !== 0) {
this.$emit('change', currentIndex - 1);
} else if (deltaX < 0 && currentIndex !== this.count - 1) {
this.$emit('change', currentIndex + 1);
}
}
},

View File

@ -7,7 +7,7 @@ export default {
type: String,
color: String,
title: String,
active: Boolean,
isActive: Boolean,
ellipsis: Boolean,
disabled: Boolean,
scrollable: Boolean,
@ -19,7 +19,7 @@ export default {
computed: {
style() {
const style = {};
const { color, active } = this;
const { color, isActive } = this;
const isCard = this.type === 'card';
// card theme color
@ -27,7 +27,7 @@ export default {
style.borderColor = color;
if (!this.disabled) {
if (active) {
if (isActive) {
style.backgroundColor = color;
} else {
style.color = color;
@ -35,7 +35,7 @@ export default {
}
}
const titleColor = active ? this.activeColor : this.inactiveColor;
const titleColor = isActive ? this.activeColor : this.inactiveColor;
if (titleColor) {
style.color = titleColor;
}
@ -64,9 +64,9 @@ export default {
return (
<div
role="tab"
aria-selected={this.active}
aria-selected={this.isActive}
class={bem({
active: this.active,
active: this.isActive,
disabled: this.disabled,
complete: !this.ellipsis
})}

View File

@ -73,7 +73,7 @@ export default createComponent({
return {
position: '',
curActive: null,
currentIndex: null,
lineStyle: {
backgroundColor: this.color
}
@ -108,13 +108,21 @@ export default createComponent({
borderColor: this.color,
background: this.background
};
},
currentName() {
const activeTab = this.children[this.currentIndex];
if (activeTab) {
return activeTab.computedName;
}
}
},
watch: {
active(val) {
if (val !== this.curActive) {
this.correctActive(val);
active(name) {
if (name !== this.currentName) {
this.setCurrentIndexByName(name);
}
},
@ -123,12 +131,12 @@ export default createComponent({
},
children() {
this.correctActive(this.curActive || this.active);
this.setCurrentIndexByName(this.currentName || this.active);
this.scrollIntoView();
this.setLine();
},
curActive() {
currentIndex() {
this.scrollIntoView();
this.setLine();
@ -201,11 +209,11 @@ export default createComponent({
this.$nextTick(() => {
const { titles } = this.$refs;
if (!titles || !titles[this.curActive] || this.type !== 'line') {
if (!titles || !titles[this.currentIndex] || this.type !== 'line') {
return;
}
const title = titles[this.curActive].$el;
const title = titles[this.currentIndex].$el;
const { lineWidth, lineHeight } = this;
const width = isDef(lineWidth) ? lineWidth : title.offsetWidth / 2;
const left = title.offsetLeft + title.offsetWidth / 2;
@ -230,47 +238,47 @@ export default createComponent({
});
},
// correct the value of active
correctActive(active) {
active = +active;
const exist = this.children.some(tab => tab.index === active);
const defaultActive = (this.children[0] || {}).index || 0;
this.setCurActive(exist ? active : defaultActive);
// correct the index of active tab
setCurrentIndexByName(name) {
const matched = this.children.filter(tab => tab.computedName === name);
const defaultIndex = (this.children[0] || {}).index || 0;
this.setCurrentIndex(matched.length ? matched[0].index : defaultIndex);
},
setCurActive(active) {
active = this.findAvailableTab(active, active < this.curActive);
if (isDef(active) && active !== this.curActive) {
this.$emit('input', active);
setCurrentIndex(currentIndex) {
currentIndex = this.findAvailableTab(currentIndex);
if (this.curActive !== null) {
this.$emit('change', active, this.children[active].title);
if (isDef(currentIndex) && currentIndex !== this.currentIndex) {
const shouldEmitChange = this.currentIndex !== null;
this.currentIndex = currentIndex;
this.$emit('input', this.currentName);
if (shouldEmitChange) {
this.$emit('change', this.currentName, this.children[currentIndex].title);
}
this.curActive = active;
}
},
findAvailableTab(active, reverse) {
const diff = reverse ? -1 : 1;
let index = active;
findAvailableTab(index) {
const diff = index < this.currentIndex ? -1 : 1;
while (index >= 0 && index < this.children.length) {
if (!this.children[index].disabled) {
return index;
}
index += diff;
}
},
// emit event when clicked
onClick(index) {
const { title, disabled } = this.children[index];
const { title, disabled, name } = this.children[index];
if (disabled) {
this.$emit('disabled', index, title);
this.$emit('disabled', name, title);
} else {
this.setCurActive(index);
this.$emit('click', index, title);
this.setCurrentIndex(index);
this.$emit('click', name, title);
}
},
@ -278,12 +286,12 @@ export default createComponent({
scrollIntoView(immediate) {
const { titles } = this.$refs;
if (!this.scrollable || !titles || !titles[this.curActive]) {
if (!this.scrollable || !titles || !titles[this.currentIndex]) {
return;
}
const { nav } = this.$refs;
const title = titles[this.curActive].$el;
const title = titles[this.currentIndex].$el;
const to = title.offsetLeft - (nav.offsetWidth - title.offsetWidth) / 2;
scrollLeftTo(nav, to, immediate ? 0 : this.duration);
@ -307,7 +315,7 @@ export default createComponent({
type={type}
title={item.title}
color={this.color}
active={index === this.curActive}
isActive={index === this.currentIndex}
ellipsis={ellipsis}
disabled={item.disabled}
scrollable={scrollable}
@ -339,11 +347,11 @@ export default createComponent({
</div>
<Content
count={this.children.length}
active={this.curActive}
animated={animated}
duration={this.duration}
swipeable={this.swipeable}
onChange={this.setCurActive}
currentIndex={this.currentIndex}
onChange={this.setCurrentIndex}
>
{this.slots()}
</Content>