Merge pull request #3554 from SergioCrisostomo/tabs-keyboard
Tabs keyboard navigation
This commit is contained in:
commit
acbd8b1792
3 changed files with 95 additions and 9 deletions
|
@ -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>
|
||||||
</Tabs>
|
|
||||||
|
<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 type="card">
|
||||||
|
<TabPane label="标签一">标签一的内容</TabPane>
|
||||||
|
<TabPane label="标签二" disabled>标签二的内容</TabPane>
|
||||||
|
<TabPane label="标签三">标签三的内容</TabPane>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 {
|
||||||
|
@ -228,4 +236,4 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue