diff --git a/src/collapse-item/index.tsx b/src/collapse-item/index.tsx index 7181ab08b..a8c72effa 100644 --- a/src/collapse-item/index.tsx +++ b/src/collapse-item/index.tsx @@ -33,7 +33,7 @@ export default createComponent({ if (!parent) { if (process.env.NODE_ENV !== 'production') { console.error( - '[Vant] CollapseItem must be a child component of Collapse.' + '[Vant] must be a child component of .' ); } return; diff --git a/src/dropdown-item/index.tsx b/src/dropdown-item/index.tsx index 33659233d..b25c6138b 100644 --- a/src/dropdown-item/index.tsx +++ b/src/dropdown-item/index.tsx @@ -58,7 +58,7 @@ export default createComponent({ if (!parent) { if (process.env.NODE_ENV !== 'production') { console.error( - '[Vant] DropdownItem must be a child component of DropdownMenu.' + '[Vant] must be a child component of .' ); } return; diff --git a/src/grid-item/index.tsx b/src/grid-item/index.tsx index a3c4a6b15..3efdcae32 100644 --- a/src/grid-item/index.tsx +++ b/src/grid-item/index.tsx @@ -31,7 +31,7 @@ export default createComponent({ if (!parent) { if (process.env.NODE_ENV !== 'production') { - console.error('[Vant] GridItem must be a child component of Grid.'); + console.error('[Vant] must be a child component of .'); } return; } diff --git a/src/index-anchor/index.tsx b/src/index-anchor/index.tsx index 7e1b400e3..5ca88e2cd 100644 --- a/src/index-anchor/index.tsx +++ b/src/index-anchor/index.tsx @@ -32,7 +32,7 @@ export default createComponent({ if (!parent) { if (process.env.NODE_ENV !== 'production') { console.error( - '[Vant] IndexAnchor must be a child component of IndexBar.' + '[Vant] must be a child component of .' ); } return; diff --git a/src/sidebar-item/index.tsx b/src/sidebar-item/index.tsx index 3275f1878..fadf6373f 100644 --- a/src/sidebar-item/index.tsx +++ b/src/sidebar-item/index.tsx @@ -24,7 +24,7 @@ export default createComponent({ if (!parent) { if (process.env.NODE_ENV !== 'production') { console.error( - '[Vant] SidebarItem must be a child component of Sidebar.' + '[Vant] must be a child component of .' ); } return; diff --git a/src/step/index.tsx b/src/step/index.tsx index 00a0c81b6..9c61a91e3 100644 --- a/src/step/index.tsx +++ b/src/step/index.tsx @@ -19,7 +19,7 @@ export default createComponent({ if (!parent) { if (process.env.NODE_ENV !== 'production') { - console.error('[Vant] Step must be a child component of Steps.'); + console.error('[Vant] must be a child component of .'); } return; } diff --git a/src/swipe-item/index.tsx b/src/swipe-item/index.tsx index aa2262fb0..7c37527b4 100644 --- a/src/swipe-item/index.tsx +++ b/src/swipe-item/index.tsx @@ -19,7 +19,9 @@ export default createComponent({ if (!parent) { if (process.env.NODE_ENV !== 'production') { - console.error('[Vant] SwipeItem must be a child component of Swipe.'); + console.error( + '[Vant] must be a child component of .' + ); } return; } diff --git a/src/tab/README.md b/src/tab/README.md index 1bc4bfe0f..fff224c05 100644 --- a/src/tab/README.md +++ b/src/tab/README.md @@ -267,11 +267,11 @@ export default { | Event | Description | Arguments | | --- | --- | --- | -| click | Emitted when a tab is clicked | name,title | -| change | Emitted when active tab changed | name,title | -| disabled | Emitted when a disabled tab is clicked | name,title | -| rendered | Emitted when content first rendered in lazy-render mode | name,title | -| scroll | Emitted when tab scrolling in sticky mode | object: { scrollTop, isFixed } | +| click | Emitted when a tab is clicked | _name: string \| number, title: string_ | +| change | Emitted when active tab changed | _name: string \| number, title: string_ | +| disabled | Emitted when a disabled tab is clicked | _name: string \| number, title: string_ | +| rendered | Emitted when content first rendered in lazy-render mode | _name: string \| number, title: string_ | +| scroll | Emitted when tab scrolling in sticky mode | _{ scrollTop: number, isFixed: boolean }_ | ### Tabs Methods @@ -280,7 +280,7 @@ Use [ref](https://v3.vuejs.org/guide/component-template-refs.html) to get Tabs i | Name | Description | Attribute | Return value | | --- | --- | --- | --- | | resize | Resize Tabs when container element resized or visibility changed | - | - | -| scrollTo | Go to specified tab in scrollspy mode | name | - | +| scrollTo | Go to specified tab in scrollspy mode | _name: string \| number_ | - | ### Tabs Slots diff --git a/src/tab/README.zh-CN.md b/src/tab/README.zh-CN.md index 7a704937b..71b072063 100644 --- a/src/tab/README.zh-CN.md +++ b/src/tab/README.zh-CN.md @@ -278,11 +278,11 @@ export default { | 事件名 | 说明 | 回调参数 | | --- | --- | --- | -| click | 点击标签时触发 | name:标识符,title:标题 | -| change | 当前激活的标签改变时触发 | name:标识符,title:标题 | -| disabled | 点击被禁用的标签时触发 | name:标识符,title:标题 | -| rendered | 标签内容首次渲染时触发(仅在开启延迟渲染后触发) | name:标识符,title:标题 | -| scroll | 滚动时触发,仅在 sticky 模式下生效 | { scrollTop: 距离顶部位置, isFixed: 是否吸顶 } | +| click | 点击标签时触发 | _name: string \| number, title: string_ | +| change | 当前激活的标签改变时触发 | _name: string \| number, title: string_ | +| disabled | 点击被禁用的标签时触发 | _name: string \| number, title: string_ | +| rendered | 标签内容首次渲染时触发(仅在开启延迟渲染后触发) | _name: string \| number, title: string_ | +| scroll | 滚动时触发,仅在 sticky 模式下生效 | _{ scrollTop: number, isFixed: boolean }_ | ### Tabs 方法 @@ -291,7 +291,7 @@ export default { | 方法名 | 说明 | 参数 | 返回值 | | --- | --- | --- | --- | | resize | 外层元素大小或组件显示状态变化时,可以调用此方法来触发重绘 | - | - | -| scrollTo | 滚动到指定的标签页,在滚动导航模式下可用 | name: 标识符 | - | +| scrollTo | 滚动到指定的标签页,在滚动导航模式下可用 | _name: string \| number_ | - | ### Tabs Slots diff --git a/src/tab/index.js b/src/tab/index.tsx similarity index 80% rename from src/tab/index.js rename to src/tab/index.tsx index eac84aa5d..be204b8da 100644 --- a/src/tab/index.js +++ b/src/tab/index.tsx @@ -1,6 +1,6 @@ -import { ref, watch, nextTick } from 'vue'; +import { ref, watch, nextTick, PropType, CSSProperties } from 'vue'; import { createNamespace, UnknownProp } from '../utils'; -import { TABS_KEY } from '../tabs'; +import { TABS_KEY, TabsProvide } from '../tabs'; // Composition import { useParent } from '@vant/use'; @@ -20,15 +20,18 @@ export default createComponent({ title: String, disabled: Boolean, titleClass: UnknownProp, - titleStyle: null, + titleStyle: [String, Object] as PropType, }, setup(props, { slots }) { const inited = ref(false); - const { parent, index } = useParent(TABS_KEY); + const { parent, index } = useParent(TABS_KEY); if (!parent) { - throw new Error('[Vant] Tabs: must be used inside '); + if (process.env.NODE_ENV !== 'production') { + console.error('[Vant] must be a child component of .'); + } + return; } const getName = () => props.name ?? index.value; @@ -38,7 +41,7 @@ export default createComponent({ if (parent.props.lazyRender) { nextTick(() => { - parent.emit('rendered', getName(), props.title); + parent.onRendered(getName(), props.title); }); } }; diff --git a/src/tabbar-item/index.tsx b/src/tabbar-item/index.tsx index 11aa73fd6..9a8c6a40f 100644 --- a/src/tabbar-item/index.tsx +++ b/src/tabbar-item/index.tsx @@ -33,7 +33,9 @@ export default createComponent({ if (!parent) { if (process.env.NODE_ENV !== 'production') { - console.error('[Vant] TabbarItem must be a child component of Tabbar.'); + console.error( + '[Vant] must be a child component of .' + ); } return; } diff --git a/src/tabs/index.js b/src/tabs/index.tsx similarity index 81% rename from src/tabs/index.js rename to src/tabs/index.tsx index 1b77ec258..88e01f285 100644 --- a/src/tabs/index.js +++ b/src/tabs/index.tsx @@ -1,4 +1,15 @@ -import { ref, watch, computed, reactive, nextTick, onActivated } from 'vue'; +import { + ref, + watch, + computed, + reactive, + nextTick, + PropType, + ComputedRef, + onActivated, + CSSProperties, + ComponentPublicInstance, +} from 'vue'; // Utils import { @@ -11,10 +22,11 @@ import { createNamespace, getVisibleHeight, setRootScrollTop, + ComponentInstance, } from '../utils'; import { scrollLeftTo, scrollTopTo } from './utils'; import { BORDER_TOP_BOTTOM } from '../utils/constant'; -import { callInterceptor } from '../utils/interceptor'; +import { callInterceptor, Interceptor } from '../utils/interceptor'; // Composition import { @@ -24,7 +36,7 @@ import { useEventListener, onMountedOrActivated, } from '@vant/use'; -import { route } from '../composables/use-route'; +import { route, RouteProps } from '../composables/use-route'; import { useRefs } from '../composables/use-refs'; import { useExpose } from '../composables/use-expose'; @@ -37,6 +49,21 @@ const [createComponent, bem] = createNamespace('tabs'); export const TABS_KEY = 'vanTabs'; +export type TabType = 'line' | 'card'; + +export type TabsProvide = { + props: { + animated?: boolean; + swipeable?: boolean; + scrollspy?: boolean; + lazyRender: boolean; + }; + setLine: () => void; + onRendered: (name: string | number, title?: string) => void; + scrollIntoView: (immediate?: boolean) => void; + currentName: ComputedRef; +}; + export default createComponent({ props: { color: String, @@ -48,11 +75,11 @@ export default createComponent({ background: String, lineWidth: [Number, String], lineHeight: [Number, String], - beforeChange: Function, + beforeChange: Function as PropType, titleActiveColor: String, titleInactiveColor: String, type: { - type: String, + type: String as PropType, default: 'line', }, active: { @@ -84,18 +111,18 @@ export default createComponent({ emits: ['click', 'change', 'scroll', 'disabled', 'rendered', 'update:active'], setup(props, { emit, slots }) { - let tabHeight; - let lockScroll; - let stickyFixed; + let tabHeight: number; + let lockScroll: boolean; + let stickyFixed: boolean; - const root = ref(); - const navRef = ref(); - const wrapRef = ref(); + const root = ref(); + const navRef = ref(); + const wrapRef = ref(); const windowSize = useWindowSize(); const scroller = useScrollParent(root); - const [titleRefs, setTitleRefs] = useRefs(); - const { children, linkChildren } = useChildren(TABS_KEY); + const [titleRefs, setTitleRefs] = useRefs(); + const { children, linkChildren } = useChildren(TABS_KEY); const state = reactive({ inited: false, @@ -103,7 +130,7 @@ export default createComponent({ currentIndex: -1, lineStyle: { backgroundColor: props.color, - }, + } as CSSProperties, }); // whether the nav is scrollable @@ -116,7 +143,10 @@ export default createComponent({ background: props.background, })); - const getTabName = (tab, index) => tab.name ?? index; + const getTabName = ( + tab: ComponentInstance, + index: number + ): number | string => tab.name ?? index; const currentName = computed(() => { const activeTab = children[state.currentIndex]; @@ -136,11 +166,11 @@ export default createComponent({ }); // scroll active tab into view - const scrollIntoView = (immediate) => { + const scrollIntoView = (immediate?: boolean) => { const nav = navRef.value; const titles = titleRefs.value; - if (!scrollable.value || !titles || !titles[state.currentIndex]) { + if (!scrollable.value || !nav || !titles || !titles[state.currentIndex]) { return; } @@ -161,7 +191,7 @@ export default createComponent({ !titles || !titles[state.currentIndex] || props.type !== 'line' || - isHidden(root.value) + isHidden(root.value!) ) { return; } @@ -170,7 +200,7 @@ export default createComponent({ const { lineWidth, lineHeight } = props; const left = title.offsetLeft + title.offsetWidth / 2; - const lineStyle = { + const lineStyle: CSSProperties = { width: addUnit(lineWidth), backgroundColor: props.color, transform: `translateX(${left}px) translateX(-50%)`, @@ -190,7 +220,7 @@ export default createComponent({ }); }; - const findAvailableTab = (index) => { + const findAvailableTab = (index: number) => { const diff = index < state.currentIndex ? -1 : 1; while (index >= 0 && index < children.length) { @@ -202,7 +232,7 @@ export default createComponent({ } }; - const setCurrentIndex = (currentIndex) => { + const setCurrentIndex = (currentIndex: number) => { const newIndex = findAvailableTab(currentIndex); if (!isDef(newIndex)) { @@ -225,7 +255,7 @@ export default createComponent({ }; // correct the index of active tab - const setCurrentIndexByName = (name) => { + const setCurrentIndexByName = (name: number | string) => { const matched = children.filter( (tab, index) => getTabName(tab, index) === name ); @@ -238,7 +268,7 @@ export default createComponent({ if (props.scrollspy) { const target = children[state.currentIndex].$el; - if (target) { + if (target && scroller.value) { const to = getElementTop(target, scroller.value) - scrollOffset.value; lockScroll = true; @@ -255,7 +285,7 @@ export default createComponent({ }; // emit event when clicked - const onClick = (item, index) => { + const onClick = (item: ComponentInstance, index: number) => { const { title, disabled } = children[index]; const name = getTabName(children[index], index); @@ -272,16 +302,19 @@ export default createComponent({ }); emit('click', name, title); - route(item); + route(item as ComponentPublicInstance); } }; - const onStickyScroll = (params) => { + const onStickyScroll = (params: { + isFixed: boolean; + scrollTop: number; + }) => { stickyFixed = params.isFixed; emit('scroll', params); }; - const scrollTo = (name) => { + const scrollTo = (name: number | string) => { nextTick(() => { setCurrentIndexByName(name); scrollToCurrentContent(true); @@ -390,7 +423,7 @@ export default createComponent({ // scroll to correct position if (stickyFixed && !props.scrollspy) { setRootScrollTop( - Math.ceil(getElementTop(root.value) - offsetTopPx.value) + Math.ceil(getElementTop(root.value!) - offsetTopPx.value) ); } } @@ -400,11 +433,15 @@ export default createComponent({ setCurrentIndexByName(props.active); nextTick(() => { state.inited = true; - tabHeight = getVisibleHeight(wrapRef.value); + tabHeight = getVisibleHeight(wrapRef.value!); scrollIntoView(true); }); }; + const onRendered = (name: string | number, title?: string) => { + emit('rendered', name, title); + }; + useExpose({ resize: setLine, scrollTo, @@ -415,9 +452,9 @@ export default createComponent({ useEventListener('scroll', onScroll, { target: scroller.value }); linkChildren({ - emit, props, setLine, + onRendered, currentName, scrollIntoView, }); diff --git a/src/tabs/utils.ts b/src/tabs/utils.ts index 2477dd899..aaee7e228 100644 --- a/src/tabs/utils.ts +++ b/src/tabs/utils.ts @@ -1,5 +1,5 @@ import { raf, cancelRaf } from '@vant/use'; -import { getScrollTop, setScrollTop } from '../utils'; +import { ScrollElement, getScrollTop, setScrollTop } from '../utils'; let rafId: number; @@ -26,7 +26,7 @@ export function scrollLeftTo( } export function scrollTopTo( - scroller: HTMLElement, + scroller: ScrollElement, to: number, duration: number, callback: () => void diff --git a/src/utils/dom/scroll.ts b/src/utils/dom/scroll.ts index 61c8ac8c0..3d2cf6312 100644 --- a/src/utils/dom/scroll.ts +++ b/src/utils/dom/scroll.ts @@ -1,6 +1,6 @@ import { isIOS as checkIsIOS } from '../validate/system'; -type ScrollElement = Element | Window; +export type ScrollElement = Element | Window; function isWindow(val: unknown): val is Window { return val === window; @@ -36,7 +36,7 @@ export function setRootScrollTop(value: number) { } // get distance from element top to page top or scroller top -export function getElementTop(el: ScrollElement, scroller?: HTMLElement) { +export function getElementTop(el: ScrollElement, scroller?: ScrollElement) { if (isWindow(el)) { return 0; } diff --git a/src/vue-tsx-shim.d.ts b/src/vue-tsx-shim.d.ts index 546357ef7..334a49c3a 100644 --- a/src/vue-tsx-shim.d.ts +++ b/src/vue-tsx-shim.d.ts @@ -20,9 +20,10 @@ declare module 'vue' { onPress?: EventHandler; onScale?: EventHandler; onCancel?: EventHandler; - onOpened?: EventHandler; onClosed?: EventHandler; onChange?: EventHandler; + onOpened?: EventHandler; + onScroll?: EventHandler; onSubmit?: EventHandler; onSelect?: EventHandler; onToggle?: EventHandler;