纯 HTML/CSS/JS 实现下拉菜单组件

393 阅读2分钟

纯 HTML/CSS/JS 实现下拉菜单组件

功能概述

本下拉菜单组件实现以下核心功能:

  • 点击/悬停触发显示
  • 自动定位与边界检测
  • 平滑过渡动画
  • 点击菜单项自动关闭
  • 外部点击/ESC 键关闭
  • 响应式布局

实现原理

1. 事件处理机制

  • 触发逻辑:通过 clickmouseenter 事件监听触发元素
  • 冒泡控制:使用 e.stopPropagation() 阻止菜单内部事件传播
  • 全局监听:在 document 上注册点击事件实现外部关闭检测
  • 键盘交互:监听 ESC 键关闭菜单

2. 定位系统

  • 初始定位:通过 getBoundingClientRect() 获取触发元素位置
  • 滚动补偿:结合 pageXOffset/pageYOffset 计算滚动偏移
  • 边界检测:动态检测视口边缘并自动调整位置

3. 动画系统

  • CSS 过渡动画:使用 transition 实现透明度与位移动画
  • 类名控制:通过 active 类名切换显示状态

代码解析

HTML 结构

<button class="dropdown-trigger">显示菜单</button>
<div class="dropdown-overlay">
  <ul class="dropdown-menu">
    <li>新建文件</li>
    <li>打开文件</li>
    <li>保存内容</li>
    <li>退出系统</li>
  </ul>
</div>

CSS 样式

body {
    padding: 50px;
    font-family: Arial, sans-serif;
}

.dropdown-trigger {
    padding: 8px 16px;
    background: #007bff;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    position: relative;
}

.dropdown-overlay {
    position: absolute;
    background: white;
    border: 1px solid #ddd;
    border-radius: 4px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.15);
    opacity: 0;
    transform: translateY(-10px);
    transition: opacity 0.2s, transform 0.2s;
    pointer-events: none;
    z-index: 1000;
    min-width: 150px;
}

.dropdown-overlay.active {
    opacity: 1;
    transform: translateY(0);
    pointer-events: auto;
}

.dropdown-menu {
    list-style: none;
    margin: 0;
    padding: 8px 0;
}

.dropdown-menu li {
    padding: 8px 16px;
    cursor: pointer;
    transition: background 0.2s;
}

.dropdown-menu li:hover {
    background: #f5f5f5;
}

JavaScript 逻辑

// 定位计算
function positionDropdown() {
  const triggerRect = trigger.getBoundingClientRect();
  overlay.style.left = `${triggerRect.left + window.pageXOffset}px`;
  overlay.style.top = `${triggerRect.bottom + window.pageYOffset + 4}px`;
}

// 边界检测
function checkBoundary() {
  if (overlayRect.bottom > viewportHeight) {
    overlay.style.top = `${triggerRect.top - overlayRect.height - 4}px`;
  }
}

// 事件监听
menuItems.forEach(item => {
  item.addEventListener('click', () => toggleDropdown(false));
});

## 完整实现代码
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Dropdown with Item Click</title>
    <style>
        body {
            padding: 50px;
            font-family: Arial, sans-serif;
        }

        .dropdown-trigger {
            padding: 8px 16px;
            background: #007bff;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            position: relative;
        }

        .dropdown-overlay {
            position: absolute;
            background: white;
            border: 1px solid #ddd;
            border-radius: 4px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.15);
            opacity: 0;
            transform: translateY(-10px);
            transition: opacity 0.2s, transform 0.2s;
            pointer-events: none;
            z-index: 1000;
            min-width: 150px;
        }

        .dropdown-overlay.active {
            opacity: 1;
            transform: translateY(0);
            pointer-events: auto;
        }

        .dropdown-menu {
            list-style: none;
            margin: 0;
            padding: 8px 0;
        }

        .dropdown-menu li {
            padding: 8px 16px;
            cursor: pointer;
            transition: background 0.2s;
        }

        .dropdown-menu li:hover {
            background: #f5f5f5;
        }
    </style>
</head>
<body>
    <button class="dropdown-trigger">显示菜单</button>
    <div class="dropdown-overlay">
        <ul class="dropdown-menu">
            <li>新建文件</li>
            <li>打开文件</li>
            <li>保存内容</li>
            <li>退出系统</li>
        </ul>
    </div>

    <script>
        const trigger = document.querySelector('.dropdown-trigger');
        const overlay = document.querySelector('.dropdown-overlay');
        const menuItems = document.querySelectorAll('.dropdown-menu li');

        // 定位函数
        function positionDropdown() {
            const triggerRect = trigger.getBoundingClientRect();
            overlay.style.left = `${triggerRect.left + window.pageXOffset}px`;
            overlay.style.top = `${triggerRect.bottom + window.pageYOffset + 4}px`;
        }

        // 切换显示状态
        function toggleDropdown(show) {
            overlay.classList.toggle('active', show);
            if (show) {
                positionDropdown();
                checkBoundary();
            }
        }

        // 新增:菜单项点击处理
        function handleMenuItemClick(e) {
            toggleDropdown(false);
            console.log('选择了:', e.target.textContent);
            // 这里可以添加具体的菜单项处理逻辑
        }

        // 为所有菜单项绑定点击事件
        menuItems.forEach(item => {
            item.addEventListener('click', handleMenuItemClick);
        });

        // 其他原有代码保持不变...
        // (保持之前的边界检测、事件监听等代码)

        // 边界检测函数
        function checkBoundary() {
            const overlayRect = overlay.getBoundingClientRect();
            const viewportWidth = window.innerWidth;
            const viewportHeight = window.innerHeight;

            if (overlayRect.right > viewportWidth) {
                overlay.style.left = `${viewportWidth - overlayRect.width - 8}px`;
            }

            if (overlayRect.bottom > viewportHeight) {
                overlay.style.top = `${trigger.getBoundingClientRect().top + window.pageYOffset - overlayRect.height - 4}px`;
            }
        }

        trigger.addEventListener('click', (e) => {
            e.stopPropagation();
            toggleDropdown(!overlay.classList.contains('active'));
        });

        document.addEventListener('click', (e) => {
            if (!overlay.contains(e.target) && !trigger.contains(e.target)) {
                toggleDropdown(false);
            }
        });

        // trigger.addEventListener('mouseenter', () => toggleDropdown(true));
        // overlay.addEventListener('mouseleave', () => toggleDropdown(false));

        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape') {
                toggleDropdown(false);
            }
        });

        window.addEventListener('resize', () => {
            if (overlay.classList.contains('active')) {
                positionDropdown();
                checkBoundary();
            }
        });
    </script>
</body>
</html>

扩展建议

  1. 动画优化
.dropdown-overlay {
  transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1),
              transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
  1. 无障碍支持
<button 
  aria-haspopup="true"
  aria-expanded="false"
  aria-label="操作菜单"
>
  显示菜单
</button>
  1. 多级菜单
function initSubmenu() {
  document.querySelectorAll('.submenu').forEach(menu => {
    menu.previousElementSibling.addEventListener('click', (e) => {
      e.stopPropagation();
      menu.classList.toggle('show');
    });
  });
}

注意事项

  1. 层级问题:确保菜单的 z-index 高于页面其他元素
  2. 移动端适配:添加 touchstart 事件支持
  3. 性能优化:使用防抖技术处理 resize 事件
  4. 服务端渲染:动态特性需在 DOMContentLoaded 后初始化

本实现方案通过纯前端技术实现了企业级下拉菜单的核心功能,可根据具体需求进行功能扩展和样式定制。