import React, { ReactNode, useState, useEffect, useMemo, CSSProperties, isValidElement, cloneElement, ReactElement, HTMLAttributes } from 'react' import classnames from 'classnames' import partition from 'lodash/partition' import { tabsContext, TabsContextValue } from './hook' import { concat } from '../utils' import { usePrefix, ComponentSize } from '../ConfigProvider' import TabNavUnderscore from './TabNavUnderscore' import TabPane, { TabPaneProps } from './TabPane' import { useValue } from '../utils/hooks' import { TabNav, TabNavProps } from './TabNav' import { Tab, TabValue } from './types' import { WithRef } from '../utils/type' export { default as TabPane } from './TabPane' export type { TabPaneProps } from './TabPane' export * from './TabNav' export * from './types' export type TabsType = 'horizontal' | 'vertical' | 'button' export type IndicatorStyle = 'fixed' | 'dynamic' export type TabsProps = { /** * Tabs 的类型 * * @default 'horizontal' */ type?: TabsType /** * 尺寸 * * @default 'default' */ size?: ComponentSize /** * 指示器样式 (选中 tab 时的文字下方的横线),仅在 type 为 horizontal 时有效 * * @default 'fixed' */ indicatorStyle?: IndicatorStyle children: ReactNode /** 对应 TabPane 的 value */ value?: T /** * @default 第一个面板 */ defaultValue?: T /** 切换 TabPane 时候的回调 */ onChange?: (value: T) => void /** 导航区域额外的区域 */ extra?: ReactNode className?: string style?: CSSProperties /** * 根元素的 html 属性 */ rootHtmlProps?: WithRef, HTMLDivElement> } export default function Tabs(props: TabsProps) { const { children, className, defaultValue, type = 'horizontal', indicatorStyle = 'fixed', value: propValue, size = 'default', style, onChange, rootHtmlProps, extra } = props const prefix = usePrefix('tabs') const sizePrefix = concat(prefix, size) const typePrefix = concat(prefix, type) const navPrefix = concat(prefix, 'nav') const [tabs, setTabs] = useState>>(getTabsFromChildren(children)) const [activeTabNavDom, setActiveTabNavDom] = useState(null) const { value: activeValue, setValue: setActiveValue } = useValue({ valueFromProps: propValue, defaultValue: defaultValue ?? tabs[0].value, onChange }) const classname = classnames( prefix, className, sizePrefix, typePrefix, rootHtmlProps?.className ) const tabNavItems = tabs.map(({ name, value }) => (
  • value === activeValue && setActiveTabNavDom(dom)} className={classnames(concat(navPrefix, 'item'), value === activeValue && concat(navPrefix, 'item', 'active'))} onClick={() => setActiveValue(value)} > {name}
  • )) const contextValue = useMemo>( () => ({ prefix, size, type, indicatorStyle, tabs, value: activeValue }), [prefix, size, type, indicatorStyle, tabs, activeValue] ) useEffect(() => { setTabs(getTabsFromChildren(children)) }, [children]) // 如果没有 value 和 defaultValue,找出第一个面板的 value 作为内部 value useEffect(() => { if (activeValue == null && tabs.length > 0) { setActiveValue(tabs[0].value) } }, [activeValue, setActiveValue, tabs]) // 检查是否有 TabPane,如果有,则 nav 会有下边框 const hasTabPane = useMemo( () => React.Children.toArray(children) .filter(isValidElement) .some(child => child.type === TabPane), [children] ) return (
      {tabNavItems} {extra &&
    • {extra}
    • } {(type === 'horizontal' || type === 'vertical') && hasTabPane &&
    • }
    {React.Children.map(children, child => { if (!isValidElement(child)) { return null } return cloneElement>(child) })}
    ) } type AvailableChild = ReactElement> | ReactElement> function getTabsFromChildren(children: ReactNode): Array> { const arr = React.Children.toArray(children).filter(isValidChild) as Array> const [tabPanes, tabNavs] = partition, ReactElement>>(arr, isTabPane) if (tabPanes.length > 0 && tabNavs.length > 0) { throw Error('Unexpected mix of `TabPane` and `TabNav`') } if (tabPanes.length > 0) { return tabPanes.map(tabPane => ({ name: tabPane.props.name, value: tabPane.props.value })) } return tabNavs.map(tabNav => ({ name: tabNav.props.children, value: tabNav.props.value })) } function isTabNav(child: ReactElement): child is ReactElement> { return child.type === TabNav } function isTabPane(child: ReactElement): child is ReactElement> { return child.type === TabPane } function isValidChild(child: ReactNode) : child is ReactElement> | ReactElement> { if (!isValidElement(child)) { return false } if (isTabNav(child)) { return true } if (isTabPane(child)) { return true } throw TypeError('Unsupported Element Type, component as Tabs\'s children should be TabPane or TabNav.') }