Browse Source

Merge pull request #2559 from baiyaaaaa/cascader

add cascader component
杨奕 8 năm trước cách đây
mục cha
commit
c672727673

+ 2 - 1
components.json

@@ -58,5 +58,6 @@
   "scrollbar": "./packages/scrollbar/index.js",
   "carousel-item": "./packages/carousel-item/index.js",
   "collapse": "./packages/collapse/index.js",
-  "collapse-item": "./packages/collapse-item/index.js"
+  "collapse-item": "./packages/collapse-item/index.js",
+  "cascader": "./packages/cascader/index.js"
 }

+ 487 - 0
examples/docs/en-US/cascader.md

@@ -0,0 +1,487 @@
+<script>
+  module.exports = {
+    data() {
+      return {
+        options: [{
+          value: 'zhejiang',
+          label: 'Zhejiang',
+          children: [{
+            value: 'hangzhou',
+            label: 'Hangzhou',
+            children: [{
+              value: 'xihu',
+              label: 'West Lake',
+            }],
+          }, {
+            value: 'ningbo',
+            label: 'NingBo',
+            children: [{
+              value: 'jiangbei',
+              label: 'Jiang Bei',
+            }],
+          }],
+        }, {
+          value: 'jiangsu',
+          label: 'Jiangsu',
+          children: [{
+            value: 'nanjing',
+            label: 'Nanjing',
+            children: [{
+              value: 'zhonghuamen',
+              label: 'Zhong Hua Men',
+            }],
+          }],
+        }],
+        optionsWithDisabled: [{
+          value: 'zhejiang',
+          label: 'Zhejiang',
+          disabled: true,
+          children: [{
+            value: 'hangzhou',
+            label: 'Hangzhou',
+            children: [{
+              value: 'xihu',
+              label: 'West Lake',
+            }],
+          }, {
+            value: 'ningbo',
+            label: 'NingBo',
+            children: [{
+              value: 'jiangbei',
+              label: 'Jiang Bei',
+            }],
+          }],
+        }, {
+          value: 'jiangsu',
+          label: 'Jiangsu',
+          children: [{
+            value: 'nanjing',
+            label: 'Nanjing',
+            children: [{
+              value: 'zhonghuamen',
+              label: 'Zhong Hua Men',
+            }],
+          }],
+        }],
+        selectedOptions: [],
+        selectedOptions2: ['jiangsu', 'nanjing', 'zhonghuamen']
+      };
+    },
+    methods: {
+      handleChange(value) {
+        console.log(value);
+      }
+    }
+  };
+</script>
+
+<style>
+  .demo-cascader {
+    .el-cascader {
+      width: 222px;
+    }
+  }
+  .demo-cascader-size {
+    .el-cascader {
+      vertical-align: top;
+      margin-right: 15px;
+    }
+  }
+</style>
+
+## Cascader
+
+It's used to select from a set of associated data set. Such as province/city/district, company level, and categories.
+
+### Basic usage
+
+:::demo
+```html
+<el-cascader
+  placeholder="Please select"
+  :options="options"
+  v-model="selectedOptions"
+  @change="handleChange"
+></el-cascader>
+<script>
+  module.exports = {
+    data() {
+      return {
+        options: [{
+          value: 'zhejiang',
+          label: 'Zhejiang',
+          children: [{
+            value: 'hangzhou',
+            label: 'Hangzhou',
+            children: [{
+              value: 'xihu',
+              label: 'West Lake',
+            }],
+          }, {
+            value: 'ningbo',
+            label: 'NingBo',
+            children: [{
+              value: 'jiangbei',
+              label: 'Jiang Bei',
+            }],
+          }],
+        }, {
+          value: 'jiangsu',
+          label: 'Jiangsu',
+          children: [{
+            value: 'nanjing',
+            label: 'Nanjing',
+            children: [{
+              value: 'zhonghuamen',
+              label: 'Zhong Hua Men',
+            }],
+          }],
+        }],
+        selectedOptions: []
+      };
+    },
+    methods: {
+      handleChange(value) {
+        console.log(value);
+      }
+    }
+  };
+</script>
+```
+:::
+
+### Disabled option
+
+:::demo
+```html
+<el-cascader
+  placeholder="Please select"
+  :options="optionsWithDisabled"
+></el-cascader>
+<script>
+  module.exports = {
+    data() {
+      return {
+        optionsWithDisabled: [{
+          value: 'zhejiang',
+          label: 'Zhejiang',
+          disabled: true,
+          children: [{
+            value: 'hangzhou',
+            label: 'Hangzhou',
+            children: [{
+              value: 'xihu',
+              label: 'West Lake',
+            }],
+          }, {
+            value: 'ningbo',
+            label: 'NingBo',
+            children: [{
+              value: 'jiangbei',
+              label: 'Jiang Bei',
+            }],
+          }],
+        }, {
+          value: 'jiangsu',
+          label: 'Jiangsu',
+          children: [{
+            value: 'nanjing',
+            label: 'Nanjing',
+            children: [{
+              value: 'zhonghuamen',
+              label: 'Zhong Hua Men',
+            }],
+          }],
+        }]
+      };
+    }
+  };
+</script>
+```
+:::
+
+### Default Value
+
+:::demo default value is assigned by an array type value.
+```html
+<el-cascader
+  placeholder="Please select"
+  :options="options"
+  v-model="selectedOptions2"
+></el-cascader>
+<script>
+  module.exports = {
+    data() {
+      return {
+        options: [{
+          value: 'zhejiang',
+          label: 'Zhejiang',
+          children: [{
+            value: 'hangzhou',
+            label: 'Hangzhou',
+            children: [{
+              value: 'xihu',
+              label: 'West Lake',
+            }],
+          }, {
+            value: 'ningbo',
+            label: 'NingBo',
+            children: [{
+              value: 'jiangbei',
+              label: 'Jiang Bei',
+            }],
+          }],
+        }, {
+          value: 'jiangsu',
+          label: 'Jiangsu',
+          children: [{
+            value: 'nanjing',
+            label: 'Nanjing',
+            children: [{
+              value: 'zhonghuamen',
+              label: 'Zhong Hua Men',
+            }],
+          }],
+        }],
+        selectedOptions2: ['jiangsu', 'nanjing', 'zhonghuamen']
+      };
+    }
+  };
+</script>
+```
+:::
+
+### Size
+
+:::demo
+```html
+<div class="demo-cascader-size">
+  <el-cascader
+    placeholder="Please select"
+    :options="options"
+    size="large"
+  ></el-cascader>
+  <el-cascader
+    placeholder="Please select"
+    :options="options"
+  ></el-cascader>
+  <el-cascader
+    placeholder="Please select"
+    :options="options"
+    size="small"
+  ></el-cascader>
+</div>
+<script>
+  module.exports = {
+    data() {
+      return {
+        options: [{
+          value: 'zhejiang',
+          label: 'Zhejiang',
+          children: [{
+            value: 'hangzhou',
+            label: 'Hangzhou',
+            children: [{
+              value: 'xihu',
+              label: 'West Lake',
+            }],
+          }, {
+            value: 'ningbo',
+            label: 'NingBo',
+            children: [{
+              value: 'jiangbei',
+              label: 'Jiang Bei',
+            }],
+          }],
+        }, {
+          value: 'jiangsu',
+          label: 'Jiangsu',
+          children: [{
+            value: 'nanjing',
+            label: 'Nanjing',
+            children: [{
+              value: 'zhonghuamen',
+              label: 'Zhong Hua Men',
+            }],
+          }],
+        }]
+      };
+    }
+  };
+</script>
+```
+:::
+
+### Hover to expand
+
+Hover to expand the next level options, click to select option.
+
+:::demo
+```html
+<el-cascader
+  placeholder="Please select"
+  :options="options"
+  expand-trigger="hover"
+></el-cascader>
+<script>
+  module.exports = {
+    data() {
+      return {
+        options: [{
+          value: 'zhejiang',
+          label: 'Zhejiang',
+          children: [{
+            value: 'hangzhou',
+            label: 'Hangzhou',
+            children: [{
+              value: 'xihu',
+              label: 'West Lake',
+            }],
+          }, {
+            value: 'ningbo',
+            label: 'NingBo',
+            children: [{
+              value: 'jiangbei',
+              label: 'Jiang Bei',
+            }],
+          }],
+        }, {
+          value: 'jiangsu',
+          label: 'Jiangsu',
+          children: [{
+            value: 'nanjing',
+            label: 'Nanjing',
+            children: [{
+              value: 'zhonghuamen',
+              label: 'Zhong Hua Men',
+            }],
+          }],
+        }]
+      };
+    }
+  };
+</script>
+```
+:::
+
+### Change on select
+
+Allow only select parent options.
+
+:::demo
+```html
+<el-cascader
+  placeholder="Please select"
+  :options="options"
+  change-on-select
+></el-cascader>
+<script>
+  module.exports = {
+    data() {
+      return {
+        options: [{
+          value: 'zhejiang',
+          label: 'Zhejiang',
+          children: [{
+            value: 'hangzhou',
+            label: 'Hangzhou',
+            children: [{
+              value: 'xihu',
+              label: 'West Lake',
+            }],
+          }, {
+            value: 'ningbo',
+            label: 'NingBo',
+            children: [{
+              value: 'jiangbei',
+              label: 'Jiang Bei',
+            }],
+          }],
+        }, {
+          value: 'jiangsu',
+          label: 'Jiangsu',
+          children: [{
+            value: 'nanjing',
+            label: 'Nanjing',
+            children: [{
+              value: 'zhonghuamen',
+              label: 'Zhong Hua Men',
+            }],
+          }],
+        }]
+      };
+    }
+  };
+</script>
+```
+:::
+
+### Search
+
+Search and select options directly.
+
+:::demo
+```html
+<el-cascader
+  placeholder="Please select"
+  :options="options"
+  filterable
+></el-cascader>
+<script>
+  module.exports = {
+    data() {
+      return {
+        options: [{
+          value: 'zhejiang',
+          label: 'Zhejiang',
+          children: [{
+            value: 'hangzhou',
+            label: 'Hangzhou',
+            children: [{
+              value: 'xihu',
+              label: 'West Lake',
+            }],
+          }, {
+            value: 'ningbo',
+            label: 'NingBo',
+            children: [{
+              value: 'jiangbei',
+              label: 'Jiang Bei',
+            }],
+          }],
+        }, {
+          value: 'jiangsu',
+          label: 'Jiangsu',
+          children: [{
+            value: 'nanjing',
+            label: 'Nanjing',
+            children: [{
+              value: 'zhonghuamen',
+              label: 'Zhong Hua Men',
+            }],
+          }],
+        }]
+      };
+    }
+  };
+</script>
+```
+:::
+
+### Attributes
+| Attribute | Description         | Type    | Options       | Default|
+|---------- |-------------------- |---------|-------------  |-------- |
+| options   | data source of the options | array  |       —        |     —    |
+| value | selected value   | array | — |     —    |
+| popper-class | className of popup overlay   | string |      —         |     —    |
+| placeholder | input placeholder | string    |      —         |     —    |
+| disabled  | 是否禁用    | boolean   |  — | false   |
+| clearable  | whether allow clear    | boolean   |  — | false   |
+| expand-trigger  | trigger mode of expandind the current item | string | click / hover | 'click'   |
+| filterable  | whether the options can be searched | boolean | — | — |
+| size  | size | string | large / small / mini | — |
+
+### Events
+| Event Name | Description | Parameters |
+|---------- |-------- |---------- |
+| change  | triggers when the binding value changes | value |

