Эх сурвалжийг харах

Cascader: refactor and add multiple mode. (#15611)

Simona 6 жил өмнө
parent
commit
bdaae8108e
77 өөрчлөгдсөн 5625 нэмэгдсэн , 2219 устгасан
  1. 2 1
      components.json
  2. 666 110
      examples/docs/en-US/cascader.md
  3. 678 128
      examples/docs/es/cascader.md
  4. 681 125
      examples/docs/fr-FR/cascader.md
  5. 637 102
      examples/docs/zh-CN/cascader.md
  6. 8 0
      packages/cascader-panel/index.js
  7. 138 0
      packages/cascader-panel/src/cascader-menu.vue
  8. 246 0
      packages/cascader-panel/src/cascader-node.vue
  9. 354 0
      packages/cascader-panel/src/cascader-panel.vue
  10. 166 0
      packages/cascader-panel/src/node.js
  11. 63 0
      packages/cascader-panel/src/store.js
  12. 1 1
      packages/cascader/index.js
  13. 639 0
      packages/cascader/src/cascader.vue
  14. 0 452
      packages/cascader/src/main.vue
  15. 0 375
      packages/cascader/src/menu.vue
  16. 124 0
      packages/theme-chalk/src/cascader-panel.scss
  17. 116 119
      packages/theme-chalk/src/cascader.scss
  18. 8 13
      packages/theme-chalk/src/common/var.scss
  19. 2 0
      packages/theme-chalk/src/index.scss
  20. 0 0
      packages/theme-chalk/src/infiniteScroll.scss
  21. 4 1
      src/index.js
  22. 2 1
      src/locale/lang/af-ZA.js
  23. 2 1
      src/locale/lang/ar.js
  24. 2 1
      src/locale/lang/bg.js
  25. 2 1
      src/locale/lang/ca.js
  26. 2 1
      src/locale/lang/cs-CZ.js
  27. 2 1
      src/locale/lang/da.js
  28. 2 1
      src/locale/lang/de.js
  29. 2 1
      src/locale/lang/ee.js
  30. 2 1
      src/locale/lang/el.js
  31. 2 1
      src/locale/lang/en.js
  32. 2 1
      src/locale/lang/es.js
  33. 2 1
      src/locale/lang/eu.js
  34. 2 1
      src/locale/lang/fa.js
  35. 2 1
      src/locale/lang/fi.js
  36. 2 1
      src/locale/lang/fr.js
  37. 2 1
      src/locale/lang/he.js
  38. 2 1
      src/locale/lang/hr.js
  39. 2 1
      src/locale/lang/hu.js
  40. 2 1
      src/locale/lang/hy-AM.js
  41. 2 1
      src/locale/lang/id.js
  42. 2 1
      src/locale/lang/it.js
  43. 2 1
      src/locale/lang/ja.js
  44. 2 1
      src/locale/lang/kg.js
  45. 2 1
      src/locale/lang/km.js
  46. 2 1
      src/locale/lang/ko.js
  47. 2 1
      src/locale/lang/ku.js
  48. 2 1
      src/locale/lang/kz.js
  49. 2 1
      src/locale/lang/lt.js
  50. 2 1
      src/locale/lang/lv.js
  51. 2 1
      src/locale/lang/mn.js
  52. 2 1
      src/locale/lang/nb-NO.js
  53. 2 1
      src/locale/lang/nl.js
  54. 2 1
      src/locale/lang/pl.js
  55. 2 1
      src/locale/lang/pt-br.js
  56. 2 1
      src/locale/lang/pt.js
  57. 2 1
      src/locale/lang/ro.js
  58. 2 1
      src/locale/lang/ru-RU.js
  59. 2 1
      src/locale/lang/sk.js
  60. 2 1
      src/locale/lang/sl.js
  61. 2 1
      src/locale/lang/sr.js
  62. 2 1
      src/locale/lang/sv-SE.js
  63. 2 1
      src/locale/lang/ta.js
  64. 2 1
      src/locale/lang/th.js
  65. 2 1
      src/locale/lang/tk.js
  66. 2 1
      src/locale/lang/tr-TR.js
  67. 2 1
      src/locale/lang/ua.js
  68. 2 1
      src/locale/lang/ug-CN.js
  69. 2 1
      src/locale/lang/vi.js
  70. 2 1
      src/locale/lang/zh-CN.js
  71. 2 1
      src/locale/lang/zh-TW.js
  72. 2 1
      src/utils/aria-utils.js
  73. 73 0
      src/utils/util.js
  74. 536 0
      test/unit/specs/cascader-panel.spec.js
  75. 276 720
      test/unit/specs/cascader.spec.js
  76. 72 0
      types/cascader-panel.d.ts
  77. 33 21
      types/cascader.d.ts

+ 2 - 1
components.json

@@ -76,5 +76,6 @@
   "calendar": "./packages/calendar/index.js",
   "backtop": "./packages/backtop/index.js",
   "infiniteScroll": "./packages/infiniteScroll/index.js",
-  "page-header": "./packages/page-header/index.js"
+  "page-header": "./packages/page-header/index.js",
+  "cascader-panel": "./packages/cascader-panel/index.js"
 }

+ 666 - 110
examples/docs/en-US/cascader.md

@@ -6,30 +6,29 @@ If the options have a clear hierarchical structure, Cascader can be used to view
 
 There are two ways to expand child option items.
 
-:::demo Assigning the `options` attribute to an array of options renders a Cascader. The `expand-trigger` attribute defines how child options are expanded. This example also demonstrates the `change` event, whose parameter is the value of Cascader, an array made up of the values of each selected level.
+:::demo Assigning the `options` attribute to an array of options renders a Cascader. The `props.expandTrigger` attribute defines how child options are expanded.
 ```html
 <div class="block">
   <span class="demonstration">Child options expand when clicked (default)</span>
   <el-cascader
+    v-model="value"
     :options="options"
-    v-model="selectedOptions"
-    @change="handleChange">
-  </el-cascader>
+    @change="handleChange"></el-cascader>
 </div>
 <div class="block">
   <span class="demonstration">Child options expand when hovered</span>
   <el-cascader
-    expand-trigger="hover"
+    v-model="value"
     :options="options"
-    v-model="selectedOptions2"
-    @change="handleChange">
-  </el-cascader>
+    :props="{ expandTrigger: 'hover' }"
+    @change="handleChange"></el-cascader>
 </div>
 
 <script>
   export default {
     data() {
       return {
+        value: [],
         options: [{
           value: 'guide',
           label: 'Guide',
@@ -224,9 +223,7 @@ There are two ways to expand child option items.
             value: 'docs',
             label: 'Design Documentation'
           }]
-        }],
-        selectedOptions: [],
-        selectedOptions2: []
+        }]
       };
     },
     methods: {
@@ -243,16 +240,15 @@ There are two ways to expand child option items.
 
 Disable an option by setting a `disabled` field in the option object.
 
-:::demo In this example, the first item in `options` array has a `disabled: true` field, so it is disabled. By default, Cascader checks the `disabled` field in each option object; if you are using another field name to indicate whether an option is disabled, you can assign it in the `props` attribute (see the API table below for details). And of course, field name `value`, `label` and `children` can also be customized in the same way.
+:::demo In this example, the first item in `options` array has a `disabled: true` field, so it is disabled. By default, Cascader checks the `disabled` field in each option object; if you are using another field name to indicate whether an option is disabled, you can assign it in the `props.disabled` attribute (see the API table below for details). And of course, field name `value`, `label` and `children` can also be customized in the same way.
 ```html
-<el-cascader
-  :options="optionsWithDisabled"
-></el-cascader>
+<el-cascader :options="options"></el-cascader>
+
 <script>
   export default {
     data() {
       return {
-        optionsWithDisabled: [{
+        options: [{
           value: 'guide',
           label: 'Guide',
           disabled: true,
@@ -455,16 +451,14 @@ Disable an option by setting a `disabled` field in the option object.
 ```
 :::
 
-### Display only the last level
+### Clearable
 
-The input can display only the last level instead of all levels.
+Set `clearable` attribute for `el-cascader` and a clear icon will appear when selected and hovered
 
-:::demo The `show-all-levels` attribute defines if all levels are displayed. If it is `false`, only the last level is displayed.
+:::demo
 ```html
-<el-cascader
-    :options="options"
-    :show-all-levels="false"
-></el-cascader>
+<el-cascader :options="options" clearable></el-cascader>
+
 <script>
   export default {
     data() {
@@ -664,21 +658,20 @@ The input can display only the last level instead of all levels.
             label: 'Design Documentation'
           }]
         }]
-      };
+      }
     }
-  };
+  }
 </script>
 ```
 :::
 
-### With default value
+### Display only the last level
+
+The input can display only the last level instead of all levels.
 
-:::demo The default value can be defined with an array.
+:::demo The `show-all-levels` attribute defines if all levels are displayed. If it is `false`, only the last level is displayed.
 ```html
-<el-cascader
-  :options="options"
-  v-model="selectedOptions"
-></el-cascader>
+<el-cascader :options="options" :show-all-levels="false"></el-cascader>
 <script>
   export default {
     data() {
@@ -877,8 +870,7 @@ The input can display only the last level instead of all levels.
             value: 'docs',
             label: 'Design Documentation'
           }]
-        }],
-        selectedOptions: ['component', 'data', 'tag']
+        }]
       };
     }
   };
@@ -886,16 +878,131 @@ The input can display only the last level instead of all levels.
 ```
 :::
 
-### Change on select
+### Multiple Selection
 
-Parent options can also be selected.
+Set `props.multiple = true` to use multiple selection.
 
-:::demo By default only the options in the last level can be selected. By assigning `change-on-select` to `true`, options in parent levels can also be selected.
+:::demo When using multiple selection, all selected tags will display by default, You can set `collapse-tags = true` to fold selected tags.
 ```html
