仿 Antd 弹出式工具菜单(无 JS 版本)

引言

在写 Markdown 转微信公众号, 又何必借助于其他工具?? 过程中, 参考了 antd 官方文档 中工具栏的样式, 发现了一个有意思的交互:

本文主要就是想着不用 JSCSS 复刻一个差不多的交互菜单, 最后完成的效果如下:

下面开始复刻....

补充: 本文案例展示点击这里(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;
+ }

好了, 本文到处结束🔚 喜欢的点个赞、收藏呗

相关推荐
hackeroink1 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者3 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-3 小时前
验证码机制
前端·后端
燃先生._.4 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖5 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
唯之为之5 小时前
巧用mask属性创建一个纯CSS图标库
css·svg
m0_748235245 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240256 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar6 小时前
纯前端实现更新检测
开发语言·前端·javascript