Ant-design 源码分析之数据展示(十二)Tabs
2021SC@SDUSC
一、组件结构
1、ant代码结构
2、组件结构
ant中Tabs的index.tsx中引入了rc-tabs。
二、antd组件调用关系
1、index.tsx
导入相应模块以及相应的ICON图标
import * as React from 'react';
import RcTabs, { TabPane, TabsProps as RcTabsProps, TabPaneProps } from 'rc-tabs';
import { EditableConfig } from 'rc-tabs/lib/interface';
import classNames from 'classnames';
import EllipsisOutlined from '@ant-design/icons/EllipsisOutlined';
import PlusOutlined from '@ant-design/icons/PlusOutlined';
import CloseOutlined from '@ant-design/icons/CloseOutlined';
import devWarning from '../_util/devWarning';
import { ConfigContext } from '../config-provider';
import SizeContext, { SizeType } from '../config-provider/SizeContext';
声明TabsProps接口
export type TabsType = 'line' | 'card' | 'editable-card';
export type TabsPosition = 'top' | 'right' | 'bottom' | 'left';
export { TabPaneProps };
export interface TabsProps extends Omit<RcTabsProps, 'editable'> {
type?: TabsType;
size?: SizeType;
hideAdd?: boolean;
centered?: boolean;
addIcon?: React.ReactNode;
onEdit?: (e: React.MouseEvent | React.KeyboardEvent | string, action: 'add' | 'remove') => void;
}
addIcon:自定义添加按钮,类型为ReactNode
size:大小,提供 large default 和 small 三种大小,类型为string
centered:标签居中展示,类型为boolean
type:页签的基本样式,可选 line、card editable-card 类型,string
hideAdd:是否隐藏加号图标,在 type=“editable-card” 时有效,类型为boolean
onEdit:新增和删除页签的回调,在 type=“editable-card” 时有效,类型为(targetKey, action): void
function Tabs({
type,
className,
size: propSize,
onEdit,
hideAdd,
centered,
addIcon,
...props
}: TabsProps) {
const { prefixCls: customizePrefixCls, moreIcon = <EllipsisOutlined /> } = props;
const { getPrefixCls, direction } = React.useContext(ConfigContext);
const prefixCls = getPrefixCls('tabs', customizePrefixCls);
//可编辑类型处理
let editable: EditableConfig | undefined;
if (type === 'editable-card') {
editable = {
onEdit: (editType, { key, event }) => {
onEdit?.(editType === 'add' ? event : key!, editType);
},
removeIcon: <CloseOutlined />,
addIcon: addIcon || <PlusOutlined />,
showAdd: hideAdd !== true,
};
}
const rootPrefixCls = getPrefixCls();
devWarning(
!('onPrevClick' in props) && !('onNextClick' in props),
'Tabs',
'`onPrevClick` and `onNextClick` has been removed. Please use `onTabScroll` instead.',
);
return (
<SizeContext.Consumer>
{contextSize => {
const size = propSize !== undefined ? propSize : contextSize;
return (
<RcTabs
direction={direction}
moreTransitionName={`${rootPrefixCls}-slide-up`}
{...props}
className={classNames(
{
[`${prefixCls}-${size}`]: size,
[`${prefixCls}-card`]: ['card', 'editable-card'].includes(type as string),
[`${prefixCls}-editable-card`]: type === 'editable-card',
[`${prefixCls}-centered`]: centered,
},
className,
)}
editable={editable}
moreIcon={moreIcon}
prefixCls={prefixCls}
/>
);
}}
</SizeContext.Consumer>
);
}
Tabs.TabPane = TabPane;
export default Tabs;
2、rc-tabs/Tabs.tsx
导入相应模块以及相应的ICON图标
// Accessibility https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/Tab_Role
import * as React from 'react';
import { useEffect, useState } from 'react';
import classNames from 'classnames';
import toArray from 'rc-util/lib/Children/toArray';
import isMobile from 'rc-util/lib/isMobile';
import useMergedState from 'rc-util/lib/hooks/useMergedState';
import TabNavList from './TabNavList';
import TabPanelList from './TabPanelList';
import type { TabPaneProps } from './TabPanelList/TabPane';
import TabPane from './TabPanelList/TabPane';
import type {
TabPosition,
RenderTabBar,
TabsLocale,
EditableConfig,
AnimatedConfig,
OnTabScroll,
Tab,
TabBarExtraContent,
} from './interface';
import TabContext from './TabContext';
声明TabsProps接口
let uuid = 0;
export interface TabsProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
prefixCls?: string;
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
id?: string;
activeKey?: string;
defaultActiveKey?: string;
direction?: 'ltr' | 'rtl';
animated?: boolean | AnimatedConfig;
renderTabBar?: RenderTabBar;
tabBarExtraContent?: TabBarExtraContent;
tabBarGutter?: number;
tabBarStyle?: React.CSSProperties;
tabPosition?: TabPosition;
destroyInactiveTabPane?: boolean;
onChange?: (activeKey: string) => void;
onTabClick?: (activeKey: string, e: React.KeyboardEvent | React.MouseEvent) => void;
onTabScroll?: OnTabScroll;
editable?: EditableConfig;
// Accessibility
locale?: TabsLocale;
// Icons
moreIcon?: React.ReactNode;
/** @private Internal usage. Not promise will rename in future */
moreTransitionName?: string;
}
activeKey:当前激活 tab 面板的 key,类型为string
addIcon:自定义添加按钮,类型为ReactNode
animated:是否使用动画切换 Tabs, 仅生效于 tabPosition=“top”,类型为boolean | { inkBar: boolean, tabPane: boolean }
centered:标签居中展示,类型为boolean
defaultActiveKey:初始化选中面板的 key,如果没有设置 activeKey,类型为string
hideAdd:是否隐藏加号图标,在 type=“editable-card” 时有效,类型为boolean
moreIcon:自定义折叠 icon,类型为ReactNode
renderTabBar:替换 TabBar,用于二次封装标签头,类型为(props: DefaultTabBarProps, DefaultTabBar: React.ComponentClass) => React.ReactElement
size:大小,提供 large default 和 small 三种大小,类型为string
tabBarExtraContent:tab bar 上额外的元素,类型为ReactNode | {left?: ReactNode, right?: ReactNode}
tabBarGutter:tabs 之间的间隙,类型为number
tabBarStyle:tab bar 的样式对象,类型为object
tabPosition:页签位置,可选值有 top right bottom left,类型为string
destroyInactiveTabPane:被隐藏时是否销毁 DOM 结构,类型为boolean
type:页签的基本样式,可选 line、card editable-card 类型,string
onChange:切换面板的回调,类型为function(activeKey) {}
onEdit:新增和删除页签的回调,在 type=“editable-card” 时有效,类型为(targetKey, action): void
onTabClick:tab 被点击的回调,类型为function(key: string, event: MouseEvent)
onTabScroll:tab 滚动时触发,类型为function({ direction: left | right | top | bottom })
function parseTabList(children: React.ReactNode): Tab[] {
return toArray(children)
.map((node: React.ReactElement<TabPaneProps>) => {
if (React.isValidElement(node)) {
const key = node.key !== undefined ? String(node.key) : undefined;
return {
key,
...node.props,
node,
};
}
return null;
})
.filter(tab => tab);
}
function Tabs(
{
id,
prefixCls = 'rc-tabs',
className,
children,
direction,
activeKey,
defaultActiveKey,
editable,
animated = {
inkBar: true,
tabPane: false,
},
tabPosition = 'top',
tabBarGutter,
tabBarStyle,
tabBarExtraContent,
locale,
moreIcon,
moreTransitionName,
destroyInactiveTabPane,
renderTabBar,
onChange,
onTabClick,
onTabScroll,
...restProps
}: TabsProps,
ref: React.Ref<HTMLDivElement>,
) {
const tabs = parseTabList(children);
const rtl = direction === 'rtl';
let mergedAnimated: AnimatedConfig | false;
if (animated === false) {
mergedAnimated = {
inkBar: false,
tabPane: false,
};
} else if (animated === true) {
mergedAnimated = {
inkBar: true,
tabPane: true,
};
} else {
mergedAnimated = {
inkBar: true,
tabPane: false,
...(typeof animated === 'object' ? animated : {}),
};
}
移动
// ======================== Mobile ========================
const [mobile, setMobile] = useState(false);
useEffect(() => {
// Only update on the client side
setMobile(isMobile());
}, []);
// ====================== Active Key ======================
//初始化
const [mergedActiveKey, setMergedActiveKey] = useMergedState<string>(() => tabs[0]?.key, {
value: activeKey,
defaultValue: defaultActiveKey,
});
const [activeIndex, setActiveIndex] = useState(() =>
tabs.findIndex(tab => tab.key === mergedActiveKey),
);
// Reset active key if not exist anymore
//如果不存在ActiveKey
useEffect(() => {
let newActiveIndex = tabs.findIndex(tab => tab.key === mergedActiveKey);
if (newActiveIndex === -1) {
newActiveIndex = Math.max(0, Math.min(activeIndex, tabs.length - 1));
setMergedActiveKey(tabs[newActiveIndex]?.key);
}
setActiveIndex(newActiveIndex);
}, [tabs.map(tab => tab.key).join('_'), mergedActiveKey, activeIndex]);
// ===================== Accessibility ====================
const [mergedId, setMergedId] = useMergedState(null, {
value: id,
});
//如果不在左右,设为顶部
let mergedTabPosition = tabPosition;
if (mobile && !['left', 'right'].includes(tabPosition)) {
mergedTabPosition = 'top';
}
// Async generate id to avoid ssr mapping failed
//异步设置id
useEffect(() => {
if (!id) {
setMergedId(`rc-tabs-${process.env.NODE_ENV === 'test' ? 'test' : uuid}`);
uuid += 1;
}
}, []);
// ======================== Events ========================
//改变标签页
function onInternalTabClick(key: string, e: React.MouseEvent | React.KeyboardEvent) {
onTabClick?.(key, e);
const isActiveChanged = key !== mergedActiveKey;
setMergedActiveKey(key);
if (isActiveChanged) {
onChange?.(key);
}
}
// ======================== Render ========================
const sharedProps = {
id: mergedId,
activeKey: mergedActiveKey,
animated: mergedAnimated,
tabPosition: mergedTabPosition,
rtl,
mobile,
};
let tabNavBar: React.ReactElement;
const tabNavBarProps = {
...sharedProps,
editable,
locale,
moreIcon,
moreTransitionName,
tabBarGutter,
onTabClick: onInternalTabClick,
onTabScroll,
extra: tabBarExtraContent,
style: tabBarStyle,
panes: children,
};
if (renderTabBar) {
tabNavBar = renderTabBar(tabNavBarProps, TabNavList);
} else {
tabNavBar = <TabNavList {...tabNavBarProps} />;
}
return (
<TabContext.Provider value={{ tabs, prefixCls }}>
<div
ref={ref}
id={id}
className={classNames(
prefixCls,
`${prefixCls}-${mergedTabPosition}`,
{
[`${prefixCls}-mobile`]: mobile,
[`${prefixCls}-editable`]: editable,
[`${prefixCls}-rtl`]: rtl,
},
className,
)}
{...restProps}
>
{tabNavBar}
<TabPanelList
destroyInactiveTabPane={destroyInactiveTabPane}
{...sharedProps}
animated={mergedAnimated}
/>
</div>
</TabContext.Provider>
);
}
const ForwardTabs = React.forwardRef(Tabs);
export type ForwardTabsType = typeof ForwardTabs & { TabPane: typeof TabPane };
(ForwardTabs as ForwardTabsType).TabPane = TabPane;
export default ForwardTabs as ForwardTabsType;
3、rc-tabs/TabPanelList/index.tsx
导入相应模块以及相应的ICON图标
import * as React from 'react';
import classNames from 'classnames';
import TabContext from '../TabContext';
import type { TabPosition, AnimatedConfig } from '../interface';
export interface TabPanelListProps {
activeKey: React.Key;
id: string;
rtl: boolean;
animated?: AnimatedConfig;
tabPosition?: TabPosition;
destroyInactiveTabPane?: boolean;
}
export default function TabPanelList({
id,
activeKey,
animated,
tabPosition,
rtl,
destroyInactiveTabPane,
}: TabPanelListProps) {
const { prefixCls, tabs } = React.useContext(TabContext);
const tabPaneAnimated = animated.tabPane;
const activeIndex = tabs.findIndex(tab => tab.key === activeKey);
return (
<div className={classNames(`${prefixCls}-content-holder`)}>
<div
className={classNames(`${prefixCls}-content`, `${prefixCls}-content-${tabPosition}`, {
[`${prefixCls}-content-animated`]: tabPaneAnimated,
})}
style={
activeIndex && tabPaneAnimated
? { [rtl ? 'marginRight' : 'marginLeft']: `-${activeIndex}00%` }
: null
}
>
{tabs.map(tab => {
return React.cloneElement(tab.node, {
key: tab.key,
prefixCls,
tabKey: tab.key,
id,
animated: tabPaneAnimated,
active: tab.key === activeKey,
destroyInactiveTabPane,
});
})}
</div>
</div>
);
}
rc-tabs/TabPanelList/TabPane.tsx
导入相应模块以及相应的ICON图标
import * as React from 'react';
import classNames from 'classnames';
export interface TabPaneProps {
tab?: React.ReactNode;
className?: string;
style?: React.CSSProperties;
disabled?: boolean;
children?: React.ReactNode;
forceRender?: boolean;
closable?: boolean;
closeIcon?: React.ReactNode;
// Pass by TabPaneList
prefixCls?: string;
tabKey?: string;
id?: string;
animated?: boolean;
active?: boolean;
destroyInactiveTabPane?: boolean;
}
closeIcon:自定义关闭图标,在 type="editable-card"时有效,类型为ReactNode
forceRender:被隐藏时是否渲染 DOM 结构,类型为boolean
key:对应 activeKey,类型为string
tab:选项卡头显示文字,类型为ReactNode
export default function TabPane({
prefixCls,
forceRender,
className,
style,
id,
active,
animated,
destroyInactiveTabPane,
tabKey,
children,
}: TabPaneProps) {
const [visited, setVisited] = React.useState(forceRender);
React.useEffect(() => {
if (active) {
setVisited(true);
} else if (destroyInactiveTabPane) {
setVisited(false);
}
}, [active, destroyInactiveTabPane]);
const mergedStyle: React.CSSProperties = {};
if (!active) {
if (animated) {
mergedStyle.visibility = 'hidden';
mergedStyle.height = 0;
mergedStyle.overflowY = 'hidden';
} else {
mergedStyle.display = 'none';
}
}
return (
<div
id={id && `${id}-panel-${tabKey}`}
role="tabpanel"
tabIndex={active ? 0 : -1}
aria-labelledby={id && `${id}-tab-${tabKey}`}
aria-hidden={!active}
style={{ ...mergedStyle, ...style }}
className={classNames(
`${prefixCls}-tabpane`,
active && `${prefixCls}-tabpane-active`,
className,
)}
>
{(active || visited || forceRender) && children}
</div>
);
}