From 1c85ec61ca5485e7398ff2a74bc8bce38b396f51 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Sun, 24 Jan 2021 00:22:53 +0800 Subject: [PATCH] refactor(tabs): ts --- src/global.d.ts | 2 +- src/tabs/index.js | 2 - src/tabs/index.ts | 2 + src/tabs/src/TabPane.js | 58 --- src/tabs/src/TabPane.ts | 70 ++++ src/tabs/src/Tabs.tsx | 383 +++++++++++++++++ src/tabs/src/Tabs.vue | 395 ------------------ .../styles/{index.cssr.js => index.cssr.ts} | 62 +-- src/tabs/styles/{_common.js => _common.ts} | 0 src/tabs/styles/{dark.js => dark.ts} | 9 +- src/tabs/styles/index.js | 2 - src/tabs/styles/index.ts | 3 + src/tabs/styles/light.js | 48 --- src/tabs/styles/light.ts | 53 +++ 14 files changed, 529 insertions(+), 560 deletions(-) delete mode 100644 src/tabs/index.js create mode 100644 src/tabs/index.ts delete mode 100644 src/tabs/src/TabPane.js create mode 100644 src/tabs/src/TabPane.ts create mode 100644 src/tabs/src/Tabs.tsx delete mode 100644 src/tabs/src/Tabs.vue rename src/tabs/src/styles/{index.cssr.js => index.cssr.ts} (77%) rename src/tabs/styles/{_common.js => _common.ts} (100%) rename src/tabs/styles/{dark.js => dark.ts} (87%) delete mode 100644 src/tabs/styles/index.js create mode 100644 src/tabs/styles/index.ts delete mode 100644 src/tabs/styles/light.js create mode 100644 src/tabs/styles/light.ts diff --git a/src/global.d.ts b/src/global.d.ts index 967268084..9b6792e3b 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -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 {} diff --git a/src/tabs/index.js b/src/tabs/index.js deleted file mode 100644 index d16baf545..000000000 --- a/src/tabs/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default as NTab } from './src/Tabs.vue' -export { default as NTabPane } from './src/TabPane.js' diff --git a/src/tabs/index.ts b/src/tabs/index.ts new file mode 100644 index 000000000..454e3df36 --- /dev/null +++ b/src/tabs/index.ts @@ -0,0 +1,2 @@ +export { default as NTab } from './src/Tabs' +export { default as NTabPane } from './src/TabPane' diff --git a/src/tabs/src/TabPane.js b/src/tabs/src/TabPane.js deleted file mode 100644 index e8b5b2384..000000000 --- a/src/tabs/src/TabPane.js +++ /dev/null @@ -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 - } -}) diff --git a/src/tabs/src/TabPane.ts b/src/tabs/src/TabPane.ts new file mode 100644 index 000000000..c071b58dc --- /dev/null +++ b/src/tabs/src/TabPane.ts @@ -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, + name: { + type: [String, Number] as PropType, + required: true + }, + disabled: { + type: Boolean, + default: false + }, + displayDirective: { + type: String as PropType<'if' | 'show'>, + default: 'if' + } +} as const + +export type TabPaneProps = ExtractPropTypes + +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('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 + } +}) diff --git a/src/tabs/src/Tabs.tsx b/src/tabs/src/Tabs.tsx new file mode 100644 index 000000000..d7f0ae163 --- /dev/null +++ b/src/tabs/src/Tabs.tsx @@ -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), + value: [String, Number] as PropType, + defaultValue: { + type: [String, Number] as PropType, + 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, + onScrollableChange: Function as PropType< + MaybeArray<(value: boolean) => void> + >, + // eslint-disable-next-line vue/prop-name-casing + 'onUpdate:value': Function as PropType< + MaybeArray<(value: T) => void> + >, + onClose: Function as PropType void>>, + // deprecated + activeName: { + type: [String, Number] as PropType, + validator: () => { + if (__DEV__) { + warn( + 'tabs', + '`active-name` is deprecated, please use `value` instead.' + ) + } + return true + }, + default: undefined + }, + onActiveNameChange: { + type: Function as PropType< + MaybeArray<(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(null) + const labelWrapperRef = ref(null) + const labelBarRef = ref(null) + + let preventYWheel = false + + const panelsRef = ref([]) + 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( + '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 ( +
+ + {{ + default: () => ( +
+ + {{ + default: () => ( +
+
+ {this.panels.map((panel, i) => ( +
+ this.handleTabClick( + e, + panel.name, + panel.disabled + ) + } + > + + {panel.label} + + {this.closable && this.type === 'card' ? ( + + this.handleCloseClick(e, panel) + } + /> + ) : null} +
+ ))} +
+ {this.type === 'line' ? ( +
+ ) : null} +
+ ) + }} + +
+ ) + }} +
+ {renderSlot(this.$slots, 'default')} +
+ ) + } +}) diff --git a/src/tabs/src/Tabs.vue b/src/tabs/src/Tabs.vue deleted file mode 100644 index 747bfa046..000000000 --- a/src/tabs/src/Tabs.vue +++ /dev/null @@ -1,395 +0,0 @@ - - - diff --git a/src/tabs/src/styles/index.cssr.js b/src/tabs/src/styles/index.cssr.ts similarity index 77% rename from src/tabs/src/styles/index.cssr.js rename to src/tabs/src/styles/index.cssr.ts index fcb9b78df..5579cb753 100644 --- a/src/tabs/src/styles/index.cssr.js +++ b/src/tabs/src/styles/index.cssr.ts @@ -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; diff --git a/src/tabs/styles/_common.js b/src/tabs/styles/_common.ts similarity index 100% rename from src/tabs/styles/_common.js rename to src/tabs/styles/_common.ts diff --git a/src/tabs/styles/dark.js b/src/tabs/styles/dark.ts similarity index 87% rename from src/tabs/styles/dark.js rename to src/tabs/styles/dark.ts index 5b014984e..76601eb77 100644 --- a/src/tabs/styles/dark.js +++ b/src/tabs/styles/dark.ts @@ -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 diff --git a/src/tabs/styles/index.js b/src/tabs/styles/index.js deleted file mode 100644 index 706a0cca6..000000000 --- a/src/tabs/styles/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default as tabsDark } from './dark.js' -export { default as tabsLight } from './light.js' diff --git a/src/tabs/styles/index.ts b/src/tabs/styles/index.ts new file mode 100644 index 000000000..ac40ee465 --- /dev/null +++ b/src/tabs/styles/index.ts @@ -0,0 +1,3 @@ +export { default as tabsDark } from './dark' +export { default as tabsLight } from './light' +export type { TabsTheme, TabsThemeVars } from './light' diff --git a/src/tabs/styles/light.js b/src/tabs/styles/light.js deleted file mode 100644 index 89313a83e..000000000 --- a/src/tabs/styles/light.js +++ /dev/null @@ -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 - } - } -} diff --git a/src/tabs/styles/light.ts b/src/tabs/styles/light.ts new file mode 100644 index 000000000..ebb691243 --- /dev/null +++ b/src/tabs/styles/light.ts @@ -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 + +const tabsLight: Theme = { + name: 'Tabs', + common: commonLight, + self +} + +export default tabsLight +export type TabsTheme = typeof tabsLight