浏览代码

Accessibility for Popover, Tooltip, Message & Notification (#8009)

* Accessibility for Tooltip & Popover

* Accessibility for message & notification

* fixbug for popover with nodeType
maranran 7 年之前
父节点
当前提交
363a80b184

+ 1 - 1
examples/docs/zh-CN/popover.md

@@ -146,7 +146,7 @@ Popover 的属性与 Tooltip 很类似,它们都是基于`Vue-popper`开发的
   width="200"
   trigger="focus"
   content="这是一段内容,这是一段内容,这是一段内容,这是一段内容。">
-  <el-button slot="reference">focus 激活</el-button>
+  <span slot="reference" style="margin-left: 10px; font-size: 14px; color: #5a5e66">focus 激活</span>
 </el-popover>
 ```
 :::

+ 1 - 1
examples/docs/zh-CN/tooltip.md

@@ -63,7 +63,7 @@
 <div class="box">
   <div class="top">
     <el-tooltip class="item" effect="dark" content="Top Left 提示文字" placement="top-start">
-      <el-button>上左</el-button>
+      <span>上左</span>
     </el-tooltip>
     <el-tooltip class="item" effect="dark" content="Top Center 提示文字" placement="top">
       <el-button>上边</el-button>

+ 18 - 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,18 @@
         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();
-        }
+//        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 +113,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 - 3
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,10 @@
     <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 { generateId } from 'element-ui/src/utils/util';
 
 export default {
   name: 'ElPopover',
@@ -49,6 +53,11 @@ export default {
     }
   },
 
+  computed: {
+    tooltipId() {
+      return `el-popover-${generateId()}`;
+    }
+  },
   watch: {
     showPopper(newVal, oldVal) {
       newVal ? this.$emit('show') : this.$emit('hide');
@@ -62,12 +71,23 @@ export default {
   },
 
   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) {
+      reference.className += ' el-tooltip';
+      reference.setAttribute('aria-describedby', this.tooltipId);
+      reference.setAttribute('tabindex', 0); // tab序列
+
+      on(reference, 'focus', this.handleFocus);
+      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 +134,20 @@ export default {
     doClose() {
       this.showPopper = false;
     },
+    handleFocus() {
+      const reference = this.referenceElm;
+      reference.className += ' focusing';
+      this.showPopper = true;
+    },
+    handleClick() {
+      const reference = this.referenceElm;
+      reference.className = reference.className.replace(/\s*focusing\s*/, ' ');
+    },
+    handleBlur() {
+      const reference = this.referenceElm;
+      reference.className = reference.className.replace(/\s*focusing\s*/, ' ');
+      this.showPopper = false;
+    },
     handleMouseEnter() {
       clearTimeout(this._timer);
       if (this.openDelay) {
@@ -124,6 +158,11 @@ export default {
         this.showPopper = true;
       }
     },
+    handleKeydown(ev) {
+      if (ev.keyCode === 27) { // esc
+        this.doClose();
+      }
+    },
     handleMouseLeave() {
       clearTimeout(this._timer);
       this._timer = setTimeout(() => {

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

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