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:
-
The user hovers mouse over sidebar to expand it
-
The user clicks on a link and gets routed to a new page
-
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
-
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!
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
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
orpointerleave
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.
An
expand
class name can be used as an alternative to passing a parameter and using themini
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
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)
ordocument.elementsFromPoint(x,y)
(i.e. the plural version). The pointer requires movement to trigger a pointer move eventin order to retrieve the
x, y
pointer coordinates.