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 (<RcTabsdirection={
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;// Accessibilitylocale?: TabsLocale;// IconsmoreIcon?: 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 sidesetMobile(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//如果不存在ActiveKeyuseEffect(() => {
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
//异步设置iduseEffect(() => {
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 }}><divref={
ref}id={
id}className={
classNames(prefixCls,`${
prefixCls}-${
mergedTabPosition}`,{
[`${
prefixCls}-mobile`]: mobile,[`${
prefixCls}-editable`]: editable,[`${
prefixCls}-rtl`]: rtl,},className,)}{
...restProps}>{
tabNavBar}<TabPanelListdestroyInactiveTabPane={
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`)}><divclassName={
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 TabPaneListprefixCls?: 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 (<divid={
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>);
}