Hi there,
I’ve been tasked with modifying an existing header for a test, so that it becomes a slimmer version on scroll. However, the existing header involves search functionality - some of which is reliant on the search button being clicked for it to appear. The search itself causes the header to expand, affecting the scroll position as a result which adds further complication.
At the moment I’m trying to optimise my code as much as possible so that it doesn’t continually add the necessary CSS class to make it slim on scroll (only when it reaches the scrollThreshold), while also ensuring that the header only animates when this happens and not when the search is opened or closed.
The following is what I have so far which I feel needs some serious optimisation. I found that the low throttleDelay was needed so that it would fire quickly enough when scrolling to the top and back down again, but I am finding that it is multiplying the addition of the slim-header class due to the scrolling and click of the search functions. Also, interacting with the search functions cause the “animate” class to be added again, forcing the animation to run when I don’t want it to (it should remain static).
Any thoughts please?
JavaScript
(function () {
console.log("Slim Header On Scroll - Throttled - 1062");
slimHeaderOnScroll();
})();
// Check for element based on timeout/interval and the selector passed,
// then run the callback function as applicable
function checkForElement(selector, callback) {
const timeout = 5000; // 5 seconds
const interval = 100; // Check every 400 milliseconds
let elapsedTime = 0;
let intervalId;
const checkElementInterval = setInterval(() => {
const element = document.querySelector(selector);
// If element has been found, run callback
if (element) {
clearInterval(intervalId);
callback(element); // Pass the element to the callback function
} else {
// If over the timeout period
if (elapsedTime >= timeout) {
clearInterval(intervalId);
} else {
elapsedTime += interval;
}
}
}, interval);
intervalId = checkElementInterval;
}
function slimHeaderOnScroll() {
const self = this;
let throttleTimeout;
let throttleDelay = 50; // Adjust throttle delay as needed
console.log("throttleDelay " + throttleDelay);
// Throttled scroll event handler
function handleScroll() {
if (!throttleTimeout) {
throttleTimeout = setTimeout(function () {
throttleTimeout = null;
console.log(self.scrollY);
// Initial position check and class application
scrollPosCheck(self);
}, throttleDelay);
}
}
window.addEventListener("scroll", handleScroll.bind(this));
}
function scrollPosCheck(self) {
const siteHeader = document.querySelector('div[data-testid="app-header"]');
const headerSearchBtn = 'button[data-testid="app-header-desktop-search-button"]';
const headerSearchCloseBtn = 'button[data-testid="app-header-search-bar-close"]';
const headerSearchBox = 'div[data-testid="app-header-search-bar"] input#search';
const headerSearchOverlay = '.sf-overlay';
const scrollThreshold = 170;
let searchBtnInputCheck = false;
if (self.scrollY === 0) {
// If top of page, change to standard header
siteHeader.classList.remove('slim-header');
siteHeader.querySelector('header').classList.remove('slim-header--animate');
} else {
// Attach event listeners
const searchBtn = document.querySelector(headerSearchBtn);
const searchCloseBtn = document.querySelector(headerSearchCloseBtn);
const searchBox = document.querySelector(headerSearchBox);
// If scroll more threshold then apply slim header styles
if (self.scrollY > scrollThreshold) {
siteHeader.classList.add('slim-header');
} else if (self.scrollY <= scrollThreshold) {
searchBtnInputCheck = false;
console.log("searchBtnInputCheck reset: " + searchBtnInputCheck);
}
console.log("searchBtnInputCheck pre-animate" + searchBtnInputCheck);
// If search not active, above threshold and searchBtn hasn't been clicked
if (self.scrollY > scrollThreshold && !searchBtnInputCheck) {
siteHeader.querySelector('header').classList.add('slim-header--animate');
}
if (searchBtn) {
// Attribute check to see if element has already been clicked
if (!searchBtn.hasAttribute('data-clicked')) {
// Add attribute to mark that the event listener has been added
// searchBtn.setAttribute('data-clicked', 'true');
searchBtn.addEventListener('click', function () {
//console.log("Search button click: " + self.scrollY);
searchBtnInputCheck = true;
// If scroll position is correct, add the class
if (self.scrollY > scrollThreshold) {
siteHeader.classList.add('slim-header');
console.log("Search button click: Slim Header class added");
}
// Check for search close button
if (searchCloseBtn) {
// Attribute check to see if element has already been clicked
if (!searchCloseBtn.hasAttribute('data-clicked')) {
// Add attribute to mark that the event listener has been added
// searchCloseBtn.setAttribute('data-clicked', 'true');
searchCloseBtn.addEventListener('click', function () {
if (self.scrollY > scrollThreshold) {
siteHeader.classList.add('slim-header');
console.log("Search close button click: Slim Header class added");
}
});
}
}
// Check for search box
if (searchBox) {
// Attribute check to see if element has already been interacted with
if (!searchBox.hasAttribute('data-clicked')) {
// Add attribute to mark that the event listener has been added
// searchBox.setAttribute('data-clicked', 'true');
let searchBoxInputCheck = false;
const inputCheckHandler = () => {
if (!searchBoxInputCheck) {
searchBoxInputCheck = true;
console.log("Search box input: Slim Header class added");
// Check for overlay and apply click event if needed
checkForElement(headerSearchOverlay, function (searchOverlay) {
// console.log("Search not already started: Search overlay found");
// Attribute check to see if element has already been clicked
if (!searchOverlay.hasAttribute('data-clicked')) {
// Add attribute to mark that the event listener has been added
// searchOverlay.setAttribute('data-clicked', 'true');
searchOverlay.addEventListener('click', function () {
if (self.scrollY > scrollThreshold) {
siteHeader.classList.add('slim-header');
console.log("Search overlay click: Slim Header class added");
}
});
}
});
}
// Remove the input event listener
searchBox.removeEventListener('input', inputCheckHandler);
};
// Add the input event listener
searchBox.addEventListener('input', inputCheckHandler);
}
}
if (self.scrollY > scrollThreshold) {
// If search has been started already then overlay will appear
checkForElement(headerSearchOverlay, function (searchOverlay) {
console.log("Search already started: Search overlay found");
// Attribute check to see if element has already been clicked
if (!searchOverlay.hasAttribute('data-clicked')) {
// Add attribute to mark that the event listener has been added
// searchOverlay.setAttribute('data-clicked', 'true');
searchOverlay.addEventListener('click', function () {
siteHeader.classList.add('slim-header');
console.log("Search overlay click: Slim Header class added");
});
}
});
}
});
}
}
}
}
CSS
@media(min-width: 992px) {
/* Header Container */
.slim-header--animate{
animation: slideDown ease-in 0.3s forwards;
}
.slim-header header {
padding-left: 1.5rem !important;
padding-right: 1.5rem !important;
}
/* Header Inner Container */
.slim-header header>div:first-child {
display: grid !important;
padding-bottom: 0 !important;
grid-template-columns: 1fr 9fr auto auto;
}
/* Logo */
.slim-header header div[data-testid="app-header-desktop-left"]~div.flex-auto {
display: flex;
justify-content: flex-start;
align-items: center;
grid-column: 1;
}
.slim-header header div[data-testid="app-header-desktop-left"]~div.flex-auto>div>div {
padding-bottom: 0.25rem !important;
}
/* Shop Finder */
.slim-header header div[data-testid="app-header-desktop-left"] {
display: none !important;
}
/* Search, Account & Bag */
.slim-header header div[data-testid="app-header-desktop-left"]~.w-52.flex.items-center.justify-end {
order: 3;
margin-left: 1rem;
width: auto !important;
grid-column: 4;
justify-content: flex-start !important;
}
.slim-header .sf-overlay {
top: 9rem !important
}
/* Navigation */
.slim-header header>div:nth-child(2) {
position: absolute;
top: 25px;
left: 0;
width: 100%;
pointer-events: none;
}
.slim-header div[data-testid="desktop-nav"] li {
pointer-events: visible;
}
.slim-header div.test-mega-menu-panel {
margin-top: -1px;
margin-left: 0 !important;
margin-right: 0 !important;
}
}
@media(min-width: 992px) and (max-width: 1280px) {
.slim-header header>div:first-child {
padding-bottom: 1rem !important;
}
.slim-header header>div:nth-child(2) {
position: relative;
top: 0;
}
}
@media(min-width: 1280px){
.slim-header--animate ~ #layout{
margin-top: 64px;
}
.slim-header--animate ~ #layout.push-up-page{
margin-top: -79px;
}
}
@keyframes slideDown{
0%{
transform: translateY(-145px);
}
100%{
transform: translateY(0);
}
}