diff --git a/internal/build/src/type-safe.json b/internal/build/src/type-safe.json index f3a89b98eeff4..bf0db2fd490b9 100644 --- a/internal/build/src/type-safe.json +++ b/internal/build/src/type-safe.json @@ -24,6 +24,7 @@ "packages/components/rate/", "packages/components/row/", "packages/components/slot/", + "packages/components/tabs/", "packages/components/tag/", "packages/components/teleport/", "packages/components/upload/", diff --git a/packages/components/tabs/index.ts b/packages/components/tabs/index.ts index 53f0eb20e2bcb..f7a4d971a1db3 100644 --- a/packages/components/tabs/index.ts +++ b/packages/components/tabs/index.ts @@ -5,8 +5,8 @@ import TabPane from './src/tab-pane.vue' export const ElTabs = withInstall(Tabs, { TabPane, }) -export default ElTabs export const ElTabPane = withNoopInstall(TabPane) +export default ElTabs export * from './src/tabs' export * from './src/tab-bar' diff --git a/packages/components/tabs/src/tab-bar.ts b/packages/components/tabs/src/tab-bar.ts index 80bf318abdb08..9689e28669e80 100644 --- a/packages/components/tabs/src/tab-bar.ts +++ b/packages/components/tabs/src/tab-bar.ts @@ -1,12 +1,14 @@ import { buildProps, definePropType, mutable } from '@element-plus/utils' -import type { TabsPaneContext } from '@element-plus/tokens' import type { ExtractPropTypes } from 'vue' +import type { TabsPaneContext } from '@element-plus/tokens' +import type TabBar from './tab-bar.vue' -export const tabBar = buildProps({ +export const tabBarProps = buildProps({ tabs: { type: definePropType(Array), default: () => mutable([] as const), }, } as const) -export type TabBar = ExtractPropTypes +export type TabBarProps = ExtractPropTypes +export type TabBarInstance = InstanceType diff --git a/packages/components/tabs/src/tab-bar.vue b/packages/components/tabs/src/tab-bar.vue index a90cfdfa54364..fe881f3380159 100644 --- a/packages/components/tabs/src/tab-bar.vue +++ b/packages/components/tabs/src/tab-bar.vue @@ -1,101 +1,92 @@ - diff --git a/packages/components/tabs/src/tab-nav.ts b/packages/components/tabs/src/tab-nav.tsx similarity index 71% rename from packages/components/tabs/src/tab-nav.ts rename to packages/components/tabs/src/tab-nav.tsx index 05312dfcfb88c..c399ecbd9dcf8 100644 --- a/packages/components/tabs/src/tab-nav.ts +++ b/packages/components/tabs/src/tab-nav.tsx @@ -1,14 +1,13 @@ import { computed, defineComponent, - h, inject, onMounted, onUpdated, ref, watch, } from 'vue' -import { NOOP, capitalize } from '@vue/shared' +import { NOOP } from '@vue/shared' import { useDocumentVisibility, useResizeObserver, @@ -16,6 +15,7 @@ import { } from '@vueuse/core' import { buildProps, + capitalize, definePropType, mutable, throwError, @@ -24,10 +24,13 @@ import { EVENT_CODE } from '@element-plus/constants' import { ElIcon } from '@element-plus/components/icon' import { ArrowLeft, ArrowRight, Close } from '@element-plus/icons-vue' import { tabsRootContextKey } from '@element-plus/tokens' +import { useNamespace } from '@element-plus/hooks' import TabBar from './tab-bar.vue' import type { CSSProperties, ExtractPropTypes } from 'vue' import type { TabsPaneContext } from '@element-plus/tokens' -export interface Scrollable { +import type { TabPanelName } from './tabs' + +interface Scrollable { next?: boolean prev?: number } @@ -44,7 +47,7 @@ export const tabNavProps = buildProps({ editable: Boolean, onTabClick: { type: definePropType< - (tab: TabsPaneContext, tabName: string | number, ev: Event) => void + (tab: TabsPaneContext, tabName: TabPanelName, ev: Event) => void >(Function), default: NOOP, }, @@ -63,27 +66,27 @@ export const tabNavProps = buildProps({ export type TabNavProps = ExtractPropTypes const COMPONENT_NAME = 'ElTabNav' -export default defineComponent({ +const TabNav = defineComponent({ name: COMPONENT_NAME, props: tabNavProps, setup(props, { expose }) { + const rootTabs = inject(tabsRootContextKey) + if (!rootTabs) throwError(COMPONENT_NAME, ``) + + const ns = useNamespace('tabs') const visibility = useDocumentVisibility() const focused = useWindowFocus() - const rootTabs = inject(tabsRootContextKey) - if (!rootTabs) - throwError(COMPONENT_NAME, `ElTabNav must be nested inside ElTabs`) + const navScroll$ = ref() + const nav$ = ref() + const el$ = ref() const scrollable = ref(false) const navOffset = ref(0) const isFocus = ref(false) const focusable = ref(true) - const navScroll$ = ref() - const nav$ = ref() - const el$ = ref() - const sizeName = computed(() => ['top', 'bottom'].includes(rootTabs.props.tabPosition) ? 'width' @@ -266,76 +269,74 @@ export default defineComponent({ return () => { const scrollBtn = scrollable.value ? [ - h( - 'span', - { - class: [ - 'el-tabs__nav-prev', - scrollable.value.prev ? '' : 'is-disabled', - ], - onClick: scrollPrev, - }, - [h(ElIcon, {}, { default: () => h(ArrowLeft) })] - ), - h( - 'span', - { - class: [ - 'el-tabs__nav-next', - scrollable.value.next ? '' : 'is-disabled', - ], - onClick: scrollNext, - }, - [h(ElIcon, {}, { default: () => h(ArrowRight) })] - ), + + + + + , + + + + + , ] : null const tabs = props.panes.map((pane, index) => { const tabName = pane.props.name || pane.index || `${index}` - const closable = pane.isClosable || props.editable + const closable: boolean = pane.isClosable || props.editable pane.index = `${index}` - const btnClose = closable - ? h( - ElIcon, - { - class: 'is-icon-close', - onClick: (ev: MouseEvent) => props.onTabRemove(pane, ev), - }, - { default: () => h(Close) } - ) - : null + const btnClose = closable ? ( + props.onTabRemove(pane, ev)} + > + + + ) : null const tabLabelContent = pane.instance.slots.label?.() || pane.props.label const tabindex = pane.active ? 0 : -1 - return h( - 'div', - { - class: { - 'el-tabs__item': true, - [`is-${rootTabs.props.tabPosition}`]: true, - 'is-active': pane.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.active, - ref: `tab-${tabName}`, - tabindex, - onFocus: () => setFocus(), - onBlur: () => removeFocus(), - onClick: (ev: MouseEvent) => { + return ( + ) }) - return h( - 'div', - { - ref: el$, - class: [ - 'el-tabs__nav-wrap', - scrollable.value ? '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}`, - props.stretch && + return ( +
+ {scrollBtn} +
+
+ {...[ + !props.type ? : null, + tabs, + ]} +
+
+
) } }, }) + +export type TabNavInstance = InstanceType +export default TabNav diff --git a/packages/components/tabs/src/tab-pane.ts b/packages/components/tabs/src/tab-pane.ts index a82db9dcf6de4..4e0507306e61b 100644 --- a/packages/components/tabs/src/tab-pane.ts +++ b/packages/components/tabs/src/tab-pane.ts @@ -1,5 +1,6 @@ import { buildProps } from '@element-plus/utils' import type { ExtractPropTypes } from 'vue' +import type TabPane from './tab-pane.vue' export const tabPaneProps = buildProps({ label: { @@ -16,3 +17,5 @@ export const tabPaneProps = buildProps({ } as const) export type TabPaneProps = ExtractPropTypes + +export type TabPaneInstance = InstanceType diff --git a/packages/components/tabs/src/tab-pane.vue b/packages/components/tabs/src/tab-pane.vue index 4505f07776de9..10937420abab0 100644 --- a/packages/components/tabs/src/tab-pane.vue +++ b/packages/components/tabs/src/tab-pane.vue @@ -3,7 +3,7 @@ v-if="shouldBeRender" v-show="active" :id="`pane-${paneName}`" - class="el-tab-pane" + :class="ns.b()" role="tabpanel" :aria-hidden="!active" :aria-labelledby="`tab-${paneName}`" @@ -11,10 +11,9 @@ - diff --git a/packages/components/tabs/src/tabs.ts b/packages/components/tabs/src/tabs.tsx similarity index 67% rename from packages/components/tabs/src/tabs.ts rename to packages/components/tabs/src/tabs.tsx index b976ba647f969..82dd865744e14 100644 --- a/packages/components/tabs/src/tabs.ts +++ b/packages/components/tabs/src/tabs.tsx @@ -2,7 +2,6 @@ import { Fragment, defineComponent, getCurrentInstance, - h, nextTick, onMounted, onUpdated, @@ -11,8 +10,14 @@ import { renderSlot, watch, } from 'vue' -import { NOOP, isPromise } from '@vue/shared' -import { buildProps, definePropType } from '@element-plus/utils' +import { NOOP } from '@vue/shared' +import { + buildProps, + definePropType, + isNumber, + isPromise, + isString, +} from '@element-plus/utils' import { EVENT_CODE, INPUT_EVENT, @@ -21,7 +26,9 @@ import { import ElIcon from '@element-plus/components/icon' import { Plus } from '@element-plus/icons-vue' import { tabsRootContextKey } from '@element-plus/tokens' +import { useNamespace } from '@element-plus/hooks' import TabNav from './tab-nav' +import type { TabNavInstance } from './tab-nav' import type { TabsPaneContext } from '@element-plus/tokens' import type { @@ -31,6 +38,9 @@ import type { Ref, VNode, } from 'vue' +import type { Awaitable } from '@element-plus/utils' + +export type TabPanelName = string | number export const tabsProps = buildProps({ type: { @@ -39,7 +49,7 @@ export const tabsProps = buildProps({ default: '', }, activeName: { - type: String, + type: [String, Number], default: '', }, closable: Boolean, @@ -57,9 +67,9 @@ export const tabsProps = buildProps({ beforeLeave: { type: definePropType< ( - newTabName: string | number, - oldTabName: string | number - ) => void | boolean | Promise + newName: TabPanelName, + oldName: TabPanelName + ) => Awaitable >(Function), default: () => true, }, @@ -67,16 +77,16 @@ export const tabsProps = buildProps({ } as const) export type TabsProps = ExtractPropTypes +const isPanelName = (value: unknown): value is string | number => + isString(value) || isNumber(value) + export const tabsEmits = { - [UPDATE_MODEL_EVENT]: (tabName: string | number) => - typeof tabName === 'string' || typeof tabName === 'number', - [INPUT_EVENT]: (tabName: string | number) => - typeof tabName === 'string' || typeof tabName === 'number', + [UPDATE_MODEL_EVENT]: (name: TabPanelName) => isPanelName(name), + [INPUT_EVENT]: (name: TabPanelName) => isPanelName(name), 'tab-click': (pane: TabsPaneContext, ev: Event) => ev instanceof Event, - edit: (paneName: string | number | null, action: 'remove' | 'add') => - action === 'remove' || action === 'add', - 'tab-remove': (paneName: string | number) => - typeof paneName === 'string' || typeof paneName === 'number', + edit: (paneName: TabPanelName | undefined, action: 'remove' | 'add') => + ['remove', 'add'].includes(action), + 'tab-remove': (name: TabPanelName) => isPanelName(name), 'tab-add': () => true, } export type TabsEmits = typeof tabsEmits @@ -106,8 +116,9 @@ export default defineComponent({ setup(props, { emit, slots, expose }) { const instance = getCurrentInstance()! - const nav$ = ref>() + const ns = useNamespace('tabs') + const nav$ = ref() const panes: Ref = ref([]) const currentName = ref(props.modelValue || props.activeName || '0') @@ -117,7 +128,7 @@ export default defineComponent({ if (slots.default) { const children = instance.subTree.children as ArrayLike const content = Array.from(children).find( - ({ props }) => props?.class === 'el-tabs__content' + ({ props }) => props?.class === ns.e('content') ) if (!content) return @@ -140,13 +151,13 @@ export default defineComponent({ } } - const changeCurrentName = (value: string | number) => { + const changeCurrentName = (value: TabPanelName) => { currentName.value = value emit(INPUT_EVENT, value) emit(UPDATE_MODEL_EVENT, value) } - const setCurrentName = (value: string | number) => { + const setCurrentName = (value: TabPanelName) => { // should do nothing. if (currentName.value === value) return @@ -171,7 +182,7 @@ export default defineComponent({ const handleTabClick = ( tab: TabsPaneContext, - tabName: string | number, + tabName: TabPanelName, event: Event ) => { if (tab.props.disabled) return @@ -187,7 +198,7 @@ export default defineComponent({ } const handleTabAdd = () => { - emit('edit', null, 'add') + emit('edit', undefined, 'add') emit('tab-add') } @@ -227,54 +238,56 @@ export default defineComponent({ return () => { const newButton = - props.editable || props.addable - ? h( - 'span', - { - class: 'el-tabs__new-tab', - tabindex: '0', - onClick: handleTabAdd, - onKeydown: (ev: KeyboardEvent) => { - if (ev.code === EVENT_CODE.enter) handleTabAdd() - }, - }, - [h(ElIcon, { class: 'is-icon-plus' }, { default: () => h(Plus) })] - ) - : null - - const header = h( - 'div', - { class: ['el-tabs__header', `is-${props.tabPosition}`] }, - [ - newButton, - h(TabNav, { - currentName: currentName.value, - editable: props.editable, - type: props.type, - panes: panes.value, - stretch: props.stretch, - ref: nav$, - onTabClick: handleTabClick, - onTabRemove: handleTabRemove, - }), - ] + props.editable || props.addable ? ( + { + if (ev.code === EVENT_CODE.enter) handleTabAdd() + }} + > + + + + + ) : null + + const header = ( +
+ {newButton} + +
) - const panels = h('div', { class: 'el-tabs__content' }, [ - renderSlot(slots, 'default'), - ]) - - return h( - 'div', - { - class: { - 'el-tabs': true, - 'el-tabs--card': props.type === 'card', - [`el-tabs--${props.tabPosition}`]: true, - 'el-tabs--border-card': props.type === 'border-card', - }, - }, - props.tabPosition !== 'bottom' ? [header, panels] : [panels, header] + const panels = ( +
{renderSlot(slots, 'default')}
+ ) + + return ( +
+ {...props.tabPosition !== 'bottom' + ? [header, panels] + : [panels, header]} +
) } }, diff --git a/typings/env.d.ts b/typings/env.d.ts index 6e843f8f3f43d..dd1baaae2745b 100644 --- a/typings/env.d.ts +++ b/typings/env.d.ts @@ -1,3 +1,5 @@ +import type { vShow } from 'vue' + declare global { const process: { env: { NODE_ENV: string } @@ -11,4 +13,14 @@ declare global { } } +declare module '@vue/runtime-core' { + export interface GlobalComponents { + Component: (props: { is: Component | string }) => void + } + + export interface ComponentCustomProperties { + vShow: typeof vShow + } +} + export {}