skip to Main Content

I am working on a multi-page web app in HTML/CSS/JS + jQuery. I am trying to build a sidebar component that expands and collapses on mouseover / mouseout. The sidebar has links to other pages on the application. The expand/collapse is working nicely, except for a very specific use case, and I’m totally stumped.

I’m using a variable mini to keep track of whether the sidebar is in collapsed (mini) or expanded state. I dynamically update the sidebar width based on whether mini is true or false, so when the user moves the mouse over the sidebar, mini = false and the sidebar expands…and when the user moves the mouse out of the sidebar, mini = true and the sidebar collapses.

The bug:

  1. The user hovers mouse over sidebar to expand it

  2. The user clicks on a link and gets routed to a new page

  3. The user keeps the mouse hovering over the sidebar, so that when they land on the new page, the mouse is still over the sidebar

  4. Now, the expand/collapse function is working inversely, so that when the user moves the mouse away, the menu expands, and when the user moves the mouse into the div, the menu collapses and disappears. Frustrating!

I think what’s happening is that the sidebar is built in the collapsed state on page load by default. Somehow I need to be able to determine whether the user has the mouse hovering inside the sidebar div on page load, so I can build the sidebar accordingly. I’d super appreciate any ideas! See below for my code. I’m not able to simulate the page routing, but the overall idea should be clear. In my code example, the sidebar functions are working as intended, so I’ve included screen grabs of what happens in the actual application with the page routing enabled. See below. Thanks!

enter image description here
enter image description here

var mini = true;
const logo = $("#nav-logo");
let activeState = false;
let activePage;
let iconsArr = [
  "#dashboard-icon",
  "#projects-icon",
  "#assess-icon",
  "#config-icon",
];

let listItemsArr = [
  "#dashboard-list-item",
  "#projects-list-item",
  "#assess-list-item",
  "#config-list-item",
];

$(function() {
  attachClickListeners();
});

const toggleSidebar = () => {
  if (mini) {
    // sidebar
    $("#mySidebar").css("width", "225px");
    // main
    $("#main").css("margin-left", "225px");
    // logo
    $("#nav-logo").attr("src", "https://www.creativefabrica.com/wp-content/uploads/2018/11/Company-business-generic-logo-by-DEEMKA-STUDIO.jpg");
    $("#nav-logo").css("width", "120px");
    // Logo text
    $("#logo-text-container").show();
    // list item styling
    $(".list-item").css("padding-left", "12px");
    $(".list-item").css("margin-bottom", "4px");
    $(".list-item-text").show();
    // Active state and page
    if (activePage != undefined) {
      // Remove active state from non-active items
      listItemsArr.forEach((item) => {
        if (item[1] != activePage[0]) {
          $(item).removeClass("active");
        }
      });

      // Add active class
      $(`#${activePage}-icon`).removeClass("active");
      $(`#${activePage}-list-item`).addClass("active");
    }

    // mini variable
    this.mini = false;
  } else {
    // sidebar
    $("#mySidebar").css("width", "60px");
    // main
    $("#main").css("margin-left", "60px");
    // logo
    $("#nav-logo").attr("src", "https://www.creativefabrica.com/wp-content/uploads/2018/11/Company-business-generic-logo-by-DEEMKA-STUDIO.jpg");
    $("#nav-logo").css("width", "30px");
    // logo text
    $("#logo-text-container").hide();
    // list item styling
    $(".list-item").css("padding-left", "0px");
    $(".list-item").css("margin-bottom", "6px");
    $(".list-item-text").hide();

    // Active state and page
    if (activePage != undefined) {
      // Active state and page
      if (activePage != undefined) {
        // Remove active state from non-active items
        iconsArr.forEach((item) => {
          if (item[1] != activePage[0]) {
            $(item).removeClass("active");
          }
        });

        // Add active class to active item
        $(`#${activePage}-icon`).addClass("active");
        $(`#${activePage}-list-item`).removeClass("active");
      }
    }

    // mini variable
    this.mini = true;
  }
};

const attachClickListeners = () => {
  $("#dashboard-list-item").off();
  $("#dashboard-list-item").on("click", () => {
    if (!activeState) {
      activeState = true;
    }
    toggleActiveState("#dashboard-icon");
    activePage = "dashboard";
  });

  $("#projects-list-item").off();
  $("#projects-list-item").on("click", () => {
    if (!activeState) {
      activeState = true;
    }
    toggleActiveState("#projects-icon");
    activePage = "projects";
  });

  $("#assess-list-item").off();
  $("#assess-list-item").on("click", () => {
    if (!activeState) {
      activeState = true;
    }
    toggleActiveState("#assess-icon");
    activePage = "assess";
  });

  $("#config-list-item").off();
  $("#config-list-item").on("click", () => {
    if (!activeState) {
      activeState = true;
    }
    toggleActiveState("#config-icon");
    activePage = "config";
  });
};

