前端关于display显隐和transition动画过渡的爱恨情仇

关于平时接触到的菜单栏,或者是手风琴功能,都涉及到展开收起的显示和隐藏,且隐藏后不占位。直接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;
        }
      }
    }
  }
}

  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值