mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-12-01 10:47:57 +08:00
feat(tabs): add tabs
This commit is contained in:
parent
ed5d989bb5
commit
18aa2638f6
@ -24,6 +24,8 @@ import ElScrollBar from '@element-plus/scrollbar'
|
||||
import ElSteps from '@element-plus/steps'
|
||||
import ElCollapse from '@element-plus/collapse'
|
||||
import ElPopper from '@element-plus/popper'
|
||||
import ElTabs from '@element-plus/tabs'
|
||||
|
||||
|
||||
export {
|
||||
ElAlert,
|
||||
@ -50,6 +52,7 @@ export {
|
||||
ElSteps,
|
||||
ElRadio,
|
||||
ElCollapse,
|
||||
ElTabs,
|
||||
}
|
||||
|
||||
export default function install(app: App): void {
|
||||
@ -78,4 +81,5 @@ export default function install(app: App): void {
|
||||
ElRadio(app)
|
||||
ElCollapse(app)
|
||||
ElPopper(app)
|
||||
ElTabs(app)
|
||||
}
|
||||
|
@ -34,6 +34,7 @@
|
||||
"@element-plus/scrollbar": "^0.0.0",
|
||||
"@element-plus/steps": "^0.0.0",
|
||||
"@element-plus/notification": "^0.0.0",
|
||||
"@element-plus/collapse": "^0.0.0"
|
||||
"@element-plus/collapse": "^0.0.0",
|
||||
"@element-plus/tabs": "^0.0.0"
|
||||
}
|
||||
}
|
||||
|
591
packages/tabs/__tests__/tabs.spec.ts
Normal file
591
packages/tabs/__tests__/tabs.spec.ts
Normal file
@ -0,0 +1,591 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { nextTick } from 'vue'
|
||||
import Tabs from '../src/tabs.vue'
|
||||
import TabPane from '../src/tab-pane.vue'
|
||||
import TabNav from '../src/tab-nav.vue'
|
||||
|
||||
describe('Tabs.vue', () => {
|
||||
test('create', async () => {
|
||||
const wrapper = mount({
|
||||
components: {
|
||||
'el-tabs': Tabs,
|
||||
'el-tab-pane': TabPane,
|
||||
},
|
||||
template: `
|
||||
<el-tabs>
|
||||
<el-tab-pane label="label-1">A</el-tab-pane>
|
||||
<el-tab-pane label="label-2">B</el-tab-pane>
|
||||
<el-tab-pane label="label-3" ref="pane-click">C</el-tab-pane>
|
||||
<el-tab-pane label="label-4">D</el-tab-pane>
|
||||
</el-tabs>
|
||||
`,
|
||||
})
|
||||
|
||||
const tabsWrapper = wrapper.findComponent(Tabs)
|
||||
const navWrapper = wrapper.findComponent(TabNav)
|
||||
const panesWrapper = wrapper.findAllComponents(TabPane)
|
||||
await nextTick()
|
||||
|
||||
const navItemsWrapper = navWrapper.findAll('.el-tabs__item')
|
||||
|
||||
expect(navItemsWrapper[0].classes('is-active')).toBe(true)
|
||||
expect(panesWrapper[0].classes('el-tab-pane')).toBe(true)
|
||||
expect(panesWrapper[0].attributes('id')).toBe('pane-0')
|
||||
expect(panesWrapper[0].attributes('aria-hidden')).toEqual('false')
|
||||
expect(tabsWrapper.vm.currentName).toEqual('0')
|
||||
|
||||
await navItemsWrapper[2].trigger('click')
|
||||
expect(navItemsWrapper[0].classes('is-active')).toBe(false)
|
||||
expect(panesWrapper[0].attributes('aria-hidden')).toEqual('true')
|
||||
expect(navItemsWrapper[2].classes('is-active')).toBe(true)
|
||||
expect(panesWrapper[2].attributes('aria-hidden')).toEqual('false')
|
||||
expect(tabsWrapper.vm.currentName).toEqual('2')
|
||||
})
|
||||
|
||||
test('active-name', async () => {
|
||||
const wrapper = mount({
|
||||
components: {
|
||||
'el-tabs': Tabs,
|
||||
'el-tab-pane': TabPane,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeName: 'b',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClick(tab) {
|
||||
this.activeName = tab.setupState.paneName
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<el-tabs :active-name="activeName" @tab-click="handleClick">
|
||||
<el-tab-pane name="a" label="label-1">A</el-tab-pane>
|
||||
<el-tab-pane name="b" label="label-2">B</el-tab-pane>
|
||||
<el-tab-pane name="c" label="label-3" ref="pane-click">C</el-tab-pane>
|
||||
<el-tab-pane name="d" label="label-4">D</el-tab-pane>
|
||||
</el-tabs>
|
||||
`,
|
||||
})
|
||||
|
||||
const tabsWrapper = wrapper.findComponent(Tabs)
|
||||
const navWrapper = wrapper.findComponent(TabNav)
|
||||
const panesWrapper = wrapper.findAllComponents(TabPane)
|
||||
await nextTick()
|
||||
|
||||
const navItemsWrapper = navWrapper.findAll('.el-tabs__item')
|
||||
expect(navItemsWrapper[1].classes('is-active')).toBe(true)
|
||||
expect(panesWrapper[1].classes('el-tab-pane')).toBe(true)
|
||||
expect(panesWrapper[1].attributes('id')).toBe('pane-b')
|
||||
expect(panesWrapper[1].attributes('aria-hidden')).toEqual('false')
|
||||
expect(tabsWrapper.vm.currentName).toEqual('b')
|
||||
|
||||
await navItemsWrapper[2].trigger('click')
|
||||
expect(navItemsWrapper[1].classes('is-active')).toBe(false)
|
||||
expect(panesWrapper[1].attributes('aria-hidden')).toEqual('true')
|
||||
expect(navItemsWrapper[2].classes('is-active')).toBe(true)
|
||||
expect(panesWrapper[2].attributes('aria-hidden')).toEqual('false')
|
||||
expect(tabsWrapper.vm.currentName).toEqual('c')
|
||||
})
|
||||
|
||||
test('card', async () => {
|
||||
const wrapper = mount({
|
||||
components: {
|
||||
'el-tabs': Tabs,
|
||||
'el-tab-pane': TabPane,
|
||||
},
|
||||
template: `
|
||||
<el-tabs type="card">
|
||||
<el-tab-pane label="label-1">A</el-tab-pane>
|
||||
<el-tab-pane label="label-2">B</el-tab-pane>
|
||||
<el-tab-pane label="label-3" ref="pane-click">C</el-tab-pane>
|
||||
<el-tab-pane label="label-4">D</el-tab-pane>
|
||||
</el-tabs>
|
||||
`,
|
||||
})
|
||||
|
||||
const tabsWrapper = wrapper.findComponent(Tabs)
|
||||
expect(tabsWrapper.classes('el-tabs--card')).toBe(true)
|
||||
})
|
||||
|
||||
test('border card', async () => {
|
||||
const wrapper = mount({
|
||||
components: {
|
||||
'el-tabs': Tabs,
|
||||
'el-tab-pane': TabPane,
|
||||
},
|
||||
template: `
|
||||
<el-tabs type="border-card">
|
||||
<el-tab-pane label="label-1">A</el-tab-pane>
|
||||
<el-tab-pane label="label-2">B</el-tab-pane>
|
||||
<el-tab-pane label="label-3" ref="pane-click">C</el-tab-pane>
|
||||
<el-tab-pane label="label-4">D</el-tab-pane>
|
||||
</el-tabs>
|
||||
`,
|
||||
})
|
||||
|
||||
const tabsWrapper = wrapper.findComponent(Tabs)
|
||||
expect(tabsWrapper.classes('el-tabs--border-card')).toBe(true)
|
||||
})
|
||||
|
||||
test('dynamic', async () => {
|
||||
const wrapper = mount({
|
||||
components: {
|
||||
'el-tabs': Tabs,
|
||||
'el-tab-pane': TabPane,
|
||||
},
|
||||
template: `
|
||||
<el-tabs type="card" ref="tabs">
|
||||
<el-tab-pane :label="tab.label" :name="tab.name" v-for="tab in tabs" :key="tab.name">Test Content</el-tab-pane>
|
||||
</el-tabs>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
tabs: [{
|
||||
label: 'tab1',
|
||||
name: 'tab1',
|
||||
}, {
|
||||
label: 'tab2',
|
||||
name: 'tab2',
|
||||
}, {
|
||||
label: 'tab3',
|
||||
name: 'tab3',
|
||||
}, {
|
||||
label: 'tab4',
|
||||
name: 'tab4',
|
||||
}],
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
let navWrapper = wrapper.findComponent(TabNav)
|
||||
let panesWrapper = wrapper.findAllComponents(TabPane)
|
||||
await nextTick()
|
||||
|
||||
let navItemsWrapper = navWrapper.findAll('.el-tabs__item')
|
||||
|
||||
expect(navItemsWrapper.length).toEqual(4)
|
||||
expect(panesWrapper.length).toEqual(4)
|
||||
|
||||
wrapper.vm.tabs.push({ label: 'tab5', name: 'tab5' })
|
||||
|
||||
await nextTick()
|
||||
navWrapper = wrapper.findComponent(TabNav)
|
||||
panesWrapper = wrapper.findAllComponents(TabPane)
|
||||
navItemsWrapper = navWrapper.findAll('.el-tabs__item')
|
||||
|
||||
expect(navItemsWrapper.length).toEqual(5)
|
||||
expect(panesWrapper.length).toEqual(5)
|
||||
})
|
||||
|
||||
test('editable', async () => {
|
||||
const wrapper = mount({
|
||||
components: {
|
||||
'el-tabs': Tabs,
|
||||
'el-tab-pane': TabPane,
|
||||
},
|
||||
template: `
|
||||
<el-tabs ref="tabs" v-model="editableTabsValue" type="card" editable @edit="handleTabsEdit">
|
||||
<el-tab-pane
|
||||
v-for="(item, index) in editableTabs"
|
||||
:key="item.name"
|
||||
:label="item.title"
|
||||
:name="item.name"
|
||||
>
|
||||
{{item.content}}
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
editableTabsValue: '2',
|
||||
editableTabs: [{
|
||||
title: 'Tab 1',
|
||||
name: '1',
|
||||
content: 'Tab 1 content',
|
||||
}, {
|
||||
title: 'Tab 2',
|
||||
name: '2',
|
||||
content: 'Tab 2 content',
|
||||
}, {
|
||||
title: 'Tab 3',
|
||||
name: '3',
|
||||
content: 'Tab 3 content',
|
||||
}],
|
||||
tabIndex: 3,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleTabsEdit(targetName, action) {
|
||||
if (action === 'add') {
|
||||
const newTabName = ++this.tabIndex + ''
|
||||
this.editableTabs.push({
|
||||
title: 'New Tab',
|
||||
name: newTabName,
|
||||
content: 'New Tab content',
|
||||
})
|
||||
this.editableTabsValue = newTabName
|
||||
}
|
||||
if (action === 'remove') {
|
||||
const tabs = this.editableTabs
|
||||
let activeName = this.editableTabsValue
|
||||
if (activeName === targetName) {
|
||||
tabs.forEach((tab, index) => {
|
||||
if (tab.name === targetName) {
|
||||
const nextTab = tabs[index + 1] || tabs[index - 1]
|
||||
if (nextTab) {
|
||||
activeName = nextTab.name
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
this.editableTabsValue = activeName
|
||||
this.editableTabs = tabs.filter(tab => tab.name !== targetName)
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const navWrapper = wrapper.findComponent(TabNav)
|
||||
let panesWrapper = wrapper.findAllComponents(TabPane)
|
||||
await nextTick()
|
||||
|
||||
let navItemsWrapper = navWrapper.findAll('.el-tabs__item')
|
||||
|
||||
expect(navItemsWrapper.length).toEqual(3)
|
||||
expect(panesWrapper.length).toEqual(3)
|
||||
expect(navItemsWrapper[1].classes('is-active')).toBe(true)
|
||||
|
||||
// remove one tab, check panes length
|
||||
await navItemsWrapper[1].find('.el-icon-close').trigger('click')
|
||||
|
||||
panesWrapper = wrapper.findAllComponents(TabPane)
|
||||
navItemsWrapper = navWrapper.findAll('.el-tabs__item')
|
||||
|
||||
expect(navItemsWrapper.length).toEqual(2)
|
||||
expect(panesWrapper.length).toEqual(2)
|
||||
|
||||
// add one tab, check panes length and current tab
|
||||
await navWrapper.find('.el-tabs__new-tab').trigger('click')
|
||||
|
||||
panesWrapper = wrapper.findAllComponents(TabPane)
|
||||
navItemsWrapper = navWrapper.findAll('.el-tabs__item')
|
||||
|
||||
expect(navItemsWrapper.length).toEqual(3)
|
||||
expect(panesWrapper.length).toEqual(3)
|
||||
expect(navItemsWrapper[2].classes('is-active')).toBe(true)
|
||||
})
|
||||
|
||||
test('addable & closable', async () => {
|
||||
const wrapper = mount({
|
||||
components: {
|
||||
'el-tabs': Tabs,
|
||||
'el-tab-pane': TabPane,
|
||||
},
|
||||
template: `
|
||||
<el-tabs
|
||||
ref="tabs"
|
||||
v-model="editableTabsValue"
|
||||
type="card"
|
||||
addable
|
||||
closable
|
||||
@tab-add="addTab"
|
||||
@tab-remove="removeTab"
|
||||
>
|
||||
<el-tab-pane
|
||||
v-for="(item, index) in editableTabs"
|
||||
:label="item.title"
|
||||
:key="item.name"
|
||||
:name="item.name"
|
||||
>
|
||||
{{item.content}}
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
editableTabsValue: '2',
|
||||
editableTabs: [{
|
||||
title: 'Tab 1',
|
||||
name: '1',
|
||||
content: 'Tab 1 content',
|
||||
}, {
|
||||
title: 'Tab 2',
|
||||
name: '2',
|
||||
content: 'Tab 2 content',
|
||||
}],
|
||||
tabIndex: 2,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addTab() {
|
||||
const newTabName = ++this.tabIndex + ''
|
||||
this.editableTabs.push({
|
||||
title: 'New Tab',
|
||||
name: newTabName,
|
||||
content: 'New Tab content',
|
||||
})
|
||||
this.editableTabsValue = newTabName
|
||||
},
|
||||
removeTab(targetName) {
|
||||
const tabs = this.editableTabs
|
||||
let activeName = this.editableTabsValue
|
||||
if (activeName === targetName) {
|
||||
tabs.forEach((tab, index) => {
|
||||
if (tab.name === targetName) {
|
||||
const nextTab = tabs[index + 1] || tabs[index - 1]
|
||||
if (nextTab) {
|
||||
activeName = nextTab.name
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
this.editableTabsValue = activeName
|
||||
this.editableTabs = tabs.filter(tab => tab.name !== targetName)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const navWrapper = wrapper.findComponent(TabNav)
|
||||
await nextTick()
|
||||
|
||||
await navWrapper.find('.el-tabs__new-tab').trigger('click')
|
||||
|
||||
let navItemsWrapper = navWrapper.findAll('.el-tabs__item')
|
||||
let panesWrapper = wrapper.findAllComponents(TabPane)
|
||||
expect(navItemsWrapper.length).toEqual(3)
|
||||
expect(panesWrapper.length).toEqual(3)
|
||||
expect(navItemsWrapper[2].classes('is-active')).toBe(true)
|
||||
|
||||
await navItemsWrapper[2].find('.el-icon-close').trigger('click')
|
||||
|
||||
panesWrapper = wrapper.findAllComponents(TabPane)
|
||||
navItemsWrapper = navWrapper.findAll('.el-tabs__item')
|
||||
|
||||
expect(navItemsWrapper.length).toEqual(2)
|
||||
expect(panesWrapper.length).toEqual(2)
|
||||
})
|
||||
|
||||
test('closable in tab-pane', async () => {
|
||||
const wrapper = mount({
|
||||
components: {
|
||||
'el-tabs': Tabs,
|
||||
'el-tab-pane': TabPane,
|
||||
},
|
||||
template: `
|
||||
<el-tabs type="card" ref="tabs">
|
||||
<el-tab-pane label="label-1" closable>A</el-tab-pane>
|
||||
<el-tab-pane label="label-2">B</el-tab-pane>
|
||||
<el-tab-pane label="label-3" closable>C</el-tab-pane>
|
||||
<el-tab-pane label="label-4">D</el-tab-pane>
|
||||
</el-tabs>
|
||||
`,
|
||||
})
|
||||
|
||||
const navWrapper = wrapper.findComponent(TabNav)
|
||||
await nextTick()
|
||||
|
||||
expect(navWrapper.findAll('.el-icon-close').length).toBe(2)
|
||||
})
|
||||
|
||||
test('disabled', async () => {
|
||||
const wrapper = mount({
|
||||
components: {
|
||||
'el-tabs': Tabs,
|
||||
'el-tab-pane': TabPane,
|
||||
},
|
||||
template: `
|
||||
<el-tabs type="card" ref="tabs">
|
||||
<el-tab-pane label="label-1">A</el-tab-pane>
|
||||
<el-tab-pane disabled label="label-2" ref="disabled">B</el-tab-pane>
|
||||
<el-tab-pane label="label-3">C</el-tab-pane>
|
||||
<el-tab-pane label="label-4">D</el-tab-pane>
|
||||
</el-tabs>
|
||||
`,
|
||||
})
|
||||
|
||||
const navWrapper = wrapper.findComponent(TabNav)
|
||||
await nextTick()
|
||||
const navItemsWrapper = navWrapper.findAll('.el-tabs__item')
|
||||
expect(navItemsWrapper[1].classes('is-active')).toBe(false)
|
||||
|
||||
await navItemsWrapper[1].trigger('click')
|
||||
expect(navItemsWrapper[1].classes('is-active')).toBe(false)
|
||||
})
|
||||
|
||||
test('tab-position', async () => {
|
||||
const wrapper = mount({
|
||||
components: {
|
||||
'el-tabs': Tabs,
|
||||
'el-tab-pane': TabPane,
|
||||
},
|
||||
template: `
|
||||
<el-tabs ref="tabs" tab-position="left">
|
||||
<el-tab-pane label="label-1">A</el-tab-pane>
|
||||
<el-tab-pane label="label-2">B</el-tab-pane>
|
||||
<el-tab-pane label="label-3" ref="pane-click">C</el-tab-pane>
|
||||
<el-tab-pane label="label-4">D</el-tab-pane>
|
||||
</el-tabs>
|
||||
`,
|
||||
})
|
||||
|
||||
const tabsWrapper = wrapper.findComponent(Tabs)
|
||||
await nextTick()
|
||||
|
||||
expect(tabsWrapper.classes('el-tabs--left')).toBe(true)
|
||||
expect(tabsWrapper.find('.el-tabs__header').classes('is-left')).toBe(true)
|
||||
expect(tabsWrapper.find('.el-tabs__nav-wrap').classes('is-left')).toBe(true)
|
||||
expect(tabsWrapper.find('.el-tabs__nav').classes('is-left')).toBe(true)
|
||||
expect(tabsWrapper.find('.el-tabs__active-bar').classes('is-left')).toBe(true)
|
||||
expect(tabsWrapper.find('.el-tabs__item').classes('is-left')).toBe(true)
|
||||
})
|
||||
|
||||
test('stretch', async () => {
|
||||
const wrapper = mount({
|
||||
components: {
|
||||
'el-tabs': Tabs,
|
||||
'el-tab-pane': TabPane,
|
||||
},
|
||||
template: `
|
||||
<el-tabs ref="tabs" stretch :tab-position="tabPosition">
|
||||
<el-tab-pane label="label-1">A</el-tab-pane>
|
||||
<el-tab-pane label="label-2">B</el-tab-pane>
|
||||
<el-tab-pane label="label-3">C</el-tab-pane>
|
||||
<el-tab-pane label="label-4">D</el-tab-pane>
|
||||
</el-tabs>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
tabPosition: 'bottom',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const tabsWrapper = wrapper.findComponent(Tabs)
|
||||
await nextTick()
|
||||
|
||||
expect(tabsWrapper.find('.el-tabs__nav').classes('is-stretch')).toBe(true)
|
||||
|
||||
wrapper.vm.tabPosition = 'left'
|
||||
await nextTick()
|
||||
|
||||
expect(tabsWrapper.find('.el-tabs__nav').classes('is-stretch')).toBe(false)
|
||||
})
|
||||
|
||||
test('horizonal-scrollable', async () => {
|
||||
// TODO: jsdom not support `clientWidth`.
|
||||
})
|
||||
|
||||
test('vertical-scrollable', async () => {
|
||||
// TODO: jsdom not support `clientWidth`.
|
||||
})
|
||||
|
||||
test('should work with lazy', async () => {
|
||||
const wrapper = mount({
|
||||
components: {
|
||||
'el-tabs': Tabs,
|
||||
'el-tab-pane': TabPane,
|
||||
},
|
||||
template: `
|
||||
<el-tabs ref="tabs">
|
||||
<el-tab-pane label="label-1" name="A">A</el-tab-pane>
|
||||
<el-tab-pane label="label-2" name="B">B</el-tab-pane>
|
||||
<el-tab-pane label="label-3" name="C">C</el-tab-pane>
|
||||
<el-tab-pane label="label-4" lazy name="D">D</el-tab-pane>
|
||||
</el-tabs>
|
||||
`,
|
||||
})
|
||||
|
||||
const navWrapper = wrapper.findComponent(TabNav)
|
||||
await nextTick()
|
||||
const navItemsWrapper = navWrapper.findAll('.el-tabs__item')
|
||||
|
||||
expect(wrapper.findAll('.el-tab-pane').length).toBe(3)
|
||||
|
||||
await navItemsWrapper[3].trigger('click')
|
||||
|
||||
expect(wrapper.findAll('.el-tab-pane').length).toBe(4)
|
||||
})
|
||||
|
||||
test('before leave', async () => {
|
||||
const wrapper = mount({
|
||||
components: {
|
||||
'el-tabs': Tabs,
|
||||
'el-tab-pane': TabPane,
|
||||
},
|
||||
template: `
|
||||
<el-tabs ref="tabs" v-model="activeName" :before-leave="beforeLeave">
|
||||
<el-tab-pane name="tab-A" label="label-1">A</el-tab-pane>
|
||||
<el-tab-pane name="tab-B" label="label-2">B</el-tab-pane>
|
||||
<el-tab-pane name="tab-C" label="label-3">C</el-tab-pane>
|
||||
<el-tab-pane name="tab-D" label="label-4">D</el-tab-pane>
|
||||
</el-tabs>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
activeName: 'tab-B',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
beforeLeave() {
|
||||
return new window.Promise((resolve, reject) => {
|
||||
reject()
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const navWrapper = wrapper.findComponent(TabNav)
|
||||
const panesWrapper = wrapper.findAllComponents(TabPane)
|
||||
await nextTick()
|
||||
const navItemsWrapper = navWrapper.findAll('.el-tabs__item')
|
||||
|
||||
expect(navItemsWrapper[1].classes('is-active')).toBe(true)
|
||||
expect(panesWrapper[1].attributes('style')).toBeFalsy()
|
||||
|
||||
await navItemsWrapper[3].trigger('click')
|
||||
|
||||
expect(navItemsWrapper[1].classes('is-active')).toBe(true)
|
||||
expect(panesWrapper[1].attributes('style')).toBeFalsy()
|
||||
})
|
||||
|
||||
test('keyboard event', async () => {
|
||||
const wrapper = mount({
|
||||
components: {
|
||||
'el-tabs': Tabs,
|
||||
'el-tab-pane': TabPane,
|
||||
},
|
||||
template: `
|
||||
<el-tabs v-model="activeName">
|
||||
<el-tab-pane label="label-1" name="first">A</el-tab-pane>
|
||||
<el-tab-pane label="label-2" name="second">B</el-tab-pane>
|
||||
<el-tab-pane label="label-3" name="third">C</el-tab-pane>
|
||||
<el-tab-pane label="label-4" name="fourth">D</el-tab-pane>
|
||||
</el-tabs>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
activeName: 'second',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const vm = wrapper.vm
|
||||
await nextTick()
|
||||
|
||||
await wrapper.find('#tab-second').trigger('keydown', { keyCode: 39 })
|
||||
expect(vm.activeName).toEqual('third')
|
||||
|
||||
await wrapper.find('#tab-third').trigger('keydown', { keyCode: 39 })
|
||||
expect(vm.activeName).toEqual('fourth')
|
||||
|
||||
await wrapper.find('#tab-fourth').trigger('keydown', { keyCode: 39 })
|
||||
expect(vm.activeName).toEqual('first')
|
||||
|
||||
await wrapper.find('#tab-first').trigger('keydown', { keyCode: 37 })
|
||||
expect(vm.activeName).toEqual('fourth')
|
||||
|
||||
await wrapper.find('#tab-fourth').trigger('keydown', { keyCode: 37 })
|
||||
expect(vm.activeName).toEqual('third')
|
||||
})
|
||||
})
|
265
packages/tabs/doc/basic.vue
Normal file
265
packages/tabs/doc/basic.vue
Normal file
@ -0,0 +1,265 @@
|
||||
<template>
|
||||
<el-button @click="show = !show">
|
||||
display/hidden tab item
|
||||
</el-button>
|
||||
<el-button
|
||||
@click="activeName = activeName === 'first' ? 'second' : 'first'"
|
||||
>
|
||||
Change
|
||||
</el-button>
|
||||
|
||||
<div class="flag">
|
||||
<el-tabs v-model="activeName" @tab-click="handleClick">
|
||||
<el-tab-pane>
|
||||
<template #label>label-1slot-title</template>
|
||||
label-1slot-title
|
||||
</el-tab-pane>
|
||||
<template v-if="true">
|
||||
<el-tab-pane
|
||||
label="label-1"
|
||||
name="first"
|
||||
>
|
||||
label-1
|
||||
</el-tab-pane>
|
||||
</template>
|
||||
<el-tab-pane
|
||||
v-if="show"
|
||||
label="label-2"
|
||||
name="fourth"
|
||||
>
|
||||
label-2
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="label-3" name="second">label-3</el-tab-pane>
|
||||
<el-tab-pane label="label-4" name="third">label-4</el-tab-pane>
|
||||
<el-tab-pane v-for="i in 3" :key="i" :label="`a-${i}`">
|
||||
{{
|
||||
`name-${i}`
|
||||
}}
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<div class="flag">
|
||||
<el-tabs v-model="activeName" type="card" @tab-click="handleClick">
|
||||
<el-tab-pane
|
||||
closable
|
||||
label="label-1"
|
||||
name="first"
|
||||
>
|
||||
label-1
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
disabled
|
||||
label="label-2"
|
||||
name="fourth"
|
||||
>
|
||||
label-2
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="label-3" name="second">label-3</el-tab-pane>
|
||||
<el-tab-pane label="label-4" name="third">label-4</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<div class="flag">
|
||||
<el-tabs
|
||||
v-model="activeName"
|
||||
type="border-card"
|
||||
@tab-click="handleClick"
|
||||
>
|
||||
<el-tab-pane
|
||||
lazy
|
||||
label="label-1"
|
||||
name="first"
|
||||
>
|
||||
label-1
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
lazy
|
||||
label="label-2"
|
||||
name="fourth"
|
||||
>
|
||||
label-2
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
lazy
|
||||
label="label-3"
|
||||
name="second"
|
||||
>
|
||||
label-3
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
lazy
|
||||
label="label-4"
|
||||
name="third"
|
||||
>
|
||||
label-4
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<div class="flag">
|
||||
<el-button @click="tabPosition = 'top'">top</el-button>
|
||||
<el-button @click="tabPosition = 'right'">right</el-button>
|
||||
<el-button @click="tabPosition = 'bottom'">bottom</el-button>
|
||||
<el-button @click="tabPosition = 'left'">left</el-button>
|
||||
|
||||
<el-tabs :tab-position="tabPosition" style="height: 200px;">
|
||||
<el-tab-pane label="label-1">label-1</el-tab-pane>
|
||||
<el-tab-pane label="label-3">label-3</el-tab-pane>
|
||||
<el-tab-pane label="label-4">label-4</el-tab-pane>
|
||||
<el-tab-pane label="label-2">label-2</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<div class="flag">
|
||||
<el-tabs type="border-card">
|
||||
<el-tab-pane>
|
||||
<template #label>
|
||||
<i class="el-icon-date"></i> label-1
|
||||
</template>
|
||||
label-1
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="label-6">label-6</el-tab-pane>
|
||||
<el-tab-pane label="label-4">label-4</el-tab-pane>
|
||||
<el-tab-pane label="label-2">label-2</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<div class="flag">
|
||||
<el-tabs
|
||||
v-model="editableTabsValue"
|
||||
type="card"
|
||||
editable
|
||||
style="width: 450px;"
|
||||
@edit="handleTabsEdit"
|
||||
>
|
||||
<el-tab-pane
|
||||
v-for="item in editableTabs"
|
||||
:key="item.name"
|
||||
:label="item.title"
|
||||
:name="item.name"
|
||||
>
|
||||
{{ item.content }}
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<div class="flag">
|
||||
<div style="margin-bottom: 20px;">
|
||||
<el-button size="small" @click="addTab(editableTabsValue)">
|
||||
add tab
|
||||
</el-button>
|
||||
</div>
|
||||
<el-tabs
|
||||
v-model="editableTabsValue"
|
||||
type="card"
|
||||
stretch
|
||||
addable
|
||||
closable
|
||||
@tab-remove="removeTab"
|
||||
@tab-add="addTab(editableTabsValue)"
|
||||
>
|
||||
<el-tab-pane
|
||||
v-for="item in editableTabs"
|
||||
:key="item.name"
|
||||
:label="item.title"
|
||||
:name="item.name"
|
||||
>
|
||||
{{ item.content }}
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
<script lang='ts'>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
activeName: 'second',
|
||||
tabPosition: 'left',
|
||||
|
||||
editableTabsValue: '2',
|
||||
editableTabs: [
|
||||
{
|
||||
title: 'Tab 1',
|
||||
name: '1',
|
||||
content: 'Tab 1 content',
|
||||
},
|
||||
{
|
||||
title: 'Tab 2',
|
||||
name: '2',
|
||||
content: 'Tab 2 content',
|
||||
},
|
||||
],
|
||||
tabIndex: 2,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addTab() {
|
||||
let newTabName = ++this.tabIndex + ''
|
||||
this.editableTabs.push({
|
||||
title: 'New Tab',
|
||||
name: newTabName,
|
||||
content: 'New Tab content',
|
||||
})
|
||||
this.editableTabsValue = newTabName
|
||||
},
|
||||
removeTab(targetName) {
|
||||
let tabs = this.editableTabs
|
||||
let activeName = this.editableTabsValue
|
||||
if (activeName === targetName) {
|
||||
tabs.forEach((tab, index) => {
|
||||
if (tab.name === targetName) {
|
||||
let nextTab = tabs[index + 1] || tabs[index - 1]
|
||||
if (nextTab) {
|
||||
activeName = nextTab.name
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
this.editableTabsValue = activeName
|
||||
this.editableTabs = tabs.filter((tab) => tab.name !== targetName)
|
||||
},
|
||||
handleTabsEdit(targetName, action) {
|
||||
if (action === 'add') {
|
||||
let newTabName = ++this.tabIndex + ''
|
||||
this.editableTabs.push({
|
||||
title: 'New Tab',
|
||||
name: newTabName,
|
||||
content: 'New Tab content',
|
||||
})
|
||||
this.editableTabsValue = newTabName
|
||||
}
|
||||
if (action === 'remove') {
|
||||
let tabs = this.editableTabs
|
||||
let activeName = this.editableTabsValue
|
||||
if (activeName === targetName) {
|
||||
tabs.forEach((tab, index) => {
|
||||
if (tab.name === targetName) {
|
||||
let nextTab = tabs[index + 1] || tabs[index - 1]
|
||||
if (nextTab) {
|
||||
activeName = nextTab.name
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
this.editableTabsValue = activeName
|
||||
this.editableTabs = tabs.filter(
|
||||
(tab) => tab.name !== targetName,
|
||||
)
|
||||
}
|
||||
},
|
||||
handleClick(tab, event) {
|
||||
console.log(tab, event)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.flag {
|
||||
border: 2px solid #eee;
|
||||
margin: 15px 0;
|
||||
padding: 10px;
|
||||
min-height: 250px;
|
||||
}
|
||||
</style>
|
6
packages/tabs/doc/index.stories.ts
Normal file
6
packages/tabs/doc/index.stories.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export { default as BasicUsage } from './basic.vue'
|
||||
|
||||
export default {
|
||||
title: 'Tabs',
|
||||
}
|
||||
|
11
packages/tabs/index.ts
Normal file
11
packages/tabs/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { App } from 'vue'
|
||||
import Tabs from './src/tabs.vue'
|
||||
import TabBar from './src/tab-bar.vue'
|
||||
import TabNav from './src/tab-nav.vue'
|
||||
import TabPane from './src/tab-pane.vue'
|
||||
export default (app: App): void => {
|
||||
app.component(Tabs.name, Tabs)
|
||||
app.component(TabBar.name, TabBar)
|
||||
app.component(TabNav.name, TabNav)
|
||||
app.component(TabPane.name, TabPane)
|
||||
}
|
12
packages/tabs/package.json
Normal file
12
packages/tabs/package.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@element-plus/tabs",
|
||||
"version": "0.0.0",
|
||||
"main": "dist/index.js",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0-rc.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/test-utils": "^2.0.0-beta.0"
|
||||
}
|
||||
}
|
78
packages/tabs/src/tab-bar.vue
Normal file
78
packages/tabs/src/tab-bar.vue
Normal file
@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div
|
||||
:class="['el-tabs__active-bar', `is-${ rootTabs.props.tabPosition }`]"
|
||||
:style="barStyle"
|
||||
></div>
|
||||
</template>
|
||||
<script lang='ts'>
|
||||
import { defineComponent, inject, getCurrentInstance, watch, nextTick, ref, ComponentInternalInstance } from 'vue'
|
||||
import { capitalize } from '@vue/shared'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElTabBar',
|
||||
props: {
|
||||
tabs: {
|
||||
type: Array as PropType<ComponentInternalInstance[]>,
|
||||
default: () => ([] as ComponentInternalInstance[]),
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const rootTabs = inject('rootTabs')
|
||||
if (!rootTabs) {
|
||||
throw new Error(`ElTabBar must use with ElTabs`)
|
||||
}
|
||||
const instance = getCurrentInstance()
|
||||
|
||||
const getBarStyle = () => {
|
||||
let style: CSSStyleDeclaration = {} as CSSStyleDeclaration
|
||||
let offset = 0
|
||||
let tabSize = 0
|
||||
|
||||
const sizeName = ['top', 'bottom'].includes(rootTabs.props.tabPosition) ? 'width' : 'height'
|
||||
const sizeDir = sizeName === 'width' ? 'x' : 'y'
|
||||
|
||||
props.tabs.every((tab) => {
|
||||
let $el = instance.parent.refs?.[`tab-${tab.setupState.paneName}`]
|
||||
if (!$el) { return false }
|
||||
if (!tab.setupState.active) {
|
||||
offset += $el[`client${capitalize(sizeName)}`]
|
||||
return true
|
||||
} else {
|
||||
tabSize = $el[`client${capitalize(sizeName)}`]
|
||||
|
||||
const tabStyles = window.getComputedStyle($el)
|
||||
|
||||
if (sizeName === 'width') {
|
||||
if (props.tabs.length > 1) {
|
||||
tabSize -= parseFloat(tabStyles.paddingLeft) + parseFloat(tabStyles.paddingRight)
|
||||
}
|
||||
offset += parseFloat(tabStyles.paddingLeft)
|
||||
}
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
const transform = `translate${capitalize(sizeDir)}(${offset}px)`
|
||||
style[sizeName] = `${tabSize}px`
|
||||
style.transform = transform
|
||||
style.msTransform = transform
|
||||
style.webkitTransform = transform
|
||||
|
||||
return style
|
||||
}
|
||||
|
||||
const barStyle = ref(getBarStyle())
|
||||
|
||||
watch(() => props.tabs, () => {
|
||||
nextTick(() => {
|
||||
barStyle.value = getBarStyle()
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
rootTabs,
|
||||
barStyle,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
380
packages/tabs/src/tab-nav.vue
Normal file
380
packages/tabs/src/tab-nav.vue
Normal file
@ -0,0 +1,380 @@
|
||||
<script lang='ts'>
|
||||
import { h, defineComponent, ref, inject, computed, onUpdated, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { addResizeListener, removeResizeListener } from '@element-plus/utils/resize-event'
|
||||
import TabBar from './tab-bar.vue'
|
||||
import { NOOP, capitalize } from '@vue/shared'
|
||||
|
||||
type RefElement = Nullable<HTMLElement>
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElTabNav',
|
||||
components: {
|
||||
TabBar,
|
||||
},
|
||||
props: {
|
||||
panes: {
|
||||
type: Array as PropType<ComponentInternalInstance[]>,
|
||||
default: () => ([] as ComponentInternalInstance[]),
|
||||
},
|
||||
currentName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
editable: Boolean,
|
||||
onTabClick: {
|
||||
type: Function,
|
||||
default: NOOP,
|
||||
},
|
||||
onTabRemove: {
|
||||
type: Function,
|
||||
default: NOOP,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
stretch: Boolean,
|
||||
},
|
||||
setup() {
|
||||
const rootTabs = inject('rootTabs')
|
||||
if (!rootTabs) {
|
||||
throw new Error(`ElTabNav must use with ElTabs`)
|
||||
}
|
||||
|
||||
const scrollable = ref(false)
|
||||
const navOffset = ref(0)
|
||||
const isFocus = ref(false)
|
||||
const focusable = ref(true)
|
||||
|
||||
const navScroll$ = ref<RefElement>(null)
|
||||
const nav$ = ref<RefElement>(null)
|
||||
const el$ = ref<RefElement>(null)
|
||||
|
||||
const sizeName = computed(() => {
|
||||
return ['top', 'bottom'].includes(rootTabs.props.tabPosition) ? 'width' : 'height'
|
||||
})
|
||||
const navStyle = computed(() => {
|
||||
const dir = sizeName.value === 'width' ? 'X' : 'Y'
|
||||
return {
|
||||
transform: `translate${dir}(-${navOffset.value}px)`,
|
||||
}
|
||||
})
|
||||
|
||||
const scrollPrev = () => {
|
||||
const containerSize = navScroll$.value[`offset${capitalize(sizeName.value)}`]
|
||||
const currentOffset = navOffset.value
|
||||
|
||||
if (!currentOffset) return
|
||||
|
||||
let newOffset = currentOffset > containerSize
|
||||
? currentOffset - containerSize
|
||||
: 0
|
||||
|
||||
navOffset.value = newOffset
|
||||
}
|
||||
|
||||
const scrollNext = () => {
|
||||
const navSize = nav$.value[`offset${capitalize(sizeName.value)}`]
|
||||
const containerSize = navScroll$.value[`offset${capitalize(sizeName.value)}`]
|
||||
const currentOffset = navOffset.value
|
||||
|
||||
if (navSize - currentOffset <= containerSize) return
|
||||
|
||||
let newOffset = navSize - currentOffset > containerSize * 2
|
||||
? currentOffset + containerSize
|
||||
: (navSize - containerSize)
|
||||
|
||||
navOffset.value = newOffset
|
||||
}
|
||||
|
||||
const scrollToActiveTab = () => {
|
||||
if (!scrollable.value) return
|
||||
const nav = nav$.value
|
||||
const activeTab = el$.value.querySelector('.is-active')
|
||||
if (!activeTab) return
|
||||
const navScroll = navScroll$.value
|
||||
const isHorizontal = ['top', 'bottom'].includes(rootTabs.props.tabPosition)
|
||||
const activeTabBounding = activeTab.getBoundingClientRect()
|
||||
const navScrollBounding = navScroll.getBoundingClientRect()
|
||||
const maxOffset = isHorizontal
|
||||
? nav.offsetWidth - navScrollBounding.width
|
||||
: nav.offsetHeight - navScrollBounding.height
|
||||
const currentOffset = navOffset.value
|
||||
let newOffset = currentOffset
|
||||
|
||||
if (isHorizontal) {
|
||||
if (activeTabBounding.left < navScrollBounding.left) {
|
||||
newOffset = currentOffset - (navScrollBounding.left - activeTabBounding.left)
|
||||
}
|
||||
if (activeTabBounding.right > navScrollBounding.right) {
|
||||
newOffset = currentOffset + activeTabBounding.right - navScrollBounding.right
|
||||
}
|
||||
} else {
|
||||
if (activeTabBounding.top < navScrollBounding.top) {
|
||||
newOffset = currentOffset - (navScrollBounding.top - activeTabBounding.top)
|
||||
}
|
||||
if (activeTabBounding.bottom > navScrollBounding.bottom) {
|
||||
newOffset = currentOffset + (activeTabBounding.bottom - navScrollBounding.bottom)
|
||||
}
|
||||
}
|
||||
newOffset = Math.max(newOffset, 0)
|
||||
navOffset.value = Math.min(newOffset, maxOffset)
|
||||
}
|
||||
|
||||
const update = () => {
|
||||
if (!nav$.value) return
|
||||
const navSize = nav$.value[`offset${capitalize(sizeName.value)}`]
|
||||
const containerSize = navScroll$.value[`offset${capitalize(sizeName.value)}`]
|
||||
const currentOffset = navOffset.value
|
||||
|
||||
if (containerSize < navSize) {
|
||||
const currentOffset = navOffset.value
|
||||
scrollable.value = scrollable.value || {}
|
||||
scrollable.value.prev = currentOffset
|
||||
scrollable.value.next = currentOffset + containerSize < navSize
|
||||
if (navSize - currentOffset < containerSize) {
|
||||
navOffset.value = navSize - containerSize
|
||||
}
|
||||
} else {
|
||||
scrollable.value = false
|
||||
if (currentOffset > 0) {
|
||||
navOffset.value = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const changeTab = (e) => {
|
||||
const keyCode = e.keyCode
|
||||
let nextIndex
|
||||
let currentIndex, tabList
|
||||
if ([37, 38, 39, 40].indexOf(keyCode) !== -1) { // 左右上下键更换tab
|
||||
tabList = e.currentTarget.querySelectorAll('[role=tab]')
|
||||
currentIndex = Array.prototype.indexOf.call(tabList, e.target)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
if (keyCode === 37 || keyCode === 38) { // left
|
||||
if (currentIndex === 0) { // first
|
||||
nextIndex = tabList.length - 1
|
||||
} else {
|
||||
nextIndex = currentIndex - 1
|
||||
}
|
||||
} else { // right
|
||||
if (currentIndex < tabList.length - 1) { // not last
|
||||
nextIndex = currentIndex + 1
|
||||
} else {
|
||||
nextIndex = 0
|
||||
}
|
||||
}
|
||||
tabList[nextIndex].focus() // 改变焦点元素
|
||||
tabList[nextIndex].click() // 选中下一个tab
|
||||
setFocus()
|
||||
}
|
||||
|
||||
const setFocus = () => {
|
||||
if (focusable.value) {
|
||||
isFocus.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const removeFocus = () => {
|
||||
isFocus.value = false
|
||||
}
|
||||
|
||||
const visibilityChangeHandler = () => {
|
||||
const visibility = document.visibilityState
|
||||
if (visibility === 'hidden') {
|
||||
focusable.value = false
|
||||
} else if (visibility === 'visible') {
|
||||
setTimeout(() => {
|
||||
focusable.value = true
|
||||
}, 50)
|
||||
}
|
||||
}
|
||||
|
||||
const windowBlurHandler = () => {
|
||||
focusable.value = false
|
||||
}
|
||||
|
||||
const windowFocusHandler = () => {
|
||||
setTimeout(() => {
|
||||
focusable.value = true
|
||||
}, 50)
|
||||
}
|
||||
|
||||
onUpdated(() => {
|
||||
update()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
addResizeListener(el$.value, update)
|
||||
document.addEventListener('visibilitychange', visibilityChangeHandler)
|
||||
window.addEventListener('blur', windowBlurHandler)
|
||||
window.addEventListener('focus', windowFocusHandler)
|
||||
setTimeout(() => {
|
||||
scrollToActiveTab()
|
||||
}, 0)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if(el$.value) {
|
||||
removeResizeListener(el$.value, update)
|
||||
}
|
||||
document.removeEventListener('visibilitychange', visibilityChangeHandler)
|
||||
window.removeEventListener('blur', windowBlurHandler)
|
||||
window.removeEventListener('focus', windowFocusHandler)
|
||||
})
|
||||
|
||||
return {
|
||||
rootTabs,
|
||||
|
||||
scrollable,
|
||||
navOffset,
|
||||
isFocus,
|
||||
focusable,
|
||||
|
||||
navScroll$,
|
||||
nav$,
|
||||
el$,
|
||||
|
||||
sizeName,
|
||||
navStyle,
|
||||
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
scrollToActiveTab,
|
||||
update,
|
||||
changeTab,
|
||||
setFocus,
|
||||
removeFocus,
|
||||
visibilityChangeHandler,
|
||||
windowBlurHandler,
|
||||
windowFocusHandler,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
const {
|
||||
type,
|
||||
panes,
|
||||
editable,
|
||||
stretch,
|
||||
onTabClick,
|
||||
onTabRemove,
|
||||
navStyle,
|
||||
scrollable,
|
||||
scrollNext,
|
||||
scrollPrev,
|
||||
changeTab,
|
||||
setFocus,
|
||||
removeFocus,
|
||||
rootTabs,
|
||||
isFocus,
|
||||
} = this
|
||||
|
||||
const scrollBtn = scrollable ? [
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
class: ['el-tabs__nav-prev', scrollable.prev ? '' : 'is-disabled'],
|
||||
onClick: scrollPrev,
|
||||
},
|
||||
[h('i', { class: 'el-icon-arrow-left' })],
|
||||
),
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
class: ['el-tabs__nav-next', scrollable.next ? '' : 'is-disabled'],
|
||||
onClick: scrollNext,
|
||||
},
|
||||
[h('i', { class: 'el-icon-arrow-right' })],
|
||||
),
|
||||
] : null
|
||||
|
||||
const tabs = panes.map((pane, index) => {
|
||||
let tabName = pane.props.name || pane.setupState.index || `${index}`
|
||||
const closable = pane.setupState.isClosable || editable
|
||||
|
||||
pane.setupState.index = `${index}`
|
||||
|
||||
const btnClose = closable ?
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
class: 'el-icon-close',
|
||||
onClick: (ev) => { onTabRemove(pane, ev) },
|
||||
},
|
||||
) : null
|
||||
|
||||
const tabLabelContent = pane.slots.label?.() || pane.props.label
|
||||
const tabindex = pane.setupState.active ? 0 : -1
|
||||
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
class: {
|
||||
'el-tabs__item': true,
|
||||
[`is-${ rootTabs.props.tabPosition }`]: true,
|
||||
'is-active': pane.setupState.active,
|
||||
'is-disabled': pane.props.disabled,
|
||||
'is-closable': closable,
|
||||
'is-focus': isFocus,
|
||||
},
|
||||
id: `tab-${tabName}`,
|
||||
key: `tab-${tabName}`,
|
||||
'aria-controls': `pane-${tabName}`,
|
||||
role: 'tab',
|
||||
'aria-selected': pane.setupState.active ,
|
||||
ref: `tab-${tabName}`,
|
||||
tabindex: tabindex,
|
||||
onFocus: () => { setFocus() },
|
||||
onBlur: () => { removeFocus() },
|
||||
onClick: (ev) => { removeFocus(); onTabClick(pane, tabName, ev) },
|
||||
onKeydown: (ev) => { if (closable && (ev.keyCode === 46 || ev.keyCode === 8)) { onTabRemove(pane, ev)} },
|
||||
},
|
||||
[tabLabelContent, btnClose],
|
||||
)
|
||||
})
|
||||
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
ref: 'el$',
|
||||
class: ['el-tabs__nav-wrap', scrollable ? 'is-scrollable' : '', `is-${ rootTabs.props.tabPosition }`],
|
||||
},
|
||||
[
|
||||
scrollBtn,
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: 'el-tabs__nav-scroll',
|
||||
ref: 'navScroll$',
|
||||
},
|
||||
[
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: ['el-tabs__nav', `is-${ rootTabs.props.tabPosition }`, stretch && ['top', 'bottom'].includes(rootTabs.props.tabPosition) ? 'is-stretch' : ''],
|
||||
ref: 'nav$',
|
||||
style: navStyle,
|
||||
role: 'tablist',
|
||||
onKeydown: changeTab,
|
||||
},
|
||||
[
|
||||
!type ? h(
|
||||
TabBar,
|
||||
{
|
||||
tabs: panes,
|
||||
},
|
||||
) : null,
|
||||
tabs,
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
</style>
|
74
packages/tabs/src/tab-pane.vue
Normal file
74
packages/tabs/src/tab-pane.vue
Normal file
@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="shouldBeRender"
|
||||
v-show="active"
|
||||
:id="`pane-${paneName}`"
|
||||
class="el-tab-pane"
|
||||
role="tabpanel"
|
||||
:aria-hidden="!active"
|
||||
:aria-labelledby="`tab-${paneName}`"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<script lang='ts'>
|
||||
import { defineComponent, ref, computed, inject } from 'vue'
|
||||
export default defineComponent({
|
||||
name: 'ElTabPane',
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
labelContent: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
closable: Boolean,
|
||||
disabled: Boolean,
|
||||
lazy: Boolean,
|
||||
},
|
||||
setup(props) {
|
||||
const index = ref(null)
|
||||
const loaded = ref(false)
|
||||
const rootTabs = inject('rootTabs')
|
||||
|
||||
if (!rootTabs) {
|
||||
throw new Error(`ElTabPane must use with ElTabs`)
|
||||
}
|
||||
|
||||
const isClosable = computed(() => {
|
||||
return props.closable || rootTabs.props.closable
|
||||
})
|
||||
|
||||
const active = computed(() => {
|
||||
const active = rootTabs.setupState.currentName === (props.name || index.value)
|
||||
if (active) {
|
||||
loaded.value = true
|
||||
}
|
||||
return active
|
||||
})
|
||||
|
||||
const paneName = computed(() => {
|
||||
return props.name || index.value
|
||||
})
|
||||
|
||||
const shouldBeRender = computed(() => {
|
||||
return (!props.lazy || loaded.value) || active.value
|
||||
})
|
||||
|
||||
return {
|
||||
index,
|
||||
loaded,
|
||||
isClosable,
|
||||
active,
|
||||
paneName,
|
||||
shouldBeRender,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
232
packages/tabs/src/tabs.vue
Normal file
232
packages/tabs/src/tabs.vue
Normal file
@ -0,0 +1,232 @@
|
||||
<script lang='ts'>
|
||||
import { h, defineComponent, ref, onMounted, onUpdated, provide, watch, nextTick, getCurrentInstance, ComponentInternalInstance } from 'vue'
|
||||
import { Fragment } from '@vue/runtime-core'
|
||||
import TabNav from './tab-nav.vue'
|
||||
|
||||
type RefElement = Nullable<HTMLElement>
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElTabs',
|
||||
components: { TabNav },
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
activeName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
closable: Boolean,
|
||||
addable: Boolean,
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
editable: Boolean,
|
||||
tabPosition: {
|
||||
type: String,
|
||||
default: 'top',
|
||||
},
|
||||
beforeLeave: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
stretch: Boolean,
|
||||
},
|
||||
emits: ['tab-click', 'edit', 'tab-remove', 'tab-add', 'input', 'update:modelValue'],
|
||||
setup(props, ctx) {
|
||||
const nav$ = ref<RefElement>(null)
|
||||
const currentName = ref(props.modelValue || props.activeName || '0')
|
||||
const panes = ref<ComponentInternalInstance[]>([])
|
||||
const instance = getCurrentInstance()
|
||||
|
||||
watch(() => props.activeName, (modelValue) => {
|
||||
setCurrentName(modelValue)
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (modelValue) => {
|
||||
setCurrentName(modelValue)
|
||||
})
|
||||
|
||||
watch(currentName, () => {
|
||||
if (nav$.value) {
|
||||
nextTick(() => {
|
||||
nav$.value.$nextTick(() => {
|
||||
nav$.value.scrollToActiveTab()
|
||||
})
|
||||
})
|
||||
}
|
||||
setPaneInstances(true)
|
||||
})
|
||||
|
||||
const getPaneInstanceFromSlot = (vnode, paneInstanceList = []) => {
|
||||
[...(vnode.children || [])].forEach((node) => {
|
||||
let type = node.type
|
||||
type = type.name || type
|
||||
if (type === 'ElTabPane' && node.component) {
|
||||
paneInstanceList.push(node.component)
|
||||
} else if(type === Fragment || type === 'template') {
|
||||
getPaneInstanceFromSlot(node, paneInstanceList)
|
||||
}
|
||||
})
|
||||
return paneInstanceList
|
||||
}
|
||||
|
||||
const setPaneInstances = (isForceUpdate = false) => {
|
||||
if(ctx.slots.default) {
|
||||
const children = instance.subTree.children
|
||||
|
||||
const content = [...children].find(({ props }) => {
|
||||
return props.class === 'el-tabs__content'
|
||||
})
|
||||
|
||||
if(!content) return
|
||||
|
||||
const paneInstanceList = getPaneInstanceFromSlot(content)
|
||||
const panesChanged = !(paneInstanceList.length === panes.value.length && paneInstanceList.every((pane, index) => pane.uid === panes.value[index].uid))
|
||||
|
||||
if (isForceUpdate || panesChanged) {
|
||||
panes.value = paneInstanceList
|
||||
}
|
||||
} else if (panes.value.length !== 0) {
|
||||
panes.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const changeCurrentName = (value) => {
|
||||
currentName.value = value
|
||||
ctx.emit('input', value)
|
||||
ctx.emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
const setCurrentName = (value) => {
|
||||
if(currentName.value !== value && props.beforeLeave) {
|
||||
const before = props.beforeLeave(value, currentName.value)
|
||||
if(before && before.then) {
|
||||
before.then(() => {
|
||||
changeCurrentName(value)
|
||||
nav$.value && nav$.value.removeFocus()
|
||||
}, () => {
|
||||
// ignore promise rejection in `before-leave` hook
|
||||
})
|
||||
} else if(before !== false) {
|
||||
changeCurrentName(value)
|
||||
}
|
||||
} else {
|
||||
changeCurrentName(value)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTabClick = (tab, tabName, event) => {
|
||||
if(tab.props.disabled) return
|
||||
setCurrentName(tabName)
|
||||
ctx.emit('tab-click', tab, event)
|
||||
}
|
||||
|
||||
const handleTabRemove = (pane, ev) => {
|
||||
if(pane.props.disabled) return
|
||||
ev.stopPropagation()
|
||||
ctx.emit('edit', pane.props.name, 'remove')
|
||||
ctx.emit('tab-remove', pane.props.name)
|
||||
}
|
||||
|
||||
const handleTabAdd = () => {
|
||||
ctx.emit('edit', null, 'add')
|
||||
ctx.emit('tab-add')
|
||||
}
|
||||
|
||||
provide('rootTabs', getCurrentInstance())
|
||||
|
||||
onUpdated(() => {
|
||||
setPaneInstances()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
setPaneInstances()
|
||||
})
|
||||
|
||||
return {
|
||||
nav$,
|
||||
handleTabClick,
|
||||
handleTabRemove,
|
||||
handleTabAdd,
|
||||
currentName,
|
||||
panes,
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
render(){
|
||||
let {
|
||||
type,
|
||||
handleTabClick,
|
||||
handleTabRemove,
|
||||
handleTabAdd,
|
||||
currentName,
|
||||
panes,
|
||||
editable,
|
||||
addable,
|
||||
tabPosition,
|
||||
stretch,
|
||||
} = this
|
||||
|
||||
const newButton = editable || addable ? h(
|
||||
'span',
|
||||
{
|
||||
class: 'el-tabs__new-tab',
|
||||
tabindex: '0',
|
||||
onClick: handleTabAdd,
|
||||
onKeydown: (ev) => { if (ev.keyCode === 13) { handleTabAdd() }},
|
||||
},
|
||||
[h('i', { class: 'el-icon-plus' })],
|
||||
) : null
|
||||
|
||||
const header = h(
|
||||
'div',
|
||||
{
|
||||
class: ['el-tabs__header', `is-${tabPosition}`],
|
||||
},
|
||||
[
|
||||
newButton,
|
||||
h(
|
||||
TabNav,
|
||||
{
|
||||
currentName,
|
||||
editable,
|
||||
type,
|
||||
panes,
|
||||
stretch,
|
||||
ref: 'nav$',
|
||||
onTabClick: handleTabClick,
|
||||
onTabRemove: handleTabRemove,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
const panels = h(
|
||||
'div',
|
||||
{
|
||||
class: 'el-tabs__content',
|
||||
},
|
||||
this.$slots?.default(),
|
||||
)
|
||||
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
class: {
|
||||
'el-tabs': true,
|
||||
'el-tabs--card': type === 'card',
|
||||
[`el-tabs--${tabPosition}`]: true,
|
||||
'el-tabs--border-card': type === 'border-card',
|
||||
},
|
||||
},
|
||||
tabPosition !== 'bottom' ? [header, panels] : [panels, header],
|
||||
)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
</style>
|
Loading…
Reference in New Issue
Block a user