-<el-cascader
-  :options="options"
-  change-on-select
-></el-cascader>
+<div class="block">
+  <span class="demonstration">Display all tags (default)</span>
+  <el-cascader
+    :options="options"
+    :props="props"
+    clearable></el-cascader>
+</div>
+<div class="block">
+  <span class="demonstration">Collapse tags</span>
+  <el-cascader
+    :options="options"
+    :props="props"
+    collapse-tags
+    clearable></el-cascader>
+</div>
+
+<script>
+  export default {
+    data() {
+      return {
+        props: { multiple: true },
+        options: [{
+          value: 1,
+          label: 'Asia',
+          children: [{
+            value: 2,
+            label: 'China',
+            children: [
+              { value: 3, label: 'Beijing' },
+              { value: 4, label: 'Shanghai' },
+              { value: 5, label: 'Hangzhou' }
+            ]
+          }, {
+            value: 6,
+            label: 'Japan',
+            children: [
+              { value: 7, label: 'Tokyo' },
+              { value: 8, label: 'Osaka' },
+              { value: 9, label: 'Kyoto' }
+            ]
+          }, {
+            value: 10,
+            label: 'Korea',
+            children: [
+              { value: 11, label: 'Seoul' },
+              { value: 12, label: 'Busan' },
+              { value: 13, label: 'Taegu' }
+            ]
+          }]
+        }, {
+          value: 14,
+          label: 'Europe',
+          children: [{
+            value: 15,
+            label: 'France',
+            children: [
+              { value: 16, label: 'Paris' },
+              { value: 17, label: 'Marseille' },
+              { value: 18, label: 'Lyon' }
+            ]
+          }, {
+            value: 19,
+            label: 'UK',
+            children: [
+              { value: 20, label: 'London' },
+              { value: 21, label: 'Birmingham' },
+              { value: 22, label: 'Manchester' }
+            ]
+          }]
+        }, {
+          value: 23,
+          label: 'North America',
+          children: [{
+            value: 24,
+            label: 'US',
+            children: [
+              { value: 25, label: 'New York' },
+              { value: 26, label: 'Los Angeles' },
+              { value: 27, label: 'Washington' }
+            ]
+          }, {
+            value: 28,
+            label: 'Canada',
+            children: [
+              { value: 29, label: 'Toronto' },
+              { value: 30, label: 'Montreal' },
+              { value: 31, label: 'Ottawa' }
+            ]
+          }]
+        }]
+      };
+    }
+  };
+</script>
+```
+:::
+
+
+### Select any level of options
+
+In single selection, only the leaf nodes can be checked, and in multiple selection, check parent nodes will lead to leaf nodes be checked eventually. When enable this feature, it can make parent and child nodes unlinked and you can select any level of options.
+
+:::demo Set `props.checkStrictly = true` to make checked state of a node not affects its parent nodes and child nodes, and then you can select any level of options.
+```html
+<div class="block">
+  <span class="demonstration">Select any level of options (Single selection)</span>
+  <el-cascader
+    :options="options"
+    :props="{ checkStrictly: true }"
+    clearable></el-cascader>
+</div>
+<div class="block">
+  <span class="demonstration">Select any level of options (Multiple selection)</span>
+  <el-cascader
+    :options="options"
+    :props="{ multiple: true, checkStrictly: true }"
+    clearable></el-cascader>
+</div>
+
 <script>
   export default {
     data() {
@@ -1102,51 +1209,37 @@ Parent options can also be selected.
 ```
 :::
 
-### Dynamically load child options
+### Dynamic loading
 
-Load child options when their parent option is clicked or hovered over.
+Dynamic load its child nodes when checked a node.
 
-:::demo In this example, the options array does not have data of cities when initialized. With the `active-item-change` event, you can load the cities of a specific state dynamically. Besides, this example also demonstrates how `props` is used.
+:::demo Set `lazy = true` to use dynamic loading, and you have to specify how to load the data source by `lazyload`. There are two parameters of `lazyload`,the first parameter `node` is the node currently clicked, and the `resolve` is a callback that indicate loading is finished which must invoke. To display the status of node more accurately, you can add a `leaf` field (can be modified by `props.leaf`) to indicate whether it is a leaf node. Otherwise, it will be inferred by if has any child nodes.
 ```html
-<el-cascader
-  :options="options"
-  @active-item-change="handleItemChange"
-  :props="props"
-></el-cascader>
+<el-cascader :props="props"></el-cascader>
 
 <script>
+  let id = 0;
+
   export default {
     data() {
       return {
-        options: [{
-          label: 'California',
-          cities: []
-        }, {
-          label: 'Florida',
-          cities: []
-        }],
         props: {
-          value: 'label',
-          children: 'cities'
+          lazy: true,
+          lazyLoad (node, resolve) {
+            const { level } = node;
+            setTimeout(() => {
+              const nodes = Array.from({ length: level + 1 })
+                .map(item => ({
+                  value: ++id,
+                  label: `Option - ${id}`,
+                  leaf: level >= 2
+                }));
+              // Invoke `resolve` callback to return the child nodes data and indicate the loading is finished.
+              resolve(nodes);
+            }, 1000);
+          }
         }
       };
-    },
-
-    methods: {
-      handleItemChange(val) {
-        console.log('active item:', val);
-        setTimeout(_ => {
-          if (val.indexOf('California') > -1 && !this.options[0].cities.length) {
-            this.options[0].cities = [{
-              label: 'Los Angeles'
-            }];
-          } else if (val.indexOf('Florida') > -1 && !this.options[1].cities.length) {
-            this.options[1].cities = [{
-              label: 'Orlando'
-            }];
-          }
-        }, 300);
-      }
     }
   };
 </script>
@@ -1157,24 +1250,22 @@ Load child options when their parent option is clicked or hovered over.
 
 Search and select options with a keyword.
 
-:::demo Adding `filterable` to `el-cascader` enables filtering
+:::demo Adding `filterable` to `el-cascader` enables filtering. Cascader will match nodes whose label or parent's label (according to `show-all-levels`) includes input keyword. Of course, you can customize search logic by `filter-method` which accepts a function, the first parameter is `node`, the second is `keyword`, and need return a boolean value indicating whether it hits.
 ```html
 <div class="block">
-  <span class="demonstration">Only options of the last level can be selected</span>
+  <span class="demonstration">Filterable (Single selection)</span>
   <el-cascader
-    placeholder="Try searching: Guide"
+    placeholder="Try searchingL Guide"
     :options="options"
-    filterable
-  ></el-cascader>
+    filterable></el-cascader>
 </div>
 <div class="block">
-  <span class="demonstration">Options of all levels can be selected</span>
+  <span class="demonstration">Filterable (Multiple selection)</span>
   <el-cascader
-    placeholder="Try searching: Guide"
+    placeholder="Try searchingL Guide"
     :options="options"
-    filterable
-    change-on-select
-  ></el-cascader>
+    :props="{ multiple: true }"
+    filterable></el-cascader>
 </div>
 
 <script>
@@ -1383,38 +1474,503 @@ Search and select options with a keyword.
 ```
 :::
 
-### Attributes
-| Attribute | Description         | Type    | Options       | Default|
-|---------- |-------------------- |---------|-------------  |-------- |
-| value / v-model | binding value  | array | — | — |
-| options   | data of the options | array | — | — |
-| props | configuration options, see the following table | object | — | — |
-| separator | option separator | string | — | / |
-| popper-class | custom class name for Cascader's dropdown | string | — | — |
-| placeholder | input placeholder | string | — | Select |
-| disabled  | whether Cascader is disabled | boolean |  — | false |
-| clearable  | whether selected value can be cleared | boolean | — | false |
-| expand-trigger  | trigger mode of expanding current item | string | click / hover | click |
+### Custom option content
+
+You can customize the content of cascader node.
+
+:::demo You can customize the content of cascader node by `scoped slot`. You'll have access to `node` and `data` in the scope,  standing for the Node object and node data of the current node respectively。
+```html
+<el-cascader :options="options">
+  <template slot-scope="{ node, data }">
+    <span>{{ data.label }}</span>
+    <span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
+  </template>
+</el-cascader>
+
+<script>
+  export default {
+    data() {
+      return {
+        options: [{
+          value: 'guide',
+          label: 'Guide',
+          children: [{
+            value: 'disciplines',
+            label: 'Disciplines',
+            children: [{
+              value: 'consistency',
+              label: 'Consistency'
+            }, {
+              value: 'feedback',
+              label: 'Feedback'
+            }, {
+              value: 'efficiency',
+              label: 'Efficiency'
+            }, {
+              value: 'controllability',
+              label: 'Controllability'
+            }]
+          }, {
+            value: 'navigation',
+            label: 'Navigation',
+            children: [{
+              value: 'side nav',
+              label: 'Side Navigation'
+            }, {
+              value: 'top nav',
+              label: 'Top Navigation'
+            }]
+          }]
+        }, {
+          value: 'component',
+          label: 'Component',
+          children: [{
+            value: 'basic',
+            label: 'Basic',
+            children: [{
+              value: 'layout',
+              label: 'Layout'
+            }, {
+              value: 'color',
+              label: 'Color'
+            }, {
+              value: 'typography',
+              label: 'Typography'
+            }, {
+              value: 'icon',
+              label: 'Icon'
+            }, {
+              value: 'button',
+              label: 'Button'
+            }]
+          }, {
+            value: 'form',
+            label: 'Form',
+            children: [{
+              value: 'radio',
+              label: 'Radio'
+            }, {
+              value: 'checkbox',
+              label: 'Checkbox'
+            }, {
+              value: 'input',
+              label: 'Input'
+            }, {
+              value: 'input-number',
+              label: 'InputNumber'
+            }, {
+              value: 'select',
+              label: 'Select'
+            }, {
+              value: 'cascader',
+              label: 'Cascader'
+            }, {
+              value: 'switch',
+              label: 'Switch'
+            }, {
+              value: 'slider',
+              label: 'Slider'
+            }, {
+              value: 'time-picker',
+              label: 'TimePicker'
+            }, {
+              value: 'date-picker',
+              label: 'DatePicker'
+            }, {
+              value: 'datetime-picker',
+              label: 'DateTimePicker'
+            }, {
+              value: 'upload',
+              label: 'Upload'
+            }, {
+              value: 'rate',
+              label: 'Rate'
+            }, {
+              value: 'form',
+              label: 'Form'
+            }]
+          }, {
+            value: 'data',
+            label: 'Data',
+            children: [{
+              value: 'table',
+              label: 'Table'
+            }, {
+              value: 'tag',
+              label: 'Tag'
+            }, {
+              value: 'progress',
+              label: 'Progress'
+            }, {
+              value: 'tree',
+              label: 'Tree'
+            }, {
+              value: 'pagination',
+              label: 'Pagination'
+            }, {
+              value: 'badge',
+              label: 'Badge'
+            }]
+          }, {
+            value: 'notice',
+            label: 'Notice',
+            children: [{
+              value: 'alert',
+              label: 'Alert'
+            }, {
+              value: 'loading',
+              label: 'Loading'
+            }, {
+              value: 'message',
+              label: 'Message'
+            }, {
+              value: 'message-box',
+              label: 'MessageBox'
+            }, {
+              value: 'notification',
+              label: 'Notification'
+            }]
+          }, {
+            value: 'navigation',
+            label: 'Navigation',
+            children: [{
+              value: 'menu',
+              label: 'NavMenu'
+            }, {
+              value: 'tabs',
+              label: 'Tabs'
+            }, {
+              value: 'breadcrumb',
+              label: 'Breadcrumb'
+            }, {
+              value: 'dropdown',
+              label: 'Dropdown'
+            }, {
+              value: 'steps',
+              label: 'Steps'
+            }]
+          }, {
+            value: 'others',
+            label: 'Others',
+            children: [{
+              value: 'dialog',
+              label: 'Dialog'
+            }, {
+              value: 'tooltip',
+              label: 'Tooltip'
+            }, {
+              value: 'popover',
+              label: 'Popover'
+            }, {
+              value: 'card',
+              label: 'Card'
+            }, {
+              value: 'carousel',
+              label: 'Carousel'
+            }, {
+              value: 'collapse',
+              label: 'Collapse'
+            }]
+          }]
+        }, {
+          value: 'resource',
+          label: 'Resource',
+          children: [{
+            value: 'axure',
+            label: 'Axure Components'
+          }, {
+            value: 'sketch',
+            label: 'Sketch Templates'
+          }, {
+            value: 'docs',
+            label: 'Design Documentation'
+          }]
+        }]
+      }
+    }
+  }
+</script>
+```
+:::
+
+### Cascader panel
+
+`CascaderPanel` is the core component of `Cascader` which has various of features such as single selection, multiple selection, dynamic loading and so on.
+
+:::demo Just like `el-cascader`, you can set alternative options by `options`, and enable other features by `props`, see the API form below for details.
+```html
+<el-cascader-panel :options="options"></el-cascader-panel>
+
+<script>
+  export default {
+    data() {
+      return {
+        options: [{
+          value: 'guide',
+          label: 'Guide',
+          children: [{
+            value: 'disciplines',
+            label: 'Disciplines',
+            children: [{
+              value: 'consistency',
+              label: 'Consistency'
+            }, {
+              value: 'feedback',
+              label: 'Feedback'
+            }, {
+              value: 'efficiency',
+              label: 'Efficiency'
+            }, {
+              value: 'controllability',
+              label: 'Controllability'
+            }]
+          }, {
+            value: 'navigation',
+            label: 'Navigation',
+            children: [{
+              value: 'side nav',
+              label: 'Side Navigation'
+            }, {
+              value: 'top nav',
+              label: 'Top Navigation'
+            }]
+          }]
+        }, {
+          value: 'component',
+          label: 'Component',
+          children: [{
+            value: 'basic',
+            label: 'Basic',
+            children: [{
+              value: 'layout',
+              label: 'Layout'
+            }, {
+              value: 'color',
+              label: 'Color'
+            }, {
+              value: 'typography',
+              label: 'Typography'
+            }, {
+              value: 'icon',
+              label: 'Icon'
+            }, {
+              value: 'button',
+              label: 'Button'
+            }]
+          }, {
+            value: 'form',
+            label: 'Form',
+            children: [{
+              value: 'radio',
+              label: 'Radio'
+            }, {
+              value: 'checkbox',
+              label: 'Checkbox'
+            }, {
+              value: 'input',
+              label: 'Input'
+            }, {
+              value: 'input-number',
+              label: 'InputNumber'
+            }, {
+              value: 'select',
+              label: 'Select'
+            }, {
+              value: 'cascader',
+              label: 'Cascader'
+            }, {
+              value: 'switch',
+              label: 'Switch'
+            }, {
+              value: 'slider',
+              label: 'Slider'
+            }, {
+              value: 'time-picker',
+              label: 'TimePicker'
+            }, {
+              value: 'date-picker',
+              label: 'DatePicker'
+            }, {
+              value: 'datetime-picker',
+              label: 'DateTimePicker'
+            }, {
+              value: 'upload',
+              label: 'Upload'
+            }, {
+              value: 'rate',
+              label: 'Rate'
+            }, {
+              value: 'form',
+              label: 'Form'
+            }]
+          }, {
+            value: 'data',
+            label: 'Data',
+            children: [{
+              value: 'table',
+              label: 'Table'
+            }, {
+              value: 'tag',
+              label: 'Tag'
+            }, {
+              value: 'progress',
+              label: 'Progress'
+            }, {
+              value: 'tree',
+              label: 'Tree'
+            }, {
+              value: 'pagination',
+              label: 'Pagination'
+            }, {
+              value: 'badge',
+              label: 'Badge'
+            }]
+          }, {
+            value: 'notice',
+            label: 'Notice',
+            children: [{
+              value: 'alert',
+              label: 'Alert'
+            }, {
+              value: 'loading',
+              label: 'Loading'
+            }, {
+              value: 'message',
+              label: 'Message'
+            }, {
+              value: 'message-box',
+              label: 'MessageBox'
+            }, {
+              value: 'notification',
+              label: 'Notification'
+            }]
+          }, {
+            value: 'navigation',
+            label: 'Navigation',
+            children: [{
+              value: 'menu',
+              label: 'NavMenu'
+            }, {
+              value: 'tabs',
+              label: 'Tabs'
+            }, {
+              value: 'breadcrumb',
+              label: 'Breadcrumb'
+            }, {
+              value: 'dropdown',
+              label: 'Dropdown'
+            }, {
+              value: 'steps',
+              label: 'Steps'
+            }]
+          }, {
+            value: 'others',
+            label: 'Others',
+            children: [{
+              value: 'dialog',
+              label: 'Dialog'
+            }, {
+              value: 'tooltip',
+              label: 'Tooltip'
+            }, {
+              value: 'popover',
+              label: 'Popover'
+            }, {
+              value: 'card',
+              label: 'Card'
+            }, {
+              value: 'carousel',
+              label: 'Carousel'
+            }, {
+              value: 'collapse',
+              label: 'Collapse'
+            }]
+          }]
+        }, {
+          value: 'resource',
+          label: 'Resource',
+          children: [{
+            value: 'axure',
+            label: 'Axure Components'
+          }, {
+            value: 'sketch',
+            label: 'Sketch Templates'
+          }, {
+            value: 'docs',
+            label: 'Design Documentation'
+          }]
+        }]
+      };
+    }
+  };
+</script>
+```
+:::
+
+### Cascader Attributes
+| Attribute | Description | Type  | Accepted Values | Default |
+|---------- |-------- |---------- |-------------  |-------- |
+| value / v-model | binding value | - | — | — |
+| options | data of the options,the key of `value` and `label` can be customize by `Props`.| array | — | — |
+| props | configuration options, see the following table. | object | — | — |
+| size | size of input | string | medium / small / mini | — |
+| placeholder | placeholder of input | string | — | Select |
+| disabled | whether Cascader is disabled | boolean | — | false |
+| clearable | whether selected value can be cleared | boolean | — | false |
 | show-all-levels | whether to display all levels of the selected value in the input | boolean | — | true |
-| filterable  | whether the options can be searched | boolean | — | — |
+| collapse-tags | whether to collapse tags in multiple selection mode | boolean | - | false |
+| separator | option label separator | string | — | ' / ' |
+| filterable | whether the options can be searched | boolean | — | — |
+| filter-method | customize search logic, the first parameter is `node`, the second is `keyword`, and need return a boolean value indicating whether it hits. | function(node, keyword) | - | - |
 | debounce | debounce delay when typing filter keyword, in milliseconds | number | — | 300 |
-| change-on-select | whether selecting an option of any level is permitted | boolean | — | false |
-| size  | size of Input | string | medium / small / mini | — |
 | before-filter | hook function before filtering with the value to be filtered as its parameter. If `false` is returned or a `Promise` is returned and then is rejected, filtering will be aborted | function(value) | — | — |
+| popper-class | custom class name for Cascader's dropdown   | string | —  | — |
 
-### props
-| Attribute | Description | Type | Accepted Values | Default |
-| --------- | ----------------- | ------ | ------ | ------ |
-| label     | specify which key of option object is used as the option's label | string | — | — |
-| value     | specify which key of option object is used as the option's value | string | — | — |
-| children  | specify which key of option object is used as the option's child options | string | — | — |
-| disabled  | specify which key of option object indicates if the option is disabled | string | — | — |
-
-### Events
+### Cascader Events
 | Event Name | Description | Parameters |
 |---------- |-------- |---------- |
-| change  | triggers when the binding value changes | value |
-| active-item-change | triggers when active option of its parent changes, only works when `change-on-select` is `false` | an array of active options |
+| change | triggers when the binding value changes | value |
+| expand-change | triggers when expand option changes | an array of the expanding node's parent nodes |
 | blur | triggers when Cascader blurs | (event: Event) |
 | focus | triggers when Cascader focuses | (event: Event) |
 | visible-change | triggers when the dropdown appears/disappears | true when it appears, and false otherwise |
+| remove-tag | triggers when remove tag in multiple selection mode | the value of the tag which is removed |
+
+### Cascader Slots
+| Slot Name | Description |
+|---------|-------------|
+| - | the custom content of cascader node, the parameter is { node, data }, which are current Node object and node data respectively. |
+| empty  | content when there is no matched options. |
+
+### CascaderPanel Attributes
+| Attribute | Description | Type  | Accepted Values | Default |
+|---------- |-------- |---------- |-------------  |-------- |
+| value / v-model | binding value | - | — | — |
+| options | data of the options,the key of `value` and `label` can be customize by `Props`.| array | — | — |
+| props | configuration options, see the following table. | object | — | — |
+
+### CascaderPanel Events
+| Event Name | Description | Parameters |
+|---------- |-------- |---------- |
+| change | triggers when the binding value changes | value |
+| expand-change | triggers when expand option changes | an array of the expanding node's parent nodes |
+
+### CascaderPanel Slots
+| Slot Name | Description |
+|---------|-------------|
+| - | the custom content of cascader node, the parameter is { node, data }, which are current Node object and node data respectively. |
+
+### Props
+| Attribute | Description | Type  | Accepted Values | Default |
+| -------- | ----------------- | ------ | ------ | ------ |
+| expandTrigger | trigger mode of expanding options | string | click / hover | 'click' |
+| multiple | whether multiple selection is enabled | boolean | - | false |
+| checkStrictly | whether checked state of a node not affects its parent and child nodes | boolean | - | false |
+| emitPath | when checked nodes change, whether to emit an array of node's path, if false, only emit the value of node. | boolean | - | true |
+| lazy | whether to dynamic load child nodes, use with `lazyload` attribute | boolean | - | false |
+| lazyLoad | method for loading child nodes data, only works when `lazy` is true | function(node, resolve) | - | - |
+| value    | specify which key of node object is used as the node's value | string | — | 'value' |
+| label    | specify which key of node object is used as the node's label | string | — | 'label' |
+| children | specify which key of node object is used as the node's children | string | — | 'children' |
+| disabled | specify which key of node object is used as the node's disabled | string | — | 'disabled' |
+| leaf     | specify which key of node object is used as the node's leaf field | string | — | 'leaf' |

+ 678 - 128
examples/docs/es/cascader.md

@@ -1,36 +1,34 @@
 ## Cascader
 
-Si las opciones tienen una estructura jerárquica clara, Cascader puede utilizarse para visualizarlas y seleccionarlas.
+If the options have a clear hierarchical structure, Cascader can be used to view and select them.
 
-### Uso básico
+### Basic usage
 
-Existen dos maneras de expandir los elementos hijo de la opción.
-
-:::demo Al asignar al atributo `options` un array de opciones, se genera un Cascader. El atributo `expand-trigger` define cómo se expanden las opciones hijo. Este ejemplo también muestra el evento `change` , cuyo parámetro es el valor de Cascader, un array formado por los valores de cada nivel seleccionado.
+There are two ways to expand child option items.
 
+:::demo Assigning the `options` attribute to an array of options renders a Cascader. The `props.expandTrigger` attribute defines how child options are expanded.
 ```html
 <div class="block">
   <span class="demonstration">Child options expand when clicked (default)</span>
   <el-cascader
+    v-model="value"
     :options="options"
-    v-model="selectedOptions"
-    @change="handleChange">
-  </el-cascader>
+    @change="handleChange"></el-cascader>
 </div>
 <div class="block">
   <span class="demonstration">Child options expand when hovered</span>
   <el-cascader
-    expand-trigger="hover"
+    v-model="value"
     :options="options"
-    v-model="selectedOptions2"
-    @change="handleChange">
-  </el-cascader>
+    :props="{ expandTrigger: 'hover' }"
+    @change="handleChange"></el-cascader>
 </div>
 
 <script>
   export default {
     data() {
       return {
+        value: [],
         options: [{
           value: 'guide',
           label: 'Guide',
@@ -225,9 +223,7 @@ Existen dos maneras de expandir los elementos hijo de la opción.
             value: 'docs',
             label: 'Design Documentation'
           }]
-        }],
-        selectedOptions: [],
-        selectedOptions2: []
+        }]
       };
     },
     methods: {
@@ -240,21 +236,19 @@ Existen dos maneras de expandir los elementos hijo de la opción.
 ```
 :::
 
-### Opcion Disabled
+### Disabled option
 
-Para desactivar una opción configure un campo `disabled` en el objeto de opción.
-
-:::demo En este ejemplo, el primer elemento del array `options` tiene un campo  `disabled: true` , por lo que está desactivado. Por defecto, Cascader verifica el campo `disabled` en cada objeto de opción; si está utilizando otro nombre de campo para indicar si una opción está deshabilitada, puede asignarla en el atributo `props` (consulte la tabla API a continuación para obtener más detalles). Y por supuesto, el valor de `value`, `label` y `children` también se pueden personalizar de la misma manera.
+Disable an option by setting a `disabled` field in the option object.
 
+:::demo In this example, the first item in `options` array has a `disabled: true` field, so it is disabled. By default, Cascader checks the `disabled` field in each option object; if you are using another field name to indicate whether an option is disabled, you can assign it in the `props.disabled` attribute (see the API table below for details). And of course, field name `value`, `label` and `children` can also be customized in the same way.
 ```html
-<el-cascader
-  :options="optionsWithDisabled"
-></el-cascader>
+<el-cascader :options="options"></el-cascader>
+
 <script>
   export default {
     data() {
       return {
-        optionsWithDisabled: [{
+        options: [{
           value: 'guide',
           label: 'Guide',
           disabled: true,
@@ -457,17 +451,14 @@ Para desactivar una opción configure un campo `disabled` en el objeto de opció
 ```
 :::
 
-### Mostrar sólo el último nivel
-
-Puede mostrar sólo el último nivel en lugar de todos los niveles.
+### Clearable
 
-:::demo El atributo `show-all-levels` define si se visualizan todos los niveles. Si es `false`, sólo se muestra el último nivel.
+Set `clearable` attribute for `el-cascader` and a clear icon will appear when selected and hovered
 
+:::demo
 ```html
-<el-cascader
-    :options="options"
-    :show-all-levels="false"
-></el-cascader>
+<el-cascader :options="options" clearable></el-cascader>
+
 <script>
   export default {
     data() {
@@ -667,21 +658,20 @@ Puede mostrar sólo el último nivel en lugar de todos los niveles.
             label: 'Design Documentation'
           }]
         }]
-      };
+      }
     }
-  };
+  }
 </script>
 ```
 :::
 
-### Con valor por defecto
+### Display only the last level
+
+The input can display only the last level instead of all levels.
 
-:::demo El valor por defecto se puede definir con un array.
+:::demo The `show-all-levels` attribute defines if all levels are displayed. If it is `false`, only the last level is displayed.
 ```html
-<el-cascader
-  :options="options"
-  v-model="selectedOptions"
-></el-cascader>
+<el-cascader :options="options" :show-all-levels="false"></el-cascader>
 <script>
   export default {
     data() {
@@ -880,8 +870,110 @@ Puede mostrar sólo el último nivel en lugar de todos los niveles.
             value: 'docs',
             label: 'Design Documentation'
           }]
-        }],
-        selectedOptions: ['component', 'data', 'tag']
+        }]
+      };
+    }
+  };
+</script>
+```
+:::
+
+### Multiple Selection
+
+Set `props.multiple = true` to use multiple selection.
+
+:::demo When using multiple selection, all selected tags will display by default, You can set `collapse-tags = true` to fold selected tags.
+```html
+<div class="block">
+  <span class="demonstration">Display all tags (default)</span>
+  <el-cascader
+    :options="options"
+    :props="props"
+    clearable></el-cascader>
+</div>
+<div class="block">
+  <span class="demonstration">Collapse tags</span>
+  <el-cascader
+    :options="options"
+    :props="props"
+    collapse-tags
+    clearable></el-cascader>
+</div>
+
+<script>
+  export default {
+    data() {
+      return {
+        props: { multiple: true },
+        options: [{
+          value: 1,
+          label: 'Asia',
+          children: [{
+            value: 2,
+            label: 'China',
+            children: [
+              { value: 3, label: 'Beijing' },
+              { value: 4, label: 'Shanghai' },
+              { value: 5, label: 'Hangzhou' }
+            ]
+          }, {
+            value: 6,
+            label: 'Japan',
+            children: [
+              { value: 7, label: 'Tokyo' },
+              { value: 8, label: 'Osaka' },
+              { value: 9, label: 'Kyoto' }
+            ]
+          }, {
+            value: 10,
+            label: 'Korea',
+            children: [
+              { value: 11, label: 'Seoul' },
+              { value: 12, label: 'Busan' },
+              { value: 13, label: 'Taegu' }
+            ]
+          }]
+        }, {
+          value: 14,
+          label: 'Europe',
+          children: [{
+            value: 15,
+            label: 'France',
+            children: [
+              { value: 16, label: 'Paris' },
+              { value: 17, label: 'Marseille' },
+              { value: 18, label: 'Lyon' }
+            ]
+          }, {
+            value: 19,
+            label: 'UK',
+            children: [
+              { value: 20, label: 'London' },
+              { value: 21, label: 'Birmingham' },
+              { value: 22, label: 'Manchester' }
+            ]
+          }]
+        }, {
+          value: 23,
+          label: 'North America',
+          children: [{
+            value: 24,
+            label: 'US',
+            children: [
+              { value: 25, label: 'New York' },
+              { value: 26, label: 'Los Angeles' },
+              { value: 27, label: 'Washington' }
+            ]
+          }, {
+            value: 28,
+            label: 'Canada',
+            children: [
+              { value: 29, label: 'Toronto' },
+              { value: 30, label: 'Montreal' },
+              { value: 31, label: 'Ottawa' }
+            ]
+          }]
+        }]
       };
     }
   };
@@ -889,17 +981,28 @@ Puede mostrar sólo el último nivel en lugar de todos los niveles.
 ```
 :::
 
-### Change on select
 
-También se pueden seleccionar las opciones del elemento padre.
+### Select any level of options
 
-:::demo Por defecto sólo se pueden seleccionar las opciones del último nivel. Al asignar `change-on-select` a `true`, también se pueden seleccionar opciones en los niveles superiores.
+In single selection, only the leaf nodes can be checked, and in multiple selection, check parent nodes will lead to leaf nodes be checked eventually. When enable this feature, it can make parent and child nodes unlinked and you can select any level of options.
 
+:::demo Set `props.checkStrictly = true` to make checked state of a node not affects its parent nodes and child nodes, and then you can select any level of options.
 ```html
-<el-cascader
-  :options="options"
-  change-on-select
-></el-cascader>
+<div class="block">
+  <span class="demonstration">Select any level of options (Single selection)</span>
+  <el-cascader
+    :options="options"
+    :props="{ checkStrictly: true }"
+    clearable></el-cascader>
+</div>
+<div class="block">
+  <span class="demonstration">Select any level of options (Multiple selection)</span>
+  <el-cascader
+    :options="options"
+    :props="{ multiple: true, checkStrictly: true }"
+    clearable></el-cascader>
+</div>
+
 <script>
   export default {
     data() {
@@ -1106,81 +1209,63 @@ También se pueden seleccionar las opciones del elemento padre.
 ```
 :::
 
-### Carga dinamica de elementos hijos
+### Dynamic loading
 
-Se puede hacer una carga dinamica de elementos hijos cuando se hace clic en el elemento padre o se pasa el ratón sobre el.
-
-:::demo En este ejemplo, el array de opciones no tiene datos de ciudades cuando se inicializa. Con el evento `active-item-change`, puede cargar dinámicamente las ciudades de un estado específico. Además, este ejemplo también demuestra cómo se utilizan los`props`.
+Dynamic load its child nodes when checked a node.
 
+:::demo Set `lazy = true` to use dynamic loading, and you have to specify how to load the data source by `lazyload`. There are two parameters of `lazyload`,the first parameter `node` is the node currently clicked, and the `resolve` is a callback that indicate loading is finished which must invoke. To display the status of node more accurately, you can add a `leaf` field (can be modified by `props.leaf`) to indicate whether it is a leaf node. Otherwise, it will be inferred by if has any child nodes.
 ```html
-<el-cascader
-  :options="options"
-  @active-item-change="handleItemChange"
-  :props="props"
-></el-cascader>
+<el-cascader :props="props"></el-cascader>
 
 <script>
+  let id = 0;
+
   export default {
     data() {
       return {
-        options: [{
-          label: 'California',
-          cities: []
-        }, {
-          label: 'Florida',
-          cities: []
-        }],
         props: {
-          value: 'label',
-          children: 'cities'
+          lazy: true,
+          lazyLoad (node, resolve) {
+            const { level } = node;
+            setTimeout(() => {
+              const nodes = Array.from({ length: level + 1 })
+                .map(item => ({
+                  value: ++id,
+                  label: `Option - ${id}`,
+                  leaf: level >= 2
+                }));
+              // Invoke `resolve` callback to return the child nodes data and indicate the loading is finished.
+              resolve(nodes);
+            }, 1000);
+          }
         }
       };
-    },
-
-    methods: {
-      handleItemChange(val) {
-        console.log('active item:', val);
-        setTimeout(_ => {
-          if (val.indexOf('California') > -1 && !this.options[0].cities.length) {
-            this.options[0].cities = [{
-              label: 'Los Angeles'
-            }];
-          } else if (val.indexOf('Florida') > -1 && !this.options[1].cities.length) {
-            this.options[1].cities = [{
-              label: 'Orlando'
-            }];
-          }
-        }, 300);
-      }
     }
   };
 </script>
 ```
 :::
 
-### Filtrable
-
-Buscar y seleccionar opciones con una palabra clave.
+### Filterable
 
-:::demo Añadir `filterable` a `el-cascader` permite filtrar
+Search and select options with a keyword.
 
+:::demo Adding `filterable` to `el-cascader` enables filtering. Cascader will match nodes whose label or parent's label (according to `show-all-levels`) includes input keyword. Of course, you can customize search logic by `filter-method` which accepts a function, the first parameter is `node`, the second is `keyword`, and need return a boolean value indicating whether it hits.
 ```html
 <div class="block">
-  <span class="demonstration">Only options of the last level can be selected</span>
+  <span class="demonstration">Filterable (Single selection)</span>
   <el-cascader
-    placeholder="Try searching: Guide"
+    placeholder="Try searchingL Guide"
     :options="options"
-    filterable
-  ></el-cascader>
+    filterable></el-cascader>
 </div>
 <div class="block">
-  <span class="demonstration">Options of all levels can be selected</span>
+  <span class="demonstration">Filterable (Multiple selection)</span>
   <el-cascader
-    placeholder="Try searching: Guide"
+    placeholder="Try searchingL Guide"
     :options="options"
-    filterable
-    change-on-select
-  ></el-cascader>
+    :props="{ multiple: true }"
+    filterable></el-cascader>
 </div>
 
 <script>
@@ -1389,38 +1474,503 @@ Buscar y seleccionar opciones con una palabra clave.
 ```
 :::
 
-### Atributos
-| Atributo         | Descripción                              | Tipo            | Opciones              | Por defecto |
-| ---------------- | ---------------------------------------- | --------------- | --------------------- | ----------- |
-| value / v-model   | valor seleccionado                           | array | —                     | —           |
-| options          | datos de las opciones                    | array           | —                     | —           |
-| props            | opciones de configuracion,  mire la tabla siguiente | object          | —                     | —           |
-| popper-class     | nombre de clase para el Cascader's dropdown | string          | —                     | —           |
-| placeholder      | input placeholder                        | string          | —                     | Select      |
-| disabled         | si Cascader esta disabled                | boolean         | —                     | false       |
-| clearable        | si se puede borrar el valor seleccionado | boolean         | —                     | false       |
-| expand-trigger   | evento del elemento actual para expandirse. | string          | click / hover         | click       |
-| show-all-levels  | si mostrar todos los niveles del valor seleccionado en la entrada | boolean         | —                     | true        |
-| filterable       | si se pueden buscar las opciones         | boolean         | —                     | —           |
-| debounce         | retardo al escribir la palabra clave del filtro, en milisegundos | number          | —                     | 300         |
-| change-on-select | si se permite seleccionar una opción de cualquier nivel | boolean         | —                     | false       |
-| size             | tamaño del input                         | string          | medium / small / mini | —           |
-| before-filter    | funcion antes de filtrar con el valor a filtrar como parámetro. Si se devuelve `false` o se devuelve una `Promise` y luego es rechazada, el filtrado será abortado. | function(value) | —                     | —           |
+### Custom option content
+
+You can customize the content of cascader node.
+
+:::demo You can customize the content of cascader node by `scoped slot`. You'll have access to `node` and `data` in the scope,  standing for the Node object and node data of the current node respectively。
+```html
+<el-cascader :options="options">
+  <template slot-scope="{ node, data }">
+    <span>{{ data.label }}</span>
+    <span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
+  </template>
+</el-cascader>
+
+<script>
+  export default {
+    data() {
+      return {
+        options: [{
+          value: 'guide',
+          label: 'Guide',
+          children: [{
+            value: 'disciplines',
+            label: 'Disciplines',
+            children: [{
+              value: 'consistency',
+              label: 'Consistency'
+            }, {
+              value: 'feedback',
+              label: 'Feedback'
+            }, {
+              value: 'efficiency',
+              label: 'Efficiency'
+            }, {
+              value: 'controllability',
+              label: 'Controllability'
+            }]
+          }, {
+            value: 'navigation',
+            label: 'Navigation',
+            children: [{
+              value: 'side nav',
+              label: 'Side Navigation'
+            }, {
+              value: 'top nav',
+              label: 'Top Navigation'
+            }]
+          }]
+        }, {
+          value: 'component',
+          label: 'Component',
+          children: [{
+            value: 'basic',
+            label: 'Basic',
+            children: [{
+              value: 'layout',
+              label: 'Layout'
+            }, {
+              value: 'color',
+              label: 'Color'
+            }, {
+              value: 'typography',
+              label: 'Typography'
+            }, {
+              value: 'icon',
+              label: 'Icon'
+            }, {
+              value: 'button',
+              label: 'Button'
+            }]
+          }, {
+            value: 'form',
+            label: 'Form',
+            children: [{
+              value: 'radio',
+              label: 'Radio'
+            }, {
+              value: 'checkbox',
+              label: 'Checkbox'
+            }, {
+              value: 'input',
+              label: 'Input'
+            }, {
+              value: 'input-number',
+              label: 'InputNumber'
+            }, {
+              value: 'select',
+              label: 'Select'
+            }, {
+              value: 'cascader',
+              label: 'Cascader'
+            }, {
+              value: 'switch',
+              label: 'Switch'
+            }, {
+              value: 'slider',
+              label: 'Slider'
+            }, {
+              value: 'time-picker',
+              label: 'TimePicker'
+            }, {
+              value: 'date-picker',
+              label: 'DatePicker'
+            }, {
+              value: 'datetime-picker',
+              label: 'DateTimePicker'
+            }, {
+              value: 'upload',
+              label: 'Upload'
+            }, {
+              value: 'rate',
+              label: 'Rate'
+            }, {
+              value: 'form',
+              label: 'Form'
+            }]
+          }, {
+            value: 'data',
+            label: 'Data',
+            children: [{
+              value: 'table',
+              label: 'Table'
+            }, {
+              value: 'tag',
+              label: 'Tag'
+            }, {
+              value: 'progress',
+              label: 'Progress'
+            }, {
+              value: 'tree',
+              label: 'Tree'
+            }, {
+              value: 'pagination',
+              label: 'Pagination'
+            }, {
+              value: 'badge',
+              label: 'Badge'
+            }]
+          }, {
+            value: 'notice',
+            label: 'Notice',
+            children: [{
+              value: 'alert',
+              label: 'Alert'
+            }, {
+              value: 'loading',
+              label: 'Loading'
+            }, {
+              value: 'message',
+              label: 'Message'
+            }, {
+              value: 'message-box',
+              label: 'MessageBox'
+            }, {
+              value: 'notification',
+              label: 'Notification'
+            }]
+          }, {
+            value: 'navigation',
+            label: 'Navigation',
+            children: [{
+              value: 'menu',
+              label: 'NavMenu'
+            }, {
+              value: 'tabs',
+              label: 'Tabs'
+            }, {
+              value: 'breadcrumb',
+              label: 'Breadcrumb'
+            }, {
+              value: 'dropdown',
+              label: 'Dropdown'
+            }, {
+              value: 'steps',
+              label: 'Steps'
+            }]
+          }, {
+            value: 'others',
+            label: 'Others',
+            children: [{
+              value: 'dialog',
+              label: 'Dialog'
+            }, {
+              value: 'tooltip',
+              label: 'Tooltip'
+            }, {
+              value: 'popover',
+              label: 'Popover'
+            }, {
+              value: 'card',
+              label: 'Card'
+            }, {
+              value: 'carousel',
+              label: 'Carousel'
+            }, {
+              value: 'collapse',
+              label: 'Collapse'
+            }]
+          }]
+        }, {
+          value: 'resource',
+          label: 'Resource',
+          children: [{
+            value: 'axure',
+            label: 'Axure Components'
+          }, {
+            value: 'sketch',
+            label: 'Sketch Templates'
+          }, {
+            value: 'docs',
+            label: 'Design Documentation'
+          }]
+        }]
+      }
+    }
+  }
+</script>
+```
+:::
+
+### Cascader panel
+
+`CascaderPanel` is the core component of `Cascader` which has various of features such as single selection, multiple selection, dynamic loading and so on.
+
+:::demo Just like `el-cascader`, you can set alternative options by `options`, and enable other features by `props`, see the API form below for details.
+```html
+<el-cascader-panel :options="options"></el-cascader-panel>
+
+<script>
+  export default {
+    data() {
+      return {
+        options: [{
+          value: 'guide',
+          label: 'Guide',
+          children: [{
+            value: 'disciplines',
+            label: 'Disciplines',
+            children: [{
+              value: 'consistency',
+              label: 'Consistency'
+            }, {
+              value: 'feedback',
+              label: 'Feedback'
+            }, {
+              value: 'efficiency',
+              label: 'Efficiency'
+            }, {
+              value: 'controllability',
+              label: 'Controllability'
+            }]
+          }, {
+            value: 'navigation',
+            label: 'Navigation',
+            children: [{
+              value: 'side nav',
+              label: 'Side Navigation'
+            }, {
+              value: 'top nav',
+              label: 'Top Navigation'
+            }]
+          }]
+        }, {
+          value: 'component',
+          label: 'Component',
+          children: [{
+            value: 'basic',
+            label: 'Basic',
+            children: [{
+              value: 'layout',
+              label: 'Layout'
+            }, {
+              value: 'color',
+              label: 'Color'
+            }, {
+              value: 'typography',
+              label: 'Typography'
+            }, {
+              value: 'icon',
+              label: 'Icon'
+            }, {
+              value: 'button',
+              label: 'Button'
+            }]
+          }, {
+            value: 'form',
+            label: 'Form',
+            children: [{
+              value: 'radio',
+              label: 'Radio'
+            }, {
+              value: 'checkbox',
+              label: 'Checkbox'
+            }, {
+              value: 'input',
+              label: 'Input'
+            }, {
+              value: 'input-number',
+              label: 'InputNumber'
+            }, {
+              value: 'select',
+              label: 'Select'
+            }, {
+              value: 'cascader',
+              label: 'Cascader'
+            }, {
+              value: 'switch',
+              label: 'Switch'
+            }, {
+              value: 'slider',
+              label: 'Slider'
+            }, {
+              value: 'time-picker',
+              label: 'TimePicker'
+            }, {
+              value: 'date-picker',
+              label: 'DatePicker'
+            }, {
+              value: 'datetime-picker',
+              label: 'DateTimePicker'
+            }, {
+              value: 'upload',
+              label: 'Upload'
+            }, {
+              value: 'rate',
+              label: 'Rate'
+            }, {
+              value: 'form',
+              label: 'Form'
+            }]
+          }, {
+            value: 'data',
+            label: 'Data',
+            children: [{
+              value: 'table',
+              label: 'Table'
+            }, {
+              value: 'tag',
+              label: 'Tag'
+            }, {
+              value: 'progress',
+              label: 'Progress'
+            }, {
+              value: 'tree',
+              label: 'Tree'
+            }, {
+              value: 'pagination',
+              label: 'Pagination'
+            }, {
+              value: 'badge',
+              label: 'Badge'
+            }]
+          }, {
+            value: 'notice',
+            label: 'Notice',
+            children: [{
+              value: 'alert',
+              label: 'Alert'
+            }, {
+              value: 'loading',
+              label: 'Loading'
+            }, {
+              value: 'message',
+              label: 'Message'
+            }, {
+              value: 'message-box',
+              label: 'MessageBox'
+            }, {
+              value: 'notification',
+              label: 'Notification'
+            }]
+          }, {
+            value: 'navigation',
+            label: 'Navigation',
+            children: [{
+              value: 'menu',
+              label: 'NavMenu'
+            }, {
+              value: 'tabs',
+              label: 'Tabs'
+            }, {
+              value: 'breadcrumb',
+              label: 'Breadcrumb'
+            }, {
+              value: 'dropdown',
+              label: 'Dropdown'
+            }, {
+              value: 'steps',
+              label: 'Steps'
+            }]
+          }, {
+            value: 'others',
+            label: 'Others',
+            children: [{
+              value: 'dialog',
+              label: 'Dialog'
+            }, {
+              value: 'tooltip',
+              label: 'Tooltip'
+            }, {
+              value: 'popover',
+              label: 'Popover'
+            }, {
+              value: 'card',
+              label: 'Card'
+            }, {
+              value: 'carousel',
+              label: 'Carousel'
+            }, {
+              value: 'collapse',
+              label: 'Collapse'
+            }]
+          }]
+        }, {
+          value: 'resource',
+          label: 'Resource',
+          children: [{
+            value: 'axure',
+            label: 'Axure Components'
+          }, {
+            value: 'sketch',
+            label: 'Sketch Templates'
+          }, {
+            value: 'docs',
+            label: 'Design Documentation'
+          }]
+        }]
+      };
+    }
+  };
+</script>
+```
+:::
+
+### Cascader Attributes
+| Attribute | Description | Type  | Accepted Values | Default |
+|---------- |-------- |---------- |-------------  |-------- |
+| value / v-model | binding value | - | — | — |
+| options | data of the options,the key of `value` and `label` can be customize by `Props`.| array | — | — |
+| props | configuration options, see the following table. | object | — | — |
+| size | size of input | string | medium / small / mini | — |
+| placeholder | placeholder of input | string | — | Select |
+| disabled | whether Cascader is disabled | boolean | — | false |
+| clearable | whether selected value can be cleared | boolean | — | false |
+| show-all-levels | whether to display all levels of the selected value in the input | boolean | — | true |
+| collapse-tags | whether to collapse tags in multiple selection mode | boolean | - | false |
+| separator | option label separator | string | — | ' / ' |
+| filterable | whether the options can be searched | boolean | — | — |
+| filter-method | customize search logic, the first parameter is `node`, the second is `keyword`, and need return a boolean value indicating whether it hits. | function(node, keyword) | - | - |
+| debounce | debounce delay when typing filter keyword, in milliseconds | number | — | 300 |
+| before-filter | hook function before filtering with the value to be filtered as its parameter. If `false` is returned or a `Promise` is returned and then is rejected, filtering will be aborted | function(value) | — | — |
+| popper-class | custom class name for Cascader's dropdown   | string | —  | — |
+
+### Cascader Events
+| Event Name | Description | Parameters |
+|---------- |-------- |---------- |
+| change | triggers when the binding value changes | value |
+| expand-change | triggers when expand option changes | an array of the expanding node's parent nodes |
+| blur | triggers when Cascader blurs | (event: Event) |
+| focus | triggers when Cascader focuses | (event: Event) |
+| visible-change | triggers when the dropdown appears/disappears | true when it appears, and false otherwise |
+| remove-tag | triggers when remove tag in multiple selection mode | the value of the tag which is removed |
+
+### Cascader Slots
+| Slot Name | Description |
+|---------|-------------|
+| - | the custom content of cascader node, the parameter is { node, data }, which are current Node object and node data respectively. |
+| empty  | content when there is no matched options. |
+
+### CascaderPanel Attributes
+| Attribute | Description | Type  | Accepted Values | Default |
+|---------- |-------- |---------- |-------------  |-------- |
+| value / v-model | binding value | - | — | — |
+| options | data of the options,the key of `value` and `label` can be customize by `Props`.| array | — | — |
+| props | configuration options, see the following table. | object | — | — |
 
-### props
-| Atributo | Descripción                              | Tipo   | Valores aceptados | Por defecto |
-| -------- | ---------------------------------------- | ------ | ----------------- | ----------- |
-| label    | especifica qué clave del elemento de la opción se utiliza como etiqueta | string | —                 | —           |
-| value    | especifica qué clave del elemento de la opción se utiliza como valor | string | —                 | —           |
-| children | especifica qué clave del elemento de la opción se utiliza como hijo | string | —                 | —           |
-| disabled | especifica qué clave del elemento de la opción indica si está deshabilitada | string | —                 | —           |
+### CascaderPanel Events
+| Event Name | Description | Parameters |
+|---------- |-------- |---------- |
+| change | triggers when the binding value changes | value |
+| expand-change | triggers when expand option changes | an array of the expanding node's parent nodes |
 
-### Eventos
-| Nombre             | Descripción                                                  | Parametros                |
-| ------------------ | ------------------------------------------------------------ | ------------------------- |
-| change             | se dispara cuando el valor cambia                            | valor                     |
-| active-item-change | se dispara cuando la opcion activa del parent cambia, sólo funciona cuando `change-on-select` es `false`. | array de opciones activas |
-| blur               | se dispara cuando Cascader pierde el foco                    | (event: Event)            |
-| focus              | se dispara cuando Cascader obtiene el foco                   | (event: Event)            |
-| visible-change     | se dispara cuando el menu desplegable aparece o desaparece   | true cuando aparece, y false en otro caso |
+### CascaderPanel Slots
+| Slot Name | Description |
+|---------|-------------|
+| - | the custom content of cascader node, the parameter is { node, data }, which are current Node object and node data respectively. |
 
+### Props
+| Attribute | Description | Type  | Accepted Values | Default |
+| -------- | ----------------- | ------ | ------ | ------ |
+| expandTrigger | trigger mode of expanding options | string | click / hover | 'click' |
+| multiple | whether multiple selection is enabled | boolean | - | false |
+| checkStrictly | whether checked state of a node not affects its parent and child nodes | boolean | - | false |
+| emitPath | when checked nodes change, whether to emit an array of node's path, if false, only emit the value of node. | boolean | - | true |
+| lazy | whether to dynamic load child nodes, use with `lazyload` attribute | boolean | - | false |
+| lazyLoad | method for loading child nodes data, only works when `lazy` is true | function(node, resolve) | - | - |
+| value    | specify which key of node object is used as the node's value | string | — | 'value' |
+| label    | specify which key of node object is used as the node's label | string | — | 'label' |
+| children | specify which key of node object is used as the node's children | string | — | 'children' |
+| disabled | specify which key of node object is used as the node's disabled | string | — | 'disabled' |
+| leaf     | specify which key of node object is used as the node's leaf field | string | — | 'leaf' |

+ 681 - 125
examples/docs/fr-FR/cascader.md

@@ -1,35 +1,34 @@
 ## Cascader
 
-Si les options ont une structure hiérarchique claire, un Cascader peut être utilisé pour les afficher et les selectionner.
+If the options have a clear hierarchical structure, Cascader can be used to view and select them.
 
-### Usage
+### Basic usage
 
-Il y a deux manières de dérouler la liste des options.
+There are two ways to expand child option items.
 
-:::demo Assigner un tableau à l'attribut `options` génère un Cascader. L'attribut `expand-trigger` détermine comment les options suivantes sont affichées. Cet exemple utilises aussi l'évènement `change`, qui a pour paramètre la valeur du Cascader, c'est à dire un tableau constitué de chaque niveau jusqu'à la valeur selectionnée, comme un chemin.
+:::demo Assigning the `options` attribute to an array of options renders a Cascader. The `props.expandTrigger` attribute defines how child options are expanded.
 ```html
 <div class="block">
-  <span class="demonstration">Les options se déroulent après un clic (défaut)</span>
+  <span class="demonstration">Child options expand when clicked (default)</span>
   <el-cascader
+    v-model="value"
     :options="options"
-    v-model="selectedOptions"
-    @change="handleChange">
-  </el-cascader>
+    @change="handleChange"></el-cascader>
 </div>
 <div class="block">
-  <span class="demonstration">Les options se déroulent au passage de la souris</span>
+  <span class="demonstration">Child options expand when hovered</span>
   <el-cascader
-    expand-trigger="hover"
+    v-model="value"
     :options="options"
-    v-model="selectedOptions2"
-    @change="handleChange">
-  </el-cascader>
+    :props="{ expandTrigger: 'hover' }"
+    @change="handleChange"></el-cascader>
 </div>
 
 <script>
   export default {
     data() {
       return {
+        value: [],
         options: [{
           value: 'guide',
           label: 'Guide',
@@ -224,9 +223,7 @@ Il y a deux manières de dérouler la liste des options.
             value: 'docs',
             label: 'Design Documentation'
           }]
-        }],
-        selectedOptions: [],
-        selectedOptions2: []
+        }]
       };
     },
     methods: {
@@ -239,20 +236,19 @@ Il y a deux manières de dérouler la liste des options.
 ```
 :::
 
-### Options désactivées
+### Disabled option
 
-Désactivez une option en configurant `disabled` dans l'objet option.
+Disable an option by setting a `disabled` field in the option object.
 
-:::demo Dans cet exemple, le premier item dans le tableau `options` est configuré avec `disabled: true`, il est donc désactivé. Par défaut, Cascader recherche le champ `disabled` dans chaque option; si vous utilisez un autre nom de champ pour indiquer que l'option est désactivée, vous pouvez l'assigner dans l'attribut `props` (voir la table de l'API ci-dessous). Les champs `value`, `label` et `children` peuvent être personnalisés de la même manière.
+:::demo In this example, the first item in `options` array has a `disabled: true` field, so it is disabled. By default, Cascader checks the `disabled` field in each option object; if you are using another field name to indicate whether an option is disabled, you can assign it in the `props.disabled` attribute (see the API table below for details). And of course, field name `value`, `label` and `children` can also be customized in the same way.
 ```html
-<el-cascader
-  :options="optionsWithDisabled"
-></el-cascader>
+<el-cascader :options="options"></el-cascader>
+
 <script>
   export default {
     data() {
       return {
-        optionsWithDisabled: [{
+        options: [{
           value: 'guide',
           label: 'Guide',
           disabled: true,
@@ -455,16 +451,14 @@ Désactivez une option en configurant `disabled` dans l'objet option.
 ```
 :::
 
-### Afficher uniquement le dernier niveau
+### Clearable
 
-Le champ peut n'afficher que le dernier niveau au lieu de tous.
+Set `clearable` attribute for `el-cascader` and a clear icon will appear when selected and hovered
 
-:::demo L'attribut `show-all-levels` détermine si tous les niveaux sont affichés. Si il est à `false`, seul le dernier niveau est affiché.
+:::demo
 ```html
-<el-cascader
-    :options="options"
-    :show-all-levels="false"
-></el-cascader>
+<el-cascader :options="options" clearable></el-cascader>
+
 <script>
   export default {
     data() {
@@ -664,21 +658,20 @@ Le champ peut n'afficher que le dernier niveau au lieu de tous.
             label: 'Design Documentation'
           }]
         }]
-      };
+      }
     }
-  };
+  }
 </script>
 ```
 :::
 
-### Valeur par défaut
+### Display only the last level
 
-:::demo La valeur par défaut peut être définit grâce à un tableau.
+The input can display only the last level instead of all levels.
+
+:::demo The `show-all-levels` attribute defines if all levels are displayed. If it is `false`, only the last level is displayed.
 ```html
-<el-cascader
-  :options="options"
-  v-model="selectedOptions"
-></el-cascader>
+<el-cascader :options="options" :show-all-levels="false"></el-cascader>
 <script>
   export default {
     data() {
@@ -877,8 +870,110 @@ Le champ peut n'afficher que le dernier niveau au lieu de tous.
             value: 'docs',
             label: 'Design Documentation'
           }]
-        }],
-        selectedOptions: ['component', 'data', 'tag']
+        }]
+      };
+    }
+  };
+</script>
+```
+:::
+
+### Multiple Selection
+
+Set `props.multiple = true` to use multiple selection.
+
+:::demo When using multiple selection, all selected tags will display by default, You can set `collapse-tags = true` to fold selected tags.
+```html
+<div class="block">
+  <span class="demonstration">Display all tags (default)</span>
+  <el-cascader
+    :options="options"
+    :props="props"
+    clearable></el-cascader>
+</div>
+<div class="block">
+  <span class="demonstration">Collapse tags</span>
+  <el-cascader
+    :options="options"
+    :props="props"
+    collapse-tags
+    clearable></el-cascader>
+</div>
+
+<script>
+  export default {
+    data() {
+      return {
+        props: { multiple: true },
+        options: [{
+          value: 1,
+          label: 'Asia',
+          children: [{
+            value: 2,
+            label: 'China',
+            children: [
+              { value: 3, label: 'Beijing' },
+              { value: 4, label: 'Shanghai' },
+              { value: 5, label: 'Hangzhou' }
+            ]
+          }, {
+            value: 6,
+            label: 'Japan',
+            children: [
+              { value: 7, label: 'Tokyo' },
+              { value: 8, label: 'Osaka' },
+              { value: 9, label: 'Kyoto' }
+            ]
+          }, {
+            value: 10,
+            label: 'Korea',
+            children: [
+              { value: 11, label: 'Seoul' },
+              { value: 12, label: 'Busan' },
+              { value: 13, label: 'Taegu' }
+            ]
+          }]
+        }, {
+          value: 14,
+          label: 'Europe',
+          children: [{
+            value: 15,
+            label: 'France',
+            children: [
+              { value: 16, label: 'Paris' },
+              { value: 17, label: 'Marseille' },
+              { value: 18, label: 'Lyon' }
+            ]
+          }, {
+            value: 19,
+            label: 'UK',
+            children: [
+              { value: 20, label: 'London' },
+              { value: 21, label: 'Birmingham' },
+              { value: 22, label: 'Manchester' }
+            ]
+          }]
+        }, {
+          value: 23,
+          label: 'North America',
+          children: [{
+            value: 24,
+            label: 'US',
+            children: [
+              { value: 25, label: 'New York' },
+              { value: 26, label: 'Los Angeles' },
+              { value: 27, label: 'Washington' }
+            ]
+          }, {
+            value: 28,
+            label: 'Canada',
+            children: [
+              { value: 29, label: 'Toronto' },
+              { value: 30, label: 'Montreal' },
+              { value: 31, label: 'Ottawa' }
+            ]
+          }]
+        }]
       };
     }
   };
@@ -886,16 +981,28 @@ Le champ peut n'afficher que le dernier niveau au lieu de tous.
 ```
 :::
 
-### Changement après sélection
 
-Les options parentes peuvent aussi être sélectionnées.
+### Select any level of options
 
-:::demo Par défaut seules les options de dernier niveau peuvent être sélectionnées. En réglant `change-on-select` à `true`, les options parentes peuvent aussi être sélectionnées.
+In single selection, only the leaf nodes can be checked, and in multiple selection, check parent nodes will lead to leaf nodes be checked eventually. When enable this feature, it can make parent and child nodes unlinked and you can select any level of options.
+
+:::demo Set `props.checkStrictly = true` to make checked state of a node not affects its parent nodes and child nodes, and then you can select any level of options.
 ```html
-<el-cascader
-  :options="options"
-  change-on-select
-></el-cascader>
+<div class="block">
+  <span class="demonstration">Select any level of options (Single selection)</span>
+  <el-cascader
+    :options="options"
+    :props="{ checkStrictly: true }"
+    clearable></el-cascader>
+</div>
+<div class="block">
+  <span class="demonstration">Select any level of options (Multiple selection)</span>
+  <el-cascader
+    :options="options"
+    :props="{ multiple: true, checkStrictly: true }"
+    clearable></el-cascader>
+</div>
+
 <script>
   export default {
     data() {
@@ -1102,79 +1209,63 @@ Les options parentes peuvent aussi être sélectionnées.
 ```
 :::
 
-### Charger les options dynamiquement
+### Dynamic loading
 
-Il est possible de charger dynamiquement les options quand on clique ou passe la souris sur leurs parent.
+Dynamic load its child nodes when checked a node.
 
-:::demo Dans cet exemple, les optionsn'ont pas de données de villes au moment de l'initialisation. Grâce à l'évènement `active-item-change` vous pouvez charger les villes de manière dynamique. De plus, cet exemple montre comment `props` peut être utilisé.
+:::demo Set `lazy = true` to use dynamic loading, and you have to specify how to load the data source by `lazyload`. There are two parameters of `lazyload`,the first parameter `node` is the node currently clicked, and the `resolve` is a callback that indicate loading is finished which must invoke. To display the status of node more accurately, you can add a `leaf` field (can be modified by `props.leaf`) to indicate whether it is a leaf node. Otherwise, it will be inferred by if has any child nodes.
 ```html
-<el-cascader
-  :options="options"
-  @active-item-change="handleItemChange"
-  :props="props"
-></el-cascader>
+<el-cascader :props="props"></el-cascader>
 
 <script>
+  let id = 0;
+
   export default {
     data() {
       return {
-        options: [{
-          label: 'California',
-          cities: []
-        }, {
-          label: 'Florida',
-          cities: []
-        }],
         props: {
-          value: 'label',
-          children: 'cities'
+          lazy: true,
+          lazyLoad (node, resolve) {
+            const { level } = node;
+            setTimeout(() => {
+              const nodes = Array.from({ length: level + 1 })
+                .map(item => ({
+                  value: ++id,
+                  label: `Option - ${id}`,
+                  leaf: level >= 2
+                }));
+              // Invoke `resolve` callback to return the child nodes data and indicate the loading is finished.
+              resolve(nodes);
+            }, 1000);
+          }
         }
       };
-    },
-
-    methods: {
-      handleItemChange(val) {
-        console.log('active item:', val);
-        setTimeout(_ => {
-          if (val.indexOf('California') > -1 && !this.options[0].cities.length) {
-            this.options[0].cities = [{
-              label: 'Los Angeles'
-            }];
-          } else if (val.indexOf('Florida') > -1 && !this.options[1].cities.length) {
-            this.options[1].cities = [{
-              label: 'Orlando'
-            }];
-          }
-        }, 300);
-      }
     }
   };
 </script>
 ```
 :::
 
-### Filtres
+### Filterable
 
-Recherchez une option particulière en entrant des mots-clé.
+Search and select options with a keyword.
 
-:::demo Ajouter `filterable` à `el-cascader` active le filtrage.
+:::demo Adding `filterable` to `el-cascader` enables filtering. Cascader will match nodes whose label or parent's label (according to `show-all-levels`) includes input keyword. Of course, you can customize search logic by `filter-method` which accepts a function, the first parameter is `node`, the second is `keyword`, and need return a boolean value indicating whether it hits.
 ```html
 <div class="block">
-  <span class="demonstration">Only options of the last level can be selected</span>
+  <span class="demonstration">Filterable (Single selection)</span>
   <el-cascader
-    placeholder="Try searching: Guide"
+    placeholder="Try searchingL Guide"
     :options="options"
-    filterable
-  ></el-cascader>
+    filterable></el-cascader>
 </div>
 <div class="block">
-  <span class="demonstration">Options of all levels can be selected</span>
+  <span class="demonstration">Filterable (Multiple selection)</span>
   <el-cascader
-    placeholder="Try searching: Guide"
+    placeholder="Try searchingL Guide"
     :options="options"
-    filterable
-    change-on-select
-  ></el-cascader>
+    :props="{ multiple: true }"
+    filterable></el-cascader>
 </div>
 
 <script>
@@ -1383,38 +1474,503 @@ Recherchez une option particulière en entrant des mots-clé.
 ```
 :::
 
-### Attributs
-| Attribut | Description         | Type    | Options       | Défaut|
-|---------- |-------------------- |---------|-------------  |-------- |
-| value / v-model | Valeur sélectionnée. | array | — | — |
-| options   | Données des options. | array | — | — |
-| props | Options de configuration, voir la table suivante. | object | — | — |
-| separator | Séparateur des options. | string | — | / |
-| popper-class | Classe personnalisée pour le menu déroulant du Cascader | string | — | — |
-| placeholder | Placeholder du champ. | string | — | Select |
-| disabled  | Désactive le Cascader. | boolean |  — | false |
-| clearable  | Détermine si la valeur peut être effacée. | boolean | — | false |
-| expand-trigger  | Mode de déclenchement du menu suivant. | string | click / hover | click |
-| show-all-levels | Détermine si tout les niveaux doivent être affichés dans le champ. | boolean | — | true |
-| filterable  | Active la recherche par mots-clé. | boolean | — | — |
-| debounce | Délai d'attente après avoir entré un mot-clé, en millisecondes. | number | — | 300 |
-| change-on-select | Détermine si il est possible de sélectionner une option de n'importe quel niveau. | boolean | — | false |
-| size  | taille du champ | string | medium / small / mini | — |
-| before-filter | Fonction de hook qui s'active avant le filtrage, le paramètre étant la valeur entrée. Si `false` est retourné ou bien une `Promise` est retournée puis rejetée, le filtrage sera annulé. | function(value) | — | — |
+### Custom option content
+
+You can customize the content of cascader node.
+
+:::demo You can customize the content of cascader node by `scoped slot`. You'll have access to `node` and `data` in the scope,  standing for the Node object and node data of the current node respectively。
+```html
+<el-cascader :options="options">
+  <template slot-scope="{ node, data }">
+    <span>{{ data.label }}</span>
+    <span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
+  </template>
+</el-cascader>
+
+<script>
+  export default {
+    data() {
+      return {
+        options: [{
+          value: 'guide',
+          label: 'Guide',
+          children: [{
+            value: 'disciplines',
+            label: 'Disciplines',
+            children: [{
+              value: 'consistency',
+              label: 'Consistency'
+            }, {
+              value: 'feedback',
+              label: 'Feedback'
+            }, {
+              value: 'efficiency',
+              label: 'Efficiency'
+            }, {
+              value: 'controllability',
+              label: 'Controllability'
+            }]
+          }, {
+            value: 'navigation',
+            label: 'Navigation',
+            children: [{
+              value: 'side nav',
+              label: 'Side Navigation'
+            }, {
+              value: 'top nav',
+              label: 'Top Navigation'
+            }]
+          }]
+        }, {
+          value: 'component',
+          label: 'Component',
+          children: [{
+            value: 'basic',
+            label: 'Basic',
+            children: [{
+              value: 'layout',
+              label: 'Layout'
+            }, {
+              value: 'color',
+              label: 'Color'
+            }, {
+              value: 'typography',
+              label: 'Typography'
+            }, {
+              value: 'icon',
+              label: 'Icon'
+            }, {
+              value: 'button',
+              label: 'Button'
+            }]
+          }, {
+            value: 'form',
+            label: 'Form',
+            children: [{
+              value: 'radio',
+              label: 'Radio'
+            }, {
+              value: 'checkbox',
+              label: 'Checkbox'
+            }, {
+              value: 'input',
+              label: 'Input'
+            }, {
+              value: 'input-number',
+              label: 'InputNumber'
+            }, {
+              value: 'select',
+              label: 'Select'
+            }, {
+              value: 'cascader',
+              label: 'Cascader'
+            }, {
+              value: 'switch',
+              label: 'Switch'
+            }, {
+              value: 'slider',
+              label: 'Slider'
+            }, {
+              value: 'time-picker',
+              label: 'TimePicker'
+            }, {
+              value: 'date-picker',
+              label: 'DatePicker'
+            }, {
+              value: 'datetime-picker',
+              label: 'DateTimePicker'
+            }, {
+              value: 'upload',
+              label: 'Upload'
+            }, {
+              value: 'rate',
+              label: 'Rate'
+            }, {
+              value: 'form',
+              label: 'Form'
+            }]
+          }, {
+            value: 'data',
+            label: 'Data',
+            children: [{
+              value: 'table',
+              label: 'Table'
+            }, {
+              value: 'tag',
+              label: 'Tag'
+            }, {
+              value: 'progress',
+              label: 'Progress'
+            }, {
+              value: 'tree',
+              label: 'Tree'
+            }, {
+              value: 'pagination',
+              label: 'Pagination'
+            }, {
+              value: 'badge',
+              label: 'Badge'
+            }]
+          }, {
+            value: 'notice',
+            label: 'Notice',
+            children: [{
+              value: 'alert',
+              label: 'Alert'
+            }, {
+              value: 'loading',
+              label: 'Loading'
+            }, {
+              value: 'message',
+              label: 'Message'
+            }, {
+              value: 'message-box',
+              label: 'MessageBox'
+            }, {
+              value: 'notification',
+              label: 'Notification'
+            }]
+          }, {
+            value: 'navigation',
+            label: 'Navigation',
+            children: [{
+              value: 'menu',
+              label: 'NavMenu'
+            }, {
+              value: 'tabs',
+              label: 'Tabs'
+            }, {
+              value: 'breadcrumb',
+              label: 'Breadcrumb'
+            }, {
+              value: 'dropdown',
+              label: 'Dropdown'
+            }, {
+              value: 'steps',
+              label: 'Steps'
+            }]
+          }, {
+            value: 'others',
+            label: 'Others',
+            children: [{
+              value: 'dialog',
+              label: 'Dialog'
+            }, {
+              value: 'tooltip',
+              label: 'Tooltip'
+            }, {
+              value: 'popover',
+              label: 'Popover'
+            }, {
+              value: 'card',
+              label: 'Card'
+            }, {
+              value: 'carousel',
+              label: 'Carousel'
+            }, {
+              value: 'collapse',
+              label: 'Collapse'
+            }]
+          }]
+        }, {
+          value: 'resource',
+          label: 'Resource',
+          children: [{
+            value: 'axure',
+            label: 'Axure Components'
+          }, {
+            value: 'sketch',
+            label: 'Sketch Templates'
+          }, {
+            value: 'docs',
+            label: 'Design Documentation'
+          }]
+        }]
+      }
+    }
+  }
+</script>
+```
+:::
+
+### Cascader panel
+
+`CascaderPanel` is the core component of `Cascader` which has various of features such as single selection, multiple selection, dynamic loading and so on.
+
+:::demo Just like `el-cascader`, you can set alternative options by `options`, and enable other features by `props`, see the API form below for details.
+```html
+<el-cascader-panel :options="options"></el-cascader-panel>
+
+<script>
+  export default {
+    data() {
+      return {
+        options: [{
+          value: 'guide',
+          label: 'Guide',
+          children: [{
+            value: 'disciplines',
+            label: 'Disciplines',
+            children: [{
+              value: 'consistency',
+              label: 'Consistency'
+            }, {
+              value: 'feedback',
+              label: 'Feedback'
+            }, {
+              value: 'efficiency',
+              label: 'Efficiency'
+            }, {
+              value: 'controllability',
+              label: 'Controllability'
+            }]
+          }, {
+            value: 'navigation',
+            label: 'Navigation',
+            children: [{
+              value: 'side nav',
+              label: 'Side Navigation'
+            }, {
+              value: 'top nav',
+              label: 'Top Navigation'
+            }]
+          }]
+        }, {
+          value: 'component',
+          label: 'Component',
+          children: [{
+            value: 'basic',
+            label: 'Basic',
+            children: [{
+              value: 'layout',
+              label: 'Layout'
+            }, {
+              value: 'color',
+              label: 'Color'
+            }, {
+              value: 'typography',
+              label: 'Typography'
+            }, {
+              value: 'icon',
+              label: 'Icon'
+            }, {
+              value: 'button',
+              label: 'Button'
+            }]
+          }, {
+            value: 'form',
+            label: 'Form',
+            children: [{
+              value: 'radio',
+              label: 'Radio'
+            }, {
+              value: 'checkbox',
+              label: 'Checkbox'
+            }, {
+              value: 'input',
+              label: 'Input'
+            }, {
+              value: 'input-number',
+              label: 'InputNumber'
+            }, {
+              value: 'select',
+              label: 'Select'
+            }, {
+              value: 'cascader',
+              label: 'Cascader'
+            }, {
+              value: 'switch',
+              label: 'Switch'
+            }, {
+              value: 'slider',
+              label: 'Slider'
+            }, {
+              value: 'time-picker',
+              label: 'TimePicker'
+            }, {
+              value: 'date-picker',
+              label: 'DatePicker'
+            }, {
+              value: 'datetime-picker',
+              label: 'DateTimePicker'
+            }, {
+              value: 'upload',
+              label: 'Upload'
+            }, {
+              value: 'rate',
+              label: 'Rate'
+            }, {
+              value: 'form',
+              label: 'Form'
+            }]
+          }, {
+            value: 'data',
+            label: 'Data',
+            children: [{
+              value: 'table',
+              label: 'Table'
+            }, {
+              value: 'tag',
+              label: 'Tag'
+            }, {
+              value: 'progress',
+              label: 'Progress'
+            }, {
+              value: 'tree',
+              label: 'Tree'
+            }, {
+              value: 'pagination',
+              label: 'Pagination'
+            }, {
+              value: 'badge',
+              label: 'Badge'
+            }]
+          }, {
+            value: 'notice',
+            label: 'Notice',
+            children: [{
+              value: 'alert',
+              label: 'Alert'
+            }, {
+              value: 'loading',
+              label: 'Loading'
+            }, {
+              value: 'message',
+              label: 'Message'
+            }, {
+              value: 'message-box',
+              label: 'MessageBox'
+            }, {
+              value: 'notification',
+              label: 'Notification'
+            }]
+          }, {
+            value: 'navigation',
+            label: 'Navigation',
+            children: [{
+              value: 'menu',
+              label: 'NavMenu'
+            }, {
+              value: 'tabs',
+              label: 'Tabs'
+            }, {
+              value: 'breadcrumb',
+              label: 'Breadcrumb'
+            }, {
+              value: 'dropdown',
+              label: 'Dropdown'
+            }, {
+              value: 'steps',
+              label: 'Steps'
+            }]
+          }, {
+            value: 'others',
+            label: 'Others',
+            children: [{
+              value: 'dialog',
+              label: 'Dialog'
+            }, {
+              value: 'tooltip',
+              label: 'Tooltip'
+            }, {
+              value: 'popover',
+              label: 'Popover'
+            }, {
+              value: 'card',
+              label: 'Card'
+            }, {
+              value: 'carousel',
+              label: 'Carousel'
+            }, {
+              value: 'collapse',
+              label: 'Collapse'
+            }]
+          }]
+        }, {
+          value: 'resource',
+          label: 'Resource',
+          children: [{
+            value: 'axure',
+            label: 'Axure Components'
+          }, {
+            value: 'sketch',
+            label: 'Sketch Templates'
+          }, {
+            value: 'docs',
+            label: 'Design Documentation'
+          }]
+        }]
+      };
+    }
+  };
+</script>
+```
+:::
+
+### Cascader Attributes
+| Attribute | Description | Type  | Accepted Values | Default |
+|---------- |-------- |---------- |-------------  |-------- |
+| value / v-model | binding value | - | — | — |
+| options | data of the options,the key of `value` and `label` can be customize by `Props`.| array | — | — |
+| props | configuration options, see the following table. | object | — | — |
+| size | size of input | string | medium / small / mini | — |
+| placeholder | placeholder of input | string | — | Select |
+| disabled | whether Cascader is disabled | boolean | — | false |
+| clearable | whether selected value can be cleared | boolean | — | false |
+| show-all-levels | whether to display all levels of the selected value in the input | boolean | — | true |
+| collapse-tags | whether to collapse tags in multiple selection mode | boolean | - | false |
+| separator | option label separator | string | — | ' / ' |
+| filterable | whether the options can be searched | boolean | — | — |
+| filter-method | customize search logic, the first parameter is `node`, the second is `keyword`, and need return a boolean value indicating whether it hits. | function(node, keyword) | - | - |
+| debounce | debounce delay when typing filter keyword, in milliseconds | number | — | 300 |
+| before-filter | hook function before filtering with the value to be filtered as its parameter. If `false` is returned or a `Promise` is returned and then is rejected, filtering will be aborted | function(value) | — | — |
+| popper-class | custom class name for Cascader's dropdown   | string | —  | — |
+
+### Cascader Events
+| Event Name | Description | Parameters |
+|---------- |-------- |---------- |
+| change | triggers when the binding value changes | value |
+| expand-change | triggers when expand option changes | an array of the expanding node's parent nodes |
+| blur | triggers when Cascader blurs | (event: Event) |
+| focus | triggers when Cascader focuses | (event: Event) |
+| visible-change | triggers when the dropdown appears/disappears | true when it appears, and false otherwise |
+| remove-tag | triggers when remove tag in multiple selection mode | the value of the tag which is removed |
 
-### props
-| Attribut | Description | Type | Valeurs acceptées | Défaut |
-| --------- | ----------------- | ------ | ------ | ------ |
-| label     | Détermine quelle clé d'option est utilisé comme label. | string | — | — |
-| value     | Détermine quelle clé d'option est utilisé comme valeur. | string | — | — |
-| children  | Détermine quelle clé d'option est utilisé en tant que sous-options. | string | — | — |
-| disabled  | Détermine quelle clé d'option est utilisé pour indiquer si l'option est désactivée. | string | — | — |
+### Cascader Slots
+| Slot Name | Description |
+|---------|-------------|
+| - | the custom content of cascader node, the parameter is { node, data }, which are current Node object and node data respectively. |
+| empty  | content when there is no matched options. |
 
-### Évènements
-| Nom | Description | Paramètres |
+### CascaderPanel Attributes
+| Attribute | Description | Type  | Accepted Values | Default |
+|---------- |-------- |---------- |-------------  |-------- |
+| value / v-model | binding value | - | — | — |
+| options | data of the options,the key of `value` and `label` can be customize by `Props`.| array | — | — |
+| props | configuration options, see the following table. | object | — | — |
+
+### CascaderPanel Events
+| Event Name | Description | Parameters |
 |---------- |-------- |---------- |
-| change  | Se déclecnhe lorsque la valeur change. | La valeur |
-| active-item-change | Se déclenche quand le parent de l'option active change, ne marche que si `change-on-select` est `false` | Un tableau des options actives |
-| blur | Se déclenche quand le Cascader perds le focus. | (event: Event) |
-| focus | Se déclenche quand le Cascader a le focus. | (event: Event) |
-| visible-change | Se déclenche quand le menu apparaît ou disparaît. | `true` quand il apparaît, `false` sinon. |
+| change | triggers when the binding value changes | value |
+| expand-change | triggers when expand option changes | an array of the expanding node's parent nodes |
+
+### CascaderPanel Slots
+| Slot Name | Description |
+|---------|-------------|
+| - | the custom content of cascader node, the parameter is { node, data }, which are current Node object and node data respectively. |
+
+### Props
+| Attribute | Description | Type  | Accepted Values | Default |
+| -------- | ----------------- | ------ | ------ | ------ |
+| expandTrigger | trigger mode of expanding options | string | click / hover | 'click' |
+| multiple | whether multiple selection is enabled | boolean | - | false |
+| checkStrictly | whether checked state of a node not affects its parent and child nodes | boolean | - | false |
+| emitPath | when checked nodes change, whether to emit an array of node's path, if false, only emit the value of node. | boolean | - | true |
+| lazy | whether to dynamic load child nodes, use with `lazyload` attribute | boolean | - | false |
+| lazyLoad | method for loading child nodes data, only works when `lazy` is true | function(node, resolve) | - | - |
+| value    | specify which key of node object is used as the node's value | string | — | 'value' |
+| label    | specify which key of node object is used as the node's label | string | — | 'label' |
+| children | specify which key of node object is used as the node's children | string | — | 'children' |
+| disabled | specify which key of node object is used as the node's disabled | string | — | 'disabled' |
+| leaf     | specify which key of node object is used as the node's leaf field | string | — | 'leaf' |

+ 637 - 102
examples/docs/zh-CN/cascader.md

@@ -6,30 +6,29 @@
 
 有两种触发子菜单的方式
 
-:::demo 只需为 Cascader 的`options`属性指定选项数组即可渲染出一个级联选择器。通过`expand-trigger`可以定义展开子级菜单的触发方式。本例还展示了`change`事件,它的参数为 Cascader 的绑定值:一个由各级菜单的值所组成的数组。
+:::demo 只需为 Cascader 的`options`属性指定选项数组即可渲染出一个级联选择器。通过`props.expandTrigger`可以定义展开子级菜单的触发方式。
 ```html
 <div class="block">
   <span class="demonstration">默认 click 触发子菜单</span>
   <el-cascader
+    v-model="value"
     :options="options"
-    v-model="selectedOptions"
-    @change="handleChange">
-  </el-cascader>
+    @change="handleChange"></el-cascader>
 </div>
 <div class="block">
   <span class="demonstration">hover 触发子菜单</span>
   <el-cascader
-    expand-trigger="hover"
+    v-model="value"
     :options="options"
-    v-model="selectedOptions2"
-    @change="handleChange">
-  </el-cascader>
+    :props="{ expandTrigger: 'hover' }"
+    @change="handleChange"></el-cascader>
 </div>
 
 <script>
   export default {
     data() {
       return {
+        value: [],
         options: [{
           value: 'zhinan',
           label: '指南',
@@ -224,9 +223,7 @@
             value: 'jiaohu',
             label: '组件交互文档'
           }]
-        }],
-        selectedOptions: [],
-        selectedOptions2: []
+        }]
       };
     },
     methods: {
@@ -243,16 +240,15 @@
 
 通过在数据源中设置 `disabled` 字段来声明该选项是禁用的
 
-:::demo 本例中,`options`指定的数组中的第一个元素含有`disabled: true`键值对,因此是禁用的。在默认情况下,Cascader 会检查数据中每一项的`disabled`字段是否为`true`,如果你的数据中表示禁用含义的字段名不为`disabled`,可以通过`props`属性来指定(详见下方 API 表格)。当然,`value`、`label`和`children`这三个字段名也可以通过同样的方式指定。
+:::demo 本例中,`options`指定的数组中的第一个元素含有`disabled: true`键值对,因此是禁用的。在默认情况下,Cascader 会检查数据中每一项的`disabled`字段是否为`true`,如果你的数据中表示禁用含义的字段名不为`disabled`,可以通过`props.disabled`属性来指定(详见下方 API 表格)。当然,`value`、`label`和`children`这三个字段名也可以通过同样的方式指定。
 ```html
-<el-cascader
-  :options="optionsWithDisabled"
-></el-cascader>
+<el-cascader :options="options"></el-cascader>
+
 <script>
   export default {
     data() {
       return {
-        optionsWithDisabled: [{
+        options: [{
           value: 'zhinan',
           label: '指南',
           disabled: true,
@@ -455,16 +451,14 @@
 ```
 :::
 
-### 仅显示最后一级
+### 可清空
 
-可以仅在输入框中显示选中项最后一级的标签,而不是选中项所在的完整路径。
+通过 `clearable` 设置输入框可清空
 
-:::demo 属性`show-all-levels`定义了是否显示完整的路径,将其赋值为`false`则仅显示最后一级
+:::demo
 ```html
-<el-cascader
-  :options="options"
-  :show-all-levels="false"
-></el-cascader>
+<el-cascader :options="options" clearable></el-cascader>
+
 <script>
   export default {
     data() {
@@ -664,21 +658,21 @@
             label: '组件交互文档'
           }]
         }]
-      };
+      }
     }
-  };
+  }
 </script>
 ```
 :::
 
-### 默认值
+### 仅显示最后一级
+
+可以仅在输入框中显示选中项最后一级的标签,而不是选中项所在的完整路径。
 
-:::demo 默认值通过数组的方式指定。
+:::demo 属性`show-all-levels`定义了是否显示完整的路径,将其赋值为`false`则仅显示最后一级
 ```html
-<el-cascader
-  :options="options"
-  v-model="selectedOptions"
-></el-cascader>
+<el-cascader :options="options" :show-all-levels="false"></el-cascader>
+
 <script>
   export default {
     data() {
@@ -877,8 +871,88 @@
             value: 'jiaohu',
             label: '组件交互文档'
           }]
-        }],
-        selectedOptions: ['zujian', 'data', 'tag']
+        }]
+      };
+    }
+  };
+</script>
+```
+:::
+
+### 多选
+
+可通过 `props.multiple = true` 来开启多选模式
+
+:::demo 在开启多选模式后,默认情况下会展示所有已选中的选项的Tag,你可以使用`collapse-tags`来折叠Tag
+```html
+<div class="block">
+  <span class="demonstration">默认显示所有Tag</span>
+  <el-cascader
+    :options="options"
+    :props="props"
+    clearable></el-cascader>
+</div>
+<div class="block">
+  <span class="demonstration">折叠展示Tag</span>
+  <el-cascader
+    :options="options"
+    :props="props"
+    collapse-tags
+    clearable></el-cascader>
+</div>
+
+<script>
+  export default {
+    data() {
+      return {
+        props: { multiple: true },
+        options: [{
+          value: 1,
+          label: '东南',
+          children: [{
+            value: 2,
+            label: '上海',
+            children: [
+              { value: 3, label: '普陀' },
+              { value: 4, label: '黄埔' },
+              { value: 5, label: '徐汇' }
+            ]
+          }, {
+            value: 7,
+            label: '江苏',
+            children: [
+              { value: 8, label: '南京' },
+              { value: 9, label: '苏州' },
+              { value: 10, label: '无锡' }
+            ]
+          }, {
+            value: 12,
+            label: '浙江',
+            children: [
+              { value: 13, label: '杭州' },
+              { value: 14, label: '宁波' },
+              { value: 15, label: '嘉兴' }
+            ]
+          }]
+        }, {
+          value: 17,
+          label: '西北',
+          children: [{
+            value: 18,
+            label: '陕西',
+            children: [
+              { value: 19, label: '西安' },
+              { value: 20, label: '延安' }
+            ]
+          }, {
+            value: 21,
+            label: '新疆维吾尔族自治区',
+            children: [
+              { value: 22, label: '乌鲁木齐' },
+              { value: 23, label: '克拉玛依' }
+            ]
+          }]
+        }]
       };
     }
   };
