feat(tabs): Remove setupState

This commit is contained in:
bastarder 2020-08-23 10:35:36 +08:00 committed by jeremywu
parent 72daa4359d
commit 423feb1051
6 changed files with 117 additions and 47 deletions

View File

@ -55,7 +55,7 @@ describe('Tabs.vue', () => {
},
methods: {
handleClick(tab) {
this.activeName = tab.setupState.paneName
this.activeName = tab.paneName
},
},
template: `

View File

@ -5,19 +5,20 @@
></div>
</template>
<script lang='ts'>
import { defineComponent, inject, getCurrentInstance, watch, nextTick, ref, ComponentInternalInstance } from 'vue'
import { defineComponent, inject, getCurrentInstance, watch, nextTick, ref, PropType } from 'vue'
import { capitalize } from '@vue/shared'
import { Pane, RootTabs } from './tabs.vue'
export default defineComponent({
name: 'ElTabBar',
props: {
tabs: {
type: Array as PropType<ComponentInternalInstance[]>,
default: () => ([] as ComponentInternalInstance[]),
type: Array as PropType<Pane[]>,
default: () => ([] as Pane[]),
},
},
setup(props) {
const rootTabs = inject('rootTabs')
const rootTabs = inject<RootTabs>('rootTabs')
if (!rootTabs) {
throw new Error(`ElTabBar must use with ElTabs`)
}
@ -32,9 +33,9 @@ export default defineComponent({
const sizeDir = sizeName === 'width' ? 'x' : 'y'
props.tabs.every(tab => {
let $el = instance.parent.refs?.[`tab-${tab.setupState.paneName}`]
let $el = instance.parent.refs?.[`tab-${tab.paneName}`] as Element
if (!$el) { return false }
if (!tab.setupState.active) {
if (!tab.active) {
offset += $el[`client${capitalize(sizeName)}`]
return true
} else {

View File

@ -1,13 +1,19 @@
<script lang='ts'>
import { h, defineComponent, ref, inject, computed, onUpdated, onMounted, onBeforeUnmount } from 'vue'
import { addResizeListener, removeResizeListener } from '@element-plus/utils/resize-event'
import { h, defineComponent, ref, inject, computed, onUpdated, onMounted, onBeforeUnmount, PropType } from 'vue'
import { addResizeListener, removeResizeListener, ResizableElement } from '@element-plus/utils/resize-event'
import { eventKeys } from '@element-plus/utils/aria'
import { on, off } from '@element-plus/utils/dom'
import TabBar from './tab-bar.vue'
import { NOOP, capitalize } from '@vue/shared'
import { RootTabs, Pane } from './tabs.vue'
type RefElement = Nullable<HTMLElement>
interface Scrollable {
next?: boolean
prev?: number
}
export default defineComponent({
name: 'ElTabNav',
components: {
@ -15,8 +21,8 @@ export default defineComponent({
},
props: {
panes: {
type: Array as PropType<ComponentInternalInstance[]>,
default: () => ([] as ComponentInternalInstance[]),
type: Array as PropType<Pane[]>,
default: () => ([] as Pane[]),
},
currentName: {
type: String,
@ -24,11 +30,11 @@ export default defineComponent({
},
editable: Boolean,
onTabClick: {
type: Function as PropType<(tab: ComponentInternalInstance, tabName: string, ev: Event) => void>,
type: Function as PropType<(tab: Pane, tabName: string, ev: Event) => void>,
default: NOOP,
},
onTabRemove: {
type: Function as PropType<(tab: ComponentInternalInstance, ev: Event) => void>,
type: Function as PropType<(tab: Pane, ev: Event) => void>,
default: NOOP,
},
type: {
@ -38,12 +44,12 @@ export default defineComponent({
stretch: Boolean,
},
setup() {
const rootTabs = inject('rootTabs')
const rootTabs = inject<RootTabs>('rootTabs')
if (!rootTabs) {
throw new Error(`ElTabNav must use with ElTabs`)
}
const scrollable = ref(false)
const scrollable = ref<boolean | Scrollable>(false)
const navOffset = ref(0)
const isFocus = ref(false)
const focusable = ref(true)
@ -131,7 +137,7 @@ export default defineComponent({
if (containerSize < navSize) {
const currentOffset = navOffset.value
scrollable.value = scrollable.value || {}
scrollable.value = (scrollable.value || {}) as Scrollable
scrollable.value.prev = currentOffset
scrollable.value.next = currentOffset + containerSize < navSize
if (navSize - currentOffset < containerSize) {
@ -211,7 +217,7 @@ export default defineComponent({
})
onMounted(() => {
addResizeListener(el$.value, update)
addResizeListener(el$.value as ResizableElement, update)
on(document, 'visibilitychange', visibilityChangeHandler)
on(window, 'blur', windowBlurHandler)
on(window, 'focus', windowFocusHandler)
@ -222,7 +228,7 @@ export default defineComponent({
onBeforeUnmount(() => {
if(el$.value) {
removeResizeListener(el$.value, update)
removeResizeListener(el$.value as ResizableElement, update)
}
off(document, 'visibilitychange', visibilityChangeHandler)
off(window, 'blur', windowBlurHandler)
@ -295,10 +301,10 @@ export default defineComponent({
] : null
const tabs = panes.map((pane, index) => {
let tabName = pane.props.name || pane.setupState.index || `${index}`
const closable = pane.setupState.isClosable || editable
let tabName = pane.props.name || pane.index || `${index}`
const closable = pane.isClosable || editable
pane.setupState.index = `${index}`
pane.index = `${index}`
const btnClose = closable ?
h(
@ -309,8 +315,8 @@ export default defineComponent({
},
) : null
const tabLabelContent = pane.slots.label?.() || pane.props.label
const tabindex = pane.setupState.active ? 0 : -1
const tabLabelContent = pane.instance.slots.label?.() || pane.props.label
const tabindex = pane.active ? 0 : -1
return h(
'div',
@ -318,7 +324,7 @@ export default defineComponent({
class: {
'el-tabs__item': true,
[`is-${ rootTabs.props.tabPosition }`]: true,
'is-active': pane.setupState.active,
'is-active': pane.active,
'is-disabled': pane.props.disabled,
'is-closable': closable,
'is-focus': isFocus,
@ -327,7 +333,7 @@ export default defineComponent({
key: `tab-${tabName}`,
'aria-controls': `pane-${tabName}`,
role: 'tab',
'aria-selected': pane.setupState.active ,
'aria-selected': pane.active ,
ref: `tab-${tabName}`,
tabindex: tabindex,
onFocus: () => { setFocus() },

View File

@ -12,7 +12,9 @@
</div>
</template>
<script lang='ts'>
import { defineComponent, ref, computed, inject } from 'vue'
import { defineComponent, ref, computed, inject, getCurrentInstance } from 'vue'
import { RootTabs, UpdatePaneStateCallback, IEPaneProps } from './tabs.vue'
export default defineComponent({
name: 'ElTabPane',
props: {
@ -28,12 +30,13 @@ export default defineComponent({
disabled: Boolean,
lazy: Boolean,
},
setup(props) {
const index = ref(null)
setup(props: IEPaneProps) {
const index = ref<string>(null)
const loaded = ref(false)
const rootTabs = inject('rootTabs')
const rootTabs = inject<RootTabs>('rootTabs')
const updatePaneState = inject<UpdatePaneStateCallback>('updatePaneState')
if (!rootTabs) {
if (!rootTabs || !updatePaneState) {
throw new Error(`ElTabPane must use with ElTabs`)
}
@ -42,14 +45,14 @@ export default defineComponent({
})
const active = computed(() => {
const active = rootTabs.setupState.currentName === (props.name || index.value)
const active = rootTabs.currentName.value === (props.name || index.value)
if (active) {
loaded.value = true
}
return active
})
const paneName = computed(() => {
const paneName = computed((): string => {
return props.name || index.value
})
@ -57,6 +60,17 @@ export default defineComponent({
return (!props.lazy || loaded.value) || active.value
})
const instance = getCurrentInstance()
updatePaneState({
uid: instance.uid,
instance,
props,
paneName,
active,
index,
isClosable,
})
return {
index,
loaded,

View File

@ -1,10 +1,49 @@
<script lang='ts'>
import { h, defineComponent, ref, onMounted, onUpdated, provide, watch, nextTick, getCurrentInstance, ComponentInternalInstance } from 'vue'
import { h, defineComponent, ref, onMounted, onUpdated, provide, watch, nextTick, getCurrentInstance, ComputedRef, PropType, Ref, ComponentInternalInstance, VNode, Component } from 'vue'
import { Fragment } from '@vue/runtime-core'
import TabNav from './tab-nav.vue'
type RefElement = Nullable<HTMLElement>
type BeforeLeave = (newTabName: string, oldTabName: string) => void | Promise<void> | boolean
export interface IETabsProps {
type: string
activeName: string
closable: boolean
addable: boolean
modelValue: string
editable: boolean
tabPosition: string
beforeLeave: BeforeLeave
stretch: boolean
}
export interface RootTabs {
props: IETabsProps
currentName: Ref<string>
}
export interface IEPaneProps {
label: string
name: string
closable: boolean
disabled: boolean
lazy: boolean
}
export interface Pane {
uid: number
instance: ComponentInternalInstance
props: IEPaneProps
paneName: ComputedRef<string>
active: ComputedRef<boolean>
index: Ref<string>
isClosable: ComputedRef<boolean>
}
export type UpdatePaneStateCallback = (pane: Pane) => void
export default defineComponent({
name: 'ElTabs',
components: { TabNav },
@ -29,17 +68,27 @@ export default defineComponent({
default: 'top',
},
beforeLeave: {
type: Function as PropType<(newTabName: string, oldTabName: string) => void>,
type: Function as PropType<BeforeLeave>,
default: null,
},
stretch: Boolean,
},
emits: ['tab-click', 'edit', 'tab-remove', 'tab-add', 'input', 'update:modelValue'],
setup(props, ctx) {
const nav$ = ref<RefElement>(null)
setup(props: IETabsProps, ctx) {
const nav$ = ref<typeof TabNav>(null)
const currentName = ref(props.modelValue || props.activeName || '0')
const panes = ref<ComponentInternalInstance[]>([])
const panes = ref([])
const instance = getCurrentInstance()
const paneStatesMap = {}
provide<RootTabs>('rootTabs', {
props,
currentName,
})
provide<UpdatePaneStateCallback>('updatePaneState', (pane: Pane) => {
paneStatesMap[pane.uid] = pane
})
watch(() => props.activeName, modelValue => {
setCurrentName(modelValue)
@ -60,11 +109,11 @@ export default defineComponent({
setPaneInstances(true)
})
const getPaneInstanceFromSlot = (vnode, paneInstanceList = []) => {
const getPaneInstanceFromSlot = (vnode: VNode, paneInstanceList: ComponentInternalInstance[] = []) => {
Array.from(vnode.children || []).forEach(node => {
Array.from((vnode.children || []) as ArrayLike<VNode>).forEach(node => {
let type = node.type
type = type.name || type
type = (type as Component).name || type
if (type === 'ElTabPane' && node.component) {
paneInstanceList.push(node.component)
} else if(type === Fragment || type === 'template') {
@ -78,13 +127,15 @@ export default defineComponent({
if(ctx.slots.default) {
const children = instance.subTree.children
const content = Array.from(children).find(({ props }) => {
const content = Array.from(children as ArrayLike<VNode>).find(({ props }) => {
return props.class === 'el-tabs__content'
})
if(!content) return
const paneInstanceList = getPaneInstanceFromSlot(content)
const paneInstanceList: Pane[] = getPaneInstanceFromSlot(content).map(paneComponent => {
return paneStatesMap[paneComponent.uid]
})
const panesChanged = !(paneInstanceList.length === panes.value.length && paneInstanceList.every((pane, index) => pane.uid === panes.value[index].uid))
if (isForceUpdate || panesChanged) {
@ -104,8 +155,8 @@ export default defineComponent({
const setCurrentName = value => {
if(currentName.value !== value && props.beforeLeave) {
const before = props.beforeLeave(value, currentName.value)
if(before && before.then) {
before.then(() => {
if(before && (before as Promise<void>).then) {
(before as Promise<void>).then(() => {
changeCurrentName(value)
nav$.value && nav$.value.removeFocus()
}, () => {
@ -137,8 +188,6 @@ export default defineComponent({
ctx.emit('tab-add')
}
provide('rootTabs', getCurrentInstance())
onUpdated(() => {
setPaneInstances()
})

View File

@ -1,7 +1,7 @@
import ResizeObserver from 'resize-observer-polyfill'
import isServer from './isServer'
type ResizableElement = CustomizedHTMLElement<{
export type ResizableElement = CustomizedHTMLElement<{
__resizeListeners__: Array<(...args: unknown[]) => unknown>
__ro__: ResizeObserver
}>;