瀏覽代碼

Merge remote-tracking branch 'eleme/dev' into es-doc

# Conflicts:
#	examples/app.vue
Leopoldthecoder 7 年之前
父節點
當前提交
de86b737e3
共有 41 個文件被更改,包括 655 次插入125 次删除
  1. 1 1
      .eslintrc
  2. 10 0
      CHANGELOG.en-US.md
  3. 10 0
      CHANGELOG.zh-CN.md
  4. 7 7
      examples/app.vue
  5. 17 1
      examples/docs/en-US/select.md
  6. 17 1
      examples/docs/zh-CN/select.md
  7. 2 2
      examples/nav.config.json
  8. 1 1
      examples/versions.json
  9. 2 1
      package.json
  10. 37 0
      packages/cascader/src/main.vue
  11. 139 5
      packages/cascader/src/menu.vue
  12. 2 0
      packages/dropdown/src/dropdown-item.vue
  13. 106 5
      packages/dropdown/src/dropdown.vue
  14. 2 2
      packages/menu/src/menu.vue
  15. 4 2
      packages/menu/src/submenu.vue
  16. 6 29
      packages/message/src/main.vue
  17. 19 2
      packages/notification/src/main.vue
  18. 42 11
      packages/popover/src/main.vue
  19. 4 3
      packages/radio/src/radio-group.vue
  20. 2 2
      packages/select/src/option-group.vue
  21. 18 2
      packages/select/src/select.vue
  22. 4 6
      packages/table/src/table-header.js
  23. 3 2
      packages/theme-chalk/package.json
  24. 2 2
      packages/theme-chalk/src/cascader.scss
  25. 6 0
      packages/theme-chalk/src/checkbox.scss
  26. 14 6
      packages/theme-chalk/src/common/var.scss
  27. 8 2
      packages/theme-chalk/src/dropdown.scss
  28. 6 0
      packages/theme-chalk/src/popover.scss
  29. 5 0
      packages/theme-chalk/src/radio.scss
  30. 9 0
      packages/theme-chalk/src/select.scss
  31. 16 7
      packages/theme-chalk/src/table.scss
  32. 3 0
      packages/theme-chalk/src/tooltip.scss
  33. 6 1
      packages/theme-chalk/src/tree.scss
  34. 39 9
      packages/tooltip/src/main.js
  35. 3 2
      packages/tree/src/model/tree-store.js
  36. 18 5
      packages/tree/src/tree-node.vue
  37. 60 2
      packages/tree/src/tree.vue
  38. 2 2
      packages/upload/src/ajax.js
  39. 1 1
      src/index.js
  40. 1 0
      src/utils/clickoutside.js
  41. 1 1
      test/unit/specs/upload.spec.js

+ 1 - 1
.eslintrc

@@ -6,7 +6,7 @@
     "expect": true,
     "sinon": true
   },
