前言
HeadMenu的样式如下:
其中核心功能百分之95%是靠css实现的,参考的T-deisgn的源码,感觉比较巧妙,并且代码很容易理解,分享一下。
非常有意思的css布局
上图这部分布局是css实现的,同志们,你们有思路怎么实现不?首先提醒是绝对定位,你去访问的各种官网的导航栏的绝大多数都是绝对定位,因为你不可能说我展开菜单把下面的dom元素撑下去,是吧!
我们来说布局这个css的难点在哪里!先把基本的数据结构交代一下 用数据结构表示一下:
{name: '电器', // Submenu组件children: [{name: '电视', // Submenu组件children: [{name: '索尼电视' // MenuItem组件},{name: '华为电视' // MenuItem组件}]},{name: '冰箱' // MenuItem组件}]
}
如上,导航栏组件主要由Submenu组件和MenuItem组件组成。
难点一 绝对定位的元素的父元素如果是inline-block,并且没设置宽度,那么这个绝对定位的元素宽度就是0,高度也是0
我在看源码的时候被一个css搞的有点懵,我们简化一下问题,jsx的结构如下:
<div className="head-menu"><div className="submenu"><div className="menu__item">电器</div><divclassName="menu__popup">下拉框内容</div></div></div>
css如下:
.head-menu {margin: 0;padding: 0;position: relative;height: 30px;
}
.submenu {position: relative;
}
.menu__popup {position: absolute;
}
结果如下:
我们往
.head-menu {margin: 0;padding: 0;position: relative;height: 30px;
}
.head-menu的css中加一条display: flex
, 表现如下:
这是咋回事呢?
我们肯定知道是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个难点做铺垫。
我的需求如下图,鼠标移入电器显示下拉框,移除关闭下拉框
在实现代码之前,先写一个工具类,目的是为了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"><divclassName={classnames({"is-opened": open},"submenu")}onMouseEnter={() => setOpen(true)}onMouseLeave={() => setOpen(false)}><div className="menu__item">电器</div><divclassName={classnames("menu__popup", {"is-opened": open})}>下拉框内容</div></div></div>);
}
难点三 移入空隙为啥下拉框没有关闭
Submenu组件和Menu组件之间有一段空白区域,如下图,我们鼠标移入的时候,是不是也要保持下方展开状态,要不这个menu组件也太不好用了。
这里用的小技巧就是点电器这个元素内使用一个伪类::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就能实现,可是这是绝对定位,咋实现呢?)
技巧:
.menu__popup {position: absolute;top: 0;left: calc(100% + 16px);
}
关键代码就是left,这里100%是啥意思呢,就是父元素的宽度,如下图:
好了,如果你的业务遇到类似需求,这篇文章希望能帮到你。
意外发现的一个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}><divclassName={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包裹的子元素// titleSubmenu这一级的名字,也就是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 (<liclassName={classNames(`${classPrefix}-submenu`, className, {[`${classPrefix}-is-opened`]: open,})}onMouseEnter={() => handleMouseEvent('enter')}onMouseLeave={() => handleMouseEvent('leave')}><divclassName={classNames(`${classPrefix}-menu__item`, {[`${classPrefix}-is-active`]: isActive,})}onClick={handleClick}style={style}><span>{title}</span></div>{showPopup && (<divclassName={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 (<liclassName={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>);
};
最后
最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。
有需要的小伙伴,可以点击下方卡片领取,无偿分享