@@ -886,16 +960,28 @@
 ```
 :::
 
-### 选择即改变
 
-点击或移入选项即表示选中该项,可用于选择任意一级菜单的选项。
+### 选择任意一级选项
+
+在单选模式下,你只能选择叶子节点;而在多选模式下,勾选父节点真正选中的都是叶子节点。启用该功能后,可让父子节点取消关联,选择任意一级选项。
 
-:::demo 若需要允许用户选择任意一级选项,则可将`change-on-select`赋值为`true`
+:::demo 可通过 `props.checkStrictly = true` 来设置父子节点取消选中关联,从而达到选择任意一级选项的目的。
 ```html
-<el-cascader
-  :options="options"
-  change-on-select
-></el-cascader>
+<div class="block">
+  <span class="demonstration">单选选择任意一级选项</span>
+  <el-cascader
+    :options="options"
+    :props="{ checkStrictly: true }"
+    clearable></el-cascader>
+</div>
+<div class="block">
+  <span class="demonstration">多选选择任意一级选项</span>
+  <el-cascader
+    :options="options"
+    :props="{ multiple: true, checkStrictly: true }"
+    clearable></el-cascader>
+</div>
+
 <script>
   export default {
     data() {
@@ -1102,51 +1188,37 @@
 ```
 :::
 
-### 动态加载次级选项
+### 动态加载
 
 当选中某一级时,动态加载该级下的选项。
 
-:::demo 本例的选项数据源在初始化时不包含城市数据。利用`active-item-change`事件,可以在用户点击某个省份时拉取该省份下的城市数据。此外,本例还展示了`props`属性的用法
+:::demo 通过`lazy`开启动态加载,并通过`lazyload`来设置加载数据源的方法。`lazyload`方法有两个参数,第一个参数`node`为当前点击的节点,第二个`resolve`为数据加载完成的回调(必须调用)。为了更准确的显示节点的状态,还可以对节点数据添加是否为叶子节点的标志位 (默认字段为`leaf`,可通过`props.leaf`修改),否则会简单的以有无子节点来判断是否为叶子节点
 ```html
-<el-cascader
-  :options="options"
-  @active-item-change="handleItemChange"
-  :props="props"
-></el-cascader>
+<el-cascader :props="props"></el-cascader>
 
 <script>
+  let id = 0;
+
   export default {
     data() {
       return {
-        options: [{
-          label: '江苏',
-          cities: []
-        }, {
-          label: '浙江',
-          cities: []
-        }],
         props: {
-          value: 'label',
-          children: 'cities'
+          lazy: true,
+          lazyLoad (node, resolve) {
+            const { level } = node;
+            setTimeout(() => {
+              const nodes = Array.from({ length: level + 1 })
+                .map(item => ({
+                  value: ++id,
+                  label: `选项${id}`,
+                  leaf: level >= 2
+                }));
+              // 通过调用resolve将子节点数据返回,通知组件数据加载完成
+              resolve(nodes);
+            }, 1000);
+          }
         }
       };
-    },
-
-    methods: {
-      handleItemChange(val) {
-        console.log('active item:', val);
-        setTimeout(_ => {
-          if (val.indexOf('江苏') > -1 && !this.options[0].cities.length) {
-            this.options[0].cities = [{
-              label: '南京'
-            }];
-          } else if (val.indexOf('浙江') > -1 && !this.options[1].cities.length) {
-            this.options[1].cities = [{
-              label: '杭州'
-            }];
-          }
-        }, 300);
-      }
     }
   };
 </script>
@@ -1157,24 +1229,22 @@
 
 可以快捷地搜索选项并选择。
 
-:::demo 将`filterable`赋值为`true`即可打开搜索功能。
+:::demo 将`filterable`赋值为`true`即可打开搜索功能,默认会匹配节点的`label`或所有父节点的`label`(由`show-all-levels`决定)中包含输入值的选项。你也可以用`filter-method`自定义搜索逻辑,接受一个函数,第一个参数是节点`node`,第二个参数是搜索关键词`keyword`,通过返回布尔值表示是否命中
 ```html
 <div class="block">
-  <span class="demonstration">只可选择最后一级菜单的选项</span>
+  <span class="demonstration">单选可搜索</span>
   <el-cascader
     placeholder="试试搜索:指南"
     :options="options"
-    filterable
-  ></el-cascader>
+    filterable></el-cascader>
 </div>
 <div class="block">
-  <span class="demonstration">可选择任意一级菜单的选项</span>
+  <span class="demonstration">多选可搜索</span>
   <el-cascader
     placeholder="试试搜索:指南"
     :options="options"
-    filterable
-    change-on-select
-  ></el-cascader>
+    :props="{ multiple: true }"
+    filterable></el-cascader>
 </div>
 
 <script>
@@ -1383,38 +1453,503 @@
 ```
 :::
 
-### Attributes
+### 自定义节点内容
+
+可以自定义备选项的节点内容
+
+:::demo 可以通过`scoped slot`对级联选择器的备选项的节点内容进行自定义,scoped slot会传入两个字段 `node` 和 `data`,分别表示当前节点的 Node 对象和数据。
+```html
+<el-cascader :options="options">
+  <template slot-scope="{ node, data }">
+    <span>{{ data.label }}</span>
+    <span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
+  </template>
+</el-cascader>
+
+<script>
+  export default {
+    data() {
+      return {
+        options: [{
+          value: 'zhinan',
+          label: '指南',
+          children: [{
+            value: 'shejiyuanze',
+            label: '设计原则',
+            children: [{
+              value: 'yizhi',
+              label: '一致'
+            }, {
+              value: 'fankui',
+              label: '反馈'
+            }, {
+              value: 'xiaolv',
+              label: '效率'
+            }, {
+              value: 'kekong',
+              label: '可控'
+            }]
+          }, {
+            value: 'daohang',
+            label: '导航',
+            children: [{
+              value: 'cexiangdaohang',
+              label: '侧向导航'
+            }, {
+              value: 'dingbudaohang',
+              label: '顶部导航'
+            }]
+          }]
+        }, {
+          value: 'zujian',
+          label: '组件',
+          children: [{
+            value: 'basic',
+            label: 'Basic',
+            children: [{
+              value: 'layout',
+              label: 'Layout 布局'
+            }, {
+              value: 'color',
+              label: 'Color 色彩'
+            }, {
+              value: 'typography',
+              label: 'Typography 字体'
+            }, {
+              value: 'icon',
+              label: 'Icon 图标'
+            }, {
+              value: 'button',
+              label: 'Button 按钮'
+            }]
+          }, {
+            value: 'form',
+            label: 'Form',
+            children: [{
+              value: 'radio',
+              label: 'Radio 单选框'
+            }, {
+              value: 'checkbox',
+              label: 'Checkbox 多选框'
+            }, {
+              value: 'input',
+              label: 'Input 输入框'
+            }, {
+              value: 'input-number',
+              label: 'InputNumber 计数器'
+            }, {
+              value: 'select',
+              label: 'Select 选择器'
+            }, {
+              value: 'cascader',
+              label: 'Cascader 级联选择器'
+            }, {
+              value: 'switch',
+              label: 'Switch 开关'
+            }, {
+              value: 'slider',
+              label: 'Slider 滑块'
+            }, {
+              value: 'time-picker',
+              label: 'TimePicker 时间选择器'
+            }, {
+              value: 'date-picker',
+              label: 'DatePicker 日期选择器'
+            }, {
+              value: 'datetime-picker',
+              label: 'DateTimePicker 日期时间选择器'
+            }, {
+              value: 'upload',
+              label: 'Upload 上传'
+            }, {
+              value: 'rate',
+              label: 'Rate 评分'
+            }, {
+              value: 'form',
+              label: 'Form 表单'
+            }]
+          }, {
+            value: 'data',
+            label: 'Data',
+            children: [{
+              value: 'table',
+              label: 'Table 表格'
+            }, {
+              value: 'tag',
+              label: 'Tag 标签'
+            }, {
+              value: 'progress',
+              label: 'Progress 进度条'
+            }, {
+              value: 'tree',
+              label: 'Tree 树形控件'
+            }, {
+              value: 'pagination',
+              label: 'Pagination 分页'
+            }, {
+              value: 'badge',
+              label: 'Badge 标记'
+            }]
+          }, {
+            value: 'notice',
+            label: 'Notice',
+            children: [{
+              value: 'alert',
+              label: 'Alert 警告'
+            }, {
+              value: 'loading',
+              label: 'Loading 加载'
+            }, {
+              value: 'message',
+              label: 'Message 消息提示'
+            }, {
+              value: 'message-box',
+              label: 'MessageBox 弹框'
+            }, {
+              value: 'notification',
+              label: 'Notification 通知'
+            }]
+          }, {
+            value: 'navigation',
+            label: 'Navigation',
+            children: [{
+              value: 'menu',
+              label: 'NavMenu 导航菜单'
+            }, {
+              value: 'tabs',
+              label: 'Tabs 标签页'
+            }, {
+              value: 'breadcrumb',
+              label: 'Breadcrumb 面包屑'
+            }, {
+              value: 'dropdown',
+              label: 'Dropdown 下拉菜单'
+            }, {
+              value: 'steps',
+              label: 'Steps 步骤条'
+            }]
+          }, {
+            value: 'others',
+            label: 'Others',
+            children: [{
+              value: 'dialog',
+              label: 'Dialog 对话框'
+            }, {
+              value: 'tooltip',
+              label: 'Tooltip 文字提示'
+            }, {
+              value: 'popover',
+              label: 'Popover 弹出框'
+            }, {
+              value: 'card',
+              label: 'Card 卡片'
+            }, {
+              value: 'carousel',
+              label: 'Carousel 走马灯'
+            }, {
+              value: 'collapse',
+              label: 'Collapse 折叠面板'
+            }]
+          }]
+        }, {
+          value: 'ziyuan',
+          label: '资源',
+          children: [{
+            value: 'axure',
+            label: 'Axure Components'
+          }, {
+            value: 'sketch',
+            label: 'Sketch Templates'
+          }, {
+            value: 'jiaohu',
+            label: '组件交互文档'
+          }]
+        }]
+      }
+    }
+  }
+</script>
+```
+:::
+
+### 级联面板
+
+级联面板是级联选择器的核心组件,与级联选择器一样,有单选、多选、动态加载等多种功能。
+
+:::demo 和级联选择器一样,通过`options`来指定选项,也可通过`props`来设置多选、动态加载等功能,具体详情见下方API表格。
+```html
+<el-cascader-panel :options="options"></el-cascader-panel>
+
+<script>
+  export default {
+    data() {
+      return {
+        options: [{
+          value: 'zhinan',
+          label: '指南',
+          children: [{
+            value: 'shejiyuanze',
+            label: '设计原则',
+            children: [{
+              value: 'yizhi',
+              label: '一致'
+            }, {
+              value: 'fankui',
+              label: '反馈'
+            }, {
+              value: 'xiaolv',
+              label: '效率'
+            }, {
+              value: 'kekong',
+              label: '可控'
+            }]
+          }, {
+            value: 'daohang',
+            label: '导航',
+            children: [{
+              value: 'cexiangdaohang',
+              label: '侧向导航'
+            }, {
+              value: 'dingbudaohang',
+              label: '顶部导航'
+            }]
+          }]
+        }, {
+          value: 'zujian',
+          label: '组件',
+          children: [{
+            value: 'basic',
+            label: 'Basic',
+            children: [{
+              value: 'layout',
+              label: 'Layout 布局'
+            }, {
+              value: 'color',
+              label: 'Color 色彩'
+            }, {
+              value: 'typography',
+              label: 'Typography 字体'
+            }, {
+              value: 'icon',
+              label: 'Icon 图标'
+            }, {
+              value: 'button',
+              label: 'Button 按钮'
+            }]
+          }, {
+            value: 'form',
+            label: 'Form',
+            children: [{
+              value: 'radio',
+              label: 'Radio 单选框'
+            }, {
+              value: 'checkbox',
+              label: 'Checkbox 多选框'
+            }, {
+              value: 'input',
+              label: 'Input 输入框'
+            }, {
+              value: 'input-number',
+              label: 'InputNumber 计数器'
+            }, {
+              value: 'select',
+              label: 'Select 选择器'
+            }, {
+              value: 'cascader',
+              label: 'Cascader 级联选择器'
+            }, {
+              value: 'switch',
+              label: 'Switch 开关'
+            }, {
+              value: 'slider',
+              label: 'Slider 滑块'
+            }, {
+              value: 'time-picker',
+              label: 'TimePicker 时间选择器'
+            }, {
+              value: 'date-picker',
+              label: 'DatePicker 日期选择器'
+            }, {
+              value: 'datetime-picker',
+              label: 'DateTimePicker 日期时间选择器'
+            }, {
+              value: 'upload',
+              label: 'Upload 上传'
+            }, {
+              value: 'rate',
+              label: 'Rate 评分'
+            }, {
+              value: 'form',
+              label: 'Form 表单'
+            }]
+          }, {
+            value: 'data',
+            label: 'Data',
+            children: [{
+              value: 'table',
+              label: 'Table 表格'
+            }, {
+              value: 'tag',
+              label: 'Tag 标签'
+            }, {
+              value: 'progress',
+              label: 'Progress 进度条'
+            }, {
+              value: 'tree',
+              label: 'Tree 树形控件'
+            }, {
+              value: 'pagination',
+              label: 'Pagination 分页'
+            }, {
+              value: 'badge',
+              label: 'Badge 标记'
+            }]
+          }, {
+            value: 'notice',
+            label: 'Notice',
+            children: [{
+              value: 'alert',
+              label: 'Alert 警告'
+            }, {
+              value: 'loading',
+              label: 'Loading 加载'
+            }, {
+              value: 'message',
+              label: 'Message 消息提示'
+            }, {
+              value: 'message-box',
+              label: 'MessageBox 弹框'
+            }, {
+              value: 'notification',
+              label: 'Notification 通知'
+            }]
+          }, {
+            value: 'navigation',
+            label: 'Navigation',
+            children: [{
+              value: 'menu',
+              label: 'NavMenu 导航菜单'
+            }, {
+              value: 'tabs',
+              label: 'Tabs 标签页'
+            }, {
+              value: 'breadcrumb',
+              label: 'Breadcrumb 面包屑'
+            }, {
+              value: 'dropdown',
+              label: 'Dropdown 下拉菜单'
+            }, {
+              value: 'steps',
+              label: 'Steps 步骤条'
+            }]
+          }, {
+            value: 'others',
+            label: 'Others',
+            children: [{
+              value: 'dialog',
+              label: 'Dialog 对话框'
+            }, {
+              value: 'tooltip',
+              label: 'Tooltip 文字提示'
+            }, {
+              value: 'popover',
+              label: 'Popover 弹出框'
+            }, {
+              value: 'card',
+              label: 'Card 卡片'
+            }, {
+              value: 'carousel',
+              label: 'Carousel 走马灯'
+            }, {
+              value: 'collapse',
+              label: 'Collapse 折叠面板'
+            }]
+          }]
+        }, {
+          value: 'ziyuan',
+          label: '资源',
+          children: [{
+            value: 'axure',
+            label: 'Axure Components'
+          }, {
+            value: 'sketch',
+            label: 'Sketch Templates'
+          }, {
+            value: 'jiaohu',
+            label: '组件交互文档'
+          }]
+        }]
+      };
+    }
+  };
+</script>
+```
+:::
+
+### Cascader Attributes
 | 参数      | 说明    | 类型      | 可选值       | 默认值   |
 |---------- |-------- |---------- |-------------  |-------- |
-| value / v-model | 选中项绑定值 | array | — | — |
-| options | 可选项数据源,键名可通过 `props` 属性配置 | array | — | — |
+| value / v-model | 选中项绑定值 | - | — | — |
+| options | 可选项数据源,键名可通过 `Props` 属性配置 | array | — | — |
 | props | 配置选项,具体见下表 | object | — | — |
-| separator | 选项分隔符 | string | — | 斜杠'/' |
-| popper-class | 自定义浮层类名   | string | —  | — |
+| size | 尺寸 | string | medium / small / mini | — |
 | placeholder | 输入框占位文本 | string | — | 请选择 |
 | disabled | 是否禁用 | boolean | — | false |
 | clearable | 是否支持清空选项 | boolean | — | false |
-| expand-trigger | 次级菜单的展开方式 | string | click / hover | click |
 | show-all-levels | 输入框中是否显示选中值的完整路径 | boolean | — | true |
+| collapse-tags | 多选模式下是否折叠Tag | boolean | - | false |
+| separator | 选项分隔符 | string | — | 斜杠' / ' |
 | filterable | 是否可搜索选项 | boolean | — | — |
+| filter-method | 自定义搜索逻辑,第一个参数是节点`node`,第二个参数是搜索关键词`keyword`,通过返回布尔值表示是否命中 | function(node, keyword) | - | - |
 | debounce | 搜索关键词输入的去抖延迟,毫秒 | number | — | 300 |
-| change-on-select | 是否允许选择任意一级的选项 | boolean | — | false |
-| size | 尺寸 | string | medium / small / mini | — |
 | before-filter | 筛选之前的钩子,参数为输入的值,若返回 false 或者返回 Promise 且被 reject,则停止筛选 | function(value) | — | — |
+| popper-class | 自定义浮层类名   | string | —  | — |
 
-### props
-| 参数     | 说明              | 类型   | 可选值 | 默认值 |
-| -------- | ----------------- | ------ | ------ | ------ |
-| value    | 指定选项的值为选项对象的某个属性值 | string | — | — |
-| label    | 指定选项标签为选项对象的某个属性值 | string | — | — |
-| children | 指定选项的子选项为选项对象的某个属性值 | string | — | — |
-| disabled | 指定选项的禁用为选项对象的某个属性值 | string | — | — |
-
-### Events
+### Cascader Events
 | 事件名称      | 说明    | 回调参数      |
 |---------- |-------- |---------- |
-| change | 当绑定值变化时触发的事件 | 当前值 |
-| active-item-change | 当父级选项变化时触发的事件,仅在 `change-on-select` 为 `false` 时可用 | 各父级选项组成的数组 |
-| blur | 在 Cascader 失去焦点时触发 | (event: Event) |
-| focus | 在 Cascader 获得焦点时触发 | (event: Event) |
+| change | 当选中节点变化时触发 | 选中节点的值 |
+| expand-change | 当展开节点发生变化时触发 | 各父级选项值组成的数组 |
+| blur | 当失去焦点时触发 | (event: Event) |
+| focus | 当获得焦点时触发 | (event: Event) |
 | visible-change | 下拉框出现/隐藏时触发 | 出现则为 true,隐藏则为 false |
+| remove-tag | 在多选模式下,移除Tag时触发 | 移除的Tag对应的节点的值 |
+
+### Cascader Slots
+| 名称     | 说明 |
+|---------|-------------|
+| - | 自定义备选项的节点内容,参数为 { node, data },分别为当前节点的 Node 对象和数据 |
+| empty  | 无匹配选项时的内容 |
+
+### CascaderPanel Attributes
+| 参数      | 说明    | 类型      | 可选值       | 默认值   |
+|---------- |-------- |---------- |-------------  |-------- |
+| value / v-model | 选中项绑定值 | - | — | — |
+| options | 可选项数据源,键名可通过 `Props` 属性配置 | array | — | — |
+| props | 配置选项,具体见下表 | object | — | — |
+
+### CascaderPanel Events
+| 事件名称      | 说明    | 回调参数      |
+|---------- |-------- |---------- |
+| change | 当选中节点变化时触发 | 选中节点的值 |
+| expand-change | 当展开节点发生变化时触发 | 各父级选项值组成的数组 |
+
+### CascaderPanel Slots
+| 名称     | 说明 |
+|---------|-------------|
+| - | 自定义备选项的节点内容,参数为 { node, data },分别为当前节点的 Node 对象和数据 |
+
+### Props
+| 参数     | 说明              | 类型   | 可选值 | 默认值 |
+| -------- | ----------------- | ------ | ------ | ------ |
+| expandTrigger | 次级菜单的展开方式 | string | click / hover | 'click' |
+| multiple | 是否多选 | boolean | - | false |
+| checkStrictly | 是否严格的遵守父子节点不互相关联 | boolean | - | false |
+| emitPath | 在选中节点改变时,是否返回由该节点所在的各级菜单的值所组成的数组,若设置 false,则只返回该节点的值 | boolean | - | true |
+| lazy | 是否动态加载子节点,需与 lazyLoad 方法结合使用 | boolean | - | false |
+| lazyLoad | 加载动态数据的方法,仅在 lazy 为 true 时有效 | function(node, resolve),`node`为当前点击的节点,`resolve`为数据加载完成的回调(必须调用) | - | - |
+| value    | 指定选项的值为选项对象的某个属性值 | string | — | 'value' |
+| label    | 指定选项标签为选项对象的某个属性值 | string | — | 'label' |
+| children | 指定选项的子选项为选项对象的某个属性值 | string | — | 'children' |
+| disabled | 指定选项的禁用为选项对象的某个属性值 | string | — | 'disabled' |
+| leaf     | 指定选项的叶子节点的标志位为选项对象的某个属性值 | string | — | 'leaf' |

+ 8 - 0
packages/cascader-panel/index.js

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

+ 138 - 0
packages/cascader-panel/src/cascader-menu.vue

@@ -0,0 +1,138 @@
+<script>
+import ElScrollbar from 'element-ui/packages/scrollbar';
+import CascaderNode from './cascader-node.vue';
+import Locale from 'element-ui/src/mixins/locale';
+import { generateId } from 'element-ui/src/utils/util';
+
+export default {
+  name: 'ElCascaderMenu',
+
+  mixins: [Locale],
+
+  inject: ['panel'],
+
+  components: {
+    ElScrollbar,
+    CascaderNode
+  },
+
+  props: {
+    nodes: {
+      type: Array,
+      required: true
+    },
+    index: Number
+  },
+
+  data() {
+    return {
+      activeNode: null,
+      hoverTimer: null,
+      id: generateId()
+    };
+  },
+
+  computed: {
+    isEmpty() {
+      return !this.nodes.length;
+    },
+    menuId() {
+      return `cascader-menu-${this.id}-${this.index}`;
+    }
+  },
+
+  methods: {
+    handleExpand(e) {
+      this.activeNode = e.target;
+    },
+    handleMouseMove(e) {
+      const { activeNode, hoverTimer } = this;
+      const { hoverZone } = this.$refs;
+
+      if (!activeNode || !hoverZone) return;
+
+      if (activeNode.contains(e.target)) {
+        clearTimeout(hoverTimer);
+
+        const { left } = this.$el.getBoundingClientRect();
+        const startX = e.clientX - left;
+        const { offsetWidth, offsetHeight } = this.$el;
+        const top = activeNode.offsetTop;
+        const bottom = top + activeNode.offsetHeight;
+
+        hoverZone.innerHTML = `
+          <path style="pointer-events: auto;" fill="transparent" d="M${startX} ${top} L${offsetWidth} 0 V${top} Z" />
+          <path style="pointer-events: auto;" fill="transparent" d="M${startX} ${bottom} L${offsetWidth} ${offsetHeight} V${bottom} Z" />
+        `;
+      } else if (!hoverTimer) {
+        this.hoverTimer = setTimeout(this.clearHoverZone, this.panel.config.hoverThreshold);
+      }
+    },
+    clearHoverZone() {
+      const { hoverZone } = this.$refs;
+      if (!hoverZone) return;
+      hoverZone.innerHTML = '';
+    },
+
+    renderEmptyText(h) {
+      return (
+        <div class="el-cascader-menu__empty-text">{ this.t('el.cascader.noData') }</div>
+      );
+    },
+    renderNodeList(h) {
+      const { menuId } = this;
+      const { isHoverMenu } = this.panel;
+      const events = { on: {} };
+
+      if (isHoverMenu) {
+        events.on.expand = this.handleExpand;
+      }
+
+      const nodes = this.nodes.map((node, index) => {
+        const { hasChildren } = node;
+        return (
+          <cascader-node
+            key={ node.uid }
+            node={ node }
+            node-id={ `${menuId}-${index}` }
+            aria-haspopup={ hasChildren }
+            aria-owns = { hasChildren ? menuId : null }
+            { ...events }></cascader-node>
+        );
+      });
+
+      return [
+        ...nodes,
+        isHoverMenu ? <svg ref='hoverZone' class='el-cascader-menu__hover-zone'></svg> : null
+      ];
+    }
+  },
+
+  render(h) {
+    const { isEmpty, menuId } = this;
+    const events = { nativeOn: {} };
+
+    // optimize hover to expand experience (#8010)
+    if (this.panel.isHoverMenu) {
+      events.nativeOn.mousemove = this.handleMouseMove;
+      // events.nativeOn.mouseleave = this.clearHoverZone;
+    }
+
+    return (
+      <el-scrollbar
+        tag="ul"
+        role="menu"
+        id={ menuId }
+        class="el-cascader-menu"
+        wrap-class="el-cascader-menu__wrap"
+        view-class={{
+          'el-cascader-menu__list': true,
+          'is-empty': isEmpty
+        }}
+        { ...events }>
+        { isEmpty ? this.renderEmptyText(h) : this.renderNodeList(h) }
+      </el-scrollbar>
+    );
+  }
+};
+</script>

+ 246 - 0
packages/cascader-panel/src/cascader-node.vue

@@ -0,0 +1,246 @@
+<script>
+  import ElCheckbox from 'element-ui/packages/checkbox';
+  import ElRadio from 'element-ui/packages/radio';
+  import { isEqual } from 'element-ui/src/utils/util';
+
+  const stopPropagation = e => e.stopPropagation();
+
+  export default {
+    inject: ['panel'],
+
+    components: {
+      ElCheckbox,
+      ElRadio
+    },
+
+    props: {
+      node: {
+        required: true
+      },
+      nodeId: String
+    },
+
+    computed: {
+      config() {
+        return this.panel.config;
+      },
+      isLeaf() {
+        return this.node.isLeaf;
+      },
+      isDisabled() {
+        return this.node.isDisabled;
+      },
+      checkedValue() {
+        return this.panel.checkedValue;
+      },
+      isChecked() {
+        return this.node.isSameNode(this.checkedValue);
+      },
+      inActivePath() {
+        return this.isInPath(this.panel.activePath);
+      },
+      inCheckedPath() {
+        if (!this.config.checkStrictly) return false;
+
+        return this.panel.checkedNodePaths
+          .some(checkedPath => this.isInPath(checkedPath));
+      },
+      value() {
+        return this.node.getValueByOption();
+      }
+    },
+
+    methods: {
+      handleExpand() {
+        const { panel, node, isDisabled, config } = this;
+        const { multiple, checkStrictly } = config;
+
+        if (!checkStrictly && isDisabled || node.loading) return;
+
+        if (config.lazy && !node.loaded) {
+          panel.lazyLoad(node, () => {
+            // do not use cached leaf value here, invoke this.isLeaf to get new value.
+            const { isLeaf } = this;
+
+            if (!isLeaf) this.handleExpand();
+            if (multiple) {
+              // if leaf sync checked state, else clear checked state
+              const checked = isLeaf ? node.checked : false;
+              this.handleMultiCheckChange(checked);
+            }
+          });
+        } else {
+          panel.handleExpand(node);
+        }
+      },
+
+      handleCheckChange() {
+        const { panel, value } = this;
+        panel.handleCheckChange(value);
+      },
+
+      handleMultiCheckChange(checked) {
+        this.node.doCheck(checked);
+        this.panel.calculateMultiCheckedValue();
+      },
+
+      isInPath(pathNodes) {
+        const { node } = this;
+        const selectedPathNode = pathNodes[node.level - 1] || {};
+        return selectedPathNode.uid === node.uid;
+      },
+
+      renderPrefix(h) {
+        const { isLeaf, isChecked, config } = this;
+        const { checkStrictly, multiple } = config;
+
+        if (multiple) {
+          return this.renderCheckbox(h);
+        } else if (checkStrictly) {
+          return this.renderRadio(h);
+        } else if (isLeaf && isChecked) {
+          return this.renderCheckIcon(h);
+        }
+
+        return null;
+      },
+
+      renderPostfix(h) {
+        const { node, isLeaf } = this;
+
+        if (node.loading) {
+          return this.renderLoadingIcon(h);
+        } else if (!isLeaf) {
+          return this.renderExpandIcon(h);
+        }
+
+        return null;
+      },
+
+      renderCheckbox(h) {
+        const { node, config, isDisabled } = this;
+        const events = {
+          on: { change: this.handleMultiCheckChange },
+          nativeOn: {}
+        };
+
+        if (config.checkStrictly) { // when every node is selectable, click event should not trigger expand event.
+          events.nativeOn.click = stopPropagation;
+        }
+
+        return (
+          <el-checkbox
+            value={ node.checked }
+            indeterminate={ node.indeterminate }
+            disabled={ isDisabled }
+            { ...events }
+          ></el-checkbox>
+        );
+      },
+
+      renderRadio(h) {
+        let { checkedValue, value, isDisabled } = this;
+
+        // to keep same reference if value cause radio's checked state is calculated by reference comparision;
+        if (isEqual(value, checkedValue)) {
+          value = checkedValue;
+        }
+
+        return (
+          <el-radio
+            value={ checkedValue }
+            label={ value }
+            disabled={ isDisabled }
+            onChange={ this.handleCheckChange }
+            nativeOnClick={ stopPropagation }>
+            {/* add an empty element to avoid render label */}
+            <span></span>
+          </el-radio>
+        );
+      },
+
+      renderCheckIcon(h) {
+        return (
+          <i class="el-icon-check el-cascader-node__prefix"></i>
+        );
+      },
+
+      renderLoadingIcon(h) {
+        return (
+          <i class="el-icon-loading el-cascader-node__postfix"></i>
+        );
+      },
+
+      renderExpandIcon(h) {
+        return (
+          <i class="el-icon-arrow-right el-cascader-node__postfix"></i>
+        );
+      },
+
+      renderContent(h) {
+        const { panel, node } = this;
+        const render = panel.renderLabelFn;
+        const vnode = render
+          ? render({ node, data: node.data })
+          : null;
+
+        return (
+          <span class="el-cascader-node__label">{ vnode || node.label }</span>
+        );
+      }
+    },
+
+    render(h) {
+      const {
+        inActivePath,
+        inCheckedPath,
+        isChecked,
+        isLeaf,
+        isDisabled,
+        config,
+        nodeId
+      } = this;
+      const { expandTrigger, checkStrictly, multiple } = config;
+      const disabled = !checkStrictly && isDisabled;
+      const events = { on: {} };
+
+      if (!isLeaf) {
+        if (expandTrigger === 'click') {
+          events.on.click = this.handleExpand;
+        } else {
+          events.on.mouseenter = e => {
+            this.handleExpand();
+            this.$emit('expand', e);
+          };
+          events.on.focus = e => {
+            this.handleExpand();
+            this.$emit('expand', e);
+          };
+        }
+      } else if (!isDisabled && !checkStrictly && !multiple) {
+        events.on.click = this.handleCheckChange;
+      }
+
+      return (
+        <li
+          role="menuitem"
+          id={ nodeId }
+          aria-expanded={ inActivePath }
+          tabindex={ disabled ? null : -1 }
+          class={{
+            'el-cascader-node': true,
+            'is-selectable': checkStrictly,
+            'in-active-path': inActivePath,
+            'in-checked-path': inCheckedPath,
+            'is-active': isChecked,
+            'is-disabled': disabled
+          }}
+          {...events}>
+          { this.renderPrefix(h) }
+          { this.renderContent(h) }
+          { this.renderPostfix(h) }
+        </li>
+      );
+    }
+  };
+</script>

+ 354 - 0
packages/cascader-panel/src/cascader-panel.vue

@@ -0,0 +1,354 @@
+<template>
+  <div
+    :class="[
+      'el-cascader-panel',
+      border && 'is-bordered'
+    ]"
+    @keydown="handleKeyDown">
+    <cascader-menu
+      ref="menu"
+      v-for="(menu, index) in menus"
+      :index="index"
+      :key="index"
+      :nodes="menu"></cascader-menu>
+  </div>
+</template>
+
+<script>
+import CascaderMenu from './cascader-menu';
+import Store from './store';
+import merge from 'element-ui/src/utils/merge';
+import AriaUtils from 'element-ui/src/utils/aria-utils';
+import scrollIntoView from 'element-ui/src/utils/scroll-into-view';
+import {
+  noop,
+  coerceTruthyValueToArray,
+  isEqual,
+  isEmpty,
+  valueEquals
+} from 'element-ui/src/utils/util';
+
+const { keys: KeyCode } = AriaUtils;
+const DefaultProps = {
+  expandTrigger: 'click', // or hover
+  multiple: false,
+  checkStrictly: false, // whether all nodes can be selected
+  emitPath: true, // wether to emit an array of all levels value in which node is located
+  lazy: false,
+  lazyLoad: noop,
+  value: 'value',
+  label: 'label',
+  children: 'children',
+  leaf: 'leaf',
+  disabled: 'disabled',
+  hoverThreshold: 500
+};
+
+const isLeaf = el => !el.getAttribute('aria-owns');
+
+const getSibling = (el, distance) => {
+  const { parentNode } = el;
+  if (parentNode) {
+    const siblings = parentNode.querySelectorAll('.el-cascader-node[tabindex="-1"]');
+    const index = Array.prototype.indexOf.call(siblings, el);
+    return siblings[index + distance] || null;
+  }
+  return null;
+};
+
+const getMenuIndex = (el, distance) => {
+  if (!el) return;
+  const pieces = el.id.split('-');
+  return Number(pieces[pieces.length - 2]);
+};
+
+const focusNode = el => {
+  if (!el) return;
+  el.focus();
+  !isLeaf(el) && el.click();
+};
+
+const checkNode = el => {
+  if (!el) return;
+
+  const input = el.querySelector('input');
+  if (input) {
+    input.click();
+  } else if (isLeaf(el)) {
+    el.click();
+  }
+};
+
+export default {
+  name: 'ElCascaderPanel',
+
+  components: {
+    CascaderMenu
+  },
+
+  props: {
+    value: {},
+    options: Array,
+    props: Object,
+    border: {
+      type: Boolean,
+      default: true
+    },
+    renderLabel: Function
+  },
+
+  provide() {
+    return {
+      panel: this
+    };
+  },
+
+  data() {
+    return {
+      checkedValue: null,
+      checkedNodePaths: [],
+      store: [],
+      menus: [],
+      activePath: []
+    };
+  },
+
+  computed: {
+    config() {
+      return merge({ ...DefaultProps }, this.props || {});
+    },
+    multiple() {
+      return this.config.multiple;
+    },
+    checkStrictly() {
+      return this.config.checkStrictly;
+    },
+    leafOnly() {
+      return !this.checkStrictly;
+    },
+    isHoverMenu() {
+      return this.config.expandTrigger === 'hover';
+    },
+    renderLabelFn() {
+      return this.renderLabel || this.$scopedSlots.default;
+    }
+  },
+
+  watch: {
+    options: {
+      handler: function() {
+        this.initStore();
+      },
+      immediate: true,
+      deep: true
+    },
+    value() {
+      this.syncCheckedValue();
+      this.checkStrictly && this.calculateCheckedNodePaths();
+    },
+    checkedValue(val) {
+      if (!isEqual(val, this.value)) {
+        this.checkStrictly && this.calculateCheckedNodePaths();
+        this.$emit('input', val);
+        this.$emit('change', val);
+      }
+    }
+  },
+
+  mounted() {
+    if (!isEmpty(this.value)) {
+      this.syncCheckedValue();
+    }
+  },
+
+  methods: {
+    initStore() {
+      const { config, options } = this;
+      if (config.lazy && isEmpty(options)) {
+        this.lazyLoad();
+      } else {
+        this.store = new Store(options, config);
+        this.menus = [this.store.getNodes()];
+        this.syncMenuState();
+      }
+    },
+    syncCheckedValue() {
+      const { value, checkedValue } = this;
+      if (!isEqual(value, checkedValue)) {
+        this.checkedValue = value;
+        this.syncMenuState();
+      }
+    },
+    syncMenuState() {
+      const { multiple, checkStrictly } = this;
+      this.syncActivePath();
+      multiple && this.syncMultiCheckState();
+      checkStrictly && this.calculateCheckedNodePaths();
+      this.$nextTick(this.scrollIntoView);
+    },
+    syncMultiCheckState() {
+      const nodes = this.getFlattedNodes(this.leafOnly);
+
+      nodes.forEach(node => {
+        node.syncCheckState(this.checkedValue);
+      });
+    },
+    syncActivePath() {
+      let { checkedValue, store, multiple } = this;
+      if (isEmpty(checkedValue)) {
+        this.activePath = [];
+        this.menus = [store.getNodes()];
+      } else {
+        checkedValue = multiple ? checkedValue[0] : checkedValue;
+        const checkedNode = this.getNodeByValue(checkedValue) || {};
+        const nodes = [];
+        let { parent } = checkedNode;
+        while (parent) {
+          nodes.unshift(parent);
+          parent = parent.parent;
+        }
+        nodes.forEach(node => this.handleExpand(node, true /* silent */));
+      }
+    },
+    calculateCheckedNodePaths() {
+      const { checkedValue, multiple } = this;
+      const checkedValues = multiple
+        ? coerceTruthyValueToArray(checkedValue)
+        : [ checkedValue ];
+      this.checkedNodePaths = checkedValues.map(v => {
+        const checkedNode = this.getNodeByValue(v);
+        return checkedNode ? checkedNode.pathNodes : [];
+      });
+    },
+    handleKeyDown(e) {
+      const { target, keyCode } = e;
+
+      switch (keyCode) {
+        case KeyCode.up:
+          const prev = getSibling(target, -1);
+          focusNode(prev);
+          break;
+        case KeyCode.down:
+          const next = getSibling(target, 1);
+          focusNode(next);
+          break;
+        case KeyCode.left:
+          const preMenu = this.$refs.menu[getMenuIndex(target) - 1];
+          if (preMenu) {
+            const expandedNode = preMenu.$el.querySelector('.el-cascader-node[aria-expanded="true"]');
+            focusNode(expandedNode);
+          }
+          break;
+        case KeyCode.right:
+          const nextMenu = this.$refs.menu[getMenuIndex(target) + 1];
+          if (nextMenu) {
+            const firstNode = nextMenu.$el.querySelector('.el-cascader-node[tabindex="-1"]');
+            focusNode(firstNode);
+          }
+          break;
+        case KeyCode.enter:
+          checkNode(target);
+          break;
+        case KeyCode.esc:
+        case KeyCode.tab:
+          this.$emit('close');
+          break;
+        default:
+          return;
+      }
+    },
+    handleExpand(node, silent) {
+      const { level } = node;
+      const path = this.activePath.slice(0, level - 1);
+      const menus = this.menus.slice(0, level);
+
+      if (!node.isLeaf) {
+        path.push(node);
+        menus.push(node.children);
+      }
+
+      if (valueEquals(path, this.activePath)) return;
+
+      this.activePath = path;
+      this.menus = menus;
+
+      if (!silent) {
+        const pathValues = path.map(node => node.getValue());
+        this.$emit('active-item-change', pathValues); // Deprecated
+        this.$emit('expand-change', pathValues);
+      }
+    },
+    handleCheckChange(value) {
+      this.checkedValue = value;
+    },
+    lazyLoad(node, onFullfiled) {
+      const { config } = this;
+      if (!node) {
+        node = node || { root: true, level: 0 };
+        this.store = new Store([], config);
+        this.menus = [this.store.getNodes()];
+      }
+      node.loading = true;
+      const resolve = dataList => {
+        const parent = node.root ? null : node;
+        dataList && dataList.length && this.store.appendNodes(dataList, parent);
+        node.loading = false;
+        node.loaded = true;
+        onFullfiled && onFullfiled(dataList);
+      };
+      config.lazyLoad(node, resolve);
+    },
+    /**
+     * public methods
+    */
+    calculateMultiCheckedValue() {
+      this.checkedValue = this.getCheckedNodes(this.leafOnly)
+        .map(node => node.getValueByOption());
+    },
+    scrollIntoView() {
+      if (this.$isServer) return;
+
+      const menus = this.$refs.menu || [];
+      menus.forEach(menu => {
+        const menuElement = menu.$el;
+        if (menuElement) {
+          const container = menuElement.querySelector('.el-scrollbar__wrap');
+          const activeNode = menuElement.querySelector('.el-cascader-node.is-active') ||
+            menuElement.querySelector('.el-cascader-node.in-active-path');
+          scrollIntoView(container, activeNode);
+        }
+      });
+    },
+    getNodeByValue(val) {
+      return this.store.getNodeByValue(val);
+    },
+    getFlattedNodes(leafOnly) {
+      const cached = !this.config.lazy;
+      return this.store.getFlattedNodes(leafOnly, cached);
+    },
+    getCheckedNodes(leafOnly) {
+      const { checkedValue, multiple } = this;
+      if (multiple) {
+        const nodes = this.getFlattedNodes(leafOnly);
+        return nodes.filter(node => node.checked);
+      } else {
+        return isEmpty(checkedValue)
+          ? []
+          : [this.getNodeByValue(checkedValue)];
+      }
+    },
+    clearCheckedNodes() {
+      const { config, leafOnly } = this;
+      const { multiple, emitPath } = config;
+      if (multiple) {
+        this.getCheckedNodes(leafOnly)
+          .filter(node => !node.isDisabled)
+          .forEach(node => node.doCheck(false));
+        this.calculateMultiCheckedValue();
+      } else {
+        this.checkedValue = emitPath ? [] : null;
+      }
+    }
+  }
+};
+</script>

+ 166 - 0
packages/cascader-panel/src/node.js

@@ -0,0 +1,166 @@
+import { isEqual, capitalize } from 'element-ui/src/utils/util';
+import { isDef } from 'element-ui/src/utils/shared';
+
+let uid = 0;
+
+export default class Node {
+
+  constructor(data, config, parentNode) {
+    this.data = data;
+    this.config = config;
+    this.parent = parentNode || null;
+    this.level = !this.parent ? 1 : this.parent.level + 1;
+    this.uid = uid++;
+
+    this.initState();
+    this.initChildren();
+  }
+
+  initState() {
+    const { value: valueKey, label: labelKey } = this.config;
+
+    this.value = this.data[valueKey];
+    this.label = this.data[labelKey];
+    this.pathNodes = this.calculatePathNodes();
+    this.path = this.pathNodes.map(node => node.value);
+    this.pathLabels = this.pathNodes.map(node => node.label);
+
+    // lazy load
+    this.loading = false;
+    this.loaded = false;
+  }
+
+  initChildren() {
+    const { config } = this;
+    const childrenKey = config.children;
+    const childrenData = this.data[childrenKey];
+    this.hasChildren = Array.isArray(childrenData);
+    this.children = (childrenData || []).map(child => new Node(child, config, this));
+  }
+
+  get isDisabled() {
+    const { data, parent, config } = this;
+    const disabledKey = config.disabled;
+    const { checkStrictly } = config;
+    return data[disabledKey] ||
+      !checkStrictly && parent && parent.isDisabled;
+  }
+
+  get isLeaf() {
+    const { data, loaded, hasChildren, children } = this;
+    const { lazy, leaf: leafKey } = this.config;
+    if (lazy) {
+      const isLeaf = isDef(data[leafKey])
+        ? data[leafKey]
+        : (loaded ? !children.length : false);
+      this.hasChildren = !isLeaf;
+      return isLeaf;
+    }
+    return !hasChildren;
+  }
+
+  calculatePathNodes() {
+    const nodes = [this];
+    let parent = this.parent;
+
+    while (parent) {
+      nodes.unshift(parent);
+      parent = parent.parent;
+    }
+
+    return nodes;
+  }
+
+  getPath() {
+    return this.path;
+  }
+
+  getValue() {
+    return this.value;
+  }
+
+  getValueByOption() {
+    return this.config.emitPath
+      ? this.getPath()
+      : this.getValue();
+  }
+
+  getText(allLevels, separator) {
+    return allLevels ? this.pathLabels.join(separator) : this.label;
+  }
+
+  isSameNode(checkedValue) {
+    const value = this.getValueByOption();
+    return this.config.multiple && Array.isArray(checkedValue)
+      ? checkedValue.some(val => isEqual(val, value))
+      : isEqual(checkedValue, value);
+  }
+
+  broadcast(event, ...args) {
+    const handlerName = `onParent${capitalize(event)}`;
+
+    this.children.forEach(child => {
+      if (child) {
+        // bottom up
+        child.broadcast(event, ...args);
+        child[handlerName] && child[handlerName](...args);
+      }
+    });
+  }
+
+  emit(event, ...args) {
+    const { parent } = this;
+    const handlerName = `onChild${capitalize(event)}`;
+    if (parent) {
+      parent[handlerName] && parent[handlerName](...args);
+      parent.emit(event, ...args);
+    }
+  }
+
+  onParentCheck(checked) {
+    if (!this.isDisabled) {
+      this.setCheckState(checked);
+    }
+  }
+
+  onChildCheck() {
+    const { children } = this;
+    const validChildren = children.filter(child => !child.isDisabled);
+    const checked = validChildren.length
+      ? validChildren.every(child => child.checked)
+      : false;
+
+    this.setCheckState(checked);
+  }
+
+  setCheckState(checked) {
+    const totalNum = this.children.length;
+    const checkedNum = this.children.reduce((c, p) => {
+      const num = p.checked ? 1 : (p.indeterminate ? 0.5 : 0);
+      return c + num;
+    }, 0);
+
+    this.checked = checked;
+    this.indeterminate = checkedNum !== totalNum && checkedNum > 0;
+  }
+
+  syncCheckState(checkedValue) {
+    const value = this.getValueByOption();
+    const checked = this.isSameNode(checkedValue, value);
+
+    this.doCheck(checked);
+  }
+
+  doCheck(checked) {
+    if (this.checked !== checked) {
+      if (this.config.checkStrictly) {
+        this.checked = checked;
+      } else {
+        // bottom up to unify the calculation of the indeterminate state
+        this.broadcast('check', checked);
+        this.setCheckState(checked);
+        this.emit('check');
+      }
+    }
+  }
+}

+ 63 - 0
packages/cascader-panel/src/store.js

@@ -0,0 +1,63 @@
+import Node from './node';
+import { coerceTruthyValueToArray } from 'element-ui/src/utils/util';
+
+const flatNodes = (data, leafOnly) => {
+  return data.reduce((res, node) => {
+    if (node.isLeaf) {
+      res.push(node);
+    } else {
+      !leafOnly && res.push(node);
+      res = res.concat(flatNodes(node.children, leafOnly));
+    }
+    return res;
+  }, []);
+};
+
+export default class Store {
+
+  constructor(data, config) {
+    this.config = config;
+    this.initNodes(data);
+  }
+
+  initNodes(data) {
+    data = coerceTruthyValueToArray(data);
+    this.nodes = data.map(nodeData => new Node(nodeData, this.config));
+    this.flattedNodes = this.getFlattedNodes(false, false);
+    this.leafNodes = this.getFlattedNodes(true, false);
+  }
+
+  appendNode(nodeData, parentNode) {
+    const node = new Node(nodeData, this.config, parentNode);
+    const children = parentNode ? parentNode.children : this.nodes;
+
+    children.push(node);
+  }
+
+  appendNodes(nodeDataList, parentNode) {
+    nodeDataList = coerceTruthyValueToArray(nodeDataList);
+    nodeDataList.forEach(nodeData => this.appendNode(nodeData, parentNode));
+  }
+
+  getNodes() {
+    return this.nodes;
+  }
+
+  getFlattedNodes(leafOnly, cached = true) {
+    const cachedNodes = leafOnly ? this.leafNodes : this.flattedNodes;
+    return cached
+      ? cachedNodes
+      : flatNodes(this.nodes, leafOnly);
+  }
+
+  getNodeByValue(value) {
+    if (value) {
+      value = Array.isArray(value) ? value[value.length - 1] : value;
+      const nodes = this.getFlattedNodes(false, !this.config.lazy)
+        .filter(node => node.value === value);
+      return nodes && nodes.length ? nodes[0] : null;
+    }
+    return null;
+  }
+
+}

+ 1 - 1
packages/cascader/index.js

@@ -1,4 +1,4 @@
-import Cascader from './src/main';
+import Cascader from './src/cascader';
 
 /* istanbul ignore next */
 Cascader.install = function(Vue) {

+ 639 - 0
packages/cascader/src/cascader.vue

@@ -0,0 +1,639 @@
+<template>
+  <div
+    ref="reference"
+    :class="[
+      'el-cascader',
+      realSize && `el-cascader--${realSize}`,
+      { 'is-disabled': isDisabled }
+    ]"
+    v-clickoutside="() => toggleDropDownVisible(false)"
+    @mouseenter="inputHover = true"
+    @mouseleave="inputHover = false"
+    @click="() => toggleDropDownVisible(readonly ? undefined : true)"
+    @keydown="handleKeyDown">
+
+    <el-input
+      ref="input"
+      v-model="multiple ? presentText : inputValue"
+      :size="realSize"
+      :placeholder="placeholder"
+      :readonly="readonly"
+      :disabled="isDisabled"
+      :validate-event="false"
+      :class="{ 'is-focus': dropDownVisible }"
+      @focus="handleFocus"
+      @blur="handleBlur"
+      @input="handleInput">
+      <template slot="suffix">
+        <i
+          v-if="clearBtnVisible"
+          key="clear"
+          class="el-input__icon el-icon-circle-close"
+          @click.stop="handleClear"></i>
+        <i
+          v-else
+          key="arrow-down"
+          :class="[
+            'el-input__icon',
+            'el-icon-arrow-down',
+            dropDownVisible && 'is-reverse'
+          ]"
+          @click.stop="toggleDropDownVisible()"></i>
+      </template>
+    </el-input>
+
+    <div v-if="multiple" class="el-cascader__tags">
+      <el-tag
+        v-for="(tag, index) in presentTags"
+        :key="tag.key"
+        type="info"
+        :size="tagSize"
+        :hit="tag.hitState"
+        :closable="tag.closable"
+        disable-transitions
+        @close="deleteTag(index)">
+        <span>{{ tag.text }}</span>
+      </el-tag>
+      <input
+        v-if="filterable && !isDisabled"
+        v-model.trim="inputValue"
+        type="text"
+        class="el-cascader__search-input"
+        :placeholder="presentTags.length ? '' : placeholder"
+        @input="e => handleInput(inputValue, e)"
+        @click.stop="toggleDropDownVisible(true)"
+        @keydown.delete="handleDelete">
+    </div>
+
+    <transition name="el-zoom-in-top" @after-leave="handleDropdownLeave">
+      <div
+        v-show="dropDownVisible"
+        ref="popper"
+        :class="['el-popper', 'el-cascader__dropdown', popperClass]">
+        <el-cascader-panel
+          ref="panel"
+          v-show="!filtering"
+          v-model="checkedValue"
+          :options="options"
+          :props="config"
+          :border="false"
+          :render-label="$scopedSlots.default"
+          @expand-change="handleExpandChange"
+          @close="toggleDropDownVisible(false)"></el-cascader-panel>
+        <el-scrollbar
+          ref="suggestionPanel"
+          v-if="filterable"
+          v-show="filtering"
+          tag="ul"
+          class="el-cascader__suggestion-panel"
+          view-class="el-cascader__suggestion-list"
+          @keydown.native="handleSuggestionKeyDown">
+          <template v-if="suggestions.length">
+            <li
+              v-for="(item, index) in suggestions"
+              :key="item.uid"
+              :class="[
+                'el-cascader__suggestion-item',
+                item.checked && 'is-checked'
+              ]"
+              :tabindex="-1"
+              @click="handleSuggestionClick(index)">
+              <span>{{ item.text }}</span>
+              <i v-if="item.checked" class="el-icon-check"></i>
+            </li>
+          </template>
+          <slot v-else name="empty">
+            <li class="el-cascader__empty-text">{{ t('el.cascader.noMatch') }}</li>
+          </slot>
+        </el-scrollbar>
+      </div>
+    </transition>
+  </div>
+</template>
+
+<script>
+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';
+import Migrating from 'element-ui/src/mixins/migrating';
+import ElInput from 'element-ui/packages/input';
+import ElTag from 'element-ui/packages/tag';
+import ElScrollbar from 'element-ui/packages/scrollbar';
+import ElCascaderPanel from 'element-ui/packages/cascader-panel';
+import AriaUtils from 'element-ui/src/utils/aria-utils';
+import { t } from 'element-ui/src/locale';
+import { isEqual, isEmpty, kebabCase } from 'element-ui/src/utils/util';
+import { isUndefined, isFunction } from 'element-ui/src/utils/types';
+import { isDef } from 'element-ui/src/utils/shared';
+import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event';
+import debounce from 'throttle-debounce/debounce';
+
+const { keys: KeyCode } = AriaUtils;
+const MigratingProps = {
+  expandTrigger: {
+    newProp: 'expandTrigger',
+    type: String
+  },
+  changeOnSelect: {
+    newProp: 'checkStrictly',
+    type: Boolean
+  },
+  hoverThreshold: {
+    newProp: 'hoverThreshold',
+    type: Number
+  }
+};
+
+const PopperMixin = {
+  props: {
+    placement: {
+      type: String,
+      default: 'bottom-start'
+    },
+    appendToBody: Popper.props.appendToBody,
+    visibleArrow: {
+      type: Boolean,
+      default: true
+    },
+    arrowOffset: Popper.props.arrowOffset,
+    offset: Popper.props.offset,
+    boundariesPadding: Popper.props.boundariesPadding,
+    popperOptions: Popper.props.popperOptions
+  },
+  methods: Popper.methods,
+  data: Popper.data,
+  beforeDestroy: Popper.beforeDestroy
+};
+
+const InputSizeMap = {
+  medium: 36,
+  small: 32,
+  mini: 28
+};
+
+export default {
+  name: 'ElCascader',
+
+  directives: { Clickoutside },
+
+  mixins: [PopperMixin, Emitter, Locale, Migrating],
+
+  inject: {
+    elForm: {
+      default: ''
+    },
+    elFormItem: {
+      default: ''
+    }
+  },
+
+  components: {
+    ElInput,
+    ElTag,
+    ElScrollbar,
+    ElCascaderPanel
+  },
+
+  props: {
+    value: {},
+    options: Array,
+    props: Object,
+    size: String,
+    placeholder: {
+      type: String,
+      default: () => t('el.cascader.placeholder')
+    },
+    disabled: Boolean,
+    clearable: Boolean,
+    filterable: Boolean,
+    filterMethod: Function,
+    separator: {
+      type: String,
+      default: ' / '
+    },
+    showAllLevels: {
+      type: Boolean,
+      default: true
+    },
+    collapseTags: Boolean,
+    debounce: {
+      type: Number,
+      default: 300
+    },
+    beforeFilter: {
+      type: Function,
+      default: () => (() => {})
+    },
+    popperClass: String
+  },
+
+  data() {
+    return {
+      dropDownVisible: false,
+      checkedValue: this.value || null,
+      inputHover: false,
+      inputValue: null,
+      presentText: null,
+      presentTags: [],
+      checkedNodes: [],
+      filtering: false,
+      suggestions: [],
+      inputInitialHeight: 0,
+      pressDeleteCount: 0
+    };
+  },
+
+  computed: {
+    realSize() {
+      const _elFormItemSize = (this.elFormItem || {}).elFormItemSize;
+      return this.size || _elFormItemSize || (this.$ELEMENT || {}).size;
+    },
+    tagSize() {
+      return ['small', 'mini'].indexOf(this.realSize) > -1
+        ? 'mini'
+        : 'small';
+    },
+    isDisabled() {
+      return this.disabled || (this.elForm || {}).disabled;
+    },
+    config() {
+      const config = this.props || {};
+      const { $attrs } = this;
+
+      Object
+        .keys(MigratingProps)
+        .forEach(oldProp => {
+          const { newProp, type } = MigratingProps[oldProp];
+          let oldValue = $attrs[oldProp] || $attrs[kebabCase(oldProp)];
+          if (isDef(oldProp) && !isDef(config[newProp])) {
+            if (type === Boolean && oldValue === '') {
+              oldValue = true;
+            }
+            config[newProp] = oldValue;
+          }
+        });
+
+      return config;
+    },
+    multiple() {
+      return this.config.multiple;
+    },
+    leafOnly() {
+      return !this.config.checkStrictly;
+    },
+    readonly() {
+      return !this.filterable || this.multiple;
+    },
+    clearBtnVisible() {
+      if (!this.clearable || this.isDisabled || this.filtering || !this.inputHover) {
+        return false;
+      }
+
+      return this.multiple
+        ? !!this.checkedNodes.filter(node => !node.isDisabled).length
+        : !!this.presentText;
+    },
+    panel() {
+      return this.$refs.panel;
+    }
+  },
+
+  watch: {
+    value(val) {
+      if (!isEqual(val, this.checkedValue)) {
+        this.checkedValue = val;
+        this.computePresentContent();
+      }
+    },
+    checkedValue(val) {
+      const { value } = this;
+      if (!isEqual(val, value) || isUndefined(value)) {
+        this.$emit('input', val);
+        this.$emit('change', val);
+        this.dispatch('ElFormItem', 'el.form.change', [val]);
+        this.computePresentContent();
+      }
+    },
+    options: {
+      handler: function() {
+        this.$nextTick(this.computePresentContent);
+      },
+      deep: true
+    },
+    presentText(val) {
+      this.inputValue = val;
+    },
+    presentTags(val, oldVal) {
+      if (this.multiple && (val.length || oldVal.length)) {
+        this.$nextTick(this.updateStyle);
+      }
+    },
+    filtering(val) {
+      this.$nextTick(this.updatePopper);
+    }
+  },
+
+  mounted() {
+    const { input } = this.$refs;
+    if (input && input.$el) {
+      this.inputInitialHeight = input.$el.offsetHeight || InputSizeMap[this.realSize] || 40;
+    }
+
+    if (!isEmpty(this.value)) {
+      this.computePresentContent();
+    }
+
+    this.filterHandler = debounce(this.debounce, () => {
+      const { inputValue } = this;
+
+      if (!inputValue) {
+        this.filtering = false;
+        return;
+      }
+
+      const before = this.beforeFilter(inputValue);
+      if (before && before.then) {
+        before.then(this.getSuggestions);
+      } else if (before !== false) {
+        this.getSuggestions();
+      } else {
+        this.filtering = false;
+      }
+    });
+
+    addResizeListener(this.$el, this.updateStyle);
+  },
+
+  beforeDestroy() {
+    removeResizeListener(this.$el, this.updateStyle);
+  },
+
+  methods: {
+    getMigratingConfig() {
+      return {
+        props: {
+          'expand-trigger': 'expand-trigger is removed, use `props.expandTrigger` instead.',
+          'change-on-select': 'change-on-select is removed, use `props.checkStrictly` instead.',
+          'hover-threshold': 'hover-threshold is removed, use `props.hoverThreshold` instead'
+        },
+        events: {
+          'active-item-change': 'active-item-change is renamed to expand-change'
+        }
+      };
+    },
+    toggleDropDownVisible(visible) {
+      if (this.isDisabled) return;
+
+      const { dropDownVisible } = this;
+      const { input } = this.$refs;
+      visible = isDef(visible) ? visible : !dropDownVisible;
+      if (visible !== dropDownVisible) {
+        this.dropDownVisible = visible;
+        if (visible) {
+          this.$nextTick(() => {
+            this.updatePopper();
+            this.panel.scrollIntoView();
+          });
+        }
+        input.$refs.input.setAttribute('aria-expanded', visible);
+        this.$emit('visible-change', visible);
+      }
+    },
+    handleDropdownLeave() {
+      this.filtering = false;
+      this.inputValue = this.presentText;
+    },
+    handleKeyDown(event) {
+      switch (event.keyCode) {
+        case KeyCode.enter:
+          this.toggleDropDownVisible();
+          break;
+        case KeyCode.down:
+          this.toggleDropDownVisible(true);
+          this.focusFirstNode();
+          event.preventDefault();
+          break;
+        case KeyCode.esc:
+        case KeyCode.tab:
+          this.toggleDropDownVisible(false);
+          break;
+      }
+    },
+    handleFocus(e) {
+      this.$emit('focus', e);
+    },
+    handleBlur(e) {
+      this.$emit('blur', e);
+    },
+    handleInput(val, event) {
+      !this.dropDownVisible && this.toggleDropDownVisible(true);
+
+      if (event && event.isComposing) return;
+      if (val) {
+        this.filterHandler();
+      } else {
+        this.filtering = false;
+      }
+    },
+    handleClear() {
+      this.presentText = '';
+      this.panel.clearCheckedNodes();
+    },
+    handleExpandChange(value) {
+      this.$nextTick(this.updatePopper.bind(this));
+      this.$emit('expand-change', value);
+      this.$emit('active-item-change', value); // Deprecated
+    },
+    focusFirstNode() {
+      this.$nextTick(() => {
+        const { filtering } = this;
+        const { popper, suggestionPanel } = this.$refs;
+        let firstNode = null;
+
+        if (filtering && suggestionPanel) {
+          firstNode = suggestionPanel.$el.querySelector('.el-cascader__suggestion-item');
+        } else {
+          const firstMenu = popper.querySelector('.el-cascader-menu');
+          firstNode = firstMenu.querySelector('.el-cascader-node[tabindex="-1"]');
+        }
+
+        if (firstNode) {
+          firstNode.focus();
+          !filtering && firstNode.click();
+        }
+      });
+    },
+    computePresentContent() {
+      this.$nextTick(() => {
+        const { multiple, checkStrictly } = this.config;
+        if (multiple) {
+          this.computePresentTags();
+          this.presentText = this.presentTags.length ? ' ' : null;
+        } else {
+          this.computePresentText();
+          if (!checkStrictly && this.dropDownVisible) {
+            this.toggleDropDownVisible(false);
+          }
+        }
+      });
+    },
+    computePresentText() {
+      const { checkedValue, config } = this;
+      if (!isEmpty(checkedValue)) {
+        const node = this.panel.getNodeByValue(checkedValue);
+        if (node && (config.checkStrictly || node.isLeaf)) {
+          this.presentText = node.getText(this.showAllLevels, this.separator);
+          return;
+        }
+      }
+      this.presentText = null;
+    },
+    computePresentTags() {
+      const { isDisabled, leafOnly, showAllLevels, separator, collapseTags } = this;
+      const checkedNodes = this.getCheckedNodes(leafOnly);
+      const tags = [];
+
+      const genTag = node => ({
+        node,
+        key: node.uid,
+        text: node.getText(showAllLevels, separator),
+        hitState: false,
+        closable: !isDisabled && !node.isDisabled
+      });
+
+      if (checkedNodes.length) {
+        const [first, ...rest] = checkedNodes;
+        const restCount = rest.length;
+        tags.push(genTag(first));
+
+        if (restCount) {
+          if (collapseTags) {
+            tags.push({
+              key: -1,
+              text: `+ ${restCount}`,
+              closable: false
+            });
+          } else {
+            rest.forEach(node => tags.push(genTag(node)));
+          }
+        }
+      }
+
+      this.checkedNodes = checkedNodes;
+      this.presentTags = tags;
+    },
+    getSuggestions() {
+      let { filterMethod } = this;
+
+      if (!isFunction(filterMethod)) {
+        filterMethod = (node, keyword) => node.text.includes(keyword);
+      }
+
+      const suggestions = this.panel.getFlattedNodes(this.leafOnly)
+        .filter(node => {
+          if (node.isDisabled) return false;
+          node.text = node.getText(this.showAllLevels, this.separator) || '';
+          return filterMethod(node, this.inputValue);
+        });
+
+      if (this.multiple) {
+        this.presentTags.forEach(tag => {
+          tag.hitState = false;
+        });
+      } else {
+        suggestions.forEach(node => {
+          node.checked = isEqual(this.checkedValue, node.getValueByOption());
+        });
+      }
+
+      this.filtering = true;
+      this.suggestions = suggestions;
+      this.$nextTick(this.updatePopper);
+    },
+    handleSuggestionKeyDown(event) {
+      const { keyCode, target } = event;
+      switch (keyCode) {
+        case KeyCode.enter:
+          target.click();
+          break;
+        case KeyCode.up:
+          const prev = target.previousElementSibling;
+          prev && prev.focus();
+          break;
+        case KeyCode.down:
+          const next = target.nextElementSibling;
+          next && next.focus();
+          break;
+        case KeyCode.esc:
+        case KeyCode.tab:
+          this.toggleDropDownVisible(false);
+          break;
+      }
+    },
+    handleDelete() {
+      const { inputValue, pressDeleteCount, presentTags } = this;
+      const lastIndex = presentTags.length - 1;
+      const lastTag = presentTags[lastIndex];
+      this.pressDeleteCount = inputValue ? 0 : pressDeleteCount + 1;
+
+      if (!lastTag) return;
+
+      if (this.pressDeleteCount) {
+        if (lastTag.hitState) {
+          this.deleteTag(lastIndex);
+        } else {
+          lastTag.hitState = true;
+        }
+      }
+    },
+    handleSuggestionClick(index) {
+      const { multiple } = this;
+      const targetNode = this.suggestions[index];
+
+      if (multiple) {
+        const { checked } = targetNode;
+        targetNode.doCheck(!checked);
+        this.panel.calculateMultiCheckedValue();
+      } else {
+        this.checkedValue = targetNode.getValueByOption();
+        this.toggleDropDownVisible(false);
+      }
+    },
+    deleteTag(index) {
+      const { checkedValue } = this;
+      const val = checkedValue[index];
+      this.checkedValue = checkedValue.filter((n, i) => i !== index);
+      this.$emit('remove-tag', val);
+    },
+    updateStyle() {
+      const { $el, inputInitialHeight } = this;
+      if (this.$isServer || !$el) return;
+
+      const { suggestionPanel } = this.$refs;
+      const inputInner = $el.querySelector('.el-input__inner');
+
+      if (!inputInner) return;
+
+      const tags = $el.querySelector('.el-cascader__tags');
+      let suggestionPanelEl = null;
+
+      if (suggestionPanel && (suggestionPanelEl = suggestionPanel.$el)) {
+        const suggestionList = suggestionPanelEl.querySelector('.el-cascader__suggestion-list');
+        suggestionList.style.minWidth = inputInner.offsetWidth + 'px';
+      }
+
+      if (tags) {
+        const { offsetHeight } = tags;
+        const height = Math.max(offsetHeight + 6, inputInitialHeight) + 'px';
+        inputInner.style.height = height;
+        this.updatePopper();
+      }
+    },
+    getCheckedNodes(leafOnly) {
+      return this.panel.getCheckedNodes(leafOnly);
+    }
+  }
+};
+</script>
+

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

