最近在学习张老师和方方的轮子课程,本系列文章是对react造轮子组件的思路的梳理
menu组件的样式
菜单的基本结构如下
- 支持点击hover高亮
- 支持disabled
- 支持下拉菜单
- 支持横向和纵向两种模式
- 横向的下拉菜单是hover之后弹出
- 纵向的下拉菜单是click之后弹出或者设置属性进行自动展开
Menu组件的使用方式
Menu组件的Html语义化话设计
<Menu
defaultIndex={'2'}
onSelect={index => {
console.log(index);
}}
>
<MenuItem>item 1</MenuItem>
<MenuItem>item 2</MenuItem>
<SubMenu title={'dropdown'}>
<MenuItem>dropdown item1</MenuItem>
<MenuItem>dropdown item2</MenuItem>
</SubMenu>
<MenuItem>item3</MenuItem>
</Menu>
Menu组件相当是一个容器,子元素必须是MenuItem或者是SubMenu,且每一项都有一个默认的index属性,默认按照以下规则:
0 MenuItem
1 SubMenu
1-0 MenuItem
1-1 MenuItem
2 MenuItem
3 SubMenu
3-0 MenuItem
3-1 MenuItem
3-2 MenuItem
Menu组件提供了一个onSelect回掉函数,当点击MenuItem的时候可以进行调用。提供了两种模式 'horizontal' | 'vertical'。当在纵向模式下,又提供了子菜单默认展开的功能。
Menu组件的属性
interface MenuProps {
defaultIndex?: string; // 默认index 默认高亮哪一个MenuItem
className?: string;
mode?: 'horizontal' | 'vertical'; // 两种模式
style?: React.CSSProperties;
onSelect?: (selectedIndex: string) => void; //像外面暴露一个回掉
defaultOpenSubMenus?: string[]; // 默认展开哪些子菜单
}
MenuItem组件的属性
interface MenuItemProps {
index?: string; // 当前MenuItem的index,和defaultIndex做比较进行高亮
disabled?: boolean;// 是否是禁用状态
className?: string;
style?: React.CSSProperties;
}
SubMenu组件的属性
interface SubMenuProps {
index?: string; // 当前SubMenu的index
title: string; // 子菜单的title
className?: string;
}
需要把Menu组件的defaultIndex等属性传递给子组件,因为使用的是children,就不能直接通过标签属性的方式进行传递,就需要使用useContext。下面是需要传递的属性。
menu的主要功能实现
active的实现
// Menu组件
// 需要传递的Context类型
interface IMenuContext {
index: string;
onSelect?: (selectedIndex: string) => void;
mode?: 'horizontal' | 'vertical';
defaultOpenSubMenus?: string[];
}
// 创建context
export const MenuContext = createContext<IMenuContext>({ index: '0' });
// 点击MenuItem需要切换active状态,active只有一个,在Menu组件创建state指示当前的active是哪一个。
const [currentActive, setActive] = useState(defaultIndex);
const handleClick = (index: string) => {
// 点击MenuItem 更换index,实现切换active,此index是子组件传递过来的
setActive(index);
// 提供给用户的回掉函数
onSelect && onSelect(index);
};
// 传递给子组件 Context
const passedContext: IMenuContext = {
index: currentActive ? currentActive : '0',
onSelect: handleClick,
mode,
defaultOpenSubMenus,
};
<MenuContext.Provider value={passedContext}>
...
</MenuContext.Provider>
MenuItem
const context = useContext(MenuContext);
const classes = classNames('gulu-menu-item', className, {
disabled: disabled,
// 从父组件中拿到currentIndex和子组件的index进行比较,如果一致就是active
active: context.index === index,
});
// 绑定点击函数,传入自己的index,父组件根据传入的index,改变currentIndex,然后在传入子组件,与子组件的index进行比较
const handleClick = () => {
if (context.onSelect && !disabled && typeof index === 'string') {
context.onSelect(index);
}
};
<li className={classes} style={style} onClick={handleClick}>
{children}
</li>
判断子组件的类型, 并且给MenuItem和SubMenu传入index
Menu组件
// 判读哪些子元素不是MenuItem和SubMenu
const renderChildren = () => {
return React.Children.map(children, (child, index) => {
const childElement = child as React.FunctionComponentElement<
MenuItemProps
>;
const { displayName } = childElement.type;
if (displayName === 'MenuItem' || displayName === 'SubMenu') {
return React.cloneElement(childElement, {
index: index.toString(),// 给MenuItem和SubMenu传入index
});
} else {
console.warn('Menu 的子元素至少有一个不是 MenuItem 或者 SubMenu');
}
});
};
Submenu
const renderChildren = () => {
const childrenComponent = React.Children.map(children, (child, i) => {
const childElement = child as FunctionComponentElement<MenuItemProps>;
if (childElement.type.displayName === 'MenuItem') {
return React.cloneElement(childElement, {
index: `${index}-${i}`,
});
} else {
console.warn('SubMenu 的子元素至少有一个不是 MenuItem');
}
});
return (
<ul >{childrenComponent}</ul>
);
};
Submenu的隐藏与展开
.gulu-submenu {
display: none;
}
// 只要有这两个类就可以让他显示出来
.gulu-submenu.menu-opened {
display: block;
}
为submenu添加menu-opened类,点击的时候会显示和隐藏
// submenu
const [menuOpen, setOpen] = useState(false);
const handleClick = (e: React.MouseEvent) => {
e.preventDefault();
setOpen(!menuOpen);
};
// renderChildren函数 添加类名字
const subMenuClasses = classNames('gulu-submenu', {
'menu-opened': menuOpen,
});
<ul className={subMenuClasses}>{childrenComponent}</ul>
//组件返回
<li key={index} className={classes} >
<div className={'gulu-submenu-title'} onClick={handleClick} >
{title}
</div>
{renderChildren()}
</li>
submenu 横向模式下hover出现,纵向模式下点击出现
// submenu
let timer: any;
const handleMouse = (e: React.MouseEvent, toggle: boolean) => {
clearTimeout(timer);
e.preventDefault();
timer = setTimeout(() => {
setOpen(toggle);
}, 300);
};
// 根据不同的模式传入不同的事件
const clickEvents =
context.mode === 'vertical' ? { onClick: handleClick } : {};
const hoverEvents =
context.mode !== 'vertical'
? {
onMouseEnter: (e: React.MouseEvent) => handleMouse(e, true),
onMouseLeave: (e: React.MouseEvent) => handleMouse(e, false),
}
: {};
// 组件返回
<li key={index} className={classes} {...hoverEvents}>
<div className={'gulu-submenu-title'} {...clickEvents}>
{title}
</div>
{renderChildren()}
</li>
submenu 纵向模式默认展开
// submenu
const openedSubMenus = context.defaultOpenSubMenus as Array<string>;
const isOpened =
index && context.mode === 'vertical'
? openedSubMenus.includes(index) //判断openedSubMenus是否包含index
: false;
const [menuOpen, setOpen] = useState(isOpened);