Commit acbd8b1792ddb134c7037fe98ceaa0716da2efb8
Committed by
GitHub
Merge pull request #3554 from SergioCrisostomo/tabs-keyboard
Tabs keyboard navigation
Showing
3 changed files
with
95 additions
and
9 deletions
Show diff stats
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 | +} | ... | ... |