mirror of
https://github.com/element-plus/element-plus.git
synced 2026-03-13 07:51:17 +08:00
fix(components): [tabs] focus may still remain in pane when tabs changed (#19433)
* fix(components): [tabs] focus may still remain in pane when tabs changed closed #19364 * fix: type * chore: style * fix: test
This commit is contained in:
@@ -6,8 +6,10 @@ import Tabs from '../src/tabs'
|
||||
import TabPane from '../src/tab-pane.vue'
|
||||
import TabNav from '../src/tab-nav'
|
||||
|
||||
import type { TabPaneName } from '../src/tabs'
|
||||
import type { TabsPaneContext } from '@element-plus/components/tabs'
|
||||
import type {
|
||||
TabPaneName,
|
||||
TabsPaneContext,
|
||||
} from '@element-plus/components/tabs'
|
||||
|
||||
const Comp = defineComponent({
|
||||
components: {
|
||||
|
||||
@@ -2,19 +2,22 @@ import type { ComputedRef, InjectionKey, Ref, Slots, UnwrapRef } from 'vue'
|
||||
import type { TabsProps } from './tabs'
|
||||
import type { TabPaneProps } from './tab-pane'
|
||||
|
||||
export type TabPaneName = string | number
|
||||
|
||||
export type TabsPaneContext = UnwrapRef<{
|
||||
uid: number
|
||||
slots: Slots
|
||||
props: TabPaneProps
|
||||
paneName: ComputedRef<string | number | undefined>
|
||||
paneName: ComputedRef<TabPaneName | undefined>
|
||||
active: ComputedRef<boolean>
|
||||
index: Ref<string | undefined>
|
||||
isClosable: ComputedRef<boolean>
|
||||
isFocusInsidePane: () => boolean | undefined
|
||||
}>
|
||||
|
||||
export interface TabsRootContext {
|
||||
props: TabsProps
|
||||
currentName: Ref<string | number>
|
||||
currentName: Ref<TabPaneName>
|
||||
registerPane: (pane: TabsPaneContext) => void
|
||||
unregisterPane: (pane: TabsPaneContext) => void
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { buildProps, definePropType, mutable } from '@element-plus/utils'
|
||||
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import type { TabsPaneContext } from './constants'
|
||||
import type { TabPaneName, TabsPaneContext } from './constants'
|
||||
import type TabBar from './tab-bar.vue'
|
||||
|
||||
export const tabBarProps = buildProps({
|
||||
@@ -9,6 +9,10 @@ export const tabBarProps = buildProps({
|
||||
type: definePropType<TabsPaneContext[]>(Array),
|
||||
default: () => mutable([] as const),
|
||||
},
|
||||
tabRefs: {
|
||||
type: definePropType<{ [key: TabPaneName]: HTMLDivElement }>(Object),
|
||||
default: () => mutable({} as const),
|
||||
},
|
||||
} as const)
|
||||
|
||||
export type TabBarProps = ExtractPropTypes<typeof tabBarProps>
|
||||
|
||||
@@ -7,16 +7,9 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
getCurrentInstance,
|
||||
inject,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
ref,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import { inject, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useResizeObserver } from '@vueuse/core'
|
||||
import { capitalize, throwError } from '@element-plus/utils'
|
||||
import { capitalize, isUndefined, throwError } from '@element-plus/utils'
|
||||
import { useNamespace } from '@element-plus/hooks'
|
||||
import { tabsRootContextKey } from './constants'
|
||||
import { tabBarProps } from './tab-bar'
|
||||
@@ -29,7 +22,6 @@ defineOptions({
|
||||
})
|
||||
const props = defineProps(tabBarProps)
|
||||
|
||||
const instance = getCurrentInstance()!
|
||||
const rootTabs = inject(tabsRootContextKey)
|
||||
if (!rootTabs) throwError(COMPONENT_NAME, '<el-tabs><el-tab-bar /></el-tabs>')
|
||||
|
||||
@@ -49,7 +41,8 @@ const getBarStyle = (): CSSProperties => {
|
||||
const position = sizeDir === 'x' ? 'left' : 'top'
|
||||
|
||||
props.tabs.every((tab) => {
|
||||
const $el = instance.parent?.refs?.[`tab-${tab.uid}`] as HTMLElement
|
||||
if (isUndefined(tab.paneName)) return false
|
||||
const $el = props.tabRefs[tab.paneName]
|
||||
if (!$el) return false
|
||||
|
||||
if (!tab.active) {
|
||||
@@ -82,16 +75,10 @@ const saveObserver = [] as ReturnType<typeof useResizeObserver>[]
|
||||
const observerTabs = () => {
|
||||
saveObserver.forEach((observer) => observer.stop())
|
||||
saveObserver.length = 0
|
||||
const list = instance.parent?.refs as Record<string, HTMLElement>
|
||||
if (!list) return
|
||||
for (const key in list) {
|
||||
if (key.startsWith('tab-')) {
|
||||
const _el = list[key]
|
||||
if (_el) {
|
||||
saveObserver.push(useResizeObserver(_el, update))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.values(props.tabRefs).forEach((tab) => {
|
||||
saveObserver.push(useResizeObserver(tab, update))
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
|
||||
@@ -27,10 +27,13 @@ import { useNamespace } from '@element-plus/hooks'
|
||||
import TabBar from './tab-bar.vue'
|
||||
import { tabsRootContextKey } from './constants'
|
||||
|
||||
import type {
|
||||
CSSProperties,
|
||||
ComponentPublicInstance,
|
||||
ExtractPropTypes,
|
||||
} from 'vue'
|
||||
import type { TabBarInstance } from './tab-bar'
|
||||
import type { CSSProperties, ExtractPropTypes } from 'vue'
|
||||
import type { TabsPaneContext } from './constants'
|
||||
import type { TabPaneName } from './tabs'
|
||||
import type { TabPaneName, TabsPaneContext } from './constants'
|
||||
|
||||
interface Scrollable {
|
||||
next?: boolean
|
||||
@@ -80,6 +83,7 @@ const TabNav = defineComponent({
|
||||
const navScroll$ = ref<HTMLDivElement>()
|
||||
const nav$ = ref<HTMLDivElement>()
|
||||
const el$ = ref<HTMLDivElement>()
|
||||
const tabRefsMap = ref<{ [key: TabPaneName]: HTMLDivElement }>({})
|
||||
|
||||
const tabBarRef = ref<TabBarInstance>()
|
||||
|
||||
@@ -139,7 +143,7 @@ const TabNav = defineComponent({
|
||||
|
||||
await nextTick()
|
||||
|
||||
const activeTab = el$.value.querySelector('.is-active')
|
||||
const activeTab = tabRefsMap.value[props.currentName]
|
||||
if (!activeTab) return
|
||||
|
||||
const navScroll = navScroll$.value
|
||||
@@ -243,6 +247,20 @@ const TabNav = defineComponent({
|
||||
}
|
||||
const removeFocus = () => (isFocus.value = false)
|
||||
|
||||
const setRefs = (
|
||||
el: Element | ComponentPublicInstance | null,
|
||||
key: TabPaneName
|
||||
) => {
|
||||
tabRefsMap.value[key] = el as HTMLDivElement
|
||||
}
|
||||
|
||||
const focusActiveTab = async () => {
|
||||
await nextTick()
|
||||
|
||||
const activeTab = tabRefsMap.value[props.currentName]
|
||||
activeTab?.focus({ preventScroll: true })
|
||||
}
|
||||
|
||||
watch(visibility, (visibility) => {
|
||||
if (visibility === 'hidden') {
|
||||
focusable.value = false
|
||||
@@ -266,6 +284,7 @@ const TabNav = defineComponent({
|
||||
expose({
|
||||
scrollToActiveTab,
|
||||
removeFocus,
|
||||
focusActiveTab,
|
||||
tabListRef: nav$,
|
||||
tabBarRef,
|
||||
})
|
||||
@@ -322,7 +341,7 @@ const TabNav = defineComponent({
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={`tab-${uid}`}
|
||||
ref={(el) => setRefs(el, tabName)}
|
||||
class={[
|
||||
ns.e('item'),
|
||||
ns.is(rootTabs.props.tabPosition),
|
||||
@@ -387,7 +406,11 @@ const TabNav = defineComponent({
|
||||
>
|
||||
{...[
|
||||
!props.type ? (
|
||||
<TabBar ref={tabBarRef} tabs={[...props.panes]} />
|
||||
<TabBar
|
||||
ref={tabBarRef}
|
||||
tabs={[...props.panes]}
|
||||
tabRefs={tabRefsMap.value}
|
||||
/>
|
||||
) : null,
|
||||
tabs,
|
||||
]}
|
||||
@@ -403,6 +426,7 @@ const TabNav = defineComponent({
|
||||
export type TabNavInstance = InstanceType<typeof TabNav> & {
|
||||
scrollToActiveTab: () => Promise<void>
|
||||
removeFocus: () => void
|
||||
focusActiveTab: () => void
|
||||
tabListRef: HTMLDivElement | undefined
|
||||
tabBarRef: TabBarInstance | undefined
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
v-if="shouldBeRender"
|
||||
v-show="active"
|
||||
:id="`pane-${paneName}`"
|
||||
ref="paneRef"
|
||||
:class="ns.b()"
|
||||
role="tabpanel"
|
||||
:aria-hidden="!active"
|
||||
@@ -44,6 +45,7 @@ if (!tabsRoot)
|
||||
|
||||
const ns = useNamespace('tab-pane')
|
||||
|
||||
const paneRef = ref<HTMLDivElement>()
|
||||
const index = ref<string>()
|
||||
const isClosable = computed(() => props.closable || tabsRoot.props.closable)
|
||||
const active = eagerComputed(
|
||||
@@ -55,6 +57,10 @@ const shouldBeRender = eagerComputed(
|
||||
() => !props.lazy || loaded.value || active.value
|
||||
)
|
||||
|
||||
const isFocusInsidePane = () => {
|
||||
return paneRef.value?.contains(document.activeElement)
|
||||
}
|
||||
|
||||
watch(active, (val) => {
|
||||
if (val) loaded.value = true
|
||||
})
|
||||
@@ -67,6 +73,7 @@ const pane = reactive({
|
||||
active,
|
||||
index,
|
||||
isClosable,
|
||||
isFocusInsidePane,
|
||||
})
|
||||
|
||||
tabsRoot.registerPane(pane)
|
||||
|
||||
@@ -26,9 +26,7 @@ import TabNav from './tab-nav'
|
||||
import type { ExtractPropTypes, VNode } from 'vue'
|
||||
import type { Awaitable } from '@element-plus/utils'
|
||||
import type { TabNavInstance } from './tab-nav'
|
||||
import type { TabsPaneContext } from './constants'
|
||||
|
||||
export type TabPaneName = string | number
|
||||
import type { TabPaneName, TabsPaneContext } from './constants'
|
||||
|
||||
export const tabsProps = buildProps({
|
||||
/**
|
||||
@@ -134,6 +132,10 @@ const Tabs = defineComponent({
|
||||
}
|
||||
|
||||
if (canLeave !== false) {
|
||||
const isFocusInsidePane = panes.value
|
||||
.find((item) => item.paneName === currentName.value)
|
||||
?.isFocusInsidePane()
|
||||
|
||||
currentName.value = value
|
||||
if (trigger) {
|
||||
emit(UPDATE_MODEL_EVENT, value)
|
||||
@@ -141,6 +143,9 @@ const Tabs = defineComponent({
|
||||
}
|
||||
|
||||
nav$.value?.removeFocus?.()
|
||||
if (isFocusInsidePane) {
|
||||
nav$.value?.focusActiveTab()
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user