Skip to content

Commit 621ab48

Browse files
authored
* add .veui-sr-only (#249)
* fix backward tabbing by adding focus ward on both directions * add aria support for DatePicker
1 parent 6bb1be5 commit 621ab48

File tree

8 files changed

+104
-34
lines changed

8 files changed

+104
-34
lines changed

packages/veui-theme-one/common.less

+4
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ button {
5353
cursor: not-allowed !important;
5454
}
5555

56+
.veui-sr-only {
57+
.veui-invisible();
58+
}
59+
5660
a {
5761
text-decoration: none;
5862
}

packages/veui-theme-one/components/calendar.less

+1-5
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@
55
overflow: hidden;
66
border: 1px solid @veui-gray-color-6;
77
background-color: #fff;
8-
9-
&-invisible {
10-
.size(0);
11-
opacity: 0;
12-
}
8+
outline: none;
139

1410
button {
1511
.veui-button-transition();

packages/veui-theme-one/components/date-picker.less

+10-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
}
3434

3535
&-clear {
36-
display: none;
3736
.absolute(10px, 10px, _, _);
3837
.size(16px);
3938
background: #fff;
@@ -44,10 +43,20 @@
4443
cursor: pointer;
4544
.veui-button-transition();
4645

46+
.veui-date-picker:hover &,
47+
&.focus-visible {
48+
.size(16px);
49+
clip: auto;
50+
}
51+
4752
&:hover {
4853
color: @veui-text-color-normal;
4954
}
5055

56+
&.focus-visible {
57+
color: @veui-brand-color;
58+
}
59+
5160
.veui-icon {
5261
display: block;
5362
}

packages/veui-theme-one/mixins.less

+11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
@import "~less-plugin-est/src/all.less";
22
@import "./variables.less";
33

4+
// https://github.com/twbs/bootstrap/blob/e43f97304eac2b276c755267e29de70ae2ac7afd/scss/mixins/_screen-reader.scss#L6-L15
5+
.veui-invisible() {
6+
position: absolute;
7+
.size(1px);
8+
padding: 0;
9+
overflow: hidden;
10+
clip: rect(0, 0, 0, 0);
11+
white-space: nowrap;
12+
border: 0;
13+
}
14+
415
.veui-button-transition() {
516
transition-property: border-color, background-color, color, opacity;
617
transition-duration: .2s;

packages/veui/src/components/Calendar.vue

+3-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
ref="prev"
1616
:class="{
1717
'veui-calendar-prev': true,
18-
'veui-calendar-invisible': pIndex !== 0 && p.view === 'days'
18+
'veui-sr-only': pIndex !== 0 && p.view === 'days'
1919
}"
2020
:disabled="disabled || readonly"
2121
@click="step(false, p.view)"
@@ -64,7 +64,7 @@
6464
ref="next"
6565
:class="{
6666
'veui-calendar-next': true,
67-
'veui-calendar-invisible': pIndex !== panels.length - 1 && p.view === 'days'
67+
'veui-sr-only': pIndex !== panels.length - 1 && p.view === 'days'
6868
}"
6969
:disabled="disabled || readonly"
7070
@click="step(true, p.view)"
@@ -96,6 +96,7 @@
9696
@keydown.right.prevent="moveFocus(p.view, 1)"
9797
@keydown.down.prevent="moveFocus(p.view, 7)"
9898
@keydown.left.prevent="moveFocus(p.view, -1)"
99+
:autofocus="day.isFocus"
99100
:aria-label="getLocaleString(day)"
100101
:tabindex="day.isFocus ? null : '-1'">
101102
<slot name="date" v-bind="{

packages/veui/src/components/DatePicker.vue

+28-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@
55
'veui-date-picker-range': range,
66
'veui-date-picker-expanded': expanded
77
}">
8-
<veui-button ref="button" class="veui-date-picker-button" :ui="ui" :disabled="realDisabled || realReadonly" @click="expanded = !expanded">
8+
<veui-button
9+
ref="button"
10+
class="veui-date-picker-button"
11+
:ui="ui"
12+
:disabled="realDisabled || realReadonly"
13+
@click="expanded = !expanded"
14+
@keydown.down.up.prevent="expanded = true">
915
<template v-if="range">
1016
<span class="veui-date-picker-label">
1117
<slot v-if="formatted" name="date" :formatted="formatted ? formatted[0] : null" :date="selected ? selected[0] : null">{{ formatted[0] }}</slot>
@@ -25,12 +31,29 @@
2531
</template>
2632
<veui-icon class="veui-date-picker-icon" :name="icons.calendar"></veui-icon>
2733
</veui-button>
28-
<button type="button" v-if="clearable" v-show="!!selected" class="veui-date-picker-clear" @click="clear">
34+
<button v-if="clearable && !!selected" type="button" class="veui-date-picker-clear veui-sr-only" @click="clear">
2935
<veui-icon :name="icons.clear"></veui-icon>
3036
</button>
31-
<veui-overlay v-if="expanded" target="button" :open="expanded" :options="realOverlayOptions" :overlay-class="overlayClass">
32-
<veui-calendar class="veui-date-picker-overlay" v-model="localSelected" v-bind="calendarProps" ref="cal"
33-
v-outside:button="close" @select="handleSelect" @selectstart="handleProgress" @selectprogress="handleProgress" :panel="realPanel">
37+
<veui-overlay
38+
v-if="expanded"
39+
target="button"
40+
:open="expanded"
41+
:options="realOverlayOptions"
42+
:overlay-class="overlayClass"
43+
autofocus
44+
modal>
45+
<veui-calendar
46+
class="veui-date-picker-overlay"
47+
v-model="localSelected"
48+
v-bind="calendarProps"
49+
ref="cal"
50+
v-outside:button="close"
51+
@select="handleSelect"
52+
@selectstart="handleProgress"
53+
@selectprogress="handleProgress"
54+
:panel="realPanel"
55+
tabindex="-1"
56+
@keydown.esc.native="close">
3457
<template :slot="shortcutsPosition" v-if="range && realShortcuts && realShortcuts.length">
3558
<div class="veui-date-picker-shortcuts">
3659
<button v-for="({from, to, label}, index) in realShortcuts" type="button" :key="index"

