├── .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 | [](https://travis-ci.org/sospedra/react-native-dual)
4 | []()
5 | []()
6 | []()
7 | [](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 | |  |  |
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 |
--------------------------------------------------------------------------------