Comparing version 1.0.1 to 1.0.2
{ | ||
"name": "scrollable", | ||
"version": "1.0.1", | ||
"version": "1.0.2", | ||
"description": "Components for layer composition and scrolling with React.js", | ||
@@ -24,2 +24,3 @@ "main": "src/scrollable.js", | ||
"browserify-istanbul": "^0.2.1", | ||
"codeclimate-test-reporter": "^0.1.0", | ||
"coveralls": "^2.11.2", | ||
@@ -26,0 +27,0 @@ "hammerjs": "^2.0.4", |
## React Scrollable | ||
[![Join the chat at https://gitter.im/yahoo/scrollable](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/yahoo/scrollable?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) | ||
[![build status](https://travis-ci.org/yahoo/scrollable.svg)](https://travis-ci.org/yahoo/scrollable) | ||
[![Coverage Status](https://coveralls.io/repos/yahoo/scrollable/badge.svg)](https://coveralls.io/r/yahoo/scrollable) | ||
[![Test Coverage](https://codeclimate.com/github/yahoo/scrollable/badges/coverage.svg)](https://codeclimate.com/github/yahoo/scrollable/coverage) | ||
[![Code Climate](https://codeclimate.com/github/yahoo/scrollable/badges/gpa.svg)](https://codeclimate.com/github/yahoo/scrollable) | ||
@@ -5,0 +8,0 @@ #### A library that brings smooth scrolling interactions to modern mobile browsers. |
@@ -102,6 +102,20 @@ /* Copyright 2015, Yahoo Inc. | ||
it("Inserting text nodes", function () { | ||
sut = React.render( | ||
<RectCacheConsumer />, | ||
div | ||
); | ||
sut.getDOMNode().appendChild(document.createTextNode(' text test node ')); | ||
expect(true, "make sure inserting the element won't throw"); | ||
}); | ||
it("won't update after unmount", function () { | ||
var resizeCallback = jasmine.createSpy(); | ||
// Adding viewport below will only force the code to have coverage | ||
// but testing the viewport resize is probably a lot of work, as the | ||
// event would need to be simulates. This feels an overkill, but Pull | ||
// requests accepted =) | ||
sut = React.render( | ||
<RectCacheConsumer onResize={resizeCallback} />, | ||
<RectCacheConsumer viewport onResize={resizeCallback} />, | ||
div | ||
@@ -108,0 +122,0 @@ ); |
@@ -23,14 +23,18 @@ /* Copyright 2015, Yahoo Inc. | ||
TestUtils.renderIntoDocument( | ||
<ScrollItem /> | ||
<ScrollItem serverStyles={true} /> | ||
); | ||
expect(console.warn).toHaveBeenCalled(); | ||
expect(console.warn.calls.count()).toEqual(2); | ||
expect(console.warn.calls.first().args[0]).toMatch('was not specified'); | ||
expect(console.warn.calls.mostRecent().args[0]).toMatch('was not specified'); | ||
expect(console.warn.calls.count()).toEqual(3); | ||
expect(console.warn.calls.argsFor(0)).toMatch('was not specified'); | ||
expect(console.warn.calls.argsFor(1)).toMatch('was not specified'); | ||
expect(console.warn.calls.argsFor(2)).toMatch('expected `function`'); | ||
}); | ||
it("Won't throw outside <Scroller>", function () { | ||
TestUtils.renderIntoDocument( | ||
<ScrollItem name="foo" scrollHandler={function(){}} /> | ||
); | ||
function go() { | ||
TestUtils.renderIntoDocument( | ||
<ScrollItem name="foo" scrollHandler={function(){}} /> | ||
); | ||
} | ||
expect(go).not.toThrow(); | ||
}); | ||
@@ -72,2 +76,56 @@ | ||
describe('Server-side rendering', function() { | ||
it("Render styles from serverStyles prop", function () { | ||
var Scroller = MockScroller(); | ||
var wrapper = React.render( | ||
<Scroller> | ||
<ScrollItem name="foo" scrollHandler={function(){}} serverStyles={function(){ | ||
return { | ||
height: '50px', | ||
}; | ||
}}> | ||
foo | ||
</ScrollItem> | ||
</Scroller>, | ||
div | ||
); | ||
var sut = TestUtils.findRenderedDOMComponentWithClass(wrapper, 'scrollable-item'); | ||
expect(sut.props.style.height).toBe('50px'); | ||
}); | ||
it("Won't throw if serverStyles returns false", function () { | ||
var Scroller = MockScroller(); | ||
function go() { | ||
React.render( | ||
<Scroller> | ||
<ScrollItem name="foo" scrollHandler={function(){}} serverStyles={function(){ | ||
return false; | ||
}}> | ||
foo | ||
</ScrollItem> | ||
</Scroller>, | ||
div | ||
); | ||
} | ||
expect(go).not.toThrow(); | ||
}); | ||
it("Won't throw if serverStyles is not a function", function () { | ||
var Scroller = MockScroller(); | ||
function go() { | ||
React.render( | ||
<Scroller> | ||
<ScrollItem name="foo" scrollHandler={function(){}} serverStyles={true}> | ||
foo | ||
</ScrollItem> | ||
</Scroller>, | ||
div | ||
); | ||
} | ||
expect(go).not.toThrow(); | ||
}); | ||
}); | ||
describe('integration with <Scroller>', function() { | ||
@@ -147,2 +205,59 @@ | ||
it("execute _prendingOperation that the parent might have setup", function () { | ||
var Scroller = MockScroller(); | ||
var SuposedConsumer = React.createClass({ | ||
componentDidMount: function() { | ||
this.refs.item._prendingOperation = function(){}; | ||
}, | ||
render: function() { | ||
return ( | ||
<Scroller> | ||
<ScrollItem ref="item" name="foo" scrollHandler={function(){}} /> | ||
</Scroller> | ||
); | ||
}, | ||
}); | ||
var consumer = React.render( | ||
<SuposedConsumer />, | ||
div | ||
); | ||
var sut = TestUtils.findRenderedComponentWithType(consumer, ScrollItem); | ||
spyOn(sut, '_prendingOperation'); | ||
sut.componentDidMount(); | ||
expect(sut._prendingOperation).toHaveBeenCalled(); | ||
}); | ||
it("Calls parent onResize method if item resizes", function () { | ||
var Scroller = MockScroller(); | ||
var SuposedConsumer = React.createClass({ | ||
getInitialState: function() {return {resizeItem:false};}, | ||
render: function() { | ||
return ( | ||
<Scroller ref="wrapper"> | ||
<ScrollItem name="foo" scrollHandler={function(){}}> | ||
<div style={{height:'20px', width:'20px'}} /> | ||
{ this.state.resizeItem && | ||
<div style={{height:'20px', width:'20px'}} /> | ||
} | ||
</ScrollItem> | ||
</Scroller> | ||
); | ||
}, | ||
}); | ||
var consumer = React.render( | ||
<SuposedConsumer />, | ||
div | ||
); | ||
var parent = TestUtils.findRenderedComponentWithType(consumer, Scroller); | ||
parent.onResize = function () {}; | ||
spyOn(parent, 'onResize'); | ||
consumer.setState({resizeItem: true}); | ||
expect(parent.onResize).toHaveBeenCalled(); | ||
}); | ||
}); | ||
@@ -149,0 +264,0 @@ |
@@ -8,2 +8,76 @@ /* exported RectCache */ | ||
/* | ||
This mixin will add a `.rect` property to the consumer element and will do a "best effort" | ||
to keep it updated, with the caveats stated below. | ||
API: | ||
---- | ||
`.rect` (Object): This property will have the same object signature as | ||
DOMElement.getBoundingClientRect() or will be a direct instance returned by | ||
the native method. | ||
`.onResize` (optional hook event): The consumer might implement this method to get notified | ||
of changes to `.rect` changes. | ||
`onResize` (callback prop): Owners of component instances that implement RectCache can hook | ||
into size changes to get notifications. | ||
`viewport` (property): Owners can initialize component instances that implement RectCache | ||
that will also update when window resizes or orientation changes. | ||
Silly example, but with all API being used: | ||
------------------------------------------- | ||
var HaveRectCache = React.createClass({ | ||
mixins: [RectCache], | ||
render: function () { | ||
return (<div onClick={this.theClick}> | ||
<img src="large1.jpg" /> | ||
<img src="large2.jpg" /> | ||
</div>); | ||
}, | ||
theClick: function() { | ||
alert(this.rect.height); | ||
}, | ||
}); | ||
var Comp = React.createClass({ | ||
render: function () { | ||
return (<div> | ||
<HaveRectCache viewport ref="theElement" onResize={this.whenResized}> | ||
</div>); | ||
}, | ||
whenResized: function() { | ||
// also called if window resizes or orientation change | ||
alert('Comp knows theElement height changed to ' + this.refs.theElement.rect.height); | ||
}, | ||
}); | ||
How it works and quirks: | ||
------------------------ | ||
The `.rect` object will have width and hight consistent at all times, but other properties | ||
might get outdated if elements are dynamically re-positioned by application logic. | ||
After initialization, the mixin will bind to DOM events to do a best effort into a watching | ||
for element size changes. Current browser APIs won't allow for perfect resize detection on | ||
a DOM node level, besides hacky solutions that add extra DOM and watch for scroll events. One | ||
famous library that uses this technique is | ||
[CSS Element Queries](https://github.com/marcj/css-element-queries). Instead, RectCache will | ||
poll for `.getBoundingClientRect()` on reasonable events like "DOMSubtreeModified/Inserted" | ||
and image onLoad events. | ||
Because some edge cases might still happen, specially when window resizes or viewport | ||
orientation changes (on mobile devices), the viewport property will help the relevant elements | ||
to keep updated. | ||
The reason this algorithm is used instead of the more reliable hacky solutions mentioned above, | ||
is because using React is already adding a lot of DOM predictability to changes, and this | ||
library only aims to "close the gap", when doing something with DOM outside of React code. | ||
*/ | ||
var initialRect = { left : 0, right : 0, top : 0, height : 0, bottom : 0, width : 0 }; | ||
@@ -13,8 +87,9 @@ | ||
rect: initialRect, | ||
_node: null, | ||
_updateRectCache: function() { | ||
if(!this.isMounted()) { | ||
return; // Edge case, this should not happen, maybe react bug? | ||
if(!this._node) { | ||
return; | ||
} | ||
var oldRect = this.rect; | ||
var newRect = this.getDOMNode().getBoundingClientRect(); | ||
var newRect = this._node.getBoundingClientRect(); | ||
this.rect = newRect; | ||
@@ -33,8 +108,14 @@ | ||
_bindImgLoad: null, | ||
componentDidMount: function(){ | ||
var node = this.getDOMNode(); | ||
var update = this._updateRectCache; | ||
this._node = node; | ||
this._bindImgLoad = function(event) { | ||
watchLoadImages(event.target, update); | ||
}; | ||
update(); | ||
getImageLoadedNotifications(node, update); | ||
watchLoadImages(node, update); | ||
node.addEventListener('DOMSubtreeModified', update); | ||
node.addEventListener('DOMNodeInserted', this._bindImgLoad); | ||
if (this.props.hasOwnProperty('viewport')) { | ||
@@ -47,8 +128,11 @@ window.addEventListener('orientationchange', update); | ||
componentWillUnmount: function(){ | ||
var node = this.getDOMNode(); | ||
var node = this._node; | ||
var update = this._updateRectCache; | ||
node.removeEventListener('DOMSubtreeModified', update); | ||
node.removeEventListener('DOMNodeInserted', this._bindImgLoad); | ||
this._node = null; | ||
this._bindImgLoad = null; | ||
if (this.props.hasOwnProperty('viewport')) { | ||
window.removeEventListener('orientationchange', update); | ||
window.removeEventListener("resize", update); | ||
window.removeEventListener('orientationchange', update); | ||
window.removeEventListener("resize", update); | ||
} | ||
@@ -58,18 +142,15 @@ }, | ||
function getImageLoadedNotifications(node, callback) { | ||
watchLoadImages(node.getElementsByTagName('img'), callback); | ||
node.addEventListener('DOMNodeInserted', function(event) { | ||
var images = event.target.getElementsByTagName && event.target.getElementsByTagName('img'); | ||
if (event.target.nodeName.toLowerCase() === 'img') { | ||
watchLoadImages([event.target], callback); | ||
} | ||
watchLoadImages(images, callback); | ||
}); | ||
function watchLoadImages(node, callback) { | ||
var imgArr = getAllImagesInTree(node); | ||
for (var i = 0; i < imgArr.length; i++) { | ||
var img = imgArr[i]; | ||
img.addEventListener('load', callback); | ||
} | ||
} | ||
function watchLoadImages(imgArr, callback) { | ||
if(imgArr && imgArr.length) { | ||
for (var i = 0; i < imgArr.length; i++) { | ||
var img = imgArr[i]; | ||
img.addEventListener('load', callback); | ||
} | ||
function getAllImagesInTree(node) { | ||
if (node && node.nodeName.toLowerCase() === 'img') { | ||
return [node]; | ||
} else { | ||
return (node.getElementsByTagName && node.getElementsByTagName('img')) || []; | ||
} | ||
@@ -76,0 +157,0 @@ } |
@@ -17,2 +17,3 @@ /* Copyright 2015, Yahoo Inc. | ||
scrollHandler: React.PropTypes.func.isRequired, | ||
serverStyles: React.PropTypes.func, | ||
}, | ||
@@ -41,8 +42,3 @@ | ||
this._node = this.getDOMNode(); | ||
var styleObject = this._pendingStyles; | ||
if (styleObject) { | ||
for(var prop in styleObject) { | ||
this._node.style[prop] = styleObject[prop]; | ||
} | ||
} | ||
this._prendingOperation && this._prendingOperation(); | ||
}, | ||
@@ -61,3 +57,6 @@ | ||
if (ssStyles) { | ||
var styleObject = ssStyles(this, this._scrollingParent); | ||
var styleObject; | ||
try { | ||
styleObject = ssStyles(this, this._scrollingParent); | ||
} catch(e) {} | ||
if (styleObject) { | ||
@@ -64,0 +63,0 @@ styleObject = StyleHelper.scrollStyles(styleObject); |
@@ -12,2 +12,4 @@ /* Copyright 2015, Yahoo Inc. | ||
var ScrollerEvents; | ||
/* istanbul ignore else */ | ||
if (inBrowser) { | ||
@@ -79,15 +81,6 @@ ScrollerEvents = require('./scroller-events'); | ||
// Using styles directly and simple for loops yeilds HUGE performance | ||
// improvements specially on iPhone 4 with iOS 7. | ||
// Set styles | ||
if (item._node) { | ||
for(var prop in styleObject) { | ||
if (!item._prevStyles || item._prevStyles[prop] !== styleObject[prop]) { | ||
item._node.style[prop] = styleObject[prop]; | ||
} | ||
} | ||
item._prevStyles = styleObject; | ||
applyStyles(item, styleObject); | ||
} else { | ||
item._pendingStyles = styleObject; | ||
item._prendingOperation = queueStylesOperation(item, styleObject); | ||
} | ||
@@ -291,2 +284,5 @@ | ||
_getContentSize: function() { | ||
if (!this.props.getContentSize) { | ||
return {width: 0, height: 0}; | ||
} | ||
return this.props.getContentSize(this._scrollItems, this); | ||
@@ -388,2 +384,17 @@ }, | ||
function queueStylesOperation(item, styleObject) { | ||
return applyStyles.bind(null, item, styleObject); | ||
} | ||
function applyStyles(item, styleObject) { | ||
// Using styles directly and simple for loops yeilds HUGE performance | ||
// improvements specially on iPhone 4 with iOS 7. | ||
for(var prop in styleObject) { | ||
if (!item._prevStyles || item._prevStyles[prop] !== styleObject[prop]) { | ||
item._node.style[prop] = styleObject[prop]; | ||
} | ||
} | ||
item._prevStyles = styleObject; | ||
} | ||
module.exports = Scroller; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1930269
19
2822
69
23