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,10 +158,25 @@
158 <!--</script>--> 158 <!--</script>-->
159 159
160 <template> 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 <Button type="ghost" @click="handleTabsAdd" size="small" slot="extra">增加</Button> 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 </template> 180 </template>
166 <script> 181 <script>
167 export default { 182 export default {
src/components/tabs/tabs.vue
@@ -2,7 +2,13 @@ @@ -2,7 +2,13 @@
2 <div :class="classes"> 2 <div :class="classes">
3 <div :class="[prefixCls + '-bar']"> 3 <div :class="[prefixCls + '-bar']">
4 <div :class="[prefixCls + '-nav-right']" v-if="showSlot"><slot name="extra"></slot></div> 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 <div ref="navWrap" :class="[prefixCls + '-nav-wrap', scrollable ? prefixCls + '-nav-scrollable' : '']"> 12 <div ref="navWrap" :class="[prefixCls + '-nav-wrap', scrollable ? prefixCls + '-nav-scrollable' : '']">
7 <span :class="[prefixCls + '-nav-prev', scrollable ? '' : prefixCls + '-nav-scroll-disabled']" @click="scrollPrev"><Icon type="chevron-left"></Icon></span> 13 <span :class="[prefixCls + '-nav-prev', scrollable ? '' : prefixCls + '-nav-scroll-disabled']" @click="scrollPrev"><Icon type="chevron-left"></Icon></span>
8 <span :class="[prefixCls + '-nav-next', scrollable ? '' : prefixCls + '-nav-scroll-disabled']" @click="scrollNext"><Icon type="chevron-right"></Icon></span> 14 <span :class="[prefixCls + '-nav-next', scrollable ? '' : prefixCls + '-nav-scroll-disabled']" @click="scrollNext"><Icon type="chevron-right"></Icon></span>
@@ -20,7 +26,7 @@ @@ -20,7 +26,7 @@
20 </div> 26 </div>
21 </div> 27 </div>
22 </div> 28 </div>
23 - <div :class="contentClasses" :style="contentStyle"><slot></slot></div> 29 + <div :class="contentClasses" :style="contentStyle" ref="panes"><slot></slot></div>
24 </div> 30 </div>
25 </template> 31 </template>
26 <script> 32 <script>
@@ -31,6 +37,28 @@ @@ -31,6 +37,28 @@
31 import elementResizeDetectorMaker from 'element-resize-detector'; 37 import elementResizeDetectorMaker from 'element-resize-detector';
32 38
33 const prefixCls = 'ivu-tabs'; 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 export default { 63 export default {
36 name: 'Tabs', 64 name: 'Tabs',
@@ -68,11 +96,13 @@ @@ -68,11 +96,13 @@
68 barWidth: 0, 96 barWidth: 0,
69 barOffset: 0, 97 barOffset: 0,
70 activeKey: this.value, 98 activeKey: this.value,
  99 + focusedKey: this.value,
71 showSlot: false, 100 showSlot: false,
72 navStyle: { 101 navStyle: {
73 transform: '' 102 transform: ''
74 }, 103 },
75 - scrollable: false 104 + scrollable: false,
  105 + transitioning: false,
76 }; 106 };
77 }, 107 },
78 computed: { 108 computed: {
@@ -183,17 +213,46 @@ @@ -183,17 +213,46 @@
183 `${prefixCls}-tab`, 213 `${prefixCls}-tab`,
184 { 214 {
185 [`${prefixCls}-tab-disabled`]: item.disabled, 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 handleChange (index) { 221 handleChange (index) {
  222 + if (this.transitioning) return;
  223 +
  224 + this.transitioning = true;
  225 + setTimeout(() => this.transitioning = false, transitionTime);
  226 +
191 const nav = this.navList[index]; 227 const nav = this.navList[index];
192 if (nav.disabled) return; 228 if (nav.disabled) return;
193 this.activeKey = nav.name; 229 this.activeKey = nav.name;
194 this.$emit('input', nav.name); 230 this.$emit('input', nav.name);
195 this.$emit('on-click', nav.name); 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 handleRemove (index) { 256 handleRemove (index) {
198 const tabs = this.getTabs(); 257 const tabs = this.getTabs();
199 const tab = tabs[index]; 258 const tab = tabs[index];
@@ -325,8 +384,10 @@ @@ -325,8 +384,10 @@
325 watch: { 384 watch: {
326 value (val) { 385 value (val) {
327 this.activeKey = val; 386 this.activeKey = val;
  387 + this.focusedKey = val;
328 }, 388 },
329 - activeKey () { 389 + activeKey (val) {
  390 + this.focusedKey = val;
330 this.updateBar(); 391 this.updateBar();
331 this.updateStatus(); 392 this.updateStatus();
332 this.broadcast('Table', 'on-visible-change', true); 393 this.broadcast('Table', 'on-visible-change', true);
@@ -351,6 +412,8 @@ @@ -351,6 +412,8 @@
351 412
352 this.mutationObserver.observe(hiddenParentNode, { attributes: true, childList: true, characterData: true, attributeFilter: ['style'] }); 413 this.mutationObserver.observe(hiddenParentNode, { attributes: true, childList: true, characterData: true, attributeFilter: ['style'] });
353 } 414 }
  415 +
  416 + this.handleTabKeyboardSelect();
354 }, 417 },
355 beforeDestroy() { 418 beforeDestroy() {
356 this.observer.removeListener(this.$refs.navWrap, this.handleResize); 419 this.observer.removeListener(this.$refs.navWrap, this.handleResize);
src/styles/components/tabs.less
@@ -39,6 +39,13 @@ @@ -39,6 +39,13 @@
39 .clearfix; 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 &-nav-container-scrolling { 49 &-nav-container-scrolling {
43 padding-left: 32px; 50 padding-left: 32px;
44 padding-right: 32px; 51 padding-right: 32px;
@@ -158,6 +165,7 @@ @@ -158,6 +165,7 @@
158 width: 100%; 165 width: 100%;
159 transition: opacity .3s; 166 transition: opacity .3s;
160 opacity: 1; 167 opacity: 1;
  168 + outline: none;
161 } 169 }
162 170
163 .@{tabs-prefix-cls}-tabpane-inactive { 171 .@{tabs-prefix-cls}-tabpane-inactive {
@@ -228,4 +236,4 @@ @@ -228,4 +236,4 @@
228 display: none; 236 display: none;
229 } 237 }
230 } 238 }
231 -}  
232 \ No newline at end of file 239 \ No newline at end of file
  240 +}