mirror of https://github.com/ElemeFE/element
Accessibility for Popover, Tooltip, Message & Notification (#8009)
* Accessibility for Tooltip & Popover * Accessibility for message & notification * fixbug for popover with nodeTypepull/8100/head
parent
6c77cd9716
commit
363a80b184
|
@ -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>
|
||||||
```
|
```
|
||||||
:::
|
:::
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue