Tab: add vertical tab (#6096)

* add tabs position

* add tabs test

* Update tabs.md
pull/6189/head
baiyaaaaa 2017-07-30 16:12:06 +08:00 committed by 杨奕
parent 5afe091c0e
commit 5c2589677a
7 changed files with 416 additions and 60 deletions

View File

@ -24,7 +24,8 @@
name: '2', name: '2',
content: 'Tab 2 content' content: 'Tab 2 content'
}], }],
tabIndex: 2 tabIndex: 2,
tabPosition: 'top'
} }
}, },
methods: { methods: {
@ -174,6 +175,40 @@ Border card tabs.
::: :::
### Tab position
You can use `tab-position` attribute to set the tab's position.
:::demo You can choose from four directions: `tabPosition="left|right|top|bottom"`
```html
<template>
<el-radio-group v-model="tabPosition" style="margin-bottom: 30px;">
<el-radio-button label="top">top</el-radio-button>
<el-radio-button label="right">right</el-radio-button>
<el-radio-button label="bottom">bottom</el-radio-button>
<el-radio-button label="left">left</el-radio-button>
</el-radio-group>
<el-tabs :tab-position="tabPosition" style="height: 200px;">
<el-tab-pane label="User">User</el-tab-pane>
<el-tab-pane label="Config">Config</el-tab-pane>
<el-tab-pane label="Role">Role</el-tab-pane>
<el-tab-pane label="Task">Task</el-tab-pane>
</el-tabs>
</template>
<script>
export default {
data() {
return {
tabPosition: 'top'
};
}
};
</script>
```
:::
### Custom Tab ### Custom Tab
You can use named slot to customize the tab label content. You can use named slot to customize the tab label content.
@ -341,6 +376,7 @@ Only card type Tabs support addable & closeable.
| editable | whether Tab is addable and closable | boolean | — | false | | editable | whether Tab is addable and closable | boolean | — | false |
| active-name(deprecated) | name of the selected tab | string | — | name of first tab | | active-name(deprecated) | name of the selected tab | string | — | name of first tab |
| value | name of the selected tab | string | — | name of first tab | | value | name of the selected tab | string | — | name of first tab |
| tab-position | position of tabs | string | top/right/bottom/left | top |
### Tabs Events ### Tabs Events
| Event Name | Description | Parameters | | Event Name | Description | Parameters |

View File

@ -24,7 +24,8 @@
name: '2', name: '2',
content: 'Tab 2 content' content: 'Tab 2 content'
}], }],
tabIndex: 2 tabIndex: 2,
tabPosition: 'top'
} }
}, },
methods: { methods: {
@ -172,6 +173,40 @@
``` ```
::: :::
### 位置
可以通过 `tab-position` 设置标签的位置
:::demo 标签一共有四个方向的设置 `tabPosition="left|right|top|bottom"`
```html
<template>
<el-radio-group v-model="tabPosition" style="margin-bottom: 30px;">
<el-radio-button label="top">top</el-radio-button>
<el-radio-button label="right">right</el-radio-button>
<el-radio-button label="bottom">bottom</el-radio-button>
<el-radio-button label="left">left</el-radio-button>
</el-radio-group>
<el-tabs :tab-position="tabPosition" style="height: 200px;">
<el-tab-pane label="用户管理">用户管理</el-tab-pane>
<el-tab-pane label="配置管理">配置管理</el-tab-pane>
<el-tab-pane label="角色管理">角色管理</el-tab-pane>
<el-tab-pane label="定时任务补偿">定时任务补偿</el-tab-pane>
</el-tabs>
</template>
<script>
export default {
data() {
return {
tabPosition: 'top'
};
}
};
</script>
```
:::
### 自定义标签页 ### 自定义标签页
可以通过具名 `slot` 来实现自定义标签页的内容 可以通过具名 `slot` 来实现自定义标签页的内容
@ -339,6 +374,7 @@
| editable | 标签是否同时可增加和关闭 | boolean | — | false | | editable | 标签是否同时可增加和关闭 | boolean | — | false |
| active-name(deprecated) | 选中选项卡的 name | string | — | 第一个选项卡的 name | | active-name(deprecated) | 选中选项卡的 name | string | — | 第一个选项卡的 name |
| value | 绑定值,选中选项卡的 name | string | — | 第一个选项卡的 name | | value | 绑定值,选中选项卡的 name | string | — | 第一个选项卡的 name |
| tab-position | 选项卡所在位置 | string | top/right/bottom/left | top |
### Tabs Events ### Tabs Events
| 事件名称 | 说明 | 回调参数 | | 事件名称 | 说明 | 回调参数 |

View File

@ -9,6 +9,8 @@
tabs: Array tabs: Array
}, },
inject: ['rootTabs'],
computed: { computed: {
barStyle: { barStyle: {
cache: false, cache: false,
@ -16,23 +18,27 @@
if (!this.$parent.$refs.tabs) return {}; if (!this.$parent.$refs.tabs) return {};
let style = {}; let style = {};
let offset = 0; let offset = 0;
let tabWidth = 0; let tabSize = 0;
const sizeName = ['top', 'bottom'].indexOf(this.rootTabs.tabPosition) !== -1 ? 'width' : 'height';
const sizeDir = sizeName === 'width' ? 'x' : 'y';
const firstUpperCase = str => {
return str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase());
};
this.tabs.every((tab, index) => { this.tabs.every((tab, index) => {
let $el = this.$parent.$refs.tabs[index]; let $el = this.$parent.$refs.tabs[index];
if (!$el) { return false; } if (!$el) { return false; }
if (!tab.active) { if (!tab.active) {
offset += $el.clientWidth; offset += $el[`client${firstUpperCase(sizeName)}`];
return true; return true;
} else { } else {
tabWidth = $el.clientWidth; tabSize = $el[`client${firstUpperCase(sizeName)}`];
return false; return false;
} }
}); });
const transform = `translateX(${offset}px)`; const transform = `translate${firstUpperCase(sizeDir)}(${offset}px)`;
style.width = tabWidth + 'px'; style[sizeName] = tabSize + 'px';
style.transform = transform; style.transform = transform;
style.msTransform = transform; style.msTransform = transform;
style.webkitTransform = transform; style.webkitTransform = transform;

View File

@ -3,6 +3,9 @@
import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event'; import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event';
function noop() {} function noop() {}
const firstUpperCase = str => {
return str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase());
};
export default { export default {
name: 'TabNav', name: 'TabNav',
@ -11,6 +14,8 @@
TabBar TabBar
}, },
inject: ['rootTabs'],
props: { props: {
panes: Array, panes: Array,
currentName: String, currentName: String,
@ -29,37 +34,47 @@
data() { data() {
return { return {
scrollable: false, scrollable: false,
navStyle: { navOffset: 0
transform: ''
}
}; };
}, },
computed: {
navStyle() {
const dir = ['top', 'bottom'].indexOf(this.rootTabs.tabPosition) !== -1 ? 'X' : 'Y';
return {
transform: `translate${dir}(-${this.navOffset}px)`
};
},
sizeName() {
return ['top', 'bottom'].indexOf(this.rootTabs.tabPosition) !== -1 ? 'width' : 'height';
}
},
methods: { methods: {
scrollPrev() { scrollPrev() {
const containerWidth = this.$refs.navScroll.offsetWidth; const containerSize = this.$refs.navScroll[`offset${firstUpperCase(this.sizeName)}`];
const currentOffset = this.getCurrentScrollOffset(); const currentOffset = this.navOffset;
if (!currentOffset) return; if (!currentOffset) return;
let newOffset = currentOffset > containerWidth let newOffset = currentOffset > containerSize
? currentOffset - containerWidth ? currentOffset - containerSize
: 0; : 0;
this.setOffset(newOffset); this.navOffset = newOffset;
}, },
scrollNext() { scrollNext() {
const navWidth = this.$refs.nav.offsetWidth; const navSize = this.$refs.nav[`offset${firstUpperCase(this.sizeName)}`];
const containerWidth = this.$refs.navScroll.offsetWidth; const containerSize = this.$refs.navScroll[`offset${firstUpperCase(this.sizeName)}`];
const currentOffset = this.getCurrentScrollOffset(); const currentOffset = this.navOffset;
if (navWidth - currentOffset <= containerWidth) return; if (navSize - currentOffset <= containerSize) return;
let newOffset = navWidth - currentOffset > containerWidth * 2 let newOffset = navSize - currentOffset > containerSize * 2
? currentOffset + containerWidth ? currentOffset + containerSize
: (navWidth - containerWidth); : (navSize - containerSize);
this.setOffset(newOffset); this.navOffset = newOffset;
}, },
scrollToActiveTab() { scrollToActiveTab() {
if (!this.scrollable) return; if (!this.scrollable) return;
@ -69,7 +84,7 @@
const activeTabBounding = activeTab.getBoundingClientRect(); const activeTabBounding = activeTab.getBoundingClientRect();
const navScrollBounding = navScroll.getBoundingClientRect(); const navScrollBounding = navScroll.getBoundingClientRect();
const navBounding = nav.getBoundingClientRect(); const navBounding = nav.getBoundingClientRect();
const currentOffset = this.getCurrentScrollOffset(); const currentOffset = this.navOffset;
let newOffset = currentOffset; let newOffset = currentOffset;
if (activeTabBounding.left < navScrollBounding.left) { if (activeTabBounding.left < navScrollBounding.left) {
@ -81,34 +96,27 @@
if (navBounding.right < navScrollBounding.right) { if (navBounding.right < navScrollBounding.right) {
newOffset = nav.offsetWidth - navScrollBounding.width; newOffset = nav.offsetWidth - navScrollBounding.width;
} }
this.setOffset(Math.max(newOffset, 0)); this.navOffset = Math.max(newOffset, 0);
},
getCurrentScrollOffset() {
const { navStyle } = this;
return navStyle.transform
? Number(navStyle.transform.match(/translateX\(-(\d+(\.\d+)*)px\)/)[1])
: 0;
},
setOffset(value) {
this.navStyle.transform = `translateX(-${value}px)`;
}, },
update() { update() {
const navWidth = this.$refs.nav.offsetWidth; if (!this.$refs.nav) return;
const containerWidth = this.$refs.navScroll.offsetWidth; const sizeName = this.sizeName;
const currentOffset = this.getCurrentScrollOffset(); const navSize = this.$refs.nav[`offset${firstUpperCase(sizeName)}`];
const containerSize = this.$refs.navScroll[`offset${firstUpperCase(sizeName)}`];
const currentOffset = this.navOffset;
if (containerWidth < navWidth) { if (containerSize < navSize) {
const currentOffset = this.getCurrentScrollOffset(); const currentOffset = this.navOffset;
this.scrollable = this.scrollable || {}; this.scrollable = this.scrollable || {};
this.scrollable.prev = currentOffset; this.scrollable.prev = currentOffset;
this.scrollable.next = currentOffset + containerWidth < navWidth; this.scrollable.next = currentOffset + containerSize < navSize;
if (navWidth - currentOffset < containerWidth) { if (navSize - currentOffset < containerSize) {
this.setOffset(navWidth - containerWidth); this.navOffset = navSize - containerSize;
} }
} else { } else {
this.scrollable = false; this.scrollable = false;
if (currentOffset > 0) { if (currentOffset > 0) {
this.setOffset(0); this.navOffset = 0;
} }
} }
} }

View File

@ -14,7 +14,17 @@
closable: Boolean, closable: Boolean,
addable: Boolean, addable: Boolean,
value: {}, value: {},
editable: Boolean editable: Boolean,
tabPosition: {
type: String,
default: 'top'
}
},
provide() {
return {
rootTabs: this
};
}, },
data() { data() {
@ -83,7 +93,8 @@
currentName, currentName,
panes, panes,
editable, editable,
addable addable,
tabPosition
} = this; } = this;
const newButton = editable || addable const newButton = editable || addable
@ -108,20 +119,26 @@
}, },
ref: 'nav' ref: 'nav'
}; };
const header = (
<div class="el-tabs__header">
{newButton}
<tab-nav { ...navData }></tab-nav>
</div>
);
const panels = (
<div class="el-tabs__content">
{this.$slots.default}
</div>
);
return ( return (
<div class={{ <div class={{
'el-tabs': true, 'el-tabs': true,
'el-tabs--card': type === 'card', 'el-tabs--card': type === 'card',
[`el-tabs--${tabPosition}`]: true,
'el-tabs--border-card': type === 'border-card' 'el-tabs--border-card': type === 'border-card'
}}> }}>
<div class="el-tabs__header"> { tabPosition !== 'bottom' ? [header, panels] : [panels, header] }
{newButton}
<tab-nav { ...navData }></tab-nav>
</div>
<div class="el-tabs__content">
{this.$slots.default}
</div>
</div> </div>
); );
}, },