+ 491 - 0
examples/docs/zh-CN/cascader.md

@@ -0,0 +1,491 @@
+<script>
+  module.exports = {
+    data() {
+      return {
+        options: [{
+          value: 'zhejiang',
+          label: 'Zhejiang',
+          children: [{
+            value: 'hangzhou',
+            label: 'Hangzhou',
+            children: [{
+              value: 'xihu',
+              label: 'West Lake'
+            }]
+          }, {
+            value: 'ningbo',
+            label: 'NingBo',
+            children: [{
+              value: 'jiangbei',
+              label: 'Jiang Bei'
+            }]
+          }]
+        }, {
+          value: 'jiangsu',
+          label: 'Jiangsu',
+          children: [{
+            value: 'nanjing',
+            label: 'Nanjing',
+            children: [{
+              value: 'zhonghuamen',
+              label: 'Zhong Hua Men'
+            }]
+          }]
+        }],
+        optionsWithDisabled: [{
+          value: 'zhejiang',
+          label: 'Zhejiang',
+          disabled: true,
+          children: [{
+            value: 'hangzhou',
+            label: 'Hangzhou',
+            children: [{
+              value: 'xihu',
+              label: 'West Lake'
+            }]
+          }, {
+            value: 'ningbo',
+            label: 'NingBo',
+            children: [{
+              value: 'jiangbei',
+              label: 'Jiang Bei'
+            }]
+          }]
+        }, {
+          value: 'jiangsu',
+          label: 'Jiangsu',
+          children: [{
+            value: 'nanjing',
+            label: 'Nanjing',
+            children: [{
+              value: 'zhonghuamen',
+              label: 'Zhong Hua Men'
+            }]
+          }]
+        }],
+        selectedOptions: [],
+        selectedOptions2: ['jiangsu', 'nanjing', 'zhonghuamen']
+      };
+    },
+    methods: {
+      handleChange(value) {
+        console.log(value);
+      }
+    }
+  };
+</script>
+
+<style>
+  .demo-cascader {
+    .el-cascader {
+      width: 222px;
+    }
+  }
+  .demo-cascader-size {
+    .el-cascader {
+      vertical-align: top;
+      margin-right: 15px;
+    }
+  }
+</style>
+
+## 级联选择
+
+需要从一组相关联的数据集合进行选择,例如省市区,公司层级,事物分类等。
+
+从一个较大的数据集合中进行选择时,用多级分类进行分隔,方便选择。
+
+### 基本使用
+
+:::demo
+```html
+<el-cascader
+  placeholder="请选择"
+  :options="options"
+  v-model="selectedOptions"
+  @change="handleChange"
+></el-cascader>
+<script>
+  module.exports = {
+    data() {
+      return {
+        options: [{
+          value: 'zhejiang',
+          label: 'Zhejiang',
+          children: [{
+            value: 'hangzhou',
+            label: 'Hangzhou',
+            children: [{
+              value: 'xihu',
+              label: 'West Lake',
+            }],
+          }, {
+            value: 'ningbo',
+            label: 'NingBo',
+            children: [{
+              value: 'jiangbei',
+              label: 'Jiang Bei',
+            }],
+          }],
+        }, {
+          value: 'jiangsu',
+          label: 'Jiangsu',
+          children: [{
+            value: 'nanjing',
+            label: 'Nanjing',
+            children: [{
+              value: 'zhonghuamen',
+              label: 'Zhong Hua Men',
+            }],
+          }],
+        }],
+        selectedOptions: []
+      };
+    },
+    methods: {
+      handleChange(value) {
+        console.log(value);
+      }
+    }
+  };
+</script>
+```
+:::
+
+### 禁用选项
+
+通过在数据源中设置 `disabled` 字段来声明该选项时禁用的
+
+:::demo
+```html
+<el-cascader
+  placeholder="请选择"
+  :options="optionsWithDisabled"
+></el-cascader>
+<script>
+  module.exports = {
+    data() {
+      return {
+        optionsWithDisabled: [{
+          value: 'zhejiang',
+          label: 'Zhejiang',
+          disabled: true,
+          children: [{
+            value: 'hangzhou',
+            label: 'Hangzhou',
+            children: [{
+              value: 'xihu',
+              label: 'West Lake',
+            }],
+          }, {
+            value: 'ningbo',
+            label: 'NingBo',
+            children: [{
+              value: 'jiangbei',
+              label: 'Jiang Bei',
+            }],
+          }],
+        }, {
+          value: 'jiangsu',
+          label: 'Jiangsu',
+          children: [{
+            value: 'nanjing',
+            label: 'Nanjing',
+            children: [{
+              value: 'zhonghuamen',
+              label: 'Zhong Hua Men',
+            }],
+          }],
+        }]
+      };
+    }
+  };
+</script>
+```
+:::
+
+### 默认值
+
+:::demo 默认值通过数组的方式指定。
+```html
+<el-cascader
+  placeholder="请选择"
+  :options="options"
+  v-model="selectedOptions2"
+></el-cascader>
+<script>
+  module.exports = {
+    data() {
+      return {
+        options: [{
+          value: 'zhejiang',
+          label: 'Zhejiang',
+          children: [{
+            value: 'hangzhou',
+            label: 'Hangzhou',
+            children: [{
+              value: 'xihu',
+              label: 'West Lake',
+            }],
+          }, {
+            value: 'ningbo',
+            label: 'NingBo',
+            children: [{
+              value: 'jiangbei',
+              label: 'Jiang Bei',
+            }],
+          }],
+        }, {
+          value: 'jiangsu',
+          label: 'Jiangsu',
+          children: [{
+            value: 'nanjing',
+            label: 'Nanjing',
+            children: [{
+              value: 'zhonghuamen',
+              label: 'Zhong Hua Men',
+            }],
+          }],
+        }],
+        selectedOptions2: ['jiangsu', 'nanjing', 'zhonghuamen']
+      };
+    }
+  };
+</script>
+```
+:::
+
+### 尺寸
+
+:::demo 提供三种尺寸的级联选择器
+```html
+<div class="demo-cascader-size">
+  <el-cascader
+    placeholder="请选择"
+    :options="options"
+    size="large"
+  ></el-cascader>
+  <el-cascader
+    placeholder="请选择"
+    :options="options"
+  ></el-cascader>
+  <el-cascader
+    placeholder="请选择"
+    :options="options"
+    size="small"
+  ></el-cascader>
+</div>
+<script>
+  module.exports = {
+    data() {
+      return {
+        options: [{
+          value: 'zhejiang',
+          label: 'Zhejiang',
+          children: [{
+            value: 'hangzhou',
+            label: 'Hangzhou',
+            children: [{
+              value: 'xihu',
+              label: 'West Lake',
+            }],
+          }, {
+            value: 'ningbo',
+            label: 'NingBo',
+            children: [{
+              value: 'jiangbei',
+              label: 'Jiang Bei',
+            }],
+          }],
+        }, {
+          value: 'jiangsu',
+          label: 'Jiangsu',
+          children: [{
+            value: 'nanjing',
+            label: 'Nanjing',
+            children: [{
+              value: 'zhonghuamen',
+              label: 'Zhong Hua Men',
+            }],
+          }],
+        }]
+      };
+    }
+  };
+</script>
+```
+:::
+
+### 移入展开
+
+在鼠标移入时就展开下级菜单,完成选择仍需要进行点击。
+
+:::demo
+```html
+<el-cascader
+  placeholder="请选择"
+  :options="options"
+  expand-trigger="hover"
+></el-cascader>
+<script>
+  module.exports = {
+    data() {
+      return {
+        options: [{
+          value: 'zhejiang',
+          label: 'Zhejiang',
+          children: [{
+            value: 'hangzhou',
+            label: 'Hangzhou',
+            children: [{
+              value: 'xihu',
+              label: 'West Lake',
+            }],
+          }, {
+            value: 'ningbo',
+            label: 'NingBo',
+            children: [{
+              value: 'jiangbei',
+              label: 'Jiang Bei',
+            }],
+          }],
+        }, {
+          value: 'jiangsu',
+          label: 'Jiangsu',
+          children: [{
+            value: 'nanjing',
+            label: 'Nanjing',
+            children: [{
+              value: 'zhonghuamen',
+              label: 'Zhong Hua Men',
+            }],
+          }],
+        }]
+      };
+    }
+  };
+</script>
+```
+:::
+
+### 选择即改变
+
+该模式下允许只选中父级选项。
+
+:::demo
+```html
+<el-cascader
+  placeholder="请选择"
+  :options="options"
+  change-on-select
+></el-cascader>
+<script>
+  module.exports = {
+    data() {
+      return {
+        options: [{
+          value: 'zhejiang',
+          label: 'Zhejiang',
+          children: [{
+            value: 'hangzhou',
+            label: 'Hangzhou',
+            children: [{
+              value: 'xihu',
+              label: 'West Lake',
+            }],
+          }, {
+            value: 'ningbo',
+            label: 'NingBo',
+            children: [{
+              value: 'jiangbei',
+              label: 'Jiang Bei',
+            }],
+          }],
+        }, {
+          value: 'jiangsu',
+          label: 'Jiangsu',
+          children: [{
+            value: 'nanjing',
+            label: 'Nanjing',
+            children: [{
+              value: 'zhonghuamen',
+              label: 'Zhong Hua Men',
+            }],
+          }],
+        }]
+      };
+    }
+  };
+</script>
+```
+:::
+
+### 可搜索
+
+可以直接搜索选项并选择。
+
+:::demo
+```html
+<el-cascader
+  placeholder="请选择"
+  :options="options"
+  filterable
+></el-cascader>
+<script>
+  module.exports = {
+    data() {
+      return {
+        options: [{
+          value: 'zhejiang',
+          label: 'Zhejiang',
+          children: [{
+            value: 'hangzhou',
+            label: 'Hangzhou',
+            children: [{
+              value: 'xihu',
+              label: 'West Lake',
+            }],
+          }, {
+            value: 'ningbo',
+            label: 'NingBo',
+            children: [{
+              value: 'jiangbei',
+              label: 'Jiang Bei',
+            }],
+          }],
+        }, {
+          value: 'jiangsu',
+          label: 'Jiangsu',
+          children: [{
+            value: 'nanjing',
+            label: 'Nanjing',
+            children: [{
+              value: 'zhonghuamen',
+              label: 'Zhong Hua Men',
+            }],
+          }],
+        }]
+      };
+    }
+  };
+</script>
+```
+:::
+
+### Attributes
+| 参数      | 说明    | 类型      | 可选值       | 默认值   |
+|---------- |-------- |---------- |-------------  |-------- |
+| options   | 可选项数据源 | array  |       —        |     —    |
+| value | 指定选中项   | array | — |     —    |
+| popper-class | 自定义浮层类名   | string |      —         |     —    |
+| placeholder | 输入框占位文本 | string    |      —         |     —    |
+| disabled  | 是否禁用    | boolean   |  — | false   |
+| clearable  | 是否支持清除    | boolean   |  — | false   |
+| expand-trigger  | 次级菜单的展开方式 | string | click / hover | 'click'   |
+| filterable  | 是否支持搜索选项 | boolean | — | — |
+| size  | 尺寸 | string | large / small / mini | — |
+
+### Events
+| 事件名称      | 说明    | 回调参数      |
+|---------- |-------- |---------- |
+| change  | 当绑定值变化时触发的事件 | 当前值 |

+ 8 - 0
examples/nav.config.json

@@ -76,6 +76,10 @@
               "path": "/select",
               "title": "Select 选择器"
             },
+            {
+              "path": "/cascader",
+              "title": "Cascader 级联选择"
+            },
             {
               "path": "/switch",
               "title": "Switch 开关"
@@ -298,6 +302,10 @@
               "path": "/select",
               "title": "Select"
             },
+            {
+              "path": "/cascader",
+              "title": "Cascader"
+            },
             {
               "path": "/switch",
               "title": "Switch"

+ 18 - 0
packages/cascader/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: 'ElCascader',
+  extends: ['vue2'],
+  alias: config.alias,
+  externals: { vue: config.vue }
+});
+
+module.exports = cooking.resolve();

+ 8 - 0
packages/cascader/index.js

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

+ 15 - 0
packages/cascader/package.json

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

+ 243 - 0
packages/cascader/src/main.vue

@@ -0,0 +1,243 @@
+<template>
+  <span
+    class="el-cascader"
+    :class="[
+      {
+        'is-opened': menuVisible,
+        'is-disabled': disabled
+      },
+      size ? 'el-cascader--' + size : ''
+    ]"
+    @click="handleClick"
+    @mouseenter="inputHover = true"
+    @mouseleave="inputHover = false"
+    ref="reference"
+    v-clickoutside="handleClickoutside"
+  >
+    <el-input
+      ref="input"
+      :readonly="!filterable"
+      :placeholder="displayValue ? undefined : placeholder"
+      v-model="inputValue"
+      @change="handleInputChange"
+      :validate-event="false"
+      :size="size"
+      :disabled="disabled"
+    >
+      <template slot="icon">
+        <i
+          key="1"
+          v-if="inputHover && displayValue !== ''"
+          class="el-input__icon el-icon-circle-close el-cascader__clearIcon"
+          @click="clearValue"
+        ></i>
+        <i
+          key="2"
+          v-else
+          class="el-input__icon el-icon-caret-bottom"
+          :class="{ 'is-reverse': menuVisible }"
+        ></i>
+      </template>
+    </el-input>
+    <span class="el-cascader__label" v-show="inputValue === ''">{{displayValue}}</span>
+  </span>
+</template>
+
+<script>
+import Vue from 'vue';
+import ElCascaderMenu from './menu';
+import ElInput from 'element-ui/packages/input';
+import Popper from 'element-ui/src/utils/vue-popper';
+import Clickoutside from 'element-ui/src/utils/clickoutside';
+import emitter from 'element-ui/src/mixins/emitter';
+import Locale from 'element-ui/src/mixins/locale';
+
+const popperMixin = {
+  props: {
+    placement: {
+      type: String,
+      default: 'bottom-start'
+    },
+    appendToBody: Popper.props.appendToBody,
+    offset: Popper.props.offset,
+    boundariesPadding: Popper.props.boundariesPadding,
+    popperOptions: Popper.props.popperOptions
+  },
+  methods: Popper.methods,
+  data: Popper.data,
+  beforeDestroy: Popper.beforeDestroy
+};
+
+export default {
+  name: 'ElCascader',
+
+  directives: { Clickoutside },
+
+  mixins: [popperMixin, emitter, Locale],
+
+  components: {
+    ElInput
+  },
+
+  props: {
+    options: {
+      type: Array,
+      required: true
+    },
+    value: {
+      type: Array,
+      default() {
+        return [];
+      }
+    },
+    placeholder: String,
+    disabled: Boolean,
+    clearable: {
+      type: Boolean,
+      default: true
+    },
+    changeOnSelect: Boolean,
+    popperClass: String,
+    expandTrigger: {
+      type: String,
+      default: 'click'
+    },
+    filterable: Boolean,
+    size: String
+  },
+
+  data() {
+    return {
+      currentValue: this.value,
+      displayValue: this.value.join('/'),
+      menuVisible: false,
+      inputHover: false,
+      inputValue: '',
+      flatOptions: this.filterable && this.flattenOptions(this.options)
+    };
+  },
+
+  watch: {
+    menuVisible(value) {
+      value ? this.showMenu() : this.hideMenu();
+    },
+    value(value) {
+      this.currentValue = value;
+    },
+    currentValue(value) {
+      this.displayValue = value.join('/');
+      this.dispatch('ElFormItem', 'el.form.change', [value]);
+    },
+    options(value) {
+      this.menu.options = value;
+    }
+  },
+
+  methods: {
+    showMenu() {
+      if (!this.menu) {
+        this.menu = new Vue(ElCascaderMenu).$mount();
+        this.menu.options = this.options;
+        this.menu.expandTrigger = this.expandTrigger;
+        this.menu.changeOnSelect = this.changeOnSelect;
+        this.menu.popperClass = this.popperClass;
+        this.popperElm = this.menu.$el;
+      }
+
+      this.menu.value = this.currentValue.slice(0);
+      this.menu.visible = true;
+      this.menu.options = this.options;
+      this.menu.$on('pick', this.handlePick);
+      this.updatePopper();
+      this.$nextTick(_ => {
+        this.menu.inputWidth = this.$refs.input.$el.offsetWidth - 2;
+      });
+    },
+    hideMenu() {
+      this.inputValue = '';
+      this.menu.visible = false;
+    },
+    handlePick(value, close = true) {
+      this.currentValue = value;
+      this.$emit('input', value);
+      this.$emit('change', value);
+
+      if (close) {
+        this.menuVisible = false;
+      }
+    },
+    handleInputChange(value) {
+      if (!this.menuVisible) return;
+      const flatOptions = this.flatOptions;
+
+      if (!value) {
+        this.menu.options = this.options;
+        return;
+      }
+
+      let filteredFlatOptions = flatOptions.filter(optionsStack => {
+        return optionsStack.some(option => option.label.indexOf(value) > -1);
+      });
+
+      if (filteredFlatOptions.length > 0) {
+        filteredFlatOptions = filteredFlatOptions.map(optionStack => {
+          return {
+            __IS__FLAT__OPTIONS: true,
+            value: optionStack.map(item => item.value),
+            label: this.renderFilteredOptionLabel(value, optionStack)
+          };
+        });
+      } else {
+        filteredFlatOptions = [{
+          __IS__FLAT__OPTIONS: true,
+          label: this.t('el.cascader.noMatch'),
+          value: '',
+          disabled: true
+        }];
+      }
+      this.menu.options = filteredFlatOptions;
+    },
+    renderFilteredOptionLabel(inputValue, optionsStack) {
+      return optionsStack.map(({ label }, index) => {
+        const node = label.indexOf(inputValue) > -1 ? this.highlightKeyword(label, inputValue) : label;
+        return index === 0 ? node : [' / ', node];
+      });
+    },
+    highlightKeyword(label, keyword) {
+      const h = this._c;
+      return label.split(keyword)
+        .map((node, index) => index === 0 ? node : [
+          h('span', { class: { 'el-cascader-menu__item__keyword': true }}, [this._v(keyword)]),
+          node
+        ]);
+    },
+    flattenOptions(options, ancestor = []) {
+      let flatOptions = [];
+      options.forEach((option) => {
+        const optionsStack = ancestor.concat(option);
+        if (!option.children) {
+          flatOptions.push(optionsStack);
+        } else {
+          flatOptions = flatOptions.concat(this.flattenOptions(option.children, optionsStack));
+        }
+      });
+      return flatOptions;
+    },
+    clearValue(ev) {
+      ev.stopPropagation();
+      this.handlePick([], true);
+    },
+    handleClickoutside() {
+      this.menuVisible = false;
+    },
+    handleClick() {
+      if (this.disabled) return;
+      if (this.filterable) {
+        this.menuVisible = true;
+        return;
+      }
+      this.menuVisible = !this.menuVisible;
+    }
+  }
+};
+</script>

+ 148 - 0
packages/cascader/src/menu.vue

@@ -0,0 +1,148 @@
+<script>
+  export default {
+    name: 'ElCascaderMenu',
+
+    data() {
+      return {
+        inputWidth: 0,
+        options: [],
+        visible: false,
+        activeValue: [],
+        value: [],
+        expandTrigger: 'click',
+        changeOnSelect: false,
+        popperClass: ''
+      };
+    },
+
+    watch: {
+      visible(value) {
+        if (value) {
+          this.activeValue = this.value;
+        }
+      },
+      value: {
+        immediate: true,
+        handler(value) {
+          this.activeValue = value;
+        }
+      }
+    },
+
+    computed: {
+      activeOptions: {
+        cache: false,
+        get() {
+          const activeValue = this.activeValue;
+
+          const loadActiveOptions = (options, activeOptions = []) => {
+            const level = activeOptions.length;
+            activeOptions[level] = options;
+            let active = activeValue[level];
+            if (active) {
+              options = options.filter(option => option.value === active)[0];
+              if (options && options.children) {
+                loadActiveOptions(options.children, activeOptions);
+              }
+            }
+            return activeOptions;
+          };
+
+          return loadActiveOptions(this.options);
+        }
+      }
+    },
+
+    methods: {
+      select(item, menuIndex) {
+        if (item.__IS__FLAT__OPTIONS) {
+          this.activeValue = item.value;
+        } else {
+          this.activeValue.splice(menuIndex, 1, item.value);
+        }
+        this.$emit('pick', this.activeValue);
+      },
+      activeItem(item, menuIndex) {
+        const len = this.activeOptions.length;
+        this.activeValue.splice(menuIndex, len, item.value);
+        this.activeOptions.splice(menuIndex + 1, len, item.children);
+        if (this.changeOnSelect) this.$emit('pick', this.activeValue, false);
+      }
+    },
+
+    render(h) {
+      const {
+        activeValue,
+        activeOptions,
+        visible,
+        expandTrigger,
+        popperClass
+      } = this;
+
+      const menus = this._l(activeOptions, (menu, menuIndex) => {
+        let isFlat = false;
+        const items = this._l(menu, item => {
+          const events = {
+            on: {}
+          };
+
+          if (item.__IS__FLAT__OPTIONS) isFlat = true;
+
+          if (!item.disabled) {
+            if (item.children) {
+              let triggerEvent = {
+                click: 'click',
+                hover: 'mouseenter'
+              }[expandTrigger];
+              events.on[triggerEvent] = () => { this.activeItem(item, menuIndex); };
+            } else {
+              events.on.click = () => { this.select(item, menuIndex); };
+            }
+          }
+
+          return (
+            <li
+              class={{
+                'el-cascader-menu__item': true,
+                'el-cascader-menu__item--extensible': item.children,
+                'is-active': item.value === activeValue[menuIndex],
+                'is-disabled': item.disabled
+              }}
+              {...events}
+            >
+              {item.label}
+            </li>
+          );
+        });
+        let menuStyle = {};
+        if (isFlat) {
+          menuStyle.width = this.inputWidth + 'px';
+        }
+
+        return (
+          <ul
+            class={{
+              'el-cascader-menu': true,
+              'el-cascader-menu--flexible': isFlat
+            }}
+            style={menuStyle}>
+            {items}
+          </ul>
+        );
+      });
+      return (
+        <transition name="el-zoom-in-top">
+          <div
+            v-show={visible}
+            class={[
+              'el-cascader-menus',
+              popperClass
+            ]}
+          >
+            {menus}
+          </div>
+        </transition>
+      );
+    }
+  };
+</script>

