Skip to content

Commit

Permalink
* add .veui-sr-only (#249)
Browse files Browse the repository at this point in the history
* fix backward tabbing by adding focus ward on both directions
* add aria support for DatePicker
  • Loading branch information
Justineo authored Mar 6, 2018
1 parent 6bb1be5 commit 621ab48
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 34 deletions.
4 changes: 4 additions & 0 deletions packages/veui-theme-one/common.less
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ button {
cursor: not-allowed !important;
}

.veui-sr-only {
.veui-invisible();
}

a {
text-decoration: none;
}
Expand Down
6 changes: 1 addition & 5 deletions packages/veui-theme-one/components/calendar.less
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@
overflow: hidden;
border: 1px solid @veui-gray-color-6;
background-color: #fff;

&-invisible {
.size(0);
opacity: 0;
}
outline: none;

button {
.veui-button-transition();
Expand Down
11 changes: 10 additions & 1 deletion packages/veui-theme-one/components/date-picker.less
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
}

&-clear {
display: none;
.absolute(10px, 10px, _, _);
.size(16px);
background: #fff;
Expand All @@ -44,10 +43,20 @@
cursor: pointer;
.veui-button-transition();

.veui-date-picker:hover &,
&.focus-visible {
.size(16px);
clip: auto;
}

&:hover {
color: @veui-text-color-normal;
}

&.focus-visible {
color: @veui-brand-color;
}

.veui-icon {
display: block;
}
Expand Down
11 changes: 11 additions & 0 deletions packages/veui-theme-one/mixins.less
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
@import "~less-plugin-est/src/all.less";
@import "./variables.less";

// https://github.com/twbs/bootstrap/blob/e43f97304eac2b276c755267e29de70ae2ac7afd/scss/mixins/_screen-reader.scss#L6-L15
.veui-invisible() {
position: absolute;
.size(1px);
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}

.veui-button-transition() {
transition-property: border-color, background-color, color, opacity;
transition-duration: .2s;
Expand Down
5 changes: 3 additions & 2 deletions packages/veui/src/components/Calendar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
ref="prev"
:class="{
'veui-calendar-prev': true,
'veui-calendar-invisible': pIndex !== 0 && p.view === 'days'
'veui-sr-only': pIndex !== 0 && p.view === 'days'
}"
:disabled="disabled || readonly"
@click="step(false, p.view)"
Expand Down Expand Up @@ -64,7 +64,7 @@
ref="next"
:class="{
'veui-calendar-next': true,
'veui-calendar-invisible': pIndex !== panels.length - 1 && p.view === 'days'
'veui-sr-only': pIndex !== panels.length - 1 && p.view === 'days'
}"
:disabled="disabled || readonly"
@click="step(true, p.view)"
Expand Down Expand Up @@ -96,6 +96,7 @@
@keydown.right.prevent="moveFocus(p.view, 1)"
@keydown.down.prevent="moveFocus(p.view, 7)"
@keydown.left.prevent="moveFocus(p.view, -1)"
:autofocus="day.isFocus"
:aria-label="getLocaleString(day)"
:tabindex="day.isFocus ? null : '-1'">
<slot name="date" v-bind="{
Expand Down
33 changes: 28 additions & 5 deletions packages/veui/src/components/DatePicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@
'veui-date-picker-range': range,
'veui-date-picker-expanded': expanded
}">
<veui-button ref="button" class="veui-date-picker-button" :ui="ui" :disabled="realDisabled || realReadonly" @click="expanded = !expanded">
<veui-button
ref="button"
class="veui-date-picker-button"
:ui="ui"
:disabled="realDisabled || realReadonly"
@click="expanded = !expanded"
@keydown.down.up.prevent="expanded = true">
<template v-if="range">
<span class="veui-date-picker-label">
<slot v-if="formatted" name="date" :formatted="formatted ? formatted[0] : null" :date="selected ? selected[0] : null">{{ formatted[0] }}</slot>
Expand All @@ -25,12 +31,29 @@
</template>
<veui-icon class="veui-date-picker-icon" :name="icons.calendar"></veui-icon>
</veui-button>
<button type="button" v-if="clearable" v-show="!!selected" class="veui-date-picker-clear" @click="clear">
<button v-if="clearable && !!selected" type="button" class="veui-date-picker-clear veui-sr-only" @click="clear">
<veui-icon :name="icons.clear"></veui-icon>
</button>
<veui-overlay v-if="expanded" target="button" :open="expanded" :options="realOverlayOptions" :overlay-class="overlayClass">
<veui-calendar class="veui-date-picker-overlay" v-model="localSelected" v-bind="calendarProps" ref="cal"
v-outside:button="close" @select="handleSelect" @selectstart="handleProgress" @selectprogress="handleProgress" :panel="realPanel">
<veui-overlay
v-if="expanded"
target="button"
:open="expanded"
:options="realOverlayOptions"
:overlay-class="overlayClass"
autofocus
modal>
<veui-calendar
class="veui-date-picker-overlay"
v-model="localSelected"
v-bind="calendarProps"
ref="cal"
v-outside:button="close"
@select="handleSelect"
@selectstart="handleProgress"
@selectprogress="handleProgress"
:panel="realPanel"
tabindex="-1"
@keydown.esc.native="close">
<template :slot="shortcutsPosition" v-if="range && realShortcuts && realShortcuts.length">
<div class="veui-date-picker-shortcuts">
<button v-for="({from, to, label}, index) in realShortcuts" type="button" :key="index"
Expand Down
44 changes: 29 additions & 15 deletions packages/veui/src/managers/focus.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,20 @@ class FocusContext {
* @param {=Element} preferred Where we should start searching for focusable elements
*/
constructor (root, { source, trap = false, preferred }) {
if (!root) {
throw new Error('Root must be specified to create a FocusContext instance.')
}

this.root = root
this.source = source
this.trap = trap
this.preferred = preferred

this.outsideHandler = () => {
this.focusFirst(true)
this.outsideStartHandler = () => {
this.focusAt(-2, true)
}
this.outsideEndHandler = () => {
this.focusAt(1, true)
}

this.init()
Expand All @@ -29,29 +36,37 @@ class FocusContext {
* 2. focus `root` by default
*/
init () {
this.focusFirst()
this.focusAt()

if (this.trap) {
let ward = document.createElement('div')
ward.tabIndex = 0
ward.addEventListener('focus', this.outsideHandler, true)
this.root.appendChild(ward)
this.ward = ward
let before = document.createElement('div')
before.tabIndex = 0
let after = before.cloneNode()

before.addEventListener('focus', this.outsideStartHandler, true)
after.addEventListener('focus', this.outsideEndHandler, true)

this.root.insertBefore(before, this.root.firstChild)
this.root.appendChild(after)

this.wardBefore = before
this.wardAfter = after
}
}

focusFirst (ignoreFocus) {
focusAt (index = 0, ignoreAutofocus) {
Vue.nextTick(() => {
if (!focusIn(this.preferred || this.root, ignoreFocus)) {
if (!focusIn(this.preferred || this.root, index, ignoreAutofocus)) {
this.root.focus()
}
})
}

destroy () {
let { trap, source, ward } = this
let { trap, source, wardBefore, wardAfter } = this
if (trap) {
ward.removeEventListener('focus', this.outsideHandler, true)
wardBefore.removeEventListener('focus', this.outsideStartHandler, true)
wardAfter.removeEventListener('focus', this.outsideEndHandler, true)
}
if (source && typeof source.focus === 'function') {
this.source = null
Expand All @@ -62,9 +77,8 @@ class FocusContext {
source.focus()
}, 0)
}
if (ward) {
ward.parentElement.removeChild(ward)
}
this.root.removeChild(wardBefore)
this.root.removeChild(wardAfter)
this.preferred = null
this.root = null
}
Expand Down
24 changes: 18 additions & 6 deletions packages/veui/src/utils/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,21 +129,33 @@ export function getFocusable (elem) {
* 将焦点移入指定元素内的第一个可聚焦的元素
*
* @param {Element} elem 需要查找的指定元素
* @param {number=} index 聚焦元素在可聚焦元素的位置
* @param {Boolean=} ignoreAutofocus 是否忽略 autofocus
* @returns {Boolean} 是否找到可聚焦的元素
*/
export function focusIn (elem, ignoreAutofocus) {
export function focusIn (elem, index = 0, ignoreAutofocus) {
if (!ignoreAutofocus) {
let auto = elem.querySelector('[autofocus]')
if (auto) {
auto.focus()
return true
}
}
let first = elem.querySelector(FOCUSABLE_SELECTOR)
if (first) {
first.focus()
return true

if (index === 0) {
let first = elem.querySelector(FOCUSABLE_SELECTOR)
if (first) {
first.focus()
return true
}
}
return false

let focusable = [...elem.querySelectorAll(FOCUSABLE_SELECTOR)]
let count = focusable.length
if (!count) {
return false
}

focusable[(index + count) % count].focus()
return true
}

0 comments on commit 621ab48

Please sign in to comment.