├── .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 | [![Join the chat at https://gitter.im/cht8687/react-listview-sticky-header](https://badges.gitter.im/cht8687/react-listview-sticky-header.svg)](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 | Circle CI 10 | 11 | 12 | 13 | NPM Version 15 | 16 | 17 | 18 | Coverage Status 19 | 20 | 21 | 22 | Build Status 24 | 25 | 26 | 27 | Downloads 29 | 30 | 31 | 32 | Dependency Status 34 | 35 | 36 | 37 | License 39 | 40 |

41 | 42 |

43 | 44 |

45 | 46 | 47 | 48 | ![React Listview sticky header](src/example/react-listview-sticky-header.gif) 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 | [![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](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 |
14 |
{header}
15 |
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 | --------------------------------------------------------------------------------