Skip to content

Commit

Permalink
feat: ✨ implement tabbar
Browse files Browse the repository at this point in the history
  • Loading branch information
huang-xiao-jian committed Jun 2, 2020
1 parent d9b67e8 commit 29ac385
Show file tree
Hide file tree
Showing 10 changed files with 342 additions and 1 deletion.
2 changes: 1 addition & 1 deletion packages/Info/Info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import './Info.css';

interface InfoProps {
// 移植属性
dot: boolean;
dot?: boolean;
info?: string;
// 调整属性
// info#customStyle
Expand Down
17 changes: 17 additions & 0 deletions packages/Tabbar/Tabbar.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.van-tabbar {
display: flex;
width: 100%;
height: 50px;
height: var(--tabbar-height, 50px);
background-color: #fff;
background-color: var(--tabbar-background-color, #fff);
}
.van-tabbar--fixed {
position: fixed;
bottom: 0;
left: 0;
}
.van-tabbar--safe {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
98 changes: 98 additions & 0 deletions packages/Tabbar/Tabbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// packages
import React, {
FunctionComponent,
CSSProperties,
Children,
isValidElement,
cloneElement,
} from 'react';
import clsx from 'clsx';
import { View } from 'remax/wechat';
// internal
import TabbarContext from './TabbarContext';
import pickStyle from '../tools/pick-style';
import withDefaultProps from '../tools/with-default-props-advance';
import './Tabbar.css';

// 默认值填充属性
interface NeutralTabbarProps {
fixed: boolean;
border: boolean;
safeAreaInsetBottom: boolean;
zIndex: number;
// pass down for tabbar item
activeColor: string;
inactiveColor: string;
}

interface ExogenousTabbarProps {
// 选中索引
active: number;
// 容器类名,用以覆盖内部
className?: string;
// trigger on every item click
onChange: (event: { detail: number }) => void;
}

type TabbarProps = NeutralTabbarProps & ExogenousTabbarProps;

const DefaultTabbarProps: NeutralTabbarProps = {
fixed: true,
border: true,
safeAreaInsetBottom: true,
zIndex: 1,
// remember TabbarContext default value, createContext type mis-annotation
activeColor: '#1989fa',
inactiveColor: '#7d7e80',
};

// TODO - support name match machanism
const Tabbar: FunctionComponent<TabbarProps> = (props) => {
const {
className,
border,
fixed,
safeAreaInsetBottom,
zIndex,
active,
activeColor,
inactiveColor,
children,
onChange,
} = props;

const style: CSSProperties = {
zIndex,
};
const classnames = {
container: clsx(className, 'van-tabbar', {
'van-tabbar--fixed': fixed,
'van-tabbar--safe': safeAreaInsetBottom,
'van-hairline--top-bottom': border,
}),
};
const payload = {
activeColor,
inactiveColor,
};
const elements = Children.map(children, (child, index) =>
!isValidElement(child)
? child
: cloneElement(child, {
active: active === index,
onClick: () => onChange({ detail: index }),
})
);

return (
<TabbarContext.Provider value={payload}>
<View style={pickStyle(style)} className={classnames.container}>
{elements}
</View>
</TabbarContext.Provider>
);
};

export default withDefaultProps<ExogenousTabbarProps, NeutralTabbarProps>(
DefaultTabbarProps
)(Tabbar);
14 changes: 14 additions & 0 deletions packages/Tabbar/TabbarContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// packages
import { createContext } from 'react';

interface TabbarContextPayload {
activeColor: string;
inactiveColor: string;
}

const TabbarContext = createContext<TabbarContextPayload>({
activeColor: '#1989fa',
inactiveColor: '#7d7e80',
});

export default TabbarContext;
4 changes: 4 additions & 0 deletions packages/Tabbar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/**
* @description - nothing but export component
*/
export { default } from './Tabbar';
34 changes: 34 additions & 0 deletions packages/TabbarItem/TabbarItem.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
:host {
flex: 1;
}
.van-tabbar-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #646566;
color: var(--tabbar-item-text-color, #646566);
font-size: 12px;
font-size: var(--tabbar-item-font-size, 12px);
line-height: 1;
line-height: var(--tabbar-item-line-height, 1);
}
.van-tabbar-item__icon {
position: relative;
margin-bottom: 5px;
margin-bottom: var(--tabbar-item-margin-bottom, 5px);
font-size: 18px;
font-size: var(--tabbar-item-icon-size, 18px);
}
.van-tabbar-item__icon__inner {
display: block;
min-width: 1em;
}
.van-tabbar-item--active {
color: #1989fa;
color: var(--tabbar-item-active-color, #1989fa);
}
.van-tabbar-item__info {
margin-top: 2px;
}
74 changes: 74 additions & 0 deletions packages/TabbarItem/TabbarItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// packages
import React, {
FunctionComponent,
useContext,
CSSProperties,
ReactNode,
} from 'react';
import clsx from 'clsx';
import { View } from 'remax/wechat';
// internal
import Icon from '../Icon';
import Info from '../Info';
import { Switch, Case } from '../tools/Switch';
import TabbarContext from '../Tabbar/TabbarContext';
import './TabbarItem.css';

// 默认值填充属性
type renderIcon = (active: boolean) => ReactNode;

interface ExogenousTabbarItemProps {
// use render prop within complicated
icon: string | renderIcon;
// 容器类名,用以覆盖内部
className?: string;
// 传递 Info 组件
dot?: boolean;
info?: string;
// 匹配状态,父组件 Tabbar 隐式传递
active?: boolean;
// 事件绑定,父组件 Tabbar 隐式传递
onClick?: (event: any) => void;
}

type TabbarItemProps = ExogenousTabbarItemProps;

const TabbarItem: FunctionComponent<TabbarItemProps> = (props) => {
const { active, className, children, icon, dot, info, onClick } = props;
const { activeColor, inactiveColor } = useContext(TabbarContext);
const style: CSSProperties = {
// weired css
flex: 1,
color: active ? activeColor : inactiveColor,
};
const classnames = {
container: clsx(className, 'van-tabbar-item', {
'van-tabbar-item--active': active,
}),
};

return (
<View style={style} className={classnames.container} onClick={onClick}>
{/* item icon */}
<View className="van-tabbar-item__icon">
<Switch>
<Case in={typeof icon === 'string'}>
<Icon
name={icon as string}
className="van-tabbar-item__icon__inner"
/>
</Case>
<Case default>
{typeof icon === 'function' && icon(active as boolean)}
</Case>
</Switch>
<Info dot={dot} info={info} className="van-tabbar-item__info" />
</View>

{/* item text */}
<View className="van-tabbar-item__text">{children}</View>
</View>
);
};

export default TabbarItem;
4 changes: 4 additions & 0 deletions packages/TabbarItem/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/**
* @description - nothing but export component
*/
export { default } from './TabbarItem';
1 change: 1 addition & 0 deletions public/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AppConfig } from 'remax/wechat';

const config: AppConfig = {
pages: [
'pages/tabbar/index',
'pages/tree-select/index',
'pages/notice-bar/index',
'pages/sidebar/index',
Expand Down
95 changes: 95 additions & 0 deletions public/pages/tabbar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// packages
import React, { useState } from 'react';
import { View, Text } from 'remax/wechat';

// internal
import Tabbar from '../../../packages/Tabbar';
import TabbarItem from '../../../packages/TabbarItem';
import Image from '../../../packages/Image';

export default () => {
const [activeIndex, seteActiveIndex] = useState(0);

const customIcons = {
inactive: (
<Image
src="https://img.yzcdn.cn/vant/user-inactive.png"
width="18px"
height="18px"
mode="aspectFit"
/>
),
active: (
<Image
src="https://img.yzcdn.cn/vant/user-active.png"
width="18px"
height="18px"
mode="aspectFit"
/>
),
};

const iconFunction = (active: boolean) =>
active ? customIcons.active : customIcons.inactive;

return (
<View className="demo-block">
<Text className="demo-block__title">基础用法</Text>
<View>
<Tabbar
active={activeIndex}
onChange={(event) => seteActiveIndex(event.detail)}
>
<TabbarItem icon="home-o">Home</TabbarItem>
<TabbarItem icon="search">Search</TabbarItem>
<TabbarItem icon="friends-o">Friends</TabbarItem>
<TabbarItem icon="setting-o">Setting</TabbarItem>
</Tabbar>
</View>
<Text className="demo-block__title">显示徽标</Text>
<View>
<Tabbar
active={activeIndex}
onChange={(event) => seteActiveIndex(event.detail)}
>
<TabbarItem icon="home-o">Home</TabbarItem>
<TabbarItem icon="search" dot>
Search
</TabbarItem>
<TabbarItem icon="friends-o" info="5+">
Friends
</TabbarItem>
<TabbarItem icon="setting-o" info="99+">
Setting
</TabbarItem>
</Tabbar>
</View>
<Text className="demo-block__title">自定义图标</Text>
<View>
<Tabbar
active={activeIndex}
onChange={(event) => seteActiveIndex(event.detail)}
>
<TabbarItem icon={iconFunction}>Home</TabbarItem>
<TabbarItem icon={iconFunction}>Search</TabbarItem>
<TabbarItem icon={iconFunction}>Friends</TabbarItem>
<TabbarItem icon={iconFunction}>Setting</TabbarItem>
</Tabbar>
</View>
<Text className="demo-block__title">自定义颜色</Text>
<View>
<Tabbar
active={activeIndex}
activeColor="#07c160"
inactiveColor="#000"
onChange={(event) => seteActiveIndex(event.detail)}
>
<TabbarItem icon="home-o">Home</TabbarItem>
<TabbarItem icon="search">Search</TabbarItem>
<TabbarItem icon="friends-o">Friends</TabbarItem>
<TabbarItem icon="setting-o">Setting</TabbarItem>
</Tabbar>
</View>
</View>
);
};

0 comments on commit 29ac385

Please sign in to comment.