Show header when at top of page - Scroll event re-firing issue
Tags: javascript,jquery,html,css,knockout.js
Problem :
I have a set up where, there is a header, which will only be shown when the scrollTop of the scrollable area is at 0 (meaning they are at the top of the page). This seems to work fine in many cases, but there is a case where it fails in a rather annoying way.
The Bug
If the content in the scrollable area is just slightly big enough to make a scroll bar, then when you scroll down, the header will disappear and the scrollable area will now grow to fill that empty space, which will now allow the content to appear without having a scroll bar. The transition from having a scroll bar to not having a scroll bar apparently triggers another scroll event to be fired in all the browsers I have tested I do not know why that is or how to solve it.
One possible solution
I can set my content to have a min-height of 101% so that there will always be a tiny amount of scroll bar no matter what the actual content height would otherwise be. This isn't my favorite solution however and I am looking for something better.
html
<div class="container">
<div class="header">Header</div>
<div class="content" data-bind="css: {'show-header': showHeader}">
<div class="sub-header">Sub Header</div>
<div class="scrollable" data-bind="event: {'scroll':test}">
<div class="stuff">asdf</div>
</div>
</div>
CSS
.container {
position: absolute;
top: 0;
left: 0;
width: 320px;
height: 352px;
border: 1px solid black;
}
.header {
height: 48px;
background: grey;
}
.content {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
transition: top 0.3s;
}
.show-header {
top: 48px;
}
.sub-header {
height: 48px;
background: lightgrey;
}
.scrollable {
position: absolute;
top: 48px;
left: 0;
right: 0;
bottom: 0;
overflow: auto;
-webkit-overflow-scrolling: touch;
overflow-x: hidden;
}
.stuff {
/*min-height: 101%;*/
height: 260px;
/*height: 2000px;*/
}
JS
var appViewModel = {};
var didScroll = false;
var scrollElem = $('.scrollable');
appViewModel.showHeader = ko.observable(true);
appViewModel.test = function() {
didScroll = true;
};
setInterval(function() {
if (didScroll) {
didScroll = false;
appViewModel.hasScrolled();
}
}, 250);
appViewModel.hasScrolled = function() {
var st = scrollElem.scrollTop();
if ( st > 0 && appViewModel.showHeader() ) {
appViewModel.showHeader(false);
}
else if ( st <= 0 && !appViewModel.showHeader() ) {
appViewModel.showHeader(true);
}
};
ko.applyBindings(appViewModel);
JS Fiddle JS Fiddle Example
Solution :
I did a bit of rewriting to make this more Knockout-y and (hopefully) a bit more straightforward. I brought the header height into the code as a variable, since I needed to use it.
scrollTop
is a throttled observable, so it will not announce updates more than every 250 ms.
I use an observable and an if
binding to control whether the header is rendered. When the scrollTop changes, a subscribed function determines whether the header should be shown. The calculation depends on whether the header is currently being shown.
scrollableHeight
controls the height of the scrollable region, expanding it if the header is hidden.
The only way I know of to avoid the problem of having the scrollbar go away when the header does is to only remove the header when scrolling beyond the header height.
var appViewModel = {};
var didScroll = false;
appViewModel.headerHeight = 48;
appViewModel.scrollTop = ko.observable(0).extend({rateLimit:250});
appViewModel.showHeader = ko.observable(true);
appViewModel.scrollTop.subscribe(function (newTop) {
console.debug("New top");
if (appViewModel.showHeader()) {
appViewModel.showHeader(newTop < appViewModel.headerHeight);
}
else {
appViewModel.showHeader(newTop <= 0);
}
});
appViewModel.scrollableHeight = ko.computed(function () {
return (appViewModel.showHeader()) ? '256px' : (256 + appViewModel.headerHeight) + 'px';
});
appViewModel.test = function (data, event) {
var scrollElem = $(event.target);
appViewModel.scrollTop(scrollElem.scrollTop());
};
ko.applyBindings(appViewModel);
.container {
position: absolute;
top: 0;
left: 0;
width: 320px;
height: 352px;
border: 1px solid black;
}
.header {
background: grey;
}
.sub-header {
height: 48px;
background: lightgrey;
}
.scrollable {
overflow-y: auto;
-webkit-overflow-scrolling: touch;
overflow-x: hidden;
height: 256px;
}
.stuff {
background-color: #fee;
height: 320px;
/*height: 2000px;*/
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div class="container">
<!-- ko if:showHeader -->
<div class="header" data-bind="style:{height:headerHeight+'px'}">Header</div>
<!-- /ko -->
<div class="sub-header">Sub Header</div>
<div class="scrollable" data-bind="style:{height:scrollableHeight}, event: {'scroll':test}">
<div class="stuff">asdf</div>
</div>
</div>