-  "plugins": ['vue'],
+  "plugins": ['vue','json'],
   "extends": 'elemefe',
   "parserOptions": {
     "ecmaFeatures": {

+ 10 - 0
CHANGELOG.en-US.md

@@ -1,5 +1,15 @@
 ## Changelog
 
+### 2.0.4
+
+*2017-11-10*
+
+- Improved accessibility for Cascader, Dropdown, Message, Notification, Popover, Tooltip and Tree
+- Fixed Container resize when the width of viewport decreases, #8042
+- Fixed Tree's `updateKeyChildren` incorrectly deleting child nodes, #8100
+- Fixed bordered CheckboxButton's height when nested in a Form, #8100
+- Fixed Menu's parsing error for custom colors, #8153 (by @zhouyixiang)
+
 ### 2.0.3
 
 *2017-11-03*

+ 10 - 0
CHANGELOG.zh-CN.md

@@ -1,5 +1,15 @@
 ## 更新日志
 
+### 2.0.4
+
+*2017-11-10*
+
+- 提升 Cascader、Dropdown、Message、Notification、Popover、Tooltip、Tree 的可访问性
+- 修复当视口变窄时 Container 无法同步更新其宽度的问题,#8042
+- 修复 Tree 的 `updateKeyChildren` 在删除子节点时的行为错误,#8100
+- 修复带有边框的 CheckboxButton 在 Form 中高度错误的问题,#8100
+- 修复 Menu 在解析自定义颜色时的错误,#8153(by @zhouyixiang)
+
 ### 2.0.3
 
 *2017-11-03*

+ 7 - 7
examples/app.vue

@@ -191,7 +191,6 @@
 
   const lang = location.hash.replace('#', '').split('/')[1] || 'zh-CN';
   const localize = lang => {
-    console.log(lang);
     switch (lang) {
       case 'zh-CN':
         use(zhLocale);
@@ -232,6 +231,7 @@
         const preferGithub = localStorage.getItem('PREFER_GITHUB');
         if (href.indexOf('element-cn') > -1 || preferGithub) return;
         setTimeout(() => {
+          if (this.lang !== 'zh-CN') return;
           this.$confirm('建议大陆用户访问部署在国内的站点,是否跳转?', '提示')
             .then(() => {
               location.href = location.href.replace('element.', 'element-cn.');
@@ -249,12 +249,12 @@
         this.suggestJump();
       }
       setTimeout(() => {
-        const notified = localStorage.getItem('RELEASE_NOTIFIED');
-        if (!notified) {
+        const notified = localStorage.getItem('ES_NOTIFIED');
+        if (!notified && this.lang !== 'zh-CN') {
           const h = this.$createElement;
           const title = this.lang === 'zh-CN'
-            ? '2.0 正式发布'
-            : '2.0 available now';
+            ? '帮助我们完成西班牙语文档'
+            : 'Help us with Spanish docs';
           const messages = this.lang === 'zh-CN'
             ? ['点击', '这里', '查看详情']
             : ['Click ', 'here', ' to learn more'];
@@ -266,13 +266,13 @@
               h('a', {
                 attrs: {
                   target: '_blank',
-                  href: `https://github.com/ElemeFE/element/issues/${ this.lang === 'zh-CN' ? '7755' : '7756' }`
+                  href: 'https://github.com/ElemeFE/element/issues/8074'
                 }
               }, messages[1]),
               messages[2]
             ]),
             onClose() {
-              localStorage.setItem('RELEASE_NOTIFIED', 1);
+              localStorage.setItem('ES_NOTIFIED', 1);
             }
           });
         }

+ 17 - 1
examples/docs/en-US/select.md

@@ -101,6 +101,7 @@
         value8: '',
         value9: [],
         value10: [],
+        value11: [],
         loading: false,
         states: ["Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut", "Delaware", "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky", "Louisiana", "Maine", "Maryland", "Massachusetts", "Michigan", "Minnesota", "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", "New Hampshire", "New Jersey", "New Mexico", "New York", "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon", "Pennsylvania", "Rhode Island", "South Carolina", "South Dakota", "Tennessee", "Texas", "Utah", "Vermont", "Virginia", "Washington", "West Virginia", "Wisconsin", "Wyoming"]
       };
@@ -320,7 +321,7 @@ You can clear Select using a clear icon.
 
 Multiple select uses tags to display selected options.
 
-:::demo Set `multiple` attribute for `el-select` to enable multiple mode. In this case, the value of `v-model` will be an array of selected options.
+:::demo Set `multiple` attribute for `el-select` to enable multiple mode. In this case, the value of `v-model` will be an array of selected options. By default the selected options will be displayed as Tags. You can collapse them to a text by using `collapse-tags` attribute.
 ```html
 <template>
   <el-select v-model="value5" multiple placeholder="Select">
@@ -331,6 +332,20 @@ Multiple select uses tags to display selected options.
       :value="item.value">
     </el-option>
   </el-select>
+  
+  <el-select
+    v-model="value11"
+    multiple
+    collapse-tags
+    style="margin-left: 20px;"
+    placeholder="Select">
+    <el-option
+      v-for="item in options"
+      :key="item.value"
+      :label="item.label"
+      :value="item.value">
+    </el-option>
+  </el-select>
 </template>
 
 <script>
@@ -650,6 +665,7 @@ If the binding value of Select is an object, make sure to assign `value-key` as
 | value-key | unique identity key name for value, required when value is an object | string | — | value |
 | size | size of Input | string | large/small/mini | — |
 | clearable | whether single select can be cleared | boolean | — | false |
+| collapse-tags | whether to collapse tags to a text when multiple selecting | boolean | — | false |
 | multiple-limit | maximum number of options user can select when `multiple` is `true`. No limit when set to 0 | number | — | 0 |
 | name | the name attribute of select input | string | — | — |
 | placeholder | placeholder | string | — | Select |

+ 17 - 1
examples/docs/zh-CN/select.md

@@ -101,6 +101,7 @@
         value8: '',
         value9: '',
         value10: [],
+        value11: [],
         loading: false,
         states: ["Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut", "Delaware", "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky", "Louisiana", "Maine", "Maryland", "Massachusetts", "Michigan", "Minnesota", "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", "New Hampshire", "New Jersey", "New Mexico", "New York", "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon", "Pennsylvania", "Rhode Island", "South Carolina", "South Dakota", "Tennessee", "Texas", "Utah", "Vermont", "Virginia", "Washington", "West Virginia", "Wisconsin", "Wyoming"]
       };
@@ -319,7 +320,7 @@
 
 适用性较广的基础多选,用 Tag 展示已选项
 
-:::demo 为`el-select`设置`multiple`属性即可启用多选,此时`v-model`的值为当前选中值所组成的数组
+:::demo 为`el-select`设置`multiple`属性即可启用多选,此时`v-model`的值为当前选中值所组成的数组。默认情况下选中值会以 Tag 的形式展现,你也可以设置`collapse-tags`属性将它们合并为一段文字。
 ```html
 <template>
   <el-select v-model="value5" multiple placeholder="请选择">
@@ -330,6 +331,20 @@
       :value="item.value">
     </el-option>
   </el-select>
+
+  <el-select
+    v-model="value11"
+    multiple
+    collapse-tags
+    style="margin-left: 20px;"
+    placeholder="请选择">
+    <el-option
+        v-for="item in options"
+        :key="item.value"
+        :label="item.label"
+        :value="item.value">
+    </el-option>
+  </el-select>
 </template>
 
 <script>
@@ -645,6 +660,7 @@
 | value-key | 作为 value 唯一标识的键名,绑定值为对象类型时必填 | string | — | value |
 | size | 输入框尺寸 | string | large/small/mini | — |
 | clearable | 单选时是否可以清空选项 | boolean | — | false |
+| collapse-tags | 多选时是否将选中值按文字的形式展示 | boolean | — | false |
 | multiple-limit | 多选时用户最多可以选择的项目数,为 0 则不限制 | number | — | 0 |
 | name | select input 的 name 属性 | string | — | — |
 | placeholder | 占位符 | string | — | 请选择 |

+ 2 - 2
examples/nav.config.json

@@ -10,7 +10,7 @@
     },
     {
       "name": "Element Angular",
-      "href": "https://eleme.github.io/element-angular/"
+      "href": "https://element-angular.faas.ele.me/"
     },
     {
       "name": "开发指南",
@@ -260,7 +260,7 @@
     },
     {
       "name": "Element Angular",
-      "href": "https://eleme.github.io/element-angular/"
+      "href": "https://element-angular.faas.ele.me/"
     },
     {
       "name": "Development",

+ 1 - 1
examples/versions.json

@@ -1 +1 @@
-{"1.0.9":"1.0","1.1.6":"1.1","1.2.9":"1.2","1.3.7":"1.3","1.4.9":"1.4","2.0.3":"2.0"}
+{"1.0.9":"1.0","1.1.6":"1.1","1.2.9":"1.2","1.3.7":"1.3","1.4.9":"1.4","2.0.4":"2.0"}

+ 2 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "element-ui",
-  "version": "2.0.3",
+  "version": "2.0.4",
   "description": "A Component Library for Vue.js.",
   "main": "lib/element-ui.common.js",
   "files": [
@@ -82,6 +82,7 @@
     "css-loader": "^0.28.7",
     "es6-promise": "^4.0.5",
     "eslint": "^3.10.2",
+    "eslint-plugin-json": "^1.2.0",
     "extract-text-webpack-plugin": "^3.0.1",
     "file-loader": "^1.1.5",
     "file-save": "^0.2.0",

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

@@ -10,9 +10,12 @@
     ]"
     @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"
@@ -63,6 +66,7 @@ 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 } from 'element-ui/src/utils/util';
 
 const popperMixin = {
   props: {
@@ -149,6 +153,10 @@ export default {
     beforeFilter: {
       type: Function,
       default: () => (() => {})
+    },
+    hoverThreshold: {
+      type: Number,
+      default: 500
     }
   },
 
@@ -191,11 +199,15 @@ export default {
     },
     cascaderSize() {
       return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
+    },
+    id() {
+      return generateId();
     }
   },
 
   watch: {
     menuVisible(value) {
+      this.$refs.input.$refs.input.setAttribute('aria-expanded', value);
       value ? this.showMenu() : this.hideMenu();
     },
     value(value) {
@@ -204,6 +216,10 @@ export default {
     currentValue(value) {
       this.dispatch('ElFormItem', 'el.form.change', [value]);
     },
+    currentLabels(value) {
+      const inputLabel = this.showAllLevels ? value.join('/') : value[value.length - 1] ;
+      this.$refs.input.$refs.input.setAttribute('value', inputLabel);
+    },
     options: {
       deep: true,
       handler(value) {
@@ -224,10 +240,13 @@ export default {
       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) {
@@ -245,6 +264,7 @@ export default {
     hideMenu() {
       this.inputValue = '';
       this.menu.visible = false;
+      this.$refs.input.focus();
     },
     handleActiveItemChange(value) {
       this.$nextTick(_ => {
@@ -252,6 +272,23 @@ export default {
       });
       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);

+ 139 - 5
packages/cascader/src/menu.vue

@@ -1,6 +1,7 @@
 <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;
@@ -39,7 +40,8 @@
         value: [],
         expandTrigger: 'click',
         changeOnSelect: false,
-        popperClass: ''
+        popperClass: '',
+        hoverTimer: 0
       };
     },
 
@@ -94,6 +96,9 @@
           formatOptions(optionsCopy);
           return loadActiveOptions(optionsCopy);
         }
+      },
+      id() {
+        return generateId();
       }
     },
 
@@ -135,11 +140,41 @@
         activeOptions,
         visible,
         expandTrigger,
-        popperClass
+        popperClass,
+        hoverThreshold
       } = this;
+      let itemId = null;
+      let itemIndex = 0;
+
+      let hoverMenuRefs = {};
+      const hoverMenuHandler = e => {
+        const offsetX = e.offsetX;
+        const width = hoverMenuRefs.activeMenu.offsetWidth;
+        const height = hoverMenuRefs.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: {}
@@ -148,12 +183,52 @@
           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) > -1) {
+                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];
-              events.on[triggerEvent] = () => {
+              events.on[triggerEvent] = events.on['focus'] = () => { // focus 选中
                 this.activeItem(item, menuIndex);
                 this.$nextTick(() => {
                   // adjust self and next level
@@ -168,7 +243,10 @@
               };
             }
           }
-
+          if (!item.disabled && !item.children) { // no children set id
+            itemId = `${menuId}-${itemIndex}`;
+            itemIndex++;
+          }
           return (
             <li
               class={{
@@ -177,7 +255,14 @@
                 '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 }
             >
               {item.label}
             </li>
@@ -188,19 +273,68 @@
           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">
+            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

+ 2 - 0
packages/dropdown/src/dropdown-item.vue

@@ -6,6 +6,8 @@
       'el-dropdown-menu__item--divided': divided
     }"
     @click="handleClick"
+    :aria-disabled="disabled"
+    :tabindex="disabled ? null : -1"
   >
     <slot></slot>
   </li>

+ 106 - 5
packages/dropdown/src/dropdown.vue

@@ -4,6 +4,7 @@
   import Migrating from 'element-ui/src/mixins/migrating';
   import ElButton from 'element-ui/packages/button';
   import ElButtonGroup from 'element-ui/packages/button-group';
+  import { generateId } from 'element-ui/src/utils/util';
 
   export default {
     name: 'ElDropdown',
@@ -61,25 +62,43 @@
       return {
         timeout: null,
         visible: false,
-        triggerElm: null
+        triggerElm: null,
+        menuItems: null,
+        menuItemsArray: null,
+        dropdownElm: null,
+        focusing: false
       };
     },
 
     computed: {
       dropdownSize() {
         return this.size || (this.$ELEMENT || {}).size;
+      },
+      listId() {
+        return `dropdown-menu-${generateId()}`;
       }
     },
 
     mounted() {
       this.$on('menu-item-click', this.handleMenuItemClick);
       this.initEvent();
+      this.initAria();
     },
 
     watch: {
       visible(val) {
         this.broadcast('ElDropdownMenu', 'visible', val);
         this.$emit('visible-change', val);
+      },
+      focusing(val) {
+        const selfDefine = this.$el.querySelector('.el-dropdown-selfdefine');
+        if (selfDefine) { // 自定义
+          if (val) {
+            selfDefine.className += ' focusing';
+          } else {
+            selfDefine.className = selfDefine.className.replace('focusing', '');
+          }
+        }
       }
     },
 
@@ -100,6 +119,8 @@
       },
       hide() {
         if (this.triggerElm.disabled) return;
+        this.removeTabindex();
+        this.resetTabindex(this.triggerElm);
         clearTimeout(this.timeout);
         this.timeout = setTimeout(() => {
           this.visible = false;
@@ -109,18 +130,98 @@
         if (this.triggerElm.disabled) return;
         this.visible = !this.visible;
       },
+      handleTriggerKeyDown(ev) {
+        const keyCode = ev.keyCode;
+        if ([38, 40].indexOf(keyCode) > -1) { // up/down
+          this.removeTabindex();
+          this.resetTabindex(this.menuItems[0]);
+          this.menuItems[0].focus();
+          ev.preventDefault();
+          ev.stopPropagation();
+        } else if (keyCode === 13) { // space enter选中
+          this.handleClick();
+        } else if ([9, 27].indexOf(keyCode) > -1) { // tab || esc
+          this.hide();
+        }
+        return;
+      },
+      handleItemKeyDown(ev) {
+        const keyCode = ev.keyCode;
+        const target = ev.target;
+        const currentIndex = this.menuItemsArray.indexOf(target);
+        const max = this.menuItemsArray.length - 1;
+        let nextIndex;
+        if ([38, 40].indexOf(keyCode) > -1) { // up/down
+          if (keyCode === 38) { // up
+            nextIndex = currentIndex !== 0 ? currentIndex - 1 : 0;
+          } else { // down
+            nextIndex = currentIndex < max ? currentIndex + 1 : max;
+          }
+          this.removeTabindex();
+          this.resetTabindex(this.menuItems[nextIndex]);
+          this.menuItems[nextIndex].focus();
+          ev.preventDefault();
+          ev.stopPropagation();
+        } else if (keyCode === 13) { // enter选中
+          this.triggerElm.focus();
+          target.click();
+          if (!this.hideOnClick) { // click关闭
+            this.visible = false;
+          }
+        } else if ([9, 27].indexOf(keyCode) > -1) { // tab // esc
+          this.hide();
+          this.triggerElm.focus();
+        }
+        return;
+      },
+      resetTabindex(ele) { // 下次tab时组件聚焦元素
+        this.removeTabindex();
+        ele.setAttribute('tabindex', '0'); // 下次期望的聚焦元素
+      },
+      removeTabindex() {
+        this.triggerElm.setAttribute('tabindex', '-1');
+        this.menuItemsArray.forEach((item) => {
+          item.setAttribute('tabindex', '-1');
+        });
+      },
+      initAria() {
+        this.dropdownElm.setAttribute('id', this.listId);
+        this.triggerElm.setAttribute('aria-haspopup', 'list');
+        this.triggerElm.setAttribute('aria-controls', this.listId);
+        this.menuItems = this.dropdownElm.querySelectorAll("[tabindex='-1']");
+        this.menuItemsArray = Array.prototype.slice.call(this.menuItems);
+
+        if (!this.splitButton) { // 自定义
+          this.triggerElm.setAttribute('role', 'button');
+          this.triggerElm.setAttribute('tabindex', '0');
+          this.triggerElm.className += ' el-dropdown-selfdefine'; // 控制
+        }
+      },
       initEvent() {
-        let { trigger, show, hide, handleClick, splitButton } = this;
+        let { trigger, show, hide, handleClick, splitButton, handleTriggerKeyDown, handleItemKeyDown } = this;
         this.triggerElm = splitButton
           ? this.$refs.trigger.$el
           : this.$slots.default[0].elm;
 
+        let dropdownElm = this.dropdownElm = this.$slots.dropdown[0].elm;
+
+        this.triggerElm.addEventListener('keydown', handleTriggerKeyDown); // triggerElm keydown
+        dropdownElm.addEventListener('keydown', handleItemKeyDown, true); // item keydown
+        // 控制自定义元素的样式
+        if (!splitButton) {
+          this.triggerElm.addEventListener('focus', () => {
+            this.focusing = true;
+          });
+          this.triggerElm.addEventListener('blur', () => {
+            this.focusing = false;
+          });
+          this.triggerElm.addEventListener('click', () => {
+            this.focusing = false;
+          });
+        }
         if (trigger === 'hover') {
           this.triggerElm.addEventListener('mouseenter', show);
           this.triggerElm.addEventListener('mouseleave', hide);
-
-          let dropdownElm = this.$slots.dropdown[0].elm;
-
           dropdownElm.addEventListener('mouseenter', show);
           dropdownElm.addEventListener('mouseleave', hide);
         } else if (trigger === 'click') {

+ 2 - 2
packages/menu/src/menu.vue

@@ -157,14 +157,14 @@
       },
       getColorChannels(color) {
         color = color.replace('#', '');
-        if (/^[1-9a-fA-F]{3}$/.test(color)) {
+        if (/^[0-9a-fA-F]{3}$/.test(color)) {
           color = color.split('');
           for (let i = 2; i >= 0; i--) {
             color.splice(i, 0, color[i]);
           }
           color = color.join('');
         }
-        if (/^[1-9a-fA-F]{6}$/.test(color)) {
+        if (/^[0-9a-fA-F]{6}$/.test(color)) {
           return {
             red: parseInt(color.slice(0, 2), 16),
             green: parseInt(color.slice(2, 4), 16),

+ 4 - 2
packages/menu/src/submenu.vue

@@ -173,11 +173,13 @@
       },
       handleTitleMouseenter() {
         if (this.mode === 'horizontal' && !this.rootMenu.backgroundColor) return;
-        this.$refs['submenu-title'].style.backgroundColor = this.rootMenu.hoverBackground;
+        const title = this.$refs['submenu-title'];
+        title && (title.style.backgroundColor = this.rootMenu.hoverBackground);
       },
       handleTitleMouseleave() {
         if (this.mode === 'horizontal' && !this.rootMenu.backgroundColor) return;
-        this.$refs['submenu-title'].style.backgroundColor = this.rootMenu.backgroundColor || '';
+        const title = this.$refs['submenu-title'];
+        title && (title.style.backgroundColor = this.rootMenu.backgroundColor || '');
       }
     },
     created() {

+ 6 - 29
packages/message/src/main.vue

@@ -9,15 +9,15 @@
       v-show="visible"
       @mouseenter="clearTimer"
       @mouseleave="startTimer"
-      role="alertdialog"
+      role="alert"
     >
       <i :class="iconClass" v-if="iconClass"></i>
       <i :class="typeClass" v-else></i>
       <slot>
-        <p v-if="!dangerouslyUseHTMLString" class="el-message__content"  tabindex="0">{{ message }}</p>
-        <p v-else v-html="message" class="el-message__content"  tabindex="0"></p>
+        <p v-if="!dangerouslyUseHTMLString" class="el-message__content">{{ message }}</p>
+        <p v-else v-html="message" class="el-message__content"></p>
       </slot>
-      <i v-if="showClose" class="el-message__closeBtn el-icon-close" @click="close" tabindex="0" role="button" aria-label="close" @keydown.enter.stop="close"></i>
+      <i v-if="showClose" class="el-message__closeBtn el-icon-close" @click="close"></i>
     </div>
   </transition>
 </template>
@@ -44,9 +44,7 @@
         closed: false,
         timer: null,
         dangerouslyUseHTMLString: false,
-        center: false,
-        initFocus: null,
-        originFocus: null
+        center: false
       };
     },
 
@@ -87,18 +85,6 @@
         if (typeof this.onClose === 'function') {
           this.onClose(this);
         }
-        if (!this.originFocus || !this.originFocus.getBoundingClientRect) return;
-
-        // restore keyboard focus
-        const { top, left, bottom, right } = this.originFocus.getBoundingClientRect();
-        const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
-        const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
-        if (top >= 0 &&
-          left >= 0 &&
-          bottom <= viewportHeight &&
-          right <= viewportWidth) {
-          this.originFocus.focus();
-        }
       },
 
       clearTimer() {
@@ -115,24 +101,15 @@
         }
       },
       keydown(e) {
-        if (e.keyCode === 46 || e.keyCode === 8) {
-          this.clearTimer(); // detele 取消倒计时
-        } else if (e.keyCode === 27) { // esc关闭消息
+        if (e.keyCode === 27) { // esc关闭消息
           if (!this.closed) {
             this.close();
           }
-        } else {
-          this.startTimer(); // 恢复倒计时
         }
       }
     },
     mounted() {
       this.startTimer();
-      this.originFocus = document.activeElement;
-      this.initFocus = this.showClose ? this.$el.querySelector('.el-icon-close') : this.$el.querySelector('.el-message__content');
-      setTimeout(() => {
-        this.initFocus && this.initFocus.focus();
-      });
       document.addEventListener('keydown', this.keydown);
     },
     beforeDestroy() {

+ 19 - 2
packages/notification/src/main.vue

@@ -6,7 +6,9 @@
       :style="positionStyle"
       @mouseenter="clearTimer()"
       @mouseleave="startTimer()"
-      @click="click">
+      @click="click"
+      role="alert"
+    >
       <i
         class="el-notification__icon"
         :class="[ typeClass, iconClass ]"
@@ -119,9 +121,19 @@
             }
           }, this.duration);
         }
+      },
+      keydown(e) {
+        if (e.keyCode === 46 || e.keyCode === 8) {
+          this.clearTimer(); // detele 取消倒计时
+        } else if (e.keyCode === 27) { // esc关闭消息
+          if (!this.closed) {
+            this.close();
+          }
+        } else {
+          this.startTimer(); // 恢复倒计时
+        }
       }
     },
-
     mounted() {
       if (this.duration > 0) {
         this.timer = setTimeout(() => {
@@ -130,6 +142,11 @@
           }
         }, this.duration);
       }
+      document.addEventListener('keydown', this.keydown);
+    },
+    beforeDestroy() {
+      document.removeEventListener('keydown', this.keydown);
     }
   };
 </script>
+

+ 42 - 11
packages/popover/src/main.vue

@@ -6,7 +6,11 @@
         :class="[popperClass, content && 'el-popover--plain']"
         ref="popper"
         v-show="!disabled && showPopper"
-        :style="{ width: width + 'px' }">
+        :style="{ width: width + 'px' }"
+        role="tooltip"
+        :id="tooltipId"
+        :aria-hidden="(disabled || !showPopper) ? 'true' : 'false'"
+      >
         <div class="el-popover__title" v-if="title" v-text="title"></div>
         <slot>{{ content }}</slot>
       </div>
@@ -14,10 +18,11 @@
     <slot name="reference"></slot>
   </span>
 </template>
-
 <script>
 import Popper from 'element-ui/src/utils/vue-popper';
 import { on, off } from 'element-ui/src/utils/dom';
+import { addClass, removeClass } from 'element-ui/src/utils/dom';
+import { generateId } from 'element-ui/src/utils/util';
 
 export default {
   name: 'ElPopover',
@@ -49,25 +54,35 @@ export default {
     }
   },
 
+  computed: {
+    tooltipId() {
+      return `el-popover-${generateId()}`;
+    }
+  },
   watch: {
-    showPopper(newVal, oldVal) {
-      newVal ? this.$emit('show') : this.$emit('hide');
-    },
-    '$refs.reference': {
-      deep: true,
-      handler(val) {
-        console.log(val);
-      }
+    showPopper(val) {
+      val ? this.$emit('show') : this.$emit('hide');
     }
   },
 
   mounted() {
-    let reference = this.reference || this.$refs.reference;
+    let reference = this.referenceElm = this.reference || this.$refs.reference;
     const popper = this.popper || this.$refs.popper;
 
     if (!reference && this.$slots.reference && this.$slots.reference[0]) {
       reference = this.referenceElm = this.$slots.reference[0].elm;
     }
+    // 可访问性
+    if (reference) {
+      addClass(reference, 'el-popover__reference');
+      reference.setAttribute('aria-describedby', this.tooltipId);
+      reference.setAttribute('tabindex', 0); // tab序列
+
+      this.trigger !== 'click' && on(reference, 'focus', this.handleFocus);
+      this.trigger !== 'click' && on(reference, 'blur', this.handleBlur);
+      on(reference, 'keydown', this.handleKeydown);
+      on(reference, 'click', this.handleClick);
+    }
     if (this.trigger === 'click') {
       on(reference, 'click', this.doToggle);
       on(document, 'click', this.handleDocumentClick);
@@ -114,6 +129,17 @@ export default {
     doClose() {
       this.showPopper = false;
     },
+    handleFocus() {
+      addClass(this.referenceElm, 'focusing');
+      this.showPopper = true;
+    },
+    handleClick() {
+      removeClass(this.referenceElm, 'focusing');
+    },
+    handleBlur() {
+      removeClass(this.referenceElm, 'focusing');
+      this.showPopper = false;
+    },
     handleMouseEnter() {
       clearTimeout(this._timer);
       if (this.openDelay) {
@@ -124,6 +150,11 @@ export default {
         this.showPopper = true;
       }
     },
+    handleKeydown(ev) {
+      if (ev.keyCode === 27) { // esc
+        this.doClose();
+      }
+    },
     handleMouseLeave() {
       clearTimeout(this._timer);
       this._timer = setTimeout(() => {

+ 4 - 3
packages/radio/src/radio-group.vue

@@ -53,9 +53,10 @@
     },
     mounted() {
       // 当radioGroup没有默认选项时,第一个可以选中Tab导航
-      let radios = this.$el.querySelectorAll('[type=radio]');
-      if (![].some.call(radios, radio => radio.checked)) {
-        this.$el.querySelectorAll('[role=radio]')[0].tabIndex = 0;
+      const radios = this.$el.querySelectorAll('[type=radio]');
+      const firstLabel = this.$el.querySelectorAll('[role=radio]')[0];
+      if (![].some.call(radios, radio => radio.checked) && firstLabel) {
+        firstLabel.tabIndex = 0;
       }
     },
     methods: {

+ 2 - 2
packages/select/src/option-group.vue

@@ -1,6 +1,6 @@
 <template>
-  <ul class="el-select-group__wrap">
-    <li class="el-select-group__title" v-show="visible">{{ label }}</li>
+  <ul class="el-select-group__wrap" v-show="visible">
+    <li class="el-select-group__title">{{ label }}</li>
     <li>
       <ul class="el-select-group">
         <slot></slot>

+ 18 - 2
packages/select/src/select.vue

@@ -9,7 +9,12 @@
       @click.stop="toggleMenu"
       ref="tags"
       :style="{ 'max-width': inputWidth - 32 + 'px' }">
-      <transition-group @after-leave="resetInputHeight">
+      <span
+        class="el-select__multiple-text"
+        v-if="collapseTags">
+        {{ multipleText }}
+      </span>
+      <transition-group @after-leave="resetInputHeight" v-if="!collapseTags">
         <el-tag
           v-for="item in selected"
           :key="getValueKey(item)"
@@ -29,6 +34,7 @@
         :class="[selectSize ? `is-${ selectSize }` : '']"
         :disabled="disabled"
         @focus="handleFocus"
+        @click.stop
         @keyup="managePlaceholder"
         @keydown="resetInputState"
         @keydown.down.prevent="navigateOptions('next')"
@@ -183,6 +189,14 @@
 
       selectSize() {
         return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
+      },
+
+      multipleText() {
+        const selected = this.selected;
+        if (!selected || !selected.length) return '';
+        const length = selected.length;
+        const countText = length > 1 ? `(+${ selected.length - 1 })` : '';
+        return `${ selected[0].currentLabel } ${ countText }`;
       }
     },
 
@@ -231,7 +245,8 @@
       valueKey: {
         type: String,
         default: 'value'
-      }
+      },
+      collapseTags: Boolean
     },
 
     data() {
@@ -534,6 +549,7 @@
       },
 
       resetInputHeight() {
+        if (this.collapseTags) return;
         this.$nextTick(() => {
           if (!this.$refs.reference) return;
           let inputChildNodes = this.$refs.reference.$el.childNodes;

+ 4 - 6
packages/table/src/table-header.js

@@ -119,12 +119,10 @@ export default {
                     {
                       column.sortable
                         ? <span class="caret-wrapper" on-click={ ($event) => this.handleSortClick($event, column) }>
-                            <span class="sort-caret ascending" on-click={ ($event) => this.handleSortClick($event, column, 'ascending') }>
-                              <i class="el-icon-sort-up"></i>
-                            </span>
-                            <span class="sort-caret descending" on-click={ ($event) => this.handleSortClick($event, column, 'descending') }>
-                              <i class="el-icon-sort-down"></i>
-                            </span>
+                            <i class="sort-caret ascending el-icon-caret-top" on-click={ ($event) => this.handleSortClick($event, column, 'ascending') }>
+                            </i>
+                            <i class="sort-caret descending el-icon-caret-bottom" on-click={ ($event) => this.handleSortClick($event, column, 'descending') }>
+                            </i>
                           </span>
                         : ''
                     }

+ 3 - 2
packages/theme-chalk/package.json

@@ -1,6 +1,6 @@
 {
   "name": "element-theme-chalk",
-  "version": "2.0.3",
+  "version": "2.0.4",
   "description": "Element component chalk theme.",
   "main": "lib/index.css",
   "style": "lib/index.css",
@@ -28,7 +28,8 @@
   "devDependencies": {
     "gulp": "^3.9.1",
     "gulp-cssmin": "^0.1.7",
-    "gulp-sass": "^3.1.0"
+    "gulp-sass": "^3.1.0",
+    "gulp-autoprefixer": "^4.0.0"
   },
   "dependencies": {}
 }

+ 2 - 2
packages/theme-chalk/src/cascader.scss

@@ -128,7 +128,7 @@
     line-height: 1.5;
     box-sizing: border-box;
     cursor: pointer;
-
+    outline: none;
     @include m(extensible) {
       &:after {
         font-family: 'element-icons';
@@ -154,7 +154,7 @@
       color: $--select-option-selected;
     }
 
-    &:hover {
+    &:hover, &:focus:not(:active) {
       background-color: $--select-option-hover-background;
     }
 

+ 6 - 0
packages/theme-chalk/src/checkbox.scss

@@ -17,6 +17,9 @@
     padding: $--checkbox-bordered-padding;
     border-radius: $--border-radius-base;
     border: $--border-base;
+    box-sizing: border-box;
+    line-height: normal;
+    height: $--checkbox-bordered-height;
 
     &.is-checked {
       border-color: $--color-primary;
@@ -34,6 +37,7 @@
     &.el-checkbox--medium {
       padding: $--checkbox-bordered-medium-padding;
       border-radius: $--button-medium-border-radius;
+      height: $--checkbox-bordered-medium-height;
 
       .el-checkbox__label {
         line-height: 17px;
@@ -49,6 +53,7 @@
     &.el-checkbox--small {
       padding: $--checkbox-bordered-small-padding;
       border-radius: $--button-small-border-radius;
+      height: $--checkbox-bordered-small-height;
 
       .el-checkbox__label {
         line-height: 15px;
@@ -69,6 +74,7 @@
     &.el-checkbox--mini {
       padding: $--checkbox-bordered-mini-padding;
       border-radius: $--button-mini-border-radius;
+      height: $--checkbox-bordered-mini-height;
 
       .el-checkbox__label {
         line-height: 12px;

+ 14 - 6
packages/theme-chalk/src/common/var.scss

@@ -137,16 +137,20 @@ $--checkbox-checked-icon-color: $--fill-base !default;
 
 $--checkbox-input-border-color-hover: $--color-primary !default;
 
+$--checkbox-bordered-height: 40px !default;
 $--checkbox-bordered-padding: 9px 20px 9px 10px !default;
 $--checkbox-bordered-medium-padding: 7px 20px 7px 10px !default;
-$--checkbox-bordered-small-padding: 3px 15px 7px 10px !default;
-$--checkbox-bordered-mini-padding: 1px 15px 5px 10px !default;
+$--checkbox-bordered-small-padding: 5px 15px 5px 10px !default;
+$--checkbox-bordered-mini-padding: 3px 15px 3px 10px !default;
 $--checkbox-bordered-medium-input-height: 14px !default;
 $--checkbox-bordered-medium-input-width: 14px !default;
+$--checkbox-bordered-medium-height: 36px !default;
 $--checkbox-bordered-small-input-height: 12px !default;
 $--checkbox-bordered-small-input-width: 12px !default;
+$--checkbox-bordered-small-height: 32px !default;
 $--checkbox-bordered-mini-input-height: 12px !default;
 $--checkbox-bordered-mini-input-width: 12px !default;
+$--checkbox-bordered-mini-height: 28px !default;
 
 $--checkbox-button-font-size: $--font-size-base !default;
 $--checkbox-button-checked-fill: $--color-primary !default;
@@ -183,16 +187,20 @@ $--radio-checked-icon-color: $--color-primary !default;
 
 $--radio-input-border-color-hover: $--color-primary !default;
 
-$--radio-bordered-padding: 10px 20px 10px 10px !default;
-$--radio-bordered-medium-padding: 8px 20px 8px 10px !default;
-$--radio-bordered-small-padding: 6px 15px 6px 10px !default;
-$--radio-bordered-mini-padding: 4px 15px 4px 10px !default;
+$--radio-bordered-height: 40px !default;
+$--radio-bordered-padding: 12px 20px 0 10px !default;
+$--radio-bordered-medium-padding: 10px 20px 0 10px !default;
+$--radio-bordered-small-padding: 8px 15px 0 10px !default;
+$--radio-bordered-mini-padding: 6px 15px 0 10px !default;
 $--radio-bordered-medium-input-height: 14px !default;
 $--radio-bordered-medium-input-width: 14px !default;
+$--radio-bordered-medium-height: 36px !default;
 $--radio-bordered-small-input-height: 12px !default;
 $--radio-bordered-small-input-width: 12px !default;
+$--radio-bordered-small-height: 32px !default;
 $--radio-bordered-mini-input-height: 12px !default;
 $--radio-bordered-mini-input-width: 12px !default;
+$--radio-bordered-mini-height: 28px !default;
 
 $--radio-button-font-size: $--font-size-base !default;
 $--radio-button-checked-fill: $--color-primary !default;

+ 8 - 2
packages/theme-chalk/src/dropdown.scss

@@ -50,6 +50,12 @@
     font-size: 12px;
     margin: 0 3px;
   }
+
+  .el-dropdown-selfdefine { // 自定义
+    &:focus:active, &:focus:not(.focusing) {
+      outline-width: 0;
+    }
+  }
 }
 
 @include b(dropdown-menu) {
@@ -72,8 +78,8 @@
     font-size: $--font-size-base;
     color: $--color-text-regular;
     cursor: pointer;
-
-    &:not(.is-disabled):hover {
+    outline: none;
+    &:not(.is-disabled):hover, &:focus {
       background-color: $--dropdown-menuItem-hover-fill;
       color: $--dropdown-menuItem-hover-color;
     }

+ 6 - 0
packages/theme-chalk/src/popover.scss

@@ -27,4 +27,10 @@
     line-height: 1;
     margin-bottom: 12px;
   }
+
+  @include e(reference) {
+    &:focus:not(.focusing), &:focus:hover {
+      outline-width: 0;
+    }
+  }
 }

+ 5 - 0
packages/theme-chalk/src/radio.scss

@@ -19,6 +19,8 @@
     padding: $--radio-bordered-padding;
     border-radius: $--border-radius-base;
     border: $--border-base;
+    box-sizing: border-box;
+    height: $--radio-bordered-height;
 
     &.is-checked {
       border-color: $--color-primary;
@@ -38,6 +40,7 @@
     &.is-bordered {
       padding: $--radio-bordered-medium-padding;
       border-radius: $--button-medium-border-radius;
+      height: $--radio-bordered-medium-height;
       .el-radio__label {
         font-size: $--button-medium-font-size;
       }
@@ -51,6 +54,7 @@
     &.is-bordered {
       padding: $--radio-bordered-small-padding;
       border-radius: $--button-small-border-radius;
+      height: $--radio-bordered-small-height;
       .el-radio__label {
         font-size: $--button-small-font-size;
       }
@@ -64,6 +68,7 @@
     &.is-bordered {
       padding: $--radio-bordered-mini-padding;
       border-radius: $--button-mini-border-radius;
+      height: $--radio-bordered-mini-height;
       .el-radio__label {
         font-size: $--button-mini-font-size;
       }

+ 9 - 0
packages/theme-chalk/src/select.scss

@@ -1,4 +1,5 @@
 @import "mixins/mixins";
+@import "mixins/utils";
 @import "common/var";
 @import "select-dropdown";
 @import "input";
@@ -88,6 +89,14 @@
     }
   }
 
+  @include e(multiple-text) {
+    margin-left: 15px;
+    color: $--input-color;
+    font-size: $--font-size-base;
+    display: block;
+    @include utils-ellipsis;
+  }
+
   @include e(close) {
     cursor: pointer;
     position: absolute;

+ 16 - 7
packages/theme-chalk/src/table.scss

@@ -426,20 +426,29 @@
   }
 
   .caret-wrapper {
-    position: relative;
-    display: inline-flex;
-    align-items: center;
-    height: 13px;
-    width: 24px;
+    position: absolute;
+    display: inline-block;
+    height: 100%;
+    vertical-align: middle;
     cursor: pointer;
     overflow: initial;
   }
 
   .sort-caret {
-    color: $--icon-color-base;
+    color: $--color-text-placeholder;
     width: 14px;
     overflow: hidden;
-    font-size: 13px;
+    font-size: 15px;
+    position: absolute;
+    left: 4px;
+
+    &.ascending {
+      top: 1px;
+    }
+
+    &.descending {
+      bottom: 1px;
+    }
   }
 
   .ascending .sort-caret.ascending {

+ 3 - 0
packages/theme-chalk/src/tooltip.scss

@@ -2,6 +2,9 @@
 @import "common/var";
 
 @include b(tooltip) {
+  &:focus:not(.focusing), &:focus:hover {
+    outline-width: 0;
+  }
   @include e(popper) {
     position: absolute;
     border-radius: 4px;

+ 6 - 1
packages/theme-chalk/src/tree.scss

@@ -25,7 +25,12 @@
 
 @include b(tree-node) {
   white-space: nowrap;
-
+  outline: none;
+  &:focus { /* focus */
+    > .el-tree-node__content {
+      background-color: $--tree-node-hover-color;
+    }
+  }
   @include e(content) {
     display: flex;
     align-items: center;

+ 39 - 9
packages/tooltip/src/main.js

@@ -1,6 +1,7 @@
 import Popper from 'element-ui/src/utils/vue-popper';
 import debounce from 'throttle-debounce/debounce';
 import { getFirstComponentChild } from 'element-ui/src/utils/vdom';
+import { generateId } from 'element-ui/src/utils/util';
 import Vue from 'vue';
 
 export default {
@@ -48,10 +49,15 @@ export default {
 
   data() {
     return {
-      timeoutPending: null
+      timeoutPending: null,
+      focusing: false
     };
   },
-
+  computed: {
+    tooltipId() {
+      return `el-tooltip-${generateId()}`;
+    }
+  },
   beforeCreate() {
     if (this.$isServer) return;
 
@@ -75,6 +81,9 @@ export default {
             onMouseleave={ () => { this.setExpectedState(false); this.debounceClose(); } }
             onMouseenter= { () => { this.setExpectedState(true); } }
             ref="popper"
+            role="tooltip"
+            id={this.tooltipId}
+            aria-hidden={ (this.disabled || !this.showPopper) ? 'true' : 'false' }
             v-show={!this.disabled && this.showPopper}
             class={
               ['el-tooltip__popper', 'is-' + this.effect, this.popperClass]
@@ -87,24 +96,38 @@ export default {
     if (!this.$slots.default || !this.$slots.default.length) return this.$slots.default;
 
     const vnode = getFirstComponentChild(this.$slots.default);
+
     if (!vnode) return vnode;
+
     const data = vnode.data = vnode.data || {};
     const on = vnode.data.on = vnode.data.on || {};
     const nativeOn = vnode.data.nativeOn = vnode.data.nativeOn || {};
 
     data.staticClass = this.concatClass(data.staticClass, 'el-tooltip');
-    on.mouseenter = this.addEventHandle(on.mouseenter, this.show);
-    on.mouseleave = this.addEventHandle(on.mouseleave, this.hide);
-    nativeOn.mouseenter = this.addEventHandle(nativeOn.mouseenter, this.show);
-    nativeOn.mouseleave = this.addEventHandle(nativeOn.mouseleave, this.hide);
-
+    nativeOn.mouseenter = on.mouseenter = this.addEventHandle(on.mouseenter, this.show);
+    nativeOn.mouseleave = on.mouseleave = this.addEventHandle(on.mouseleave, this.hide);
+    nativeOn.focus = on.focus = this.addEventHandle(on.focus, this.handleFocus);
+    nativeOn.blur = on.blur = this.addEventHandle(on.blur, this.handleBlur);
+    nativeOn.click = on.click = this.addEventHandle(on.click, () => { this.focusing = false; });
     return vnode;
   },
 
   mounted() {
     this.referenceElm = this.$el;
+    if (this.$el.nodeType === 1) {
+      this.$el.setAttribute('aria-describedby', this.tooltipId);
+      this.$el.setAttribute('tabindex', 0);
+    }
+  },
+  watch: {
+    focusing(val) {
+      if (val) {
+        this.referenceElm.className += ' focusing';
+      } else {
+        this.referenceElm.className = this.referenceElm.className.replace('focusing', '');
+      }
+    }
   },
-
   methods: {
     show() {
       this.setExpectedState(true);
@@ -115,7 +138,14 @@ export default {
       this.setExpectedState(false);
       this.debounceClose();
     },
-
+    handleFocus() {
+      this.focusing = true;
+      this.show();
+    },
+    handleBlur() {
+      this.focusing = false;
+      this.hide();
+    },
     addEventHandle(old, fn) {
       if (!old) {
         return fn;

+ 3 - 2
packages/tree/src/model/tree-store.js

@@ -198,9 +198,10 @@ export default class TreeStore {
     const node = this.nodesMap[key];
     if (!node) return;
     const childNodes = node.childNodes;
-    childNodes.forEach(child => {
+    for (let i = childNodes.length - 1; i >= 0; i--) {
+      const child = childNodes[i];
       this.remove(child.data);
-    });
+    }
     for (let i = 0, j = data.length; i < j; i++) {
       const child = data[i];
       this.append(child, node.data);

+ 18 - 5
packages/tree/src/tree-node.vue

@@ -1,12 +1,21 @@
 <template>
-  <div class="el-tree-node"
+  <div
+    class="el-tree-node"
     @click.stop="handleClick"
     v-show="node.visible"
     :class="{
       'is-expanded': expanded,
       'is-current': tree.store.currentNode === node,
-      'is-hidden': !node.visible
-    }">
+      'is-hidden': !node.visible,
+      'is-focusable': !node.disabled,
+      'is-checked': !node.disabled && node.checked
+    }"
+    role="treeitem"
+    tabindex="-1"
+    :aria-expanded="expanded"
+    :aria-disabled="node.disabled"
+    :aria-checked="node.checked"
+  >
     <div class="el-tree-node__content"
       :style="{ 'padding-left': (node.level - 1) * tree.indent + 'px' }">
       <span
@@ -20,7 +29,8 @@
         :indeterminate="node.indeterminate"
         :disabled="!!node.disabled"
         @click.native.stop
-        @change="handleCheckChange">
+        @change="handleCheckChange"
+      >
       </el-checkbox>
       <span
         v-if="node.loading"
@@ -32,7 +42,10 @@
       <div
         class="el-tree-node__children"
         v-if="childNodeRendered"
-        v-show="expanded">
+        v-show="expanded"
+        role="group"
+        :aria-expanded="expanded"
+      >
         <el-tree-node
           :render-content="renderContent"
           v-for="child in node.childNodes"

+ 60 - 2
packages/tree/src/tree.vue

@@ -1,5 +1,9 @@
 <template>
-  <div class="el-tree" :class="{ 'el-tree--highlight-current': highlightCurrent }">
+  <div
+    class="el-tree"
+    :class="{ 'el-tree--highlight-current': highlightCurrent }"
+    role="tree"
+  >
     <el-tree-node
       v-for="child in root.childNodes"
       :node="child"
@@ -33,7 +37,9 @@
       return {
         store: null,
         root: null,
-        currentNode: null
+        currentNode: null,
+        treeItems: null,
+        checkboxItems: []
       };
     },
 
@@ -101,6 +107,9 @@
         get() {
           return this.data;
         }
+      },
+      treeItemArray() {
+        return Array.prototype.slice.call(this.treeItems);
       }
     },
 
@@ -115,6 +124,11 @@
       },
       data(newVal) {
         this.store.setData(newVal);
+      },
+      checkboxItems(val) {
+        Array.prototype.forEach.call(val, (checkbox) => {
+          checkbox.setAttribute('tabindex', -1);
+        });
       }
     },
 
@@ -171,6 +185,42 @@
       updateKeyChildren(key, data) {
         if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in updateKeyChild');
         this.store.updateChildren(key, data);
+      },
+      initTabindex() {
+        this.treeItems = this.$el.querySelectorAll('.is-focusable[role=treeitem]');
+        this.checkboxItems = this.$el.querySelectorAll('input[type=checkbox]');
+        const checkedItem = this.$el.querySelectorAll('.is-checked[role=treeitem]');
+        if (checkedItem.length) {
+          checkedItem[0].setAttribute('tabindex', 0);
+          return;
+        }
+        this.treeItems[0].setAttribute('tabindex', 0);
+      },
+      handelKeydown(ev) {
+        const currentItem = ev.target;
+        const keyCode = ev.keyCode;
+        this.treeItems = this.$el.querySelectorAll('.is-focusable[role=treeitem]');
+        const currentIndex = this.treeItemArray.indexOf(currentItem);
+        let nextIndex;
+        if ([38, 40].indexOf(keyCode) > -1) { // up、down
+          if (keyCode === 38) { // up
+            nextIndex = currentIndex !== 0 ? currentIndex - 1 : 0;
+          } else {
+            nextIndex = (currentIndex < this.treeItemArray.length - 1) ? currentIndex + 1 : 0;
+          }
+          this.treeItemArray[nextIndex].focus(); // 选中
+        }
+        const hasInput = currentItem.querySelector('[type="checkbox"]');
+        if ([37, 39].indexOf(keyCode) > -1) { // left、right 展开
+          currentItem.click(); // 选中
+        }
+        if ([13, 32].indexOf(keyCode) > -1) { // space enter选中checkbox
+          if (hasInput) {
+            hasInput.click();
+          }
+          ev.stopPropagation();
+          ev.preventDefault();
+        }
       }
     },
 
@@ -194,6 +244,14 @@
       });
 
       this.root = this.store.root;
+    },
+    mounted() {
+      this.initTabindex();
+      this.$el.addEventListener('keydown', this.handelKeydown);
+    },
+    updated() {
+      this.treeItems = this.$el.querySelectorAll('[role=treeitem]');
+      this.checkboxItems = this.$el.querySelectorAll('input[type=checkbox]');
     }
   };
 </script>

+ 2 - 2
packages/upload/src/ajax.js

@@ -1,9 +1,9 @@
 function getError(action, option, xhr) {
   let msg;
   if (xhr.response) {
-    msg = `${xhr.status} ${xhr.response.error || xhr.response}`;
+    msg = `${xhr.response.error || xhr.response}`;
   } else if (xhr.responseText) {
-    msg = `${xhr.status} ${xhr.responseText}`;
+    msg = `${xhr.responseText}`;
   } else {
     msg = `fail to post ${action} ${xhr.status}`;
   }

+ 1 - 1
src/index.js

@@ -173,7 +173,7 @@ if (typeof window !== 'undefined' && window.Vue) {
 };
 
 module.exports = {
-  version: '2.0.3',
+  version: '2.0.4',
   locale: locale.use,
   i18n: locale.i18n,
   install,

+ 1 - 0
src/utils/clickoutside.js

@@ -65,5 +65,6 @@ export default {
         break;
       }
     }
+    delete el[ctx];
   }
 };

+ 1 - 1
test/unit/specs/upload.spec.js

@@ -41,7 +41,7 @@ describe('ajax', () => {
   });
   it('40x code should be error', done => {
     option.onError = e => {
-      expect(e.toString()).to.contain('404 Not found');
+      expect(e.toString()).to.contain('Not found');
       done();
     };