这个css布局样式真巧妙啊!react导航栏组件:Headmenu实现

前言

HeadMenu的样式如下:

image.png

其中核心功能百分之95%是靠css实现的,参考的T-deisgn的源码,感觉比较巧妙,并且代码很容易理解,分享一下。

下面讲解难点的在线demo如下:

非常有意思的css布局

上图这部分布局是css实现的,同志们,你们有思路怎么实现不?首先提醒是绝对定位,你去访问的各种官网的导航栏的绝大多数都是绝对定位,因为你不可能说我展开菜单把下面的dom元素撑下去,是吧!

我们来说布局这个css的难点在哪里!先把基本的数据结构交代一下
用数据结构表示一下:

{
  name: '电器', // Submenu组件
  children: [
    {
      name: '电视', // Submenu组件
      children: [
        {
          name: '索尼电视' // MenuItem组件
        },
        {
          name: '华为电视' // MenuItem组件
        }
      ]
    },
    {
      name: '冰箱' // MenuItem组件
    }
  ]
}

如上,导航栏组件主要由Submenu组件和MenuItem组件组成。

image.png

image.png

难点一 绝对定位的元素的父元素如果是inline-block,并且没设置宽度,那么这个绝对定位的元素宽度就是0,高度也是0

我在看源码的时候被一个css搞的有点懵,我们简化一下问题,jsx的结构如下:

 <div className="head-menu">
      <div className="submenu">
        <div className="menu__item">电器</div>
        <div
          className="menu__popup">
              下拉框内容
        </div>
      </div>
    </div>

css如下:

.head-menu {
  margin: 0;
  padding: 0;
  position: relative;
  height: 30px;
}
.submenu {
  position: relative;
}
.menu__popup {
  position: absolute;
}

结果如下:

image.png

我们往

.head-menu {
  margin: 0;
  padding: 0;
  position: relative;
  height: 30px;
}

.head-menu的css中加一条display: flex, 表现如下:

image.png

这是咋回事呢?

我们肯定知道是flex的问题,到底为啥会导致这样的问题?如何解决呢?

flex的元素,包裹的子元素的display可以认为变成了inline-block。对于我们这个案例来说就是类名为submenu的元素由块元素变为了inline-block,并且它没有设置宽度。

我们接着看绝对定位的元素,

.menu__popup {
  position: absolute;
}

也就是类名menu__popup的元素,它的上一级定位元素是submenu,因为他没有宽度所以自己也没有宽度了。

但为啥宽度又跟”电器“文字一样宽呢?因为电器这个文字的父级是submenu,它把submenu撑开了,所以submenu的宽度就是电器(电器是Submenu组件的标题,如果没有它撑开宽度,那么Submenu宽度就是0了)的宽度。

是不是有点绕!简单记住就是绝对定位的元素的父级(父级只有它一个子元素的话)是inline-block时候,绝对定位元素的宽度就是0,高度也是0。(我们这里是因为本身文字有最小宽度)

怎么解决呢,只有显式的给绝对定位元素宽高。

难点二 如何动态的控制样式的增加和删除

这个问题比较简单,目的是为了第3个难点做铺垫。

我的需求如下图,鼠标移入电器显示下拉框,移除关闭下拉框

image.png

在实现代码之前,先写一个工具类,目的是为了css能够动态改变(相当于是乞丐版的classnames这个库)

function classnames(v): string {
  const classNames = [];
  for(let i = 0; i < v.length, i++){
      if(typeof k === 'string') {
        classNames.push(k);
        continue;
      }
      // 这里判断是普通对象写法有点问题,表达这个意思吧
      if(typeof k === 'object') {
           Object.keys(v).forEach((k) => {
             else if (v[k]) {
              classNames.push(k);
            }
      });
     }
  }
 
  return classNames.join(' ');
}

用法如下:

const Demo = (props) => {
    const [isOpen, setOpen] = useState(false);
    return <div className={classnames(
    {
        'opened': isOpen,
    },
    hello'
    )}></div>
}

看到了吗isOpen可以动态的控制css的样式。

接着我们回到难点2问题本身,如何动态增删css类名。

好了,我们接着用css实现文章开头说的那个样式吧,
核心原理就是:

1、鼠标进入submenu的时候,触发mouseEnter事件,此时把变量open设置为true

2、鼠标离开submenu的时候,触发mouseLeave事件,此时把变量open设置为false

3、open控制着css变量is-opened的增加和删除(is-opened这个css可以看做display: block,默认submenu类上displyy:none),所以移入就display: block了

export default function App() {
  const [open, setOpen] = useState(false);
  return (
    <div className="head-menu">
      <div
        className={classnames(
          {
            "is-opened": open
          },
          "submenu"
        )}
        onMouseEnter={() => setOpen(true)}
        onMouseLeave={() => setOpen(false)}
      >
        <div className="menu__item">电器</div>
        <div
          className={classnames("menu__popup", {
            "is-opened": open
          })}
        >
          下拉框内容
        </div>
      </div>
    </div>
  );
}

难点三 移入空隙为啥下拉框没有关闭

Submenu组件和Menu组件之间有一段空白区域,如下图,我们鼠标移入的时候,是不是也要保持下方展开状态,要不这个menu组件也太不好用了。

image.png

这里用的小技巧就是点电器这个元素内使用一个伪类::before,去填充一个透明元素,样式如下:

.t-head-menu .t-submenu>.t-menu__item:before {
  content: "";
  display: block;
  position: absolute;
  bottom: -20px;
  left: 0;
  right: 0;
  height: 40px;
}

难点四,绝对定位如何实现动态偏移

啥意思呢,如下图,这些间隔都是固定宽度,并且每一列的文字增加或者减少,这些宽度都是固定的。(这在正常的文档流里,我们设置margin-left就能实现,可是这是绝对定位,咋实现呢?)
image.png