@@ -1,452 +0,0 @@
-<template>
-  <span
-    class="el-cascader"
-    :class="[
-      {
-        'is-opened': menuVisible,
-        'is-disabled': cascaderDisabled
-      },
-      cascaderSize ? 'el-cascader--' + cascaderSize : ''
-    ]"
-    @click="handleClick"
-    @mouseenter="inputHover = true"
-    @focus="inputHover = true"
-    @mouseleave="inputHover = false"
-    @blur="inputHover = false"
-    ref="reference"
-    v-clickoutside="handleClickoutside"
-    @keydown="handleKeydown"
-  >
-    <el-input
-      ref="input"
-      :readonly="readonly"
-      :placeholder="currentLabels.length ? undefined : placeholder"
-      v-model="inputValue"
-      @input="debouncedInputChange"
-      @focus="handleFocus"
-      @blur="handleBlur"
-      @compositionstart.native="handleComposition"
-      @compositionend.native="handleComposition"
-      :validate-event="false"
-      :size="size"
-      :disabled="cascaderDisabled"
-      :class="{ 'is-focus': menuVisible }"
-    >
-      <template slot="suffix">
-        <i
-          key="1"
-          v-if="clearable && inputHover && currentLabels.length"
-          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-arrow-down"
-          :class="{ 'is-reverse': menuVisible }"
-        ></i>
-      </template>
-    </el-input>
-    <span class="el-cascader__label" v-show="inputValue === '' && !isOnComposition">
-      <template v-if="showAllLevels">
-        <template v-for="(label, index) in currentLabels">
-          {{ label }}
-          <span v-if="index < currentLabels.length - 1" :key="index"> {{ separator }} </span>
-        </template>
-      </template>
-      <template v-else>
-        {{ currentLabels[currentLabels.length - 1] }}
-      </template>
-    </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';
-import { t } from 'element-ui/src/locale';
-import debounce from 'throttle-debounce/debounce';
-import { generateId, escapeRegexpString, isIE, isEdge } from 'element-ui/src/utils/util';
-
-const popperMixin = {
-  props: {
-    placement: {
-      type: String,
-      default: 'bottom-start'
-    },
-    appendToBody: Popper.props.appendToBody,
-    arrowOffset: Popper.props.arrowOffset,
-    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],
-
-  inject: {
-    elForm: {
-      default: ''
-    },
-    elFormItem: {
-      default: ''
-    }
-  },
-
-  components: {
-    ElInput
-  },
-
-  props: {
-    options: {
-      type: Array,
-      required: true
-    },
-    props: {
-      type: Object,
-      default() {
-        return {
-          children: 'children',
-          label: 'label',
-          value: 'value',
-          disabled: 'disabled'
-        };
-      }
-    },
-    value: {
-      type: Array,
-      default() {
-        return [];
-      }
-    },
-    separator: {
-      type: String,
-      default: '/'
-    },
-    placeholder: {
-      type: String,
-      default() {
-        return t('el.cascader.placeholder');
-      }
-    },
-    disabled: Boolean,
-    clearable: {
-      type: Boolean,
-      default: false
-    },
-    changeOnSelect: Boolean,
-    popperClass: String,
-    expandTrigger: {
-      type: String,
-      default: 'click'
-    },
-    filterable: Boolean,
-    size: String,
-    showAllLevels: {
-      type: Boolean,
-      default: true
-    },
-    debounce: {
-      type: Number,
-      default: 300
-    },
-    beforeFilter: {
-      type: Function,
-      default: () => (() => {})
-    },
-    hoverThreshold: {
-      type: Number,
-      default: 500
-    }
-  },
-
-  data() {
-    return {
-      currentValue: this.value || [],
-      menu: null,
-      debouncedInputChange() {},
-      menuVisible: false,
-      inputHover: false,
-      inputValue: '',
-      flatOptions: null,
-      id: generateId(),
-      needFocus: true,
-      isOnComposition: false
-    };
-  },
-
-  computed: {
-    labelKey() {
-      return this.props.label || 'label';
-    },
-    valueKey() {
-      return this.props.value || 'value';
-    },
-    childrenKey() {
-      return this.props.children || 'children';
-    },
-    disabledKey() {
-      return this.props.disabled || 'disabled';
-    },
-    currentLabels() {
-      let options = this.options;
-      let labels = [];
-      this.currentValue.forEach(value => {
-        const targetOption = options && options.filter(option => option[this.valueKey] === value)[0];
-        if (targetOption) {
-          labels.push(targetOption[this.labelKey]);
-          options = targetOption[this.childrenKey];
-        }
-      });
-      return labels;
-    },
-    _elFormItemSize() {
-      return (this.elFormItem || {}).elFormItemSize;
-    },
-    cascaderSize() {
-      return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
-    },
-    cascaderDisabled() {
-      return this.disabled || (this.elForm || {}).disabled;
-    },
-    readonly() {
-      return !this.filterable || (!isIE() && !isEdge() && !this.menuVisible);
-    }
-  },
-
-  watch: {
-    menuVisible(value) {
-      this.$refs.input.$refs.input.setAttribute('aria-expanded', value);
-      value ? this.showMenu() : this.hideMenu();
-      this.$emit('visible-change', value);
-    },
-    value(value) {
-      this.currentValue = value;
-    },
-    currentValue(value) {
-      this.dispatch('ElFormItem', 'el.form.change', [value]);
-    },
-    options: {
-      deep: true,
-      handler(value) {
-        if (!this.menu) {
-          this.initMenu();
-        }
-        this.flatOptions = this.flattenOptions(this.options);
-        this.menu.options = value;
-      }
-    }
-  },
-
-  methods: {
-    initMenu() {
-      this.menu = new Vue(ElCascaderMenu).$mount();
-      this.menu.options = this.options;
-      this.menu.props = this.props;
-      this.menu.expandTrigger = this.expandTrigger;
-      this.menu.changeOnSelect = this.changeOnSelect;
-      this.menu.popperClass = this.popperClass;
-      this.menu.hoverThreshold = this.hoverThreshold;
-      this.popperElm = this.menu.$el;
-      this.menu.$refs.menus[0].setAttribute('id', `cascader-menu-${this.id}`);
-      this.menu.$on('pick', this.handlePick);
-      this.menu.$on('activeItemChange', this.handleActiveItemChange);
-      this.menu.$on('menuLeave', this.doDestroy);
-      this.menu.$on('closeInside', this.handleClickoutside);
-    },
-    showMenu() {
-      if (!this.menu) {
-        this.initMenu();
-      }
-
-      this.menu.value = this.currentValue.slice(0);
-      this.menu.visible = true;
-      this.menu.options = this.options;
-      this.$nextTick(_ => {
-        this.updatePopper();
-        this.menu.inputWidth = this.$refs.input.$el.offsetWidth - 2;
-      });
-    },
-    hideMenu() {
-      this.inputValue = '';
-      this.menu.visible = false;
-      if (this.needFocus) {
-        this.$refs.input.focus();
-      } else {
-        this.needFocus = true;
-      }
-    },
-    handleActiveItemChange(value) {
-      this.$nextTick(_ => {
-        this.updatePopper();
-      });
-      this.$emit('active-item-change', value);
-    },
-    handleKeydown(e) {
-      const keyCode = e.keyCode;
-      if (keyCode === 13) {
-        this.handleClick();
-      } else if (keyCode === 40) { // down
-        this.menuVisible = true; // 打开
-        setTimeout(() => {
-          const firstMenu = this.popperElm.querySelectorAll('.el-cascader-menu')[0];
-          firstMenu.querySelectorAll("[tabindex='-1']")[0].focus();
-        });
-        e.stopPropagation();
-        e.preventDefault();
-      } else if (keyCode === 27 || keyCode === 9) { // esc  tab
-        this.inputValue = '';
-        if (this.menu) this.menu.visible = false;
-      }
-    },
-    handlePick(value, close = true) {
-      this.currentValue = value;
-      this.$emit('input', value);
-      this.$emit('change', value);
-
-      if (close) {
-        this.menuVisible = false;
-      } else {
-        this.$nextTick(this.updatePopper);
-      }
-    },
-    handleInputChange(value) {
-      if (!this.menuVisible) return;
-      const flatOptions = this.flatOptions;
-
-      if (!value) {
-        this.menu.options = this.options;
-        this.$nextTick(this.updatePopper);
-        return;
-      }
-
-      let filteredFlatOptions = flatOptions.filter(optionsStack => {
-        return optionsStack.some(option => new RegExp(escapeRegexpString(value), 'i')
-          .test(option[this.labelKey]));
-      });
-
-      if (filteredFlatOptions.length > 0) {
-        filteredFlatOptions = filteredFlatOptions.map(optionStack => {
-          return {
-            __IS__FLAT__OPTIONS: true,
-            value: optionStack.map(item => item[this.valueKey]),
-            label: this.renderFilteredOptionLabel(value, optionStack),
-            disabled: optionStack.some(item => item[this.disabledKey])
-          };
-        });
-      } else {
-        filteredFlatOptions = [{
-          __IS__FLAT__OPTIONS: true,
-          label: this.t('el.cascader.noMatch'),
-          value: '',
-          disabled: true
-        }];
-      }
-      this.menu.options = filteredFlatOptions;
-      this.$nextTick(this.updatePopper);
-    },
-    renderFilteredOptionLabel(inputValue, optionsStack) {
-      return optionsStack.map((option, index) => {
-        const label = option[this.labelKey];
-        const keywordIndex = label.toLowerCase().indexOf(inputValue.toLowerCase());
-        const labelPart = label.slice(keywordIndex, inputValue.length + keywordIndex);
-        const node = keywordIndex > -1 ? this.highlightKeyword(label, labelPart) : label;
-        return index === 0 ? node : [` ${this.separator} `, 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[this.childrenKey]) {
-          flatOptions.push(optionsStack);
-        } else {
-          if (this.changeOnSelect) {
-            flatOptions.push(optionsStack);
-          }
-          flatOptions = flatOptions.concat(this.flattenOptions(option[this.childrenKey], optionsStack));
-        }
-      });
-      return flatOptions;
-    },
-    clearValue(ev) {
-      ev.stopPropagation();
-      this.handlePick([], true);
-    },
-    handleClickoutside(pickFinished = false) {
-      if (this.menuVisible && !pickFinished) {
-        this.needFocus = false;
-      }
-      this.menuVisible = false;
-    },
-    handleClick() {
-      if (this.cascaderDisabled) return;
-      this.$refs.input.focus();
-      if (this.filterable) {
-        this.menuVisible = true;
-        return;
-      }
-      this.menuVisible = !this.menuVisible;
-    },
-    handleFocus(event) {
-      this.$emit('focus', event);
-    },
-    handleBlur(event) {
-      this.$emit('blur', event);
-    },
-    handleComposition(event) {
-      this.isOnComposition = event.type !== 'compositionend';
-    }
-  },
-
-  created() {
-    this.debouncedInputChange = debounce(this.debounce, value => {
-      const before = this.beforeFilter(value);
-
-      if (before && before.then) {
-        this.menu.options = [{
-          __IS__FLAT__OPTIONS: true,
-          label: this.t('el.cascader.loading'),
-          value: '',
-          disabled: true
-        }];
-        before
-          .then(() => {
-            this.$nextTick(() => {
-              this.handleInputChange(value);
-            });
-          });
-      } else if (before !== false) {
-        this.$nextTick(() => {
-          this.handleInputChange(value);
-        });
-      }
-    });
-  },
-
-  mounted() {
-    this.flatOptions = this.flattenOptions(this.options);
-  }
-};
-</script>

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

