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

* Accessibility for Tooltip & Popover

* Accessibility for message & notification

* fixbug for popover with nodeType
This commit is contained in:
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"
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>
```
:::

View File

@ -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>

View File

@ -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() {

View File

@ -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>

View File

@ -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(() => {

View File

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

View File

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