View File

@ -48,6 +48,7 @@
@when scrollable { @when scrollable {
padding: 0 15px; padding: 0 15px;
box-sizing: border-box;
} }
} }
@e nav-scroll { @e nav-scroll {
@ -76,7 +77,7 @@
padding: 0 16px; padding: 0 16px;
height: 42px; height: 42px;
box-sizing: border-box; box-sizing: border-box;
line-height: @height; line-height: 40px;
display: inline-block; display: inline-block;
list-style: none; list-style: none;
font-size: 14px; font-size: 14px;
@ -176,19 +177,164 @@
>.el-tabs__header .el-tabs__item { >.el-tabs__header .el-tabs__item {
transition: all .3s cubic-bezier(.645,.045,.355,1); transition: all .3s cubic-bezier(.645,.045,.355,1);
border: 1px solid transparent; border: 1px solid transparent;
border-top: 0; margin: -1px -1px 0;
margin: * -1px;
&.is-active { &.is-active {
background-color: var(--color-white); background-color: var(--color-white);
border-right-color: var(--color-base-gray); border-right-color: var(--color-base-gray);
border-left-color: var(--color-base-gray); border-left-color: var(--color-base-gray);
}
}
}
@m bottom {
.el-tabs__header {
margin-bottom: 0;
margin-top: 10px;
}
&.el-tabs--border-card {
.el-tabs__header {
border-bottom: 0;
border-top: 1px solid var(--color-base-gray);
}
.el-tabs__nav-wrap {
margin-top: -1px;
margin-bottom: 0;
}
.el-tabs__item {
border: 1px solid transparent;
margin: 0 -1px -1px -1px;
}
}
}
@m left, right {
overflow: hidden;
&:first-child { .el-tabs__header,
border-left-color: var(--color-base-gray); .el-tabs__nav-wrap,
.el-tabs__nav-scroll {
height: 100%;
}
.el-tabs__active-bar {
top: 0;
bottom: auto;
width: 3px;
height: auto;
}
.el-tabs__nav-wrap {
margin-bottom: 0;
&.is-scrollable {
padding: 30px 0;
}
}
.el-tabs__nav {
float: none;
}
.el-tabs__item {
display: block;
}
.el-tabs__nav-prev,
.el-tabs__nav-next {
height: 30px;
line-height: 30px;
width: 100%;
text-align: center;
cursor: pointer;
i {
transform: rotateZ(90deg);
}
}
.el-tabs__nav-prev {
left: auto;
top: 0;
}
.el-tabs__nav-next {
right: auto;
bottom: 0;
}
}
@m left {
.el-tabs__header {
float: left;
border-bottom: none;
border-right: 1px solid var(--color-base-gray);
margin-bottom: 0;
margin-right: 10px;
}
.el-tabs__nav-wrap {
margin-right: -1px;
}
.el-tabs__active-bar {
right: 0;
left: auto;
}
.el-tabs__item {
text-align: right;
}
&.el-tabs--card {
.el-tabs__item.is-active {
border: 1px solid rgb(209, 219, 229);
border-right-color: #fff;
border-radius: 4px 0 0 4px;
}
.el-tabs__new-tab {
float: none;
}
}
&.el-tabs--border-card {
.el-tabs__item {
border: 1px solid transparent;
margin: -1px 0 -1px -1px;
&.is-active {
border-color: transparent;
border-top-color: rgb(209, 219, 229);
border-bottom-color: rgb(209, 219, 229);
} }
&:last-child { }
border-right-color: var(--color-base-gray); }
}
@m right {
.el-tabs__header {
float: right;
border-bottom: none;
border-left: 1px solid var(--color-base-gray);
margin-bottom: 0;
margin-left: 10px;
}
.el-tabs__nav-wrap {
margin-left: -1px;
}
.el-tabs__active-bar {
left: 0;
}
&.el-tabs--card {
.el-tabs__item.is-active {
border: 1px solid rgb(209, 219, 229);
border-left-color: #fff;
border-radius: 0 4px 4px 0;
}
}
&.el-tabs--border-card {
.el-tabs__item {
border: 1px solid transparent;
margin: -1px -1px -1px 0;
&.is-active {
border-color: transparent;
border-top-color: rgb(209, 219, 229);
border-bottom-color: rgb(209, 219, 229);
} }
} }
} }

View File

@ -348,4 +348,111 @@ describe('Tabs', () => {
}); });
}); });
}); });
it('tab-position', done => {
vm = createVue({
template: `
<el-tabs ref="tabs" tab-position="left">
<el-tab-pane label="用户管理">A</el-tab-pane>
<el-tab-pane label="配置管理">B</el-tab-pane>
<el-tab-pane label="角色管理" ref="pane-click">C</el-tab-pane>
<el-tab-pane label="定时任务补偿">D</el-tab-pane>
</el-tabs>
`
}, true);
let paneList = vm.$el.querySelector('.el-tabs__content').children;
let spy = sinon.spy();
vm.$refs.tabs.$on('tab-click', spy);
setTimeout(_ => {
const tabList = vm.$refs.tabs.$refs.nav.$refs.tabs;
expect(tabList[0].classList.contains('is-active')).to.be.true;
expect(paneList[0].style.display).to.not.ok;
tabList[2].click();
vm.$nextTick(_ => {
expect(spy.withArgs(vm.$refs['pane-click']).calledOnce).to.true;
expect(tabList[2].classList.contains('is-active')).to.be.true;
expect(paneList[2].style.display).to.not.ok;
done();
});
}, 100);
});
it('horizonal-scrollable', done => {
vm = createVue({
template: `
<el-tabs ref="tabs" style="width: 200px;">
<el-tab-pane label="用户管理">A</el-tab-pane>
<el-tab-pane label="配置管理">B</el-tab-pane>
<el-tab-pane label="用户管理">A</el-tab-pane>
<el-tab-pane label="配置管理">B</el-tab-pane>
<el-tab-pane label="用户管理">A</el-tab-pane>
<el-tab-pane label="配置管理">B</el-tab-pane>
<el-tab-pane label="定时任务补偿">D</el-tab-pane>
</el-tabs>
`
}, true);
setTimeout(_ => {
const btnPrev = vm.$el.querySelector('.el-tabs__nav-prev');
btnPrev.click();
vm.$nextTick(_ => {
const tabNav = vm.$el.querySelector('.el-tabs__nav-wrap');
expect(tabNav.__vue__.navOffset).to.be.equal(0);
const btnNext = vm.$el.querySelector('.el-tabs__nav-next');
btnNext.click();
vm.$nextTick(_ => {
expect(tabNav.__vue__.navOffset).to.not.be.equal(0);
btnPrev.click();
vm.$nextTick(_ => {
expect(tabNav.__vue__.navOffset).to.be.equal(0);
done();
});
});
});
}, 100);
});
it('vertical-scrollable', done => {
vm = createVue({
template: `
<el-tabs ref="tabs" tab-position="left" style="height: 200px;">
<el-tab-pane label="用户管理">A</el-tab-pane>
<el-tab-pane label="配置管理">B</el-tab-pane>
<el-tab-pane label="用户管理">A</el-tab-pane>
<el-tab-pane label="配置管理">B</el-tab-pane>
<el-tab-pane label="用户管理">A</el-tab-pane>
<el-tab-pane label="配置管理">B</el-tab-pane>
<el-tab-pane label="定时任务补偿">D</el-tab-pane>
</el-tabs>
`
}, true);
setTimeout(_ => {
const btnPrev = vm.$el.querySelector('.el-tabs__nav-prev');
btnPrev.click();
vm.$nextTick(_ => {
const tabNav = vm.$el.querySelector('.el-tabs__nav-wrap');
expect(tabNav.__vue__.navOffset).to.be.equal(0);
const btnNext = vm.$el.querySelector('.el-tabs__nav-next');
btnNext.click();
vm.$nextTick(_ => {
expect(tabNav.__vue__.navOffset).to.not.be.equal(0);
btnPrev.click();
vm.$nextTick(_ => {
expect(tabNav.__vue__.navOffset).to.be.equal(0);
done();
});
});
});
}, 100);
});
}); });