Commit 7be1069a93aa5480c39049d16717361a29604075

Authored by Sergio Crisostomo
1 parent 5a12b03a

Add tab navigation to Tabs

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="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, direction, countDisabledAlso);
  47 + else return nextTab;
  48 + };
  49 +
  50 + const focusFirst = (element, root) => {
  51 + try {element.focus();}
  52 + catch(err) {}
  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,45 @@
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;
  241 + const nextIndex = this.navList.findIndex(tab => tab.name === this.focusedKey);
  242 + [...this.$refs.panes.children].forEach((el, i) => {
  243 + if (nextIndex === i) {
  244 + [...el.children].forEach(child => child.style.display = 'block');
  245 + setTimeout(() => {
  246 + focusFirst(el, el);
  247 + }, transitionTime);
  248 + } else {
  249 + setTimeout(() => {
  250 + [...el.children].forEach(child => child.style.display = 'none');
  251 + }, transitionTime);
  252 + }
  253 + });
  254 + },
197 255 handleRemove (index) {
198 256 const tabs = this.getTabs();
199 257 const tab = tabs[index];
... ... @@ -325,8 +383,10 @@
325 383 watch: {
326 384 value (val) {
327 385 this.activeKey = val;
  386 + this.focusedKey = val;
328 387 },
329   - activeKey () {
  388 + activeKey (val) {
  389 + this.focusedKey = val;
330 390 this.updateBar();
331 391 this.updateStatus();
332 392 this.broadcast('Table', 'on-visible-change', true);
... ...
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 +}
... ...