mirror of
https://github.com/tusen-ai/naive-ui.git
synced 2024-12-09 04:31:35 +08:00
refactor(tabs): ts
This commit is contained in:
parent
ead07b751d
commit
1c85ec61ca
2
src/global.d.ts
vendored
2
src/global.d.ts
vendored
@ -6,7 +6,7 @@ declare global {
|
||||
}
|
||||
|
||||
// TODO: remove it, since it may conflict with user's d.ts
|
||||
type ConflictKeys = 'title'
|
||||
type ConflictKeys = 'title' | 'label'
|
||||
|
||||
declare module 'vue' {
|
||||
interface ComponentCustomProps extends Omit<HTMLAttributes, ConflictKeys> {}
|
||||
|
@ -1,2 +0,0 @@
|
||||
export { default as NTab } from './src/Tabs.vue'
|
||||
export { default as NTabPane } from './src/TabPane.js'
|
2
src/tabs/index.ts
Normal file
2
src/tabs/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as NTab } from './src/Tabs'
|
||||
export { default as NTabPane } from './src/TabPane'
|
@ -1,58 +0,0 @@
|
||||
import { h, withDirectives, vShow, defineComponent } from 'vue'
|
||||
import { getSlot } from '../../_utils'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TabPane',
|
||||
alias: ['TabPanel'],
|
||||
inject: ['NTab'],
|
||||
props: {
|
||||
label: {
|
||||
type: [String, Number],
|
||||
default: undefined
|
||||
},
|
||||
name: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
displayDirective: {
|
||||
type: String,
|
||||
default: 'if'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
type () {
|
||||
return this.NTab.type
|
||||
}
|
||||
},
|
||||
created () {
|
||||
if (this.NTab) {
|
||||
this.NTab.addPanel(this)
|
||||
}
|
||||
},
|
||||
beforeUnmount () {
|
||||
if (this.NTab) {
|
||||
this.NTab.removePanel(this)
|
||||
}
|
||||
},
|
||||
render () {
|
||||
const show = this.name === this.NTab.value
|
||||
const useVShow = this.displayDirective === 'show'
|
||||
return useVShow || show
|
||||
? withDirectives(
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: 'n-tab-panel',
|
||||
key: this.name
|
||||
},
|
||||
getSlot(this)
|
||||
),
|
||||
[[vShow, !useVShow || show]]
|
||||
)
|
||||
: null
|
||||
}
|
||||
})
|
70
src/tabs/src/TabPane.ts
Normal file
70
src/tabs/src/TabPane.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import {
|
||||
h,
|
||||
withDirectives,
|
||||
vShow,
|
||||
defineComponent,
|
||||
ExtractPropTypes,
|
||||
inject,
|
||||
onBeforeUnmount,
|
||||
computed,
|
||||
PropType
|
||||
} from 'vue'
|
||||
import { getSlot } from '../../_utils'
|
||||
|
||||
const tabPaneProps = {
|
||||
label: [String, Number] as PropType<string | number>,
|
||||
name: {
|
||||
type: [String, Number] as PropType<string | number>,
|
||||
required: true
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
displayDirective: {
|
||||
type: String as PropType<'if' | 'show'>,
|
||||
default: 'if'
|
||||
}
|
||||
} as const
|
||||
|
||||
export type TabPaneProps = ExtractPropTypes<typeof tabPaneProps>
|
||||
|
||||
export interface TabsInjection {
|
||||
value: string | number | null
|
||||
type: 'line' | 'card'
|
||||
addPanel: (props: TabPaneProps) => void
|
||||
removePanel: (props: TabPaneProps) => void
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TabPane',
|
||||
alias: ['TabPanel'],
|
||||
props: tabPaneProps,
|
||||
setup (props) {
|
||||
const NTab = inject<TabsInjection>('NTabs') as TabsInjection
|
||||
NTab.addPanel(props)
|
||||
onBeforeUnmount(() => {
|
||||
NTab.removePanel(props)
|
||||
})
|
||||
return {
|
||||
type: computed(() => NTab.type),
|
||||
show: computed(() => props.name === NTab.value)
|
||||
}
|
||||
},
|
||||
render () {
|
||||
const useVShow = this.displayDirective === 'show'
|
||||
return useVShow || this.show
|
||||
? withDirectives(
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: 'n-tab-panel',
|
||||
key: this.name
|
||||
},
|
||||
getSlot(this)
|
||||
),
|
||||
[[vShow, !useVShow || this.show]]
|
||||
)
|
||||
: null
|
||||
}
|
||||
})
|
383
src/tabs/src/Tabs.tsx
Normal file
383
src/tabs/src/Tabs.tsx
Normal file
@ -0,0 +1,383 @@
|
||||
import {
|
||||
h,
|
||||
ref,
|
||||
defineComponent,
|
||||
computed,
|
||||
PropType,
|
||||
provide,
|
||||
CSSProperties,
|
||||
watch,
|
||||
nextTick,
|
||||
onMounted,
|
||||
reactive,
|
||||
toRef,
|
||||
Ref,
|
||||
renderSlot
|
||||
} from 'vue'
|
||||
import { VResizeObserver } from 'vueuc'
|
||||
import { throttle } from 'lodash-es'
|
||||
import { useCompitable, onFontsReady, useMergedState } from 'vooks'
|
||||
import { NBaseClose } from '../../_base'
|
||||
import { useTheme } from '../../_mixins'
|
||||
import type { ThemeProps } from '../../_mixins'
|
||||
import { warn, createKey, call } from '../../_utils'
|
||||
import type { MaybeArray } from '../../_utils'
|
||||
import { tabsLight } from '../styles'
|
||||
import type { TabsTheme } from '../styles'
|
||||
import type { TabsInjection, TabPaneProps } from './TabPane'
|
||||
import style from './styles/index.cssr'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Tabs',
|
||||
props: {
|
||||
...(useTheme.props as ThemeProps<TabsTheme>),
|
||||
value: [String, Number] as PropType<string | number>,
|
||||
defaultValue: {
|
||||
type: [String, Number] as PropType<string | number | null>,
|
||||
default: null
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<'line' | 'card'>,
|
||||
default: 'line'
|
||||
},
|
||||
closable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
justifyContent: String as PropType<
|
||||
'space-between' | 'space-around' | 'space-evenly'
|
||||
>,
|
||||
labelSize: {
|
||||
type: String as PropType<'small' | 'medium' | 'large' | 'huge'>,
|
||||
default: 'medium'
|
||||
},
|
||||
navStyle: [String, Object] as PropType<string | CSSProperties>,
|
||||
onScrollableChange: Function as PropType<
|
||||
MaybeArray<(value: boolean) => void>
|
||||
>,
|
||||
// eslint-disable-next-line vue/prop-name-casing
|
||||
'onUpdate:value': Function as PropType<
|
||||
MaybeArray<<T extends string | number>(value: T) => void>
|
||||
>,
|
||||
onClose: Function as PropType<MaybeArray<() => void>>,
|
||||
// deprecated
|
||||
activeName: {
|
||||
type: [String, Number] as PropType<string | number | undefined>,
|
||||
validator: () => {
|
||||
if (__DEV__) {
|
||||
warn(
|
||||
'tabs',
|
||||
'`active-name` is deprecated, please use `value` instead.'
|
||||
)
|
||||
}
|
||||
return true
|
||||
},
|
||||
default: undefined
|
||||
},
|
||||
onActiveNameChange: {
|
||||
type: Function as PropType<
|
||||
MaybeArray<<T extends string | number>(value: T) => void>
|
||||
>,
|
||||
validator: () => {
|
||||
if (__DEV__) {
|
||||
warn(
|
||||
'tabs',
|
||||
'`on-active-name-change` is deprecated, please use `on-update:value` instead.'
|
||||
)
|
||||
}
|
||||
return true
|
||||
},
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
const themeRef = useTheme('Tabs', 'Tabs', style, tabsLight, props)
|
||||
|
||||
const navRef = ref<HTMLElement | null>(null)
|
||||
const labelWrapperRef = ref<HTMLElement | null>(null)
|
||||
const labelBarRef = ref<HTMLElement | null>(null)
|
||||
|
||||
let preventYWheel = false
|
||||
|
||||
const panelsRef = ref<TabPaneProps[]>([])
|
||||
const transitionDisabledRef = ref(false)
|
||||
const compitableValueRef = useCompitable(props, ['activeName', 'value'])
|
||||
const uncontrolledValueRef = ref(props.defaultValue)
|
||||
const mergedValueRef = useMergedState(
|
||||
compitableValueRef,
|
||||
uncontrolledValueRef
|
||||
)
|
||||
|
||||
const compitableOnValueChangeRef = useCompitable(props, [
|
||||
'onActiveNameChange',
|
||||
'onUpdate:value'
|
||||
])
|
||||
const labelWrapperStyleRef = computed(() => {
|
||||
if (!props.justifyContent) return undefined
|
||||
return {
|
||||
display: 'flex',
|
||||
justifyContent: props.justifyContent
|
||||
}
|
||||
})
|
||||
const panelLabelsRef = computed(() => {
|
||||
return panelsRef.value.map((panel) => panel.label)
|
||||
})
|
||||
|
||||
watch(mergedValueRef, () => {
|
||||
updateCurrentBarPosition()
|
||||
})
|
||||
watch(panelLabelsRef, () => {
|
||||
void nextTick(updateScrollStatus)
|
||||
})
|
||||
|
||||
function updateScrollStatus (): void {
|
||||
const { value: navScrollEl } = navRef
|
||||
if (navScrollEl) {
|
||||
if (navScrollEl.scrollWidth > navScrollEl.offsetWidth) {
|
||||
preventYWheel = true
|
||||
} else {
|
||||
preventYWheel = false
|
||||
}
|
||||
}
|
||||
}
|
||||
function addPanel (panelProps: TabPaneProps): void {
|
||||
panelsRef.value.push(panelProps)
|
||||
}
|
||||
function removePanel (panelProps: TabPaneProps): void {
|
||||
const index = panelsRef.value.findIndex(
|
||||
(panel) => panel.name === panelProps.name
|
||||
)
|
||||
if (~index) {
|
||||
panelsRef.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
function updateBarPosition (labelEl: HTMLElement): void {
|
||||
if (props.type === 'card') return
|
||||
const { value: labelBarEl } = labelBarRef
|
||||
if (!labelBarEl) return
|
||||
if (labelEl) {
|
||||
labelBarEl.style.left = `${labelEl.offsetLeft}px`
|
||||
labelBarEl.style.width = '8192px'
|
||||
labelBarEl.style.maxWidth = `${labelEl.offsetWidth + 1}px`
|
||||
}
|
||||
}
|
||||
function updateCurrentBarPosition (): void {
|
||||
if (props.type === 'card') return
|
||||
const value = mergedValueRef.value
|
||||
for (const panel of panelsRef.value) {
|
||||
if (panel.name === value) {
|
||||
const labelEl = navRef.value?.querySelector(
|
||||
`[data-name="${panel.name}"]`
|
||||
)
|
||||
if (labelEl) {
|
||||
updateBarPosition(labelEl as HTMLElement)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
function handleTabsWheel (e: WheelEvent): void {
|
||||
if (!preventYWheel || !e.deltaY) return
|
||||
;(e.currentTarget as HTMLElement).scrollLeft += e.deltaY + e.deltaX
|
||||
e.preventDefault()
|
||||
}
|
||||
function handleTabClick (
|
||||
e: MouseEvent,
|
||||
panelName: string | number,
|
||||
disabled: boolean
|
||||
): void {
|
||||
if (!disabled) {
|
||||
setPanelActive(panelName)
|
||||
}
|
||||
}
|
||||
function setPanelActive (panelName: string | number): void {
|
||||
const { value: compitableOnValueChange } = compitableOnValueChangeRef
|
||||
if (compitableOnValueChange) call(compitableOnValueChange, panelName)
|
||||
}
|
||||
function handleCloseClick (e: MouseEvent, panel: TabPaneProps): void {
|
||||
const { onClose } = props
|
||||
if (onClose) call(onClose, panel.name)
|
||||
e.stopPropagation()
|
||||
}
|
||||
const handleNavResize = throttle(function handleNavResize () {
|
||||
if (props.type === 'card') {
|
||||
updateScrollStatus()
|
||||
} else if (props.type === 'line') {
|
||||
transitionDisabledRef.value = true
|
||||
void nextTick(() => {
|
||||
updateCurrentBarPosition()
|
||||
transitionDisabledRef.value = false
|
||||
})
|
||||
}
|
||||
}, 64)
|
||||
const handleScrollContentResize = throttle(
|
||||
function handleScrollContentResize () {
|
||||
updateScrollStatus()
|
||||
},
|
||||
64
|
||||
)
|
||||
provide<TabsInjection>(
|
||||
'NTabs',
|
||||
reactive({
|
||||
type: toRef(props, 'type') as Ref<'line' | 'card'>,
|
||||
value: mergedValueRef,
|
||||
removePanel,
|
||||
addPanel
|
||||
})
|
||||
)
|
||||
onMounted(() => {
|
||||
updateScrollStatus()
|
||||
})
|
||||
onFontsReady(() => {
|
||||
updateScrollStatus()
|
||||
updateCurrentBarPosition()
|
||||
})
|
||||
return {
|
||||
mergedValue: mergedValueRef,
|
||||
compitableOnValueChange: compitableOnValueChangeRef,
|
||||
navRef,
|
||||
labelWrapperRef,
|
||||
labelBarRef,
|
||||
labelWrapperStyle: labelWrapperStyleRef,
|
||||
panels: panelsRef,
|
||||
transitionDisabled: transitionDisabledRef,
|
||||
handleTabClick,
|
||||
handleScrollContentResize,
|
||||
handleNavResize,
|
||||
handleCloseClick,
|
||||
handleTabsWheel,
|
||||
cssVars: computed(() => {
|
||||
const { labelSize } = props
|
||||
const {
|
||||
self: {
|
||||
labelTextColor,
|
||||
labelTextColorActive,
|
||||
labelTextColorHover,
|
||||
labelTextColorDisabled,
|
||||
labelBarColor,
|
||||
closeColor,
|
||||
closeColorHover,
|
||||
closeColorPressed,
|
||||
tabColor,
|
||||
tabBorderColorActive,
|
||||
tabTextColor,
|
||||
tabTextColorActive,
|
||||
tabBorderColor,
|
||||
paneTextColor,
|
||||
tabFontWeight,
|
||||
tabBorderRadius,
|
||||
labelFontSizeCard,
|
||||
[createKey('labelFontSizeLine', labelSize)]: labelFontSizeLine
|
||||
},
|
||||
common: { cubicBezierEaseInOut }
|
||||
} = themeRef.value
|
||||
return {
|
||||
'--bezier': cubicBezierEaseInOut,
|
||||
'--label-bar-color': labelBarColor,
|
||||
'--label-font-size-card': labelFontSizeCard,
|
||||
'--label-font-size-line': labelFontSizeLine,
|
||||
'--label-text-color': labelTextColor,
|
||||
'--label-text-color-active': labelTextColorActive,
|
||||
'--label-text-color-disabled': labelTextColorDisabled,
|
||||
'--label-text-color-hover': labelTextColorHover,
|
||||
'--pane-text-color': paneTextColor,
|
||||
'--tab-border-color': tabBorderColor,
|
||||
'--tab-border-color-active': tabBorderColorActive,
|
||||
'--tab-border-radius': tabBorderRadius,
|
||||
'--close-color': closeColor,
|
||||
'--close-color-hover': closeColorHover,
|
||||
'--close-color-pressed': closeColorPressed,
|
||||
'--tab-color': tabColor,
|
||||
'--tab-font-weight': tabFontWeight,
|
||||
'--tab-text-color': tabTextColor,
|
||||
'--tab-text-color-active': tabTextColorActive
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
render () {
|
||||
return (
|
||||
<div
|
||||
class={[
|
||||
'n-tabs',
|
||||
`n-tabs--${this.type}-type`,
|
||||
`n-tabs--${this.labelSize}-size`,
|
||||
{
|
||||
'n-tabs--flex': this.justifyContent
|
||||
}
|
||||
]}
|
||||
style={this.cssVars as CSSProperties}
|
||||
>
|
||||
<VResizeObserver onResize={this.handleNavResize}>
|
||||
{{
|
||||
default: () => (
|
||||
<div
|
||||
ref="navRef"
|
||||
class="n-tabs-nav"
|
||||
onWheel={this.handleTabsWheel}
|
||||
style={this.navStyle}
|
||||
>
|
||||
<VResizeObserver onResize={this.handleScrollContentResize}>
|
||||
{{
|
||||
default: () => (
|
||||
<div ref="labelWrapperRef" class="n-tabs-label-wrapper">
|
||||
<div style={this.labelWrapperStyle}>
|
||||
{this.panels.map((panel, i) => (
|
||||
<div
|
||||
key={i}
|
||||
data-name={panel.name}
|
||||
class={[
|
||||
'n-tabs-label',
|
||||
{
|
||||
'n-tabs-label--active':
|
||||
this.mergedValue === panel.name,
|
||||
'n-tabs-label--disabled': panel.disabled
|
||||
}
|
||||
]}
|
||||
onClick={(e) =>
|
||||
this.handleTabClick(
|
||||
e,
|
||||
panel.name,
|
||||
panel.disabled
|
||||
)
|
||||
}
|
||||
>
|
||||
<span class="n-tabs-label__label">
|
||||
{panel.label}
|
||||
</span>
|
||||
{this.closable && this.type === 'card' ? (
|
||||
<NBaseClose
|
||||
class="n-tabs-label__close"
|
||||
onClick={(e) =>
|
||||
this.handleCloseClick(e, panel)
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{this.type === 'line' ? (
|
||||
<div
|
||||
ref="labelBarRef"
|
||||
class={[
|
||||
'n-tabs-label-bar',
|
||||
{
|
||||
'n-tabs-label-bar--transition-disabled': this
|
||||
.transitionDisabled
|
||||
}
|
||||
]}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</VResizeObserver>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</VResizeObserver>
|
||||
{renderSlot(this.$slots, 'default')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
@ -1,395 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="n-tabs"
|
||||
:class="{
|
||||
[`n-tabs--${type}-type`]: true,
|
||||
'n-tabs--scroll': showScrollButton,
|
||||
[`n-tabs--${labelSize}-size`]: labelSize,
|
||||
[`n-tabs--flex`]: justifyContent
|
||||
}"
|
||||
:style="cssVars"
|
||||
>
|
||||
<v-resize-observer @resize="handleNavResize">
|
||||
<div ref="navRef" class="n-tabs-nav" :style="navStyle">
|
||||
<div
|
||||
v-if="showScrollButton"
|
||||
class="n-tabs-nav-scroll-button n-tabs-nav-scroll-button--left"
|
||||
:class="{
|
||||
'n-tabs-nav-scroll-button--disabled': leftScrollButtonDisabled
|
||||
}"
|
||||
@click="scroll('left')"
|
||||
>
|
||||
<n-base-icon>
|
||||
<backward-icon />
|
||||
</n-base-icon>
|
||||
</div>
|
||||
<div ref="navScrollRef" class="n-tabs-nav-scroll">
|
||||
<v-resize-observer @resize="handleScrollContentResize">
|
||||
<div ref="labelWrapperRef" class="n-tabs-label-wrapper">
|
||||
<div :style="labelWrapperStyle">
|
||||
<div
|
||||
v-for="(panel, i) in panels"
|
||||
:key="i"
|
||||
:ref="`label(${i})`"
|
||||
class="n-tabs-label"
|
||||
:class="{
|
||||
'n-tabs-label--active': compitableValue === panel.name,
|
||||
'n-tabs-label--disabled': panel.disabled
|
||||
}"
|
||||
@click="handleTabClick($event, panel.name, panel.disabled)"
|
||||
>
|
||||
<span class="n-tabs-label__label">{{ panel.label }}</span>
|
||||
<n-base-close
|
||||
v-if="closable && typeIsCard"
|
||||
class="n-tabs-label__close"
|
||||
@click.stop="handleCloseClick(panel)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!typeIsCard"
|
||||
ref="labelBarRef"
|
||||
class="n-tabs-label-bar"
|
||||
:class="{
|
||||
'n-tabs-label-bar--transition-disabled': transitionDisabled
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</v-resize-observer>
|
||||
</div>
|
||||
<div
|
||||
v-if="showScrollButton"
|
||||
class="n-tabs-nav-scroll-button n-tabs-nav-scroll-button--right"
|
||||
:class="{
|
||||
'n-tabs-nav-scroll-button--disabled': rightScrollButtonDisabled
|
||||
}"
|
||||
@click="scroll('right')"
|
||||
>
|
||||
<n-base-icon>
|
||||
<forward-icon />
|
||||
</n-base-icon>
|
||||
</div>
|
||||
</div>
|
||||
</v-resize-observer>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, getCurrentInstance, defineComponent, computed } from 'vue'
|
||||
import { VResizeObserver } from 'vueuc'
|
||||
import { throttle } from 'lodash-es'
|
||||
import { useCompitable, onFontsReady } from 'vooks'
|
||||
import {
|
||||
ChevronLeftIcon as BackwardIcon,
|
||||
ChevronRightIcon as ForwardIcon
|
||||
} from '../../_base/icons'
|
||||
import { NBaseIcon, NBaseClose } from '../../_base'
|
||||
import { useTheme } from '../../_mixins'
|
||||
import { warn, createKey } from '../../_utils'
|
||||
import { tabsLight } from '../styles'
|
||||
import style from './styles/index.cssr.js'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Tabs',
|
||||
components: {
|
||||
NBaseIcon,
|
||||
BackwardIcon,
|
||||
ForwardIcon,
|
||||
NBaseClose,
|
||||
VResizeObserver
|
||||
},
|
||||
provide () {
|
||||
return {
|
||||
NTab: this
|
||||
}
|
||||
},
|
||||
props: {
|
||||
...useTheme.props,
|
||||
value: {
|
||||
type: String || Number,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
validator (type) {
|
||||
return ['line', 'card'].includes(type)
|
||||
},
|
||||
default: 'line'
|
||||
},
|
||||
closable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
justifyContent: {
|
||||
validator (value) {
|
||||
return ['space-between', 'space-around', 'space-evenly'].includes(value)
|
||||
},
|
||||
default: undefined
|
||||
},
|
||||
labelSize: {
|
||||
validator (value) {
|
||||
return ['small', 'medium', 'large', 'huge'].includes(value)
|
||||
},
|
||||
default: 'medium'
|
||||
},
|
||||
navStyle: {
|
||||
type: [String, Object],
|
||||
default: undefined
|
||||
},
|
||||
onScrollableChange: {
|
||||
type: Function,
|
||||
default: undefined
|
||||
},
|
||||
// eslint-disable-next-line vue/prop-name-casing
|
||||
'onUpdate:value': {
|
||||
type: Function,
|
||||
default: undefined
|
||||
},
|
||||
onClose: {
|
||||
type: Function,
|
||||
default: undefined
|
||||
},
|
||||
// deprecated
|
||||
activeName: {
|
||||
validator () {
|
||||
if (__DEV__) {
|
||||
warn(
|
||||
'tabs',
|
||||
'`active-name` is deprecated, please use `value` instead.'
|
||||
)
|
||||
}
|
||||
return true
|
||||
},
|
||||
default: undefined
|
||||
},
|
||||
onActiveNameChange: {
|
||||
validator () {
|
||||
if (__DEV__) {
|
||||
warn(
|
||||
'tabs',
|
||||
'`on-active-name-change` is deprecated, please use `on-update:value` instead.'
|
||||
)
|
||||
}
|
||||
return true
|
||||
},
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
const themeRef = useTheme('Tabs', 'Tabs', style, tabsLight, props)
|
||||
const vm = getCurrentInstance().proxy
|
||||
onFontsReady(() => {
|
||||
vm.updateScrollStatus()
|
||||
if (vm.typeIsLine) {
|
||||
vm.updateCurrentBarPosition()
|
||||
}
|
||||
})
|
||||
return {
|
||||
compitableValue: useCompitable(props, ['activeName', 'value']),
|
||||
compitableOnValueChange: useCompitable(props, [
|
||||
'onActiveNameChange',
|
||||
'onUpdate:value'
|
||||
]),
|
||||
navScrollRef: ref(null),
|
||||
labelWrapperRef: ref(null),
|
||||
navRef: ref(null),
|
||||
labelBarRef: ref(null),
|
||||
cssVars: computed(() => {
|
||||
const { labelSize } = props
|
||||
const {
|
||||
self: {
|
||||
labelTextColor,
|
||||
labelTextColorActive,
|
||||
labelTextColorHover,
|
||||
labelTextColorDisabled,
|
||||
labelBarColor,
|
||||
scrollButtonColor,
|
||||
scrollButtonColorDisabled,
|
||||
closeColor,
|
||||
closeColorHover,
|
||||
closeColorPressed,
|
||||
tabColor,
|
||||
tabBorderColorActive,
|
||||
tabTextColor,
|
||||
tabTextColorActive,
|
||||
tabBorderColor,
|
||||
paneTextColor,
|
||||
tabFontWeight,
|
||||
tabBorderRadius,
|
||||
labelFontSizeCard,
|
||||
[createKey('labelFontSizeLine', labelSize)]: labelFontSizeLine
|
||||
},
|
||||
common: { cubicBezierEaseInOut }
|
||||
} = themeRef.value
|
||||
return {
|
||||
'--bezier': cubicBezierEaseInOut,
|
||||
'--label-bar-color': labelBarColor,
|
||||
'--label-font-size-card': labelFontSizeCard,
|
||||
'--label-font-size-line': labelFontSizeLine,
|
||||
'--label-text-color': labelTextColor,
|
||||
'--label-text-color-active': labelTextColorActive,
|
||||
'--label-text-color-disabled': labelTextColorDisabled,
|
||||
'--label-text-color-hover': labelTextColorHover,
|
||||
'--pane-text-color': paneTextColor,
|
||||
'--scroll-button-color': scrollButtonColor,
|
||||
'--scroll-button-color-disabled': scrollButtonColorDisabled,
|
||||
'--tab-border-color': tabBorderColor,
|
||||
'--tab-border-color-active': tabBorderColorActive,
|
||||
'--tab-border-radius': tabBorderRadius,
|
||||
'--close-color': closeColor,
|
||||
'--close-color-hover': closeColorHover,
|
||||
'--close-color-pressed': closeColorPressed,
|
||||
'--tab-color': tabColor,
|
||||
'--tab-font-weight': tabFontWeight,
|
||||
'--tab-text-color': tabTextColor,
|
||||
'--tab-text-color-active': tabTextColorActive
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
panels: [],
|
||||
barStyleInitialized: false,
|
||||
showScrollButton: false,
|
||||
leftScrollButtonDisabled: true,
|
||||
rightScrollButtonDisabled: false,
|
||||
transitionDisabled: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
typeIsCard () {
|
||||
return this.type === 'card'
|
||||
},
|
||||
typeIsLine () {
|
||||
return this.type === 'line'
|
||||
},
|
||||
labelWrapperStyle () {
|
||||
if (!this.justifyContent) return null
|
||||
return {
|
||||
display: 'flex',
|
||||
justifyContent: this.justifyContent
|
||||
}
|
||||
},
|
||||
panelLabels () {
|
||||
return this.panels.map((panel) => panel.label)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
compitableValue () {
|
||||
if (this.typeIsLine) {
|
||||
this.updateCurrentBarPosition()
|
||||
}
|
||||
},
|
||||
showScrollButton (value) {
|
||||
const { onScrollableChange } = this
|
||||
if (onScrollableChange) onScrollableChange(value)
|
||||
},
|
||||
panelLabels () {
|
||||
this.$nextTick(this.updateScrollStatus)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.updateScrollStatus()
|
||||
},
|
||||
methods: {
|
||||
scroll (direction) {
|
||||
const navScroll = this.navScrollRef
|
||||
const scrollLeft = navScroll.scrollLeft
|
||||
const labelWrapper = this.labelWrapperRef
|
||||
const scrollWidth = navScroll.offsetWidth * 0.8
|
||||
let left = 0
|
||||
if (direction === 'left') {
|
||||
left = scrollLeft - scrollWidth
|
||||
} else {
|
||||
left = scrollLeft + scrollWidth
|
||||
}
|
||||
if (left <= 0) {
|
||||
this.leftScrollButtonDisabled = true
|
||||
this.rightScrollButtonDisabled = false
|
||||
} else if (left >= labelWrapper.offsetWidth - navScroll.offsetWidth) {
|
||||
this.rightScrollButtonDisabled = true
|
||||
this.leftScrollButtonDisabled = false
|
||||
} else {
|
||||
this.rightScrollButtonDisabled = false
|
||||
this.leftScrollButtonDisabled = false
|
||||
}
|
||||
navScroll.scrollTo({
|
||||
left,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
},
|
||||
updateScrollStatus () {
|
||||
const labelWrapper = this.labelWrapperRef
|
||||
const nav = this.navRef
|
||||
if (labelWrapper.offsetWidth > nav.offsetWidth) {
|
||||
this.showScrollButton = true
|
||||
} else {
|
||||
this.showScrollButton = false
|
||||
}
|
||||
},
|
||||
addPanel (panelInstance) {
|
||||
this.panels.push(panelInstance)
|
||||
},
|
||||
removePanel (panelInstance) {
|
||||
const index = this.panels.findIndex((panel) => panel === panelInstance)
|
||||
if (~index) {
|
||||
this.panels.splice(index, 1)
|
||||
}
|
||||
},
|
||||
updateBarPosition (labelEl) {
|
||||
const labelBarEl = this.labelBarRef
|
||||
// BUG? this.$el is null
|
||||
if (labelBarEl && labelEl) {
|
||||
labelBarEl.style.left = labelEl.offsetLeft + 'px'
|
||||
labelBarEl.style.width = '8192px'
|
||||
if (this.type === 'card') {
|
||||
labelBarEl.style.maxWidth = labelEl.offsetWidth + 'px'
|
||||
} else {
|
||||
labelBarEl.style.maxWidth = labelEl.offsetWidth + 1 + 'px'
|
||||
}
|
||||
}
|
||||
},
|
||||
updateCurrentBarPosition () {
|
||||
let index = 0
|
||||
const value = this.compitableValue
|
||||
const refs = this.$refs
|
||||
for (const panel of this.panels) {
|
||||
if (panel.name === value) {
|
||||
this.updateBarPosition(refs[`label(${index})`])
|
||||
break
|
||||
}
|
||||
index++
|
||||
}
|
||||
},
|
||||
handleTabClick (e, panelName, disabled) {
|
||||
if (!disabled) {
|
||||
this.setPanelActive(panelName)
|
||||
}
|
||||
},
|
||||
setPanelActive (panelName) {
|
||||
const { compitableOnValueChange } = this
|
||||
if (compitableOnValueChange) compitableOnValueChange(panelName)
|
||||
},
|
||||
handleCloseClick (panel) {
|
||||
const { onClose } = this
|
||||
if (onClose) onClose(panel.name)
|
||||
},
|
||||
handleNavResize: throttle(function handleNavResize () {
|
||||
if (this.typeIsCard || (this.typeIsLine && !this.justifyContent)) {
|
||||
this.updateScrollStatus()
|
||||
}
|
||||
if (this.typeIsLine) {
|
||||
this.transitionDisabled = true
|
||||
this.$nextTick().then(() => {
|
||||
this.updateCurrentBarPosition()
|
||||
this.transitionDisabled = false
|
||||
})
|
||||
}
|
||||
}, 64),
|
||||
handleScrollContentResize: throttle(function handleScrollContentResize () {
|
||||
this.updateScrollStatus()
|
||||
}, 64)
|
||||
}
|
||||
})
|
||||
</script>
|
@ -13,8 +13,6 @@ import { c, cM, cB, cE } from '../../../_utils/cssr'
|
||||
// --label-text-color-disabled
|
||||
// --label-text-color-hover
|
||||
// --pane-text-color
|
||||
// --scroll-button-color
|
||||
// --scroll-button-color-disabled
|
||||
// --tab-border-color
|
||||
// --tab-border-color-active
|
||||
// --tab-border-radius
|
||||
@ -29,17 +27,15 @@ export default cB('tabs', `
|
||||
border-color .3s var(--bezier);
|
||||
`, [
|
||||
cM('flex', [
|
||||
cB('tabs-nav', [
|
||||
cB('tabs-nav-scroll', {
|
||||
cB('tabs-nav', {
|
||||
width: '100%'
|
||||
}, [
|
||||
cB('tabs-label-wrapper', {
|
||||
width: '100%'
|
||||
}, [
|
||||
cB('tabs-label-wrapper', {
|
||||
width: '100%'
|
||||
}, [
|
||||
cB('tabs-label', {
|
||||
marginRight: 0
|
||||
})
|
||||
])
|
||||
cB('tabs-label', {
|
||||
marginRight: 0
|
||||
})
|
||||
])
|
||||
])
|
||||
]),
|
||||
@ -48,30 +44,13 @@ export default cB('tabs', `
|
||||
display: flex;
|
||||
background-clip: padding-box;
|
||||
transition: border-color .3s var(--bezier);
|
||||
overflow: auto;
|
||||
scrollbar-width: none;
|
||||
`, [
|
||||
cB('tabs-nav-scroll', {
|
||||
overflow: 'hidden'
|
||||
}),
|
||||
cB('tabs-nav-scroll-button', `
|
||||
font-size: 20px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
align-self: center;
|
||||
cursor: pointer;
|
||||
color: var(--scroll-button-color);
|
||||
transition: color .3s var(--bezier);
|
||||
`, [
|
||||
cM('left', {
|
||||
marginRight: '8px'
|
||||
}),
|
||||
cM('right', {
|
||||
marginLeft: '8px'
|
||||
}),
|
||||
cM('disabled', {
|
||||
cursor: 'not-allowed',
|
||||
color: 'var(--scroll-button-color-disabled)'
|
||||
})
|
||||
])
|
||||
c('&::-webkit-scrollbar', `
|
||||
width: 0;
|
||||
height: 0;
|
||||
`)
|
||||
]),
|
||||
cB('tabs-label-wrapper', `
|
||||
display: inline-block;
|
||||
@ -128,11 +107,6 @@ export default cB('tabs', `
|
||||
background-color .3s var(--bezier);
|
||||
`),
|
||||
cM('line-type', [
|
||||
cB('tabs-nav', [
|
||||
cB('tabs-nav-scroll-button', `
|
||||
padding-bottom: 4px;
|
||||
`)
|
||||
]),
|
||||
cB('tabs-label', `
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 2px;
|
||||
@ -166,16 +140,6 @@ export default cB('tabs', `
|
||||
border-top: 1px solid var(--tab-border-color);
|
||||
border-bottom: 1px solid var(--tab-border-color);
|
||||
`, [
|
||||
cB('tabs-nav-scroll-button', [
|
||||
cM('left', `
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
`),
|
||||
cM('right', `
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
`)
|
||||
]),
|
||||
cB('tabs-label-bar', `
|
||||
bottom: 0;
|
||||
border-radius: 0;
|
@ -1,7 +1,8 @@
|
||||
import sizeVariables from './_common'
|
||||
import { commonDark } from '../../_styles/new-common'
|
||||
import type { TabsTheme } from './light'
|
||||
|
||||
export default {
|
||||
const tabsDark: TabsTheme = {
|
||||
name: 'Tabs',
|
||||
common: commonDark,
|
||||
self (vars) {
|
||||
@ -9,8 +10,6 @@ export default {
|
||||
textColor2Overlay,
|
||||
primaryColor,
|
||||
textColorDisabledOverlay,
|
||||
iconColorOverlay,
|
||||
iconColorDisabledOverlay,
|
||||
closeColorOverlay,
|
||||
closeColorHoverOverlay,
|
||||
closeColorPressedOverlay,
|
||||
@ -29,8 +28,6 @@ export default {
|
||||
labelTextColorHover: primaryColor,
|
||||
labelTextColorDisabled: textColorDisabledOverlay,
|
||||
labelBarColor: primaryColor,
|
||||
scrollButtonColor: iconColorOverlay,
|
||||
scrollButtonColorDisabled: iconColorDisabledOverlay,
|
||||
closeColor: closeColorOverlay,
|
||||
closeColorHover: closeColorHoverOverlay,
|
||||
closeColorPressed: closeColorPressedOverlay,
|
||||
@ -45,3 +42,5 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default tabsDark
|
@ -1,2 +0,0 @@
|
||||
export { default as tabsDark } from './dark.js'
|
||||
export { default as tabsLight } from './light.js'
|
3
src/tabs/styles/index.ts
Normal file
3
src/tabs/styles/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { default as tabsDark } from './dark'
|
||||
export { default as tabsLight } from './light'
|
||||
export type { TabsTheme, TabsThemeVars } from './light'
|
@ -1,48 +0,0 @@
|
||||
import sizeVariables from './_common'
|
||||
import { commonLight } from '../../_styles/new-common'
|
||||
|
||||
export default {
|
||||
name: 'Tabs',
|
||||
common: commonLight,
|
||||
self (vars) {
|
||||
const {
|
||||
textColor2,
|
||||
primaryColor,
|
||||
textColorDisabled,
|
||||
iconColorOverlay,
|
||||
iconColorDisabledOverlay,
|
||||
closeColor,
|
||||
closeColorHover,
|
||||
closeColorPressed,
|
||||
tabColorOverlay,
|
||||
borderColor,
|
||||
textColor1,
|
||||
dividerColorOverlay,
|
||||
fontWeightStrong,
|
||||
borderRadius,
|
||||
fontSize
|
||||
} = vars
|
||||
return {
|
||||
...sizeVariables,
|
||||
labelFontSizeCard: fontSize,
|
||||
labelTextColor: textColor2,
|
||||
labelTextColorActive: primaryColor,
|
||||
labelTextColorHover: primaryColor,
|
||||
labelTextColorDisabled: textColorDisabled,
|
||||
labelBarColor: primaryColor,
|
||||
scrollButtonColor: iconColorOverlay,
|
||||
scrollButtonColorDisabled: iconColorDisabledOverlay,
|
||||
closeColor: closeColor,
|
||||
closeColorHover: closeColorHover,
|
||||
closeColorPressed: closeColorPressed,
|
||||
tabColor: tabColorOverlay,
|
||||
tabBorderColorActive: borderColor,
|
||||
tabTextColor: textColor2,
|
||||
tabTextColorActive: textColor1,
|
||||
tabBorderColor: dividerColorOverlay,
|
||||
tabFontWeight: fontWeightStrong,
|
||||
tabBorderRadius: borderRadius,
|
||||
paneTextColor: textColor2
|
||||
}
|
||||
}
|
||||
}
|
53
src/tabs/styles/light.ts
Normal file
53
src/tabs/styles/light.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import sizeVariables from './_common'
|
||||
import { commonLight } from '../../_styles/new-common'
|
||||
import type { ThemeCommonVars } from '../../_styles/new-common'
|
||||
import { Theme } from '../../_mixins'
|
||||
|
||||
const self = (vars: ThemeCommonVars) => {
|
||||
const {
|
||||
textColor2,
|
||||
primaryColor,
|
||||
textColorDisabled,
|
||||
closeColor,
|
||||
closeColorHover,
|
||||
closeColorPressed,
|
||||
tabColorOverlay,
|
||||
borderColor,
|
||||
textColor1,
|
||||
dividerColorOverlay,
|
||||
fontWeightStrong,
|
||||
borderRadius,
|
||||
fontSize
|
||||
} = vars
|
||||
return {
|
||||
...sizeVariables,
|
||||
labelFontSizeCard: fontSize,
|
||||
labelTextColor: textColor2,
|
||||
labelTextColorActive: primaryColor,
|
||||
labelTextColorHover: primaryColor,
|
||||
labelTextColorDisabled: textColorDisabled,
|
||||
labelBarColor: primaryColor,
|
||||
closeColor: closeColor,
|
||||
closeColorHover: closeColorHover,
|
||||
closeColorPressed: closeColorPressed,
|
||||
tabColor: tabColorOverlay,
|
||||
tabBorderColorActive: borderColor,
|
||||
tabTextColor: textColor2,
|
||||
tabTextColorActive: textColor1,
|
||||
tabBorderColor: dividerColorOverlay,
|
||||
tabFontWeight: fontWeightStrong,
|
||||
tabBorderRadius: borderRadius,
|
||||
paneTextColor: textColor2
|
||||
}
|
||||
}
|
||||
|
||||
export type TabsThemeVars = ReturnType<typeof self>
|
||||
|
||||
const tabsLight: Theme<TabsThemeVars> = {
|
||||
name: 'Tabs',
|
||||
common: commonLight,
|
||||
self
|
||||
}
|
||||
|
||||
export default tabsLight
|
||||
export type TabsTheme = typeof tabsLight
|
Loading…
Reference in New Issue
Block a user