引言
在写 Markdown 转微信公众号, 又何必借助于其他工具?? 过程中, 参考了 antd 官方文档 中工具栏的样式, 发现了一个有意思的交互:
本文主要就是想着不用 JS
纯 CSS
复刻一个差不多的交互菜单, 最后完成的效果如下:
下面开始复刻....
补充: 本文案例展示点击这里(css/pop-up-menu), 源码点这儿(css/PopUpMenu)....
一、分析
不会分析, 就随便分析分析.....
功能分析:
- 点击按钮展开菜单, 按钮变为关闭按钮
- 点击关闭按钮, 菜单收起
- 点击页面空白区域, 菜单收起
- 菜单展开收起, 透明度、位置发生过渡动画
难点: 这里难点可能在于, 如何记录菜单展开、收起状态? 目前能想到的就是通过 表单
+ 伪类
来实现
二、布局
第一步, 先完成基本的布局: 基本代码、样式如下
jsx
// React 代码
import React from 'react';
import scss from './index.module.scss';
import {
ToolOutlined,
AppleOutlined,
CloseOutlined,
DeleteOutlined,
PieChartOutlined,
AliwangwangOutlined,
} from '@ant-design/icons';
export default () => (
<div className={scss.body}>
<div className={scss.mene}>
<div className={scss['mene-item-wrapper']}>
<div className={scss['mene-item']}>
<AliwangwangOutlined />
</div>
<div className={scss['mene-item']}>
<DeleteOutlined />
</div>
<div className={scss['mene-item']}>
<PieChartOutlined />
</div>
<div className={scss['mene-item']}>
<AppleOutlined />
</div>
</div>
<div className={scss['mene-toggle']}>
<ToolOutlined />
</div>
</div>
</div>
);
scss
// scss 样式
.body {
width: 100%;
height: 100%;
background-color: #fff;
position: relative;
}
.mene {
top: 100px;
left: 100px;
position: absolute;
overflow: hidden;
}
.mene-item,
.mene-toggle {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #fff;
box-shadow: 0 0 10px rgba($color: #000, $alpha: 10%);
margin: 10px;
display: flex;
align-items: center;
justify-content: center;
}
最终效果如下:
三、记录状态
在 React
中我们可能会通过 useState
声明一个状态, 来记录 UI
的交互状态, 但是如果不用任何 JS
我们有啥办法可以记录状态呢?
3.1 checkbox 配合 :checked
最开始想到的通过 checkbox
来记录状态, 并且通过 :checked
伪类来选中 勾选状态
的元素, 同时配合兄弟选择器 ~
来选中同层级的元素, 下面我们来看个简单 DEMO
如下代码所示:
label
关联到同层级的checkbox
表单label
本身展示为一个按钮, 按钮文案默认是展开
, 当checkbox
选中时按钮显示收起
- 然后通过
#checkbox:checked ~ label
选中checkbox
勾选状态下的同层级兄弟元素label
- 同理通过
#checkbox:checked ~ .content
选中checkbox
勾选状态下的同层级兄弟元素.content
html
<style>
.content {
width: 500px;
max-height: 46px;
overflow: hidden;
transition: all 0.4s;
margin-bottom: 20px;
}
label {
padding: 2px 10px;
border-radius: 4px;
background-color: #4096ff;
}
label::before {
content: '展开';
}
#checkbox:checked ~ label::before {
content: '收起';
}
#checkbox:checked ~ .content {
max-height: 400px;
}
</style>
<input type="checkbox" id="checkbox" />
<div class="content">
一个民族的复兴需要强大的物质力量,也需要强大的精神力量。党的十八大以来,以习近平同志为核心的党中央总揽全局,把宣传思想文化工作摆在重要位置,指引宣传思想文化事业在举旗定向、正本清源中取得历史性成就、发生历史性变革,在守正创新、开拓进取中展现新气象、迈向新征程。
</div>
<label for="checkbox"></label>
上面代码效果如下: 点击按钮将改变 checkbox
的状态, 长文本高度又受控于 checkbox
的状态
3.2 input 配合 :focus
下面介绍另一种方法, 也是本文案例最后选用的方案, 那就是通过 input
配合 :focus
来实现, 下面我们对上文的 DEMO
进行改造, 如下代码:
- 两个
label
, 一个关联到同层级的input
表单, 一个关联到不存在的表单(目的是为了让input
失焦) - 两个
label
展示为两个按钮, 点击展开
按钮,input
将获取到焦点, 点击收起
或者其他空白位置,input
将失去焦点 - 然后通过
#show:focus ~ .content
选中input
获取焦点状态下的同层级兄弟元素.content
, 然后为其设置max-height
- 同理通过
#show:focus ~ label[for=show]
选中input
获取焦点状态下的同层级兄弟元素label[for=show]
- 通过
#show:not(:focus) ~ label[for=hide]
选中input
没有
获取焦点状态下的同层级兄弟元素label[for=hide]
html
<style>
.content {
width: 500px;
max-height: 46px;
overflow: hidden;
transition: all 0.4s;
margin-bottom: 20px;
}
label {
padding: 2px 10px;
border-radius: 4px;
background-color: #4096ff;
}
#show:focus ~ .content {
max-height: 400px;
}
#show:focus ~ label[for=show] {
display: none;
}
#show:not(:focus) ~ label[for=hide] {
display: none;
}
</style>
<div class="container">
<input id="show" />
<div class="content">
一个民族的复兴需要强大的物质力量,也需要强大的精神力量。党的十八大以来,以习近平同志为核心的党中央总揽全局,把宣传思想文化工作摆在重要位置,指引宣传思想文化事业在举旗定向、正本清源中取得历史性成就、发生历史性变革,在守正创新、开拓进取中展现新气象、迈向新征程。
</div>
<label for="hide">收起</label>
<label for="show">展开</label>
</div>
上面代码效果如下: 点击按钮、页面空白区域, 将改变 input
的获取焦点的状态, 长文本高度又受控于 input
的状态
3.3 回到正题
上面两小节主要就是介绍了两种 CSS
记录 UI
状态的方法, 下面回到正题: 使用 input
配合 :focus
来记录菜单展开收起的一个状态
下面直接看代码:
- 新增一个
input
表单, 用于记录UI
状态:input
获取焦点则打开菜单栏 - 新增两个
label
标签做切换按钮- 一个绑定上方
input
表单, 点击将使input
获取到焦点(展示菜单) - 另一个则绑定不存在的一个表单, 点击将使得
input
失去焦点(隐藏菜单) - 点击空白区域(非
input
标签), 将使得input
失去焦点(隐藏菜单)
- 一个绑定上方
- 新增
CSS
样式- 设置
label
样式, 比较常规, 就不展开了 - 通过
input:focus
选中获取焦点的input
表单, 此时菜单应该是打开状态, 需要隐藏显示菜单的按钮
(这里是通过兄弟选择器~
来实现) - 通过
input:not(:focus)
选中失去焦点的input
表单, 此时菜单应该是关闭状态, 需要隐藏关闭菜单的按钮
(这里是通过兄弟选择器~
来实现)
- 设置
diff
// React 代码
export default () => (
<div className={scss.body}>
<div className={scss.mene}>
+ <input id="open" />
<div className={scss['mene-item-wrapper']}>
.....
</div>
<div className={scss['mene-toggle']}>
+ <label htmlFor="open">
+ <ToolOutlined />
+ </label>
+ <label htmlFor="close">
+ <CloseOutlined />
+ </label>
</div>
</div>
</div>
);
diff
// scss 样式
+ // 设置 label 样式
+ .mene-toggle {
+ label {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ }
+ // 获取焦点(开启状态)
+ input:focus {
+ ~ .mene-toggle label[for="open"] {
+ display: none;
+ }
+ }
+ // 失去焦点(关闭状态)
+ input:not(:focus) {
+ ~ .mene-toggle label[for="close"] {
+ display: none;
+ }
+ }
到此, 代码效果如下:
- 点击
🔧
按钮,input
获取焦点, 此时会隐藏🔧
按钮, 展示关闭按钮 - 点击关闭按钮,
input
失去焦点, 此时会隐藏关闭按钮, 展示🔧
按钮 - 点击页面空白位置(非
input
),input
失去焦点, 此时会隐藏关闭按钮, 展示🔧
按钮
四、动画
上面我们成功记录了 UI
的一个状态, 剩下就差菜单列表隐藏、显示动画了
直接看代码:
.mene-item-wrapper
主要就是设置复用的样式: 设置transform
缩放的基点、设置过渡属性transition
input:not(:focus)
状态(关闭菜单)下, 通过~ .mene-item-wrapper
将菜单列表通过opacity
以及transform
将其隐藏起来input:focus
状态(关闭菜单)下, 通过~ .mene-item-wrapper
将菜单显示出来
diff
// scss 样式
+ .mene-item-wrapper {
+ transition: all 0.4s;
+ transform-origin: 30px bottom;
+ }
// 获取焦点(开启状态)
input:focus {
~ .mene-toggle label[for="open"] {
display: none;
}
+ // 显示菜单
+ ~ .mene-item-wrapper {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
}
// 失去焦点(关闭状态)
input:not(:focus) {
~ .mene-toggle label[for="close"] {
display: none;
}
+ // 隐藏菜单
+ ~ .mene-item-wrapper {
+ opacity: 0;
+ transform: scale(0) translateY(200px);
+ }
}
最后看下实际效果:
- 点击
🔧
按钮, 显示菜单 - 点击关闭按钮, 隐藏菜单
- 点击空白区域(非
input
区域), 隐藏菜单
五、收尾
认真的同学, 应该会发现在菜单列表隐藏、显示过程中, 菜单列表和切换按钮层级是有问题的:
关于层级问题, 脑海里第一反应就是设置一个 z-index: 9999
, 但如果真去设置了会发现并没有什么用, 甚至 z-index
属性都没生效:
如上图, 其实 z-index
属性没有生效的原因, 浏览器已经给我们提示了!!! 这里只需要为 切换按钮
设置个 position: relative;
即可, 目的是为了开启层叠上下文, 至于 z-index
其实这里并不需要再设置, 因为默认情况下, 在同层级的 DOM
列表中, 默认情况下后一个元素的层级是高于前面的元素的; 所以这里只需要做如下代码调整即可:
diff
// scss 样式
.mene-toggle {
+ position: relative;
label {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}
这样层级问题就解决了, 来看效果:
最后, 我们还需要将 input
输入框隐藏掉, 这里方法就很多咯!! 这边我直接将其宽度、高度、边框、边距设置为 0
:
diff
+ input {
+ width: 0;
+ height: 0;
+ border: 0;
+ padding: 0;
+ }
好了, 本文到处结束🔚 喜欢的点个赞、收藏呗