Browse Source

Accessibility for Tree (#8049)

* tree

* Update yarn.lock
maranran 7 years ago
parent
commit
6c77cd9716
3 changed files with 84 additions and 8 deletions
  1. 6 1
      packages/theme-chalk/src/tree.scss
  2. 18 5
      packages/tree/src/tree-node.vue
  3. 60 2
      packages/tree/src/tree.vue

+ 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;

+ 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].includes(keyCode)) { // 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].includes(keyCode)) { // left、right 展开
+          currentItem.click(); // 选中
+        }
+        if ([13, 32].includes(keyCode)) { // 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>