关于平时接触到的菜单栏,或者是手风琴功能,都涉及到展开收起的显示和隐藏,且隐藏后不占位。直接display显的生硬,想加transition动画过渡,却和display两个老死不相往来。当然可以考虑第三方组件库现成组件,但是也不是什么情况都适用于把一个组件库弄进来。
然后就有了以下代码,简单做了一个菜单的展示,实现了动画的过渡。
import { useCallback, useEffect, useState } from 'react';
import styles from './styles.module.scss';
const items = [
{
key: '1',
title: '项目管理',
children: [
{
key: '1',
title: '列表页',
},
{
key: '2',
title: '详情页',
},
{
key: '3',
title: '结果页',
},
],
},
{
key: '2',
title: '开发管理',
children: [
{
key: '1',
title: '前端系统',
},
{
key: '2',
title: '后端系统',
},
{
key: '3',
title: '管理系统',
},
],
},
{
key: '3',
title: '测试管理',
children: [
{
key: '1',
title: '测试功能',
},
{
key: '2',
title: '测试bug',
},
{
key: '3',
title: '测试用例',
},
],
},
];
// 通过控制外部容器来控制子菜单的显隐,通过定时器给子菜单容器制造一个高度的过渡计算
function toggleOpen(targetId: any, open: boolean) {
try {
const target: any = document.getElementById(`#${targetId}`);
if (target) {
const parentElement = target.parentElement; // 菜单的父容器
const nextSibling = target.nextSibling; // 子菜单
const nextSiblingHeight = nextSibling.clientHeight; // 子菜单容器的高度
// open 0 ---> nextSiblingHeight
// close nextSiblingHeight ---> 0
if (open) {
parentElement.style.height = 'auto';
nextSibling.style = `height: 0px;`;
setTimeout(() => {
nextSibling.style = `height:${nextSiblingHeight}px`;
}, 100);
} else {
nextSibling.style = `height:${nextSiblingHeight}px`;
setTimeout(() => {
nextSibling.style = `height: 0px`;
}, 100);
}
// 时间为动画过渡时间 + 制造高度时间差100
// 可以适当调整过渡时间,配合切换时防抖效果将会更好
setTimeout(() => {
nextSibling.style = '';
parentElement.style.height = open ? 'auto' : `${target.clientHeight}px`;
}, 400);
}
} catch (error) {}
}
interface MenuProps {
sigerMenu?: boolean; // 只允许单个菜单展开
defaultOpenKeys?: React.Key[]; // 默认展开项
}
function Menu(props: MenuProps) {
const { sigerMenu = true, defaultOpenKeys = ['1'] } = props;
const [openKeys, setOpenKeys] = useState<React.Key[]>([]);
const [currentKey, setCurrentKey] = useState<React.Key>(''); // 当前点击的key
// 菜单展开项
const handleOpenChange = useCallback(
(openKey: React.Key) => {
setOpenKeys((preState) => {
let newState: React.Key[] = [];
if (sigerMenu) {
newState = preState.includes(openKey) ? [] : [openKey];
// 只允许单个菜单展开的情况下,如果当前有展开项并且点击的菜单和当前展开的菜单不是同一项,要把原来展开那一项关闭
if (preState.length > 0 && !preState.includes(openKey)) {
toggleOpen(preState[0], false);
}
return newState;
}
if (preState.includes(openKey)) {
// 移除
newState = preState.filter((item) => item !== openKey);
} else {
// 添加
newState = preState.concat(openKey);
}
return newState;
});
setCurrentKey(openKey);
},
[sigerMenu]
);
useEffect(() => {
toggleOpen(currentKey, openKeys.includes(currentKey));
}, [openKeys, currentKey]);
useEffect(() => {
setOpenKeys(defaultOpenKeys);
defaultOpenKeys.forEach((openKey) => {
toggleOpen(openKey, true);
}, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className={styles.menu}>
{items.map((titles) => (
<div key={titles.key} className={styles.menuItems}>
<div
className={styles.menuTitle}
onClick={() => handleOpenChange(titles.key)}
id={`#${titles.key}`}
>
<span>{titles.title}</span>
<i
className={openKeys.includes(titles.key) ? styles.openArrow : ''}
></i>
</div>
<ul>
{titles.children.map((menus) => (
<li key={menus.key}>{menus.title}</li>
))}
</ul>
</div>
))}
</div>
);
}
export default Menu;
styles.module.scss文件
.menu {
.menuItems {
cursor: pointer;
height: 51px; // 这个高度比较重要
overflow: hidden;
.menuTitle {
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: inset 0 -1px 0px 0px #f5f5f5;
padding: 13px 24px;
font-size: 18px;
font-weight: bold;
i {
display: inline-block;
width: 8px;
height: 8px;
border-bottom: 1px solid #666;
border-right: 1px solid #666;
transform: rotate(45deg);
transition: all 0.3s linear;
}
.openArrow {
transform: scale(-1) rotate(45deg);
}
}
ul {
background-color: #f9fcff;
transition: all 0.3s linear;
opacity: 1;
overflow: hidden;
li {
padding: 17px 0 17px 74px;
color: #666666;
box-shadow: inset 0px 1px 0px 0px #f5f5f5,
inset 0px -1px 0px 0px #f5f5f5;
&:hover {
color: #234bf8;
}
}
}
}
}