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',
content: 'Tab 2 content'
}],
tabIndex: 2
tabIndex: 2,
tabPosition: 'top'
}
},
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
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 |
| active-name(deprecated) | 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
| Event Name | Description | Parameters |

View File

@ -24,7 +24,8 @@
name: '2',
content: 'Tab 2 content'
}],
tabIndex: 2
tabIndex: 2,
tabPosition: 'top'
}
},
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` 来实现自定义标签页的内容
@ -339,6 +374,7 @@
| editable | 标签是否同时可增加和关闭 | boolean | — | false |
| active-name(deprecated) | 选中选项卡的 name | string | — | 第一个选项卡的 name |
| value | 绑定值,选中选项卡的 name | string | — | 第一个选项卡的 name |
| tab-position | 选项卡所在位置 | string | top/right/bottom/left | top |
### Tabs Events
| 事件名称 | 说明 | 回调参数 |

View File

@ -9,6 +9,8 @@
tabs: Array
},
inject: ['rootTabs'],
computed: {
barStyle: {
cache: false,
@ -16,23 +18,27 @@
if (!this.$parent.$refs.tabs) return {};
let style = {};
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) => {
let $el = this.$parent.$refs.tabs[index];
if (!$el) { return false; }
if (!tab.active) {
offset += $el.clientWidth;
offset += $el[`client${firstUpperCase(sizeName)}`];
return true;
} else {
tabWidth = $el.clientWidth;
tabSize = $el[`client${firstUpperCase(sizeName)}`];
return false;
}
});
const transform = `translateX(${offset}px)`;
style.width = tabWidth + 'px';
const transform = `translate${firstUpperCase(sizeDir)}(${offset}px)`;
style[sizeName] = tabSize + 'px';
style.transform = transform;
style.msTransform = transform;
style.webkitTransform = transform;

View File

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

View File

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

View File

@ -48,6 +48,7 @@
@when scrollable {
padding: 0 15px;
box-sizing: border-box;
}
}
@e nav-scroll {
@ -76,7 +77,7 @@
padding: 0 16px;
height: 42px;
box-sizing: border-box;
line-height: @height;
line-height: 40px;
display: inline-block;
list-style: none;
font-size: 14px;
@ -176,19 +177,164 @@
>.el-tabs__header .el-tabs__item {
transition: all .3s cubic-bezier(.645,.045,.355,1);
border: 1px solid transparent;
border-top: 0;
margin: * -1px;
margin: -1px -1px 0;
&.is-active {
background-color: var(--color-white);
border-right-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 {
border-left-color: var(--color-base-gray);
.el-tabs__header,
.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);
});
});