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

* Accessibility for Tooltip & Popover

* Accessibility for message & notification

* fixbug for popover with nodeType
pull/8100/head
maranran 2017-11-08 21:21:20 -06:00 committed by 杨奕
parent 6c77cd9716
commit 363a80b184
7 changed files with 123 additions and 45 deletions

View File

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

View File

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

View File

@ -9,15 +9,15 @@
v-show="visible" v-show="visible"
@mouseenter="clearTimer" @mouseenter="clearTimer"
@mouseleave="startTimer" @mouseleave="startTimer"
role="alertdialog" role="alert"
> >
<i :class="iconClass" v-if="iconClass"></i> <i :class="iconClass" v-if="iconClass"></i>
<i :class="typeClass" v-else></i> <i :class="typeClass" v-else></i>
<slot> <slot>
<p v-if="!dangerouslyUseHTMLString" class="el-message__content" tabindex="0">{{ message }}</p> <p v-if="!dangerouslyUseHTMLString" class="el-message__content">{{ message }}</p>
<p v-else v-html="message" class="el-message__content" tabindex="0"></p> <p v-else v-html="message" class="el-message__content"></p>
</slot> </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> </div>
</transition> </transition>
</template> </template>
@ -44,9 +44,7 @@
closed: false, closed: false,
timer: null, timer: null,
dangerouslyUseHTMLString: false, dangerouslyUseHTMLString: false,
center: false, center: false
initFocus: null,
originFocus: null
}; };
}, },
@ -87,18 +85,18 @@
if (typeof this.onClose === 'function') { if (typeof this.onClose === 'function') {
this.onClose(this); this.onClose(this);
} }
if (!this.originFocus || !this.originFocus.getBoundingClientRect) return; // if (!this.originFocus || !this.originFocus.getBoundingClientRect) return;
//
// restore keyboard focus // // restore keyboard focus
const { top, left, bottom, right } = this.originFocus.getBoundingClientRect(); // const { top, left, bottom, right } = this.originFocus.getBoundingClientRect();
const viewportHeight = window.innerHeight || document.documentElement.clientHeight; // const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
const viewportWidth = window.innerWidth || document.documentElement.clientWidth; // const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
if (top >= 0 && // if (top >= 0 &&
left >= 0 && // left >= 0 &&
bottom <= viewportHeight && // bottom <= viewportHeight &&
right <= viewportWidth) { // right <= viewportWidth) {
this.originFocus.focus(); // this.originFocus.focus();
} // }
}, },
clearTimer() { clearTimer() {
@ -115,24 +113,15 @@
} }
}, },
keydown(e) { keydown(e) {
if (e.keyCode === 46 || e.keyCode === 8) { if (e.keyCode === 27) { // esc
this.clearTimer(); // detele
} else if (e.keyCode === 27) { // esc
if (!this.closed) { if (!this.closed) {
this.close(); this.close();
} }
} else {
this.startTimer(); //
} }
} }
}, },
mounted() { mounted() {
this.startTimer(); 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); document.addEventListener('keydown', this.keydown);
}, },
beforeDestroy() { beforeDestroy() {

View File

@ -6,7 +6,9 @@
:style="positionStyle" :style="positionStyle"
@mouseenter="clearTimer()" @mouseenter="clearTimer()"
@mouseleave="startTimer()" @mouseleave="startTimer()"
@click="click"> @click="click"
role="alert"
>
<i <i
class="el-notification__icon" class="el-notification__icon"
:class="[ typeClass, iconClass ]" :class="[ typeClass, iconClass ]"
@ -119,9 +121,19 @@
} }
}, this.duration); }, 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() { mounted() {
if (this.duration > 0) { if (this.duration > 0) {
this.timer = setTimeout(() => { this.timer = setTimeout(() => {
@ -130,6 +142,11 @@
} }
}, this.duration); }, this.duration);
} }
document.addEventListener('keydown', this.keydown);
},
beforeDestroy() {
document.removeEventListener('keydown', this.keydown);
} }
}; };
</script> </script>

View File

@ -6,7 +6,11 @@
:class="[popperClass, content && 'el-popover--plain']" :class="[popperClass, content && 'el-popover--plain']"
ref="popper" ref="popper"
v-show="!disabled && showPopper" 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> <div class="el-popover__title" v-if="title" v-text="title"></div>
<slot>{{ content }}</slot> <slot>{{ content }}</slot>
</div> </div>
@ -14,10 +18,10 @@
<slot name="reference"></slot> <slot name="reference"></slot>
</span> </span>
</template> </template>
<script> <script>
import Popper from 'element-ui/src/utils/vue-popper'; import Popper from 'element-ui/src/utils/vue-popper';
import { on, off } from 'element-ui/src/utils/dom'; import { on, off } from 'element-ui/src/utils/dom';
import { generateId } from 'element-ui/src/utils/util';
export default { export default {
name: 'ElPopover', name: 'ElPopover',
@ -49,6 +53,11 @@ export default {
} }
}, },
computed: {
tooltipId() {
return `el-popover-${generateId()}`;
}
},
watch: { watch: {
showPopper(newVal, oldVal) { showPopper(newVal, oldVal) {
newVal ? this.$emit('show') : this.$emit('hide'); newVal ? this.$emit('show') : this.$emit('hide');
@ -62,12 +71,23 @@ export default {
}, },
mounted() { mounted() {
let reference = this.reference || this.$refs.reference; let reference = this.referenceElm = this.reference || this.$refs.reference;
const popper = this.popper || this.$refs.popper; const popper = this.popper || this.$refs.popper;
if (!reference && this.$slots.reference && this.$slots.reference[0]) { if (!reference && this.$slots.reference && this.$slots.reference[0]) {
reference = this.referenceElm = this.$slots.reference[0].elm; 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') { if (this.trigger === 'click') {
on(reference, 'click', this.doToggle); on(reference, 'click', this.doToggle);
on(document, 'click', this.handleDocumentClick); on(document, 'click', this.handleDocumentClick);
@ -114,6 +134,20 @@ export default {
doClose() { doClose() {
this.showPopper = false; 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() { handleMouseEnter() {
clearTimeout(this._timer); clearTimeout(this._timer);
if (this.openDelay) { if (this.openDelay) {
@ -124,6 +158,11 @@ export default {
this.showPopper = true; this.showPopper = true;
} }
}, },
handleKeydown(ev) {
if (ev.keyCode === 27) { // esc
this.doClose();
}
},
handleMouseLeave() { handleMouseLeave() {
clearTimeout(this._timer); clearTimeout(this._timer);
this._timer = setTimeout(() => { this._timer = setTimeout(() => {

View File

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

View File

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