skip to Main Content

Demo: https://codesandbox.io/p/sandbox/navbar-k2szsq (or see code snippet below)

I have a navbar that contains multiple submenus when the user open/close a submenu it opened/closed with a height transition. However only one submenu can be opened at a time that means if the user opened another submenu without closing the first one the previous one gets closed automatically and here’s the problem:

If the user open a submenu that is below to the previous opened submenu and both have a long content height and since both gets open/close with height transition at the same time it causes the scroll to scroll down away from the new submenu that user opened. (please run the below code snippet and do these steps: First open "Submenu 1" (and don’t close it) then open "Submenu 2" and you’ll notice the issue I described)

So is there is a smooth solution to solve this issue I know I can wait until both transitions end then scroll to the opened submenu (but that’s not a good user experience) or maybe first close the previous opened submenu wait until it is closed then open the new submenu (but that’s not what I want) So is there’s a smooth user-friendly solution to close the previous submenu and open the new submenu at the same time (both with height transition) while maintaining the new opened submenu to be on screen? (at least the new opened submenu button (e.g. "Submenu 2") is visible on the top edge of the navbar)

generateNavbar(".navbar");

$(".submenu-title").on("click", function() {
  const $submenuTitle = $(this);
  const $submenu = $submenuTitle.closest(".submenu");
  $submenu.siblings(".submenu.opened").each(function() {
    toggleSubmenu($(this), false);
  });
  toggleSubmenu($submenu, !$submenu.hasClass("opened"));
});

$(".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);
}
* {
  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%;
  height: 50%;
  border-bottom: 1px solid #9e9e9e;
  cursor: pointer;
}

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

3

Answers


  1. generateNavbar(".navbar");
    
    $(".submenu-title").on("click", function() {
      const $submenuTitle = $(this);
      const $submenu = $submenuTitle.closest(".submenu");
    
      // Calculate the offset before the transition
      const initialOffset = $submenuTitle.offset().top - $('.navbar').scrollTop();
    
      // Toggle the current submenu and close others
      $submenu.siblings(".submenu.opened").each(function() {
        toggleSubmenu($(this), false);
      });
      toggleSubmenu($submenu, !$submenu.hasClass("opened"));
    
      // Adjust scroll position after transition over
      $submenu.one('transitionend', function() {
        if ($submenu.hasClass("opened")) {
          const currentOffset = $submenuTitle.offset().top - $('.navbar').scrollTop();
          const scrollPosition = $('.navbar').scrollTop();
          $('.navbar').scrollTop(scrollPosition + (currentOffset - initialOffset));
        }
      });
    });
    
    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);
    }
    * {
      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%;
      height: 50%;
      border-bottom: 1px solid #9e9e9e;
      cursor: pointer;
    }
    
    .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;
    }
    <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>

    calculated the distance of the submenu from the top of the screen before any opening or closing initialOffset the opening and closing transitions of the submenus as usual toggleSubmenu after the transitions over adjust the scroll position based on the new position of the submenu scrollTop

    Login or Signup to reply.
  2. I have added a callback for the ToggleSubMenu to handl the transitionen event. which fired when the csstrasition is completed

            $(document).ready(function() {
                generateNavbar(".navbar");
    
                $(".submenu-title").on("click", function() {
                    const $submenuTitle = $(this);
                    const $submenu = $submenuTitle.closest(".submenu");
                    const navbar = $('.navbar');
    
                    $submenu.siblings(".submenu.opened").each(function() {
                        toggleSubmenu($(this), false);
                    });
    
                    toggleSubmenu($submenu, !$submenu.hasClass("opened"), function(openedSubmenu) {
                        if (openedSubmenu.hasClass("opened")) {
                            const offsetTop = openedSubmenu.offset().top - navbar.offset().top + navbar.scrollTop();
                            navbar.animate({ scrollTop: offsetTop }, 325);
                        }
                    });
                });
    
                function toggleSubmenu($submenu, isOpen, callback) {
                    const $submenuContent = $submenu.find(".submenu-content");
    
                    if (isOpen) {
                        $submenuContent.css("height", $submenuContent.get(0).scrollHeight);
                        $submenu.addClass("opened");
                    } else {
                        $submenuContent.css("height", $submenuContent.get(0).scrollHeight);
                        // reflow
                        $submenuContent.get(0).offsetHeight;
                        $submenuContent.css("height", 0);
                        $submenu.removeClass("opened");
                    }
    
                    $submenuContent.off('transitionend').on('transitionend', function() {
                        if (typeof callback === 'function') {
                            callback($submenu);
                        }
                    });
                }
    
                function generateNavbar(selector) {
                    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}`
                            ),
                        })
                    );
    
                    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);
                }
            });
    * {
      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%;
      height: 50%;
      border-bottom: 1px solid #9e9e9e;
      cursor: pointer;
    }
    
    .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;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
    <div class="backdrop"></div>
    <div class="navbar">Loading...</div>
    Login or Signup to reply.
  3. I changed your code a little bit,
    in your code you directly toggled the submenu without knowing whether it is opening or not so i create a variable isOpening for that purpose.
    i also added new function scrollIntoViewIfNeeded which checks if submenu is outside of viewport so it scrolls to that submenu

        <script>
      generateNavbar(".navbar");
    
      $(".submenu-title").on("click", function () {
        const $submenuTitle = $(this);
        const $submenu = $submenuTitle.closest(".submenu");
        const isOpening = !$submenu.hasClass("opened");
    
        $submenu.siblings(".submenu.opened").each(function () {
            toggleSubmenu($(this), false);
        });
        toggleSubmenu($submenu, isOpening);
    
        if (isOpening) {
            // Wait for the transition to complete before scrolling
            $submenu.find(".submenu-content").one("transitionend", function () {
                scrollIntoViewIfNeeded($submenuTitle);
            });
        }
      });
    
      function scrollIntoViewIfNeeded($element) {
        const rect = $element[0].getBoundingClientRect();
        if (rect.top < 0 || rect.bottom > window.innerHeight) {
          $element[0].scrollIntoView({ behavior: "smooth", block: "start" });
        }
      }
    
      $(".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);
      }
    
      
    </script>`
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search