You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

同时切换子菜单(含高度过渡)导致新菜单移出视口问题求助

解决导航栏子菜单切换时的滚动偏移问题

问题场景

导航栏子菜单展开/收起带有高度过渡动画,同一时间仅允许一个子菜单打开。当用户点击已展开子菜单下方的新子菜单时,旧菜单收起、新菜单展开的动画同时执行,会导致导航栏滚动条下移,新打开的子菜单标题(如"Submenu 2")移出视口。需要在保持两个动画同步进行的前提下,确保新子菜单标题始终处于导航栏视口内。

解决方案核心思路

通过计算新旧子菜单高度变化的差值,在动画执行过程中用requestAnimationFrame动态修正导航栏的滚动位置,抵消容器总高度变化带来的滚动偏移。具体步骤:

  1. 切换子菜单前,记录新子菜单标题的当前位置(相对于导航栏顶部的偏移)
  2. 计算旧菜单收起减少的高度与新菜单展开增加的高度的差值
  3. 在动画周期内,根据动画进度动态调整滚动位置,让新菜单标题保持在视口内

修改后的完整代码

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);
}

关键修改说明

  1. 修正了CSS中.submenu-title的height:50%错误,改为min-height:52px,避免布局异常
  2. 在子菜单点击事件中,计算旧菜单收起和新菜单展开的高度差,通过requestAnimationFrame在动画周期内逐步调整滚动位置,抵消容器高度变化带来的偏移
  3. 动画结束后进行最终修正,确保新子菜单标题稳定处于视口内

内容的提问来源于stack exchange,提问作者Amjed Omar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 06:34:52