浏览代码

Merge branch '1.1' into feat/ssr

cinwell.li 8 年之前
父节点
当前提交
05be27e162
共有 43 个文件被更改,包括 1305 次插入140 次删除
  1. 1 0
      .travis.yml
  2. 4 1
      components.json
  3. 150 0
      examples/docs/en-US/collapse.md
  4. 1 0
      examples/docs/en-US/table.md
  5. 1 0
      examples/docs/en-US/time-picker.md
  6. 150 0
      examples/docs/zh-CN/collapse.md
  7. 1 0
      examples/docs/zh-CN/table.md
  8. 1 0
      examples/docs/zh-CN/time-picker.md
  9. 5 0
      examples/index.tpl
  10. 8 0
      examples/nav.config.json
  11. 2 0
      package.json
  12. 8 0
      packages/collapse-item/index.js
  13. 13 0
      packages/collapse/_index.js
  14. 18 0
      packages/collapse/cooking.conf.js
  15. 9 0
      packages/collapse/index.js
  16. 15 0
      packages/collapse/package.json
  17. 115 0
      packages/collapse/src/collapse-item.vue
  18. 62 0
      packages/collapse/src/collapse.vue
  19. 65 44
      packages/date-picker/src/basic/time-spinner.vue
  20. 16 7
      packages/date-picker/src/panel/time-select.vue
  21. 8 0
      packages/scrollbar/index.js
  22. 87 0
      packages/scrollbar/src/bar.js
  23. 125 0
      packages/scrollbar/src/main.js
  24. 38 0
      packages/scrollbar/src/util.js
  25. 8 4
      packages/select/src/select.vue
  26. 1 1
      packages/table/src/filter-panel.vue
  27. 13 9
      packages/table/src/table-body.js
  28. 3 9
      packages/table/src/table-layout.js
  29. 18 8
      packages/table/src/table.vue
  30. 0 25
      packages/table/src/util.js
  31. 0 0
      packages/theme-default/src/collapse-item.css
  32. 52 0
      packages/theme-default/src/collapse.css
  33. 18 0
      packages/theme-default/src/common/var.css
  34. 7 8
      packages/theme-default/src/date-picker/time-spinner.css
  35. 2 0
      packages/theme-default/src/index.css
  36. 68 0
      packages/theme-default/src/scrollbar.css
  37. 5 3
      packages/theme-default/src/select-dropdown.css
  38. 0 5
      packages/theme-default/src/time-select.css
  39. 10 1
      src/index.js
  40. 28 0
      src/utils/scrollbar-width.js
  41. 96 0
      test/unit/specs/collapse.spec.js
  42. 15 15
      test/unit/specs/time-picker.spec.js
  43. 58 0
      test/unit/specs/time-select.spec.js

+ 1 - 0
.travis.yml

@@ -5,6 +5,7 @@ cache:
   directories:
   - $HOME/.npm
   - $HOME/.yarn-cache
+  - node_modules
 before_install:
 - curl -o- -L https://yarnpkg.com/install.sh | bash
 - export PATH=$HOME/.yarn/bin:$PATH

+ 4 - 1
components.json

@@ -55,5 +55,8 @@
   "steps": "./packages/steps/index.js",
   "step": "./packages/step/index.js",
   "carousel": "./packages/carousel/index.js",
-  "carousel-item": "./packages/carousel-item/index.js"
+  "scrollbar": "./packages/scrollbar/index.js",
+  "carousel-item": "./packages/carousel-item/index.js",
+  "collapse": "./packages/collapse/index.js",
+  "collapse-item": "./packages/collapse-item/index.js"
 }

+ 150 - 0
examples/docs/en-US/collapse.md

@@ -0,0 +1,150 @@
+<script>
+  export default {
+    data() {
+      return {
+        activeNames: ['1'],
+        activeName: '1'
+      };
+    },
+    methods: {
+      handleChange(val) {
+        console.log(val);
+      }
+    }
+  }
+</script>
+<style>
+  .demo-collapse {
+    .el-collapse-item__header {
+      .header-icon {
+        margin-left: 5px;
+      }
+    }
+  }
+</style>
+
+## Collapse
+
+Use Collapse to storage content.
+
+### Basic usage
+
+可同时展开多个面板,面板之间不影响
+
+:::demo
+```html
+<el-collapse v-model="activeNames" @change="handleChange">
+  <el-collapse-item title="Consistency" name="1">
+    <div>与现实生活一致:与现实生活的流程、逻辑保持一致,遵循用户习惯的语言和概念;</div>
+    <div>在界面中一致:所有的元素和结构需保持一致,比如:设计样式、图标和文本、元素的位置等。</div>
+  </el-collapse-item>
+  <el-collapse-item title="Feedback" name="2">
+    <div>控制反馈:通过界面样式和交互动效让用户可以清晰的感知自己的操作;</div>
+    <div>页面反馈:操作后,通过页面元素的变化清晰地展现当前状态。</div>
+  </el-collapse-item>
+  <el-collapse-item title="Efficiency" name="3">
+    <div>简化流程:设计简洁直观的操作流程;</div>
+    <div>清晰明确:语言表达清晰且表意明确,让用户快速理解进而作出决策;</div>
+    <div>帮助用户识别:界面简单直白,让用户快速识别而非回忆,减少用户记忆负担。</div>
+  </el-collapse-item>
+  <el-collapse-item title="Controllability" name="4">
+    <div>用户决策:根据场景可给予用户操作建议或安全提示,但不能代替用户进行决策;</div>
+    <div>结果可控:用户可以自由的进行操作,包括撤销、回退和终止当前操作等。</div>
+  </el-collapse-item>
+</el-collapse>
+<script>
+  export default {
+    data() {
+      return {
+        activeNames: ['1']
+      };
+    }
+  }
+</script>
+```
+:::
+
+### Accordion
+
+At the same time only one item can be opened.
+
+:::demo 通过 `accordion` 属性来设置是否以手风琴模式显示。
+```html
+<el-collapse v-model="activeName" accordion>
+  <el-collapse-item title="Consistency" name="1">
+    <div>与现实生活一致:与现实生活的流程、逻辑保持一致,遵循用户习惯的语言和概念;</div>
+    <div>在界面中一致:所有的元素和结构需保持一致,比如:设计样式、图标和文本、元素的位置等。</div>
+  </el-collapse-item>
+  <el-collapse-item title="Feedback" name="2">
+    <div>控制反馈:通过界面样式和交互动效让用户可以清晰的感知自己的操作;</div>
+    <div>页面反馈:操作后,通过页面元素的变化清晰地展现当前状态。</div>
+  </el-collapse-item>
+  <el-collapse-item title="Efficiency" name="3">
+    <div>简化流程:设计简洁直观的操作流程;</div>
+    <div>清晰明确:语言表达清晰且表意明确,让用户快速理解进而作出决策;</div>
+    <div>帮助用户识别:界面简单直白,让用户快速识别而非回忆,减少用户记忆负担。</div>
+  </el-collapse-item>
+  <el-collapse-item title="Controllability" name="4">
+    <div>用户决策:根据场景可给予用户操作建议或安全提示,但不能代替用户进行决策;</div>
+    <div>结果可控:用户可以自由的进行操作,包括撤销、回退和终止当前操作等。</div>
+  </el-collapse-item>
+</el-collapse>
+<script>
+  export default {
+    data() {
+      return {
+        activeName: '1'
+      };
+    }
+  }
+</script>
+```
+:::
+
+### Customize Title
+
+除了可以通过 `title` 属性以外,还可以通过作用域插槽来实现自定义面板的标题内容,以实现增加图标等效果。
+
+:::demo
+```html
+<el-collapse accordion>
+  <el-collapse-item title="Consistency">
+    <template slot="title" scope="props">
+      {{props.title}}<i class="header-icon el-icon-information"></i>
+    </template>
+    <div>与现实生活一致:与现实生活的流程、逻辑保持一致,遵循用户习惯的语言和概念;</div>
+    <div>在界面中一致:所有的元素和结构需保持一致,比如:设计样式、图标和文本、元素的位置等。</div>
+  </el-collapse-item>
+  <el-collapse-item title="Feedback">
+    <div>控制反馈:通过界面样式和交互动效让用户可以清晰的感知自己的操作;</div>
+    <div>页面反馈:操作后,通过页面元素的变化清晰地展现当前状态。</div>
+  </el-collapse-item>
+  <el-collapse-item title="Efficiency">
+    <div>简化流程:设计简洁直观的操作流程;</div>
+    <div>清晰明确:语言表达清晰且表意明确,让用户快速理解进而作出决策;</div>
+    <div>帮助用户识别:界面简单直白,让用户快速识别而非回忆,减少用户记忆负担。</div>
+  </el-collapse-item>
+  <el-collapse-item title="Controllability">
+    <div>用户决策:根据场景可给予用户操作建议或安全提示,但不能代替用户进行决策;</div>
+    <div>结果可控:用户可以自由的进行操作,包括撤销、回退和终止当前操作等。</div>
+  </el-collapse-item>
+</el-collapse>
+```
+:::
+
+### Collapse Attributes
+| 参数      | 说明          | 类型      | 可选值                           | 默认值  |
+|---------- |-------------- |---------- |--------------------------------  |-------- |
+| accordion | 是否手风琴模式 | boolean | — | false |
+| value | 当前激活的面板(如果是手风琴模式,绑定值类型需要为`string`,否则为`array`) | string/array | — | — |
+
+### Collapse Events
+| 事件名称 | 说明 | 回调参数 |
+|---------|---------|---------|
+| change | 当前激活面板改变时触发(如果是手风琴模式,参数 `activeNames` 类型为`string`,否则为`array`) | (activeNames: array\|string) |
+
+### Collapse Item Attributes
+| 参数      | 说明          | 类型      | 可选值                           | 默认值  |
+|---------- |-------------- |---------- |--------------------------------  |-------- |
+| name | 唯一标志符 | string/number | — | — |
+| title | 面板标题 | string | — | — |

