├── .babelrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── src ├── DualFlatList.js ├── DualListView.js ├── DualScrollView.js ├── DualSectionList.js ├── index.js └── services.js ├── test ├── __snapshots__ │ └── index.spec.js.snap └── index.spec.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-native"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # build 61 | dist/ 62 | 63 | .demo 64 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | demo 2 | test 3 | coverage 4 | .travis.yml 5 | yarn.lock 6 | rollup.config.js 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | notifications: 3 | email: false 4 | node_js: 5 | - '8' 6 | branches: 7 | except: 8 | - /^v\d+\.\d+\.\d+$/ 9 | addons: 10 | code_climate: 11 | repo_token: CODECLIMATE 12 | after_success: 13 | - yarn global add codeclimate-test-reporter 14 | - codeclimate-test-reporter < coverage/lcov.info 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Rubén Sospedra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-dual 2 | 3 | [![Build Status](https://travis-ci.org/sospedra/react-native-dual.svg?branch=master)](https://travis-ci.org/sospedra/react-native-dual) 4 | [![Code Climate](https://img.shields.io/codeclimate/maintainability/sospedra/react-native-dual.svg)]() 5 | [![Code Climate](https://img.shields.io/codeclimate/c/sospedra/react-native-dual.svg)]() 6 | [![David](https://img.shields.io/david/sospedra/react-native-dual.svg)]() 7 | [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 8 | 9 | ScrollView, FlatList, SectionList and ListView with vertical dual background 10 | 11 | ### Check the demo ([live](https://expo.io/@sospedra/react-native-dual-demo) | [source](https://github.com/sospedra/react-native-dual-demo)) 12 | 13 | | Before (problem) | After (with dual) | 14 | |------------------|-------------------| 15 | | ![no-dual](https://user-images.githubusercontent.com/3116899/33805424-f3b0875e-ddb8-11e7-8353-352c6bceee75.gif) | ![with-dual](https://user-images.githubusercontent.com/3116899/33805413-c0db71e0-ddb8-11e7-89d0-d1aebbaf7b3b.gif) | 16 | 17 | ### Usage 18 | 19 | Instead of using normal React Native component favour the Dual one and share two 20 | special props: **`bottom` and `top` to set the colors you want to display**. 21 | 22 | ```js 23 | 28 | Mercury 29 | Venus 30 | Earth 31 | 32 | ``` 33 | 34 | If you don't specify a `bottom` color will fallback to the component style 35 | *(if you're already passing a style object there's no need for `bottom` extra prop)*: 36 | 37 | ```js 38 | 43 | Mars 44 | Jupiter 45 | Saturn 46 | 47 | ``` 48 | 49 | ### API 50 | 51 | Exposed components are: 52 | 53 | * DualFlatList 54 | * DualListView (notice will be deprecated by React Native in the future) 55 | * DualScrollView 56 | * DualSectionList 57 | 58 | And all of them intakes both `top` and `bottom` props: 59 | 60 | * `top: string` 61 | * `bottom?: string` 62 | 63 | Also `ScrollView` accepts: 64 | 65 | * `animated: bool` - switch from `ScrollView` component to `Animated.ScrollView` 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-dual", 3 | "version": "1.4.0", 4 | "description": "View, ScrollView, ListView and FlatList with dual background", 5 | "main": "src/index.js", 6 | "files": [ 7 | "src/" 8 | ], 9 | "scripts": { 10 | "test:lint": "standard", 11 | "test:unit": "jest", 12 | "test": "npm run test:lint && npm run test:unit" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/sospedra/react-native-dual.git" 17 | }, 18 | "keywords": [ 19 | "react-native", 20 | "background", 21 | "color", 22 | "dual", 23 | "scrollview", 24 | "flatlist", 25 | "listview" 26 | ], 27 | "author": "Ruben Sospedra", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/sospedra/react-native-dual/issues" 31 | }, 32 | "homepage": "https://github.com/sospedra/react-native-dual#readme", 33 | "peerDependencies": { 34 | "react": "*", 35 | "react-native": "*" 36 | }, 37 | "devDependencies": { 38 | "babel-jest": "^21.2.0", 39 | "babel-preset-react-native": "^4.0.0", 40 | "jest": "^21.2.1", 41 | "react": "*", 42 | "react-native": "*", 43 | "react-test-renderer": "^16.2.0", 44 | "standard": "^10.0.3" 45 | }, 46 | "jest": { 47 | "preset": "react-native", 48 | "collectCoverage": true 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/DualFlatList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { FlatList } from 'react-native' 4 | 5 | import { getListHeader, contentProps } from './services' 6 | 7 | /** 8 | * A FlatList assigns the colors as follows: 9 | * - bottom is the FlatList's contentContainerStyle backgroundColor 10 | * - top is the FlatList's self backgroundColor 11 | * 12 | * The renderHeader is given the BOUNCE_MARGIN height to create the dual effect 13 | */ 14 | export default function DualFlatList ({ bottom, children, top, ...props }) { 15 | return ( 16 | 21 | ) 22 | } 23 | 24 | DualFlatList.propTypes = { 25 | bottom: PropTypes.string, 26 | top: PropTypes.string.isRequired 27 | } 28 | -------------------------------------------------------------------------------- /src/DualListView.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { ListView } from 'react-native' 4 | 5 | import { getListHeader, contentProps } from './services' 6 | 7 | /** 8 | * A ListView assigns the colors as follows: 9 | * - bottom is the ListView's contentContainerStyle backgroundColor 10 | * - top is the ListView's self backgroundColor 11 | * 12 | * The renderHeader is given the BOUNCE_MARGIN height to create the dual effect 13 | */ 14 | export default function DualListView ({ bottom, children, top, ...props }) { 15 | return ( 16 | 21 | ) 22 | } 23 | 24 | DualListView.propTypes = { 25 | bottom: PropTypes.string, 26 | top: PropTypes.string.isRequired 27 | } 28 | -------------------------------------------------------------------------------- /src/DualScrollView.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { ScrollView, Animated } from 'react-native' 4 | 5 | import { getBounceCorrection, contentProps } from './services' 6 | 7 | /** 8 | * A ScrollView assigns the colors as follows: 9 | * - bottom is the ScrollView's contentContainerStyle backgroundColor 10 | * - top is the ScrollView's self backgroundColor 11 | * 12 | * The ScrollView contains an extra top-placed child with the BOUNCE_MARGIN 13 | * height to create the dual effect 14 | */ 15 | export default function DualScrollView ({ bottom, children, top, ...props }) { 16 | const Component = props.animated ? Animated.ScrollView : ScrollView 17 | 18 | return ( 19 | 23 | {getBounceCorrection(top)} 24 | {children} 25 | 26 | ) 27 | } 28 | 29 | DualScrollView.propTypes = { 30 | animated: PropTypes.bool, 31 | bottom: PropTypes.string, 32 | top: PropTypes.string.isRequired 33 | } 34 | -------------------------------------------------------------------------------- /src/DualSectionList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { SectionList } from 'react-native' 4 | 5 | import { getListHeader, contentProps } from './services' 6 | 7 | /** 8 | * A SectionList assigns the colors as follows: 9 | * - bottom is the SectionList's contentContainerStyle backgroundColor 10 | * - top is the SectionList's self backgroundColor 11 | * 12 | * The renderHeader is given the BOUNCE_MARGIN height to create the dual effect 13 | */ 14 | export default function DualSectionList ({ bottom, children, top, ...props }) { 15 | return ( 16 | 21 | ) 22 | } 23 | 24 | DualSectionList.propTypes = { 25 | bottom: PropTypes.string, 26 | top: PropTypes.string.isRequired 27 | } 28 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import DualFlatList from './DualFlatList' 2 | import DualListView from './DualListView' 3 | import DualScrollView from './DualScrollView' 4 | import DualSectionList from './DualSectionList' 5 | 6 | module.exports = { 7 | DualFlatList, 8 | DualListView, 9 | DualScrollView, 10 | DualSectionList 11 | } 12 | 13 | module.exports.default = module.exports 14 | -------------------------------------------------------------------------------- /src/services.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Dimensions, Platform, View } from 'react-native' 3 | 4 | const IS_ANDROID = Platform.OS === 'android' 5 | const BOUNCE_MARGIN = Dimensions.get('window').height / 2 6 | 7 | /** 8 | * Merge the original prop with the lib enrichment 9 | * Take cares also of fallbacks and default values 10 | * 11 | * @param {Bool} [shouldEnrich=true] - If the Dual component intaked the enrichment or not 12 | * @param {Object} [prop={}] - Original prop 13 | * @param {Object} enrichment - Dual enhancement 14 | * @return {Object} 15 | */ 16 | const mergeProp = (shouldEnrich = true, prop = {}, enrichment) => { 17 | return shouldEnrich ? Object.assign({}, prop, enrichment) : prop 18 | } 19 | 20 | /** 21 | * Apply needed props to create a space on the top of the scrollable element 22 | * with top color set. The idea is to add a View with the bounce height and the top 23 | * color. Then, start the scroll view at the end of this bounce corrector (inset). 24 | * And finally correct the initial scroll position (offset). 25 | * 26 | * @param {Object} props 27 | * @param {String} bottom 28 | * @param {String} top 29 | * @return {Object} 30 | */ 31 | export const contentProps = (props, bottom, top) => ({ 32 | contentContainerStyle: mergeProp(!!top, props.contentContainerStyle), 33 | contentInset: mergeProp(props.contentInset, { top: -BOUNCE_MARGIN }), 34 | contentOffset: mergeProp(props.contentOffset, { y: BOUNCE_MARGIN }), 35 | style: mergeProp(!!bottom, props.style, { backgroundColor: bottom }) 36 | }) 37 | 38 | /** 39 | * Create the element added at the top of the scrollable element. 40 | * With BOUNCE_MARGIN equal height and top color 41 | * Don't return anything if Android 42 | * 43 | * @param {String} top 44 | * @return {React.Node?} 45 | */ 46 | export const getBounceCorrection = (top) => { 47 | return IS_ANDROID ? null : 51 | } 52 | 53 | /** 54 | * Create the header component for all the scrollable React Native components. 55 | * Include the bounce corrector and the header (if any) send by the end user. 56 | * Just return the custom prop if Android 57 | * 58 | * @param {React.Node|Function} ListHeader 59 | * @param {string} top 60 | * @return {React.Node?} 61 | */ 62 | export const getListHeader = (ListHeader, top) => { 63 | if (IS_ANDROID && !ListHeader) return null 64 | if (IS_ANDROID && ListHeader) return ListHeader 65 | 66 | return () => ( 67 | 68 | {getBounceCorrection(top)} 69 | {ListHeader && } 70 | 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /test/__snapshots__/index.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`react-native-dual suite should render DualFlatList with bouncer props 1`] = ` 4 | 60 | 61 | 65 | 66 | 74 | 75 | 76 | 80 | 85 | 0 86 | 87 | 88 | 92 | 97 | 1 98 | 99 | 100 | 104 | 109 | 2 110 | 111 | 112 | 116 | 121 | 3 122 | 123 | 124 | 125 | 126 | `; 127 | 128 | exports[`react-native-dual suite should render DualListView with bouncer props 1`] = ` 129 | 155 | 156 | 157 | 165 | 166 | 171 | 0 172 | 173 | 178 | 1 179 | 180 | 185 | 2 186 | 187 | 192 | 3 193 | 194 | 195 | 196 | `; 197 | 198 | exports[`react-native-dual suite should render DualScrollView with bouncer props 1`] = ` 199 | 217 | 218 | 226 | 227 | 232 | Cool header 233 | 234 | 239 | With different background 240 | 241 | 242 | 247 | Item 0 248 | 249 | 254 | Item 1 255 | 256 | 261 | Item 2 262 | 263 | 268 | Item 3 269 | 270 | 275 | Item 4 276 | 277 | 282 | Item 5 283 | 284 | 289 | Item 6 290 | 291 | 296 | Item 7 297 | 298 | 303 | Item 8 304 | 305 | 310 | Item 9 311 | 312 | 317 | Item 10 318 | 319 | 324 | Item 11 325 | 326 | 331 | Item 12 332 | 333 | 338 | Item 13 339 | 340 | 345 | Item 14 346 | 347 | 352 | Item 15 353 | 354 | 359 | Item 16 360 | 361 | 366 | Item 17 367 | 368 | 373 | Item 18 374 | 375 | 380 | Item 19 381 | 382 | 387 | Item 20 388 | 389 | 390 | 391 | `; 392 | 393 | exports[`react-native-dual suite should render DualSectionList with bouncer props 1`] = ` 394 | 521 | 522 | 526 | 527 | 535 | 536 | 537 | 541 | 542 | 547 | Section Header 548 | A 549 | 550 | 551 | 552 | 556 | 561 | 0 562 | 563 | 564 | 568 | 573 | 1 574 | 575 | 576 | 580 | 585 | 2 586 | 587 | 588 | 592 | 597 | 3 598 | 599 | 600 | 604 | 609 | 4 610 | 611 | 612 | 616 | 621 | 5 622 | 623 | 624 | 628 | 633 | 6 634 | 635 | 636 | 640 | 644 | 645 | 650 | Section Header 651 | B 652 | 653 | 654 | 655 | 662 | 663 | 664 | `; 665 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, expect, it */ 2 | import { ListView, Text, View } from 'react-native' 3 | import React from 'react' 4 | import renderer from 'react-test-renderer' 5 | 6 | import { 7 | DualFlatList, 8 | DualListView, 9 | DualScrollView, 10 | DualSectionList 11 | } from '../src' 12 | 13 | describe('react-native-dual suite', () => { 14 | const COLOR = '#3498db' 15 | 16 | it('should render DualFlatList with bouncer props', () => { 17 | const wrapper = renderer.create( ({ key }))} 21 | renderItem={({item}) => {item.key}} 22 | keyExtractor={({ key }) => key} 23 | />).toJSON() 24 | 25 | expect(wrapper).toMatchSnapshot() 26 | expect(wrapper.props.contentInset.top < 0).toBe(true) 27 | expect(wrapper.props.contentOffset.y > 0).toBe(true) 28 | expect(wrapper.props.style.backgroundColor).toBe(COLOR) 29 | expect(wrapper.props.ListHeaderComponent).toBeInstanceOf(Function) 30 | }) 31 | 32 | it('should render DualListView with bouncer props', () => { 33 | class WrapperComponent extends React.Component { 34 | constructor () { 35 | super() 36 | 37 | const ds = new ListView.DataSource({ 38 | rowHasChanged: (r1, r2) => r1 !== r2 39 | }) 40 | 41 | this.state = { 42 | dataSource: ds.cloneWithRows( 43 | Array(4).fill().map((x, key) => `${key}`) 44 | ) 45 | } 46 | } 47 | 48 | render () { 49 | return {rowData}} 52 | /> 53 | } 54 | } 55 | 56 | const wrapper = renderer.create().toJSON() 57 | 58 | expect(wrapper).toMatchSnapshot() 59 | expect(wrapper.props.contentInset.top < 0).toBe(true) 60 | expect(wrapper.props.contentOffset.y > 0).toBe(true) 61 | expect(wrapper.props.style.backgroundColor).toBe(COLOR) 62 | expect(wrapper.props.renderHeader).toBeInstanceOf(Function) 63 | }) 64 | 65 | it('should render DualScrollView with bouncer props', () => { 66 | const wrapper = renderer.create( 67 | 68 | Cool header 69 | With different background 70 | 71 | {Array(21).fill().map((x, i) => ( 72 | {`Item ${i}`} 73 | ))} 74 | ).toJSON() 75 | 76 | expect(wrapper).toMatchSnapshot() 77 | expect(wrapper.props.contentInset.top < 0).toBe(true) 78 | expect(wrapper.props.contentOffset.y > 0).toBe(true) 79 | expect(wrapper.props.style.backgroundColor).toBe(COLOR) 80 | }) 81 | 82 | it('should render DualSectionList with bouncer props', () => { 83 | const wrapper = renderer.create( item} 85 | renderItem={({ item }) => {item}} 86 | renderSectionHeader={({ section }) => 87 | Section Header {section.title} 88 | } 89 | sections={[ 90 | {data: Array(7).fill().map((x, key) => key), title: 'A'}, 91 | {data: Array(7).fill().map((x, key) => key), title: 'B'}, 92 | {data: Array(7).fill().map((x, key) => key), title: 'C'} 93 | ]} 94 | />).toJSON() 95 | 96 | expect(wrapper).toMatchSnapshot() 97 | expect(wrapper.props.contentInset.top < 0).toBe(true) 98 | expect(wrapper.props.contentOffset.y > 0).toBe(true) 99 | expect(wrapper.props.style.backgroundColor).toBe(COLOR) 100 | expect(wrapper.props.ListHeaderComponent).toBeInstanceOf(Function) 101 | }) 102 | }) 103 | --------------------------------------------------------------------------------