Merge pull request #3554 from SergioCrisostomo/tabs-keyboard

Tabs keyboard navigation
This commit is contained in:
Aresn 2018-05-08 20:22:12 +08:00 committed by GitHub
commit acbd8b1792
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 95 additions and 9 deletions

View file

@ -158,10 +158,25 @@
<!--</script>--> <!--</script>-->
<template> <template>
<Tabs type="card"> <div>
<TabPane v-for="tab in tabs" :key="tab" :label="'标签' + tab">标签{{ tab }}</TabPane> <i-input></i-input>
<Button type="ghost" @click="handleTabsAdd" size="small" slot="extra">增加</Button> <Button type="ghost" @click="handleTabsAdd" size="small" slot="extra">增加</Button>
<hr style="margin: 10px 0;">
<Tabs type="card">
<TabPane v-for="tab in tabs" :key="tab" :label="'Tab' + tab">
<div>
<h3>Some text...</h3>
<i-button>Some focusable content...{{ tab }}</i-button>
</div>
</TabPane>
</Tabs> </Tabs>
<Tabs type="card">
<TabPane label="标签一">标签一的内容</TabPane>
<TabPane label="标签二" disabled>标签二的内容</TabPane>
<TabPane label="标签三">标签三的内容</TabPane>
</Tabs>
</div>
</template> </template>
<script> <script>
export default { export default {

View file

@ -2,7 +2,13 @@
<div :class="classes"> <div :class="classes">
<div :class="[prefixCls + '-bar']"> <div :class="[prefixCls + '-bar']">
<div :class="[prefixCls + '-nav-right']" v-if="showSlot"><slot name="extra"></slot></div> <div :class="[prefixCls + '-nav-right']" v-if="showSlot"><slot name="extra"></slot></div>
<div :class="[prefixCls + '-nav-container']"> <div
:class="[prefixCls + '-nav-container']"
tabindex="0"
ref="navContainer"
@keydown="handleTabKeyNavigation"
@keydown.space.prevent="handleTabKeyboardSelect"
>
<div ref="navWrap" :class="[prefixCls + '-nav-wrap', scrollable ? prefixCls + '-nav-scrollable' : '']"> <div ref="navWrap" :class="[prefixCls + '-nav-wrap', scrollable ? prefixCls + '-nav-scrollable' : '']">
<span :class="[prefixCls + '-nav-prev', scrollable ? '' : prefixCls + '-nav-scroll-disabled']" @click="scrollPrev"><Icon type="chevron-left"></Icon></span> <span :class="[prefixCls + '-nav-prev', scrollable ? '' : prefixCls + '-nav-scroll-disabled']" @click="scrollPrev"><Icon type="chevron-left"></Icon></span>
<span :class="[prefixCls + '-nav-next', scrollable ? '' : prefixCls + '-nav-scroll-disabled']" @click="scrollNext"><Icon type="chevron-right"></Icon></span> <span :class="[prefixCls + '-nav-next', scrollable ? '' : prefixCls + '-nav-scroll-disabled']" @click="scrollNext"><Icon type="chevron-right"></Icon></span>
@ -20,7 +26,7 @@
</div> </div>
</div> </div>
</div> </div>
<div :class="contentClasses" :style="contentStyle"><slot></slot></div> <div :class="contentClasses" :style="contentStyle" ref="panes"><slot></slot></div>
</div> </div>
</template> </template>
<script> <script>
@ -31,6 +37,28 @@
import elementResizeDetectorMaker from 'element-resize-detector'; import elementResizeDetectorMaker from 'element-resize-detector';
const prefixCls = 'ivu-tabs'; const prefixCls = 'ivu-tabs';
const transitionTime = 300; // from CSS
const getNextTab = (list, activeKey, direction, countDisabledAlso) => {
const currentIndex = list.findIndex(tab => tab.name === activeKey);
const nextIndex = (currentIndex + direction + list.length) % list.length;
const nextTab = list[nextIndex];
if (nextTab.disabled) return getNextTab(list, nextTab.name, direction, countDisabledAlso);
else return nextTab;
};
const focusFirst = (element, root) => {
try {element.focus();}
catch(err) {} // eslint-disable-line no-empty
if (document.activeElement == element && element !== root) return true;
const candidates = element.children;
for (let candidate of candidates) {
if (focusFirst(candidate, root)) return true;
}
return false;
};
export default { export default {
name: 'Tabs', name: 'Tabs',
@ -68,11 +96,13 @@
barWidth: 0, barWidth: 0,
barOffset: 0, barOffset: 0,
activeKey: this.value, activeKey: this.value,
focusedKey: this.value,
showSlot: false, showSlot: false,
navStyle: { navStyle: {
transform: '' transform: ''
}, },
scrollable: false scrollable: false,
transitioning: false,
}; };
}, },
computed: { computed: {
@ -183,17 +213,46 @@
`${prefixCls}-tab`, `${prefixCls}-tab`,
{ {
[`${prefixCls}-tab-disabled`]: item.disabled, [`${prefixCls}-tab-disabled`]: item.disabled,
[`${prefixCls}-tab-active`]: item.name === this.activeKey [`${prefixCls}-tab-active`]: item.name === this.activeKey,
[`${prefixCls}-tab-focused`]: item.name === this.focusedKey,
} }
]; ];
}, },
handleChange (index) { handleChange (index) {
if (this.transitioning) return;
this.transitioning = true;
setTimeout(() => this.transitioning = false, transitionTime);
const nav = this.navList[index]; const nav = this.navList[index];
if (nav.disabled) return; if (nav.disabled) return;
this.activeKey = nav.name; this.activeKey = nav.name;
this.$emit('input', nav.name); this.$emit('input', nav.name);
this.$emit('on-click', nav.name); this.$emit('on-click', nav.name);
}, },
handleTabKeyNavigation(e){
if (e.keyCode !== 37 && e.keyCode !== 39) return;
const direction = e.keyCode === 39 ? 1 : -1;
const nextTab = getNextTab(this.navList, this.focusedKey, direction);
this.focusedKey = nextTab.name;
},
handleTabKeyboardSelect(){
this.activeKey = this.focusedKey || 0;
const nextIndex = Math.max(this.navList.findIndex(tab => tab.name === this.focusedKey), 0);
[...this.$refs.panes.children].forEach((el, i) => {
if (nextIndex === i) {
[...el.children].forEach(child => child.style.display = 'block');
setTimeout(() => {
focusFirst(el, el);
}, transitionTime);
} else {
setTimeout(() => {
[...el.children].forEach(child => child.style.display = 'none');
}, transitionTime);
}
});
},
handleRemove (index) { handleRemove (index) {
const tabs = this.getTabs(); const tabs = this.getTabs();
const tab = tabs[index]; const tab = tabs[index];
@ -325,8 +384,10 @@
watch: { watch: {
value (val) { value (val) {
this.activeKey = val; this.activeKey = val;
this.focusedKey = val;
}, },
activeKey () { activeKey (val) {
this.focusedKey = val;
this.updateBar(); this.updateBar();
this.updateStatus(); this.updateStatus();
this.broadcast('Table', 'on-visible-change', true); this.broadcast('Table', 'on-visible-change', true);
@ -351,6 +412,8 @@
this.mutationObserver.observe(hiddenParentNode, { attributes: true, childList: true, characterData: true, attributeFilter: ['style'] }); this.mutationObserver.observe(hiddenParentNode, { attributes: true, childList: true, characterData: true, attributeFilter: ['style'] });
} }
this.handleTabKeyboardSelect();
}, },
beforeDestroy() { beforeDestroy() {
this.observer.removeListener(this.$refs.navWrap, this.handleResize); this.observer.removeListener(this.$refs.navWrap, this.handleResize);

View file

@ -39,6 +39,13 @@
.clearfix; .clearfix;
} }
&-nav-container:focus {
outline: none;
.@{tabs-prefix-cls}-tab-focused {
border-color: @link-hover-color !important;
}
}
&-nav-container-scrolling { &-nav-container-scrolling {
padding-left: 32px; padding-left: 32px;
padding-right: 32px; padding-right: 32px;
@ -158,6 +165,7 @@
width: 100%; width: 100%;
transition: opacity .3s; transition: opacity .3s;
opacity: 1; opacity: 1;
outline: none;
} }
.@{tabs-prefix-cls}-tabpane-inactive { .@{tabs-prefix-cls}-tabpane-inactive {