+ 1 - 0
examples/docs/en-US/table.md

@@ -1362,6 +1362,7 @@ Customize table column so it can be integrated with other components.
 | row-key | key of row data, used for optimizing rendering. Required if `reserve-selection` is on | Function(row)/String | — | — |
 | context | context of Table, e.g. `_self` refers to the current context, `$parent` parent context, `$root` root context, can be overridden by `context` in `el-table-column` | Object | - | current context where Table lies |
 | empty-text | Displayed text when data is empty. You can customize this area with `slot="empty"` | String | | - | No Data |
+| virtual-scrollbar | Enable virtual scrollbar | Boolean | - | false |
 
 ### Table Events
 | Event Name | Description | Parameters |

+ 1 - 0
examples/docs/en-US/time-picker.md

@@ -161,6 +161,7 @@ Can pick an arbitrary time range.
 | end | end time | string | — | 18:00 |
 | step | time step | string | — | 00:30 |
 | minTime | minimum time, any time before this time will be disabled | string | — | 00:00 |
+| maxTime | maximum time, any time after this time will be disabled | string | — | - |
 
 ### Time Picker Options
 | Attribute      | Description          | Type      | Accepted Values       | Default  |

+ 150 - 0
examples/docs/zh-CN/collapse.md

@@ -0,0 +1,150 @@
+<script>
+  export default {
+    data() {
+      return {
+        activeNames: ['1'],
+        activeName: '1'
+      };
+    },
+    methods: {
+      handleChange(val) {
+        console.log(val);
+      }
+    }
+  }
+</script>
+<style>
+  .demo-collapse {
+    .el-collapse-item__header {
+      .header-icon {
+        margin-left: 5px;
+      }
+    }
+  }
+</style>
+
+## Collapse 折叠面板
+
+通过折叠面板收纳内容区域
+
+### 基础用法
+
+可同时展开多个面板,面板之间不影响
+
+:::demo
+```html
+<el-collapse v-model="activeNames" @change="handleChange">
+  <el-collapse-item title="一致性 Consistency" name="1">
+    <div>与现实生活一致:与现实生活的流程、逻辑保持一致,遵循用户习惯的语言和概念;</div>
+    <div>在界面中一致:所有的元素和结构需保持一致,比如:设计样式、图标和文本、元素的位置等。</div>
+  </el-collapse-item>
+  <el-collapse-item title="反馈 Feedback" name="2">
+    <div>控制反馈:通过界面样式和交互动效让用户可以清晰的感知自己的操作;</div>
+    <div>页面反馈:操作后,通过页面元素的变化清晰地展现当前状态。</div>
+  </el-collapse-item>
+  <el-collapse-item title="效率 Efficiency" name="3">
+    <div>简化流程:设计简洁直观的操作流程;</div>
+    <div>清晰明确:语言表达清晰且表意明确,让用户快速理解进而作出决策;</div>
+    <div>帮助用户识别:界面简单直白,让用户快速识别而非回忆,减少用户记忆负担。</div>
+  </el-collapse-item>
+  <el-collapse-item title="可控 Controllability" name="4">
+    <div>用户决策:根据场景可给予用户操作建议或安全提示,但不能代替用户进行决策;</div>
+    <div>结果可控:用户可以自由的进行操作,包括撤销、回退和终止当前操作等。</div>
+  </el-collapse-item>
+</el-collapse>
+<script>
+  export default {
+    data() {
+      return {
+        activeNames: ['1']
+      };
+    }
+  }
+</script>
+```
+:::
+
+### 手风琴效果
+
+每次只能展开一个面板
+
+:::demo 通过 `accordion` 属性来设置是否以手风琴模式显示。
+```html
+<el-collapse v-model="activeName" accordion>
+  <el-collapse-item title="一致性 Consistency" name="1">
+    <div>与现实生活一致:与现实生活的流程、逻辑保持一致,遵循用户习惯的语言和概念;</div>
+    <div>在界面中一致:所有的元素和结构需保持一致,比如:设计样式、图标和文本、元素的位置等。</div>
+  </el-collapse-item>
+  <el-collapse-item title="反馈 Feedback" name="2">
+    <div>控制反馈:通过界面样式和交互动效让用户可以清晰的感知自己的操作;</div>
+    <div>页面反馈:操作后,通过页面元素的变化清晰地展现当前状态。</div>
+  </el-collapse-item>
+  <el-collapse-item title="效率 Efficiency" name="3">
+    <div>简化流程:设计简洁直观的操作流程;</div>
+    <div>清晰明确:语言表达清晰且表意明确,让用户快速理解进而作出决策;</div>
+    <div>帮助用户识别:界面简单直白,让用户快速识别而非回忆,减少用户记忆负担。</div>
+  </el-collapse-item>
+  <el-collapse-item title="可控 Controllability" name="4">
+    <div>用户决策:根据场景可给予用户操作建议或安全提示,但不能代替用户进行决策;</div>
+    <div>结果可控:用户可以自由的进行操作,包括撤销、回退和终止当前操作等。</div>
+  </el-collapse-item>
+</el-collapse>
+<script>
+  export default {
+    data() {
+      return {
+        activeName: '1'
+      };
+    }
+  }
+</script>
+```
+:::
+
+### 自定义面板标题
+
+除了可以通过 `title` 属性以外,还可以通过作用域插槽来实现自定义面板的标题内容,以实现增加图标等效果。
+
+:::demo
+```html
+<el-collapse accordion>
+  <el-collapse-item title="一致性 Consistency">
+    <template slot="title" scope="props">
+      {{props.title}}<i class="header-icon el-icon-information"></i>
+    </template>
+    <div>与现实生活一致:与现实生活的流程、逻辑保持一致,遵循用户习惯的语言和概念;</div>
+    <div>在界面中一致:所有的元素和结构需保持一致,比如:设计样式、图标和文本、元素的位置等。</div>
+  </el-collapse-item>
+  <el-collapse-item title="反馈 Feedback">
+    <div>控制反馈:通过界面样式和交互动效让用户可以清晰的感知自己的操作;</div>
+    <div>页面反馈:操作后,通过页面元素的变化清晰地展现当前状态。</div>
+  </el-collapse-item>
+  <el-collapse-item title="效率 Efficiency">
+    <div>简化流程:设计简洁直观的操作流程;</div>
+    <div>清晰明确:语言表达清晰且表意明确,让用户快速理解进而作出决策;</div>
+    <div>帮助用户识别:界面简单直白,让用户快速识别而非回忆,减少用户记忆负担。</div>
+  </el-collapse-item>
+  <el-collapse-item title="可控 Controllability">
+    <div>用户决策:根据场景可给予用户操作建议或安全提示,但不能代替用户进行决策;</div>
+    <div>结果可控:用户可以自由的进行操作,包括撤销、回退和终止当前操作等。</div>
+  </el-collapse-item>
+</el-collapse>
+```
+:::
+
+### Collapse Attributes
+| 参数      | 说明          | 类型      | 可选值                           | 默认值  |
+|---------- |-------------- |---------- |--------------------------------  |-------- |
+| accordion | 是否手风琴模式 | boolean | — | false |
+| value | 当前激活的面板(如果是手风琴模式,绑定值类型需要为`string`,否则为`array`) | string/array | — | — |
+
+### Collapse Events
+| 事件名称 | 说明 | 回调参数 |
+|---------|---------|---------|
+| change | 当前激活面板改变时触发(如果是手风琴模式,参数 `activeNames` 类型为`string`,否则为`array`) | (activeNames: array\|string) |
+
+### Collapse Item Attributes
+| 参数      | 说明          | 类型      | 可选值                           | 默认值  |
+|---------- |-------------- |---------- |--------------------------------  |-------- |
+| name | 唯一标志符 | string/number | — | — |
+| title | 面板标题 | string | — | — |

