这个css布局样式真巧妙啊!react导航栏组件:Headmenu实现
前言
HeadMenu的样式如下:
其中核心功能百分之95%是靠css实现的,参考的T-deisgn的源码,感觉比较巧妙,并且代码很容易理解,分享一下。
下面讲解难点的在线demo如下:
非常有意思的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>
<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;
}
结果如下:
我们往
.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">
<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组件也太不好用了。
这里用的小技巧就是点电器这个元素内使用一个伪类::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}>
<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>
);
};
转载自:https://juejin.cn/post/7172008374056255495