本文旨在指导开发者如何使用原生 JavaScript、html 和 css 构建一个响应式的多级下拉菜单。该菜单在桌面端采用悬停触发,而在移动端则切换为点击触发模式,无需依赖 jquery 库。我们将重点解决移动端点击事件无法正确展开子菜单的问题,并提供代码示例和优化建议,帮助读者构建用户体验良好的导航菜单。
HTML 结构
首先,我们来看一下 HTML 的基本结构。一个 div 元素作为菜单容器,内部包含一个无序列表 ul,列表项 li 可以包含链接 a 和嵌套的子列表 ul。
<div class="menu"> <ul> <li> Parent 1 <ul> <li class="link"> <a href="">Child 1</a> </li> <li class="link"> <a href="">Child 2</a> </li> <li> Nested child <ul> <li class="link"> <a href="">Something</a> </li> <li class="link"> <a href="">Something</a> </li> </ul> </li> </ul> </li> <li> Parent 2 <ul> <li class="link"> <a href="">Basic link 1</a> </li> <li class="link"> <a href="">Basic link 2</a> </li> <li> Nested child 2 <ul> <li class="link"> <a href="">Basic nested link 1</a> </li> <li class="link"> <a href="">Basic nested link 2</a> </li> </ul> </li> </ul> </li> <li class="link">Simple Link</li> <li class="link">Another Link</li> </ul> </div>
CSS 样式
接下来,我们定义 CSS 样式。关键在于实现悬停效果和移动端的显示/隐藏逻辑。通过媒体查询 @media 来区分桌面端和移动端。
.menu { --menu-height: 40px; box-sizing: border-box; position: fixed; top: 0; left: 0; width: 100vw; } .menu ul { list-style: none; padding: 16px; margin: 0; } .menu ul li, .menu ul li a { opacity: 0.8; color: #ffffff; cursor: pointer; transition: 200ms; text-decoration: none; white-space: nowrap; font-weight: 700; } .menu ul li a, .menu ul li a a { display: flex; align-items: center; height: 100%; width: 100%; } .menu ul li { padding-right: 36px; } .menu ul li::before { content: ""; width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 5px solid #ffa500; position: absolute; right: 8px; top: 50%; transform: translateY(-50%); } .menu ul .link::before { padding-right: 0; display: none; } .menu > ul { display: flex; height: var(--menu-height); align-items: center; background-color: #000000; } .menu > ul li { position: relative; margin: 0 8px; } .menu > ul li ul { visibility: hidden; opacity: 0; padding: 0; min-width: 160px; background-color: #333; position: absolute; top: 45px; left: 50%; transform: translateX(-50%); transition: 200ms; transition-delay: 200ms; } .menu > ul li ul li { margin: 0; padding: 8px 16px; display: flex; align-items: center; justify-content: flex-start; height: 30px; padding-right: 40px; } .menu > ul li ul li::before { width: 0; height: 0; border-top: 5px solid transparent; border-bottom: 5px solid transparent; border-left: 5px solid #ffa500; } .menu > ul li ul li ul { top: -2%; left: 100%; transform: translate(0); } .show { display: flex; } .hide { display: none; } @media screen and (min-width: 1200px) { .menu ul li:hover, .menu ul li a:hover { opacity: 1; } .menu > ul li ul li:hover { background: black; } .menu > ul li:hover > ul { opacity: 1; visibility: visible; transition-delay: 0ms; } } @media screen and (max-width: 1200px) { .menu { height: 100vh; top: 0; left: 0; width: 20vw; } .menu ul { flex-direction: column; height: 100vh; } .menu ul li { margin: 1rem 0; } .menu ul li ul { visibility: hidden; opacity: 0; padding: 0; min-width: 160px; background-color: #333; position: absolute; top: 0px; left: 200%; transform: translateX(-50%); transition: 200ms; transition-delay: 200ms; height: fit-content; } .menu ul.show { visibility: visible; opacity: 1; } }
关键点:
- 桌面端: 使用 :hover 伪类控制子菜单的显示和隐藏 (opacity: 1; visibility: visible;)。
- 移动端: 初始状态下,子菜单 ul 的 visibility 设置为 hidden 和 opacity 设置为 0。 通过添加 .show 类来改变 visibility 和 opacity。
JavaScript 交互
JavaScript 代码负责处理移动端的点击事件,切换子菜单的显示状态。
let menu = document.querySelector(".menu ul"); menu = menu.children; for (let i = 0; i < menu.length; i++) { console.log(menu[i]); menu[i].addEventListener("click", function() { menu[i].firstElementChild.classlist.toggle("show"); console.log(menu[i].firstElementChild); }); }
这段代码存在一个问题,它直接尝试给每个 li 元素添加点击事件,并切换其第一个子元素的 show 类。 这会导致只有第一层子菜单能够正常工作,更深层次的子菜单无法正确展开。
正确的做法是,首先判断当前是否是移动端,如果是,则添加点击事件,否则不添加。 并且,需要针对所有层级的 li 元素添加事件监听,并阻止事件冒泡,避免父元素和子元素同时触发点击事件。
以下是改进后的 JavaScript 代码:
document.addEventListener('domContentLoaded', function() { const menuItems = document.querySelectorAll('.menu ul li'); const mediaQuery = window.matchMedia('(max-width: 1200px)'); function handleMenuItemClick(event) { // 阻止事件冒泡,防止父元素同时触发 event.stopPropagation(); const subMenu = this.querySelector('ul'); if (subMenu) { subMenu.classList.toggle('show'); } } function attachEventListeners(matches) { menuItems.forEach(item => { item.removeEventListener('click', handleMenuItemClick); // 移除之前的监听器 if (matches) { item.addEventListener('click', handleMenuItemClick); } }); } // 初始化时执行一次 attachEventListeners(mediaQuery.matches); // 监听媒体查询变化 mediaQuery.addEventListener('change', (event) => { attachEventListeners(event.matches); }); });
代码解释:
- DOMContentLoaded 事件: 确保在 DOM 加载完成后执行 JavaScript 代码。
- querySelectorAll(‘.menu ul li’): 获取所有菜单项。
- window.matchMedia(‘(max-width: 1200px)’): 创建一个媒体查询对象,用于检测当前屏幕是否是移动端。
- handleMenuItemClick(event): 点击事件处理函数。
- event.stopPropagation(): 阻止事件冒泡,防止点击子菜单时,父菜单也触发点击事件。
- this.querySelector(‘ul’): 查找当前菜单项下的子菜单。
- subMenu.classList.toggle(‘show’): 切换子菜单的 show 类,控制其显示和隐藏。
- attachEventListeners(matches): 根据媒体查询结果,添加或移除点击事件监听器。
- item.removeEventListener(‘click’, handleMenuItemClick):在添加新的监听器之前,先移除之前的监听器,避免重复绑定。
- item.addEventListener(‘click’, handleMenuItemClick):添加点击事件监听器。
- mediaQuery.addEventListener(‘change’, (event) => { … }): 监听媒体查询的变化,当屏幕尺寸改变时,重新添加或移除点击事件监听器。
优化建议
- 语义化 HTML: 可以使用 <nav> 元素包裹菜单,使用 aria-label 属性增强可访问性。
- CSS 动画: 可以使用 CSS 动画来增强菜单的过渡效果,提升用户体验。
- 性能优化: 如果菜单项非常多,可以考虑使用事件委托来减少事件监听器的数量。
- 使用 <button> 替代 <span>: 对于可点击的菜单项,使用 <button> 元素可以更好地表达其交互行为,并且更易于使用键盘进行导航。
总结
通过以上步骤,我们实现了一个响应式的多级下拉菜单,可以在桌面端使用悬停,在移动端使用点击进行交互。 关键在于 CSS 样式的媒体查询和 JavaScript 的事件处理。 通过不断优化,可以创建一个用户体验良好的导航菜单。
评论(已关闭)
评论已关闭