前言
因为近期换了个团队,所以就整理了一下自己很久之前做的一些需求,这也是这个很简单的需求的由来 这篇文章很简单,所以就不写太多的东西了
总所周知,移动端其实很容易出现各种奇奇怪怪的tabs样式
也就容易导致了我们经常在做需求的时候需要重复去写不同样式的tabs组件
但是总所周知,tabs这个组件的行为本质上是提供了“切换tabs”、“内容展示”等功能
那我们能不能把tabs的相关行为进行一个封装,在每次遇到不同样式的tabs的时候只需要处理UI层面东西就可以了?
例子
所以在这个前提下尝试做了这个操作,所以以下六个例子中看到的tabs的组件实际上都是同一个组件,已经在两个已上线的需求中愉快使用
设计与实现
思考&需求拆解
1. 一个tabs 可能会有什么需求细节?
对应tabs这个组件相信大家其实都很熟悉,我们想要去抽离tabs的行为,那么第一件要做的事件就是先去梳理tabs这个组件能想到的所有需求细节。然后再将相关的行为进行一个抽离和实现。
那么对于一个tabs来说,有可能会有什么样的需求细节呢?
-
标签栏
- 自由点击切换激活态和非激活态
- 提供内容超出可滚动 和 内容超出不滚动两种模式
- 两个tabs之间可能会需要插入一个node,例如分割线或者图标
- tabs 存在下划线及其样式,例如圆角、颜色、高度
- tabs 内容区域的padding
- tabs内的文字样式
- tabs是否进行拉伸,即内容宽度不够的时候是否拉伸为铺满
- tabs内容可能为图标或者图片
- tabs可能需要徽标
- 需要提供当前活动的tabs的key
- 需要可以设置默认的活动 tabs 的功能,当不设置时默认为第一个tabs
- 需要提供tab切换时的onchange方法,需要支持返回的是一个promise来应对某些特殊业务场景
- 需要提供横向和竖向的布局
- 如果抽离行为,那样式需要传入非激活态和激活态时的样式
- tabsList 的 classname需要可以传入
- 有可能需要限制至少存在一个tabs才显示选项卡栏
- 需要可以传入 tabsList 的内容数组,至少包括了 key 与 title ,title 因为有可能是图标或者其他什么,所以需要支持ReactNode
- tabs对应的内容
-
内容区域(pane)
- 需要提供当前活动的pane的index
- 需要可以传入pane相关的内容
- 需要可以传入当前tabs的活动key作为映射
- 需要提供内容区域不可滚动 和 可滚动两种模式
- 需要提供滚动时触发的方法,以支持某些业务场景
2. 要抽离哪些需求?
因为我们这个组件是tabs的行为组件的抽离,所以我们要遵循一个原则:UI的归UI,行为和逻辑的归行为和逻辑
所以根据这个原则,那么我们最后需要实现的需求就是:
-
标签栏
- 需要可以传入 tabsList 的内容数组,至少包括了 key 与 title,title类型为string / ReactNode
- 需要提供当前活动的tabs的key
- 需要可以设置默认的活动 tabs 的功能,当不设置时默认为第一个tabs
- 需要提供tab切换时的onchange方法,需要支持返回的是一个promise来应对某些特殊业务场景
- 需要传入非激活态和激活态时的样式
- 自由点击切换激活态和非激活态
- 提供内容超出可滚动 和 内容超出不滚动两种模式
- 两个tabs之间可能会需要插入一个node
- tabs是否进行拉伸,即内容宽度不够的时候是否拉伸为铺满
- 需要提供横向和竖向的布局
- tabsList 的 classname需要可以传入
- 有可能需要限制至少存在一个tabs才显示选项卡栏
- tabs对应的内容
-
内容区域(pane)
- 需要可以传入pane相关的内容
- 需要提供当前活动的pane的index
- 需要可以传入当前tabs的活动key作为映射
- 需要提供内容区域不可滚动 和 可滚动两种模式
- 需要提供滚动时触发的方法,以支持某些业务场景
实现
1. api 设计
(1) tabs
/** 非激活态和激活态时的节点的props */
export interface ITabsItemProps {
tabsItem: ITabsListItem;
index?: number;
activityKey?: number;
}
export interface ITabsListItem {
key: string | number;
title: string | React.ReactNode;
[key: string]: any
}
export interface ITabsProps {
/** tabsList 的内容数组,至少包括了 key 与 title,title类型为string / ReactNode */
tabList: ITabsListItem[];
/** 当前活动的tabs的key */
activityKey: number;
/** tabs切换时的onchange方法,需要支持返回的是一个promise来应对某些特殊业务场景 */
onChange: (index: number) => void | Promise<void>;
/** tabs对应的内容 */
children?: React.ReactNode;
/** 非激活态的样式 */
TabsItem: (props: ITabsItemProps) => JSX.Element;
/** 激活态的样式 */
TabsItemActive: (props: ITabsItemProps) => JSX.Element;
/** tabsList 的 classname */
tabsListClassName?: string;
/** tabs容器的flex样式 */
tabsContainerFlex?: IFlex;
/** tabsList容器的flex样式 */
tabsListFlex?: IFlex;
/** 两个tabs之间需要插入的node */
middleExtraNode?: React.ReactNode;
/** 是否限制至少存在一个tabs才显示选项卡栏 */
tabsListShowLimit?: boolean;
/** tabs是否进行拉伸,即内容宽度不够的时候是否拉伸为铺满 */
tabListItemAverage?: boolean;
}
(2) pane
interface IPosition {
left: number;
top: number;
}
interface IProps {
/** 当前活动的tabs的key */
activityKey: number;
/** pane对应的内容 */
children?: React.ReactNode;
/** pane对应的index */
index: number;
/** pane滚动事件 */
onScroll?: (position: IPosition, activityKey: number) => void;
/** 当前pane是否允许滚动 */
isScroll?: boolean;
}
2. tabs 具体的功能实现
export default (props: ITabsProps) => {
const {
middleExtraNode,
tabList,
activityKey,
onChange,
TabsItem,
TabsItemActive,
children,
tabsListClassName,
tabsListFlex,
tabsContainerFlex,
tabsListShowLimit,
tabListItemAverage
} = props;
const tabListRender = () => {
return <TabsList
tabList={tabList}
activityKey={activityKey}
onChange={onChange}
TabsItem={TabsItem}
TabsItemActive={TabsItemActive}
tabsListClassName={tabsListClassName}
middleExtraNode={middleExtraNode}
tabsListFlex={tabsListFlex}
tabListItemAverage={tabListItemAverage}
/>
}
return (
<div className="tabs-container" style={tabsContainerFlex}>
{
tabsListShowLimit ? tabList.length > 1 && tabListRender() : tabListRender()
}
<Pane>{children}</Pane>
</div>
);
};
const TabsList = React.memo((props: ITabsProps) => {
const { middleExtraNode, tabList, activityKey, onChange, TabsItem, TabsItemActive, tabsListClassName, tabsListFlex, tabListItemAverage } = props;
// 激活与非激活态样式控制
const tabsRender = (activityKey: number, item: ITabsListItem, index: number) => {
if (activityKey === index) return <TabsItemActive tabsItem={item} index={index} />;
return <TabsItem tabsItem={item} index={index} />;
};
// tabs间node插入
const tabsMiddleExtraNode = (index: number) => {
if (tabList.length > 2 && index < tabList.length - 1) {
return middleExtraNode;
}
return null;
};
const tabsListClass = classNames('tabs-list', tabsListClassName);
return (
<div
className={tabsListClass}
style={tabsListFlex}>
{tabList.map((item, index) => {
return (
<div className="tabs-list-item-container" key={item.key} onClick={async() => {await onChange(index);}} style={tabListItemAverage ? { flex: 1 } : {}}>
{tabsRender(activityKey, item, index)}
{middleExtraNode && tabsMiddleExtraNode(index)}
</div>
);
})}
</div>
);
});
const Pane = (props: IPane) => {
const { children } = props;
return <div className="tabs-behavior-pane">{children}</div>;
};
/* css */
.tabs-container {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
.tabs-list {
display: flex;
align-items: center;
justify-content: center;
.tabs-list-item-container {
display: flex;
flex-shrink: 0;
align-items: center;
}
}
.tabs-list:-webkit-scrollbar {
display: none;
}
.tabs-behavior-pane {
height: 100%;
flex: 1;
overflow: hidden;
}
}
3. pane 具体的功能实现
const Pane = (props: IProps) => {
const { activityKey, children, index, onScroll, isScroll } = props;
const isShow = () => {
if (activityKey === index) {
return 'block';
}
return 'none';
};
const refs = useRef(null);
const position = useScroll(refs, (val) => (onScroll ? true : false));
useEffect(() => {
onScroll && onScroll(position, activityKey);
}, [position, activityKey]);
return (
<div className="pane" style={{ display: isShow(), overflowY: isScroll ? 'scroll' : 'hidden' }} ref={refs}>
{children}
</div>
);
};
.pane {
height: 100%;
min-height: 300px;
}
使用
在使用的时候我们只需要写UI相关的样式,然后进行一个简单的二次封装就可以愉快的使用啦
下面就是一个简单的例子