const toggleActiveState = (id) => {
  let element = $(id);

  iconsArr.forEach((item) => {
    if (item === id) {
      $(element).addClass("active");
    } else {
      $(item).removeClass("active");
    }
  });
};
.active {
  background: lightblue;
}

main .sidebar {
  position: absolute;
  top: 0;
  right: 25px;
  font-size: 36px;
  margin-left: 50px;
}

#main {
  padding: 16px;
  margin-left: 85px;
  transition: margin-left 0.5s;
}

body {
  font-family: "Poppins", sans-serif;
  font-size: 14px;
  font-weight: 400;
}

.sidebar {
  height: 100vh;
  width: 60px;
  position: fixed;
  transition: 0.5s ease;
  left: 0;
  top: 0;
  /* padding-left: 15px; */
  padding-top: 9px;
  background-color: #fafafa;
  border-right: 1px solid #e6e6e6;
  white-space: nowrap;
  overflow-x: hidden;
  z-index: 1;
}

#nav-logo,
#logo-text {
  transition: 0.5s ease;
}

.body-text {
  height: 100%;
  width: 100%;
  margin-left: 250px;
  padding-top: 1px;
  padding-left: 25px;
}

#nav-logo {
  width: 30px;
  margin-left: 15px;
}

#logo-text-container {
  margin-left: 30px;
  display: none;
}

#logo-text {
  font-size: 18px;
  margin-block-start: 0em;
}

.list-item {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  cursor: pointer;
  border: 1px solid transparent;
  border-radius: 100px;
  margin-bottom: 7px;
}

.list-item:hover {
  background: lightblue;
}

.list-item-text {
  font-size: 14px;
  margin-top: 15px !important;
  display: none;
}

.li-text-margin-left {
  margin-left: 7px;
}

#add-assessment-list-item-text {
  margin-left: 4px;
}

#projects-list-item-text {
  margin-left: 1px;
}

#nav-menu-items {
  padding-inline-start: 7px;
  width: 206px;
  transition: 0.5s ease;
}

#nav-menu-items i {
  font-size: 1.2rem;
  /* margin-right: 0.7rem; */
  padding: 10px 10px;
  border-radius: 60px;
}

.list-item-text {
  margin-block-start: 0.2em;
}
<link href="https://kit.fontawesome.com/ee3b09a28a.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<!-- Sidebar -->
<div id="mySidebar" class="sidebar" onmouseover="toggleSidebar()" onmouseout="toggleSidebar()">
  <!-- Expanded Logo Image -->
  <div id="logo-image-container">
    <img src="https://www.creativefabrica.com/wp-content/uploads/2018/11/Company-business-generic-logo-by-DEEMKA-STUDIO.jpg" id="nav-logo" />
  </div>
  <!-- /Expanded Logo Image -->
  <!--  Expanded Logo Image Text -->
  <div id="logo-text-container">
    <p id="logo-text">Logo Text</p>
  </div>
  <!--  /Expanded Logo Image Text -->

  <!-- Menu Items -->
  <ul id="nav-menu-items">
    <li class="list-item" id="dashboard-list-item">
      <i class="fa-duotone fa-table-columns" id="dashboard-icon"></i>
      <p class="list-item-text li-text-margin-left">Dashboard</p>
    </li>

    <li class="list-item" id="projects-list-item">
      <i class="fa-duotone fa-rectangle-history-circle-user" id="projects-icon"></i>
      <p class="list-item-text" id="projects-list-item-text">Projects</p>
    </li>

    <li class="list-item" id="assess-list-item">
      <i class="fa-duotone fa-address-card" id="assess-icon"></i>
      <p class="list-item-text" id="add-assessment-list-item-text">
        Add Assessment
      </p>
    </li>

    <li class="list-item" id="config-list-item">
      <i class="fa-duotone fa-folder-gear" id="config-icon"></i>
      <p class="list-item-text li-text-margin-left">Configuration</p>
    </li>
  </ul>
  <!-- /Menu Items -->
</div>
<!-- /Sidebar -->

<!-- Main -->
<div id="main">
  <h2>Open/Collapse Sidebar on Hover</h2>
  <p>Hover over any part of the sidebar to open the it.</p>
  <p>To close the sidebar, move your mouse out of the sidebar.</p>
</div>
<!-- /Main -->

2