+ 144 - 25
packages/theme-default/src/cascader.css

@@ -1,44 +1,163 @@
 @charset "UTF-8";
 @import "./input.css";
 @import "./common/var.css";
-/*@import "./core/dropdown.css";*/
 
-@component-namespace element {
+@component-namespace el {
 
   @b cascader {
     display: inline-block;
     position: relative;
+    background-color: #fff;
 
-    @e dropdown {
-      background-color: var(--cascader-menu-fill);
-      border: var(--cascader-menu-border);
-      border-radius: var(--cascader-menu-radius);
-      box-shadow: var(--cascader-menu-submenu-shadow);
-      margin-top: 5px;
-      max-height: var(--cascader-height);
+    .el-input,
+    .el-input__inner {
+      cursor: pointer;
+      background-color: transparent;
+      z-index: 1;
+    }
+
+    .el-input__icon {
+      transition: none;
+    }
+
+    .el-icon-caret-bottom {
+      transition: transform .3s;
+
+      @when reverse {
+        transform: rotateZ(180deg);
+      }
+    }
+
+    @e label {
       position: absolute;
+      left: 0;
+      top: 0;
+      height: 100%;
+      line-height: 34px;
+      padding: 0 15px 0 10px;
+      color: var(--input-color);
+      width: 100%;
       white-space: nowrap;
-      z-index: 10;
+      text-overflow: ellipsis;
+      overflow: hidden;
+      box-sizing: border-box;
+      cursor: pointer;
     }
 
-    @e wrap {
-      overflow: hidden;
+    @m large {
+      font-size: var(--input-large-font-size);
+
+      .el-cascader__label {
+        line-height: calc(var(--input-large-height) - 2);
+      }
     }
+    @m small {
+      font-size: var(--input-small-font-size);
+
+      .el-cascader__label {
+        line-height: calc(var(--input-small-height) - 2);
+      }
+    }
+  }
 
-    @e menu {
-      border: 0;
-      box-shadow: none;
-      display: inline-block;
-      margin: 0;
+  @b cascader-menus {
+    white-space: nowrap;
+    background: #fff;
+    position: absolute;
+    margin: 5px 0;
+    z-index: 1001;
+    border: var(--select-dropdown-border);
+    border-radius: var(--border-radius-small);
+    overflow: hidden;
+    box-shadow: var(--select-dropdown-shadow);
+  }
+
+  @b cascader-menu {
+    display: inline-block;
+    vertical-align: top;
+    height: 180px;
+    overflow: auto;
+    border-right: var(--select-dropdown-border);
+    background-color: var(--select-dropdown-background);
+    box-sizing: border-box;
+    margin: 0;
+    padding: 0;
+    min-width: 110px;
+
+    &:last-child {
+      border-right: 0;
+    }
+
+    @e item {
+      font-size: var(--select-font-size);
+      padding: 8px 30px 8px 10px;
       position: relative;
-      vertical-align: top;
-
-      &::before {
-        border-left: var(--cascader-menu-border);
-        content: " ";
-        height: var(--cascader-height);
-        left: 0;
-        position: absolute;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      color: var(--select-option-color);
+      height: var(--select-option-height);
+      line-height: 1.5;
+      box-sizing: border-box;
+      cursor: pointer;
+
+      @e keyword {
+        color: var(--color-danger);
+      }
+      
+      @m extensible {
+        &:after {
+          font-family: 'element-icons';
+          content: "\e602";
+          font-size: 12px;
+          transform: scale(0.8);
+          color: rgb(191, 203, 217);
+          position: absolute;
+          right: 10px;
+          margin-top: 1px;
+        }
+      }
+
+      @when disabled {
+        color: var(--select-option-disabled-color);
+        background-color: var(--select-option-disabled-background);
+        cursor: not-allowed;
+
+        &:hover {
+          background-color: var(--color-white);
+        }
+      }
+
+      @when active {
+        color: var(--color-white);
+        background-color: var(--select-option-selected);
+
+        &.hover {
+          background-color: var(--select-option-selected-hover);
+        }
+      }
+
+      &:hover {
+        background-color: var(--select-option-hover-background);
+      }
+
+      &.selected {
+        color: var(--color-white);
+        background-color: var(--select-option-selected);
+
+        &.hover {
+          background-color: var(--select-option-selected-hover);
+        }
+      }
+    }
+
+    @m flexible {
+      height: auto;
+      max-height: 180px;
+      overflow: auto;
+
+      .el-cascader-menu__item {
+        overflow: visible;
       }
     }
   }

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

@@ -161,6 +161,7 @@
 
   --select-option-color: var(--link-color);
   --select-option-disabled-color: var(--color-extra-light-silver);
+  --select-option-disabled-background: var(--color-white);
   --select-option-height: 36px;
   --select-option-hover-background: var(--color-light-gray);
   --select-option-selected: var(--color-primary);

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

@@ -44,3 +44,4 @@
 @import "./carousel.css";
 @import "./carousel-item.css";
 @import "./collapse.css";
+@import "./cascader.css";

+ 5 - 2
src/index.js

@@ -60,6 +60,7 @@ import Scrollbar from '../packages/scrollbar';
 import CarouselItem from '../packages/carousel-item';
 import Collapse from '../packages/collapse';
 import CollapseItem from '../packages/collapse-item';
+import Cascader from '../packages/cascader';
 import locale from 'element-ui/src/locale';
 
 const components = [
@@ -118,7 +119,8 @@ const components = [
   Scrollbar,
   CarouselItem,
   Collapse,
-  CollapseItem
+  CollapseItem,
+  Cascader
 ];
 
 const install = function(Vue, opts = {}) {
@@ -211,5 +213,6 @@ module.exports = {
   Scrollbar,
   CarouselItem,
   Collapse,
-  CollapseItem
+  CollapseItem,
+  Cascader
 };

+ 4 - 0
src/locale/lang/en.js

@@ -56,6 +56,10 @@ export default {
       noData: 'No data',
       placeholder: 'Select'
     },
+    cascader: {
+      noMatch: 'No matching data',
+      placeholder: 'Select'
+    },
     pagination: {
       goto: 'Go to',
       pagesize: '/page',

+ 4 - 0
src/locale/lang/zh-CN.js

@@ -56,6 +56,10 @@ export default {
       noData: '无数据',
       placeholder: '请选择'
     },
+    cascader: {
+      noMatch: '无匹配数据',
+      placeholder: '请选择'
+    },
     pagination: {
       goto: '前往',
       pagesize: '条/页',

+ 1 - 0
src/utils/vue-popper.js

@@ -83,6 +83,7 @@ export default {
           this.$slots.reference[0]) {
         reference = this.referenceElm = this.$slots.reference[0].elm;
       }
+
       if (!popper || !reference) return;
       if (this.visibleArrow) this.appendArrow(popper);
       if (this.appendToBody) document.body.appendChild(this.popperElm);

+ 524 - 0
test/unit/specs/cascader.spec.js

@@ -0,0 +1,524 @@
+import { createVue, destroyVM, triggerEvent } from '../util';
+
+describe('Cascader', () => {
+  let vm;
+  afterEach(() => {
+    destroyVM(vm);
+  });
+
+  it('create', done => {
+    vm = createVue({
+      template: `
+        <el-cascader
+          ref="cascader"
+          placeholder="请选择"
+          :options="options"
+          v-model="selectedOptions"
+        ></el-cascader>
+      `,
+      data() {
+        return {
+          options: [{
+            value: 'zhejiang',
+            label: 'Zhejiang',
+            children: [{
+              value: 'hangzhou',
+              label: 'Hangzhou',
+              children: [{
+                value: 'xihu',
+                label: 'West Lake'
+              }]
+            }, {
+              value: 'ningbo',
+              label: 'NingBo',
+              children: [{
+                value: 'jiangbei',
+                label: 'Jiang Bei'
+              }]
+            }]
+          }, {
+            value: 'jiangsu',
+            label: 'Jiangsu',
+            children: [{
+              value: 'nanjing',
+              label: 'Nanjing',
+              children: [{
+                value: 'zhonghuamen',
+                label: 'Zhong Hua Men'
+              }]
+            }]
+          }],
+          selectedOptions: []
+        };
+      }
+    }, true);
+    expect(vm.$el).to.be.exist;
+    vm.$el.click();
+    setTimeout(_ => {
+      expect(document.body.querySelector('.el-cascader-menus')).to.be.exist;
+
+      const menu = vm.$refs.cascader.menu;
+      const menuElm = menu.$el;
+      const item1 = menuElm.querySelector('.el-cascader-menu__item');
+
+      item1.click();
+      menu.$nextTick(_ => {
+        expect(menuElm.children.length).to.be.equal(2);
+        expect(item1.classList.contains('is-active')).to.be.true;
+
+        const item2 = menuElm.children[1].querySelector('.el-cascader-menu__item');
+        item2.click();
+
+        menu.$nextTick(_ => {
+          expect(menuElm.children.length).to.be.equal(3);
+          expect(item2.classList.contains('is-active')).to.be.true;
+
+          const item3 = menuElm.children[2].querySelector('.el-cascader-menu__item');
+          item3.click();
+
+          setTimeout(_ => {
+            expect(document.body.querySelector('.el-cascader-menus').style.display).to.be.equal('none');
+            expect(vm.selectedOptions[0]).to.be.equal('zhejiang');
+            expect(vm.selectedOptions[1]).to.be.equal('hangzhou');
+            expect(vm.selectedOptions[2]).to.be.equal('xihu');
+
+            triggerEvent(vm.$refs.cascader.$el, 'mouseenter');
+            vm.$nextTick(_ => {
+              vm.$refs.cascader.$el.querySelector('.el-cascader__clearIcon').click();
+              vm.$nextTick(_ => {
+                expect(vm.selectedOptions.length).to.be.equal(0);
+                done();
+              });
+            });
+          }, 500);
+        });
+      });
+    }, 300);
+  });
+  it('not allow clearable', done => {
+    vm = createVue({
+      template: `
+        <el-cascader
+          ref="cascader"
+          placeholder="请选择"
+          :options="options"
+          :clearable="false"
+          v-model="selectedOptions"
+        ></el-cascader>
+      `,
+      data() {
+        return {
+          options: [{
+            value: 'zhejiang',
+            label: 'Zhejiang',
+            children: [{
+              value: 'hangzhou',
+              label: 'Hangzhou',
+              children: [{
+                value: 'xihu',
+                label: 'West Lake'
+              }]
+            }, {
+              value: 'ningbo',
+              label: 'NingBo',
+              children: [{
+                value: 'jiangbei',
+                label: 'Jiang Bei'
+              }]
+            }]
+          }, {
+            value: 'jiangsu',
+            label: 'Jiangsu',
+            children: [{
+              value: 'nanjing',
+              label: 'Nanjing',
+              children: [{
+                value: 'zhonghuamen',
+                label: 'Zhong Hua Men'
+              }]
+            }]
+          }],
+          selectedOptions: []
+        };
+      }
+    }, true);
+    expect(vm.$el).to.be.exist;
+    triggerEvent(vm.$refs.cascader.$el, 'mouseenter');
+    vm.$nextTick(_ => {
+      expect(vm.$refs.cascader.$el.querySelector('.el-cascader__clearIcon')).to.not.exist;
+      done();
+    });
+  });
+  it('disabled options', done => {
+    vm = createVue({
+      template: `
+        <el-cascader
+          ref="cascader"
+          placeholder="请选择"
+          :options="options"
+          v-model="selectedOptions"
+        ></el-cascader>
+      `,
+      data() {
+        return {
+          options: [{
+            value: 'zhejiang',
+            label: 'Zhejiang',
+            disabled: true,
+            children: [{
+              value: 'hangzhou',
+              label: 'Hangzhou',
+              children: [{
+                value: 'xihu',
+                label: 'West Lake'
+              }]
+            }, {
+              value: 'ningbo',
+              label: 'NingBo',
+              children: [{
+                value: 'jiangbei',
+                label: 'Jiang Bei'
+              }]
+            }]
+          }, {
+            value: 'jiangsu',
+            label: 'Jiangsu',
+            children: [{
+              value: 'nanjing',
+              label: 'Nanjing',
+              children: [{
+                value: 'zhonghuamen',
+                label: 'Zhong Hua Men'
+              }]
+            }]
+          }],
+          selectedOptions: []
+        };
+      }
+    }, true);
+    expect(vm.$el).to.be.exist;
+    vm.$el.click();
+    setTimeout(_ => {
+      expect(document.body.querySelector('.el-cascader-menus')).to.be.exist;
+
+      const menu = vm.$refs.cascader.menu;
+      const menuElm = menu.$el;
+      const item1 = menuElm.querySelector('.el-cascader-menu__item');
+
+      item1.click();
+      menu.$nextTick(_ => {
+        expect(menuElm.children.length).to.be.equal(1);
+        expect(item1.classList.contains('is-active')).to.be.false;
+        done();
+      });
+    }, 300);
+  });
+  it('default value', done => {
+    vm = createVue({
+      template: `
+        <el-cascader
+          ref="cascader"
+          placeholder="请选择"
+          :options="options"
+          v-model="selectedOptions"
+        ></el-cascader>
+      `,
+      data() {
+        return {
+          options: [{
+            value: 'zhejiang',
+            label: 'Zhejiang',
+            children: [{
+              value: 'hangzhou',
+              label: 'Hangzhou',
+              children: [{
+                value: 'xihu',
+                label: 'West Lake'
+              }]
+            }, {
+              value: 'ningbo',
+              label: 'NingBo',
+              children: [{
+                value: 'jiangbei',
+                label: 'Jiang Bei'
+              }]
+            }]
+          }, {
+            value: 'jiangsu',
+            label: 'Jiangsu',
+            children: [{
+              value: 'nanjing',
+              label: 'Nanjing',
+              children: [{
+                value: 'zhonghuamen',
+                label: 'Zhong Hua Men'
+              }]
+            }]
+          }],
+          selectedOptions: ['zhejiang', 'hangzhou', 'xihu']
+        };
+      }
+    }, true);
+    expect(vm.$el).to.be.exist;
+    vm.$el.click();
+    setTimeout(_ => {
+      expect(document.body.querySelector('.el-cascader-menus')).to.be.exist;
+
+      const menu = vm.$refs.cascader.menu;
+      const menuElm = menu.$el;
+      const item1 = menuElm.children[0].querySelector('.el-cascader-menu__item');
+      const item2 = menuElm.children[1].querySelector('.el-cascader-menu__item');
+      const item3 = menuElm.children[2].querySelector('.el-cascader-menu__item');
+
+      expect(menuElm.children.length).to.be.equal(3);
+      expect(item1.classList.contains('is-active')).to.be.true;
+      expect(item2.classList.contains('is-active')).to.be.true;
+      expect(item3.classList.contains('is-active')).to.be.true;
+
+      document.body.click();
+      setTimeout(_ => {
+        expect(document.body.querySelector('.el-cascader-menus').style.display).to.be.equal('none');
+        done();
+      }, 500);
+    }, 300);
+  });
+  it('expand by hover', done => {
+    vm = createVue({
+      template: `
+        <el-cascader
+          ref="cascader"
+          placeholder="请选择"
+          :options="options"
+          expand-trigger="hover"
+          v-model="selectedOptions"
+        ></el-cascader>
+      `,
+      data() {
+        return {
+          options: [{
+            value: 'zhejiang',
+            label: 'Zhejiang',
+            children: [{
+              value: 'hangzhou',
+              label: 'Hangzhou',
+              children: [{
+                value: 'xihu',
+                label: 'West Lake'
+              }]
+            }, {
+              value: 'ningbo',
+              label: 'NingBo',
+              children: [{
+                value: 'jiangbei',
+                label: 'Jiang Bei'
+              }]
+            }]
+          }, {
+            value: 'jiangsu',
+            label: 'Jiangsu',
+            children: [{
+              value: 'nanjing',
+              label: 'Nanjing',
+              children: [{
+                value: 'zhonghuamen',
+                label: 'Zhong Hua Men'
+              }]
+            }]
+          }],
+          selectedOptions: []
+        };
+      }
+    }, true);
+    expect(vm.$el).to.be.exist;
+    vm.$el.click();
+    setTimeout(_ => {
+      expect(document.body.querySelector('.el-cascader-menus')).to.be.exist;
+
+      const menu = vm.$refs.cascader.menu;
+      const menuElm = menu.$el;
+      const item1 = menuElm.querySelector('.el-cascader-menu__item');
+
+      triggerEvent(item1, 'mouseenter');
+      menu.$nextTick(_ => {
+        expect(menuElm.children.length).to.be.equal(2);
+        expect(item1.classList.contains('is-active')).to.be.true;
+
+        const item2 = menuElm.children[1].querySelector('.el-cascader-menu__item');
+        triggerEvent(item2, 'mouseenter');
+
+        menu.$nextTick(_ => {
+          expect(menuElm.children.length).to.be.equal(3);
+          expect(item2.classList.contains('is-active')).to.be.true;
+
+          const item3 = menuElm.children[2].querySelector('.el-cascader-menu__item');
+          item3.click();
+
+          setTimeout(_ => {
+            expect(document.body.querySelector('.el-cascader-menus').style.display).to.be.equal('none');
+            expect(vm.selectedOptions[0]).to.be.equal('zhejiang');
+            expect(vm.selectedOptions[1]).to.be.equal('hangzhou');
+            expect(vm.selectedOptions[2]).to.be.equal('xihu');
+            done();
+          }, 500);
+        });
+      });
+    }, 300);
+  });
+  it('change on select', done => {
+    vm = createVue({
+      template: `
+        <el-cascader
+          ref="cascader"
+          placeholder="请选择"
+          :options="options"
+          change-on-select
+          v-model="selectedOptions"
+        ></el-cascader>
+      `,
+      data() {
+        return {
+          options: [{
+            value: 'zhejiang',
+            label: 'Zhejiang',
+            children: [{
+              value: 'hangzhou',
+              label: 'Hangzhou',
+              children: [{
+                value: 'xihu',
+                label: 'West Lake'
+              }]
+            }, {
+              value: 'ningbo',
+              label: 'NingBo',
+              children: [{
+                value: 'jiangbei',
+                label: 'Jiang Bei'
+              }]
+            }]
+          }, {
+            value: 'jiangsu',
+            label: 'Jiangsu',
+            children: [{
+              value: 'nanjing',
+              label: 'Nanjing',
+              children: [{
+                value: 'zhonghuamen',
+                label: 'Zhong Hua Men'
+              }]
+            }]
+          }],
+          selectedOptions: []
+        };
+      }
+    }, true);
+    expect(vm.$el).to.be.exist;
+    vm.$el.click();
+    setTimeout(_ => {
+      expect(document.body.querySelector('.el-cascader-menus')).to.be.exist;
+
+      const menu = vm.$refs.cascader.menu;
+      const menuElm = menu.$el;
+      const item1 = menuElm.querySelector('.el-cascader-menu__item');
+
+      item1.click();
+      menu.$nextTick(_ => {
+        expect(menuElm.children.length).to.be.equal(2);
+        expect(item1.classList.contains('is-active')).to.be.true;
+        expect(vm.selectedOptions[0]).to.be.equal('zhejiang');
+
+        const item2 = menuElm.children[1].querySelector('.el-cascader-menu__item');
+        item2.click();
+
+        menu.$nextTick(_ => {
+          expect(menuElm.children.length).to.be.equal(3);
+          expect(item2.classList.contains('is-active')).to.be.true;
+          expect(vm.selectedOptions[1]).to.be.equal('hangzhou');
+
+          const item3 = menuElm.children[2].querySelector('.el-cascader-menu__item');
+          item3.click();
+
+          setTimeout(_ => {
+            expect(document.body.querySelector('.el-cascader-menus').style.display).to.be.equal('none');
+            expect(vm.selectedOptions[0]).to.be.equal('zhejiang');
+            expect(vm.selectedOptions[1]).to.be.equal('hangzhou');
+            expect(vm.selectedOptions[2]).to.be.equal('xihu');
+            done();
+          }, 500);
+        });
+      });
+    }, 300);
+  });
+  it('filterable', done => {
+    vm = createVue({
+      template: `
+        <el-cascader
+          ref="cascader"
+          placeholder="请选择"
+          :options="options"
+          filterable
+          v-model="selectedOptions"
+        ></el-cascader>
+      `,
+      data() {
+        return {
+          options: [{
+            value: 'zhejiang',
+            label: 'Zhejiang',
+            children: [{
+              value: 'hangzhou',
+              label: 'Hangzhou',
+              children: [{
+                value: 'xihu',
+                label: 'West Lake'
+              }]
+            }, {
+              value: 'ningbo',
+              label: 'NingBo',
+              children: [{
+                value: 'jiangbei',
+                label: 'Jiang Bei'
+              }]
+            }]
+          }, {
+            value: 'jiangsu',
+            label: 'Jiangsu',
+            children: [{
+              value: 'nanjing',
+              label: 'Nanjing',
+              children: [{
+                value: 'zhonghuamen',
+                label: 'Zhong Hua Men'
+              }]
+            }]
+          }],
+          selectedOptions: []
+        };
+      }
+    }, true);
+    expect(vm.$el).to.be.exist;
+    vm.$refs.cascader.inputValue = 'z';
+    vm.$el.click();
+
+    setTimeout(_ => {
+      expect(document.body.querySelector('.el-cascader-menus')).to.be.exist;
+
+      const menu = vm.$refs.cascader.menu;
+      const menuElm = menu.$el;
+      const item1 = menuElm.querySelector('.el-cascader-menu__item');
+
+      expect(menuElm.children.length).to.be.equal(1);
+      expect(menuElm.children[0].children.length).to.be.equal(1);
+      done();
+
+      item1.click();
+
+      setTimeout(_ => {
+        expect(document.body.querySelector('.el-cascader-menus').style.display).to.be.equal('none');
+        expect(vm.selectedOptions[0]).to.be.equal('zhejiang');
+        expect(vm.selectedOptions[1]).to.be.equal('hangzhou');
+        expect(vm.selectedOptions[2]).to.be.equal('xihu');
+        done();
+      }, 500);
+    }, 300);
+  });
+});