同时切换子菜单(含高度过渡)导致新菜单移出视口问题求助
解决导航栏子菜单切换时的滚动偏移问题
问题场景
导航栏子菜单展开/收起带有高度过渡动画,同一时间仅允许一个子菜单打开。当用户点击已展开子菜单下方的新子菜单时,旧菜单收起、新菜单展开的动画同时执行,会导致导航栏滚动条下移,新打开的子菜单标题(如"Submenu 2")移出视口。需要在保持两个动画同步进行的前提下,确保新子菜单标题始终处于导航栏视口内。
解决方案核心思路
通过计算新旧子菜单高度变化的差值,在动画执行过程中用requestAnimationFrame动态修正导航栏的滚动位置,抵消容器总高度变化带来的滚动偏移。具体步骤:
- 切换子菜单前,记录新子菜单标题的当前位置(相对于导航栏顶部的偏移)
- 计算旧菜单收起减少的高度与新菜单展开增加的高度的差值
- 在动画周期内,根据动画进度动态调整滚动位置,让新菜单标题保持在视口内
修改后的完整代码
HTML
<div class="backdrop"></div> <div class="navbar">Loading...</div> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
CSS
* { box-sizing: border-box; margin: 0; padding: 0; } .backdrop { position: fixed; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); top: 0; left: 0; } .navbar { position: fixed; width: 100%; height: 80%; z-index: 100; background-color: #fff; bottom: 0; left: 0; overflow-y: scroll; } .submenu-title { display: flex; align-items: center; padding: 16px 8px; width: 100%; border-bottom: 1px solid #9e9e9e; cursor: pointer; min-height: 52px; } .submenu-content { background-color: #f0f0f0; transition: height 325ms ease; overflow: hidden; } .submenu-item { padding: 16px 8px; height: 50px; border-bottom: 1px solid #9e9e9e; cursor: pointer; }
JavaScript
generateNavbar(".navbar"); $(".submenu-title").on("click", function() { const $submenuTitle = $(this); const $submenu = $submenuTitle.closest(".submenu"); const $navbar = $(".navbar"); // 获取当前打开的旧子菜单 const $oldSubmenu = $submenu.siblings(".submenu.opened"); let oldContentHeight = 0; if ($oldSubmenu.length) { oldContentHeight = $oldSubmenu.find(".submenu-content")[0].scrollHeight; } // 记录新子菜单标题的初始偏移(相对于navbar顶部) const targetTitleOffset = $submenuTitle.offset().top - $navbar.offset().top; // 记录当前滚动位置 const initialScrollTop = $navbar.scrollTop(); // 计算需要修正的滚动偏移量:旧菜单收起减少的高度 - 新菜单展开增加的高度 const newContentHeight = $submenu.find(".submenu-content")[0].scrollHeight; const scrollAdjustDelta = oldContentHeight - newContentHeight; // 先处理旧菜单关闭 $oldSubmenu.each(function() { toggleSubmenu($(this), false); }); // 打开新菜单 toggleSubmenu($submenu, !$submenu.hasClass("opened")); // 如果有需要调整的偏移,在动画期间动态修正滚动位置 if ($oldSubmenu.length && scrollAdjustDelta !== 0) { const animationDuration = 325; // 和CSS过渡时长一致 const startTime = Date.now(); function adjustScroll() { const elapsed = Date.now() - startTime; if (elapsed >= animationDuration) { // 动画结束后最终修正,确保标题在视口内 const finalOffset = $submenuTitle.offset().top - $navbar.offset().top; if (finalOffset < 0 || finalOffset > $navbar.height() - $submenuTitle.height()) { $navbar.scrollTop(initialScrollTop + (targetTitleOffset - finalOffset) + scrollAdjustDelta); } return; } // 按动画进度计算当前需要调整的滚动值 const progress = elapsed / animationDuration; const currentAdjust = scrollAdjustDelta * progress; $navbar.scrollTop(initialScrollTop + currentAdjust); requestAnimationFrame(adjustScroll); } requestAnimationFrame(adjustScroll); } }); $(".submenu-content").on("transitionend", function() { const $submenuContent = $(this); if ($submenuContent.hasClass("opened")) { $submenuContent.css("height", ""); } }); function toggleSubmenu($submenu, isOpen) { const $submenuContent = $submenu.find(".submenu-content"); if (isOpen) { $submenuContent.css("height", $submenuContent.get(0).scrollHeight); } else { $submenuContent.css("height", $submenuContent.get(0).scrollHeight); // reflow $submenuContent.get(0).offsetHeight; $submenuContent.css("height", 0); } $submenu.toggleClass("opened"); } function generateNavbar(selector) { // data const submenusItemsCount = [ 36, 27, 14, 1, 6, 3, 23, 50, 21, 4, 5, 50, 3, 12, 4, 4, 6, 9, 20, 2, ]; const submenus = submenusItemsCount.map( (submenuItemsCount, submenuIndex) => ({ title: `Submenu ${submenuIndex + 1}`, items: [...new Array(submenuItemsCount)].map( (_, itemIndex) => `Submenu ${submenuIndex + 1} - Item ${itemIndex + 1}` ), }) ); // dom const $navbar = $(selector); const $submenus = submenus.map((submenu) => { const $submenu = $('<div class="submenu" data-submenu />'); const $submenuTitle = $( `<div class="submenu-title" data-submenu-title>${submenu.title}</div>` ); const $submenuContent = $( `<div class="submenu-content" style="height: 0" data-submenu-content />` ); submenu.items.map((submenuItem) => { $submenuContent.append( `<div class="submenu-item" data-submenu-item>${submenuItem}</div>` ); }); $submenu.append($submenuTitle); $submenu.append($submenuContent); return $submenu; }); $navbar.html($submenus); }
关键修改说明
- 修正了CSS中
.submenu-title的height:50%错误,改为min-height:52px,避免布局异常 - 在子菜单点击事件中,计算旧菜单收起和新菜单展开的高度差,通过
requestAnimationFrame在动画周期内逐步调整滚动位置,抵消容器高度变化带来的偏移 - 动画结束后进行最终修正,确保新子菜单标题稳定处于视口内
内容的提问来源于stack exchange,提问作者Amjed Omar
相关产品推荐
相关产品推荐

