Commit acbd8b1792ddb134c7037fe98ceaa0716da2efb8

Authored by Aresn
Committed by GitHub
2 parents d4f39edc 45dbc6fd

Merge pull request #3554 from SergioCrisostomo/tabs-keyboard

Tabs keyboard navigation
examples/routers/tabs.vue
... ... @@ -158,10 +158,25 @@
158 158 <!--</script>-->
159 159  
160 160 <template>
161   - <Tabs type="card">
162   - <TabPane v-for="tab in tabs" :key="tab" :label="'标签' + tab">标签{{ tab }}</TabPane>
  161 + <div>
  162 + <i-input></i-input>
163 163 <Button type="ghost" @click="handleTabsAdd" size="small" slot="extra">增加</Button>
164   - </Tabs>
  164 +
  165 + <hr style="margin: 10px 0;">
  166 + <Tabs type="card">
  167 + <TabPane v-for="tab in tabs" :key="tab" :label="'Tab' + tab">
  168 + <div>
  169 + <h3>Some text...</h3>
  170 + <i-button>Some focusable content...{{ tab }}</i-button>
  171 + </div>
  172 + </TabPane>
  173 + </Tabs>
  174 + <Tabs type="card">
  175 + <TabPane label="标签一">标签一的内容</TabPane>
  176 + <TabPane label="标签二" disabled>标签二的内容</TabPane>
  177 + <TabPane label="标签三">标签三的内容</TabPane>
  178 + </Tabs>
  179 + </div>
165 180 </template>
166 181 <script>
167 182 export default {
... ...
src/components/tabs/tabs.vue
... ... @@ -2,7 +2,13 @@
2 2 <div :class="classes">
3 3 <div :class="[prefixCls + '-bar']">
4 4 <div :class="[prefixCls + '-nav-right']" v-if="showSlot"><slot name="extra"></slot></div>
5   - <div :class="[prefixCls + '-nav-container']">
  5 + <div
  6 + :class="[prefixCls + '-nav-container']"
  7 + tabindex="0"
  8 + ref="navContainer"
  9 + @keydown="handleTabKeyNavigation"
  10 + @keydown.space.prevent="handleTabKeyboardSelect"
  11 + >
6 12 <div ref="navWrap" :class="[prefixCls + '-nav-wrap', scrollable ? prefixCls + '-nav-scrollable' : '']">
7 13 <span :class="[prefixCls + '-nav-prev', scrollable ? '' : prefixCls + '-nav-scroll-disabled']" @click="scrollPrev"><Icon type="chevron-left"></Icon></span>
8 14 <span :class="[prefixCls + '-nav-next', scrollable ? '' : prefixCls + '-nav-scroll-disabled']" @click="scrollNext"><Icon type="chevron-right"></Icon></span>
... ... @@ -20,7 +26,7 @@
20 26 </div>
21 27 </div>
22 28 </div>
23   - <div :class="contentClasses" :style="contentStyle"><slot></slot></div>
  29 + <div :class="contentClasses" :style="contentStyle" ref="panes"><slot></slot></div>
24 30 </div>
25 31 </template>
26 32 <script>
... ... @@ -31,6 +37,28 @@
31 37 import elementResizeDetectorMaker from 'element-resize-detector';
32 38  
33 39 const prefixCls = 'ivu-tabs';
  40 + const transitionTime = 300; // from CSS
  41 +
  42 + const getNextTab = (list, activeKey, direction, countDisabledAlso) => {
  43 + const currentIndex = list.findIndex(tab => tab.name === activeKey);
  44 + const nextIndex = (currentIndex + direction + list.length) % list.length;
  45 + const nextTab = list[nextIndex];
  46 + if (nextTab.disabled) return getNextTab(list, nextTab.name, direction, countDisabledAlso);
  47 + else return nextTab;
  48 + };
  49 +
  50 + const focusFirst = (element, root) => {
  51 + try {element.focus();}
  52 + catch(err) {} // eslint-disable-line no-empty
  53 +
  54 + if (document.activeElement == element && element !== root) return true;
  55 +
  56 + const candidates = element.children;
  57 + for (let candidate of candidates) {
  58 + if (focusFirst(candidate, root)) return true;
  59 + }
  60 + return false;
  61 + };
34 62  
35 63 export default {
36 64 name: 'Tabs',
... ... @@ -68,11 +96,13 @@
68 96 barWidth: 0,
69 97 barOffset: 0,
70 98 activeKey: this.value,
  99 + focusedKey: this.value,
71 100 showSlot: false,
72 101 navStyle: {
73 102 transform: ''
74 103 },
75   - scrollable: false
  104 + scrollable: false,
  105 + transitioning: false,
76 106 };
77 107 },
78 108 computed: {
... ... @@ -183,17 +213,46 @@
183 213 `${prefixCls}-tab`,
184 214 {
185 215 [`${prefixCls}-tab-disabled`]: item.disabled,
186   - [`${prefixCls}-tab-active`]: item.name === this.activeKey
  216 + [`${prefixCls}-tab-active`]: item.name === this.activeKey,
  217 + [`${prefixCls}-tab-focused`]: item.name === this.focusedKey,
187 218 }
188 219 ];
189 220 },
190 221 handleChange (index) {
  222 + if (this.transitioning) return;
  223 +
  224 + this.transitioning = true;
  225 + setTimeout(() => this.transitioning = false, transitionTime);
  226 +
191 227 const nav = this.navList[index];
192 228 if (nav.disabled) return;
193 229 this.activeKey = nav.name;
194 230 this.$emit('input', nav.name);
195 231 this.$emit('on-click', nav.name);
196 232 },
  233 + handleTabKeyNavigation(e){
  234 + if (e.keyCode !== 37 && e.keyCode !== 39) return;
  235 + const direction = e.keyCode === 39 ? 1 : -1;
  236 + const nextTab = getNextTab(this.navList, this.focusedKey, direction);
  237 + this.focusedKey = nextTab.name;
  238 + },
  239 + handleTabKeyboardSelect(){
  240 + this.activeKey = this.focusedKey || 0;
  241 + const nextIndex = Math.max(this.navList.findIndex(tab => tab.name === this.focusedKey), 0);
  242 +
  243 + [...this.$refs.panes.children].forEach((el, i) => {
  244 + if (nextIndex === i) {
  245 + [...el.children].forEach(child => child.style.display = 'block');
  246 + setTimeout(() => {
  247 + focusFirst(el, el);
  248 + }, transitionTime);
  249 + } else {
  250 + setTimeout(() => {
  251 + [...el.children].forEach(child => child.style.display = 'none');
  252 + }, transitionTime);
  253 + }
  254 + });
  255 + },
