├── .DS_Store
├── .babelrc
├── .circleci
└── config.yml
├── .eslintrc
├── .gitignore
├── .nvmrc
├── .travis.yml
├── README.md
├── package-lock.json
├── package.json
├── react-listview-sticky-header.css
├── src
├── .eslintrc
├── ReactListView.js
├── example
│ ├── Example.js
│ ├── data.js
│ └── react-listview-sticky-header.gif
├── index.js
└── lib
│ ├── ListHeader.js
│ └── ListItems.js
├── test
├── .eslintrc
├── data.js
├── index.js
└── style.js
├── webpack.config.js
└── yarn.lock
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cht8687/react-listview-sticky-header/18fc8a5a17c41d8aef4dd278bf3dd967e0669682/.DS_Store
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | "@babel/preset-react"
5 | ],
6 | "plugins": [
7 | "css-modules-transform",
8 | "@babel/plugin-proposal-class-properties",
9 | "@babel/plugin-syntax-object-rest-spread"
10 | ]
11 | }
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 |
3 | orbs:
4 | node: circleci/node@1.1.4
5 |
6 | jobs:
7 | build:
8 | executor:
9 | name: node/default
10 | tag: lts
11 | steps:
12 | - checkout
13 | - node/install-yarn
14 | - node/with-cache:
15 | steps:
16 | - run: yarn install --frozen-lockfile
17 | - run: npm run lint
18 | - run: npm run test
19 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "rules": {
4 | "quotes": 0,
5 | "no-trailing-spaces": 0,
6 | "eqeqeq": 0,
7 | "no-underscore-dangle": 0,
8 | "no-undef": 0,
9 | "no-extra-boolean-cast": 0,
10 | "no-mixed-spaces-and-tabs": 0,
11 | "no-alert": 0,
12 | "no-shadow": 0,
13 | "no-empty": 0,
14 | "no-irregular-whitespace": 0,
15 | "no-multi-spaces": 0,
16 | "no-new": 0,
17 | "no-unused-vars": 2, // disallow declaration of variables that are not used in the code
18 | "no-redeclare": 2, // disallow declaring the same variable more then once
19 | "new-cap": 0,
20 | //
21 | // eslint-plugin-react
22 | //
23 | // React specific linting rules for ESLint
24 | //
25 | //"react/display-name": 0, // Prevent missing displayName in a React component definition
26 | "react/jsx-boolean-value": [2, "always"], // Enforce boolean attributes notation in JSX
27 | "react/jsx-no-undef": 2, // Disallow undeclared variables in JSX
28 | "react/jsx-quotes": 0,
29 | "react/jsx-sort-prop-types": 0, // Enforce propTypes declarations alphabetical sorting
30 | "react/jsx-sort-props": 0, // Enforce props alphabetical sorting
31 | "react/jsx-uses-react": 2, // Prevent React to be incorrectly marked as unused
32 | "react/jsx-uses-vars": 2, // Prevent variables used in JSX to be incorrectly marked as unused
33 | //"react/no-did-mount-set-state": 2, // Prevent usage of setState in componentDidMount
34 | "react/no-did-update-set-state": 2, // Prevent usage of setState in componentDidUpdate
35 | "react/no-multi-comp": 0, // Prevent multiple component definition per file
36 | "react/no-unknown-property": 2, // Prevent usage of unknown DOM property
37 | "react/prop-types": 2, // Prevent missing props validation in a React component definition
38 | "react/react-in-jsx-scope": 2, // Prevent missing React when using JSX
39 | "react/self-closing-comp": 2, // Prevent extra closing tags for components without children
40 | },
41 | "globals": {
42 | "jQuery": true,
43 | "$": true,
44 | "reveal": true,
45 | "Pikaday": true,
46 | "NProgress": true,
47 | "cytoscape": true
48 | },
49 | "plugins": ["react"],
50 | "ecmaFeatures": {
51 | "arrowFunctions": true,
52 | "binaryLiterals": true,
53 | "blockBindings": true,
54 | "classes": true,
55 | "defaultParams": true,
56 | "destructuring": true,
57 | "forOf": true,
58 | "generators": true,
59 | "modules": true,
60 | "objectLiteralComputedProperties": true,
61 | "objectLiteralDuplicateProperties": true,
62 | "objectLiteralShorthandMethods": true,
63 | "objectLiteralShorthandProperties": true,
64 | "octalLiterals": true,
65 | "regexUFlag": true,
66 | "regexYFlag": true,
67 | "spread": true,
68 | "superInFunctions": true,
69 | "templateStrings": true,
70 | "unicodeCodePointEscapes": true,
71 | "globalReturn": true,
72 | "jsx": true
73 | },
74 | "env": {
75 | "browser": true,
76 | "es6": true
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | /dist/
11 |
12 | # Dependency directory
13 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
14 | node_modules
15 |
16 | # Precommit hook
17 | .jshint*
18 |
19 | /example/
20 | .coveralls.yml
21 | /reports/
22 | gulpfile.js
23 | coverage
24 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v8.15.1
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "8"
4 | - "10"
5 | after_script:
6 | - 'npm run coveralls'
7 | - 'npm run coveralls'
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://gitter.im/cht8687/react-listview-sticky-header?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
2 |
3 |
4 | React listview with sticky header
5 |
6 |
7 |
8 |
10 |
11 |
12 |
13 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
24 |
25 |
26 |
27 |
29 |
30 |
31 |
32 |
34 |
35 |
36 |
37 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | 
49 |
50 |
51 | ## Installation
52 |
53 |
54 | ### npm
55 |
56 | ```
57 | $ npm install --save react-listview-sticky-header
58 | ```
59 |
60 | Since React is peer dependency, you need to install it manually if you haven't.
61 |
62 |
63 | ## Demo
64 |
65 | [http://cht8687.github.io/react-listview-sticky-header/example/](http://cht8687.github.io/react-listview-sticky-header/example/)
66 |
67 |
68 | ## Usage
69 |
70 | ```js
71 |
77 | ```
78 |
79 | ## Options
80 |
81 | #### `data`: PropTypes.array.isRequired
82 |
83 | ```js
84 | const DATALIST = [
85 | {
86 | headerName : "ListA",
87 | items : [{
88 | title : "items1"
89 | }, {
90 | title : "items2"
91 | }]
92 | },
93 | {
94 | headerName : "ListB",
95 | items : [{
96 | title : "items1"
97 | }, {
98 | title : "items2"
99 | }]
100 | }
101 | ];
102 | ```
103 |
104 | #### `headerAttName`: PropTypes.string.isRequired
105 |
106 | variable name of header in your `data` object.
107 | In above example, it's `headerName`.
108 |
109 | #### `itemsAttName`: PropTypes.string.isRequired
110 |
111 | variable name which hold items data in your `data` object.
112 | In above example, it's `items`.
113 |
114 | #### `styles`: PropTypes.object.isRequired
115 |
116 | ```js
117 | let styles = {
118 | outerDiv: {
119 | height: '420px',
120 | overflowY: 'auto',
121 | outline: '#b9ceb6 dashed 1px',
122 | width: '383px'
123 | },
124 |
125 | ul: {
126 | margin: '0px',
127 | listStyleType: 'none',
128 | padding: '0px'
129 | },
130 |
131 | fixedPosition: {
132 | position: 'fixed',
133 | width: '383px',
134 | top: '0px'
135 | },
136 |
137 | listHeader: {
138 | width: '383px',
139 | height: '27px',
140 | background: '#94D6CF',
141 | color: 'white'
142 | },
143 |
144 | listItems: {
145 | color: '#a9adab'
146 | }
147 | }
148 | ```
149 |
150 | `outerDiv`, `ul`, `fixedPosition`, `listHeader`, `listItems` are required, you can modify the CSS to meet your needs.
151 |
152 |
153 | ## Development
154 |
155 | ```
156 | $ git clone git@github.com:cht8687/react-listview-sticky-header.git
157 | $ cd react-listview-sticky-header
158 | $ npm install
159 | $ webpack-dev-server
160 | ```
161 |
162 | Then
163 |
164 | ```
165 | open http://localhost:8080/webpack-dev-server/
166 | ```
167 |
168 | ## License
169 |
170 | MIT
171 |
172 |
173 | [](https://github.com/feross/standard)
174 |
175 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-listview-sticky-header",
3 | "version": "0.7.1",
4 | "description": "react listview with sticky header",
5 | "main": "dist/index.js",
6 | "scripts": {
7 | "lint": "standard src/**/*.js | snazzy",
8 | "pretest": "npm run lint",
9 | "prepublishOnly": "parallelshell -w \"npm run build:dist -s\" \"npm run build:example -s\" \"npm run build:bower -s\"",
10 | "prebuild": "rimraf dist example build",
11 | "build:dist": "babel src --out-dir dist --source-maps --ignore src/example",
12 | "build:example": "webpack --config webpack.config.js",
13 | "postbuild": "npm run test -s",
14 | "test": "babel-node test/index.js | tnyan",
15 | "coverage": "babel-node node_modules/isparta/bin/isparta cover test/index.js",
16 | "coveralls": "npm run coverage -s && coveralls < coverage/lcov.info",
17 | "postcoveralls": "rimraf ./coverage"
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "git+https://github.com/cht8687/react-listview-sticky-header.git"
22 | },
23 | "keywords": [
24 | "react",
25 | "react-component",
26 | "component",
27 | "react-listview",
28 | "listview",
29 | "sticky"
30 | ],
31 | "files": [
32 | "dist"
33 | ],
34 | "author": "Robert Chang ",
35 | "license": "MIT",
36 | "bugs": {
37 | "url": "https://github.com/cht8687/react-listview-sticky-header/issues"
38 | },
39 | "homepage": "https://github.com/cht8687/react-listview-sticky-header#readme",
40 | "peerDependencies": {
41 | "react": ">=0.14"
42 | },
43 | "devDependencies": {
44 | "@babel/cli": "^7.14.5",
45 | "@babel/core": "^7.0.1",
46 | "@babel/node": "^7.0.0",
47 | "@babel/plugin-proposal-class-properties": "^7.0.0",
48 | "@babel/plugin-syntax-object-rest-spread": "^7.0.0",
49 | "@babel/preset-env": "^7.0.0",
50 | "@babel/preset-react": "^7.0.0",
51 | "babel-eslint": "^9.0.0",
52 | "babel-loader": "^8.0.2",
53 | "babel-plugin-css-modules-transform": "^1.2.7",
54 | "babel-tape-runner": "^3.0.0",
55 | "classnames": "^2.2.5",
56 | "codecov.io": "^0.1.6",
57 | "coveralls": "^3.0.2",
58 | "css-loader": "^3.2.0",
59 | "enzyme": "^3.7.0",
60 | "enzyme-adapter-react-16": "^1.6.0",
61 | "eslint": "^5.6.0",
62 | "eslint-loader": "^2.1.0",
63 | "eslint-plugin-react": "^7.11.1",
64 | "extract-text-webpack-plugin": "^3.0.2",
65 | "faucet": "0.0.1",
66 | "html-webpack-plugin": "^3.2.0",
67 | "isparta": "^4.0.0",
68 | "mini-css-extract-plugin": "^0.4.2",
69 | "parallelshell": "^3.0.0",
70 | "react": "^16.6.0",
71 | "react-dom": "^16.6.0",
72 | "react-hot-loader": "^3.1.1",
73 | "react-test-renderer": "^16.6.0",
74 | "rimraf": "^2.4.3",
75 | "sinon": "^1.17.3",
76 | "snazzy": "^8.0.0",
77 | "tap-nyan": "0.0.2",
78 | "tap-xunit": "^2.3.0",
79 | "tape": "^4.5.1",
80 | "webpack": "^4.19.0",
81 | "webpack-cli": "^3.1.0",
82 | "webpack-dev-server": "^3.11.2"
83 | },
84 | "dependencies": {
85 | "prop-types": "^15.7.2"
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/react-listview-sticky-header.css:
--------------------------------------------------------------------------------
1 | .stickyHeader-listview_outerDiv {
2 | height: 400px;
3 | overflow-y: auto;
4 | outline: 1px dashed blue;
5 | width: 400px;
6 | }
7 |
8 | .stickyHeader-listview_ul {
9 | margin: 0px;
10 | list-style-type: none;
11 | padding: 0;
12 | }
13 |
14 | .stickyHeader-listview_fixedPosition {
15 | position : fixed;
16 | width : 383px;
17 | top: 0px;
18 | }
19 |
20 | .stickyHeader-listview_listHeader {
21 | width: 383px;
22 | height: 20px;
23 | background: orange;
24 | color: white;
25 | }
26 |
27 | .stickyHeader-listview_listItems {
28 | color: blue;
29 | }
30 |
--------------------------------------------------------------------------------
/src/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "rules": {
4 | "quotes": 0,
5 | "no-trailing-spaces": 0,
6 | "eqeqeq": 0,
7 | "no-underscore-dangle": 0,
8 | "no-undef": 0,
9 | "no-extra-boolean-cast": 0,
10 | "no-mixed-spaces-and-tabs": 0,
11 | "no-alert": 0,
12 | "no-shadow": 0,
13 | "no-empty": 0,
14 | "no-irregular-whitespace": 0,
15 | "no-multi-spaces": 0,
16 | "no-new": 0,
17 | "no-unused-vars": 2, // disallow declaration of variables that are not used in the code
18 | "no-redeclare": 2, // disallow declaring the same variable more then once
19 | "new-cap": 0,
20 | //
21 | // eslint-plugin-react
22 | //
23 | // React specific linting rules for ESLint
24 | //
25 | //"react/display-name": 0, // Prevent missing displayName in a React component definition
26 | "react/jsx-boolean-value": [2, "always"], // Enforce boolean attributes notation in JSX
27 | "react/jsx-no-undef": 2, // Disallow undeclared variables in JSX
28 | "react/jsx-quotes": 0,
29 | "react/jsx-sort-prop-types": 0, // Enforce propTypes declarations alphabetical sorting
30 | "react/jsx-sort-props": 0, // Enforce props alphabetical sorting
31 | "react/jsx-uses-react": 2, // Prevent React to be incorrectly marked as unused
32 | "react/jsx-uses-vars": 2, // Prevent variables used in JSX to be incorrectly marked as unused
33 | //"react/no-did-mount-set-state": 2, // Prevent usage of setState in componentDidMount
34 | "react/no-did-update-set-state": 2, // Prevent usage of setState in componentDidUpdate
35 | "react/no-multi-comp": 0, // Prevent multiple component definition per file
36 | "react/no-unknown-property": 2, // Prevent usage of unknown DOM property
37 | "react/prop-types": 2, // Prevent missing props validation in a React component definition
38 | "react/react-in-jsx-scope": 2, // Prevent missing React when using JSX
39 | "react/require-extension": [1, { "extensions": [".js"] }], // Restrict file extensions that may be required
40 | "react/self-closing-comp": 2, // Prevent extra closing tags for components without children
41 | "react/wrap-multilines": 2 // Prevent missing parentheses around multilines JSX
42 | },
43 | "globals": {
44 | "jQuery": true,
45 | "$": true,
46 | "reveal": true,
47 | "Pikaday": true,
48 | "NProgress": true,
49 | "cytoscape": true
50 | },
51 | "plugins": ["react"],
52 | "ecmaFeatures": {
53 | "arrowFunctions": true,
54 | "binaryLiterals": true,
55 | "blockBindings": true,
56 | "classes": true,
57 | "defaultParams": true,
58 | "destructuring": true,
59 | "forOf": true,
60 | "generators": true,
61 | "modules": true,
62 | "objectLiteralComputedProperties": true,
63 | "objectLiteralDuplicateProperties": true,
64 | "objectLiteralShorthandMethods": true,
65 | "objectLiteralShorthandProperties": true,
66 | "octalLiterals": true,
67 | "regexUFlag": true,
68 | "regexYFlag": true,
69 | "spread": true,
70 | "superInFunctions": true,
71 | "templateStrings": true,
72 | "unicodeCodePointEscapes": true,
73 | "globalReturn": true,
74 | "jsx": true
75 | },
76 | "env": {
77 | "browser": true,
78 | "es6": true
79 | }
80 | }
--------------------------------------------------------------------------------
/src/ReactListView.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { findDOMNode } from 'react-dom'
3 | import ListHeader from './lib/ListHeader'
4 | import ListItems from './lib/ListItems'
5 | import PropTypes from 'prop-types'
6 |
7 | export default class ReactListView extends Component {
8 | static propTypes = {
9 | data: PropTypes.array.isRequired,
10 | headerAttName: PropTypes.string.isRequired,
11 | itemsAttName: PropTypes.string.isRequired,
12 | styles: PropTypes.object.isRequired
13 | }
14 |
15 | constructor(props) {
16 | super(props)
17 |
18 | this.state = {
19 | events: ['scroll', 'mousewheel', 'DOMMouseScroll', 'MozMousePixelScroll', 'resize', 'touchmove', 'touchend'],
20 | _firstChildWrapper: '',
21 | _headerFixedPosition: '',
22 | _instances: {}
23 | }
24 | }
25 |
26 | componentDidMount() {
27 | this.initStickyHeaders()
28 | }
29 |
30 | componentWillUnmount() {
31 | // unRegister events listeners with the listview div
32 | this.state.events.forEach((type) => {
33 | if (window.addEventListener) {
34 | findDOMNode(this.refs.listview).removeEventListener(type, this.onScroll.bind(this), false)
35 | } else {
36 | findDOMNode(this.refs.listview).detachEvent('on' + type, this.onScroll.bind(this), false)
37 | }
38 | })
39 | }
40 |
41 | refsToArray(ctx, prefix) {
42 | let results = []
43 | for (let i = 0; ; i++) {
44 | let ref = ctx.refs[prefix + '-' + String(i)]
45 | if (ref) results.push(ref)
46 | else return results
47 | }
48 | }
49 |
50 | initStickyHeaders() {
51 | let listHeaders = this.refsToArray(this, 'ListHeader')
52 | let _originalPositions = listHeaders.map((l) => {
53 | let headerAndPosInfo = {
54 | headerObj: l,
55 | originalPosition: l.refs.header.getBoundingClientRect().top
56 | }
57 | return headerAndPosInfo
58 | })
59 | this.setState({
60 | _instances: Object.assign(this.state._instances, { _originalPositions }),
61 | _firstChildWrapper: listHeaders[0].refs.followWrap,
62 | _headerFixedPosition: listHeaders[0].refs.header.getBoundingClientRect().top
63 | })
64 |
65 | // Register events listeners with the listview div
66 | this.state.events.forEach((type) => {
67 | if (window.addEventListener) {
68 | findDOMNode(this.refs.listview).addEventListener(type, this.onScroll.bind(this), false)
69 | } else {
70 | findDOMNode(this.refs.listview).attachEvent('on' + type, this.onScroll.bind(this), false)
71 | }
72 | })
73 | }
74 |
75 | onScroll() {
76 | // update current header positions and apply fixed positions to the top one
77 | let currentWindowScrollTop = this.state._headerFixedPosition - this.state._firstChildWrapper.getBoundingClientRect().top
78 | this.state._instances._originalPositions.forEach((c, index) => {
79 | let currentNode = c.headerObj.refs.header
80 | const currentHeaderHeight = parseInt(currentNode.style.height, 10)
81 | let nextNode = null
82 | let topPos = null
83 | let ignoreCheck = false
84 | if (index < this.state._instances._originalPositions.length - 1) {
85 | nextNode = this.state._instances._originalPositions[index + 1]
86 | }
87 | if (index === 0 && currentWindowScrollTop === c.originalPosition) {
88 | currentNode.style.position = ''
89 | ignoreCheck = true
90 | }
91 | if (!ignoreCheck && (c.originalPosition) < (currentWindowScrollTop + this.state._headerFixedPosition + index * currentHeaderHeight)) {
92 | Object.assign(currentNode.style, this.props.styles.fixedPosition)
93 | // apply top value
94 | currentNode.style.top = `${this.state._headerFixedPosition}px`
95 | if (currentWindowScrollTop + index * currentHeaderHeight > nextNode.originalPosition) {
96 | currentNode.style.position = 'absolute'
97 | currentNode.style.top = `${topPos}px`
98 | }
99 | } else {
100 | currentNode.style.position = ''
101 | }
102 | })
103 | }
104 |
105 | render() {
106 | const { data, headerAttName, itemsAttName } = this.props
107 | const { styles: { outerDiv, ul, listHeader, listItems, li } } = this.props
108 | let _refi = 0
109 | let makeRef = () => {
110 | return `ListHeader-${_refi++}`
111 | }
112 |
113 | return (
114 |
115 |
116 | {
117 | Object.keys(data).map((k) => {
118 | const header = data[k][headerAttName]
119 | const items = data[k][itemsAttName]
120 | return (
121 | -
122 |
127 |
131 |
132 | )
133 | })
134 | }
135 |
136 |
137 | )
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/example/Example.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import { render } from 'react-dom'
3 | import ReactListView from '..'
4 | import { DATA } from './data'
5 |
6 | let styles = {
7 | outerDiv: {
8 | height: '420px',
9 | overflowY: 'auto',
10 | outline: '#b9ceb6 dashed 1px',
11 | width: '383px',
12 | margin: '0 auto'
13 | },
14 |
15 | ul: {
16 | margin: '0px',
17 | listStyleType: 'none',
18 | padding: '0px'
19 | },
20 |
21 | fixedPosition: {
22 | position: 'fixed',
23 | width: '383px',
24 | top: '0px'
25 | },
26 |
27 | listHeader: {
28 | width: '383px',
29 | height: '27px',
30 | background: '#94D6CF',
31 | color: 'white'
32 | },
33 |
34 | listItems: {
35 | color: '#a9adab'
36 | }
37 | }
38 |
39 | class App extends Component {
40 | static propTypes = {
41 | data: PropTypes.array.isRequired
42 | };
43 |
44 | render () {
45 | const { data } = this.props
46 | return (
47 |
53 | )
54 | }
55 | }
56 |
57 | const appRoot = document.createElement('div')
58 | appRoot.id = 'app'
59 | document.body.appendChild(appRoot)
60 |
61 | render(, appRoot)
62 |
--------------------------------------------------------------------------------
/src/example/data.js:
--------------------------------------------------------------------------------
1 | export const DATA = [
2 | {
3 | headerName: 'Electronics',
4 | items: [{
5 | title: 'Macbook'
6 | },
7 | {
8 | title: 'Dell'
9 | },
10 | {
11 | title: 'HP'
12 | },
13 | {
14 | title: 'Asus'
15 | },
16 | {
17 | title: 'SAMSUNG'
18 | },
19 | {
20 | title: 'Office Works'
21 | }]
22 | },
23 | {
24 | headerName: 'Home Appliances',
25 | items: [{
26 | title: 'LG'
27 | },
28 | {
29 | title: 'Philips'
30 | },
31 | {
32 | title: 'BEKO'
33 | },
34 | {
35 | title: 'Breville'
36 | },
37 | {
38 | title: 'Fisher & Paykel'
39 | },
40 | {
41 | title: 'Hisense'
42 | }]
43 | },
44 | {
45 | headerName: 'Headphones & DJ',
46 | items: [{
47 | title: 'BOSE'
48 | },
49 | {
50 | title: 'Beats'
51 | },
52 | {
53 | title: 'Sony'
54 | },
55 | {
56 | title: 'JBL'
57 | },
58 | {
59 | title: 'Marley'
60 | },
61 | {
62 | title: 'Pioneer DJ'
63 | }]
64 | },
65 | {
66 | headerName: 'Kitchen',
67 | items: [{
68 | title: 'items1'
69 | },
70 | {
71 | title: 'items2'
72 | },
73 | {
74 | title: 'items3'
75 | },
76 | {
77 | title: 'items4'
78 | },
79 | {
80 | title: 'items5'
81 | },
82 | {
83 | title: 'items6'
84 | }]
85 | },
86 | {
87 | headerName: 'ListE',
88 | items: [{
89 | title: 'items1'
90 | },
91 | {
92 | title: 'items2'
93 | },
94 | {
95 | title: 'items3'
96 | },
97 | {
98 | title: 'items4'
99 | },
100 | {
101 | title: 'items5'
102 | },
103 | {
104 | title: 'items6'
105 | }]
106 | },
107 | {
108 | headerName: 'ListF',
109 | items: [{
110 | title: 'items1'
111 | },
112 | {
113 | title: 'items2'
114 | },
115 | {
116 | title: 'items3'
117 | },
118 | {
119 | title: 'items4'
120 | },
121 | {
122 | title: 'items5'
123 | },
124 | {
125 | title: 'items6'
126 | }]
127 | },
128 | {
129 | headerName: 'ListG',
130 | items: [{
131 | title: 'items1'
132 | },
133 | {
134 | title: 'items2'
135 | },
136 | {
137 | title: 'items3'
138 | },
139 | {
140 | title: 'items4'
141 | },
142 | {
143 | title: 'items5'
144 | },
145 | {
146 | title: 'items6'
147 | }]
148 | },
149 | {
150 | headerName: 'ListH',
151 | items: [{
152 | title: 'items1'
153 | },
154 | {
155 | title: 'items2'
156 | },
157 | {
158 | title: 'items3'
159 | },
160 | {
161 | title: 'items4'
162 | },
163 | {
164 | title: 'items5'
165 | },
166 | {
167 | title: 'items6'
168 | }]
169 | },
170 | {
171 | headerName: 'ListI',
172 | items: [{
173 | title: 'items1'
174 | },
175 | {
176 | title: 'items2'
177 | },
178 | {
179 | title: 'items3'
180 | },
181 | {
182 | title: 'items4'
183 | },
184 | {
185 | title: 'items5'
186 | },
187 | {
188 | title: 'items6'
189 | }]
190 | },
191 | {
192 | headerName: 'ListJ',
193 | items: [{
194 | title: 'items1'
195 | },
196 | {
197 | title: 'items2'
198 | },
199 | {
200 | title: 'items3'
201 | },
202 | {
203 | title: 'items4'
204 | },
205 | {
206 | title: 'items5'
207 | },
208 | {
209 | title: 'items6'
210 | }]
211 | },
212 | {
213 | headerName: 'ListK',
214 | items: [{
215 | title: 'items1'
216 | },
217 | {
218 | title: 'items2'
219 | },
220 | {
221 | title: 'items3'
222 | },
223 | {
224 | title: 'items4'
225 | },
226 | {
227 | title: 'items5'
228 | },
229 | {
230 | title: 'items6'
231 | }]
232 | },
233 | {
234 | headerName: 'ListL',
235 | items: [{
236 | title: 'items1'
237 | },
238 | {
239 | title: 'items2'
240 | },
241 | {
242 | title: 'items3'
243 | },
244 | {
245 | title: 'items4'
246 | },
247 | {
248 | title: 'items5'
249 | },
250 | {
251 | title: 'items6'
252 | }]
253 | },
254 | {
255 | headerName: 'ListE',
256 | items: [{
257 | title: 'items1'
258 | },
259 | {
260 | title: 'items2'
261 | },
262 | {
263 | title: 'items3'
264 | },
265 | {
266 | title: 'items4'
267 | },
268 | {
269 | title: 'items5'
270 | },
271 | {
272 | title: 'items6'
273 | }]
274 | },
275 | {
276 | headerName: 'ListE',
277 | items: [{
278 | title: 'items1'
279 | },
280 | {
281 | title: 'items2'
282 | },
283 | {
284 | title: 'items3'
285 | },
286 | {
287 | title: 'items4'
288 | },
289 | {
290 | title: 'items5'
291 | },
292 | {
293 | title: 'items6'
294 | }]
295 | },
296 | {
297 | headerName: 'ListE',
298 | items: [{
299 | title: 'items1'
300 | },
301 | {
302 | title: 'items2'
303 | },
304 | {
305 | title: 'items3'
306 | },
307 | {
308 | title: 'items4'
309 | },
310 | {
311 | title: 'items5'
312 | },
313 | {
314 | title: 'items6'
315 | }]
316 | }
317 | ]
318 |
--------------------------------------------------------------------------------
/src/example/react-listview-sticky-header.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cht8687/react-listview-sticky-header/18fc8a5a17c41d8aef4dd278bf3dd967e0669682/src/example/react-listview-sticky-header.gif
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import ReactListView from './ReactListView'
2 | export default ReactListView
3 |
4 |
--------------------------------------------------------------------------------
/src/lib/ListHeader.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | export default class ListHeader extends Component {
5 | static propTypes = {
6 | header: PropTypes.string.isRequired,
7 | styles: PropTypes.object.isRequired
8 | }
9 |
10 | render () {
11 | const { header, styles } = this.props
12 | return (
13 |
16 | )
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/lib/ListItems.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | export default class ListItem extends Component {
5 | static propTypes = {
6 | items: PropTypes.array.isRequired,
7 | styles: PropTypes.object.isRequired
8 | }
9 |
10 | render () {
11 | const { items, styles } = this.props
12 | return (
13 |
14 | {
15 | [...items].map((item, index) => {
16 | return (
17 | {item.title}
18 | )
19 | })
20 | }
21 |
22 | )
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../src/.eslintrc",
3 |
4 | "env": {
5 | "jasmine": true
6 | },
7 |
8 | "rules": {
9 | "one-var": 0,
10 | "no-undefined": 0
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/test/data.js:
--------------------------------------------------------------------------------
1 | export const data = [
2 | {
3 | headerName: 'ListA',
4 | items: [{
5 | title: 'items1'
6 | },
7 | {
8 | title: 'items2'
9 | },
10 | {
11 | title: 'items3'
12 | },
13 | {
14 | title: 'items4'
15 | },
16 | {
17 | title: 'items5'
18 | },
19 | {
20 | title: 'items6'
21 | }]
22 | },
23 | {
24 | headerName: 'ListB is bravo',
25 | items: [{
26 | title: 'items1'
27 | },
28 | {
29 | title: 'items2'
30 | },
31 | {
32 | title: 'items3'
33 | },
34 | {
35 | title: 'items4'
36 | },
37 | {
38 | title: 'items5'
39 | },
40 | {
41 | title: 'items6'
42 | }]
43 | },
44 | {
45 | headerName: 'ListC is Charlie',
46 | items: [{
47 | title: 'items1'
48 | },
49 | {
50 | title: 'items2'
51 | },
52 | {
53 | title: 'items3'
54 | },
55 | {
56 | title: 'items4'
57 | },
58 | {
59 | title: 'items5'
60 | },
61 | {
62 | title: 'items6'
63 | }]
64 | },
65 | {
66 | headerName: 'ListD is Dynamic',
67 | items: [{
68 | title: 'items1'
69 | },
70 | {
71 | title: 'items2'
72 | },
73 | {
74 | title: 'items3'
75 | },
76 | {
77 | title: 'items4'
78 | },
79 | {
80 | title: 'items5'
81 | },
82 | {
83 | title: 'items6'
84 | }]
85 | },
86 | {
87 | headerName: 'ListE',
88 | items: [{
89 | title: 'items1'
90 | },
91 | {
92 | title: 'items2'
93 | },
94 | {
95 | title: 'items3'
96 | },
97 | {
98 | title: 'items4'
99 | },
100 | {
101 | title: 'items5'
102 | },
103 | {
104 | title: 'items6'
105 | }]
106 | }
107 | ]
108 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import test from 'tape'
3 | import ListHeader from '../src/lib/ListHeader'
4 | import ListItems from '../src/lib/ListItems'
5 | import ReactListView from '../src/ReactListView'
6 | import { data } from './data'
7 | import { styles } from './style'
8 | import { shallow } from 'enzyme'
9 | import { sinon } from 'sinon'
10 |
11 | test('----- React Component Tests: ReactListView -----', t => {
12 | t.plan(3)
13 | const app = shallow()
14 | t.ok(ReactListView instanceof Function, 'should be function')
15 | t.equal(5, app.find(ListHeader).length, 'should have 5 list headers')
16 | t.equal(5, app.find(ListItems).length, 'should have 5 list items')
17 | })
18 |
19 | test('----- ReactListView state test-----', t => {
20 | t.plan(2)
21 | const app = shallow()
22 | t.equal(7, app.update().state('events').length, 'should have 7 events in state')
23 | t.ok(app.update().state('_instances') instanceof Object, 'should render _instances')
24 | })
25 |
26 | test('----- React Component Tests: ListHeader -----', t => {
27 | t.plan(2)
28 | t.ok(ListHeader instanceof Function, 'should be function')
29 | const listHeader = {
30 | width: '383px',
31 | height: '20px',
32 | background: 'green',
33 | color: 'white'
34 | }
35 | const listheader = shallow()
36 | t.equal(true, listheader.containsMatchingElement([ListA
]))
37 | })
38 |
39 | test('----- React Component Tests: ListItems -----', t => {
40 | t.plan(2)
41 | t.ok(ListItems instanceof Function, 'should be function')
42 | const items = [{
43 | title: 'items1'
44 | }, {
45 | title: 'items2'
46 | }, {
47 | title: 'items3'
48 | }, {
49 | title: 'items4'
50 | }, {
51 | title: 'items5'
52 | }, {
53 | title: 'items6'
54 | }]
55 | const listItems = {
56 | color: 'blue'
57 | }
58 | const listitems = shallow()
59 | t.equal(true, listitems.containsMatchingElement([items1]))
60 | t.end()
61 | })
62 |
--------------------------------------------------------------------------------
/test/style.js:
--------------------------------------------------------------------------------
1 | export let styles = {
2 | outerDiv: {
3 | height: '400px',
4 | overflowY: 'auto',
5 | outline: '1px dashed blue',
6 | width: '400px'
7 | },
8 |
9 | ul: {
10 | margin: '0px',
11 | listStyleType: 'none',
12 | padding: '0'
13 | },
14 |
15 | fixedPosition: {
16 | position: 'fixed',
17 | width: '383px',
18 | top: '0px'
19 | },
20 |
21 | listHeader: {
22 | width: '383px',
23 | height: '20px',
24 | background: 'green',
25 | color: 'white'
26 | },
27 |
28 | listItems: {
29 | color: 'blue'
30 | }
31 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var HtmlWebpackPlugin = require('html-webpack-plugin');
3 | var path = require('path');
4 | var env = process.env.NODE_ENV || 'development';
5 | const MiniCssExtractPlugin = require("mini-css-extract-plugin");
6 |
7 | module.exports = {
8 | devtool: 'source-map',
9 | entry: [
10 | './src/example/Example.js',
11 | 'webpack-dev-server/client?http://localhost:8080',
12 | 'webpack/hot/only-dev-server'
13 | ],
14 | output: {filename: 'bundle.js', path: path.resolve('example')},
15 | plugins: [
16 | new HtmlWebpackPlugin(),
17 | new webpack.DefinePlugin({
18 | 'process.env': {
19 | NODE_ENV: '"' + env + '"'
20 | }
21 | }),
22 | new webpack.HotModuleReplacementPlugin(),
23 | new MiniCssExtractPlugin({
24 | // Options similar to the same options in webpackOptions.output
25 | // both options are optional
26 | filename: "[name].css",
27 | chunkFilename: "[id].css"
28 | })
29 | ],
30 | module: {
31 | rules: [
32 | {
33 | test: /\.js$/,
34 | loaders: ['babel-loader'],
35 | include: [path.resolve('src')]
36 | },
37 | {
38 | test: /\.css$/,
39 | use: [
40 | {
41 | loader: MiniCssExtractPlugin.loader,
42 | options: {
43 | // you can specify a publicPath here
44 | // by default it use publicPath in webpackOptions.output
45 | publicPath: '../'
46 | }
47 | },
48 | "css-loader"
49 | ]
50 | },
51 | {
52 | enforce: 'pre',
53 | test: /\.js$/,
54 | loaders: ['eslint-loader'],
55 | include: [path.resolve('src')]
56 | }
57 | ]
58 | },
59 | resolve: { extensions: ['.js'] },
60 | stats: { colors: true },
61 | devServer: {
62 | hot: true,
63 | historyApiFallback: true,
64 | stats: {
65 | chunkModules: false,
66 | colors: true
67 | }
68 | }};
69 |
--------------------------------------------------------------------------------