Browse Source

update tab api

baiyaaaaa 8 years ago
parent
commit
0bec05ada3

+ 210 - 37
examples/docs/en-US/tabs.md

@@ -2,9 +2,20 @@
   export default {
     data() {
       return {
-        activeName: 'first',
+        activeName: 'second',
         activeName2: 'first',
-        tabs: [{
+        editableTabsValue: '2',
+        editableTabsValue2: '2',
+        editableTabs: [{
+          title: 'Tab 1',
+          name: '1',
+          content: 'Tab 1 content'
+        }, {
+          title: 'Tab 2',
+          name: '2',
+          content: 'Tab 2 content'
+        }],
+        editableTabs2: [{
           title: 'Tab 1',
           name: '1',
           content: 'Tab 1 content'
@@ -17,11 +28,62 @@
       }
     },
     methods: {
-      handleRemove(tab) {
-        console.log(tab);
-      },
       handleClick(tab, event) {
         console.log(tab, event);
+      },
+      handleTabsEdit(targetName, action) {
+        if (action === 'add') {
+          let newTabName = ++this.tabIndex + '';
+          this.editableTabs.push({
+            title: 'New Tab',
+            name: newTabName,
+            content: 'New Tab content'
+          });
+          this.editableTabsValue = newTabName;
+        }
+        if (action === 'remove') {
+          let tabs = this.editableTabs;
+          let activeName = this.editableTabsValue;
+          if (activeName === targetName) {
+            tabs.forEach((tab, index) => {
+              if (tab.name === targetName) {
+                let nextTab = tabs[index + 1] || tabs[index - 1];
+                if (nextTab) {
+                  activeName = nextTab.name;
+                }
+              }
+            });
+          }
+          
+          this.editableTabsValue = activeName;
+          this.editableTabs = tabs.filter(tab => tab.name !== targetName);
+        }
+      },
+      addTab(targetName) {
+        let newTabName = ++this.tabIndex + '';
+        this.editableTabs2.push({
+          title: 'New Tab',
+          name: newTabName,
+          content: 'New Tab content'
+        });
+        this.editableTabsValue2 = newTabName;
+      },
+      removeTab(targetName) {
+        let tabs = this.editableTabs2;
+        let activeName = this.editableTabsValue2;
+        if (activeName === targetName) {
+          tabs.forEach((tab, index) => {
+            if (tab.name === targetName) {
+              let nextTab = tabs[index + 1] || tabs[index - 1];
+              if (nextTab) {
+                activeName = nextTab.name;
+              }
+            }
+          });
+        }
+        
+        this.editableTabsValue2 = activeName;
+        this.editableTabs2 = tabs.filter(tab => tab.name !== targetName);
       }
     }
   }
@@ -95,37 +157,6 @@ Tabs styled as cards.
 ```
 :::
 
-### Closable
-
-Closable tabs.
-
-:::demo You can set the closable attribute in el-tabs to make all tabs closable. Also, closable can be set in a tab panel to make that specific tab closable.
-
-```html
-<template>
-  <el-tabs type="card" :closable="true" @tab-click="handleClick" @tab-remove="handleRemove">
-    <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 {
-    methods: {
-      handleRemove(tab) {
-        console.log(tab);
-      },
-      handleClick(tab, event) {
-        console.log(tab, event);
-      }
-    }
-  };
-</script>
-```
-
-:::
-
 ### Border card
 
 Border card tabs.
@@ -161,11 +192,151 @@ You can use named slot to customize the tab label content.
 ```
 :::
 
+### Add & close tab
+
+Only card type Tabs support addable & closeable.
+
+:::demo
+```html
+<el-tabs v-model="editableTabsValue" type="card" editable @edit="handleTabsEdit">
+  <el-tab-pane
+    v-for="(item, index) in editableTabs"
+    :label="item.title"
+    :name="item.name"
+  >
+    {{item.content}}
+  </el-tab-pane>
+</el-tabs>
+<script>
+  export default {
+    data() {
+      return {
+        editableTabsValue: '2',
+        editableTabs: [{
+          title: 'Tab 1',
+          name: '1',
+          content: 'Tab 1 content'
+        }, {
+          title: 'Tab 2',
+          name: '2',
+          content: 'Tab 2 content'
+        }],
+        tabIndex: 2
+      }
+    },
+    methods: {
+      handleTabsEdit(targetName, action) {
+        if (action === 'add') {
+          let newTabName = ++this.tabIndex + '';
+          this.editableTabs.push({
+            title: 'New Tab',
+            name: newTabName,
+            content: 'New Tab content'
+          });
+          this.editableTabsValue = newTabName;
+        }
+        if (action === 'remove') {
+          let tabs = this.editableTabs;
+          let activeName = this.editableTabsValue;
+          if (activeName === targetName) {
+            tabs.forEach((tab, index) => {
+              if (tab.name === targetName) {
+                let nextTab = tabs[index + 1] || tabs[index - 1];
+                if (nextTab) {
+                  activeName = nextTab.name;
+                }
+              }
+            });
+          }
+          
+          this.editableTabsValue = activeName;
+          this.editableTabs = tabs.filter(tab => tab.name !== targetName);
+        }
+      }
+    }
+  }
+</script>
+```
+:::
+
+### Customized trigger button of new tab
+
+:::demo
+```html
+<div style="margin-bottom: 20px;">
+  <el-button
+    size="small"
+    @click="addTab(editableTabsValue2)"
+  >
+    add tab
+  </el-button>
+</div>
+<el-tabs v-model="editableTabsValue2" type="card" closable @tab-remove="removeTab">
+  <el-tab-pane
+    v-for="(item, index) in editableTabs2"
+    :label="item.title"
+    :name="item.name"
+  >
+    {{item.content}}
+  </el-tab-pane>
+</el-tabs>
+<script>
+  export default {
+    data() {
+      return {
+        editableTabsValue2: '2',
+        editableTabs2: [{
+          title: 'Tab 1',
+          name: '1',
+          content: 'Tab 1 content'
+        }, {
+          title: 'Tab 2',
+          name: '2',
+          content: 'Tab 2 content'
+        }],
+        tabIndex: 2
+      }
+    },
+    methods: {
+      addTab(targetName) {
+        let newTabName = ++this.tabIndex + '';
+        this.editableTabs2.push({
+          title: 'New Tab',
+          name: newTabName,
+          content: 'New Tab content'
+        });
+        this.editableTabsValue2 = newTabName;
+      },
+      removeTab(targetName) {
+        let tabs = this.editableTabs2;
+        let activeName = this.editableTabsValue2;
+        if (activeName === targetName) {
+          tabs.forEach((tab, index) => {
+            if (tab.name === targetName) {
+              let nextTab = tabs[index + 1] || tabs[index - 1];
+              if (nextTab) {
+                activeName = nextTab.name;
+              }
+            }
+          });
+        }
+        
+        this.editableTabsValue2 = activeName;
+        this.editableTabs2 = tabs.filter(tab => tab.name !== targetName);
+      }
+    }
+  }
+</script>
+```
+:::
+
 ### Tabs Attributes
 | Attribute      | Description          | Type      | Accepted Values       | Default  |
 |---------- |-------- |---------- |-------------  |-------- |
 | type     | type of Tab | string   | card/border-card  |     —    |
 | closable  | whether Tab is closable | boolean   | — |  false  |
+| addable  | whether Tab is addable   | boolean   | — |  false  |
+| 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 |
 
@@ -173,7 +344,9 @@ You can use named slot to customize the tab label content.
 | Event Name | Description | Parameters |
 |---------- |-------- |---------- |
 | tab-click  | triggers when a tab is clicked | clicked tab |
-| tab-remove  | triggers when a tab is removed  | removed tab |
+| tab-remove  | triggers when tab-remove button is clicked | name of the removed tab |
+| tab-add  | triggers when tab-add button is clicked  | — |
+| edit  | triggers when tab-add button or tab-remove is clicked | (targetName, action) |
 
 ### Tab-pane Attributes
 | Attribute      | Description          | Type      | Accepted Values       | Default  |

+ 200 - 41
examples/docs/zh-CN/tabs.md

@@ -4,7 +4,18 @@
       return {
         activeName: 'second',
         activeName2: 'first',
-        tabs: [{
+        editableTabsValue: '2',
+        editableTabsValue2: '2',
+        editableTabs: [{
+          title: 'Tab 1',
+          name: '1',
+          content: 'Tab 1 content'
+        }, {
+          title: 'Tab 2',
+          name: '2',
+          content: 'Tab 2 content'
+        }],
+        editableTabs2: [{
           title: 'Tab 1',
           name: '1',
           content: 'Tab 1 content'
@@ -17,11 +28,62 @@
       }
     },
     methods: {
-      handleRemove(tab) {
-        console.log(tab);
-      },
       handleClick(tab, event) {
         console.log(tab, event);
+      },
+      handleTabsEdit(targetName, action) {
+        if (action === 'add') {
+          let newTabName = ++this.tabIndex + '';
+          this.editableTabs.push({
+            title: 'New Tab',
+            name: newTabName,
+            content: 'New Tab content'
+          });
+          this.editableTabsValue = newTabName;
+        }
+        if (action === 'remove') {
+          let tabs = this.editableTabs;
+          let activeName = this.editableTabsValue;
+          if (activeName === targetName) {
+            tabs.forEach((tab, index) => {
+              if (tab.name === targetName) {
+                let nextTab = tabs[index + 1] || tabs[index - 1];
+                if (nextTab) {
+                  activeName = nextTab.name;
+                }
+              }
+            });
+          }
+          
+          this.editableTabsValue = activeName;
+          this.editableTabs = tabs.filter(tab => tab.name !== targetName);
+        }
+      },
+      addTab(targetName) {
+        let newTabName = ++this.tabIndex + '';
+        this.editableTabs2.push({
+          title: 'New Tab',
+          name: newTabName,
+          content: 'New Tab content'
+        });
+        this.editableTabsValue2 = newTabName;
+      },
+      removeTab(targetName) {
+        let tabs = this.editableTabs2;
+        let activeName = this.editableTabsValue2;
+        if (activeName === targetName) {
+          tabs.forEach((tab, index) => {
+            if (tab.name === targetName) {
+              let nextTab = tabs[index + 1] || tabs[index - 1];
+              if (nextTab) {
+                activeName = nextTab.name;
+              }
+            }
+          });
+        }
+        
+        this.editableTabsValue2 = activeName;
+        this.editableTabs2 = tabs.filter(tab => tab.name !== targetName);
       }
     }
   }
@@ -95,36 +157,6 @@
 ```
 :::
 
-### 可关闭
-
-可以关闭标签页。
-
-:::demo 通过设置 `closable` 属性来打开 `Tabs` 的可关闭标签效果, `closable` 也可以设置在 `Tab Panel` 中实现部分标签页的可关闭效果。
-
-```html
-<template>
-  <el-tabs type="card" closable @tab-click="handleClick" @tab-remove="handleRemove">
-    <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 {
-    methods: {
-      handleRemove(tab) {
-        console.log(tab);
-      },
-      handleClick(tab, event) {
-        console.log(tab, event);
-      }
-    }
-  };
-</script>
-```
-:::
-
 ### 卡片化
 
 卡片化的标签页。
@@ -158,18 +190,141 @@
 ```
 :::
 
-### 动态增加标签页
+### 动态增减标签页
+
+增减标签页按钮只能在选项卡样式的标签页下使用
+
+:::demo
+```html
+<el-tabs v-model="editableTabsValue" type="card" editable @edit="handleTabsEdit">
+  <el-tab-pane
+    v-for="(item, index) in editableTabs"
+    :label="item.title"
+    :name="item.name"
+  >
+    {{item.content}}
+  </el-tab-pane>
+</el-tabs>
+<script>
+  export default {
+    data() {
+      return {
+        editableTabsValue: '2',
+        editableTabs: [{
+          title: 'Tab 1',
+          name: '1',
+          content: 'Tab 1 content'
+        }, {
+          title: 'Tab 2',
+          name: '2',
+          content: 'Tab 2 content'
+        }],
+        tabIndex: 2
+      }
+    },
+    methods: {
+      handleTabsEdit(targetName, action) {
+        if (action === 'add') {
+          let newTabName = ++this.tabIndex + '';
+          this.editableTabs.push({
+            title: 'New Tab',
+            name: newTabName,
+            content: 'New Tab content'
+          });
+          this.editableTabsValue = newTabName;
+        }
+        if (action === 'remove') {
+          let tabs = this.editableTabs;
+          let activeName = this.editableTabsValue;
+          if (activeName === targetName) {
+            tabs.forEach((tab, index) => {
+              if (tab.name === targetName) {
+                let nextTab = tabs[index + 1] || tabs[index - 1];
+                if (nextTab) {
+                  activeName = nextTab.name;
+                }
+              }
+            });
+          }
+          
+          this.editableTabsValue = activeName;
+          this.editableTabs = tabs.filter(tab => tab.name !== targetName);
+        }
+      }
+    }
+  }
+</script>
+```
+:::
 
-展示如何通过触发器来动态增加标签页
+### 自定义增加标签页触发器
 
 :::demo
 ```html
 <div style="margin-bottom: 20px;">
-  <el-button size="small" @click="tabs.push({ name: 'Tab ' + ++tabIndex, title: 'new Tab', content: 'new Tab content' })">add tab</el-button>
+  <el-button
+    size="small"
+    @click="addTab(editableTabsValue2)"
+  >
+    add tab
+  </el-button>
 </div>
-<el-tabs type="card" closable>
-  <el-tab-pane v-for="(item, index) in tabs" :label="item.title" :name="item.name">{{item.content}}</el-tab-pane>
+<el-tabs v-model="editableTabsValue2" type="card" closable @tab-remove="removeTab">
+  <el-tab-pane
+    v-for="(item, index) in editableTabs2"
+    :label="item.title"
+    :name="item.name"
+  >
+    {{item.content}}
+  </el-tab-pane>
 </el-tabs>
+<script>
+  export default {
+    data() {
+      return {
+        editableTabsValue2: '2',
+        editableTabs2: [{
+          title: 'Tab 1',
+          name: '1',
+          content: 'Tab 1 content'
+        }, {
+          title: 'Tab 2',
+          name: '2',
+          content: 'Tab 2 content'
+        }],
+        tabIndex: 2
+      }
+    },
+    methods: {
+      addTab(targetName) {
+        let newTabName = ++this.tabIndex + '';
+        this.editableTabs2.push({
+          title: 'New Tab',
+          name: newTabName,
+          content: 'New Tab content'
+        });
+        this.editableTabsValue2 = newTabName;
+      },
+      removeTab(targetName) {
+        let tabs = this.editableTabs2;
+        let activeName = this.editableTabsValue2;
+        if (activeName === targetName) {
+          tabs.forEach((tab, index) => {
+            if (tab.name === targetName) {
+              let nextTab = tabs[index + 1] || tabs[index - 1];
+              if (nextTab) {
+                activeName = nextTab.name;
+              }
+            }
+          });
+        }
+        
+        this.editableTabsValue2 = activeName;
+        this.editableTabs2 = tabs.filter(tab => tab.name !== targetName);
+      }
+    }
+  }
+</script>
 ```
 :::
 
@@ -178,14 +333,18 @@
 |---------- |-------- |---------- |-------------  |-------- |
 | type     | 风格类型   | string   | card/border-card  |     —    |
 | closable  | 标签是否可关闭   | boolean   | — |  false  |
+| addable  | 标签是否可增加   | boolean   | — |  false  |
+| editable  | 标签是否同时可增加和关闭   | boolean   | — |  false  |
 | active-name(deprecated)  | 选中选项卡的 name  | string   |  —  |  第一个选项卡的 name |
 | value  | 绑定值,选中选项卡的 name  | string   |  —  |  第一个选项卡的 name |
 
 ### Tabs Events
 | 事件名称 | 说明 | 回调参数 |
 |---------- |-------- |---------- |
-| tab-click  | tab 被选中的钩子 | 被选中的标签 tab 实例 |
-| tab-remove  | tab 被删除的钩子  | 被删除的标签 tab 实例 |
+| tab-click  | tab 被选中时触发 | 被选中的标签 tab 实例 |
+| tab-remove  | 点击 tab 移除按钮后触发  | 被删除的标签的 name |
+| tab-add  | 点击 tabs 的新增按钮后触发  | — |
+| edit  | 点击 tabs 的新增按钮或 tab 被关闭后触发  | (targetName, action) |
 
 ### Tab-pane Attributes
 | 参数       | 说明     | 类型      | 可选值       | 默认值   |

+ 34 - 51
packages/tabs/src/tabs.vue

@@ -10,11 +10,10 @@
     props: {
       type: String,
       activeName: String,
-      closable: {
-        type: Boolean,
-        default: false
-      },
-      value: {}
+      closable: Boolean,
+      addable: Boolean,
+      value: {},
+      editable: Boolean
     },
 
     data() {
@@ -34,54 +33,21 @@
       }
     },
 
-    computed: {
-      currentTab() {
-        let result;
-        this.panes.forEach(tab => {
-          if (this.currentName === (tab.name || tab.index)) {
-            result = tab;
-          }
-        });
-        return result;
-      }
-    },
-
     methods: {
-      handleTabRemove(pane, event) {
-        event.stopPropagation();
-        const panes = this.panes;
-        const currentTab = this.currentTab;
-
-        let index = panes.indexOf(pane);
-
-        if (index === -1) return;
-
-        panes.splice(index, 1);
-        pane.$destroy();
-
-        this.$emit('tab-remove', pane);
-
-        this.$nextTick(_ => {
-          if (pane.active) {
-            const panes = this.panes;
-            let nextChild = panes[index];
-            let prevChild = panes[index - 1];
-            let nextActiveTab = nextChild || prevChild || null;
-
-            if (nextActiveTab) {
-              this.setCurrentName(nextActiveTab.name || nextActiveTab.index);
-            }
-            return;
-          } else {
-            this.setCurrentName(currentTab.name || currentTab.index);
-          }
-        });
-      },
       handleTabClick(tab, tabName, event) {
         if (tab.disabled) return;
         this.setCurrentName(tabName);
         this.$emit('tab-click', tab, event);
       },
+      handleTabRemove(pane, ev) {
+        ev.stopPropagation();
+        this.$emit('edit', pane.name, 'remove');
+        this.$emit('tab-remove', pane.name);
+      },
+      handleTabAdd() {
+        this.$emit('edit', null, 'add');
+        this.$emit('tab-add');
+      },
       setCurrentName(value) {
         this.currentName = value;
         this.$emit('input', value);
@@ -100,21 +66,37 @@
     render(h) {
       let {
         type,
-        handleTabRemove,
         handleTabClick,
+        handleTabRemove,
+        handleTabAdd,
         currentName,
-        panes
+        panes,
+        editable,
+        addable
       } = this;
 
+      const newButton = editable || addable
+        ? (
+            <span
+              class="el-tabs__new-button"
+              on-click={ handleTabAdd }
+            >
+                <i class="el-icon-plus"></i>
+            </span>
+          )
+        : null;
+
       const tabs = this._l(panes, (pane, index) => {
         let tabName = pane.name || pane.index || index;
+        const closable = pane.isClosable || editable;
+
         if (currentName === undefined && index === 0) {
           this.setCurrentName(tabName);
         }
 
         pane.index = index;
 
-        const btnClose = pane.isClosable
+        const btnClose = closable
           ? <span class="el-icon-close" on-click={(ev) => { handleTabRemove(pane, ev); }}></span>
           : null;
 
@@ -125,7 +107,7 @@
               'el-tabs__item': true,
               'is-active': pane.active,
               'is-disabled': pane.disabled,
-              'is-closable': pane.isClosable
+              'is-closable': closable
             }}
             ref="tabs"
             refInFor
@@ -146,6 +128,7 @@
           <div class="el-tabs__header">
             {!type ? <tab-bar tabs={panes}></tab-bar> : null}
             {tabs}
+            {newButton}
           </div>
           <div class="el-tabs__content">
             {this.$slots.default}

+ 22 - 0
packages/theme-default/src/tabs.css

@@ -20,6 +20,28 @@
       transition: transform .3s cubic-bezier(.645,.045,.355,1);
       list-style: none;
     }
+    @e new-button {
+      float: left;
+      border: 1px solid #d3dce6;
+      height: 18px;
+      width: @height;
+      line-height: @height;
+      margin: 12px 0 9px 10px;
+      border-radius: 3px;
+      text-align: center;
+      font-size: 12px;
+      color: #d3dce6;
+      cursor: pointer;
+      transition: all .15s;
+
+      .el-icon-plus {
+        transform: scale(0.8, 0.8);
+      }
+
+      &:hover {
+        color: var(--color-primary);
+      }
+    }
     @e item {
       padding: 0 16px;
       height: 42px;

+ 153 - 50
test/unit/specs/tabs.spec.js

@@ -140,33 +140,172 @@ describe('Tabs', () => {
       });
     }, 100);
   });
-  it('closable', done => {
+  it('editable', done => {
     vm = createVue({
       template: `
-        <el-tabs type="card" :closable="true" ref="tabs">
-          <el-tab-pane label="用户管理">A</el-tab-pane>
-          <el-tab-pane label="配置管理">B</el-tab-pane>
-          <el-tab-pane label="角色管理">C</el-tab-pane>
-          <el-tab-pane label="定时任务补偿">D</el-tab-pane>
+        <el-tabs ref="tabs" v-model="editableTabsValue" type="card" editable @edit="handleTabsEdit">
+          <el-tab-pane
+            v-for="(item, index) in editableTabs"
+            :label="item.title"
+            :name="item.name"
+          >
+            {{item.content}}
+          </el-tab-pane>
         </el-tabs>
-      `
+      `,
+      data() {
+        return {
+          editableTabsValue: '2',
+          editableTabs: [{
+            title: 'Tab 1',
+            name: '1',
+            content: 'Tab 1 content'
+          }, {
+            title: 'Tab 2',
+            name: '2',
+            content: 'Tab 2 content'
+          }, {
+            title: 'Tab 3',
+            name: '3',
+            content: 'Tab 3 content'
+          }],
+          tabIndex: 3
+        };
+      },
+      methods: {
+        handleTabsEdit(targetName, action) {
+          if (action === 'add') {
+            let newTabName = ++this.tabIndex + '';
+            this.editableTabs.push({
+              title: 'New Tab',
+              name: newTabName,
+              content: 'New Tab content'
+            });
+            this.editableTabsValue = newTabName;
+          }
+          if (action === 'remove') {
+            let tabs = this.editableTabs;
+            let activeName = this.editableTabsValue;
+            if (activeName === targetName) {
+              tabs.forEach((tab, index) => {
+                if (tab.name === targetName) {
+                  let nextTab = tabs[index + 1] || tabs[index - 1];
+                  if (nextTab) {
+                    activeName = nextTab.name;
+                  }
+                }
+              });
+            }
+            this.editableTabsValue = activeName;
+            this.editableTabs = tabs.filter(tab => tab.name !== targetName);
+          }
+        }
+      }
     }, true);
 
-    let spy = sinon.spy();
-    vm.$refs.tabs.$on('tab-remove', spy);
-
     setTimeout(_ => {
       const tabList = vm.$refs.tabs.$refs.tabs;
       const paneList = vm.$el.querySelector('.el-tabs__content').children;
 
       tabList[1].querySelector('.el-icon-close').click();
+      vm.$nextTick(_ => {
+        expect(tabList.length).to.be.equal(2);
+        expect(paneList.length).to.be.equal(2);
+        expect(tabList[1].classList.contains('is-active')).to.be.true;
+
+        vm.$refs.tabs.$el.querySelector('.el-tabs__new-button').click();
+        vm.$nextTick(_ => {
+          expect(tabList.length).to.be.equal(3);
+          expect(paneList.length).to.be.equal(3);
+          expect(tabList[2].classList.contains('is-active')).to.be.true;
+          done();
+        });
+      });
+    }, 100);
+  });
+  it('addable & closable', done => {
+    vm = createVue({
+      template: `
+        <el-tabs
+          ref="tabs"
+          v-model="editableTabsValue"
+          type="card"
+          addable
+          closable
+          @tab-add="addTab"
+          @tab-remove="removeTab"
+        >
+          <el-tab-pane
+            v-for="(item, index) in editableTabs"
+            :label="item.title"
+            :name="item.name"
+          >
+            {{item.content}}
+          </el-tab-pane>
+        </el-tabs>
+      `,
+      data() {
+        return {
+          editableTabsValue: '2',
+          editableTabs: [{
+            title: 'Tab 1',
+            name: '1',
+            content: 'Tab 1 content'
+          }, {
+            title: 'Tab 2',
+            name: '2',
+            content: 'Tab 2 content'
+          }],
+          tabIndex: 2
+        };
+      },
+      methods: {
+        addTab(targetName) {
+          let newTabName = ++this.tabIndex + '';
+          this.editableTabs.push({
+            title: 'New Tab',
+            name: newTabName,
+            content: 'New Tab content'
+          });
+          this.editableTabsValue = newTabName;
+        },
+        removeTab(targetName) {
+          let tabs = this.editableTabs;
+          let activeName = this.editableTabsValue;
+          if (activeName === targetName) {
+            tabs.forEach((tab, index) => {
+              if (tab.name === targetName) {
+                let nextTab = tabs[index + 1] || tabs[index - 1];
+                if (nextTab) {
+                  activeName = nextTab.name;
+                }
+              }
+            });
+          }
+          this.editableTabsValue = activeName;
+          this.editableTabs = tabs.filter(tab => tab.name !== targetName);
+        }
+      }
+    }, true);
+
+    setTimeout(_ => {
+      const tabList = vm.$refs.tabs.$refs.tabs;
+      const paneList = vm.$el.querySelector('.el-tabs__content').children;
+
+      vm.$refs.tabs.$el.querySelector('.el-tabs__new-button').click();
+
       vm.$nextTick(_ => {
         expect(tabList.length).to.be.equal(3);
         expect(paneList.length).to.be.equal(3);
-        expect(spy.calledOnce).to.true;
-        expect(tabList[1].innerText.trim()).to.be.equal('角色管理');
-        expect(paneList[0].innerText.trim()).to.be.equal('A');
-        done();
+        expect(tabList[2].classList.contains('is-active')).to.be.true;
+
+        tabList[2].querySelector('.el-icon-close').click();
+        vm.$nextTick(_ => {
+          expect(tabList.length).to.be.equal(2);
+          expect(paneList.length).to.be.equal(2);
+          expect(tabList[1].classList.contains('is-active')).to.be.true;
+          done();
+        });
       });
     }, 100);
   });
@@ -187,42 +326,6 @@ describe('Tabs', () => {
       done();
     }, 100);
   });
-  it('closable edge', done => {
-    vm = createVue({
-      template: `
-        <el-tabs type="card" :closable="true" ref="tabs">
-          <el-tab-pane label="用户管理">A</el-tab-pane>
-          <el-tab-pane label="配置管理">B</el-tab-pane>
-          <el-tab-pane label="角色管理">C</el-tab-pane>
-          <el-tab-pane label="定时任务补偿">D</el-tab-pane>
-        </el-tabs>
-      `
-    }, true);
-
-    vm.$nextTick(_ => {
-      const paneList = vm.$el.querySelector('.el-tabs__content').children;
-      const tabList = vm.$refs.tabs.$refs.tabs;
-
-      tabList[0].querySelector('.el-icon-close').click();
-      vm.$nextTick(_ => {
-        expect(tabList.length).to.be.equal(3);
-        expect(paneList.length).to.be.equal(3);
-        expect(tabList[0].innerText.trim()).to.be.equal('配置管理');
-        expect(paneList[0].innerText.trim()).to.be.equal('B');
-
-        tabList[2].click();
-        tabList[2].querySelector('.el-icon-close').click();
-        setTimeout(_ => {
-          expect(tabList.length).to.be.equal(2);
-          expect(paneList.length).to.be.equal(2);
-          expect(tabList[1].classList.contains('is-active')).to.be.true;
-          expect(tabList[1].innerText.trim()).to.be.equal('角色管理');
-          expect(paneList[1].innerText.trim()).to.be.equal('C');
-          done();
-        }, 100);
-      });
-    });
-  });
   it('disabled', done => {
     vm = createVue({
       template: `