@@ -1,375 +0,0 @@
-<script>
-  import { isDef } from 'element-ui/src/utils/shared';
-  import scrollIntoView from 'element-ui/src/utils/scroll-into-view';
-  import { generateId } from 'element-ui/src/utils/util';
-
-  const copyArray = (arr, props) => {
-    if (!arr || !Array.isArray(arr) || !props) return arr;
-    const result = [];
-    const configurableProps = ['__IS__FLAT__OPTIONS', 'label', 'value', 'disabled'];
-    const childrenProp = props.children || 'children';
-    arr.forEach(item => {
-      const itemCopy = {};
-      configurableProps.forEach(prop => {
-        let name = props[prop];
-        let value = item[name];
-        if (value === undefined) {
-          name = prop;
-          value = item[name];
-        }
-        if (value !== undefined) itemCopy[name] = value;
-      });
-      if (Array.isArray(item[childrenProp])) {
-        itemCopy[childrenProp] = copyArray(item[childrenProp], props);
-      }
-      result.push(itemCopy);
-    });
-    return result;
-  };
-
-  export default {
-    name: 'ElCascaderMenu',
-
-    data() {
-      return {
-        inputWidth: 0,
-        options: [],
-        props: {},
-        visible: false,
-        activeValue: [],
-        value: [],
-        expandTrigger: 'click',
-        changeOnSelect: false,
-        popperClass: '',
-        hoverTimer: 0,
-        clicking: false,
-        id: generateId()
-      };
-    },
-
-    watch: {
-      visible(value) {
-        if (value) {
-          this.activeValue = this.value;
-        }
-      },
-      value: {
-        immediate: true,
-        handler(value) {
-          this.activeValue = value;
-        }
-      }
-    },
-
-    computed: {
-      activeOptions: {
-        get() {
-          const activeValue = this.activeValue;
-          const configurableProps = ['label', 'value', 'children', 'disabled'];
-
-          const formatOptions = options => {
-            options.forEach(option => {
-              if (option.__IS__FLAT__OPTIONS) return;
-              configurableProps.forEach(prop => {
-                const value = option[this.props[prop] || prop];
-                if (value !== undefined) option[prop] = value;
-              });
-              if (Array.isArray(option.children)) {
-                formatOptions(option.children);
-              }
-            });
-          };
-
-          const loadActiveOptions = (options, activeOptions = []) => {
-            const level = activeOptions.length;
-            activeOptions[level] = options;
-            let active = activeValue[level];
-            if (isDef(active)) {
-              options = options.filter(option => option.value === active)[0];
-              if (options && options.children) {
-                loadActiveOptions(options.children, activeOptions);
-              }
-            }
-            return activeOptions;
-          };
-
-          const optionsCopy = copyArray(this.options, this.props);
-          formatOptions(optionsCopy);
-          return loadActiveOptions(optionsCopy);
-        }
-      }
-    },
-
-    methods: {
-      select(item, menuIndex) {
-        if (item.__IS__FLAT__OPTIONS) {
-          this.activeValue = item.value;
-        } else if (menuIndex) {
-          this.activeValue.splice(menuIndex, this.activeValue.length - 1, item.value);
-        } else {
-          this.activeValue = [item.value];
-        }
-        this.$emit('pick', this.activeValue.slice());
-      },
-      handleMenuLeave() {
-        this.$emit('menuLeave');
-      },
-      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.slice(), false);
-        } else {
-          this.$emit('activeItemChange', this.activeValue);
-        }
-      },
-      scrollMenu(menu) {
-        scrollIntoView(menu, menu.getElementsByClassName('is-active')[0]);
-      },
-      handleMenuEnter() {
-        this.$nextTick(() => this.$refs.menus.forEach(menu => this.scrollMenu(menu)));
-      }
-    },
-
-    render(h) {
-      const {
-        activeValue,
-        activeOptions,
-        visible,
-        expandTrigger,
-        popperClass,
-        hoverThreshold
-      } = this;
-      let itemId = null;
-      let itemIndex = 0;
-
-      let hoverMenuRefs = {};
-      const hoverMenuHandler = e => {
-        const activeMenu = hoverMenuRefs.activeMenu;
-        if (!activeMenu) return;
-        const offsetX = e.offsetX;
-        const width = activeMenu.offsetWidth;
-        const height = activeMenu.offsetHeight;
-
-        if (e.target === hoverMenuRefs.activeItem) {
-          clearTimeout(this.hoverTimer);
-          const {activeItem} = hoverMenuRefs;
-          const offsetY_top = activeItem.offsetTop;
-          const offsetY_Bottom = offsetY_top + activeItem.offsetHeight;
-
-          hoverMenuRefs.hoverZone.innerHTML = `
-            <path style="pointer-events: auto;" fill="transparent" d="M${offsetX} ${offsetY_top} L${width} 0 V${offsetY_top} Z" />
-            <path style="pointer-events: auto;" fill="transparent" d="M${offsetX} ${offsetY_Bottom} L${width} ${height} V${offsetY_Bottom} Z" />
-          `;
-        } else {
-          if (!this.hoverTimer) {
-            this.hoverTimer = setTimeout(() => {
-              hoverMenuRefs.hoverZone.innerHTML = '';
-            }, hoverThreshold);
-          }
-        }
-      };
-
-      const menus = this._l(activeOptions, (menu, menuIndex) => {
-        let isFlat = false;
-        const menuId = `menu-${this.id}-${ menuIndex}`;
-        const ownsId = `menu-${this.id}-${ menuIndex + 1 }`;
-        const items = this._l(menu, item => {
-          const events = {
-            on: {}
-          };
-
-          if (item.__IS__FLAT__OPTIONS) isFlat = true;
-
-          if (!item.disabled) {
-            // keydown up/down/left/right/enter
-            events.on.keydown = (ev) => {
-              const keyCode = ev.keyCode;
-              if ([37, 38, 39, 40, 13, 9, 27].indexOf(keyCode) < 0) {
-                return;
-              }
-              const currentEle = ev.target;
-              const parentEle = this.$refs.menus[menuIndex];
-              const menuItemList = parentEle.querySelectorAll("[tabindex='-1']");
-              const currentIndex = Array.prototype.indexOf.call(menuItemList, currentEle); // 当前索引
-              let nextIndex, nextMenu;
-              if ([38, 40].indexOf(keyCode) > -1) {
-                if (keyCode === 38) { // up键
-                  nextIndex = currentIndex !== 0 ? (currentIndex - 1) : currentIndex;
-                } else if (keyCode === 40) { // down
-                  nextIndex = currentIndex !== (menuItemList.length - 1) ? currentIndex + 1 : currentIndex;
-                }
-                menuItemList[nextIndex].focus();
-              } else if (keyCode === 37) { // left键
-                if (menuIndex !== 0) {
-                  const previousMenu = this.$refs.menus[menuIndex - 1];
-                  previousMenu.querySelector('[aria-expanded=true]').focus();
-                }
-              } else if (keyCode === 39) { // right
-                if (item.children) {
-                  // 有子menu 选择子menu的第一个menuitem
-                  nextMenu = this.$refs.menus[menuIndex + 1];
-                  nextMenu.querySelectorAll("[tabindex='-1']")[0].focus();
-                }
-              } else if (keyCode === 13) {
-                if (!item.children) {
-                  const id = currentEle.getAttribute('id');
-                  parentEle.setAttribute('aria-activedescendant', id);
-                  this.select(item, menuIndex);
-                  this.$nextTick(() => this.scrollMenu(this.$refs.menus[menuIndex]));
-                }
-              } else if (keyCode === 9 || keyCode === 27) { // esc tab
-                this.$emit('closeInside');
-              }
-            };
-            if (item.children) {
-              let triggerEvent = {
-                click: 'click',
-                hover: 'mouseenter'
-              }[expandTrigger];
-              const triggerHandler = () => {
-                if (this.visible) {
-                  this.activeItem(item, menuIndex);
-                  this.$nextTick(() => {
-                    // adjust self and next level
-                    this.scrollMenu(this.$refs.menus[menuIndex]);
-                    this.scrollMenu(this.$refs.menus[menuIndex + 1]);
-                  });
-                }
-              };
-              events.on[triggerEvent] = triggerHandler;
-              if (triggerEvent === 'mouseenter' && this.changeOnSelect) {
-                events.on.click = () => {
-                  if (this.activeValue.indexOf(item.value) !== -1) {
-                    this.$emit('closeInside', true);
-                  }
-                };
-              }
-              events.on['mousedown'] = () => {
-                this.clicking = true;
-              };
-              events.on['focus'] = () => { // focus 选中
-                if (this.clicking) {
-                  this.clicking = false;
-                  return;
-                }
-                triggerHandler();
-              };
-            } else {
-              events.on.click = () => {
-                this.select(item, menuIndex);
-                this.$nextTick(() => this.scrollMenu(this.$refs.menus[menuIndex]));
-              };
-            }
-          }
-          if (!item.disabled && !item.children) { // no children set id
-            itemId = `${menuId}-${itemIndex}`;
-            itemIndex++;
-          }
-          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
-              }}
-              ref={item.value === activeValue[menuIndex] ? 'activeItem' : null}
-              {...events}
-              tabindex= { item.disabled ? null : -1 }
-              role="menuitem"
-              aria-haspopup={ !!item.children }
-              aria-expanded={ item.value === activeValue[menuIndex] }
-              id = { itemId }
-              aria-owns = { !item.children ? null : ownsId }
-            >
-              <span>{item.label}</span>
-            </li>
-          );
-        });
-        let menuStyle = {};
-        if (isFlat) {
-          menuStyle.minWidth = this.inputWidth + 'px';
-        }
-
-        const isHoveredMenu = expandTrigger === 'hover' && activeValue.length - 1 === menuIndex;
-        const hoverMenuEvent = {
-          on: {
-          }
-        };
-
-        if (isHoveredMenu) {
-          hoverMenuEvent.on.mousemove = hoverMenuHandler;
-          menuStyle.position = 'relative';
-        }
-
-        return (
-          <ul
-            class={{
-              'el-cascader-menu': true,
-              'el-cascader-menu--flexible': isFlat
-            }}
-            {...hoverMenuEvent}
-            style={menuStyle}
-            refInFor
-            ref="menus"
-            role="menu"
-            id = { menuId }
-          >
-            {items}
-            {
-              isHoveredMenu
-                ? (<svg
-                  ref="hoverZone"
-                  style={{
-                    position: 'absolute',
-                    top: 0,
-                    height: '100%',
-                    width: '100%',
-                    left: 0,
-                    pointerEvents: 'none'
-                  }}
-                ></svg>) : null
-            }
-          </ul>
-        );
-      });
-
-      if (expandTrigger === 'hover') {
-        this.$nextTick(() => {
-          const activeItem = this.$refs.activeItem;
-
-          if (activeItem) {
-            const activeMenu = activeItem.parentElement;
-            const hoverZone = this.$refs.hoverZone;
-
-            hoverMenuRefs = {
-              activeMenu,
-              activeItem,
-              hoverZone
-            };
-          } else {
-            hoverMenuRefs = {};
-          }
-        });
-      }
-
-      return (
-        <transition name="el-zoom-in-top" on-before-enter={this.handleMenuEnter} on-after-leave={this.handleMenuLeave}>
-          <div
-            v-show={visible}
-            class={[
-              'el-cascader-menus el-popper',
-              popperClass
-            ]}
-            ref="wrapper"
-          >
-            <div x-arrow class="popper__arrow"></div>
-            {menus}
-          </div>
-        </transition>
-      );
-    }
-  };
-</script>

