Antd Menu 简述
Menu 为页面和功能提供导航的菜单列表。
导航菜单是一个网站的灵魂,用户依赖导航在各个页面中进行跳转。一般分为顶部导航和侧边导航,顶部导航提供全局性的类目和功能,侧边导航提供多级结构来收纳和排列网站架构。
要点提取
核心实现
Menu 的用法。
<Menu selectedKeys={["analysis"]} >
<Menu.Item index="analysis">分析</Menu.Item>
<Menu.Item index="market">营销</Menu.Item>
<Menu.SubMenu index="set" title={<span>设置</span>}>
<Menu.Item index="4">二级标题</Menu.Item>
<Menu.Item index="5">二级标题</Menu.Item>
<Menu.SubMenu index="sub3" title={<span>二级菜单</span>}>
<Menu.Item index="6">三级标题</Menu.Item>
</Menu.SubMenu>
</Menu.SubMenu>
</Menu>
通过上面的 Demo, 我们可以看出 Menu 组件的三个核心部分:Menu, Menu.Item, Menu.SubMenu
Menu 为一个用例中最顶层组件,SubMenu 的父组件为 SubMenu 或 Menu。MenuItem 作为叶子 (leaf) 节点,父组件为 SubMenu 或 Menu.
注:自实现组件库中 mode 命名有点偏差,vertical 其实对应 antd 中的 inline
Menu.tsx
export default class Menu extends React.Component<IMenuProps, any> {
static propTypes = {...}
static defaultProps = {...}
static Item = MenuItem;
static SubMenu = SubMenu;
static childContextTypes = {
level: PropTypes.number,
mode: PropTypes.string,
onClick: PropTypes.func,
onSelect: PropTypes.func,
selectedKeys: PropTypes.arrayOf(PropTypes.string)
};
getChildContext() {
return {
level: 1,
mode: this.props.mode,
onClick: this.onClick,
onSelect: this.onSelect,
selectedKeys: this.state.selectedKeys
};
}
constructor(props) {
super(props);
this.state = {
selectedKeys: this.props.selectedKeys || []
};
}
onSelect = (params: ClickParam) => {
this.setState({
selectedKeys: [params.key]
});
if (this.props.onSelect) {
this.props.onSelect(params);
}
}
onClick = (params: ClickParam) => {
const {onClick} = this.props;
if (typeof onClick === 'function') {
onClick(params);
}
}
render() {
const {
classPrefix,
className,
style,
children,
mode,
onClick,
selectedKeys,
...restProps
} = this.props;
return (
<div
className={classNames(className, `${classPrefix}-container`, {
})}
ref="menus-container"
style={style}
{...restProps}
>
<ul
className={classNames(classPrefix, {
[`${classPrefix}-vertical`]: mode === 'vertical',
[`${classPrefix}-horizontal`]: mode === 'horizontal'
})}
>
{children}
</ul>
</div>
);
}
}
上述对 Menu 实现的要点如下:
1. 负责菜单布局:比如水平 (horizontal),垂直 (vertical),内嵌 (inline)
2. 菜单项 (MenuItem, SubMenu) 选中或点击的事件代理: 将 Menu 的 onClick 与 onSelect 事件回调作为 context 传递到子组件,在 MenuItem 或 SubMenu 的 click 事件中调用,达到一个类似于事件代理的效果
3. 菜单顶层 level = 1: Menu 作为最顶层组件,将 level 作为 context 传递到子组件,作为一个标的。SubMenu 自己的 context 中也有 level,值为父级 Level + 1。由于 MenuItem 是叶子节点,它的 level 即为父级 context.level + 1.
注:在React中,如果父组件和祖先组件具有相同的 context 字段名,父组件会覆盖祖先组件。因此通过level 的层层自增,我们总能取得当前节点的 level 值。
SubMenu.tsx:
import {Transition} from 'react-transition-group';
import Popover from '../Popover/Popover';
...
export default class SubMenu extends React.Component<ISubMenuProps, any> {
static propTypes = {
...
disabled: PropTypes.bool,
key: PropTypes.string,
expanded: PropTypes.bool
}
static defaultProps = {...}
// 来自父组件 Menu 或 SubMenu
static contextTypes = {
level: PropTypes.number
};
// 作为父组件传递 context
static childContextTypes = {
level: PropTypes.number
};
// 可能有 subMenu -> subMenu 情况
getChildContext() {
return {
level: this.context.level + 1
};
}
...
render() {
const {...} = this.props;
const {mode, level} = this.context;
// Menu 为横向布局时
const popMenuItems = (
<ul className={`${classPrefix}-pop`} style={{minWidth: 100}}>
{children && <div className={`${classPrefix}-pop-content`}>{children}</div>}
</ul>
);
// SubMenu 本体
const subMenu = (
<div
className={`${classPrefix}-title`}
onClick={this.onTitleClick.bind(this)}
>
<div className={`${classPrefix}-title-content`} style={style}>
{title}
{mode === 'vertical' && <Icon type={`arrow-${this.state.expanded ? 'up' : 'down'}`} className={`${classPrefix}-arrow`}/>}
</div>
</div>
);
return (
<li
{...restProps}
key={key}
ref="submenu"
className={classNames(classPrefix, className, {
[`${classPrefix}-disabled`]: disabled,
[`${classPrefix}-horizontal`]: mode === 'horizontal' && level === 1
})}
>
{
mode === 'horizontal'
? <Popover
direction={level === 1 ? 'bottom' : 'rightTop'}
content={popMenuItems}
className={...}
trigger="hover"
>
{subMenu}
</Popover>
: subMenu
}
<Transition ... in={this.state.expanded}>
<ul className={`${classPrefix}-sub`}>
{children}
</ul>
</Transition>
</li>
);
}
}
上述对 SubMenu 的实现要点如下:
- 根据 mode 的不同有不同的实现
-
mode 为 horizontal 时使用 Popover 组件,menuItem 以 “弹出” 的形式展现(主要考虑到的交互是 SubMenu hover 时展现 MenuItem)
-
mode 为 vertical 时正常展现,不用 Popover. 展开时的动画效果由第三方控件 Transition 提供。
-
MenuItem.tsx:
export default class MenuItem extends React.Component<IMenuItemProps, any> {
static propTypes = {
...
index: PropTypes.string // item 的唯一标志
}
static defaultProps = {...}
static contextTypes = {
level: PropTypes.number,
mode: PropTypes.string,
onClick: PropTypes.func,
onSelect: PropTypes.func,
selectedKeys: PropTypes.arrayOf(PropTypes.string)
};
constructor(props) {
super(props);
this.state = {
expanded: false,
isSelected: false
};
}
componentWillReceiveProps(nextProps, nextContext) {
const selectedKeys = nextContext.selectedKeys;
const key = this.props.index;
if (selectedKeys.indexOf(key) > -1) {
this.setState({
isSelected: true
});
} else {
this.setState({
isSelected: false
});
}
}
componentDidMount() {
const selectedKeys = this.context.selectedKeys;
const key = this.props.index;
if (selectedKeys.indexOf(key) > -1) {
this.setState({
isSelected: true
});
} else {
this.setState({
isSelected: false
});
}
}
onClickItem(e) {
this.setState({isSelected: !this.state.isSelected}, () => {
const {index} = this.props;
const params: ClickParam = {
domEvent: e,
item: this,
key: index,
keyPath: [index]
};
this.context.onSelect(params);
this.context.onClick(params);
});
}
render() {
const {
classPrefix,
className,
index,
style,
disabled,
children,
...restProps
} = this.props;
const {isSelected, expanded} = this.state;
const {mode, level} = this.context;
const menuItem = (
<div
className={classNames(`${classPrefix}-content`, {
[`${classPrefix}-content-selected`]: isSelected,
[`${classPrefix}-vertical-content`]: mode === 'vertical',
[`${classPrefix}-horizontal-content`]: mode === 'horizontal'
})}
style={style}
>
{children}
</div>
);
return (
// TODO: Tooltip is for inlineCollapsed, set visible to false tempararily
<li
{...restProps}
className={classNames(classPrefix, className, {
[`${classPrefix}-vertical`]: mode === 'vertical',
[`${classPrefix}-horizontal`]: mode === 'horizontal',
[`${classPrefix}-vertical-selected`]: isSelected && mode === 'vertical',
[`${classPrefix}-horizontal-selected`]: isSelected && mode === 'horizontal' && level === 1,
[`${classPrefix}-disabled`]: disabled
})}
onClick={!disabled && this.onClickItem.bind(this)}
>
<Tooltip
visible={expanded}
direction="right"
message={menuItem}
className={classNames(`${classPrefix}-tooltip`, {
})}
trigger="hover"
showArrow={false}
>
{menuItem}
</Tooltip>
</li>
);
}
}
上述对 MenuItem 的实现要点如下:
-
实现关键在于从 Menu 或 SubMenu 组件传递的 context
-
点击 MenuItem 时同时触发 SubMenu 或 Menu 通过 context 传递来的 click 回调
-
mode 与 level 会影响 MenuItem 的展现,这也是为什么 mode 与 level 需要通过 context 传递过来。
-
更多组件
更多组件自实现系列,更多文章请参考: