import { Children, cloneElement, CSSProperties, forwardRef, ReactElement, ReactNode, useEffect, useRef, useState, useImperativeHandle, ForwardRefExoticComponent, Fragment, ExoticComponent, } from 'react' import classNames from 'classnames' import { CommonComponentProps } from '../../utils/types' import { useMapSet, useScroll, useSetTimeout, useEvent } from '../../use' import './Tabs.scss' import { Swiper, SwiperItem, SwiperProps, SwiperRef } from '../swiper/Swiper' import { TabPane, TabPaneProps } from './TabPane' import { TabLabel } from './TabLabel' import { matchScrollVisible } from '../../utils' import { pageScrollTop } from '../../utils/dom' export * from './TabPane' export interface TabsImperativeHandle { scrollTo(name: any, animated?: boolean): void } export interface TabsProps extends CommonComponentProps { className?: string style?: CSSProperties children?: ReactNode defaultActiveName?: any activeName?: any onChange?: (name: any) => void onLabelClick?: (name: any) => void scrollCount?: number type?: 'line' | 'card' | 'pill' | 'border' headerClass?: string headerStyle?: CSSProperties bodyClass?: string bodyStyle?: CSSProperties wrapperClass?: string wrapperStyle?: CSSProperties labelStyle?: CSSProperties labelClass?: string activeLabelStyle?: CSSProperties line?: ReactNode lineWidth?: string lineStyle?: CSSProperties sticky?: boolean prepend?: ReactNode append?: ReactNode animated?: boolean swipeable?: boolean swiperProps?: SwiperProps scrollspy?: boolean offset?: number vertical?: boolean } export interface Tabs extends ForwardRefExoticComponent { Pane: TabPane } export const Tabs = forwardRef( (props, ref) => { const { className, children, defaultActiveName, activeName, onChange, onLabelClick, scrollCount = 5, type = 'line', headerClass, headerStyle, bodyClass, bodyStyle, wrapperClass, wrapperStyle, labelStyle, labelClass, activeLabelStyle, line, lineWidth, lineStyle, sticky, prepend, append, animated = false, swipeable = false, swiperProps, scrollspy, offset = 0, vertical = false, ...restProps } = props const [innerName, setInnerName] = useState(() => { const firstPane = Children.toArray( children )[0] as ReactElement return activeName ?? defaultActiveName ?? firstPane?.props.name ?? 0 }) const wrapperRef = useRef() const swiperRef = useRef() const labelSet = useMapSet([]) // 受控 useEffect(() => { if (!scrollspy && activeName != null) { setInnerName(activeName) } }, [activeName]) useEffect(() => { swiperRef.current?.swipeTo(innerName) const label = labelSet.get(innerName) if (wrapperRef.current && label) { wrapperRef.current.scrollTo({ left: label.offsetLeft - (wrapperRef.current.offsetWidth - label.offsetWidth) / 2, behavior: 'smooth', }) } }, [innerName]) const handleSwiperChange = useEvent((name: any) => { setInnerName(name) onChange?.(name) }) const paneSet = useMapSet([]) const scrollLock = useRef(false) const { reset } = useSetTimeout(() => { scrollLock.current = false }, 500) const scrollTo = (name: any, animated = true) => { if (!scrollspy) { return } const el = paneSet.get(name) if (el) { const top = el.getBoundingClientRect().top + window.scrollY - offset reset() scrollLock.current = true pageScrollTop(top, animated) } } const switchTo = (name: any, animated?: boolean) => { if (name !== innerName) { // 非受控 if (scrollspy || activeName == null) { setInnerName(name) } onChange?.(name) } scrollTo(name, animated) } const handleLabelClick = (name: any) => { onLabelClick?.(name) switchTo(name, true) } useScroll( () => { if (!scrollspy || scrollLock.current) { return } const srcData = paneSet.getData() matchScrollVisible( srcData.map((item) => item[1]), (index) => { setInnerName(srcData[index][0]) }, offset ) }, 150, { leading: false, } ) useImperativeHandle(ref, () => ({ scrollTo(name: any, animated?: boolean) { switchTo(name, animated) }, })) const tabsClass = classNames( 's-tab', 's-tab-' + type, { 's-tab-auto': Children.count(children) > scrollCount, 's-tab-sticky': sticky, 's-tab-is-swiper': animated || swipeable, 's-tab-scrollspy': scrollspy, 's-tab-vertical': vertical, }, className ) const renderPane = (Comp: SwiperItem | ExoticComponent) => { return Children.map( children as ReactElement, (pane: ReactElement, index: number) => { const name = pane.props.name ?? index return ( {cloneElement(pane, { key: name, name, activeName: innerName, ref: (el: any) => paneSet.set(name, el), })} ) } ) } return (
{prepend &&
{prepend}
}
{Children.map( children as ReactElement, (pane: ReactElement, index: any) => { const name = pane.props.name ?? index return ( labelSet.set(name, el)} showLine={type === 'card' || type === 'line'} line={line} lineWidth={lineWidth} lineStyle={lineStyle} onClick={handleLabelClick} > {pane.props.label} ) } )}
{append &&
{append}
}
{animated || swipeable ? ( {renderPane(SwiperItem)} ) : ( renderPane(Fragment) )}
) } ) as Tabs Tabs.Pane = TabPane export default Tabs