+ 124 - 0
packages/theme-chalk/src/cascader-panel.scss

@@ -0,0 +1,124 @@
+@import "mixins/mixins";
+@import "common/var";
+@import "./checkbox";
+@import "./radio";
+@import "./scrollbar";
+
+@include b(cascader-panel) {
+  display: flex;
+  border-radius: $--cascader-menu-radius;
+  font-size: $--cascader-menu-font-size;
+
+  @include when(bordered) {
+    border: $--cascader-menu-border;
+    border-radius: $--cascader-menu-radius;
+  }
+}
+
+@include b(cascader-menu) {
+  min-width: 180px;
+  box-sizing: border-box;
+  color: $--cascader-menu-font-color;
+  border-right: $--cascader-menu-border;
+
+  &:last-child {
+    border-right: none;
+    .el-cascader-node {
+      padding-right: 20px;
+    }
+  }
+
+  @include e(wrap) {
+    height: 204px;
+  }
+
+  @include e(list) {
+    position: relative;
+    min-height: 100%;
+    margin: 0;
+    padding: 6px 0;
+    list-style: none;
+    box-sizing: border-box;
+  }
+
+  @include e(hover-zone) {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    pointer-events: none;
+  }
+
+  @include e(empty-text) {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    text-align: center;
+    color: $--cascader-color-empty;
+  }
+}
+
+@include b(cascader-node) {
+  position: relative;
+  display: flex;
+  align-items: center;
+  padding: 0 30px 0 20px;
+  height: 34px;
+  line-height: 34px;
+  outline: none;
+
+  &.is-selectable.in-active-path {
+    color: $--cascader-menu-font-color;
+  }
+
+  &.in-active-path,
+  &.is-selectable.in-checked-path,
+  &.is-active {
+    color: $--cascader-menu-selected-font-color;
+    font-weight: bold;
+  }
+
+  &:not(.is-disabled) {
+    cursor: pointer;
+    &:hover, &:focus {
+      background: $--cascader-node-background-hover;
+    }
+  }
+
+  @include when(disabled) {
+    color: $--cascader-node-color-disabled;
+    cursor: not-allowed;
+  }
+
+  @include e(prefix) {
+    position: absolute;
+    left: 10px;
+  }
+
+  @include e(postfix) {
+    position: absolute;
+    right: 10px;
+  }
+
+  @include e(label) {
+    flex: 1;
+    padding: 0 10px;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+
+  > .el-checkbox {
+    margin-right: 0;
+  }
+
+  > .el-radio {
+    margin-right: 0;
+
+    .el-radio__label {
+      padding-left: 0;
+    }
+  }
+}

+ 116 - 119
packages/theme-chalk/src/cascader.scss

@@ -1,7 +1,9 @@
 @import "mixins/mixins";
 @import "common/var";
-@import "./input.scss";
+@import "./input";
 @import "./popper";
+@import "./tag";
+@import "./cascader-panel";
 
 @include b(cascader) {
   display: inline-block;
@@ -9,76 +11,57 @@
   font-size: $--font-size-base;
   line-height: $--input-height;
 
-  .el-input,
-  .el-input__inner {
-    cursor: pointer;
-  }
-
-  .el-input.is-focus .el-input__inner {
-    border-color: $--input-focus-border;
+  &:not(.is-disabled):hover {
+    .el-input__inner {
+      cursor: pointer;
+      border-color: $--input-hover-border;
+    }
   }
 
-  .el-input__icon {
-    transition: none;
-  }
+  .el-input {
+    cursor: pointer;
 
-  .el-icon-arrow-down {
-    transition: transform .3s;
-    font-size: 14px;
+    .el-input__inner {
+      text-overflow: ellipsis;
 
-    @include when(reverse) {
-      transform: rotateZ(180deg);
+      &:focus {
+        border-color: $--input-focus-border;
+      }
     }
-  }
 
-  .el-icon-circle-close {
-    z-index: #{$--index-normal + 1};
-    transition: $--color-transition-base;
+    .el-icon-arrow-down {
+      transition: transform .3s;
+      font-size: 14px;
 
-    &:hover {
-      color: $--color-text-secondary;
+      @include when(reverse) {
+        transform: rotateZ(180deg);
+      }
     }
-  }
-
-  @include e(clearIcon) {
-    z-index: 2;
-    position: relative;
-  }
 
-  @include e(label) {
-    position: absolute;
-    left: 0;
-    top: 0;
-    height: 100%;
-    padding: 0 25px 0 15px;
-    color: $--cascader-menu-font-color;
-    width: 100%;
-    white-space: nowrap;
-    text-overflow: ellipsis;
-    overflow: hidden;
-    box-sizing: border-box;
-    cursor: pointer;
-    text-align: left;
-    font-size: inherit;
+    .el-icon-circle-close:hover {
+      color: $--input-clear-hover-color;
+    }
 
-    span {
-      color: $--color-black;
+    @include when(focus) {
+      .el-input__inner {
+        border-color: $--input-focus-border;
+      }
     }
   }
 
   @include m(medium) {
     font-size: $--input-medium-font-size;
-    line-height: #{$--input-medium-height};
+    line-height: $--input-medium-height;
   }
 
   @include m(small) {
     font-size: $--input-small-font-size;
-    line-height: #{$--input-small-height};
+    line-height: $--input-small-height;
   }
 
   @include m(mini) {
     font-size: $--input-mini-font-size;
-    line-height: #{$--input-mini-height};
+    line-height: $--input-mini-height;
   }
 
   @include when(disabled) {
@@ -87,99 +70,113 @@
       color: $--disabled-color-base;
     }
   }
-}
 
-@include b(cascader-menus) {
-  white-space: nowrap;
-  background: #fff;
-  position: absolute;
-  margin: 5px 0;
-  z-index: #{$--index-normal + 1};
-  border: $--select-dropdown-border;
-  border-radius: $--border-radius-small;
-  box-shadow: $--select-dropdown-shadow;
-}
-
-@include b(cascader-menu) {
-  display: inline-block;
-  vertical-align: top;
-  height: 204px;
-  overflow: auto;
-  border-right: $--select-dropdown-border;
-  background-color: $--select-dropdown-background;
-  box-sizing: border-box;
-  margin: 0;
-  padding: 6px 0;
-  min-width: 160px;
-
-  &:last-child {
-    border-right: 0;
+  @include e(dropdown) {
+    margin: 5px 0;
+    font-size: $--cascader-menu-font-size;
+    background: $--cascader-menu-fill;
+    border: $--cascader-menu-border;
+    border-radius: $--cascader-menu-radius;
+    box-shadow: $--cascader-menu-shadow;
   }
 
-  @include e(item) {
-    font-size: $--select-font-size;
-    padding: 8px 20px;
-    position: relative;
-    white-space: nowrap;
-    overflow: hidden;
-    text-overflow: ellipsis;
-    color: $--select-option-color;
-    height: $--select-option-height;
-    line-height: 1.5;
+  @include e(tags) {
+    position: absolute;
+    left: 0;
+    right: 30px;
+    top: 50%;
+    transform: translateY(-50%);
+    display: flex;
+    flex-wrap: wrap;
+    line-height: normal;
+    text-align: left;
     box-sizing: border-box;
-    cursor: pointer;
-    outline: none;
 
-    span {
-      padding-right: 10px;
-    }
+    .el-tag {
+      display: inline-flex;
+      align-items: center;
+      max-width: 100%;
+      margin: 2px 0 2px 6px;
+      text-overflow: ellipsis;
+      background: $--cascader-tag-background;
 
-    @include m(extensible) {
-      &:after {
-        font-family: 'element-icons';
-        content: "\e6e0";
-        font-size: 14px;
-        color: rgb(191, 203, 217);
-        position: absolute;
-        right: 15px;
+      &:not(.is-hit) {
+        border-color: transparent;
+      }
+
+      > span {
+        flex: 1;
+        overflow: hidden;
+        text-overflow: ellipsis;
       }
-    }
 
-    @include when(disabled) {
-      color: $--select-option-disabled-color;
-      background-color: $--select-option-disabled-background;
-      cursor: not-allowed;
+      .el-icon-close {
+        flex: none;
+        background-color: $--color-text-placeholder;
+        color: $--color-white;
 
-      &:hover {
-        background-color: $--color-white;
+        &:hover {
+          background-color: $--color-text-secondary;
+        }
       }
     }
+  }
 
-    @include when(active) {
-      color: $--cascader-menu-selected-font-color;
+  @include e(suggestion-panel) {
+    border-radius: $--cascader-menu-radius;
+  }
+
+  @include e(suggestion-list) {
+    max-height: 204px;
+    margin: 0;
+    padding: 6px 0;
+    font-size: $--font-size-base;
+    color: $--cascader-menu-font-color;
+    text-align: center;
+  }
+
+  @include e(suggestion-item) {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    height: 34px;
+    padding: 0 15px;
+    text-align: left;
+    outline: none;
+    cursor: pointer;
+
+    &:hover, &:focus {
+      background: $--cascader-node-background-hover;
     }
 
-    &:hover, &:focus:not(:active) {
-      background-color: $--select-option-hover-background;
+    &.is-checked {
+      color: $--cascader-menu-selected-font-color;
+      font-weight: bold;
     }
 
-    &.selected {
-      color: $--color-white;
-      background-color: $--select-option-selected-hover;
+    > span {
+      margin-right: 10px;
     }
   }
 
-  @include e(item__keyword) {
-    font-weight: bold;
+  @include e(empty-text) {
+    margin: 10px 0;
+    color: $--cascader-color-empty;
   }
 
-  @include m(flexible) {
-    height: auto;
-    max-height: 180px;
-    overflow: auto;
+  @include e(search-input) {
+    flex: 1;
+    height: 24px;
+    min-width: 60px;
+    margin: 2px 0 2px 15px;
+    padding: 0;
+    color: $--cascader-menu-font-color;
+    border: none;
+    outline: none;
+    box-sizing: border-box;
 
-    .el-cascader-menu__item {
-      overflow: visible;
+    &::placeholder {
+      color: $--color-text-placeholder;
     }
   }
 }

+ 8 - 13
packages/theme-chalk/src/common/var.scss

@@ -474,19 +474,14 @@ $--cascader-menu-font-color: $--color-text-regular !default;
 /// color||Color|0
 $--cascader-menu-selected-font-color: $--color-primary !default;
 $--cascader-menu-fill: $--fill-base !default;
-$--cascader-menu-border: $--border-base !default;
-$--cascader-menu-border-width: $--border-width-base !default;
-$--cascader-menu-color: $--color-text-regular !default;
-$--cascader-menu-option-color-active: $--color-text-secondary !default;
-$--cascader-menu-option-fill-active: rgba($--color-text-secondary, 0.12) !default;
-$--cascader-menu-option-color-hover: $--color-text-regular !default;
-$--cascader-menu-option-fill-hover: rgba($--color-text-primary, 0.06) !default;
-$--cascader-menu-option-color-disabled: #999 !default;
-$--cascader-menu-option-fill-disabled: rgba($--color-black, 0.06) !default;
-$--cascader-menu-option-empty-color: #666 !default;
-$--cascader-menu-shadow: 0 1px 2px rgba($--color-black, 0.14), 0 0 3px rgba($--color-black, 0.14) !default;
-$--cascader-menu-option-pinyin-color: #999 !default;
-$--cascader-menu-submenu-shadow: 1px 1px 2px rgba($--color-black, 0.14), 1px 0 2px rgba($--color-black, 0.14) !default;
+$--cascader-menu-font-size: $--font-size-base !default;
+$--cascader-menu-radius: $--border-radius-base !default;
+$--cascader-menu-border: solid 1px $--border-color-light !default;
+$--cascader-menu-shadow: $--box-shadow-light !default;
+$--cascader-node-background-hover: $--background-color-base !default;
+$--cascader-node-color-disabled:$--color-text-placeholder !default;
+$--cascader-color-empty:$--color-text-placeholder !default;
+$--cascader-tag-background: #f0f2f5;
 
 /* Group
 -------------------------- */

+ 2 - 0
packages/theme-chalk/src/index.scss

@@ -72,4 +72,6 @@
 @import "./image.scss";
 @import "./calendar.scss";
 @import "./backtop.scss";
+@import "./infiniteScroll.scss";
 @import "./page-header.scss";
+@import "./cascader-panel.scss";

+ 0 - 0
packages/theme-chalk/src/infiniteScroll.scss


+ 4 - 1
src/index.js

@@ -78,6 +78,7 @@ import Calendar from '../packages/calendar/index.js';
 import Backtop from '../packages/backtop/index.js';
 import InfiniteScroll from '../packages/infiniteScroll/index.js';
 import PageHeader from '../packages/page-header/index.js';
+import CascaderPanel from '../packages/cascader-panel/index.js';
 import locale from 'element-ui/src/locale';
 import CollapseTransition from 'element-ui/src/transitions/collapse-transition';
 
@@ -155,6 +156,7 @@ const components = [
   Calendar,
   Backtop,
   PageHeader,
+  CascaderPanel,
   CollapseTransition
 ];
 
@@ -272,5 +274,6 @@ export default {
   Calendar,
   Backtop,
   InfiniteScroll,
-  PageHeader
+  PageHeader,
+  CascaderPanel
 };

+ 2 - 1
src/locale/lang/af-ZA.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Geen toepaslike data',
       loading: 'Laai',
-      placeholder: 'Kies'
+      placeholder: 'Kies',
+      noData: 'Geen data'
     },
     pagination: {
       goto: 'Gaan na',

+ 2 - 1
src/locale/lang/ar.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'لايوجد بيانات مطابقة',
       loading: 'جار التحميل',
-      placeholder: 'أختر'
+      placeholder: 'أختر',
+      noData: 'لايوجد بيانات'
     },
     pagination: {
       goto: 'أذهب إلى',

+ 2 - 1
src/locale/lang/bg.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Няма намерени',
       loading: 'Зареждане',
-      placeholder: 'Избери'
+      placeholder: 'Избери',
+      noData: 'Няма данни'
     },
     pagination: {
       goto: 'Иди на',

+ 2 - 1
src/locale/lang/ca.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'No hi ha dades que coincideixin',
       loading: 'Carregant',
-      placeholder: 'Seleccionar'
+      placeholder: 'Seleccionar',
+      noData: 'Sense Dades'
     },
     pagination: {
       goto: 'Anar a',

+ 2 - 1
src/locale/lang/cs-CZ.js

@@ -69,7 +69,8 @@ export default {
     cascader: {
       noMatch: 'Žádná shoda',
       loading: 'Načítání',
-      placeholder: 'Vybrat'
+      placeholder: 'Vybrat',
+      noData: 'Žádná data'
     },
     pagination: {
       goto: 'Jít na',

+ 2 - 1
src/locale/lang/da.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Ingen matchende data',
       loading: 'Henter',
-      placeholder: 'Vælg'
+      placeholder: 'Vælg',
+      noData: 'Ingen data'
     },
     pagination: {
       goto: 'Gå til',

+ 2 - 1
src/locale/lang/de.js

@@ -69,7 +69,8 @@ export default {
     cascader: {
       noMatch: 'Nichts gefunden.',
       loading: 'Lädt.',
-      placeholder: 'Daten wählen'
+      placeholder: 'Daten wählen',
+      noData: 'Keine Daten'
     },
     pagination: {
       goto: 'Gehe zu',

+ 2 - 1
src/locale/lang/ee.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Sobivad andmed puuduvad',
       loading: 'Laadimine',
-      placeholder: 'Vali'
+      placeholder: 'Vali',
+      noData: 'Andmed puuduvad'
     },
     pagination: {
       goto: 'Mine lehele',

+ 2 - 1
src/locale/lang/el.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Δεν βρέθηκαν αποτελέσματα',
       loading: 'Φόρτωση',
-      placeholder: 'Επιλογή'
+      placeholder: 'Επιλογή',
+      noData: 'Χωρίς δεδομένα'
     },
     pagination: {
       goto: 'Μετάβαση σε',

+ 2 - 1
src/locale/lang/en.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'No matching data',
       loading: 'Loading',
-      placeholder: 'Select'
+      placeholder: 'Select',
+      noData: 'No data'
     },
     pagination: {
       goto: 'Go to',

+ 2 - 1
src/locale/lang/es.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'No hay datos que coincidan',
       loading: 'Cargando',
-      placeholder: 'Seleccionar'
+      placeholder: 'Seleccionar',
+      noData: 'Sin datos'
     },
     pagination: {
       goto: 'Ir a',

+ 2 - 1
src/locale/lang/eu.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Bat datorren daturik ez',
       loading: 'Kargatzen',
-      placeholder: 'Hautatu'
+      placeholder: 'Hautatu',
+      noData: 'Daturik ez'
     },
     pagination: {
       goto: 'Joan',

+ 2 - 1
src/locale/lang/fa.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'هیچ داده‌ای پیدا نشد',
       loading: 'بارگیری',
-      placeholder: 'انتخاب کنید'
+      placeholder: 'انتخاب کنید',
+      noData: 'اطلاعاتی وجود ندارد'
     },
     pagination: {
       goto: 'برو به',

+ 2 - 1
src/locale/lang/fi.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Ei vastaavia tietoja',
       loading: 'Lataa',
-      placeholder: 'Valitse'
+      placeholder: 'Valitse',
+      noData: 'Ei tietoja'
     },
     pagination: {
       goto: 'Mene',

+ 2 - 1
src/locale/lang/fr.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Aucune correspondance',
       loading: 'Chargement',
-      placeholder: 'Choisir'
+      placeholder: 'Choisir',
+      noData: 'Aucune donnée'
     },
     pagination: {
       goto: 'Aller à',

+ 2 - 1
src/locale/lang/he.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'ללא נתונים מתאימים',
       loading: 'טוען',
-      placeholder: 'בחר'
+      placeholder: 'בחר',
+      noData: 'ללא נתונים'
     },
     pagination: {
       goto: 'עבור ל',

+ 2 - 1
src/locale/lang/hr.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Nema pronađenih podataka',
       loading: 'Učitavanje',
-      placeholder: 'Izaberi'
+      placeholder: 'Izaberi',
+      noData: 'Nema podataka'
     },
     pagination: {
       goto: 'Idi na',

+ 2 - 1
src/locale/lang/hu.js

@@ -66,7 +66,8 @@ export default {
     cascader: {
       noMatch: 'Nincs találat',
       loading: 'Betöltés',
-      placeholder: 'Válassz'
+      placeholder: 'Válassz',
+      noData: 'Nincs adat'
     },
     pagination: {
       goto: 'Ugrás',

+ 2 - 1
src/locale/lang/hy-AM.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Համապատասխան տուեալներ չկան',
       loading: 'Բեռնում',
-      placeholder: 'Ընտրել'
+      placeholder: 'Ընտրել',
+      noData: 'Տվյալներ չկան'
     },
     pagination: {
       goto: 'Անցնել',

+ 2 - 1
src/locale/lang/id.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Tidak ada data yg cocok',
       loading: 'Memuat',
-      placeholder: 'Pilih'
+      placeholder: 'Pilih',
+      noData: 'Tidak ada data'
     },
     pagination: {
       goto: 'Pergi ke',

+ 2 - 1
src/locale/lang/it.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Nessuna corrispondenza',
       loading: 'Caricamento',
-      placeholder: 'Seleziona'
+      placeholder: 'Seleziona',
+      noData: 'Nessun dato'
     },
     pagination: {
       goto: 'Vai a',

+ 2 - 1
src/locale/lang/ja.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'データなし',
       loading: 'ロード中',
-      placeholder: '選択してください'
+      placeholder: '選択してください',
+      noData: 'データなし'
     },
     pagination: {
       goto: '',

+ 2 - 1
src/locale/lang/kg.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Дал келген маалыматтар',
       loading: 'Жүктөлүүдө',
-      placeholder: 'тандоо'
+      placeholder: 'тандоо',
+      noData: 'маалымат жок'
     },
     pagination: {
       goto: 'Мурунку',

+ 2 - 1
src/locale/lang/km.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'គ្មានទិន្ន័យដូច',
       loading: 'កំពុងផ្ទុក',
-      placeholder: 'ជ្រើសរើស'
+      placeholder: 'ជ្រើសរើស',
+      noData: 'គ្មានទិន្ន័យ'
     },
     pagination: {
       goto: 'ទៅកាន់',

+ 2 - 1
src/locale/lang/ko.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: '맞는 데이터가 없습니다',
       loading: '불러오는 중',
-      placeholder: '선택'
+      placeholder: '선택',
+      noData: '데이터 없음'
     },
     pagination: {
       goto: '이동',

+ 2 - 1
src/locale/lang/ku.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Li hembere ve agahî tune',
       loading: 'Bardibe',
-      placeholder: 'Bibijêre'
+      placeholder: 'Bibijêre',
+      noData: 'Agahî tune'
     },
     pagination: {
       goto: 'Biçe',

+ 2 - 1
src/locale/lang/kz.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Сәйкес деректер жоқ',
       loading: 'Жүктелуде',
-      placeholder: 'Таңдаңыз'
+      placeholder: 'Таңдаңыз',
+      noData: 'Деректер жоқ'
     },
     pagination: {
       goto: 'Бару',

+ 2 - 1
src/locale/lang/lt.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Duomenų nerasta',
       loading: 'Kraunasi',
-      placeholder: 'Pasirink'
+      placeholder: 'Pasirink',
+      noData: 'Nėra duomenų'
     },
     pagination: {
       goto: 'Eiti į',

+ 2 - 1
src/locale/lang/lv.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Nav atbilstošu datu',
       loading: 'Ielādē',
-      placeholder: 'Izvēlēties'
+      placeholder: 'Izvēlēties',
+      noData: 'Nav datu'
     },
     pagination: {
       goto: 'Iet uz',

+ 2 - 1
src/locale/lang/mn.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Тохирох өгөгдөл байхгүй',
       loading: 'Ачаалж байна',
-      placeholder: 'Сонгох'
+      placeholder: 'Сонгох',
+      noData: 'Өгөгдөл байхгүй'
     },
     pagination: {
       goto: 'Очих',

+ 2 - 1
src/locale/lang/nb-NO.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Ingen samsvarende data',
       loading: 'Laster',
-      placeholder: 'Velg'
+      placeholder: 'Velg',
+      noData: 'Ingen data'
     },
     pagination: {
       goto: 'Gå til',

+ 2 - 1
src/locale/lang/nl.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Geen overeenkomende resultaten',
       loading: 'Laden',
-      placeholder: 'Selecteer'
+      placeholder: 'Selecteer',
+      noData: 'Geen data'
     },
     pagination: {
       goto: 'Ga naar',

+ 2 - 1
src/locale/lang/pl.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Brak dopasowań',
       loading: 'Ładowanie',
-      placeholder: 'Wybierz'
+      placeholder: 'Wybierz',
+      noData: 'Brak danych'
     },
     pagination: {
       goto: 'Idź do',

+ 2 - 1
src/locale/lang/pt-br.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Sem resultados',
       loading: 'Carregando',
-      placeholder: 'Selecione'
+      placeholder: 'Selecione',
+      noData: 'Sem dados'
     },
     pagination: {
       goto: 'Ir para',

+ 2 - 1
src/locale/lang/pt.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Sem correspondência',
       loading: 'A carregar',
-      placeholder: 'Selecione'
+      placeholder: 'Selecione',
+      noData: 'Sem dados'
     },
     pagination: {
       goto: 'Ir para',

+ 2 - 1
src/locale/lang/ro.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Nu există date potrivite',
       loading: 'Se încarcă',
-      placeholder: 'Selectează'
+      placeholder: 'Selectează',
+      noData: 'Nu există date'
     },
     pagination: {
       goto: 'Go to',

+ 2 - 1
src/locale/lang/ru-RU.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Совпадений не найдено',
       loading: 'Загрузка',
-      placeholder: 'Выбрать'
+      placeholder: 'Выбрать',
+      noData: 'Нет данных'
     },
     pagination: {
       goto: 'Перейти',

+ 2 - 1
src/locale/lang/sk.js

@@ -69,7 +69,8 @@ export default {
     cascader: {
       noMatch: 'Žiadna zhoda',
       loading: 'Načítavanie',
-      placeholder: 'Vybrať'
+      placeholder: 'Vybrať',
+      noData: 'Žiadne dáta'
     },
     pagination: {
       goto: 'Choď na',

+ 2 - 1
src/locale/lang/sl.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Ni ustreznih podatkov',
       loading: 'Nalaganje',
-      placeholder: 'Izberi'
+      placeholder: 'Izberi',
+      noData: 'Ni podatkov'
     },
     pagination: {
       goto: 'Pojdi na',

+ 2 - 1
src/locale/lang/sr.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Нема резултата',
       loading: 'Учитавање',
-      placeholder: 'Изабери'
+      placeholder: 'Изабери',
+      noData: 'Нема података'
     },
     pagination: {
       goto: 'Иди на',

+ 2 - 1
src/locale/lang/sv-SE.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Hittade inget',
       loading: 'Laddar',
-      placeholder: 'Välj'
+      placeholder: 'Välj',
+      noData: 'Ingen data'
     },
     pagination: {
       goto: 'Gå till',

+ 2 - 1
src/locale/lang/ta.js

@@ -66,7 +66,8 @@ export default {
     cascader: {
       noMatch: 'பொருத்தமான தரவு கிடைக்கவில்லை',
       loading: 'தயாராகிக்கொண்டிருக்கிறது',
-      placeholder: 'தேர்வு செய்'
+      placeholder: 'தேர்வு செய்',
+      noData: 'தரவு இல்லை'
     },
     pagination: {
       goto: 'தேவையான் பகுதிக்கு செல்',

+ 2 - 1
src/locale/lang/th.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'ไม่พบข้อมูลที่ตรงกัน',
       loading: 'กำลังโหลด',
-      placeholder: 'เลือก'
+      placeholder: 'เลือก',
+      noData: 'ไม่พบข้อมูล'
     },
     pagination: {
       goto: 'ไปที่',

+ 2 - 1
src/locale/lang/tk.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Hiçzat tapylmady',
       loading: 'Indirilýär',
-      placeholder: 'Saýlaň'
+      placeholder: 'Saýlaň',
+      noData: 'Hiçzat ýok'
     },
     pagination: {
       goto: 'Git',

+ 2 - 1
src/locale/lang/tr-TR.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Eşleşen veri bulunamadı',
       loading: 'Yükleniyor',
-      placeholder: 'Seç'
+      placeholder: 'Seç',
+      noData: 'Veri yok'
     },
     pagination: {
       goto: 'Git',

+ 2 - 1
src/locale/lang/ua.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Співпадінь не знайдено',
       loading: 'Завантаження',
-      placeholder: 'Обрати'
+      placeholder: 'Обрати',
+      noData: 'Немає даних'
     },
     pagination: {
       goto: 'Перейти',

+ 2 - 1
src/locale/lang/ug-CN.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'ئۇچۇر تېپىلمىدى',
       loading: 'يۈكلىنىۋاتىدۇ',
-      placeholder: 'تاللاڭ'
+      placeholder: 'تاللاڭ',
+      noData: 'ئۇچۇر يوق'
     },
     pagination: {
       goto: 'كىيىنكى بەت',

+ 2 - 1
src/locale/lang/vi.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: 'Dữ liệu không phù hợp',
       loading: 'Đang tải',
-      placeholder: 'Chọn'
+      placeholder: 'Chọn',
+      noData: 'Không tìm thấy dữ liệu'
     },
     pagination: {
       goto: 'Nhảy tới',

+ 2 - 1
src/locale/lang/zh-CN.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: '无匹配数据',
       loading: '加载中',
-      placeholder: '请选择'
+      placeholder: '请选择',
+      noData: '暂无数据'
     },
     pagination: {
       goto: '前往',

+ 2 - 1
src/locale/lang/zh-TW.js

@@ -67,7 +67,8 @@ export default {
     cascader: {
       noMatch: '無匹配資料',
       loading: '加載中',
-      placeholder: '請選擇'
+      placeholder: '請選擇',
+      noData: '無資料'
     },
     pagination: {
       goto: '前往',

+ 2 - 1
src/utils/aria-utils.js

@@ -115,7 +115,8 @@ aria.Utils.keys = {
   left: 37,
   up: 38,
   right: 39,
-  down: 40
+  down: 40,
+  esc: 27
 };
 
 export default aria.Utils;

+ 73 - 0
src/utils/util.js

@@ -1,4 +1,5 @@
 import Vue from 'vue';
+import { isString, isObject } from 'element-ui/src/utils/types';
 
 const hasOwnProperty = Object.prototype.hasOwnProperty;
 
@@ -143,3 +144,75 @@ export const kebabCase = function(str) {
     .replace(hyphenateRE, '$1-$2')
     .toLowerCase();
 };
+
+export const capitalize = function(str) {
+  if (!isString(str)) return str;
+  return str.charAt(0).toUpperCase() + str.slice(1);
+};
+
+export const looseEqual = function(a, b) {
+  const isObjectA = isObject(a);
+  const isObjectB = isObject(b);
+  if (isObjectA && isObjectB) {
+    return JSON.stringify(a) === JSON.stringify(b);
+  } else if (!isObjectA && !isObjectB) {
+    return String(a) === String(b);
+  } else {
+    return false;
+  }
+};
+
+export const arrayEquals = function(arrayA, arrayB) {
+  arrayA = arrayA || [];
+  arrayB = arrayB || [];
+
+  if (arrayA.length !== arrayB.length) {
+    return false;
+  }
+
+  for (let i = 0; i < arrayA.length; i++) {
+    if (!looseEqual(arrayA[i], arrayB[i])) {
+      return false;
+    }
+  }
+
+  return true;
+};
+
+export const isEqual = function(value1, value2) {
+  if (Array.isArray(value1) && Array.isArray(value2)) {
+    return arrayEquals(value1, value2);
+  }
+  return looseEqual(value1, value2);
+};
+
+export const isEmpty = function(val) {
+  // null or undefined
+  if (val == null) return true;
+
+  if (typeof val === 'boolean') return false;
+
+  if (typeof val === 'number') return !val;
+
+  if (val instanceof Error) return val.message === '';
+
+  switch (Object.prototype.toString.call(val)) {
+    // String or Array
+    case '[object String]':
+    case '[object Array]':
+      return !val.length;
+
+    // Map or Set or File
+    case '[object File]':
+    case '[object Map]':
+    case '[object Set]': {
+      return !val.size;
+    }
+    // Plain Object
+    case '[object Object]': {
+      return !Object.keys(val).length;
+    }
+  }
+
+  return false;
+};

+ 536 - 0
test/unit/specs/cascader-panel.spec.js

@@ -0,0 +1,536 @@
+import {
+  createTest,
+  createVue,
+  destroyVM,
+  waitImmediate,
+  wait,
+  triggerEvent
+} from '../util';
+import CascaderPanel from 'packages/cascader-panel';
+
+const selectedValue = ['zhejiang', 'hangzhou', 'xihu'];
+
+const options = [{
+  value: 'zhejiang',
+  label: 'Zhejiang',
+  children: [{
+    value: 'hangzhou',
+    label: 'Hangzhou',
+    children: [{
+      value: 'xihu',
+      label: 'West Lake'
+    }, {
+      value: 'binjiang',
+      label: 'Bin Jiang'
+    }]
+  }, {
+    value: 'ningbo',
+    label: 'NingBo',
+    children: [{
+      value: 'jiangbei',
+      label: 'Jiang Bei'
+    }, {
+      value: 'jiangdong',
+      label: 'Jiang Dong',
+      disabled: true
+    }]
+  }]
+}, {
+  value: 'jiangsu',
+  label: 'Jiangsu',
+  disabled: true,
+  children: [{
+    value: 'nanjing',
+    label: 'Nanjing',
+    children: [{
+      value: 'zhonghuamen',
+      label: 'Zhong Hua Men'
+    }]
+  }]
+}];
+
+const options2 = [{
+  id: 'zhejiang',
+  name: 'Zhejiang',
+  areas: [{
+    id: 'hangzhou',
+    name: 'Hangzhou',
+    areas: [{
+      id: 'xihu',
+      name: 'West Lake'
+    }, {
+      id: 'binjiang',
+      name: 'Bin Jiang'
+    }]
+  }, {
+    id: 'ningbo',
+    name: 'NingBo',
+    areas: [{
+      id: 'jiangbei',
+      label: 'Jiang Bei'
+    }, {
+      id: 'jiangdong',
+      name: 'Jiang Dong',
+      invalid: true
+    }]
+  }]
+}, {
+  id: 'jiangsu',
+  name: 'Jiangsu',
+  invalid: true,
+  areas: [{
+    id: 'nanjing',
+    name: 'Nanjing',
+    areas: [{
+      id: 'zhonghuamen',
+      name: 'Zhong Hua Men'
+    }]
+  }]
+}];
+
+const getMenus = el => el.querySelectorAll('.el-cascader-menu');
+const getOptions = (el, menuIndex) => getMenus(el)[menuIndex].querySelectorAll('.el-cascader-node');
+const getValidOptions = (el, menuIndex) => getMenus(el)[menuIndex].querySelectorAll('.el-cascader-node[tabindex="-1"]');
+const getLabel = el => el.querySelector('.el-cascader-node__label').textContent;
+
+describe('CascaderPanel', () => {
+  let vm;
+  afterEach(() => {
+    destroyVM(vm);
+  });
+
+  it('create', () => {
+    vm = createTest(CascaderPanel, true);
+    expect(vm.$el).to.exist;
+  });
+
+  it('expand and check', async() => {
+    vm = createVue({
+      template: `
+        <el-cascader-panel
+          ref="panel"
+          v-model="value"
+          :options="options"></el-cascader-panel>
+      `,
+      data() {
+        return {
+          value: [],
+          options
+        };
+      }
+    }, true);
+
+    const el = vm.$el;
+    const expandHandler = sinon.spy();
+    const changeHandler = sinon.spy();
+    vm.$refs.panel.$on('expand-change', expandHandler);
+    vm.$refs.panel.$on('change', changeHandler);
+
+    expect(getMenus(el).length).to.equal(1);
+    expect(getOptions(el, 0).length).to.equal(2);
+
+    const firstOption = getOptions(el, 0)[0];
+    expect(getLabel(firstOption)).to.equal('Zhejiang');
+    firstOption.click();
+    await waitImmediate();
+    expect(expandHandler.calledOnceWith(['zhejiang'])).to.be.true;
+    expect(getMenus(el).length).to.equal(2);
+
+    getOptions(el, 1)[0].click();
+    await waitImmediate();
+    expect(getMenus(el).length).to.equal(3);
+
+    getOptions(el, 2)[0].click();
+    await waitImmediate();
+    expect(changeHandler.calledOnceWith(selectedValue)).to.be.true;
+    expect(vm.value).to.deep.equal(selectedValue);
+  });
+
+  it('with default value', async() => {
+    vm = createVue({
+      template: `
+        <el-cascader-panel
+          ref="panel"
+          v-model="value"
+          :options="options"></el-cascader-panel>
+      `,
+      data() {
+        return {
+          value: selectedValue,
+          options
+        };
+      }
+    }, true);
+
+    const el = vm.$el;
+
+    await waitImmediate();
+    expect(getMenus(el).length).to.equal(3);
+    expect(getOptions(el, 0)[0].className).to.includes('in-active-path');
+    expect(getOptions(el, 2)[0].className).to.includes('is-active');
+    expect(getOptions(el, 2)[0].querySelector('.el-icon-check')).to.exist;
+  });
+
+  it('disabled options', async() => {
+    vm = createVue({
+      template: `
+        <el-cascader-panel
+          ref="panel"
+          :value="value"
+          :options="options"></el-cascader-panel>
+      `,
+      data() {
+        return {
+          value: [],
+          options
+        };
+      }
+    }, true);
+
+    const el = vm.$el;
+    const expandHandler = sinon.spy();
+    vm.$refs.panel.$on('expand-change', expandHandler);
+
+    expect(getOptions(el, 0).length).to.equal(2);
+    expect(getValidOptions(el, 0).length).to.equal(1);
+
+    const secondOption = getOptions(el, 0)[1];
+    expect(secondOption.className).to.includes('is-disabled');
+    secondOption.click();
+
+    await waitImmediate();
+    expect(expandHandler.called).to.be.false;
+    expect(getMenus(el).length).to.equal(1);
+  });
+
+  it('expand by hover', async() => {
+    vm = createVue({
+      template: `
+        <el-cascader-panel
+          :options="options"
+          :props="props"></el-cascader-panel>
+      `,
+      data() {
+        return {
+          options,
+          props: {
+            expandTrigger: 'hover'
+          }
+        };
+      }
+    }, true);
+
+    const el = vm.$el;
+    triggerEvent(getOptions(el, 0)[1], 'mouseenter');
+    await waitImmediate();
+    expect(getMenus(el).length).to.equal(1);
+    triggerEvent(getOptions(el, 0)[0], 'mouseenter');
+    await waitImmediate();
+    expect(getMenus(el).length).to.equal(2);
+    triggerEvent(getOptions(el, 1)[0], 'mouseenter');
+    await waitImmediate();
+    expect(getMenus(el).length).to.equal(3);
+  });
+
+  it('emit value only', async() => {
+    vm = createVue({
+      template: `
+        <el-cascader-panel
+          ref="panel"
+          v-model="value"
+          :options="options"
+          :props="props"></el-cascader-panel>
+      `,
+      data() {
+        return {
+          value: 'xihu',
+          options,
+          props: {
+            emitPath: false
+          }
+        };
+      }
+    }, true);
+
+    const el = vm.$el;
+
+    await waitImmediate();
+    expect(getMenus(el).length).to.equal(3);
+    expect(getOptions(el, 2)[0].querySelector('.el-icon-check')).to.exist;
+
+    getOptions(el, 1)[1].click();
+    await waitImmediate();
+    getOptions(el, 2)[0].click();
+    await waitImmediate();
+    expect(vm.value).to.equal('jiangbei');
+  });
+
+  it('multiple mode', async() => {
+    vm = createVue({
+      template: `
+        <el-cascader-panel
+          v-model="value"
+          :options="options"
+          :props="props"></el-cascader-panel>
+      `,
+      data() {
+        return {
+          value: [],
+          options: options,
+          props: {
+            multiple: true
+          }
+        };
+      }
+    }, true);
+
+    const el = vm.$el;
+    const checkbox = getOptions(el, 0)[0].querySelector('.el-checkbox');
+    expect(checkbox).to.exist;
+    expect(checkbox.querySelector('.el-checkbox__input').className).to.not.includes('is-checked');
+    checkbox.querySelector('input').click();
+
+    await waitImmediate();
+    expect(checkbox.querySelector('.el-checkbox__input').className).to.includes('is-checked');
+    expect(vm.value.length).to.equal(3);
+  });
+
+  it('multiple mode with disabled default value', async() => {
+    vm = createVue({
+      template: `
+        <el-cascader-panel
+          v-model="value"
+          :options="options"
+          :props="props"></el-cascader-panel>
+      `,
+      data() {
+        return {
+          value: [['zhejiang', 'ningbo', 'jiangdong']],
+          options: options,
+          props: {
+            multiple: true
+          }
+        };
+      }
+    }, true);
+
+    const el = vm.$el;
+    const checkbox = getOptions(el, 0)[0].querySelector('.el-checkbox');
+
+    await waitImmediate();
+    expect(checkbox).to.exist;
+    expect(checkbox.querySelector('.el-checkbox__input').className).to.includes('is-indeterminate');
+    checkbox.querySelector('input').click();
+
+    await waitImmediate();
+    expect(checkbox.querySelector('.el-checkbox__input').className).to.includes('is-checked');
+    expect(vm.value.length).to.equal(4);
+
+    getOptions(el, 1)[1].click();
+    await waitImmediate();
+    getOptions(el, 2)[1].querySelector('input').click();
+    await waitImmediate();
+    expect(vm.value.length).to.equal(4);
+  });
+
+  it('check strictly in single mode', async() => {
+    vm = createVue({
+      template: `
+        <el-cascader-panel
+          v-model="value"
+          :options="options"
+          :props="props"></el-cascader-panel>
+      `,
+      data() {
+        return {
+          value: ['zhejiang'],
+          options: options,
+          props: {
+            checkStrictly: true
+          }
+        };
+      }
+    }, true);
+
+    const el = vm.$el;
+    const radio = getOptions(el, 0)[0].querySelector('.el-radio');
+
+    await waitImmediate();
+    expect(radio).to.exist;
+    expect(radio.className).to.includes('is-checked');
+
+    getOptions(el, 0)[0].click();
+    await waitImmediate();
+    getOptions(el, 1)[0].querySelector('input').click();
+    await waitImmediate();
+    expect(vm.value).to.deep.equal(['zhejiang', 'hangzhou']);
+    expect(getOptions(el, 0)[1].querySelector('.el-radio').className).to.includes('is-disabled');
+  });
+
+  it('check strictly in multiple mode', async() => {
+    vm = createVue({
+      template: `
+        <el-cascader-panel
+          v-model="value"
+          :options="options"
+          :props="props"></el-cascader-panel>
+      `,
+      data() {
+        return {
+          value: [['zhejiang']],
+          options: options,
+          props: {
+            multiple: true,
+            checkStrictly: true,
+            emitPath: false
+          }
+        };
+      }
+    }, true);
+
+    const el = vm.$el;
+    const checkbox = getOptions(el, 0)[0].querySelector('.el-checkbox');
+
+    await waitImmediate();
+    expect(checkbox).to.exist;
+    expect(checkbox.className).to.includes('is-checked');
+
+    getOptions(el, 0)[0].click();
+    await waitImmediate();
+    expect(getOptions(el, 1)[0].querySelector('.el-checkbox').className).to.not.includes('is-checked');
+    getOptions(el, 1)[0].querySelector('input').click();
+    await waitImmediate();
+    expect(vm.value).to.deep.equal(['zhejiang', 'hangzhou']);
+    expect(getOptions(el, 0)[1].querySelector('.el-checkbox').className).to.includes('is-disabled');
+  });
+
+  it('custom props', async() => {
+    vm = createVue({
+      template: `
+        <el-cascader-panel
+          v-model="value"
+          :options="options"
+          :props="props"></el-cascader-panel>
+      `,
+      data() {
+        return {
+          value: [],
+          options: options2,
+          props: {
+            value: 'id',
+            label: 'name',
+            children: 'areas',
+            disabled: 'invalid'
+          }
+        };
+      }
+    }, true);
+
+    const el = vm.$el;
+
+    expect(getMenus(el).length).to.equal(1);
+    expect(getOptions(el, 0).length).to.equal(2);
+    expect(getValidOptions(el, 0).length).to.equal(1);
+
+    const firstOption = getOptions(el, 0)[0];
+    expect(getLabel(firstOption)).to.equal('Zhejiang');
+    firstOption.click();
+    await waitImmediate();
+    expect(getMenus(el).length).to.equal(2);
+
+    getOptions(el, 1)[0].click();
+    await waitImmediate();
+    expect(getMenus(el).length).to.equal(3);
+
+    getOptions(el, 2)[0].click();
+    await waitImmediate();
+    expect(vm.value).to.deep.equal(selectedValue);
+  });
+
+  it('value key is same as label key', async() => {
+    vm = createVue({
+      template: `
+        <el-cascader-panel
+          v-model="value"
+          :options="options"
+          :props="props"></el-cascader-panel>
+      `,
+      data() {
+        return {
+          value: [],
+          options,
+          props: {
+            label: 'value'
+          }
+        };
+      }
+    }, true);
+
+    const el = vm.$el;
+
+    expect(getMenus(el).length).to.equal(1);
+    expect(getOptions(el, 0).length).to.equal(2);
+    expect(getValidOptions(el, 0).length).to.equal(1);
+
+    const firstOption = getOptions(el, 0)[0];
+    expect(getLabel(firstOption)).to.equal('zhejiang');
+    firstOption.click();
+    await waitImmediate();
+    expect(getMenus(el).length).to.equal(2);
+
+    getOptions(el, 1)[0].click();
+    await waitImmediate();
+    expect(getMenus(el).length).to.equal(3);
+
+    getOptions(el, 2)[0].click();
+    await waitImmediate();
+    expect(vm.value).to.deep.equal(selectedValue);
+  });
+
+  it('dynamic loading', async() => {
+    vm = createVue({
+      template: `
+        <el-cascader-panel
+          v-model="value"
+          :props="props"></el-cascader-panel>
+      `,
+      data() {
+        let id = 0;
+        return {
+          value: [],
+          props: {
+            lazy: true,
+            lazyLoad(node, resolve) {
+              const { level } = node;
+              setTimeout(() => {
+                const nodes = Array.from({ length: level + 1 })
+                  .map(() => ({
+                    value: ++id,
+                    label: `选项${id}`,
+                    leaf: level >= 2
+                  }));
+                resolve(nodes);
+              }, 1000);
+            }
+          }
+        };
+      }
+    }, true);
+
+    const el = vm.$el;
+    await wait(1000);
+    const firstOption = getOptions(el, 0)[0];
+    firstOption.click();
+    await waitImmediate();
+    expect(firstOption.querySelector('i').className).to.includes('el-icon-loading');
+    await wait(1000);
+    expect(firstOption.querySelector('i').className).to.includes('el-icon-arrow-right');
+    expect(getMenus(el).length).to.equal(2);
+    getOptions(el, 1)[0].click();
+    await wait(1000);
+    getOptions(el, 2)[0].click();
+    await waitImmediate();
+    expect(vm.value.length).to.equal(3);
+  });
+});
+

+ 276 - 720
test/unit/specs/cascader.spec.js

@@ -1,4 +1,55 @@
-import { createVue, destroyVM, triggerEvent, triggerClick } from '../util';
+import {
+  createTest,
+  createVue,
+  destroyVM,
+  waitImmediate,
+  wait,
+  triggerEvent
+} from '../util';
+import Cascader from 'packages/cascader';
+
+const options = [{
+  value: 'zhejiang',
+  label: 'Zhejiang',
+  children: [{
+    value: 'hangzhou',
+    label: 'Hangzhou',
+    children: [{
+      value: 'xihu',
+      label: 'West Lake'
+    }, {
+      value: 'binjiang',
+      label: 'Bin Jiang'
+    }]
+  }, {
+    value: 'ningbo',
+    label: 'NingBo',
+    children: [{
+      value: 'jiangbei',
+      label: 'Jiang Bei'
+    }, {
+      value: 'jiangdong',
+      label: 'Jiang Dong',
+      disabled: true
+    }]
+  }]
+}, {
+  value: 'jiangsu',
+  label: 'Jiangsu',
+  disabled: true,
+  children: [{
+    value: 'nanjing',
+    label: 'Nanjing',
+    children: [{
+      value: 'zhonghuamen',
+      label: 'Zhong Hua Men'
+    }]
+  }]
+}];
+
+const getMenus = el => el.querySelectorAll('.el-cascader-menu');
+const getOptions = (el, menuIndex) => getMenus(el)[menuIndex].querySelectorAll('.el-cascader-node');
+const selectedValue = ['zhejiang', 'hangzhou', 'xihu'];
 
 describe('Cascader', () => {
   let vm;
@@ -6,847 +57,352 @@ describe('Cascader', () => {
     destroyVM(vm);
   });
 
-  it('create', done => {
-    vm = createVue({
+  it('create', () => {
+    vm = createTest(Cascader, true);
+    expect(vm.$el).to.exist;
+  });
+
+  it('toggle dropdown visible', async() => {
+    vm = createTest(Cascader, true);
+    expect(vm.$refs.popper.style.display).to.equal('none');
+    vm.$el.click();
+    await waitImmediate();
+    expect(vm.$refs.popper.style.display).to.includes('');
+    vm.$el.click();
+    await wait(500);
+    expect(vm.$refs.popper.style.display).to.includes('none');
+  });
+
+  it('expand and check', async() => {
+    vm = createTest({
       template: `
         <el-cascader
           ref="cascader"
-          placeholder="请选择"
-          :options="options"
-          clearable
-          v-model="selectedOptions"
-        ></el-cascader>
+          v-model="value"
+          :options="options"></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: []
+          value: [],
+          options
         };
       }
     }, 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(3); // two menus and an arrow
-        expect(item1.classList.contains('is-active')).to.be.true;
 
-        const item2 = menuElm.children[2].querySelector('.el-cascader-menu__item');
-        item2.click();
-
-        menu.$nextTick(_ => {
-          expect(menuElm.children.length).to.be.equal(4);
-          expect(item2.classList.contains('is-active')).to.be.true;
-
-          const item3 = menuElm.children[3].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');
-            expect(vm.$refs.cascader.$el.querySelector('.el-input__inner').value).to.be.equal('');
+    const { body } = document;
+    const expandHandler = sinon.spy();
+    const changeHandler = sinon.spy();
+
+    vm.$refs.cascader.$on('expand-change', expandHandler);
+    vm.$refs.cascader.$on('change', changeHandler);
+
+    getOptions(body, 0)[0].click();
+    await waitImmediate();
+    expect(expandHandler.calledOnceWith(['zhejiang'])).to.be.true;
+    getOptions(body, 1)[0].click();
+    await waitImmediate();
+    const checkedOption = getOptions(body, 2)[0];
+    checkedOption.click();
+    await waitImmediate();
+    expect(changeHandler.calledOnceWith(selectedValue)).to.be.true;
+    expect(vm.value).to.deep.equal(selectedValue);
+    expect(checkedOption.querySelector('i.el-icon-check')).to.exist;
+    expect(vm.$el.querySelector('input').value).to.equal('Zhejiang / Hangzhou / West Lake');
+  });
 
-            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('disabled', async() => {
+    vm = createTest(Cascader, {
+      disabled: true
+    }, true);
+    expect(vm.$el.className).to.includes('is-disabled');
+    vm.$el.click();
+    await waitImmediate();
+    expect(vm.$refs.popper.style.display).to.includes('none');
   });
-  // Github issue #3470
-  it('should work with zero', done => {
+
+  it('with default value', async() => {
     vm = createVue({
       template: `
         <el-cascader
-          ref="cascader"
-          placeholder="请选择"
-          :options="options"
-          clearable
-          v-model="selectedOptions"
-        ></el-cascader>
+          v-model="value"
+          :options="options"></el-cascader>
       `,
       data() {
         return {
-          options: [{
-            value: 'zhejiang',
-            label: 'Zhejiang',
-            children: [{
-              value: 0,
-              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: []
+          value: selectedValue,
+          options
         };
       }
     }, 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(3);
-        expect(item1.classList.contains('is-active')).to.be.true;
-
-        const item2 = menuElm.children[2].querySelector('.el-cascader-menu__item');
-        item2.click();
-
-        menu.$nextTick(_ => {
-          expect(menuElm.children.length).to.be.equal(4);
-          expect(item2.classList.contains('is-active')).to.be.true;
-
-          const item3 = menuElm.children[3].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(0);
-            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);
+    const el = vm.$el;
+    await waitImmediate();
+    expect(getMenus(el).length).to.equal(3);
+    expect(getOptions(el, 2)[0].querySelector('i').className).to.includes('el-icon-check');
+    expect(vm.$el.querySelector('input').value).to.equal('Zhejiang / Hangzhou / West Lake');
   });
-  it('not allow clearable', done => {
+
+  it('async set selected value', async() => {
     vm = createVue({
       template: `
         <el-cascader
-          ref="cascader"
-          placeholder="请选择"
-          :options="options"
-          :clearable="false"
-          v-model="selectedOptions"
-        ></el-cascader>
+          v-model="value"
+          :options="options"></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: []
+          value: [],
+          options
         };
       }
     }, 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();
-    });
+
+    const el = vm.$el;
+    vm.value = selectedValue;
+    await waitImmediate();
+    expect(getMenus(el).length).to.equal(3);
+    expect(getOptions(el, 2)[0].querySelector('i').className).to.includes('el-icon-check');
+    expect(vm.$el.querySelector('input').value).to.equal('Zhejiang / Hangzhou / West Lake');
   });
-  it('disabled options', done => {
+
+  it('default value with async options', async() => {
     vm = createVue({
       template: `
         <el-cascader
-          ref="cascader"
-          placeholder="请选择"
-          :options="options"
-          v-model="selectedOptions"
-        ></el-cascader>
+          v-model="value"
+          :options="options"></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: []
+          value: selectedValue,
+          options: []
         };
       }
     }, 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.false;
-        done();
-      });
-    }, 300);
+    const el = vm.$el;
+    vm.options = options;
+    await waitImmediate();
+    expect(getMenus(el).length).to.equal(3);
+    expect(getOptions(el, 2)[0].querySelector('i').className).to.includes('el-icon-check');
+    expect(vm.$el.querySelector('input').value).to.equal('Zhejiang / Hangzhou / West Lake');
   });
-  it('default value', done => {
+
+  it('clearable', async() => {
     vm = createVue({
       template: `
         <el-cascader
-          ref="cascader"
-          placeholder="请选择"
+          v-model="value"
           :options="options"
-          v-model="selectedOptions"
-        ></el-cascader>
+          clearable></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']
+          value: selectedValue,
+          options
         };
       }
     }, 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[1].querySelector('.el-cascader-menu__item');
-      const item2 = menuElm.children[2].querySelector('.el-cascader-menu__item');
-      const item3 = menuElm.children[3].querySelector('.el-cascader-menu__item');
 
-      expect(menuElm.children.length).to.be.equal(4);
-      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;
-      triggerClick(document, 'mouseup');
-      setTimeout(_ => {
-        expect(document.body.querySelector('.el-cascader-menus').style.display).to.be.equal('none');
-        done();
-      }, 500);
-    }, 300);
+    triggerEvent(vm.$el, 'mouseenter');
+    await waitImmediate();
+    const closeBtn = vm.$el.querySelector('i.el-input__icon');
+    expect(closeBtn).to.exist;
+    closeBtn.click();
+    await waitImmediate();
+    expect(vm.value).to.deep.equal([]);
   });
-  it('expand by hover', done => {
+
+  it('show last level label', async() => {
     vm = createVue({
       template: `
         <el-cascader
-          ref="cascader"
-          placeholder="请选择"
+          v-model="value"
           :options="options"
-          expand-trigger="hover"
-          v-model="selectedOptions"
-        ></el-cascader>
+          :show-all-levels="false"></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: []
+          value: selectedValue,
+          options
         };
       }
     }, 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(3);
-        expect(item1.classList.contains('is-active')).to.be.true;
 
-        const item2 = menuElm.children[2].querySelector('.el-cascader-menu__item');
-        triggerEvent(item2, 'mouseenter');
-
-        menu.$nextTick(_ => {
-          expect(menuElm.children.length).to.be.equal(4);
-          expect(item2.classList.contains('is-active')).to.be.true;
-
-          const item3 = menuElm.children[3].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);
+    const el = vm.$el;
+    await waitImmediate();
+    expect(getMenus(el).length).to.equal(3);
+    expect(getOptions(el, 2)[0].querySelector('i').className).to.includes('el-icon-check');
+    expect(vm.$el.querySelector('input').value).to.equal('West Lake');
   });
-  it('change on select', done => {
+
+  it('multiple mode', async() => {
     vm = createVue({
       template: `
         <el-cascader
-          ref="cascader"
-          placeholder="请选择"
+          v-model="value"
           :options="options"
-          change-on-select
-          v-model="selectedOptions"
-        ></el-cascader>
+          :props="props"></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: []
+          value: [],
+          options,
+          props: {
+            multiple: true
+          }
         };
       }
     }, 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(3);
-        expect(item1.classList.contains('is-active')).to.be.true;
-        expect(vm.selectedOptions[0]).to.be.equal('zhejiang');
-
-        const item2 = menuElm.children[2].querySelector('.el-cascader-menu__item');
-        item2.click();
-
-        menu.$nextTick(_ => {
-          expect(menuElm.children.length).to.be.equal(4);
-          expect(item2.classList.contains('is-active')).to.be.true;
-          expect(vm.selectedOptions[1]).to.be.equal('hangzhou');
 
-          const item3 = menuElm.children[3].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);
+    getOptions(document.body, 0)[0].querySelector('.el-checkbox input').click();
+    await waitImmediate();
+    expect(vm.value.length).to.equal(3);
+
+    const tags = vm.$el.querySelectorAll('.el-tag');
+    const closeBtn = tags[0].querySelector('.el-tag__close');
+    expect(tags.length).to.equal(3);
+    expect(closeBtn).to.exist;
+    closeBtn.click();
+    await waitImmediate();
+    expect(vm.value.length).to.equal(2);
+    expect(vm.$el.querySelectorAll('.el-tag').length).to.equal(2);
   });
-  it('hover and select', done => {
+
+  it('clearable in multiple mode', async() => {
     vm = createVue({
       template: `
         <el-cascader
-          ref="cascader"
-          placeholder="请选择"
+          v-model="value"
           :options="options"
-          expand-trigger="hover"
-          change-on-select
-          v-model="selectedOptions"
-        ></el-cascader>
+          :props="props"
+          clearable></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: []
+          value: [],
+          options,
+          props: {
+            multiple: true,
+            emitPath: false
+          }
         };
       }
     }, true);
-    vm.$el.click();
-    vm.$nextTick(() => {
-      const menu = vm.$refs.cascader.menu;
-      const menuElm = menu.$el;
-      const item1 = menuElm.querySelector('.el-cascader-menu__item');
-
-      triggerEvent(item1, 'mouseenter');
-      menu.$nextTick(() => {
-        expect(vm.selectedOptions[0]).to.be.equal('zhejiang');
-
-        const spy = sinon.spy();
-        menu.$on('closeInside', spy);
-        item1.click();
-
-        menu.$nextTick(() => {
-          expect(spy.calledWith(true)).to.be.true;
-          expect(menu.visible).to.be.false;
-          done();
-        });
-      });
-    });
+    vm.value = ['xihu', 'binjiang', 'jiangbei', 'jiangdong'];
+    await waitImmediate();
+    expect(getOptions(document.body, 0)[0].querySelector('.el-checkbox.is-checked')).to.exist;
+    triggerEvent(vm.$el, 'mouseenter');
+    await waitImmediate();
+    const closeBtn = vm.$el.querySelector('i.el-input__icon');
+    expect(closeBtn).to.exist;
+    closeBtn.click();
+    await waitImmediate();
+    expect(vm.value.length).to.equal(1);
   });
-  it('filterable', done => {
+
+  it('collapse tags', async() => {
     vm = createVue({
       template: `
         <el-cascader
-          ref="cascader"
-          placeholder="请选择"
+          v-model="value"
           :options="options"
-          filterable
-          :debounce="0"
-          v-model="selectedOptions"
-        ></el-cascader>
+          :props="props"
+          collapse-tags></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: []
+          value: ['xihu', 'binjiang', 'jiangbei', 'jiangdong'],
+          options,
+          props: {
+            multiple: true,
+            emitPath: false
+          }
         };
       }
     }, true);
-    expect(vm.$el).to.be.exist;
-    vm.$el.click();
-    vm.$nextTick(_ => {
-      vm.$refs.cascader.handleInputChange('z');
-      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(2);
-        expect(menuElm.children[1].children.length).to.be.equal(3);
-
-        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);
-    });
+    await waitImmediate();
+    const tags = vm.$el.querySelectorAll('.el-tag');
+    expect(tags.length).to.equal(2);
+    expect(tags[0].querySelector('.el-tag__close')).to.exist;
+    expect(tags[1].querySelector('.el-tag__close')).to.be.null;
+    tags[0].querySelector('.el-tag__close').click();
+    expect(tags[1].textContent).to.equal('+ 3');
+    await waitImmediate();
+    expect(vm.value.length).to.equal(3);
+    vm.$el.querySelector('.el-tag .el-tag__close').click();
+    await waitImmediate();
+    vm.$el.querySelector('.el-tag .el-tag__close').click();
+    await waitImmediate();
+    expect(vm.$el.querySelector('.el-tag')).to.exist;
+    // disabled tag can not be closed
+    expect(vm.$el.querySelector('.el-tag .el-tag__close')).to.be.null;
   });
-  it('props', done => {
+
+  it('filterable', async() => {
     vm = createVue({
       template: `
         <el-cascader
-          ref="cascader"
+          v-model="value"
           :options="options"
-          :props="props"
-          v-model="selectedOptions"
-        ></el-cascader>
+          filterable></el-cascader>
       `,
       data() {
         return {
-          options: [{
-            label: 'Zhejiang',
-            cities: [{
-              label: 'Hangzhou'
-            }, {
-              label: 'NingBo'
-            }]
-          }, {
-            label: 'Jiangsu',
-            cities: [{
-              label: 'Nanjing'
-            }]
-          }],
-          props: {
-            value: 'label',
-            children: 'cities'
-          },
-          selectedOptions: []
+          value: [],
+          options
         };
       }
     }, true);
-    vm.$el.click();
-    setTimeout(_ => {
-      expect(document.body.querySelector('.el-cascader-menus')).to.be.exist;
-
-      const menu = vm.$refs.cascader.menu;
-      const menuElm = menu.$el;
-      let items = menuElm.querySelectorAll('.el-cascader-menu__item');
-      expect(items.length).to.equal(2);
-      items[0].click();
-      setTimeout(_ => {
-        items = menuElm.querySelectorAll('.el-cascader-menu__item');
-        expect(items.length).to.equal(4);
-        expect(items[items.length - 1].innerText).to.equal('NingBo');
-        done();
-      }, 100);
-    }, 100);
+    const el = vm.$el;
+    const { body } = document;
+    const input = el.querySelector('input');
+    el.click();
+    await waitImmediate();
+    input.value = 'Zhejiang';
+    triggerEvent(input, 'input');
+    await wait(300);
+    expect(body.querySelector('.el-cascader__suggestion-list')).to.exist;
+    expect(body.querySelectorAll('.el-cascader__suggestion-item').length).to.equal(3);
+    body.querySelectorAll('.el-cascader__suggestion-item')[0].click();
+    await waitImmediate();
+    expect(vm.value).to.deep.equal(selectedValue);
   });
-  it('show last level', done => {
+
+  it('filter method', async() => {
     vm = createVue({
       template: `
         <el-cascader
-          ref="cascader"
+          v-model="value"
           :options="options"
-          :show-all-levels="false"
-          v-model="selectedOptions"
-        ></el-cascader>
+          :filter-method="filterMethod"
+          filterable></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', 'ningbo', 'jiangbei']
+          value: [],
+          options
         };
+      },
+      methods: {
+        filterMethod(node, keyword) {
+          const { text, path } = node;
+          return text.includes(keyword) || path.includes(keyword);
+        }
       }
     }, true);
-    setTimeout(_ => {
-      const span = vm.$el.querySelector('.el-cascader__label');
-      expect(span.innerText).to.equal('Jiang Bei');
-      done();
-    }, 100);
-  });
-  describe('Cascader Events', () => {
-    it('event:focus & blur', done => {
-      vm = createVue({
-        template: `
-          <el-cascader
-            ref="cascader"
-            placeholder="请选择"
-            :options="options"
-            clearable
-            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);
-
-      const spyFocus = sinon.spy();
-      const spyBlur = sinon.spy();
-
-      vm.$refs.cascader.$on('focus', spyFocus);
-      vm.$refs.cascader.$on('blur', spyBlur);
-      vm.$el.querySelector('input').focus();
-      vm.$el.querySelector('input').blur();
-
-      vm.$nextTick(_ => {
-        expect(spyFocus.calledOnce).to.be.true;
-        expect(spyBlur.calledOnce).to.be.true;
-        done();
-      });
-    });
+    const el = vm.$el;
+    const { body } = document;
+    const input = el.querySelector('input');
+    el.click();
+    await waitImmediate();
+    input.value = 'Zhejiang';
+    triggerEvent(input, 'input');
+    await wait(300);
+    expect(body.querySelectorAll('.el-cascader__suggestion-item').length).to.equal(3);
+    input.value = 'xihu';
+    triggerEvent(input, 'input');
+    await wait(300);
+    expect(body.querySelector('.el-cascader__suggestion-item').textContent).to.equal('Zhejiang / Hangzhou / West Lake');
   });
 });

+ 72 - 0
types/cascader-panel.d.ts

@@ -0,0 +1,72 @@
+import { VNode, CreateElement } from 'vue';
+import { ElementUIComponent } from './component'
+
+/** Trigger mode of expanding current item */
+export type ExpandTrigger = 'click' | 'hover'
+
+/** Cascader Option */
+export interface CascaderOption {
+  label: string,
+  value: any,
+  children?: CascaderOption[],
+  disabled?: boolean,
+  leaf?: boolean
+}
+
+/** Cascader Props */
+export interface CascaderProps<V, D> {
+  expandTrigger?: ExpandTrigger,
+  multiple?: boolean,
+  checkStrictly?: boolean,
+  emitPath?: boolean,
+  lazy?: boolean,
+  lazyLoad?: (node: CascaderNode<V, D>, resolve: Resolve<D>) => void,
+  value?: string,
+  label?: string,
+  children?: string,
+  disabled?: string
+  leaf?: string
+}
+
+/** Cascader Node */
+export interface CascaderNode<V, D> {
+  uid: number,
+  data: D,
+  value: V,
+  label: string,
+  level: number,
+  isDisabled: boolean,
+  isLeaf: boolean,
+  parent: CascaderNode<V, D> | null,
+  children: CascaderNode<V, D>[]
+  config: CascaderProps<V, D>
+}
+
+type Resolve<D> = (dataList?: D[]) => void
+
+export interface CascaderPanelSlots {
+  /** Custom label content */
+  default: VNode[]
+
+  [key: string]: VNode[]
+}
+
+/** CascaderPanel Component */
+export declare class ElCascaderPanel<V = any, D = CascaderOption> extends ElementUIComponent {
+  /** Selected value */
+  value: V | V[]
+
+  /** Data of the options */
+  options: D[]
+
+  /** Configuration options */
+  props: CascaderProps<V, D>
+
+  /** Whether to add border */
+  border: boolean
+
+  /** Render function of custom label content */
+  renderLabel: (h: CreateElement, context: { node: CascaderNode<V, D>; data: D }) => VNode
+
+  $slots: CascaderPanelSlots
+}

+ 33 - 21
types/cascader.d.ts

@@ -1,29 +1,36 @@
+import { VNode } from 'vue';
 import { ElementUIComponent, ElementUIComponentSize } from './component'
+import { CascaderOption, CascaderProps, CascaderNode } from './cascader-panel';
 
-/** Trigger mode of expanding current item */
-export type ExpandTrigger = 'click' | 'hover'
+export type CascaderOption = CascaderOption
 
-/** Cascader Option */
-export interface CascaderOption {
-  label: string,
-  value: any,
-  children?: CascaderOption[],
-  disabled?: boolean
+export type CascaderProps<V, D> = CascaderProps<V, D>
+
+export type CascaderNode<V, D> = CascaderNode<V, D>
+
+export interface CascaderSlots {
+  /** Custom label content */
+  default: VNode[],
+
+  /** Empty content when no option matches */
+  empty: VNode[]
+
+  [key: string]: VNode[]
 }
 
 /** Cascader Component */
-export declare class ElCascader extends ElementUIComponent {
+export declare class ElCascader<V = any, D = CascaderOption> extends ElementUIComponent {
   /** Data of the options */
   options: CascaderOption[]
 
   /** Configuration options */
-  props: object
+  props: CascaderProps<V, D>
 
   /** Selected value */
-  value: any[]
+  value: V | V[]
 
-  /** Custom class name for Cascader's dropdown */
-  popperClass: string
+  /** Size of Input */
+  size: ElementUIComponentSize
 
   /** Input placeholder */
   placeholder: string
@@ -34,24 +41,29 @@ export declare class ElCascader extends ElementUIComponent {
   /** Whether selected value can be cleared */
   clearable: boolean
 
-  /** Trigger mode of expanding current item */
-  expandTrigger: ExpandTrigger
-
   /** Whether to display all levels of the selected value in the input */
   showAllLevels: boolean
 
+  /** Whether to collapse selected tags in multiple selection mode */
+  collapseTags: boolean
+
+  /** Separator of option labels */
+  separator: string
+
   /** Whether the options can be searched */
   filterable: boolean
 
+  /** filter method to match options according to input keyword */
+  filterMethod: (node: CascaderNode<V, D>, keyword: string) => boolean
+
   /** Debounce delay when typing filter keyword, in millisecond */
   debounce: number
 
-  /** Whether selecting an option of any level is permitted */
-  changeOnSelect: boolean
-
-  /** Size of Input */
-  size: ElementUIComponentSize
+  /** Custom class name for Cascader's dropdown */
+  popperClass: string
 
   /** Hook function before filtering with the value to be filtered as its parameter */
   beforeFilter: (value: string) => boolean | Promise<any>
+
+  $slots: CascaderSlots
 }