Skip to content

Commit

Permalink
Merge pull request #25 from yahoo/height-change
Browse files Browse the repository at this point in the history
fix the unexpected situation when Sticky height changes
  • Loading branch information
hankhsiao committed Mar 25, 2016
2 parents c7433ba + 2bf146d commit 38a233c
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 38 deletions.
8 changes: 5 additions & 3 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ plugins:
env: {
es6: true
}
ecmaFeatures: {
modules: true,
jsx: true
parserOptions: {
sourceType: "module",
ecmaFeatures: {
jsx: true
}
}
rules:
valid-jsdoc: [2, { "requireReturn": false }]
Expand Down
16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,22 @@
"babel": "^5.0.0",
"coveralls": "^2.11.1",
"es5-shim": "^4.0.0",
"eslint-plugin-react": "^3.4.2",
"eslint": "^1.5.1",
"eslint-plugin-react": "^4.2.3",
"eslint": "^2.4.0",
"expect.js": "^0.3.1",
"grunt-atomizer": "^3.0.0",
"grunt-babel": "^5.0.0",
"grunt-cli": "^0.1.13",
"grunt-contrib-clean": "^0.7.0",
"grunt-contrib-connect": "^0.11.2",
"grunt-contrib-jshint": "^0.11.2",
"grunt-contrib-watch": "^0.6.1",
"grunt-cli": "^1.1.0",
"grunt-contrib-clean": "^1.0.0",
"grunt-contrib-connect": "^1.0.0",
"grunt-contrib-jshint": "^1.0.0",
"grunt-contrib-watch": "^1.0.0",
"grunt-saucelabs": "^8.3.2",
"grunt-shell": "^1.1.2",
"grunt-webpack": "^1.0.8",
"grunt": "^0.4.5",
"istanbul": "^0.4.0",
"jsdom": "^7.0.2",
"jsdom": "^8.0.0",
"jsx-loader": "^0.13.2",
"jsx-test": "^0.8.0",
"minimist": "^1.2.0",
Expand Down
68 changes: 41 additions & 27 deletions src/Sticky.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ class Sticky extends Component {
this.subscribers;

this.state = {
top: 0, // A top offset px from screen top for Sticky when scrolling down
bottom: 0, // A bottom offset px from screen top for Sticky when scrolling up *1*
top: 0, // A top offset from viewport top where Sticky sticks to when scrolling up
bottom: 0, // A bottom offset from viewport top where Sticky sticks to when scrolling down
width: 0, // Sticky width
height: 0, // Sticky height
x: 0, // The original x of Sticky
Expand Down Expand Up @@ -184,19 +184,6 @@ class Sticky extends Component {

/**
* Update Sticky position.
* In this function, all coordinates of Sticky and scren are projected to document, so the local variables
* "top"/"bottom" mean the expected top/bottom of Sticky on document. They will move when scrolling.
*
* There are 2 principles to make sure Sticky won't get wrong so much:
* 1. Reset Sticky to the original postion when "top" <= topBoundary
* 2. Release Sticky to the bottom boundary when "bottom" >= bottomBoundary
*
* If "top" and "bottom" are between the boundaries, Sticky will always fix to the top of screen
* when it is shorter then screen. If Sticky is taller then screen, then it will
* 1. Fix to the bottom of screen when scrolling down and "bottom" > Sticky current bottom
* 2. Fix to the top of screen when scrolling up and "top" < Sticky current top
* (The above 2 points act kind of "bottom" dragging Sticky down or "top" dragging it up.)
* 3. Release Sticky when "top" and "bottom" are between Sticky current top and bottom.
*/
update () {
var self = this;
Expand All @@ -212,49 +199,72 @@ class Sticky extends Component {
}

var delta = scrollDelta;
// "top" and "bottom" are the positions that self.state.top and self.state.bottom project
// on document from viewport.
var top = scrollTop + self.state.top;
var bottom = scrollTop + self.state.bottom;

if (top <= self.state.topBoundary) {
// There are 2 principles to make sure Sticky won't get wrong so much:
// 1. Reset Sticky to the original postion when "top" <= topBoundary
// 2. Release Sticky to the bottom boundary when "bottom" >= bottomBoundary
if (top <= self.state.topBoundary) { // #1
self.reset();
} else if (bottom >= self.state.bottomBoundary) {
} else if (bottom >= self.state.bottomBoundary) { // #2
self.stickyBottom = self.state.bottomBoundary;
self.stickyTop = self.stickyBottom - self.state.height;
self.release(self.stickyTop);
} else {
if (self.state.height > winHeight - self.state.top) {
// In this case, Sticky is larger then screen minus sticky top
// In this case, Sticky is higher then viewport minus top offset
switch (self.state.status) {
case STATUS_ORIGINAL:
self.release(self.state.y);
self.stickyTop = self.state.y;
self.stickyBottom = self.stickyTop + self.state.height;
break;
case STATUS_RELEASED:
if (delta > 0 && bottom > self.stickyBottom) { // scroll down
// If "top" and "bottom" are inbetween stickyTop and stickyBottom, then Sticky is in
// RELEASE status. Otherwise, it changes to FIXED status, and its bottom sticks to
// viewport bottom when scrolling down, or its top sticks to viewport top when scrolling up.
if (delta > 0 && bottom > self.stickyBottom) {
self.fix(self.state.bottom - self.state.height);
} else if (delta < 0 && top < self.stickyTop) { // scroll up
} else if (delta < 0 && top < self.stickyTop) {
this.fix(self.state.top);
}
break;
case STATUS_FIXED:
var isChanged = true;
if (delta > 0 && self.state.pos === self.state.top) { // scroll down
var toRelease = true;
var pos = self.state.pos;
var height = self.state.height;
// In regular cases, when Sticky is in FIXED status,
// 1. it's top will stick to the screen top,
// 2. it's bottom will stick to the screen bottom,
// 3. if not the cases above, then it's height gets changed
if (delta > 0 && pos === self.state.top) { // case 1, and scrolling down
self.stickyTop = top - delta;
self.stickyBottom = self.stickyTop + self.state.height;
} else if (delta < 0 && self.state.pos === self.state.bottom - self.state.height) { // up
self.stickyBottom = self.stickyTop + height;
} else if (delta < 0 && pos === self.state.bottom - height) { // case 2, and scrolling up
self.stickyBottom = bottom - delta;
self.stickyTop = self.stickyBottom - self.state.height;
self.stickyTop = self.stickyBottom - height;
} else if (pos !== self.state.bottom - height && pos !== self.state.top) { // case 3
// This case only happens when Sticky's bottom sticks to the screen bottom and
// its height gets changed. Sticky should be in RELEASE status and update its
// sticky bottom by calculating how much height it changed.
var deltaHeight = (pos + height - self.state.bottom);
self.stickyBottom = bottom - delta + deltaHeight;
self.stickyTop = self.stickyBottom - height;
} else {
isChanged = false;
toRelease = false;
}

if (isChanged) {
if (toRelease) {
self.release(self.stickyTop);
}
break;
}
} else {
// In this case, Sticky is shorter then viewport minus top offset
// and will always fix to the top offset of viewport
self.fix(self.state.top);
}
}
Expand All @@ -274,9 +284,13 @@ class Sticky extends Component {

componentDidMount () {
var self = this;
// when mount, the scrollTop is not necessary on the top
scrollTop = docBody.scrollTop + docEl.scrollTop;

if (self.props.enabled) {
self.setState({activated: true});
self.updateInitialDimension();
this.update();
self.subscribers = [
subscribe('scrollStart', self.handleScrollStart.bind(self), {useRAF: true}),
subscribe('scroll', self.handleScroll.bind(self), {useRAF: true, enableScrollInfo: true}),
Expand Down
39 changes: 39 additions & 0 deletions tests/unit/Sticky-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,5 +316,44 @@ describe('Sticky', function () {
window.resizeTo(0, 900);
shouldBeFixedAt(inner, 0);
expect(outer.className).to.contain('active');

// Resize back
window.resizeTo(0, 768);
});

it('should release when height gets changed (long Sticky)', function () {
STICKY_HEIGHT = 1200;
sticky = jsx.renderComponent(Sticky);
outer = ReactDOM.findDOMNode(sticky);
inner = outer.firstChild;

// regular case
expect(outer.className).to.contain('sticky-outer-wrapper');
expect(inner.className).to.contain('sticky-inner-wrapper');
// should always have translate3d
checkTransform3d(inner);

// Scroll down to 10px, and Sticky should stay as it was
window.scrollTo(0, 10);
shouldBeReleasedAt(inner, 0);
expect(outer.className).to.not.contain('active');

// Scroll down to 1500px, and Sticky should fix to the bottom
window.scrollTo(0, 1500);
shouldBeFixedAt(inner, -432);
expect(outer.className).to.contain('active');

// Change Sticky's height
STICKY_HEIGHT = 1300;

// Scroll up to 1550px, and Sticky should release and stay where it was
window.scrollTo(0, 1550);
shouldBeReleasedAt(inner, 1068);
expect(outer.className).to.not.contain('active');

// Scroll down to 1650px, and Sticky should release as it was
window.scrollTo(0, 1650);
shouldBeFixedAt(inner, -532);
expect(outer.className).to.contain('active');
});
});

0 comments on commit 38a233c

Please sign in to comment.