技巧:

.menu__popup {
  position: absolute;
  top: 0;
  left: calc(100% + 16px);
}

关键代码就是left,这里100%是啥意思呢,就是父元素的宽度,如下图:

image.png

好了,如果你的业务遇到类似需求,这篇文章希望能帮到你。

意外发现的一个css法则

当父容器里有 绝对定位 的子元素时,子元素设置width:100%实际上指的是相对于父容器的padding+content的宽度。
当子元素是非绝对定位的元素时width:100%才是指子元素的 content 等于父元素的 content宽度

这个案例是从网上找的:
查看范例

晦涩部分,源码解析

下面是整个源码,没兴趣的朋友可以略过。

最外层的包裹元素:HeadMenu,这里我提取最关键的代码


const HeadMenu: FC<HeadMenuProps> = (props) => {
  const { children, className, theme = 'light', style } = props;
  // classPrefix是css的前缀,自定义,比如这里叫是字符串t,也就是classPrefix=’t‘
  const { classPrefix } = useConfig();
  // 让所有menu里的Submenu和MenuItem都共享一些数据,主要用于数据间通信
  const { value } = useMenuContext({ ...props, children, mode: 'title' });

  return (
    <MenuContext.Provider value={value}>
      <div
        className={classNames(`${classPrefix}-head-menu`, className)}
        style={{ ...style }}
      >
        <div className={`${classPrefix}-head-menu__inner`}>
          <ul className={`${classPrefix}-menu`}>{children}</ul>
        </div>
      </div>
    </MenuContext.Provider>
  );
};

export default HeadMenu;

我们看下上面涉及的css样式

.t-head-menu {
  position: relative;
  background-color: #fff;
}
.t-head-menu__inner {
    display: flex;
    height: 64px;
}
.t-head-menu .t-menu {
    flex: 1;
    display: flex;
    align-items: center;
}

.t-menu {
    list-style: none;
    padding: 0;
    margin: 0;
}

接着看Submenu组件

const Submenu: FC<SubMenuWithCustomizeProps> = (props) => {
  // className 自定义Submenu的class
  // style 自定义Submenu的style
  // children Submenu包裹的子元素
  // title  Submenu这一级的名字,也就是menu上显示的名字
  // value 表示某个菜单项被点击了,菜单项唯一标识
  const { className, style, children, title, value } = props;
  const { active, onChange } = useContext(MenuContext);
  const { classPrefix } = useConfig();
  const [open, setOpen] = useState(false);
  const popRef = useRef<HTMLUListElement>();

  const handleClick = () => onChange(value);

  // 当前二级导航激活
  const isActive = checkSubMenuChildrenActive(children, active) || active === value;

  // 鼠标移入移除会触发open变量的改变,导致类名的显示和隐藏
  const handleMouseEvent = (type: 'leave' | 'enter') => {
    if (type === 'enter') setOpen(true);
    else if (type === 'leave') setOpen(false);
  };


  const showPopup = React.children.toArray(children).length > 0;

  return (
    <li
      className={classNames(`${classPrefix}-submenu`, className, {
        [`${classPrefix}-is-opened`]: open,
      })}
      onMouseEnter={() => handleMouseEvent('enter')}
      onMouseLeave={() => handleMouseEvent('leave')}
    >
      <div
        className={classNames(`${classPrefix}-menu__item`, {
          [`${classPrefix}-is-active`]: isActive,
        })}
        onClick={handleClick}
        style={style}
      >
        <span>{title}</span>
      </div>
      {showPopup && (
        <div
          className={classNames(`${classPrefix}-menu__popup`, {
            [`${classPrefix}-is-opened`]: true,
          })}
        >
          <ul className={classNames(`${classPrefix}-menu__popup-wrapper`)}>{children}</ul>
        </div>
      )}
    </li>
  );
};

对应的css

.t-menu .t-submenu {
  position: relative;
}

.t-menu__item.t-is-active {
  color: var(--td-font-gray-1);
  background-color: var(--td-gray-color-2);
}


.t-menu__item {
  min-width: 104px;
  position: relative;
  display: flex;
  align-items: center;
  text-align: center;
  cursor: pointer;
  text-overflow: ellipsis;
  box-sizing: border-box;
  white-space: nowrap;
}
.t-menu__popup.t-is-opened .t-menu__popup:before {
  content: "";
  display: block;
  position: absolute;
  left: -16px;
  width: 16px;
  top: 0;
  bottom: 0;
}

MenuItem就很简单了,可以看做就是一个div而已

const MenuItem: FC<MenuItemProps> = (props) => {
  const {
    content,
    children = content,
    disabled,
    href,
    target = '_self',
    value,
    className,
    style,
    icon,
    onClick,
  } = props;
  const { classPrefix } = useConfig();

Ω

  const { onChange, setState, active } = useContext(MenuContext);

  const handeClick = (e: React.MouseEvent<HTMLLIElement>) => {
    e.stopPropagation();
    if (disabled) return;

    onClick && onClick({ e });
    onChange(value);
    setState({ active: value });
  };

  return (
    <li
      className={classNames(`${classPrefix}-menu__item`, className, {
        [`${classPrefix}-is-disabled`]: disabled,
        [`${classPrefix}-is-active`]: value === active,
      })}
      style={{ ...style }}
      onClick={handleClick}
    >
      {href ? (
        <a href={href} target={target} className={classNames(`${classPrefix}-menu__item-link`)}>
          <span className={`${classPrefix}-menu__content`}>{children}</span>
        </a>
      ) : (
        <span className={`${classPrefix}-menu__content`}>{children}</span>
      )}
    </li>
  );
};

© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容