Answers


  1. Chosen as BEST ANSWER

    I got some helpful answers here. The solution proposed by @James in the first comment is the only thing that worked. I needed to pass a param to the toggleSidebar function that would track whether there was a pointerenter or pointerleave event and then behave accordingly.

    While @DaveB's answer did not solve my bug, it did tremendously optimize my code!

    Here's what my toggleSidebar function looks like now. Otherwise everything is the same as in DaveB's code.

    function init() {
      // all the same code from Dave B's example, see below for the only change:
    
      $(sidebarEl).on("pointerenter", () => {
        toggleSidebar("enter");
      });
      $(sidebarEl).on("pointerleave", () => {
        toggleSidebar("leave");
      });
    }
    
    // This function now takes a param, and chooses whether to collapse
    // or expand based on whether the param is 'enter' or 'leave'
    function toggleSidebar(pointer) {
      if (pointer === "leave") {
          // Collapse sidebar
          sidebarState = "collapse";
          sidebarEl.classList.remove("expand");
      } else if (pointer === "enter") {
          // Expand sidebar
          sidebarState = "expand";
          sidebarEl.classList.add("expand");
      }
    }
    

  2. An expand class name can be used as an alternative to passing a parameter and using the mini variable. The following solution removes the hardcoded CSS style settings in your JavaScript and moves it all to CSS, therefore, simplifying code.

    CSS Grid for layout

    The solution uses CSS Grid as the layout mechanism for the sidebar and the main content. This allows for setting a specific sidebar width without having to match/set any properties for the main content element. Your code sample sets margin-left on the main element to the value of the sidebar width in the expanded/collapsed state.

    Detecting mouse hover inside sidebar on page load

    The user keeps the mouse hovering over the sidebar, so that when they land on the new page, the mouse is still over the sidebar

    Somehow I need to be able to determine whether the user has the mouse hovering inside the sidebar div on page load, so I can build the sidebar accordingly.

    Per the following Stack Overflow posts, retrieving the mouse/pointer position to set the initial sidebar expand/collapse state is not possible on page load using document.elementFromPoint(x,y) or document.elementsFromPoint(x,y) (i.e. the plural version). The pointer requires movement to trigger a pointer move event
    in order to retrieve the x, y pointer coordinates.

    let sidebarEl;
    
    document.addEventListener("DOMContentLoaded", init);
    
    function init() {
      sidebarEl = document.querySelector("#mySidebar");
    
      const pageId = document.querySelector("#main").getAttribute("data-page-id");
      const selector = `.list-item[data-page-id="${pageId}"]`;
      const activeLiEl = sidebarEl.querySelector(selector);
      if (activeLiEl) {
        activeLiEl.classList.add("active");
      }
    
      // https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events
      // "Pointer events are DOM events that are fired for
      // a pointing device. They are designed to create a single
      // DOM event model to handle pointing input devices such as
      // a mouse, pen/stylus or touch (such as one or more fingers)."
      sidebarEl.addEventListener("pointerenter", toggleSidebar);
      sidebarEl.addEventListener("pointerleave", toggleSidebar);
    
      sidebarEl.querySelectorAll(".list-item").forEach(el => {
        el.addEventListener("click", listItemClick);
      });
    
      document.querySelector("#toggle-sidebar-overlap")
        .addEventListener("click", toggleSidebarOverlap);
    }
    
    function toggleSidebar(e) {
      if (sidebarEl.classList.contains("expand")) {
        // Collapse sidebar
        sidebarEl.classList.remove("expand");
      } else {
        // Expand sidebar
        sidebarEl.classList.add("expand");
      }
    }
    
    function listItemClick() {
      const pageId = this.getAttribute("data-page-id");
      console.log(`Go to page ${pageId}`);
      if (pageId) {
        window.location.href = `/${pageId}`;
      }
    }
    
    function toggleSidebarOverlap() {
      const method = document.querySelector("body")
        .classList.contains("overlapSidebar") ? "remove" : "add";
      document.querySelector("body").classList[method]("overlapSidebar");
    }
    :root {
      --sidebar-collapsed-width: 60px;
      --active-bg-color: lightblue;
      --hover-bg-color: #9ebfca;
    }
    
    body {
      font-family: "Poppins", sans-serif;
      font-size: 14px;
      font-weight: 400;
      height: 100vh;
      margin: 0;
      padding: 0;
    }
    
    
    /* -----------------------------------
        Sidebar and content layout
    */
    
    body {
      display: grid;
      grid-auto-flow: column;
      grid-template-columns: auto 1fr;
    }
    
    body.overlapSidebar {
      grid-template-columns: var(--sidebar-collapsed-width) 1fr;
    }
    
    
    /* -----------------------------------
        Sidebar
    */
    
    .sidebar {
      width: var(--sidebar-collapsed-width);
      font-size: 36px;
      transition: 0.5s ease;
      padding-top: 9px;
      background-color: #fafafa;
      border-right: 1px solid #e6e6e6;
      white-space: nowrap;
      overflow-x: hidden;
      z-index: 1;
    }
    
    .sidebar>ul {
      padding: 7px;
    }
    
    
    /* -----------------------------------
        Sidebar logo image and logo text
    */
    
    #nav-logo,
    #logo-text {
      transition: 0.5s ease;
    }
    
    #logo-image-container {
      display: flex;
    }
    
    #nav-logo {
      width: 30px;
      margin-left: 15px;
    }
    
    #logo-text-container {
      margin-left: 30px;
      display: none;
    }
    
    #logo-text {
      font-size: 18px;
      margin-block-start: 0em;
    }
    
    
    /* -----------------------------------
        Sidebar list item
    */
    
    .sidebar .list-item {
      display: flex;
      align-items: center;
      justify-content: flex-start;
      cursor: pointer;
      border: 1px solid transparent;
      border-radius: 100px;
      margin-bottom: 7px;
    }
    
    .sidebar .list-item:hover {
      background-color: var(--hover-bg-color);
    }
    
    .sidebar .list-item i {
      font-size: 1.2rem;
      padding: 10px 10px;
      border-radius: 60px;
    }
    
    .sidebar .list-item.active i {
      background-color: var(--active-bg-color);
    }
    
    .sidebar .list-item p {
      display: none;
      font-size: 14px;
      margin-top: 15px !important;
      margin-block-start: 0.2em;
    }
    
    
    /* -----------------------------------
        Sidebar expanded styles.
        The following CSS rules override style
        settings previously set.
    */
    
    .sidebar.expand {
      width: 225px;
    }
    
    .sidebar.expand #nav-logo {
      width: 120px;
    }
    
    .sidebar.expand #logo-text-container,
    .sidebar.expand .list-item p {
      display: block;
    }
    
    .sidebar.expand .list-item {
      padding-left: 12px;
      margin-bottom: 4px;
    }
    
    .sidebar.expand .list-item.active {
      background-color: var(--active-bg-color);
    }
    
    .sidebar.expand .list-item.active i {
      background-color: unset;
    }
    
    
    /* -----------------------------------
        Main content
    */
    
    #main {
      padding: 16px;
    }
    
    .body-text {
      height: 100%;
      width: 100%;
      margin-left: 250px;
      padding-top: 1px;
      padding-left: 25px;
    }
    <link href="https://kit.fontawesome.com/ee3b09a28a.css" rel="stylesheet" />
    
    <!-- Sidebar -->
    <div id="mySidebar" class="sidebar">
      <!-- Expanded Logo Image -->
      <div id="logo-image-container">
        <img src="https://www.creativefabrica.com/wp-content/uploads/2018/11/Company-business-generic-logo-by-DEEMKA-STUDIO.jpg" id="nav-logo" />
      </div>
      <!-- /Expanded Logo Image -->
      <!--  Expanded Logo Image Text -->
      <div id="logo-text-container">
        <p id="logo-text">Logo Text</p>
      </div>
      <!--  /Expanded Logo Image Text -->
      <!-- Menu Items -->
      <ul id="nav-menu-items">
        <li class="list-item" data-page-id="dashboard">
          <i class="fa-duotone fa-table-columns"></i>
          <p>Dashboard</p>
        </li>
    
        <li class="list-item" data-page-id="projects">
          <i class="fa-duotone fa-rectangle-history-circle-user"></i>
          <p>Projects</p>
        </li>
    
        <li class="list-item" data-page-id="assess">
          <i class="fa-duotone fa-address-card"></i>
          <p>Add Assessment</p>
        </li>
    
        <li class="list-item" data-page-id="config">
          <i class="fa-duotone fa-folder-gear"></i>
          <p>Configuration</p>
        </li>
      </ul>
      <!-- /Menu Items -->
    </div>
    <!-- /Sidebar -->
    
    <!-- Main -->
    <div id="main" data-page-id="dashboard">
      <h1>Dashboard</h1>
      <h2>Open/Collapse Sidebar on Hover</h2>
      <p>Hover over any part of the sidebar to open the it.</p>
      <p>To close the sidebar, move your mouse out of the sidebar.</p>
      <p>Click button to toggle how the sidebar expands: overlap main content or pushes main content to the right.
        <button id="toggle-sidebar-overlap">Toggle sidebar overlap</button>
    </div>
    <!-- /Main -->
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search