197 256 handleRemove (index) {
198 257 const tabs = this.getTabs();
199 258 const tab = tabs[index];
... ... @@ -325,8 +384,10 @@
325 384 watch: {
326 385 value (val) {
327 386 this.activeKey = val;
  387 + this.focusedKey = val;
328 388 },
329   - activeKey () {
  389 + activeKey (val) {
  390 + this.focusedKey = val;
330 391 this.updateBar();
331 392 this.updateStatus();
332 393 this.broadcast('Table', 'on-visible-change', true);
... ... @@ -351,6 +412,8 @@
351 412  
352 413 this.mutationObserver.observe(hiddenParentNode, { attributes: true, childList: true, characterData: true, attributeFilter: ['style'] });
353 414 }
  415 +
  416 + this.handleTabKeyboardSelect();
354 417 },
355 418 beforeDestroy() {
356 419 this.observer.removeListener(this.$refs.navWrap, this.handleResize);
... ...
src/styles/components/tabs.less
... ... @@ -39,6 +39,13 @@
39 39 .clearfix;
40 40 }
41 41  
  42 + &-nav-container:focus {
  43 + outline: none;
  44 + .@{tabs-prefix-cls}-tab-focused {
  45 + border-color: @link-hover-color !important;
  46 + }
  47 + }
  48 +
42 49 &-nav-container-scrolling {
43 50 padding-left: 32px;
44 51 padding-right: 32px;
... ... @@ -158,6 +165,7 @@
158 165 width: 100%;
159 166 transition: opacity .3s;
160 167 opacity: 1;
  168 + outline: none;
161 169 }
162 170  
163 171 .@{tabs-prefix-cls}-tabpane-inactive {
... ... @@ -228,4 +236,4 @@
228 236 display: none;
229 237 }
230 238 }
231   -}
232 239 \ No newline at end of file
  240 +}
... ...