+ 1 - 0
examples/docs/zh-CN/table.md

@@ -1369,6 +1369,7 @@
 | row-key | 行数据的 Key,用来优化 Table 的渲染;在使用 reserve-selection 功能的情况下,该属性是必填的 | Function(row)/String | — | — |
 | context | 设置上下文环境,例如设置当前上下文就是 `_self`,父级就是 `$parent`,根组件 `$root`。优先读取 column 的 context 属性。 | Object | - | Table 所处上下文 |
 | empty-text | 空数据时显示的文本内容,也可以通过 `slot="empty"` 设置 | String | | - | 暂无数据 |
+| virtual-scrollbar | 启用虚拟滚动条 | Boolean | - | false |
 
 ### Table Events
 | 事件名 | 说明 | 参数 |

+ 1 - 0
examples/docs/zh-CN/time-picker.md

@@ -168,6 +168,7 @@
 | end | 结束时间 | string | — | 18:00 |
 | step | 间隔时间 | string | — | 00:30 |
 | minTime | 最小时间,小于该时间的时间段将被禁用 | string | — | 00:00 |
+| maxTime | 最大时间,大于该时间的时间段将被禁用 | string | — | - |
 
 ### Time Picker Options
 | 参数      | 说明          | 类型      | 可选值                           | 默认值  |

+ 5 - 0
examples/index.tpl

@@ -19,5 +19,10 @@
 
     ga('create', 'UA-84335471-1', 'auto');
     ga('send', 'pageview');
+
+    window.addEventListener('hashchange', function () {
+      ga('set', 'page', window.location.href);
+      ga('send', 'pageview');
+    });
   </script><% } %>
 </html>

+ 8 - 0
examples/nav.config.json

@@ -211,6 +211,10 @@
             {
               "path": "/carousel",
               "title": "Carousel 走马灯"
+            },
+            {
+              "path": "/collapse",
+              "title": "Collapse 折叠面板"
             }
           ]
         }
@@ -429,6 +433,10 @@
             {
               "path": "/carousel",
               "title": "Carousel"
+            },
+            {
+              "path": "/collapse",
+              "title": "Collapse"
             }
           ]
         }

+ 2 - 0
package.json

@@ -41,6 +41,8 @@
   "bugs": {
     "url": "https://github.com/ElemeFE/element/issues"
   },
+  "unpkg": "lib/index.js",
+  "style": "lib/theme-default/index.css",
   "dependencies": {
     "async-validator": "^1.6.6",
     "babel-helper-vue-jsx-merge-props": "^2.0.0",

+ 8 - 0
packages/collapse-item/index.js

@@ -0,0 +1,8 @@
+import ElCollapseItem from '../collapse/src/collapse-item.vue';
+
+/* istanbul ignore next */
+ElCollapseItem.install = function(Vue) {
+  Vue.component(ElCollapseItem.name, ElCollapseItem);
+};
+
+export default ElCollapseItem;

+ 13 - 0
packages/collapse/_index.js

@@ -0,0 +1,13 @@
+import ElCollapse from './src/collapse';
+import ElCollapseItem from './src/collapse-item.vue';
+
+/* istanbul ignore next */
+export default function install(Vue) {
+  Vue.component(ElCollapseItem.name, ElCollapseItem);
+  Vue.component(ElCollapse.name, ElCollapse);
+};
+
+export {
+  ElCollapse,
+  ElCollapseItem
+};

+ 18 - 0
packages/collapse/cooking.conf.js

@@ -0,0 +1,18 @@
+var cooking = require('cooking');
+var path = require('path');
+var config = require('../../build/config');
+
+cooking.set({
+  entry: {
+    index: path.join(__dirname, '_index.js')
+  },
+  dist: path.join(__dirname, 'lib'),
+  template: false,
+  format: 'umd',
+  moduleName: 'ElCollapse',
+  extends: ['vue2'],
+  alias: config.alias,
+  externals: { vue: config.vue }
+});
+
+module.exports = cooking.resolve();

+ 9 - 0
packages/collapse/index.js

@@ -0,0 +1,9 @@
+import ElCollapse from './src/collapse';
+
+/* istanbul ignore next */
+ElCollapse.install = function(Vue) {
+  Vue.component(ElCollapse.name, ElCollapse);
+};
+
+export default ElCollapse;
+

+ 15 - 0
packages/collapse/package.json

@@ -0,0 +1,15 @@
+{
+  "name": "element-collapse",
+  "version": "1.0.0",
+  "description": "A row component for Vue.",
+  "keywords": [
+    "element",
+    "vue",
+    "component"
+  ],
+  "main": "./lib/index.js",
+  "repository": "https://github.com/ElemeFE/element/tree/master/packages/collapse",
+  "author": "haiping.zeng@ele.me",
+  "license": "MIT",
+  "dependencies": {}
+}

+ 115 - 0
packages/collapse/src/collapse-item.vue

@@ -0,0 +1,115 @@
+<template>
+  <div class="el-collapse-item" :class="{'is-active': isActive}">
+    <div class="el-collapse-item__header" @click="handleHeaderClick">
+      <i class="el-collapse-item__header__arrow el-icon-arrow-right"></i>
+      <slot name="title" :title="title">{{title}}</slot>
+    </div>
+    <div class="el-collapse-item__wrap" ref="content" :style="contentStyle">
+      <div class="el-collapse-item__content">
+        <slot></slot>
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+  import { once } from 'wind-dom';
+  import Emitter from 'element-ui/src/mixins/emitter';
+
+  function getTransitionendEvent(el) {
+    let t;
+    let transitions = {
+      'transition': 'transitionend',
+      'OTransition': 'oTransitionEnd',
+      'MozTransition': 'transitionend',
+      'WebkitTransition': 'webkitTransitionEnd'
+    };
+
+    for (t in transitions) {
+      if (el.style[t] !== undefined) {
+        return transitions[t];
+      }
+    }
+  };
+
+  export default {
+    name: 'ElCollapseItem',
+
+    componentName: 'ElCollapseItem',
+
+    mixins: [Emitter],
+
+    data() {
+      return {
+        contentStyle: {},
+        contentHeight: 0
+      };
+    },
+
+    props: {
+      title: String,
+      name: {
+        type: [String, Number],
+        default() {
+          return this._uid;
+        }
+      }
+    },
+
+    computed: {
+      isActive() {
+        return this.$parent.activeNames.indexOf(this.name) > -1;
+      }
+    },
+
+    watch: {
+      'isActive'(value) {
+        value ? this.open() : this.close();
+      }
+    },
+
+    methods: {
+      open() {
+        const contentElm = this.$refs.content;
+        const contentStyle = this.contentStyle;
+
+        contentStyle.display = 'block';
+        this.$nextTick(_ => {
+          contentStyle.height = this.contentHeight + 'px';
+          once(contentElm, getTransitionendEvent(contentElm), () => {
+            contentStyle.height = 'auto';
+          });
+        });
+      },
+      close() {
+        const contentElm = this.$refs.content;
+        const contentHeight = contentElm.clientHeight;
+        const contentStyle = this.contentStyle;
+
+        this.contentHeight = contentHeight;
+        this.$set(this.contentStyle, 'height', contentHeight + 'px');
+
+        this.$nextTick(_ => {
+          contentStyle.height = '0';
+          once(contentElm, getTransitionendEvent(contentElm), () => {
+            this.$set(this.contentStyle, 'display', 'none');
+          });
+        });
+      },
+      init() {
+        this.contentHeight = this.$refs.content.clientHeight;
+
+        if (!this.isActive) {
+          this.$set(this.contentStyle, 'height', '0');
+          this.$set(this.contentStyle, 'display', 'none');
+        }
+      },
+      handleHeaderClick() {
+        this.dispatch('ElCollapse', 'item-click', this);
+      }
+    },
+
+    mounted() {
+      this.init();
+    }
+  };
+</script>

+ 62 - 0
packages/collapse/src/collapse.vue

@@ -0,0 +1,62 @@
+<template>
+  <div class="el-collapse">
+    <slot></slot>
+  </div>
+</template>
+<script>
+  export default {
+    name: 'ElCollapse',
+
+    componentName: 'ElCollapse',
+
+    props: {
+      accordion: Boolean,
+      value: {
+        type: [Array, String],
+        default() {
+          return [];
+        }
+      }
+    },
+
+    data() {
+      return {
+        activeNames: [].concat(this.value)
+      };
+    },
+
+    watch: {
+      value(value) {
+        this.activeNames = [].concat(value);
+      }
+    },
+
+    methods: {
+      setActiveNames(activeNames) {
+        let value = this.accordion ? activeNames[0] : activeNames;
+        this.activeNames = [].concat(value);
+        this.$emit('input', value);
+        this.$emit('change', value);
+      },
+      handleItemClick(item) {
+        if (this.accordion) {
+          this.setActiveNames(this.activeNames[0] === item.name ? '' : item.name);
+        } else {
+          let activeNames = this.activeNames.slice(0);
+          let index = activeNames.indexOf(item.name);
+
+          if (index > -1) {
+            activeNames.splice(index, 1);
+          } else {
+            activeNames.push(item.name);
+          }
+          this.setActiveNames(activeNames);
+        }
+      }
+    },
+
+    created() {
+      this.$on('item-click', this.handleItemClick);
+    }
+  };
+</script>

+ 65 - 44
packages/date-picker/src/basic/time-spinner.vue

@@ -1,56 +1,65 @@
 <template>
   <div class="el-time-spinner" :class="{ 'has-seconds': showSeconds }">
-    <div
-      @mouseenter="emitSelectRange('hours')"
-      @mousewheel="handleScroll('hour')"
+    <el-scrollbar
+      @mouseenter.native="emitSelectRange('hours')"
+      @mousewheel.native="handleScroll('hour')"
       class="el-time-spinner__wrapper"
+      wrap-style="max-height: inherit;"
+      view-class="el-time-spinner__list"
+      noresize
+      tag="ul"
       ref="hour">
-      <ul class="el-time-spinner__list">
-        <li
-          @click="handleClick('hours', { value: hour, disabled: disabled }, true)"
-          v-for="(disabled, hour) in hoursList"
-          track-by="hour"
-          class="el-time-spinner__item"
-          :class="{ 'active': hour === hours, 'disabled': disabled }"
-          v-text="hour"></li>
-      </ul>
-    </div>
-    <div
-      @mouseenter="emitSelectRange('minutes')"
-      @mousewheel="handleScroll('minute')"
+      <li
+        @click="handleClick('hours', { value: hour, disabled: disabled }, true)"
+        v-for="(disabled, hour) in hoursList"
+        track-by="hour"
+        class="el-time-spinner__item"
+        :class="{ 'active': hour === hours, 'disabled': disabled }"
+        v-text="hour"></li>
+    </el-scrollbar>
+    <el-scrollbar
+      @mouseenter.native="emitSelectRange('minutes')"
+      @mousewheel.native="handleScroll('minute')"
       class="el-time-spinner__wrapper"
+      wrap-style="max-height: inherit;"
+      view-class="el-time-spinner__list"
+      noresize
+      tag="ul"
       ref="minute">
-      <ul class="el-time-spinner__list">
-        <li
-          @click="handleClick('minutes', key, true)"
-          v-for="(minute, key) in 60"
-          class="el-time-spinner__item"
-          :class="{ 'active': key === minutes }"
-          v-text="key"></li>
-      </ul>
-    </div>
-    <div
+      <li
+        @click="handleClick('minutes', key, true)"
+        v-for="(minute, key) in 60"
+        class="el-time-spinner__item"
+        :class="{ 'active': key === minutes }"
+        v-text="key"></li>
+    </el-scrollbar>
+    <el-scrollbar
       v-show="showSeconds"
-      @mouseenter="emitSelectRange('seconds')"
-      @mousewheel="handleScroll('second')"
+      @mouseenter.native="emitSelectRange('seconds')"
+      @mousewheel.native="handleScroll('second')"
       class="el-time-spinner__wrapper"
+      wrap-style="max-height: inherit;"
+      view-class="el-time-spinner__list"
+      noresize
+      tag="ul"
       ref="second">
-      <ul class="el-time-spinner__list">
-        <li
-          @click="handleClick('seconds', key, true)"
-          v-for="(second, key) in 60"
-          class="el-time-spinner__item"
-          :class="{ 'active': key === seconds }"
-          v-text="key"></li>
-      </ul>
-    </div>
+      <li
+        @click="handleClick('seconds', key, true)"
+        v-for="(second, key) in 60"
+        class="el-time-spinner__item"
+        :class="{ 'active': key === seconds }"
+        v-text="key"></li>
+    </el-scrollbar>
   </div>
 </template>
 
 <script type="text/babel">
   import { getRangeHours } from '../util';
+  import ElScrollbar from 'element-ui/packages/scrollbar';
 
   export default {
+    components: { ElScrollbar },
+
     props: {
       hours: {
         type: Number,
@@ -78,7 +87,7 @@
         if (!(newVal >= 0 && newVal <= 23)) {
           this.hoursPrivate = oldVal;
         }
-        this.$refs.hour.scrollTop = Math.max(0, (this.hoursPrivate - 2.5) * 32 + 80);
+        this.hourEl.scrollTop = Math.max(0, (this.hoursPrivate - 2.5) * 32 + 80);
         this.$emit('change', { hours: newVal });
       },
 
@@ -86,7 +95,7 @@
         if (!(newVal >= 0 && newVal <= 59)) {
           this.minutesPrivate = oldVal;
         }
-        this.$refs.minute.scrollTop = Math.max(0, (this.minutesPrivate - 2.5) * 32 + 80);
+        this.minuteEl.scrollTop = Math.max(0, (this.minutesPrivate - 2.5) * 32 + 80);
         this.$emit('change', { minutes: newVal });
       },
 
@@ -94,7 +103,7 @@
         if (!(newVal >= 0 && newVal <= 59)) {
           this.secondsPrivate = oldVal;
         }
-        this.$refs.second.scrollTop = Math.max(0, (this.secondsPrivate - 2.5) * 32 + 80);
+        this.secondEl.scrollTop = Math.max(0, (this.secondsPrivate - 2.5) * 32 + 80);
         this.$emit('change', { seconds: newVal });
       }
     },