packages/veui/src/managers/focus.js

+29-15
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,20 @@ class FocusContext {
1111
* @param {=Element} preferred Where we should start searching for focusable elements
1212
*/
1313
constructor (root, { source, trap = false, preferred }) {
14+
if (!root) {
15+
throw new Error('Root must be specified to create a FocusContext instance.')
16+
}
17+
1418
this.root = root
1519
this.source = source
1620
this.trap = trap
1721
this.preferred = preferred
1822

19-
this.outsideHandler = () => {
20-
this.focusFirst(true)
23+
this.outsideStartHandler = () => {
24+
this.focusAt(-2, true)
25+
}
26+
this.outsideEndHandler = () => {
27+
this.focusAt(1, true)
2128
}
2229

2330
this.init()
@@ -29,29 +36,37 @@ class FocusContext {
2936
* 2. focus `root` by default
3037
*/
3138
init () {
32-
this.focusFirst()
39+
this.focusAt()
3340

3441
if (this.trap) {
35-
let ward = document.createElement('div')
36-
ward.tabIndex = 0
37-
ward.addEventListener('focus', this.outsideHandler, true)
38-
this.root.appendChild(ward)
39-
this.ward = ward
42+
let before = document.createElement('div')
43+
before.tabIndex = 0
44+
let after = before.cloneNode()
45+
46+
before.addEventListener('focus', this.outsideStartHandler, true)
47+
after.addEventListener('focus', this.outsideEndHandler, true)
48+
49+
this.root.insertBefore(before, this.root.firstChild)
50+
this.root.appendChild(after)
51+
52+
this.wardBefore = before
53+
this.wardAfter = after
4054
}
4155
}
4256

43-
focusFirst (ignoreFocus) {
57+
focusAt (index = 0, ignoreAutofocus) {
4458
Vue.nextTick(() => {
45-
if (!focusIn(this.preferred || this.root, ignoreFocus)) {
59+
if (!focusIn(this.preferred || this.root, index, ignoreAutofocus)) {
4660
this.root.focus()
4761
}
4862
})
4963
}
5064

5165
destroy () {
52-
let { trap, source, ward } = this
66+
let { trap, source, wardBefore, wardAfter } = this
5367
if (trap) {
54-
ward.removeEventListener('focus', this.outsideHandler, true)
68+
wardBefore.removeEventListener('focus', this.outsideStartHandler, true)
69+
wardAfter.removeEventListener('focus', this.outsideEndHandler, true)
5570
}
5671
if (source && typeof source.focus === 'function') {
5772
this.source = null
@@ -62,9 +77,8 @@ class FocusContext {
6277
source.focus()
6378
}, 0)
6479
}
65-
if (ward) {
66-
ward.parentElement.removeChild(ward)
67-
}
80+
this.root.removeChild(wardBefore)
81+
this.root.removeChild(wardAfter)
6882
this.preferred = null
6983
this.root = null
7084
}

packages/veui/src/utils/dom.js

+18-6
Original file line numberDiff line numberDiff line change
@@ -129,21 +129,33 @@ export function getFocusable (elem) {
129129
* 将焦点移入指定元素内的第一个可聚焦的元素
130130
*
131131
* @param {Element} elem 需要查找的指定元素
132+
* @param {number=} index 聚焦元素在可聚焦元素的位置
132133
* @param {Boolean=} ignoreAutofocus 是否忽略 autofocus
133134
* @returns {Boolean} 是否找到可聚焦的元素
134135
*/
135-
export function focusIn (elem, ignoreAutofocus) {
136+
export function focusIn (elem, index = 0, ignoreAutofocus) {
136137
if (!ignoreAutofocus) {
137138
let auto = elem.querySelector('[autofocus]')
138139
if (auto) {
139140
auto.focus()
140141
return true
141142
}
142143
}
143-
let first = elem.querySelector(FOCUSABLE_SELECTOR)
144-
if (first) {
145-
first.focus()
146-
return true
144+
145+
if (index === 0) {
146+
let first = elem.querySelector(FOCUSABLE_SELECTOR)
147+
if (first) {
148+
first.focus()
149+
return true
150+
}
147151
}
148-
return false
152+
153+
let focusable = [...elem.querySelectorAll(FOCUSABLE_SELECTOR)]
154+
let count = focusable.length
155+
if (!count) {
156+
return false
157+
}
158+
159+
focusable[(index + count) % count].focus()
160+
return true
149161
}

0 commit comments

Comments
 (0)