@@ -102,6 +111,18 @@
     computed: {
       hoursList() {
         return getRangeHours(this.selectableRange);
+      },
+
+      hourEl() {
+        return this.$refs.hour.wrap;
+      },
+
+      minuteEl() {
+        return this.$refs.minute.wrap;
+      },
+
+      secondEl() {
+        return this.$refs.second.wrap;
       }
     },
 
@@ -138,14 +159,14 @@
       handleScroll(type) {
         const ajust = {};
 
-        ajust[`${type}s`] = Math.min(Math.floor((this.$refs[type].scrollTop - 80) / 32 + 3), 59);
+        ajust[`${type}s`] = Math.min(Math.floor((this[`${type}Elm`].scrollTop - 80) / 32 + 3), 59);
         this.$emit('change', ajust);
       },
 
       ajustScrollTop() {
-        this.$refs.hour.scrollTop = Math.max(0, (this.hours - 2.5) * 32 + 80);
-        this.$refs.minute.scrollTop = Math.max(0, (this.minutes - 2.5) * 32 + 80);
-        this.$refs.second.scrollTop = Math.max(0, (this.seconds - 2.5) * 32 + 80);
+        this.hourEl.scrollTop = Math.max(0, (this.hours - 2.5) * 32 + 80);
+        this.minuteEl.scrollTop = Math.max(0, (this.minutes - 2.5) * 32 + 80);
+        this.secondEl.scrollTop = Math.max(0, (this.seconds - 2.5) * 32 + 80);
       }
     }
   };

+ 16 - 7
packages/date-picker/src/panel/time-select.vue

@@ -3,20 +3,22 @@
     <div
       v-show="visible"
       :style="{ width: width + 'px' }"
-      class="el-picker-panel time-select"
-      :class="popperClass">
-      <div class="el-picker-panel__content">
+      :class="popperClass"
+      class="el-picker-panel time-select">
+      <el-scrollbar noresize wrap-class="el-picker-panel__content">
         <div class="time-select-item"
           v-for="item in items"
           :class="{ selected: value === item.value, disabled: item.disabled }"
           :disabled="item.disabled"
           @click="handleClick(item)">{{ item.value }}</div>
-      </div>
+      </el-scrollbar>
     </div>
   </transition>
 </template>
 
 <script type="text/babel">
+  import ElScrollbar from 'element-ui/packages/scrollbar';
+
   const parseTime = function(time) {
     const values = ('' || time).split(':');
     if (values.length >= 2) {
@@ -69,9 +71,14 @@
   };
 
   export default {
+    components: { ElScrollbar },
+
     watch: {
-      minTime(val) {
-        if (this.value && val && compareTime(this.value, val) === -1) {
+      value(val) {
+        if (!val) return;
+        if (this.minTime && compareTime(val, this.minTime) < 0) {
+          this.$emit('pick');
+        } else if (this.maxTime && compareTime(val, this.maxTime) > 0) {
           this.$emit('pick');
         }
       }
@@ -98,6 +105,7 @@
         value: '',
         visible: false,
         minTime: '',
+        maxTime: '',
         width: 0
       };
     },
@@ -115,7 +123,8 @@
           while (compareTime(current, end) <= 0) {
             result.push({
               value: current,
-              disabled: compareTime(current, this.minTime || '-1:-1') <= 0
+              disabled: compareTime(current, this.minTime || '-1:-1') <= 0 ||
+                compareTime(current, this.maxTime || '100:100') > 0
             });
             current = nextTime(current, step);
           }

+ 8 - 0
packages/scrollbar/index.js

@@ -0,0 +1,8 @@
+import Scrollbar from './src/main';
+
+/* istanbul ignore next */
+Scrollbar.install = function(Vue) {
+  Vue.component(Scrollbar.name, Scrollbar);
+};
+
+export default Scrollbar;

+ 87 - 0
packages/scrollbar/src/bar.js

@@ -0,0 +1,87 @@
+import { on, off } from 'wind-dom/src/event';
+import { renderThumbStyle, BAR_MAP } from './util';
+
+export default {
+  name: 'Bar',
+
+  props: {
+    vertical: Boolean,
+    size: String,
+    move: Number
+  },
+
+  computed: {
+    bar() {
+      return BAR_MAP[this.vertical ? 'vertical' : 'horizontal'];
+    },
+
+    wrap() {
+      return this.$parent.wrap;
+    }
+  },
+
+  render(h) {
+    const { size, move, bar } = this;
+
+    return (
+      <div
+        class={ ['el-scrollbar__bar', 'is-' + bar.key] }
+        onMousedown={ this.clickTrackHandler } >
+        <div
+          ref="thumb"
+          class="el-scrollbar__thumb"
+          onMousedown={ this.clickThumbHandler }
+          style={ renderThumbStyle({ size, move, bar }) }>
+        </div>
+      </div>
+    );
+  },
+
+  methods: {
+    clickThumbHandler(e) {
+      this.startDrag(e);
+      this[this.bar.axis] = (e.currentTarget[this.bar.offset] - (e[this.bar.client] - e.currentTarget.getBoundingClientRect()[this.bar.direction]));
+    },
+
+    clickTrackHandler(e) {
+      const offset = Math.abs(e.target.getBoundingClientRect()[this.bar.direction] - e[this.bar.client]);
+      const thumbHalf = (this.$refs.thumb[this.bar.offset] / 2);
+      const thumbPositionPercentage = ((offset - thumbHalf) * 100 / this.$el[this.bar.offset]);
+
+      this.wrap[this.bar.scroll] = (thumbPositionPercentage * this.wrap[this.bar.scrollSize] / 100);
+    },
+
+    startDrag(e) {
+      e.stopImmediatePropagation();
+      this.cursorDown = true;
+
+      on(document, 'mousemove', this.mouseMoveDocumentHandler);
+      on(document, 'mouseup', this.mouseUpDocumentHandler);
+      document.onselectstart = () => false;
+    },
+
+    mouseMoveDocumentHandler(e) {
+      if (this.cursorDown === false) return;
+      const prevPage = this[this.bar.axis];
+
+      if (!prevPage) return;
+
+      const offset = ((this.$el.getBoundingClientRect()[this.bar.direction] - e[this.bar.client]) * -1);
+      const thumbClickPosition = (this.$refs.thumb[this.bar.offset] - prevPage);
+      const thumbPositionPercentage = ((offset - thumbClickPosition) * 100 / this.$el[this.bar.offset]);
+
+      this.wrap[this.bar.scroll] = (thumbPositionPercentage * this.wrap[this.bar.scrollSize] / 100);
+    },
+
+    mouseUpDocumentHandler(e) {
+      this.cursorDown = false;
+      this[this.bar.axis] = 0;
+      off(document, 'mousemove', this.mouseMoveDocumentHandler);
+      document.onselectstart = null;
+    }
+  },
+
+  destroyed() {
+    off(document, 'mouseup', this.mouseUpDocumentHandler);
+  }
+};

+ 125 - 0
packages/scrollbar/src/main.js

@@ -0,0 +1,125 @@
+// reference https://github.com/noeldelgado/gemini-scrollbar/blob/master/index.js
+
+import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event';
+import scrollbarWidth from 'element-ui/src/utils/scrollbar-width';
+import * as util from './util';
+import Bar from './bar';
+
+export default {
+  name: 'ElScrollbar',
+
+  components: { Bar },
+
+  props: {
+    native: Boolean,
+    wrapStyle: {},
+    wrapClass: {},
+    viewClass: {},
+    viewStyle: {},
+    noresize: Boolean, // 如果 container 尺寸不会发生变化,最好设置它可以优化性能
+    tag: {
+      type: String,
+      default: 'div'
+    }
+  },
+
+  data() {
+    return {
+      sizeWidth: '0',
+      sizeHeight: '0',
+      moveX: 0,
+      moveY: 0
+    };
+  },
+
+  computed: {
+    wrap() {
+      return this.$refs.wrap;
+    }
+  },
+
+  render(h) {
+    let gutter = scrollbarWidth();
+    let style = this.wrapStyle;
+
+    if (gutter) {
+      const gutterWith = `-${gutter}px`;
+
+      if (Array.isArray(this.wrapStyle)) {
+        style = util.toObject(this.wrapStyle);
+        style.marginRight = style.marginBottom = gutterWith;
+      } else if (typeof this.wrapStyle === 'string') {
+        style += `margin-bottom: ${gutterWith}; margin-right: ${gutterWith};`;
+      }
+    }
+    const view = h(this.tag, {
+      class: ['el-scrollbar__view', this.viewClass],
+      style: this.viewStyle,
+      ref: 'resize'
+    }, this.$slots.default);
+    const wrap = (
+      <div
+        ref="wrap"
+        style={ style }
+        onScroll={ this.handleScroll }
+        class={ [this.wrapClass, 'el-scrollbar__wrap el-scrollbar__wrap--hidden-default'] }>
+        { [view] }
+      </div>
+    );
+    let nodes;
+
+    if (!this.native) {
+      nodes = ([
+        wrap,
+        <Bar
+          move={ this.moveX }
+          size={ this.sizeWidth }></Bar>,
+        <Bar
+          vertical
+          move={ this.moveY }
+          size={ this.sizeHeight }></Bar>
+      ]);
+    } else {
+      nodes = ([
+        <div
+          ref="wrap"
+          class={ [this.wrapClass, 'el-scrollbar__wrap'] }
+          style={ style }>
+          { [view] }
+        </div>
+      ]);
+    }
+    return h('div', { class: 'el-scrollbar' }, nodes);
+  },
+
+  methods: {
+    handleScroll() {
+      const wrap = this.wrap;
+
+      this.moveY = ((wrap.scrollTop * 100) / wrap.clientHeight);
+      this.moveX = ((wrap.scrollLeft * 100) / wrap.clientWidth);
+    },
+
+    update() {
+      let heightPercentage, widthPercentage;
+      const wrap = this.wrap;
+
+      heightPercentage = (wrap.clientHeight * 100 / wrap.scrollHeight);
+      widthPercentage = (wrap.clientWidth * 100 / wrap.scrollWidth);
+
+      this.sizeHeight = (heightPercentage < 100) ? (heightPercentage + '%') : '';
+      this.sizeWidth = (widthPercentage < 100) ? (widthPercentage + '%') : '';
+    }
+  },
+
+  mounted() {
+    if (this.native) return;
+    this.$nextTick(this.update);
+    !this.noresize && addResizeListener(this.$refs.resize, this.update);
+  },
+
+  destroyed() {
+    if (this.native) return;
+    !this.noresize && removeResizeListener(this.$refs.resize, this.update);
+  }
+};

+ 38 - 0
packages/scrollbar/src/util.js

@@ -0,0 +1,38 @@
+import Vue from 'vue';
+
+export const BAR_MAP = {
+  vertical: {
+    offset: 'offsetHeight',
+    scroll: 'scrollTop',
+    scrollSize: 'scrollHeight',
+    size: 'height',
+    key: 'vertical',
+    axis: 'Y',
+    client: 'clientY',
+    direction: 'top'
+  },
+  horizontal: {
+    offset: 'offsetWidth',
+    scroll: 'scrollLeft',
+    scrollSize: 'scrollWidth',
+    size: 'width',
+    key: 'horizontal',
+    axis: 'X',
+    client: 'clientX',
+    direction: 'left'
+  }
+};
+
+export function renderThumbStyle({ move, size, bar }) {
+  const style = {};
+  const translate = `translate${bar.axis}(${ move }%)`;
+
+  style[bar.size] = size;
+  style.transform = translate;
+  style.msTransform = translate;
+  style.webkitTransform = translate;
+
+  return style;
+};
+
+export const toObject = Vue.util.toObject;

+ 8 - 4
packages/select/src/select.vue

@@ -65,8 +65,10 @@
       <el-select-menu
         ref="popper"
         v-show="visible && emptyText !== false">
-        <ul
-          class="el-select-dropdown__list"
+        <el-scrollbar
+          tag="ul"
+          wrap-class="el-select-dropdown__wrap"
+          view-class="el-select-dropdown__list"
           :class="{ 'is-empty': !allowCreate && filteredOptionsCount === 0 }"
           v-show="options.length > 0 && !loading">
           <el-option
@@ -75,7 +77,7 @@
             v-if="showNewOption">
           </el-option>
           <slot></slot>
-        </ul>
+        </el-scrollbar>
         <p class="el-select-dropdown__empty" v-if="emptyText && !allowCreate">{{ emptyText }}</p>
       </el-select-menu>
     </transition>
@@ -89,6 +91,7 @@
   import ElSelectMenu from './select-dropdown.vue';
   import ElOption from './option.vue';
   import ElTag from 'element-ui/packages/tag';
+  import ElScrollbar from 'element-ui/packages/scrollbar';
   import debounce from 'throttle-debounce/debounce';
   import Clickoutside from 'element-ui/src/utils/clickoutside';
   import { addClass, removeClass, hasClass } from 'element-ui/src/utils/dom';
@@ -148,7 +151,8 @@
       ElInput,
       ElSelectMenu,
       ElOption,
-      ElTag
+      ElTag,
+      ElScrollbar
     },
 
     directives: { Clickoutside },

+ 1 - 1
packages/table/src/filter-panel.vue

@@ -163,7 +163,7 @@
     mounted() {
       this.popperElm = this.$el;
       this.referenceElm = this.cell;
-      this.table.$refs.bodyWrapper.addEventListener('scroll', () => {
+      this.table.bodyWrapper.addEventListener('scroll', () => {
         this.updatePopper();
       });
 

+ 13 - 9
packages/table/src/table-body.js

@@ -40,7 +40,7 @@ export default {
             this._l(this.data, (row, $index) =>
               <tr
                 style={ this.rowStyle ? this.getRowStyle(row, $index) : null }
-                key={ this.$parent.rowKey ? this.getKeyOfRow(row, $index) : $index }
+                key={ this.table.rowKey ? this.getKeyOfRow(row, $index) : $index }
                 on-dblclick={ ($event) => this.handleDoubleClick($event, row) }
                 on-click={ ($event) => this.handleClick($event, row) }
                 on-contextmenu={ ($event) => this.handleContextMenu($event, row) }
@@ -54,7 +54,7 @@ export default {
                       on-mouseenter={ ($event) => this.handleCellMouseEnter($event, row) }
                       on-mouseleave={ this.handleCellMouseLeave }>
                       {
-                        column.renderCell.call(this._renderProxy, h, { row, column, $index, store: this.store, _self: this.context || this.$parent.$vnode.context })
+                        column.renderCell.call(this._renderProxy, h, { row, column, $index, store: this.store, _self: this.context || this.table.$vnode.context })
                       }
                     </td>
                   )
@@ -103,6 +103,10 @@ export default {
   },
 
   computed: {
+    table() {
+      return this.$parent.$parent.columns ? this.$parent.$parent : this.$parent;
+    },
+
     data() {
       return this.store.states.data;
     },
@@ -132,7 +136,7 @@ export default {
 
   methods: {
     getKeyOfRow(row, index) {
-      const rowKey = this.$parent.rowKey;
+      const rowKey = this.table.rowKey;
       if (rowKey) {
         return getRowIdentity(row, rowKey);
       }
@@ -171,7 +175,7 @@ export default {
     },
 
     handleCellMouseEnter(event, row) {
-      const table = this.$parent;
+      const table = this.table;
       const cell = getCell(event);
 
       if (cell) {
@@ -190,8 +194,8 @@ export default {
       const cell = getCell(event);
       if (!cell) return;
 
-      const oldHoverState = this.$parent.hoverState;
-      this.$parent.$emit('cell-mouse-leave', oldHoverState.row, oldHoverState.column, oldHoverState.cell, event);
+      const oldHoverState = this.table.hoverState;
+      this.table.$emit('cell-mouse-leave', oldHoverState.row, oldHoverState.column, oldHoverState.cell, event);
     },
 
     handleMouseEnter(index) {
@@ -203,17 +207,17 @@ export default {
     },
 
     handleContextMenu(event, row) {
-      const table = this.$parent;
+      const table = this.table;
       table.$emit('row-contextmenu', row, event);
     },
 
     handleDoubleClick(event, row) {
-      const table = this.$parent;
+      const table = this.table;
       table.$emit('row-dblclick', row, event);
     },
 
     handleClick(event, row) {
-      const table = this.$parent;
+      const table = this.table;
       const cell = getCell(event);
       let column;
       if (cell) {

+ 3 - 9
packages/table/src/table-layout.js

@@ -1,7 +1,5 @@
-import { getScrollBarWidth } from './util';
-import Vue from 'vue';
+import scrollbarWidth from 'element-ui/src/utils/scrollbar-width';
 
-let GUTTER_WIDTH;
 
 class TableLayout {
   constructor(options) {
@@ -22,11 +20,7 @@ class TableLayout {
     this.viewportHeight = null; // Table Height - Scroll Bar Height
     this.bodyHeight = null; // Table Height - Table Header Height
     this.fixedBodyHeight = null; // Table Height - Table Header Height - Scroll Bar Height
-
-    if (GUTTER_WIDTH === undefined && !Vue.prototype.$isServer) {
-      GUTTER_WIDTH = getScrollBarWidth();
-    }
-    this.gutterWidth = GUTTER_WIDTH;
+    this.gutterWidth = scrollbarWidth();
 
     for (let name in options) {
       if (options.hasOwnProperty(name)) {
@@ -45,7 +39,7 @@ class TableLayout {
   updateScrollY() {
     const height = this.height;
     if (typeof height !== 'string' && typeof height !== 'number') return;
-    const bodyWrapper = this.table.$refs.bodyWrapper;
+    const bodyWrapper = this.table.bodyWrapper;
     if (this.table.$el && bodyWrapper) {
       const body = bodyWrapper.querySelector('.el-table__body');
       this.scrollY = body.offsetHeight > bodyWrapper.offsetHeight;

+ 18 - 8
packages/table/src/table.vue

@@ -18,7 +18,11 @@
         :style="{ width: layout.bodyWidth ? layout.bodyWidth + 'px' : '' }">
       </table-header>
     </div>
-    <div class="el-table__body-wrapper" ref="bodyWrapper" :style="[bodyHeight]">
+    <el-scrollbar
+      class="el-table__body-wrapper"
+      ref="bodyWrapper"
+      :native="!virtualScrollbar"
+      :wrap-style="[bodyHeight]">
       <table-body
         :context="context"
         :store="store"
@@ -29,9 +33,9 @@
         :style="{ width: layout.bodyWidth ? layout.bodyWidth - (layout.scrollY ? layout.gutterWidth : 0 ) + 'px' : '' }">
       </table-body>
       <div class="el-table__empty-block" v-if="!data || data.length === 0">
-      <span class="el-table__empty-text"><slot name="empty">{{ emptyText || t('el.table.emptyText') }}</slot></span>
+        <span class="el-table__empty-text"><slot name="empty">{{ emptyText || t('el.table.emptyText') }}</slot></span>
       </div>
-    </div>
+    </el-scrollbar>
     <div class="el-table__fixed" ref="fixedWrapper"
       v-if="fixedColumns.length > 0"
       :style="[
@@ -130,6 +134,8 @@
 
       width: [String, Number],
 
+      virtualScrollbar: Boolean,
+
       height: [String, Number],
 
       maxHeight: [String, Number],
@@ -205,22 +211,22 @@
       },
 
       bindEvents() {
-        const { bodyWrapper, headerWrapper } = this.$refs;
+        const { headerWrapper } = this.$refs;
         const refs = this.$refs;
-        bodyWrapper.addEventListener('scroll', function() {
+        this.bodyWrapper.addEventListener('scroll', function() {
           if (headerWrapper) headerWrapper.scrollLeft = this.scrollLeft;
           if (refs.fixedBodyWrapper) refs.fixedBodyWrapper.scrollTop = this.scrollTop;
           if (refs.rightFixedBodyWrapper) refs.rightFixedBodyWrapper.scrollTop = this.scrollTop;
         });
 
         if (headerWrapper) {
-          mousewheel(headerWrapper, throttle(16, function(event) {
+          mousewheel(headerWrapper, throttle(16, event => {
             const deltaX = event.deltaX;
 
             if (deltaX > 0) {
-              bodyWrapper.scrollLeft = bodyWrapper.scrollLeft + 10;
+              this.bodyWrapper.scrollLeft += 10;
             } else {
-              bodyWrapper.scrollLeft = bodyWrapper.scrollLeft - 10;
+              this.bodyWrapper.scrollLeft -= 10;
             }
           }));
         }
@@ -255,6 +261,10 @@
     },
 
     computed: {
+      bodyWrapper() {
+        return this.$refs.bodyWrapper.wrap;
+      },
+
       shouldUpdateHeight() {
         return typeof this.height === 'number' ||
           this.fixedColumns.length > 0 ||

+ 0 - 25
packages/table/src/util.js

@@ -1,28 +1,3 @@
-let scrollBarWidth;
-
-export const getScrollBarWidth = () => {
-  if (scrollBarWidth !== undefined) return scrollBarWidth;
-
-  const outer = document.createElement('div');
-  outer.style.visibility = 'hidden';
-  outer.style.width = '100px';
-  outer.style.position = 'absolute';
-  outer.style.top = '-9999px';
-  document.body.appendChild(outer);
-
-  const widthNoScroll = outer.offsetWidth;
-  outer.style.overflow = 'scroll';
-
-  const inner = document.createElement('div');
-  inner.style.width = '100%';
-  outer.appendChild(inner);
-
-  const widthWithScroll = inner.offsetWidth;
-  outer.parentNode.removeChild(outer);
-
-  return widthNoScroll - widthWithScroll;
-};
-
 export const getCell = function(event) {
   let cell = event.target;
 

+ 0 - 0
packages/theme-default/src/collapse-item.css


+ 52 - 0
packages/theme-default/src/collapse.css

@@ -0,0 +1,52 @@
+@charset "UTF-8";
+@import "./common/var.css";
+
+@component-namespace el {
+  @b collapse {
+    border: 1px solid var(--collapse-border-color);
+    border-radius: var(--collapse-border-radius);
+  }
+  @b collapse-item {
+    @e header {
+      height: var(--collapse-header-height);
+      line-height: @height;
+      padding-left: 15px;
+      background-color: var(--collapse-header-fill);
+      color: var(--collapse-header-color);
+      cursor: pointer;
+      border-bottom: 1px solid var(--collapse-border-color);
+      font-size: var(--collapse-header-size);
+
+      @e arrow {
+        margin-right: 8px;
+        transition: transform .3s;
+      }
+    }
+
+    @e wrap {
+      will-change: height;
+      background-color: var(--collapse-content-fill);
+      transition: height .3s cubic-bezier(0.215, 0.61, 0.355, 1);
+      overflow: hidden;
+      box-sizing: border-box;
+      border-bottom: 1px solid var(--collapse-border-color);
+    }
+
+    @e content {
+      padding: 10px 15px;
+      font-size: var(--collapse-content-size);
+      color: var(--collapse-content-color);
+      line-height: 1.769230769230769;
+    }
+
+    @when active {
+      .el-collapse-item__header__arrow {
+        transform: rotate(90deg);
+      }
+    }
+
+    &:last-child {
+      margin-bottom: -1px;
+    }
+  }
+}

+ 18 - 0
packages/theme-default/src/common/var.css

@@ -545,6 +545,11 @@
   --loading-spinner-size: 42px;
   --loading-fullscreen-spinner-size: 50px;
 
+  /* Scrollbar
+  --------------------------*/
+  --scrollbar-background-color: rgba(#99a9bf, .3);
+  --scrollbar-hover-background-color: rgba(#99a9bf, .5);
+
   /* Carousel
   --------------------------*/
   --carousel-arrow-font-size: 12px;
@@ -556,4 +561,17 @@
   --carousel-indicator-padding-horizontal: 4px;
   --carousel-indicator-padding-vertical: 12px;
   --carousel-indicator-out-color: var(--border-color-hover);
+
+  /* Collapse
+  --------------------------*/
+  --collapse-border-color: #e0e6ed;
+  --collapse-header-height: 43px;
+  --collapse-border-radius: 0;
+  --collapse-header-padding: 20px;
+  --collapse-header-fill: #fff;
+  --collapse-header-color: #475669;
+  --collapse-header-size: 13px;
+  --collapse-content-fill: #f9fafc;
+  --collapse-content-size: 13px;
+  --collapse-content-color: #1f2d3d;
 }

+ 7 - 8
packages/theme-default/src/date-picker/time-spinner.css

@@ -4,22 +4,21 @@
   @b time-spinner {
     &.has-seconds {
       .el-time-spinner__wrapper {
-        width: calc(100% / 3);
+        width: 33%;
+      }
+
+      .el-time-spinner__wrapper:nth-child(2) {
+        padding-left: 1%;
       }
     }
 
     @e wrapper {
-      height: 190px;
-      overflow: hidden;
+      max-height: 190px;
+      overflow: auto;
       display: inline-block;
       width: 50%;
       vertical-align: top;
       position: relative;
-      -ms-overflow-style: none;
-
-      &:hover {
-        overflow-y: auto;
-      }
     }
 
     @e list {

+ 2 - 0
packages/theme-default/src/index.css

@@ -40,5 +40,7 @@
 @import "./rate.css";
 @import "./steps.css";
 @import "./step.css";
+@import "./scrollbar.css";
 @import "./carousel.css";
 @import "./carousel-item.css";
+@import "./collapse.css";

+ 68 - 0
packages/theme-default/src/scrollbar.css

@@ -0,0 +1,68 @@
+@component-namespace el {
+  @b scrollbar {
+    overflow: hidden;
+    position: relative;
+
+    &:hover,
+    &:active,
+    &:focus {
+      .el-scrollbar__bar {
+        opacity: 1;
+        transition: opacity 340ms ease-out;
+      }
+    }
+
+    @e wrap {
+      overflow: scroll;
+
+      @m hidden-default {
+        &::-webkit-scrollbar {
+          width: 0;
+          height: 0;
+        }
+      }
+    }
+
+    @e thumb {
+      position: relative;
+      display: block;
+      size: 0;
+      cursor: pointer;
+      border-radius: inherit;
+      background-color: var(--scrollbar-background-color);
+      transition: .3s background-color;
+
+      &:hover {
+        background-color: var(--scrollbar-hover-background-color);
+      }
+    }
+
+    @e bar {
+      position: absolute;
+      right: 2px;
+      bottom: 2px;
+      z-index: 1;
+      border-radius: 4px;
+      opacity: 0;
+      transition: opacity 120ms ease-out;
+
+      @when vertical {
+        width: 6px;
+        top: 2px;
+
+        > div {
+          width: 100%;
+        }
+      }
+
+      @when horizontal {
+        height: 6px;
+        left: 2px;
+
+        > div {
+          height: 100%;
+        }
+      }
+    }
+  }
+}

+ 5 - 3
packages/theme-default/src/select-dropdown.css

@@ -43,14 +43,16 @@
     font-size: var(--select-font-size);
   }
 
+  @b select-dropdown__wrap {
+    max-height: var(--select-dropdown-max-height);
+    width: 100%;
+  }
+
   @b select-dropdown__list {
     list-style: none;
     padding: var(--select-dropdown-padding);
     margin: 0;
-    width: 100%;
-    max-height: var(--select-dropdown-max-height);
     box-sizing: border-box;
-    overflow-y: auto;
 
     @when empty {
       padding: 0;

+ 0 - 5
packages/theme-default/src/time-select.css

@@ -9,12 +9,7 @@
 
 .time-select .el-picker-panel__content {
   max-height: 200px;
-  overflow: hidden;
   margin: 0;
-
-  &:hover {
-    overflow-y: auto;
-  }
 }
 
 .time-select-item {

+ 10 - 1
src/index.js

@@ -56,7 +56,10 @@ import Rate from '../packages/rate';
 import Steps from '../packages/steps';
 import Step from '../packages/step';
 import Carousel from '../packages/carousel';
+import Scrollbar from '../packages/scrollbar';
 import CarouselItem from '../packages/carousel-item';
+import Collapse from '../packages/collapse';
+import CollapseItem from '../packages/collapse-item';
 import locale from 'element-ui/src/locale';
 
 const install = function(Vue, opts = {}) {
@@ -116,7 +119,10 @@ const install = function(Vue, opts = {}) {
   Vue.component(Steps.name, Steps);
   Vue.component(Step.name, Step);
   Vue.component(Carousel.name, Carousel);
+  Vue.component(Scrollbar.name, Scrollbar);
   Vue.component(CarouselItem.name, CarouselItem);
+  Vue.component(Collapse.name, Collapse);
+  Vue.component(CollapseItem.name, CollapseItem);
 
   Vue.use(Loading.directive);
 
@@ -194,5 +200,8 @@ module.exports = {
   Steps,
   Step,
   Carousel,
-  CarouselItem
+  Scrollbar,
+  CarouselItem,
+  Collapse,
+  CollapseItem
 };

+ 28 - 0
src/utils/scrollbar-width.js

@@ -0,0 +1,28 @@
+import Vue from 'vue';
+
+let scrollBarWidth;
+
+export default function() {
+  if (Vue.prototype.$isServer) return 0;
+  if (scrollBarWidth !== undefined) return scrollBarWidth;
+
+  const outer = document.createElement('div');
+  outer.className = 'el-scrollbar__wrap';
+  outer.style.visibility = 'hidden';
+  outer.style.width = '100px';
+  outer.style.position = 'absolute';
+  outer.style.top = '-9999px';
+  document.body.appendChild(outer);
+
+  const widthNoScroll = outer.offsetWidth;
+  outer.style.overflow = 'scroll';
+
+  const inner = document.createElement('div');
+  inner.style.width = '100%';
+  outer.appendChild(inner);
+
+  const widthWithScroll = inner.offsetWidth;
+  outer.parentNode.removeChild(outer);
+
+  return widthNoScroll - widthWithScroll;
+};

+ 96 - 0
test/unit/specs/collapse.spec.js

@@ -0,0 +1,96 @@
+import { createVue, destroyVM } from '../util';
+
+describe('Collapse', () => {
+  let vm;
+  afterEach(() => {
+    destroyVM(vm);
+  });
+
+  it('create', done => {
+    vm = createVue({
+      data() {
+        return {
+          activeNames: ['1']
+        };
+      },
+      template: `
+        <el-collapse v-model="activeNames" ref="collapse">
+          <el-collapse-item title="一致性 Consistency" name="1" ref="item1">
+            <div>与现实生活一致:与现实生活的流程、逻辑保持一致,遵循用户习惯的语言和概念;</div>
+            <div>在界面中一致:所有的元素和结构需保持一致,比如:设计样式、图标和文本、元素的位置等。</div>
+          </el-collapse-item>
+          <el-collapse-item title="反馈 Feedback" name="2">
+            <div>控制反馈:通过界面样式和交互动效让用户可以清晰的感知自己的操作;</div>
+            <div>页面反馈:操作后,通过页面元素的变化清晰地展现当前状态。</div>
+          </el-collapse-item>
+          <el-collapse-item title="效率 Efficiency" name="3" ref="item3">
+            <div>简化流程:设计简洁直观的操作流程;</div>
+            <div>清晰明确:语言表达清晰且表意明确,让用户快速理解进而作出决策;</div>
+            <div>帮助用户识别:界面简单直白,让用户快速识别而非回忆,减少用户记忆负担。</div>
+          </el-collapse-item>
+          <el-collapse-item title="可控 Controllability" name="4">
+            <div>用户决策:根据场景可给予用户操作建议或安全提示,但不能代替用户进行决策;</div>
+            <div>结果可控:用户可以自由的进行操作,包括撤销、回退和终止当前操作等。</div>
+          </el-collapse-item>
+        </el-collapse>
+      `
+    }, true);
+    const collapse = vm.$refs.collapse;
+    const spy = sinon.spy();
+
+    collapse.$on('change', spy);
+    expect(vm.$refs.item1.isActive).to.be.true;
+    vm.$refs.item3.$el.querySelector('.el-collapse-item__header').click();
+    vm.$nextTick(_ => {
+      expect(vm.$refs.item1.isActive).to.be.true;
+      expect(vm.$refs.item3.isActive).to.be.true;
+      expect(spy.calledOnce).to.be.true;
+
+      vm.$refs.item1.$el.querySelector('.el-collapse-item__header').click();
+
+      vm.$nextTick(_ => {
+        expect(vm.$refs.item1.isActive).to.be.false;
+        done();
+      });
+    });
+  });
+
+  it('accordion', done => {
+    vm = createVue({
+      data() {
+        return {
+          activeNames: ['1']
+        };
+      },
+      template: `
+        <el-collapse accordion v-model="activeNames" ref="collapse">
+          <el-collapse-item title="一致性 Consistency" name="1" ref="item1">
+            <div>与现实生活一致:与现实生活的流程、逻辑保持一致,遵循用户习惯的语言和概念;</div>
+            <div>在界面中一致:所有的元素和结构需保持一致,比如:设计样式、图标和文本、元素的位置等。</div>
+          </el-collapse-item>
+          <el-collapse-item title="反馈 Feedback" name="2">
+            <div>控制反馈:通过界面样式和交互动效让用户可以清晰的感知自己的操作;</div>
+            <div>页面反馈:操作后,通过页面元素的变化清晰地展现当前状态。</div>
+          </el-collapse-item>
+          <el-collapse-item title="效率 Efficiency" name="3" ref="item3">
+            <div>简化流程:设计简洁直观的操作流程;</div>
+            <div>清晰明确:语言表达清晰且表意明确,让用户快速理解进而作出决策;</div>
+            <div>帮助用户识别:界面简单直白,让用户快速识别而非回忆,减少用户记忆负担。</div>
+          </el-collapse-item>
+          <el-collapse-item title="可控 Controllability" name="4">
+            <div>用户决策:根据场景可给予用户操作建议或安全提示,但不能代替用户进行决策;</div>
+            <div>结果可控:用户可以自由的进行操作,包括撤销、回退和终止当前操作等。</div>
+          </el-collapse-item>
+        </el-collapse>
+      `
+    }, true);
+
+    expect(vm.$refs.item1.isActive).to.be.true;
+    vm.$refs.item3.$el.querySelector('.el-collapse-item__header').click();
+    vm.$nextTick(_ => {
+      expect(vm.$refs.item1.isActive).to.be.false;
+      expect(vm.$refs.item3.isActive).to.be.true;
+      done();
+    });
+  });
+});

+ 15 - 15
test/unit/specs/time-picker.spec.js

@@ -58,23 +58,23 @@ describe('TimePicker', () => {
 
     Vue.nextTick(_ => {
       const list = vm.picker.$el.querySelectorAll('.el-time-spinner__list');
-      const hoursElm = list[0];
-      const minutesElm = list[1];
-      const secondsElm = list[2];
-      const hourElm = hoursElm.querySelectorAll('.el-time-spinner__item')[4];
-      const minuteElm = minutesElm.querySelectorAll('.el-time-spinner__item')[36];
-      const secondElm = secondsElm.querySelectorAll('.el-time-spinner__item')[20];
+      const hoursEl = list[0];
+      const minutesEl = list[1];
+      const secondsEl = list[2];
+      const hourEl = hoursEl.querySelectorAll('.el-time-spinner__item')[4];
+      const minuteEl = minutesEl.querySelectorAll('.el-time-spinner__item')[36];
+      const secondEl = secondsEl.querySelectorAll('.el-time-spinner__item')[20];
 
-      hourElm.click();
-      minuteElm.click();
-      secondElm.click();
+      hourEl.click();
+      minuteEl.click();
+      secondEl.click();
 
       Vue.nextTick(_ => {
         const date = vm.picker.currentDate;
 
-        expect(hourElm.classList.contains('active')).to.true;
-        expect(minuteElm.classList.contains('active')).to.true;
-        expect(secondElm.classList.contains('active')).to.true;
+        expect(hourEl.classList.contains('active')).to.true;
+        expect(minuteEl.classList.contains('active')).to.true;
+        expect(secondEl.classList.contains('active')).to.true;
         expect(date.getHours()).to.equal(4);
         expect(date.getMinutes()).to.equal(36);
         expect(date.getSeconds()).to.equal(20);
@@ -165,12 +165,12 @@ describe('TimePicker', () => {
 
     setTimeout(_ => {
       const list = vm.picker.$el.querySelectorAll('.el-time-spinner__list');
-      const hoursElm = list[0];
+      const hoursEl = list[0];
       const disabledHours = [].slice
-        .call(hoursElm.querySelectorAll('.disabled'))
+        .call(hoursEl.querySelectorAll('.disabled'))
         .map(node => Number(node.textContent));
 
-      hoursElm.querySelectorAll('.disabled')[0].click();
+      hoursEl.querySelectorAll('.disabled')[0].click();
       expect(disabledHours).to.not.include.members([18, 19, 20]);
       destroyVM(vm);
       done();

+ 58 - 0
test/unit/specs/time-select.spec.js

@@ -140,4 +140,62 @@ describe('TimeSelect', () => {
       }, 50);
     }, 50);
   });
+
+  it('set maxTime', done => {
+    vm = createVue(`
+      <el-time-select
+        ref="picker"
+        :picker-options="{
+          maxTime: '14:30',
+          step: '00:30'
+        }">
+      </el-time-select>
+    `, true);
+    const input = vm.$el.querySelector('input');
+    const picker = vm.$refs.picker;
+
+    input.focus();
+    input.blur();
+
+    setTimeout(_ => {
+      const elm = picker.picker.$el.querySelector('.disabled');
+
+      expect(elm.textContent).to.equal('15:00');
+      destroyVM(vm);
+      done();
+    }, 50);
+  });
+
+  it('maxTime > value', done => {
+    vm = createVue({
+      template: `
+        <el-time-select
+          ref="picker"
+          v-model="value"
+          :picker-options="{
+            maxTime: '14:30'
+          }">
+        </el-time-select>
+      `,
+      data() {
+        return { value: '09:30' };
+      }
+    }, true);
+    const input = vm.$el.querySelector('input');
+    const picker = vm.$refs.picker;
+
+    input.focus();
+    input.blur();
+
+    setTimeout(_ => {
+      vm.value = '10:30';
+
+      setTimeout(_ => {
+        expect(picker.picker.value).to.equal('09:30');
+        destroyVM(vm);
+        done();
+      }, 50);
+    }, 50);
+  });
+
 });