├── .babelrc ├── .bithoundrc ├── .editorconfig ├── .eslintrc.js ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── LICENSE ├── README.md ├── demo ├── src │ ├── components │ │ └── App │ │ │ ├── App.js │ │ │ ├── App.less │ │ │ ├── components │ │ │ ├── Example0 │ │ │ │ └── Example0.js │ │ │ ├── Example1 │ │ │ │ └── Example1.js │ │ │ ├── Example10 │ │ │ │ └── Example10.js │ │ │ ├── Example11 │ │ │ │ ├── Example11.js │ │ │ │ └── Example11.less │ │ │ ├── Example12 │ │ │ │ ├── Example12.js │ │ │ │ └── Example12.less │ │ │ ├── Example2 │ │ │ │ └── Example2.js │ │ │ ├── Example3 │ │ │ │ └── Example3.js │ │ │ ├── Example4 │ │ │ │ └── Example4.js │ │ │ ├── Example5 │ │ │ │ └── Example5.js │ │ │ ├── Example6 │ │ │ │ └── Example6.js │ │ │ ├── Example7 │ │ │ │ └── Example7.js │ │ │ ├── Example8 │ │ │ │ └── Example8.js │ │ │ ├── Example9 │ │ │ │ ├── Example9.js │ │ │ │ └── countries.js │ │ │ ├── ForkMeOnGitHub │ │ │ │ ├── ForkMeOnGitHub.js │ │ │ │ └── ForkMeOnGitHub.less │ │ │ ├── SourceCodeLink │ │ │ │ ├── SourceCodeLink.js │ │ │ │ └── SourceCodeLink.less │ │ │ └── theme.less │ │ │ ├── redux.js │ │ │ └── utils.js │ ├── index.html │ └── index.js └── standalone │ ├── app.css │ ├── app.js │ ├── compiled.app.js │ └── index.html ├── package-lock.json ├── package.json ├── scripts └── deploy-to-gh-pages.sh ├── server.js ├── src ├── Autowhatever.js ├── Item.js ├── ItemsList.js ├── SectionTitle.js ├── compareObjects.js └── index.js ├── test ├── compareObjects.test.js ├── helpers.js ├── multi-section │ ├── Autowhatever.test.js │ ├── AutowhateverApp.js │ └── sections.js ├── plain-list │ ├── Autowhatever.test.js │ ├── AutowhateverApp.js │ └── items.js ├── render-input-component │ ├── Autowhatever.test.js │ ├── AutowhateverApp.js │ └── items.js ├── render-items-container │ ├── Autowhatever.test.js │ ├── AutowhateverApp.js │ └── items.js └── setup.js ├── webpack.dev.config.js ├── webpack.gh-pages.config.js ├── webpack.standalone-demo.config.js └── webpack.standalone.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /.bithoundrc: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": [ 3 | "**/node_modules/**", 4 | "demo/standalone/compiled.app.js", 5 | "index.js" 6 | ], 7 | "test": [ 8 | "**/test/**" 9 | ], 10 | "critics": { 11 | "wc": { 12 | "limit": 5000 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | node: true, 5 | browser: true, 6 | mocha: true 7 | }, 8 | parser: 'babel-eslint', 9 | plugins: [ 10 | 'react' 11 | ], 12 | extends: [ 13 | 'eslint:recommended' 14 | ], 15 | rules: { 16 | 'array-callback-return': 2, 17 | 'brace-style': [2, '1tbs'], 18 | 'camelcase': [2, { properties: 'always' }], 19 | 'comma-dangle': [2, 'never'], 20 | 'comma-style': [2, 'last'], 21 | 'eol-last': 2, 22 | 'func-call-spacing': 2, 23 | 'indent': [2, 2, { SwitchCase: 1 }], 24 | 'key-spacing': [2, { beforeColon: false, afterColon: true }], 25 | 'keyword-spacing': 2, 26 | 'linebreak-style': [2, 'unix'], 27 | 'no-cond-assign': [2, 'always'], 28 | 'no-console': 2, 29 | 'no-global-assign': 2, 30 | 'no-multiple-empty-lines': [2, { max: 1 }], 31 | 'no-restricted-properties': [2, 32 | { 33 | object: 'describe', 34 | property: 'only', 35 | message: 'Please run all tests!' 36 | }, 37 | { 38 | object: 'describe', 39 | property: 'skip', 40 | message: 'Please run all tests!' 41 | }, 42 | { 43 | object: 'it', 44 | property: 'only', 45 | message: 'Please run all tests!' 46 | }, 47 | { 48 | object: 'it', 49 | property: 'skip', 50 | message: 'Please run all tests!' 51 | } 52 | ], 53 | 'no-template-curly-in-string': 2, 54 | 'no-trailing-spaces': 2, 55 | 'no-unused-vars': 2, 56 | 'no-whitespace-before-property': 2, 57 | 'newline-after-var': [2, 'always'], 58 | 'object-curly-spacing': [2, 'always'], 59 | 'prefer-rest-params': 2, 60 | 'quote-props': [2, 'as-needed'], 61 | 'quotes': [2, 'single'], 62 | 'semi': [2, 'always'], 63 | 'space-before-blocks': [2, 'always'], 64 | 'space-before-function-paren': [2, 'never'], 65 | 'space-in-parens': [2, 'never'], 66 | 'template-curly-spacing': [2, 'never'], 67 | 68 | 'react/display-name': 0, 69 | 'react/forbid-prop-types': 0, 70 | 'react/no-comment-textnodes': 0, 71 | 'react/no-danger': 2, 72 | 'react/no-danger-with-children': 2, 73 | 'react/no-deprecated': 2, 74 | 'react/no-did-mount-set-state': 2, 75 | 'react/no-did-update-set-state': 2, 76 | 'react/no-direct-mutation-state': 2, 77 | 'react/no-find-dom-node': 2, 78 | 'react/no-is-mounted': 2, 79 | 'react/no-multi-comp': [2, { ignoreStateless: true }], 80 | 'react/no-render-return-value': 2, 81 | 'react/no-set-state': 0, 82 | 'react/no-string-refs': 2, 83 | 'react/no-unknown-property': 2, 84 | 'react/no-unused-prop-types': 0, // https://github.com/yannickcr/eslint-plugin-react/pull/835 85 | 'react/prefer-es6-class': [2, 'always'], 86 | 'react/prefer-stateless-function': 2, 87 | 'react/prop-types': [2, { skipUndeclared: true }], 88 | 'react/react-in-jsx-scope': 2, 89 | 'react/require-optimization': 0, 90 | 'react/require-render-return': 2, 91 | 'react/self-closing-comp': 2, 92 | 'react/sort-comp': 2, 93 | 'react/sort-prop-types': 0, 94 | 'react/style-prop-object': 2, 95 | 96 | 'react/jsx-boolean-value': [2, 'always'], 97 | 'react/jsx-closing-bracket-location': [2, 'tag-aligned'], 98 | 'react/jsx-curly-spacing': [2, 'never', { allowMultiline: true }], 99 | 'react/jsx-equals-spacing': [2, 'never'], 100 | 'react/jsx-filename-extension': [2, { extensions: ['.js'] }], 101 | 'react/jsx-first-prop-new-line': [2, 'multiline'], 102 | 'react/jsx-handler-names': 0, 103 | 'react/jsx-indent': [2, 2], 104 | 'react/jsx-indent-props': [2, 2], 105 | 'react/jsx-key': 2, 106 | 'react/jsx-max-props-per-line': 0, 107 | 'react/jsx-no-bind': 2, 108 | 'react/jsx-no-duplicate-props': 2, 109 | 'react/jsx-no-literals': 0, 110 | 'react/jsx-no-target-blank': 2, 111 | 'react/jsx-no-undef': 2, 112 | 'react/jsx-pascal-case': 2, 113 | 'react/jsx-sort-props': 0, 114 | 'react/jsx-space-before-closing': [2, 'always'], 115 | 'react/jsx-uses-react': 2, 116 | 'react/jsx-uses-vars': 2, 117 | 'react/jsx-wrap-multilines': 2 118 | } 119 | }; 120 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Before submitting a Pull Request, please open an issue to discuss the proposed change. 2 | 3 | Once you know that your change is aligned with `react-autowhatever` vision: 4 | 5 | 1. Add your goodness 6 | 2. Add tests 7 | 3. Update docs to reflect changes 8 | 4. Make sure that `npm run build` is happy 9 | 5. Submit a Pull Request (with a detailed description please) 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Are you reporting a bug? 2 | 3 | * Please create a Codepen that demonstrates your issue. You can start from [this example](http://codepen.io/moroshko/pen/rraQYx). 4 | 5 | * Provide the steps to reproduce the issue. 6 | 7 | ## Are you making a feature request? 8 | 9 | * Please describe your use case. 10 | 11 | * If you have ideas how to extend the Autowhatever API to support your new feature, please share! 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Thanks a lot for contributing to react-autowhatever :beers: 2 | 3 | Before submitting the Pull Request, please: 4 | 5 | * write a clear description of what this Pull Request is trying to achieve 6 | * `npm run build` to see that you didn't break anything 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | demo/dist 4 | npm-debug.log 5 | yarn.lock 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2016 Misha Moroshko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the “Software”), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | 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 | [![Build Status](https://img.shields.io/codeship/6c79f8c0-2565-0133-4af8-72f090cba113/master.svg?style=flat-square)](https://codeship.com/projects/96953) 2 | 3 | [![npm Downloads](https://img.shields.io/npm/dm/react-autowhatever.svg?style=flat-square)](https://npmjs.org/package/react-autowhatever) 4 | [![npm Version](https://img.shields.io/npm/v/react-autowhatever.svg?style=flat-square)](https://npmjs.org/package/react-autowhatever) 5 | 6 | # React Autowhatever 7 | 8 | Accessible rendering layer for Autosuggest and Autocomplete components. 9 | 10 | ## Demo 11 | 12 | Check out the [Homepage](http://react-autowhatever.js.org) and the [Codepen examples](http://codepen.io/collection/nmZqgW). 13 | 14 | ## Installation 15 | 16 | ```shell 17 | yarn add react-autowhatever 18 | ``` 19 | 20 | or 21 | 22 | ```shell 23 | npm install react-autowhatever --save 24 | ``` 25 | 26 | ## Related 27 | 28 | * [react-autosuggest](https://github.com/moroshko/react-autosuggest) - WAI-ARIA compliant React autosuggest component 29 | 30 | ## License 31 | 32 | [MIT](http://moroshko.mit-license.org) 33 | -------------------------------------------------------------------------------- /demo/src/components/App/App.js: -------------------------------------------------------------------------------- 1 | import styles from './App.less'; 2 | 3 | import React from 'react'; 4 | import ForkMeOnGitHub from 'ForkMeOnGitHub/ForkMeOnGitHub'; 5 | import Example0 from 'Example0/Example0'; 6 | import Example1 from 'Example1/Example1'; 7 | import Example2 from 'Example2/Example2'; 8 | import Example3 from 'Example3/Example3'; 9 | import Example4 from 'Example4/Example4'; 10 | import Example5 from 'Example5/Example5'; 11 | import Example6 from 'Example6/Example6'; 12 | import Example7 from 'Example7/Example7'; 13 | import Example8 from 'Example8/Example8'; 14 | import Example9 from 'Example9/Example9'; 15 | import Example10 from 'Example10/Example10'; 16 | import Example11 from 'Example11/Example11'; 17 | import Example12 from 'Example12/Example12'; 18 | 19 | export default function App() { 20 | return ( 21 |
22 |
23 |

24 | React Autowhatever 25 |

26 |

27 | Accessible rendering layer for Autosuggest and Autocomplete components 28 |

29 |
30 |
31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 | 39 |
40 |
41 | 42 |
43 |
44 | 45 |
46 |
47 | 48 |
49 |
50 | 51 |
52 |
53 | 54 |
55 |
56 | 57 |
58 |
59 | 60 |
61 |
62 | 63 |
64 |
65 | 66 |
67 |
68 | 69 |
70 |
71 | 72 |
73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /demo/src/components/App/App.less: -------------------------------------------------------------------------------- 1 | .container { 2 | font-family: Helvetica, Arial, sans-serif; 3 | margin-bottom: 300px; 4 | } 5 | 6 | .headerContainer { 7 | margin-bottom: 30px; 8 | } 9 | 10 | .header { 11 | font-size: 26px; 12 | } 13 | 14 | .subHeader { 15 | font-size: 18px; 16 | } 17 | 18 | .examplesContainer { 19 | display: flex; 20 | flex-wrap: wrap; 21 | } 22 | 23 | .exampleContainer { 24 | margin: 0 30px 30px 0; 25 | } 26 | -------------------------------------------------------------------------------- /demo/src/components/App/components/Example0/Example0.js: -------------------------------------------------------------------------------- 1 | import theme from '../theme.less'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { connect } from 'react-redux'; 6 | import { updateInputValue } from '../../redux'; 7 | import Autowhatever from 'Autowhatever'; 8 | import SourceCodeLink from 'SourceCodeLink/SourceCodeLink'; 9 | 10 | const exampleId = '0'; 11 | const file = `demo/src/components/App/components/Example${exampleId}/Example${exampleId}.js`; 12 | 13 | function mapStateToProps(state) { 14 | return { 15 | value: state[exampleId].value 16 | }; 17 | } 18 | 19 | function mapDispatchToProps(dispatch) { 20 | return { 21 | onChange: event => dispatch(updateInputValue(exampleId, event.target.value)) 22 | }; 23 | } 24 | 25 | function Example(props) { 26 | const { value, onChange } = props; 27 | const inputProps = { value, onChange }; 28 | 29 | return ( 30 |
31 | 37 | 38 |
39 | ); 40 | } 41 | 42 | Example.propTypes = { 43 | value: PropTypes.string.isRequired, 44 | onChange: PropTypes.func.isRequired 45 | }; 46 | 47 | export default connect(mapStateToProps, mapDispatchToProps)(Example); 48 | -------------------------------------------------------------------------------- /demo/src/components/App/components/Example1/Example1.js: -------------------------------------------------------------------------------- 1 | import theme from '../theme.less'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { connect } from 'react-redux'; 6 | import { updateInputValue } from '../../redux'; 7 | import Autowhatever from 'Autowhatever'; 8 | import SourceCodeLink from 'SourceCodeLink/SourceCodeLink'; 9 | 10 | const exampleId = '1'; 11 | const file = `demo/src/components/App/components/Example${exampleId}/Example${exampleId}.js`; 12 | 13 | function mapStateToProps(state) { 14 | return { 15 | value: state[exampleId].value 16 | }; 17 | } 18 | 19 | function mapDispatchToProps(dispatch) { 20 | return { 21 | onChange: event => dispatch(updateInputValue(exampleId, event.target.value)) 22 | }; 23 | } 24 | 25 | const items = [{ 26 | text: 'Apple' 27 | }, { 28 | text: 'Banana' 29 | }, { 30 | text: 'Cherry' 31 | }, { 32 | text: 'Grapefruit' 33 | }, { 34 | text: 'Lemon' 35 | }]; 36 | 37 | function renderItem(item) { 38 | return ( 39 | {item.text} 40 | ); 41 | } 42 | 43 | function Example(props) { 44 | const { value, onChange } = props; 45 | const inputProps = { value, onChange }; 46 | 47 | return ( 48 |
49 | 56 | 57 |
58 | ); 59 | } 60 | 61 | Example.propTypes = { 62 | value: PropTypes.string.isRequired, 63 | onChange: PropTypes.func.isRequired 64 | }; 65 | 66 | export default connect(mapStateToProps, mapDispatchToProps)(Example); 67 | -------------------------------------------------------------------------------- /demo/src/components/App/components/Example10/Example10.js: -------------------------------------------------------------------------------- 1 | import theme from '../theme.less'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { connect } from 'react-redux'; 6 | import { updateInputValue } from '../../redux'; 7 | import Autowhatever from 'Autowhatever'; 8 | import SourceCodeLink from 'SourceCodeLink/SourceCodeLink'; 9 | 10 | const exampleId = '10'; 11 | const file = `demo/src/components/App/components/Example${exampleId}/Example${exampleId}.js`; 12 | 13 | const renderInputComponent = inputProps => { 14 | const style = { 15 | border: '0 solid green', 16 | borderBottomWidth: '1px', 17 | borderRadius: 0 18 | }; 19 | 20 | return ( 21 | 22 | ); 23 | }; 24 | 25 | const mapStateToProps = state => ({ 26 | value: state[exampleId].value 27 | }); 28 | 29 | const mapDispatchToProps = dispatch => ({ 30 | onChange: event => dispatch(updateInputValue(exampleId, event.target.value)) 31 | }); 32 | 33 | const Example = props => { 34 | const { value, onChange } = props; 35 | const inputProps = { 36 | placeholder: 'Custom input, no items here', 37 | value, 38 | onChange 39 | }; 40 | 41 | return ( 42 |
43 | 50 | 51 |
52 | ); 53 | }; 54 | 55 | Example.propTypes = { 56 | value: PropTypes.string.isRequired, 57 | onChange: PropTypes.func.isRequired 58 | }; 59 | 60 | export default connect(mapStateToProps, mapDispatchToProps)(Example); 61 | -------------------------------------------------------------------------------- /demo/src/components/App/components/Example11/Example11.js: -------------------------------------------------------------------------------- 1 | import styles from './Example11.less'; 2 | 3 | import React, { Component } from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { connect } from 'react-redux'; 6 | import { updateInputValue } from '../../redux'; 7 | import Autowhatever from 'Autowhatever'; 8 | import SourceCodeLink from 'SourceCodeLink/SourceCodeLink'; 9 | 10 | const theme = { 11 | container: { 12 | position: 'relative' 13 | }, 14 | input: { 15 | width: '240px', 16 | height: '30px', 17 | padding: '10px 20px', 18 | fontSize: '16px', 19 | fontFamily: 'Helvetica, Arial, sans-serif', 20 | border: '1px solid #aaa', 21 | borderRadius: '4px', 22 | boxSizing: 'content-box' 23 | }, 24 | inputOpen: { 25 | borderBottomLeftRadius: 0, 26 | borderBottomRightRadius: 0 27 | }, 28 | inputFocused: { 29 | outline: 'none' 30 | }, 31 | itemsContainer: { 32 | display: 'none' 33 | }, 34 | itemsContainerOpen: { 35 | display: 'block', 36 | position: 'relative', 37 | top: '-1px', 38 | width: '280px', 39 | border: '1px solid #aaa', 40 | backgroundColor: '#fff', 41 | fontSize: '16px', 42 | lineHeight: 1.25, 43 | borderBottomLeftRadius: '4px', 44 | borderBottomRightRadius: '4px', 45 | zIndex: 2, 46 | maxHeight: '260px', 47 | overflowY: 'auto' 48 | }, 49 | itemsList: { 50 | margin: 0, 51 | padding: 0, 52 | listStyleType: 'none' 53 | }, 54 | item: { 55 | cursor: 'pointer', 56 | padding: '10px 20px' 57 | }, 58 | itemHighlighted: { 59 | backgroundColor: '#ddd' 60 | } 61 | }; 62 | 63 | const exampleId = '11'; 64 | const file = `demo/src/components/App/components/Example${exampleId}/Example${exampleId}.js`; 65 | 66 | const renderItemsContainer = ({ children, containerProps, query }) => ( 67 |
68 | {children} 69 |
70 | { 71 | query 72 | ? Press Enter to search {query} 73 | : Powered by react-autowhatever 74 | } 75 |
76 |
77 | ); 78 | 79 | const mapStateToProps = state => ({ 80 | value: state[exampleId].value 81 | }); 82 | 83 | const mapDispatchToProps = dispatch => ({ 84 | onChange: event => dispatch(updateInputValue(exampleId, event.target.value)) 85 | }); 86 | 87 | const items = [{ 88 | text: 'Apple' 89 | }, { 90 | text: 'Banana' 91 | }, { 92 | text: 'Cherry' 93 | }, { 94 | text: 'Grapefruit' 95 | }, { 96 | text: 'Lemon' 97 | }]; 98 | 99 | function renderItem(item) { 100 | return ( 101 | {item.text} 102 | ); 103 | } 104 | 105 | class Example extends Component { 106 | static propTypes = { 107 | value: PropTypes.string.isRequired, 108 | onChange: PropTypes.func.isRequired 109 | }; 110 | 111 | renderItemsContainer = ({ children, containerProps }) => { 112 | const { value } = this.props; 113 | 114 | return renderItemsContainer({ 115 | children, 116 | containerProps, 117 | query: value.trim() 118 | }); 119 | }; 120 | 121 | render() { 122 | const { value, onChange } = this.props; 123 | const inputProps = { 124 | placeholder: 'Custom items container', 125 | value, 126 | onChange 127 | }; 128 | 129 | return ( 130 |
131 | 139 | 140 |
141 | ); 142 | } 143 | } 144 | 145 | export default connect(mapStateToProps, mapDispatchToProps)(Example); 146 | -------------------------------------------------------------------------------- /demo/src/components/App/components/Example11/Example11.less: -------------------------------------------------------------------------------- 1 | .footer { 2 | font-size: 12px; 3 | color: #aaa; 4 | padding: 10px 20px; 5 | } 6 | -------------------------------------------------------------------------------- /demo/src/components/App/components/Example12/Example12.js: -------------------------------------------------------------------------------- 1 | import styles from './Example12.less'; 2 | 3 | import React, { Component } from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { connect } from 'react-redux'; 6 | import { updateInputValue, updateHighlightedItem } from '../../redux'; 7 | import Autowhatever from 'Autowhatever'; 8 | import SourceCodeLink from 'SourceCodeLink/SourceCodeLink'; 9 | import IsolatedScroll from 'react-isolated-scroll'; 10 | 11 | const theme = { 12 | container: { 13 | position: 'relative' 14 | }, 15 | input: { 16 | width: '240px', 17 | height: '30px', 18 | padding: '10px 20px', 19 | fontSize: '16px', 20 | fontFamily: 'Helvetica, Arial, sans-serif', 21 | border: '1px solid #aaa', 22 | borderRadius: '4px', 23 | boxSizing: 'content-box' 24 | }, 25 | inputOpen: { 26 | borderBottomLeftRadius: 0, 27 | borderBottomRightRadius: 0 28 | }, 29 | inputFocused: { 30 | outline: 'none' 31 | }, 32 | itemsContainer: { 33 | display: 'none' 34 | }, 35 | itemsContainerOpen: { 36 | display: 'block', 37 | position: 'relative', 38 | top: '-1px', 39 | width: '280px', 40 | border: '1px solid #aaa', 41 | backgroundColor: '#fff', 42 | fontSize: '16px', 43 | lineHeight: 1.25, 44 | borderBottomLeftRadius: '4px', 45 | borderBottomRightRadius: '4px', 46 | zIndex: 2 47 | }, 48 | itemsList: { 49 | margin: 0, 50 | padding: 0, 51 | listStyleType: 'none' 52 | }, 53 | item: { 54 | cursor: 'pointer', 55 | padding: '10px 20px' 56 | }, 57 | itemHighlighted: { 58 | backgroundColor: '#ddd' 59 | } 60 | }; 61 | 62 | const exampleId = '12'; 63 | const file = `demo/src/components/App/components/Example${exampleId}/Example${exampleId}.js`; 64 | 65 | const renderItemsContainer = ({ children, containerProps, query }) => { 66 | const { ref, ...restContainerProps } = containerProps; 67 | const callRef = isolatedScroll => { 68 | if (isolatedScroll !== null) { 69 | ref(isolatedScroll.component); 70 | } 71 | }; 72 | 73 | return ( 74 |
75 |
76 | Suggestions 77 |
78 | 79 | {children} 80 | 81 |
82 | { 83 | query 84 | ? Press Enter to search {query} 85 | : Powered by react-autowhatever 86 | } 87 |
88 |
89 | ); 90 | }; 91 | 92 | const mapStateToProps = state => ({ 93 | value: state[exampleId].value, 94 | highlightedSectionIndex: state[exampleId].highlightedSectionIndex, 95 | highlightedItemIndex: state[exampleId].highlightedItemIndex 96 | }); 97 | 98 | const mapDispatchToProps = dispatch => ({ 99 | onChange: event => { 100 | dispatch(updateInputValue(exampleId, event.target.value)); 101 | }, 102 | onKeyDown: (event, { newHighlightedSectionIndex, newHighlightedItemIndex }) => { 103 | event.preventDefault(); // Don't move the cursor to start/end 104 | 105 | if (typeof newHighlightedItemIndex !== 'undefined') { 106 | dispatch(updateHighlightedItem(exampleId, newHighlightedSectionIndex, newHighlightedItemIndex)); 107 | } 108 | } 109 | }); 110 | 111 | const items = [{ 112 | text: 'Apple' 113 | }, { 114 | text: 'Banana' 115 | }, { 116 | text: 'Cherry' 117 | }, { 118 | text: 'Grapefruit' 119 | }, { 120 | text: 'Lemon' 121 | }]; 122 | 123 | function renderItem(item) { 124 | return ( 125 | {item.text} 126 | ); 127 | } 128 | 129 | class Example extends Component { 130 | static propTypes = { 131 | value: PropTypes.string.isRequired, 132 | highlightedSectionIndex: PropTypes.number, 133 | highlightedItemIndex: PropTypes.number, 134 | onChange: PropTypes.func.isRequired, 135 | onKeyDown: PropTypes.func.isRequired 136 | }; 137 | 138 | renderItemsContainer = ({ children, containerProps }) => { 139 | const { value } = this.props; 140 | 141 | return renderItemsContainer({ 142 | children, 143 | containerProps, 144 | query: value.trim() 145 | }); 146 | }; 147 | 148 | render() { 149 | const { value, highlightedSectionIndex, highlightedItemIndex, onChange, onKeyDown } = this.props; 150 | const inputProps = { 151 | placeholder: 'Custom scrollable items container', 152 | value, 153 | onChange, 154 | onKeyDown 155 | }; 156 | 157 | return ( 158 |
159 | 169 | 170 |
171 | ); 172 | } 173 | } 174 | 175 | export default connect(mapStateToProps, mapDispatchToProps)(Example); 176 | -------------------------------------------------------------------------------- /demo/src/components/App/components/Example12/Example12.less: -------------------------------------------------------------------------------- 1 | .header { 2 | font-size: 12px; 3 | color: #aaa; 4 | padding: 10px 20px; 5 | } 6 | 7 | .suggestions { 8 | max-height: 140px; 9 | overflow-y: auto; 10 | } 11 | 12 | .footer { 13 | font-size: 12px; 14 | color: #aaa; 15 | padding: 10px 20px; 16 | } 17 | -------------------------------------------------------------------------------- /demo/src/components/App/components/Example2/Example2.js: -------------------------------------------------------------------------------- 1 | import theme from '../theme.less'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { connect } from 'react-redux'; 6 | import { updateInputValue } from '../../redux'; 7 | import Autowhatever from 'Autowhatever'; 8 | import SourceCodeLink from 'SourceCodeLink/SourceCodeLink'; 9 | 10 | const exampleId = '2'; 11 | const file = `demo/src/components/App/components/Example${exampleId}/Example${exampleId}.js`; 12 | 13 | function mapStateToProps(state) { 14 | return { 15 | value: state[exampleId].value 16 | }; 17 | } 18 | 19 | function mapDispatchToProps(dispatch) { 20 | return { 21 | onChange: event => dispatch(updateInputValue(exampleId, event.target.value)) 22 | }; 23 | } 24 | 25 | const items = [{ 26 | text: 'Apple' 27 | }, { 28 | text: 'Banana' 29 | }, { 30 | text: 'Cherry' 31 | }, { 32 | text: 'Grapefruit' 33 | }, { 34 | text: 'Lemon' 35 | }]; 36 | 37 | function renderItem(item) { 38 | return ( 39 | {item.text} 40 | ); 41 | } 42 | 43 | function Example(props) { 44 | const { value, onChange } = props; 45 | const inputProps = { value, onChange }; 46 | 47 | return ( 48 |
49 | 57 | 58 |
59 | ); 60 | } 61 | 62 | Example.propTypes = { 63 | value: PropTypes.string.isRequired, 64 | onChange: PropTypes.func.isRequired 65 | }; 66 | 67 | export default connect(mapStateToProps, mapDispatchToProps)(Example); 68 | -------------------------------------------------------------------------------- /demo/src/components/App/components/Example3/Example3.js: -------------------------------------------------------------------------------- 1 | import theme from '../theme.less'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { connect } from 'react-redux'; 6 | import { updateInputValue } from '../../redux'; 7 | import Autowhatever from 'Autowhatever'; 8 | import SourceCodeLink from 'SourceCodeLink/SourceCodeLink'; 9 | 10 | const exampleId = '3'; 11 | const file = `demo/src/components/App/components/Example${exampleId}/Example${exampleId}.js`; 12 | 13 | function mapStateToProps(state) { 14 | return { 15 | value: state[exampleId].value 16 | }; 17 | } 18 | 19 | function mapDispatchToProps(dispatch) { 20 | return { 21 | onChange: event => dispatch(updateInputValue(exampleId, event.target.value)) 22 | }; 23 | } 24 | 25 | const items = [{ 26 | title: 'A', 27 | items: [{ 28 | text: 'Apple' 29 | }, { 30 | text: 'Apricot' 31 | }] 32 | }, { 33 | title: 'B', 34 | items: [{ 35 | text: 'Banana' 36 | }] 37 | }, { 38 | title: 'C', 39 | items: [{ 40 | text: 'Cherry' 41 | }] 42 | }]; 43 | 44 | function renderSectionTitle(section) { 45 | return ( 46 | {section.title} 47 | ); 48 | } 49 | 50 | function getSectionItems(section) { 51 | return section.items; 52 | } 53 | 54 | function renderItem(item) { 55 | return ( 56 | {item.text} 57 | ); 58 | } 59 | 60 | function Example(props) { 61 | const { value, onChange } = props; 62 | const inputProps = { value, onChange }; 63 | 64 | return ( 65 |
66 | 76 | 77 |
78 | ); 79 | } 80 | 81 | Example.propTypes = { 82 | value: PropTypes.string.isRequired, 83 | onChange: PropTypes.func.isRequired 84 | }; 85 | 86 | export default connect(mapStateToProps, mapDispatchToProps)(Example); 87 | -------------------------------------------------------------------------------- /demo/src/components/App/components/Example4/Example4.js: -------------------------------------------------------------------------------- 1 | import theme from '../theme.less'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { connect } from 'react-redux'; 6 | import { updateInputValue } from '../../redux'; 7 | import Autowhatever from 'Autowhatever'; 8 | import SourceCodeLink from 'SourceCodeLink/SourceCodeLink'; 9 | 10 | const exampleId = '4'; 11 | const file = `demo/src/components/App/components/Example${exampleId}/Example${exampleId}.js`; 12 | 13 | function mapStateToProps(state) { 14 | return { 15 | value: state[exampleId].value 16 | }; 17 | } 18 | 19 | function mapDispatchToProps(dispatch) { 20 | return { 21 | onChange: event => dispatch(updateInputValue(exampleId, event.target.value)) 22 | }; 23 | } 24 | 25 | const items = [{ 26 | title: 'A', 27 | items: [{ 28 | text: 'Apple' 29 | }, { 30 | text: 'Apricot' 31 | }] 32 | }, { 33 | title: 'B', 34 | items: [{ 35 | text: 'Banana' 36 | }] 37 | }, { 38 | title: 'C', 39 | items: [{ 40 | text: 'Cherry' 41 | }] 42 | }]; 43 | 44 | function renderSectionTitle(section) { 45 | return ( 46 | {section.title} 47 | ); 48 | } 49 | 50 | function getSectionItems(section) { 51 | return section.items; 52 | } 53 | 54 | function renderItem(item) { 55 | return ( 56 | {item.text} 57 | ); 58 | } 59 | 60 | function Example(props) { 61 | const { value, onChange } = props; 62 | const inputProps = { value, onChange }; 63 | 64 | return ( 65 |
66 | 78 | 79 |
80 | ); 81 | } 82 | 83 | Example.propTypes = { 84 | value: PropTypes.string.isRequired, 85 | onChange: PropTypes.func.isRequired 86 | }; 87 | 88 | export default connect(mapStateToProps, mapDispatchToProps)(Example); 89 | -------------------------------------------------------------------------------- /demo/src/components/App/components/Example5/Example5.js: -------------------------------------------------------------------------------- 1 | import theme from '../theme.less'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { connect } from 'react-redux'; 6 | import { updateInputValue, updateHighlightedItem } from '../../redux'; 7 | import Autowhatever from 'Autowhatever'; 8 | import SourceCodeLink from 'SourceCodeLink/SourceCodeLink'; 9 | 10 | const exampleId = '5'; 11 | const file = `demo/src/components/App/components/Example${exampleId}/Example${exampleId}.js`; 12 | 13 | const items = [{ 14 | text: 'Apple' 15 | }, { 16 | text: 'Banana' 17 | }, { 18 | text: 'Cherry' 19 | }, { 20 | text: 'Grapefruit' 21 | }, { 22 | text: 'Lemon' 23 | }]; 24 | 25 | function mapStateToProps(state) { 26 | return { 27 | value: state[exampleId].value, 28 | highlightedSectionIndex: state[exampleId].highlightedSectionIndex, 29 | highlightedItemIndex: state[exampleId].highlightedItemIndex 30 | }; 31 | } 32 | 33 | function mapDispatchToProps(dispatch) { 34 | return { 35 | onChange: event => { 36 | dispatch(updateInputValue(exampleId, event.target.value)); 37 | }, 38 | onMouseEnter: (event, { sectionIndex, itemIndex }) => { 39 | dispatch(updateHighlightedItem(exampleId, sectionIndex, itemIndex)); 40 | }, 41 | onMouseLeave: () => { 42 | dispatch(updateHighlightedItem(exampleId, null, null)); 43 | }, 44 | onMouseDown: (event, { itemIndex }) => { 45 | dispatch(updateInputValue(exampleId, items[itemIndex].text + ' clicked')); 46 | } 47 | }; 48 | } 49 | 50 | function renderItem(item) { 51 | return ( 52 | {item.text} 53 | ); 54 | } 55 | 56 | function Example(props) { 57 | const { value, highlightedSectionIndex, highlightedItemIndex, onChange, 58 | onMouseEnter, onMouseLeave, onMouseDown } = props; 59 | const inputProps = { value, onChange }; 60 | const itemProps = { onMouseEnter, onMouseLeave, onMouseDown }; 61 | 62 | return ( 63 |
64 | 74 | 75 |
76 | ); 77 | } 78 | 79 | Example.propTypes = { 80 | value: PropTypes.string.isRequired, 81 | highlightedSectionIndex: PropTypes.number, 82 | highlightedItemIndex: PropTypes.number, 83 | 84 | onChange: PropTypes.func.isRequired, 85 | onMouseEnter: PropTypes.func.isRequired, 86 | onMouseLeave: PropTypes.func.isRequired, 87 | onMouseDown: PropTypes.func.isRequired 88 | }; 89 | 90 | export default connect(mapStateToProps, mapDispatchToProps)(Example); 91 | -------------------------------------------------------------------------------- /demo/src/components/App/components/Example6/Example6.js: -------------------------------------------------------------------------------- 1 | import theme from '../theme.less'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { connect } from 'react-redux'; 6 | import { updateInputValue, updateHighlightedItem } from '../../redux'; 7 | import Autowhatever from 'Autowhatever'; 8 | import SourceCodeLink from 'SourceCodeLink/SourceCodeLink'; 9 | 10 | const exampleId = '6'; 11 | const file = `demo/src/components/App/components/Example${exampleId}/Example${exampleId}.js`; 12 | 13 | const items = [{ 14 | text: 'Apple' 15 | }, { 16 | text: 'Banana' 17 | }, { 18 | text: 'Cherry' 19 | }, { 20 | text: 'Grapefruit' 21 | }, { 22 | text: 'Lemon' 23 | }]; 24 | 25 | function mapStateToProps(state) { 26 | return { 27 | value: state[exampleId].value, 28 | highlightedSectionIndex: state[exampleId].highlightedSectionIndex, 29 | highlightedItemIndex: state[exampleId].highlightedItemIndex 30 | }; 31 | } 32 | 33 | function mapDispatchToProps(dispatch) { 34 | return { 35 | onChange: event => { 36 | dispatch(updateInputValue(exampleId, event.target.value)); 37 | }, 38 | onKeyDown: (event, { newHighlightedSectionIndex, newHighlightedItemIndex }) => { 39 | event.preventDefault(); // Don't move the cursor to start/end 40 | 41 | if (typeof newHighlightedItemIndex !== 'undefined') { 42 | dispatch(updateHighlightedItem(exampleId, newHighlightedSectionIndex, newHighlightedItemIndex)); 43 | } 44 | } 45 | }; 46 | } 47 | 48 | function renderItem(item) { 49 | return ( 50 | {item.text} 51 | ); 52 | } 53 | 54 | function Example(props) { 55 | const { value, highlightedSectionIndex, highlightedItemIndex, onChange, onKeyDown } = props; 56 | const inputProps = { value, onChange, onKeyDown }; 57 | 58 | return ( 59 |
60 | 69 | 70 |
71 | ); 72 | } 73 | 74 | Example.propTypes = { 75 | value: PropTypes.string.isRequired, 76 | highlightedSectionIndex: PropTypes.number, 77 | highlightedItemIndex: PropTypes.number, 78 | 79 | onChange: PropTypes.func.isRequired, 80 | onKeyDown: PropTypes.func.isRequired 81 | }; 82 | 83 | export default connect(mapStateToProps, mapDispatchToProps)(Example); 84 | -------------------------------------------------------------------------------- /demo/src/components/App/components/Example7/Example7.js: -------------------------------------------------------------------------------- 1 | import theme from '../theme.less'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { connect } from 'react-redux'; 6 | import { updateInputValue, updateHighlightedItem } from '../../redux'; 7 | import Autowhatever from 'Autowhatever'; 8 | import SourceCodeLink from 'SourceCodeLink/SourceCodeLink'; 9 | import IsolatedScroll from 'react-isolated-scroll'; 10 | 11 | const exampleId = '7'; 12 | const file = `demo/src/components/App/components/Example${exampleId}/Example${exampleId}.js`; 13 | 14 | const items = [{ 15 | text: 'Apple cake' 16 | }, { 17 | text: 'Apple pie' 18 | }, { 19 | text: 'Banana cake' 20 | }, { 21 | text: 'Banana pie' 22 | }, { 23 | text: 'Cherry cake' 24 | }, { 25 | text: 'Cherry pie' 26 | }, { 27 | text: 'Grapefruit cake' 28 | }, { 29 | text: 'Grapefruit pie' 30 | }, { 31 | text: 'Lemon cake' 32 | }, { 33 | text: 'Lemon pie' 34 | }]; 35 | 36 | function mapStateToProps(state) { 37 | return { 38 | value: state[exampleId].value, 39 | highlightedSectionIndex: state[exampleId].highlightedSectionIndex, 40 | highlightedItemIndex: state[exampleId].highlightedItemIndex 41 | }; 42 | } 43 | 44 | function mapDispatchToProps(dispatch) { 45 | return { 46 | onChange: event => { 47 | dispatch(updateInputValue(exampleId, event.target.value)); 48 | }, 49 | onKeyDown: (event, { newHighlightedSectionIndex, newHighlightedItemIndex }) => { 50 | event.preventDefault(); // Don't move the cursor to start/end 51 | 52 | if (typeof newHighlightedItemIndex !== 'undefined') { 53 | dispatch(updateHighlightedItem(exampleId, newHighlightedSectionIndex, newHighlightedItemIndex)); 54 | } 55 | } 56 | }; 57 | } 58 | 59 | function renderItemsContainer({ children, containerProps }) { 60 | const { ref, ...restContainerProps } = containerProps; 61 | const callRef = isolatedScroll => { 62 | if (isolatedScroll !== null) { 63 | ref(isolatedScroll.component); 64 | } 65 | }; 66 | 67 | return ( 68 | 69 | {children} 70 | 71 | ); 72 | } 73 | 74 | function renderItem(item) { 75 | return ( 76 | {item.text} 77 | ); 78 | } 79 | 80 | function Example(props) { 81 | const { value, highlightedSectionIndex, highlightedItemIndex, onChange, onKeyDown } = props; 82 | const inputProps = { value, onChange, onKeyDown }; 83 | 84 | return ( 85 |
86 | 96 | 97 |
98 | ); 99 | } 100 | 101 | Example.propTypes = { 102 | value: PropTypes.string.isRequired, 103 | highlightedSectionIndex: PropTypes.number, 104 | highlightedItemIndex: PropTypes.number, 105 | 106 | onChange: PropTypes.func.isRequired, 107 | onKeyDown: PropTypes.func.isRequired 108 | }; 109 | 110 | export default connect(mapStateToProps, mapDispatchToProps)(Example); 111 | -------------------------------------------------------------------------------- /demo/src/components/App/components/Example8/Example8.js: -------------------------------------------------------------------------------- 1 | import theme from '../theme.less'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { connect } from 'react-redux'; 6 | import { updateInputValue, updateHighlightedItem } from '../../redux'; 7 | import Autowhatever from 'Autowhatever'; 8 | import SourceCodeLink from 'SourceCodeLink/SourceCodeLink'; 9 | 10 | const exampleId = '8'; 11 | const file = `demo/src/components/App/components/Example${exampleId}/Example${exampleId}.js`; 12 | 13 | const items = [{ 14 | title: 'A', 15 | items: [{ 16 | text: 'Apple' 17 | }, { 18 | text: 'Apricot' 19 | }] 20 | }, { 21 | title: 'B', 22 | items: [{ 23 | text: 'Banana' 24 | }] 25 | }, { 26 | title: 'C', 27 | items: [{ 28 | text: 'Cherry' 29 | }] 30 | }]; 31 | 32 | function renderSectionTitle(section) { 33 | return ( 34 | {section.title} 35 | ); 36 | } 37 | 38 | function getSectionItems(section) { 39 | return section.items; 40 | } 41 | 42 | function renderItem(item) { 43 | return ( 44 | {item.text} 45 | ); 46 | } 47 | 48 | function mapStateToProps(state) { 49 | return { 50 | value: state[exampleId].value, 51 | highlightedSectionIndex: state[exampleId].highlightedSectionIndex, 52 | highlightedItemIndex: state[exampleId].highlightedItemIndex 53 | }; 54 | } 55 | 56 | function mapDispatchToProps(dispatch) { 57 | return { 58 | onChange: event => { 59 | dispatch(updateInputValue(exampleId, event.target.value)); 60 | }, 61 | onKeyDown: (event, { highlightedSectionIndex, highlightedItemIndex, newHighlightedSectionIndex, newHighlightedItemIndex }) => { 62 | switch (event.key) { 63 | case 'ArrowDown': 64 | case 'ArrowUp': 65 | event.preventDefault(); // Don't move the cursor to start/end 66 | dispatch(updateHighlightedItem(exampleId, newHighlightedSectionIndex, newHighlightedItemIndex)); 67 | break; 68 | 69 | case 'Enter': 70 | if (highlightedItemIndex !== null) { 71 | dispatch(updateInputValue(exampleId, items[highlightedSectionIndex].items[highlightedItemIndex].text + ' selected')); 72 | } 73 | break; 74 | } 75 | } 76 | }; 77 | } 78 | 79 | function Example(props) { 80 | const { value, highlightedSectionIndex, highlightedItemIndex, onChange, onKeyDown } = props; 81 | const inputProps = { value, onChange, onKeyDown }; 82 | 83 | return ( 84 |
85 | 97 | 98 |
99 | ); 100 | } 101 | 102 | Example.propTypes = { 103 | value: PropTypes.string.isRequired, 104 | highlightedSectionIndex: PropTypes.number, 105 | highlightedItemIndex: PropTypes.number, 106 | 107 | onChange: PropTypes.func.isRequired, 108 | onKeyDown: PropTypes.func.isRequired 109 | }; 110 | 111 | export default connect(mapStateToProps, mapDispatchToProps)(Example); 112 | -------------------------------------------------------------------------------- /demo/src/components/App/components/Example9/Example9.js: -------------------------------------------------------------------------------- 1 | import theme from '../theme.less'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { connect } from 'react-redux'; 6 | import match from 'autosuggest-highlight/match'; 7 | import parse from 'autosuggest-highlight/parse'; 8 | import { updateInputValue, hideItems, updateHighlightedItem } from '../../redux'; 9 | import Autowhatever from 'Autowhatever'; 10 | import SourceCodeLink from 'SourceCodeLink/SourceCodeLink'; 11 | import countries from './countries'; 12 | import { escapeRegexCharacters } from '../../utils'; 13 | import IsolatedScroll from 'react-isolated-scroll'; 14 | 15 | const exampleId = '9'; 16 | const file = `demo/src/components/App/components/Example${exampleId}/Example${exampleId}.js`; 17 | 18 | function getMatchingCountries(value) { 19 | const escapedValue = escapeRegexCharacters(value.trim()); 20 | 21 | if (escapedValue === '') { 22 | return []; 23 | } 24 | 25 | const regex = new RegExp('^' + escapedValue, 'i'); 26 | 27 | return countries.filter(country => regex.test(country.name)); 28 | } 29 | 30 | function findItemElement(startNode) { 31 | let node = startNode; 32 | 33 | do { 34 | if (node.getAttribute('data-item-index') !== null) { 35 | return node; 36 | } 37 | 38 | node = node.parentNode; 39 | } while (node !== null); 40 | 41 | console.error('Clicked item:', startNode); // eslint-disable-line no-console 42 | throw new Error('Couldn\'t find the clicked item element'); 43 | } 44 | 45 | function mapStateToProps(state) { 46 | return { 47 | value: state[exampleId].value, 48 | highlightedSectionIndex: state[exampleId].highlightedSectionIndex, 49 | highlightedItemIndex: state[exampleId].highlightedItemIndex, 50 | items: state[exampleId].items 51 | }; 52 | } 53 | 54 | function mapDispatchToProps(dispatch) { 55 | return { 56 | onChange: event => { 57 | const newValue = event.target.value; 58 | const newItems = getMatchingCountries(newValue); 59 | 60 | dispatch(updateInputValue(exampleId, newValue, newItems)); 61 | }, 62 | onFocus: () => { 63 | dispatch(updateInputValue(exampleId, '')); 64 | }, 65 | onBlur: () => { 66 | dispatch(hideItems(exampleId)); 67 | }, 68 | onMouseEnter: (event, { sectionIndex, itemIndex }) => { 69 | dispatch(updateHighlightedItem(exampleId, sectionIndex, itemIndex)); 70 | }, 71 | onMouseLeave: () => { 72 | dispatch(updateHighlightedItem(exampleId, null, null)); 73 | }, 74 | onMouseDown: clickedItem => { 75 | dispatch(updateInputValue(exampleId, clickedItem.name)); 76 | } 77 | }; 78 | } 79 | 80 | function renderItemsContainer({ children, containerProps }) { 81 | const { ref, ...restContainerProps } = containerProps; 82 | const callRef = isolatedScroll => { 83 | if (isolatedScroll !== null) { 84 | ref(isolatedScroll.component); 85 | } 86 | }; 87 | 88 | return ( 89 | 90 | {children} 91 | 92 | ); 93 | } 94 | 95 | function renderItem(country, { value }) { 96 | const matches = match(country.name, value.trim()); 97 | const parts = parse(country.name, matches); 98 | 99 | return ( 100 | 101 | { 102 | parts.map((part, index) => { 103 | const className = part.highlight ? theme.highlight : null; 104 | 105 | return ( 106 | {part.text} 107 | ); 108 | }) 109 | } 110 | 111 | ); 112 | } 113 | 114 | function Example(props) { 115 | const { 116 | value, highlightedSectionIndex, highlightedItemIndex, items, 117 | onChange, onFocus, onBlur, onMouseEnter, onMouseLeave, onMouseDown 118 | } = props; 119 | const inputProps = { 120 | placeholder: 'Search and click countries', 121 | value, 122 | onChange, 123 | onFocus, 124 | onBlur 125 | }; 126 | const itemProps = ({ itemIndex }) => ({ 127 | 'data-item-index': itemIndex, 128 | onMouseEnter, 129 | onMouseLeave, 130 | onMouseDown: event => { 131 | const clickedItem = findItemElement(event.target); 132 | const clickedItemIndex = clickedItem.getAttribute('data-item-index'); 133 | 134 | onMouseDown(items[clickedItemIndex]); 135 | } 136 | }); 137 | 138 | return ( 139 |
140 | 152 | 153 |
154 | ); 155 | } 156 | 157 | Example.propTypes = { 158 | value: PropTypes.string.isRequired, 159 | highlightedSectionIndex: PropTypes.number, 160 | highlightedItemIndex: PropTypes.number, 161 | items: PropTypes.array.isRequired, 162 | 163 | onChange: PropTypes.func.isRequired, 164 | onFocus: PropTypes.func.isRequired, 165 | onBlur: PropTypes.func.isRequired, 166 | onMouseEnter: PropTypes.func.isRequired, 167 | onMouseLeave: PropTypes.func.isRequired, 168 | onMouseDown: PropTypes.func.isRequired 169 | }; 170 | 171 | export default connect(mapStateToProps, mapDispatchToProps)(Example); 172 | -------------------------------------------------------------------------------- /demo/src/components/App/components/Example9/countries.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { name: 'Afghanistan', code: 'AF' }, 3 | { name: 'Åland Islands', code: 'AX' }, 4 | { name: 'Albania', code: 'AL' }, 5 | { name: 'Algeria', code: 'DZ' }, 6 | { name: 'American Samoa', code: 'AS' }, 7 | { name: 'AndorrA', code: 'AD' }, 8 | { name: 'Angola', code: 'AO' }, 9 | { name: 'Anguilla', code: 'AI' }, 10 | { name: 'Antarctica', code: 'AQ' }, 11 | { name: 'Antigua and Barbuda', code: 'AG' }, 12 | { name: 'Argentina', code: 'AR' }, 13 | { name: 'Armenia', code: 'AM' }, 14 | { name: 'Aruba', code: 'AW' }, 15 | { name: 'Australia', code: 'AU' }, 16 | { name: 'Austria', code: 'AT' }, 17 | { name: 'Azerbaijan', code: 'AZ' }, 18 | { name: 'Bahamas', code: 'BS' }, 19 | { name: 'Bahrain', code: 'BH' }, 20 | { name: 'Bangladesh', code: 'BD' }, 21 | { name: 'Barbados', code: 'BB' }, 22 | { name: 'Belarus', code: 'BY' }, 23 | { name: 'Belgium', code: 'BE' }, 24 | { name: 'Belize', code: 'BZ' }, 25 | { name: 'Benin', code: 'BJ' }, 26 | { name: 'Bermuda', code: 'BM' }, 27 | { name: 'Bhutan', code: 'BT' }, 28 | { name: 'Bolivia', code: 'BO' }, 29 | { name: 'Bosnia and Herzegovina', code: 'BA' }, 30 | { name: 'Botswana', code: 'BW' }, 31 | { name: 'Bouvet Island', code: 'BV' }, 32 | { name: 'Brazil', code: 'BR' }, 33 | { name: 'British Indian Ocean Territory', code: 'IO' }, 34 | { name: 'Brunei Darussalam', code: 'BN' }, 35 | { name: 'Bulgaria', code: 'BG' }, 36 | { name: 'Burkina Faso', code: 'BF' }, 37 | { name: 'Burundi', code: 'BI' }, 38 | { name: 'Cambodia', code: 'KH' }, 39 | { name: 'Cameroon', code: 'CM' }, 40 | { name: 'Canada', code: 'CA' }, 41 | { name: 'Cape Verde', code: 'CV' }, 42 | { name: 'Cayman Islands', code: 'KY' }, 43 | { name: 'Central African Republic', code: 'CF' }, 44 | { name: 'Chad', code: 'TD' }, 45 | { name: 'Chile', code: 'CL' }, 46 | { name: 'China', code: 'CN' }, 47 | { name: 'Christmas Island', code: 'CX' }, 48 | { name: 'Cocos (Keeling) Islands', code: 'CC' }, 49 | { name: 'Colombia', code: 'CO' }, 50 | { name: 'Comoros', code: 'KM' }, 51 | { name: 'Congo', code: 'CG' }, 52 | { name: 'Congo, The Democratic Republic of the', code: 'CD' }, 53 | { name: 'Cook Islands', code: 'CK' }, 54 | { name: 'Costa Rica', code: 'CR' }, 55 | { name: 'Cote D\'Ivoire', code: 'CI' }, 56 | { name: 'Croatia', code: 'HR' }, 57 | { name: 'Cuba', code: 'CU' }, 58 | { name: 'Cyprus', code: 'CY' }, 59 | { name: 'Czech Republic', code: 'CZ' }, 60 | { name: 'Denmark', code: 'DK' }, 61 | { name: 'Djibouti', code: 'DJ' }, 62 | { name: 'Dominica', code: 'DM' }, 63 | { name: 'Dominican Republic', code: 'DO' }, 64 | { name: 'Ecuador', code: 'EC' }, 65 | { name: 'Egypt', code: 'EG' }, 66 | { name: 'El Salvador', code: 'SV' }, 67 | { name: 'Equatorial Guinea', code: 'GQ' }, 68 | { name: 'Eritrea', code: 'ER' }, 69 | { name: 'Estonia', code: 'EE' }, 70 | { name: 'Ethiopia', code: 'ET' }, 71 | { name: 'Falkland Islands (Malvinas)', code: 'FK' }, 72 | { name: 'Faroe Islands', code: 'FO' }, 73 | { name: 'Fiji', code: 'FJ' }, 74 | { name: 'Finland', code: 'FI' }, 75 | { name: 'France', code: 'FR' }, 76 | { name: 'French Guiana', code: 'GF' }, 77 | { name: 'French Polynesia', code: 'PF' }, 78 | { name: 'French Southern Territories', code: 'TF' }, 79 | { name: 'Gabon', code: 'GA' }, 80 | { name: 'Gambia', code: 'GM' }, 81 | { name: 'Georgia', code: 'GE' }, 82 | { name: 'Germany', code: 'DE' }, 83 | { name: 'Ghana', code: 'GH' }, 84 | { name: 'Gibraltar', code: 'GI' }, 85 | { name: 'Greece', code: 'GR' }, 86 | { name: 'Greenland', code: 'GL' }, 87 | { name: 'Grenada', code: 'GD' }, 88 | { name: 'Guadeloupe', code: 'GP' }, 89 | { name: 'Guam', code: 'GU' }, 90 | { name: 'Guatemala', code: 'GT' }, 91 | { name: 'Guernsey', code: 'GG' }, 92 | { name: 'Guinea', code: 'GN' }, 93 | { name: 'Guinea-Bissau', code: 'GW' }, 94 | { name: 'Guyana', code: 'GY' }, 95 | { name: 'Haiti', code: 'HT' }, 96 | { name: 'Heard Island and Mcdonald Islands', code: 'HM' }, 97 | { name: 'Holy See (Vatican City State)', code: 'VA' }, 98 | { name: 'Honduras', code: 'HN' }, 99 | { name: 'Hong Kong', code: 'HK' }, 100 | { name: 'Hungary', code: 'HU' }, 101 | { name: 'Iceland', code: 'IS' }, 102 | { name: 'India', code: 'IN' }, 103 | { name: 'Indonesia', code: 'ID' }, 104 | { name: 'Iran, Islamic Republic Of', code: 'IR' }, 105 | { name: 'Iraq', code: 'IQ' }, 106 | { name: 'Ireland', code: 'IE' }, 107 | { name: 'Isle of Man', code: 'IM' }, 108 | { name: 'Israel', code: 'IL' }, 109 | { name: 'Italy', code: 'IT' }, 110 | { name: 'Jamaica', code: 'JM' }, 111 | { name: 'Japan', code: 'JP' }, 112 | { name: 'Jersey', code: 'JE' }, 113 | { name: 'Jordan', code: 'JO' }, 114 | { name: 'Kazakhstan', code: 'KZ' }, 115 | { name: 'Kenya', code: 'KE' }, 116 | { name: 'Kiribati', code: 'KI' }, 117 | { name: 'Korea, Democratic People\'S Republic of', code: 'KP' }, 118 | { name: 'Korea, Republic of', code: 'KR' }, 119 | { name: 'Kuwait', code: 'KW' }, 120 | { name: 'Kyrgyzstan', code: 'KG' }, 121 | { name: 'Lao People\'S Democratic Republic', code: 'LA' }, 122 | { name: 'Latvia', code: 'LV' }, 123 | { name: 'Lebanon', code: 'LB' }, 124 | { name: 'Lesotho', code: 'LS' }, 125 | { name: 'Liberia', code: 'LR' }, 126 | { name: 'Libyan Arab Jamahiriya', code: 'LY' }, 127 | { name: 'Liechtenstein', code: 'LI' }, 128 | { name: 'Lithuania', code: 'LT' }, 129 | { name: 'Luxembourg', code: 'LU' }, 130 | { name: 'Macao', code: 'MO' }, 131 | { name: 'Macedonia, The Former Yugoslav Republic of', code: 'MK' }, 132 | { name: 'Madagascar', code: 'MG' }, 133 | { name: 'Malawi', code: 'MW' }, 134 | { name: 'Malaysia', code: 'MY' }, 135 | { name: 'Maldives', code: 'MV' }, 136 | { name: 'Mali', code: 'ML' }, 137 | { name: 'Malta', code: 'MT' }, 138 | { name: 'Marshall Islands', code: 'MH' }, 139 | { name: 'Martinique', code: 'MQ' }, 140 | { name: 'Mauritania', code: 'MR' }, 141 | { name: 'Mauritius', code: 'MU' }, 142 | { name: 'Mayotte', code: 'YT' }, 143 | { name: 'Mexico', code: 'MX' }, 144 | { name: 'Micronesia, Federated States of', code: 'FM' }, 145 | { name: 'Moldova, Republic of', code: 'MD' }, 146 | { name: 'Monaco', code: 'MC' }, 147 | { name: 'Mongolia', code: 'MN' }, 148 | { name: 'Montserrat', code: 'MS' }, 149 | { name: 'Morocco', code: 'MA' }, 150 | { name: 'Mozambique', code: 'MZ' }, 151 | { name: 'Myanmar', code: 'MM' }, 152 | { name: 'Namibia', code: 'NA' }, 153 | { name: 'Nauru', code: 'NR' }, 154 | { name: 'Nepal', code: 'NP' }, 155 | { name: 'Netherlands', code: 'NL' }, 156 | { name: 'Netherlands Antilles', code: 'AN' }, 157 | { name: 'New Caledonia', code: 'NC' }, 158 | { name: 'New Zealand', code: 'NZ' }, 159 | { name: 'Nicaragua', code: 'NI' }, 160 | { name: 'Niger', code: 'NE' }, 161 | { name: 'Nigeria', code: 'NG' }, 162 | { name: 'Niue', code: 'NU' }, 163 | { name: 'Norfolk Island', code: 'NF' }, 164 | { name: 'Northern Mariana Islands', code: 'MP' }, 165 | { name: 'Norway', code: 'NO' }, 166 | { name: 'Oman', code: 'OM' }, 167 | { name: 'Pakistan', code: 'PK' }, 168 | { name: 'Palau', code: 'PW' }, 169 | { name: 'Palestinian Territory, Occupied', code: 'PS' }, 170 | { name: 'Panama', code: 'PA' }, 171 | { name: 'Papua New Guinea', code: 'PG' }, 172 | { name: 'Paraguay', code: 'PY' }, 173 | { name: 'Peru', code: 'PE' }, 174 | { name: 'Philippines', code: 'PH' }, 175 | { name: 'Pitcairn', code: 'PN' }, 176 | { name: 'Poland', code: 'PL' }, 177 | { name: 'Portugal', code: 'PT' }, 178 | { name: 'Puerto Rico', code: 'PR' }, 179 | { name: 'Qatar', code: 'QA' }, 180 | { name: 'Reunion', code: 'RE' }, 181 | { name: 'Romania', code: 'RO' }, 182 | { name: 'Russian Federation', code: 'RU' }, 183 | { name: 'RWANDA', code: 'RW' }, 184 | { name: 'Saint Helena', code: 'SH' }, 185 | { name: 'Saint Kitts and Nevis', code: 'KN' }, 186 | { name: 'Saint Lucia', code: 'LC' }, 187 | { name: 'Saint Pierre and Miquelon', code: 'PM' }, 188 | { name: 'Saint Vincent and the Grenadines', code: 'VC' }, 189 | { name: 'Samoa', code: 'WS' }, 190 | { name: 'San Marino', code: 'SM' }, 191 | { name: 'Sao Tome and Principe', code: 'ST' }, 192 | { name: 'Saudi Arabia', code: 'SA' }, 193 | { name: 'Senegal', code: 'SN' }, 194 | { name: 'Serbia and Montenegro', code: 'CS' }, 195 | { name: 'Seychelles', code: 'SC' }, 196 | { name: 'Sierra Leone', code: 'SL' }, 197 | { name: 'Singapore', code: 'SG' }, 198 | { name: 'Slovakia', code: 'SK' }, 199 | { name: 'Slovenia', code: 'SI' }, 200 | { name: 'Solomon Islands', code: 'SB' }, 201 | { name: 'Somalia', code: 'SO' }, 202 | { name: 'South Africa', code: 'ZA' }, 203 | { name: 'South Georgia and the South Sandwich Islands', code: 'GS' }, 204 | { name: 'Spain', code: 'ES' }, 205 | { name: 'Sri Lanka', code: 'LK' }, 206 | { name: 'Sudan', code: 'SD' }, 207 | { name: 'Suriname', code: 'SR' }, 208 | { name: 'Svalbard and Jan Mayen', code: 'SJ' }, 209 | { name: 'Swaziland', code: 'SZ' }, 210 | { name: 'Sweden', code: 'SE' }, 211 | { name: 'Switzerland', code: 'CH' }, 212 | { name: 'Syrian Arab Republic', code: 'SY' }, 213 | { name: 'Taiwan, Province of China', code: 'TW' }, 214 | { name: 'Tajikistan', code: 'TJ' }, 215 | { name: 'Tanzania, United Republic of', code: 'TZ' }, 216 | { name: 'Thailand', code: 'TH' }, 217 | { name: 'Timor-Leste', code: 'TL' }, 218 | { name: 'Togo', code: 'TG' }, 219 | { name: 'Tokelau', code: 'TK' }, 220 | { name: 'Tonga', code: 'TO' }, 221 | { name: 'Trinidad and Tobago', code: 'TT' }, 222 | { name: 'Tunisia', code: 'TN' }, 223 | { name: 'Turkey', code: 'TR' }, 224 | { name: 'Turkmenistan', code: 'TM' }, 225 | { name: 'Turks and Caicos Islands', code: 'TC' }, 226 | { name: 'Tuvalu', code: 'TV' }, 227 | { name: 'Uganda', code: 'UG' }, 228 | { name: 'Ukraine', code: 'UA' }, 229 | { name: 'United Arab Emirates', code: 'AE' }, 230 | { name: 'United Kingdom', code: 'GB' }, 231 | { name: 'United States', code: 'US' }, 232 | { name: 'United States Minor Outlying Islands', code: 'UM' }, 233 | { name: 'Uruguay', code: 'UY' }, 234 | { name: 'Uzbekistan', code: 'UZ' }, 235 | { name: 'Vanuatu', code: 'VU' }, 236 | { name: 'Venezuela', code: 'VE' }, 237 | { name: 'Viet Nam', code: 'VN' }, 238 | { name: 'Virgin Islands, British', code: 'VG' }, 239 | { name: 'Virgin Islands, U.S.', code: 'VI' }, 240 | { name: 'Wallis and Futuna', code: 'WF' }, 241 | { name: 'Western Sahara', code: 'EH' }, 242 | { name: 'Yemen', code: 'YE' }, 243 | { name: 'Zambia', code: 'ZM' }, 244 | { name: 'Zimbabwe', code: 'ZW' } 245 | ]; 246 | -------------------------------------------------------------------------------- /demo/src/components/App/components/ForkMeOnGitHub/ForkMeOnGitHub.js: -------------------------------------------------------------------------------- 1 | import styles from './ForkMeOnGitHub.less'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | 6 | export default function ForkMeOnGitHub(props) { 7 | const { user, repo } = props; 8 | 9 | return ( 10 | 11 | Fork me on GitHub 17 | 18 | ); 19 | } 20 | 21 | ForkMeOnGitHub.propTypes = { 22 | user: PropTypes.string.isRequired, 23 | repo: PropTypes.string.isRequired 24 | }; 25 | -------------------------------------------------------------------------------- /demo/src/components/App/components/ForkMeOnGitHub/ForkMeOnGitHub.less: -------------------------------------------------------------------------------- 1 | .image { 2 | position: absolute; 3 | top: 0; 4 | right: 0; 5 | border: 0; 6 | } 7 | -------------------------------------------------------------------------------- /demo/src/components/App/components/SourceCodeLink/SourceCodeLink.js: -------------------------------------------------------------------------------- 1 | import styles from './SourceCodeLink.less'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | 6 | export default function SourceCodeLink(props) { 7 | const { file } = props; 8 | 9 | return ( 10 | 16 | Source code 17 | 18 | ); 19 | } 20 | 21 | SourceCodeLink.propTypes = { 22 | file: PropTypes.string.isRequired 23 | }; 24 | -------------------------------------------------------------------------------- /demo/src/components/App/components/SourceCodeLink/SourceCodeLink.less: -------------------------------------------------------------------------------- 1 | .link { 2 | float: right; 3 | text-decoration: none; 4 | color: #2c7ea7; 5 | font-size: 12px; 6 | margin-top: 3px; 7 | margin-right: 5px; 8 | } 9 | -------------------------------------------------------------------------------- /demo/src/components/App/components/theme.less: -------------------------------------------------------------------------------- 1 | @border-color: #aaa; 2 | @border-radius: 4px; 3 | 4 | .container { 5 | position: relative; 6 | } 7 | 8 | .input { 9 | width: 240px; 10 | height: 30px; 11 | padding: 10px 20px; 12 | font-size: 16px; 13 | font-family: Helvetica, Arial, sans-serif; 14 | border: 1px solid @border-color; 15 | border-radius: @border-radius; 16 | box-sizing: content-box; 17 | } 18 | 19 | .inputOpen { 20 | border-bottom-left-radius: 0; 21 | border-bottom-right-radius: 0; 22 | } 23 | 24 | .inputFocused { 25 | outline: none; 26 | } 27 | 28 | .itemsContainer { 29 | display: none; 30 | } 31 | 32 | .itemsContainerOpen { 33 | display: block; 34 | position: relative; 35 | top: -1px; 36 | width: 280px; 37 | border: 1px solid @border-color; 38 | background-color: #fff; 39 | font-size: 16px; 40 | line-height: 1.25; 41 | border-bottom-left-radius: @border-radius; 42 | border-bottom-right-radius: @border-radius; 43 | z-index: 2; 44 | max-height: 260px; 45 | overflow-y: auto; 46 | } 47 | 48 | .itemsList { 49 | margin: 0; 50 | padding: 0; 51 | list-style-type: none; 52 | } 53 | 54 | .item { 55 | cursor: pointer; 56 | padding: 10px 20px; 57 | } 58 | 59 | .itemHighlighted { 60 | background-color: #ddd; 61 | } 62 | 63 | .sectionContainer { 64 | border-top: 1px dashed #ccc; 65 | } 66 | 67 | .sectionContainerFirst { 68 | border-top: 0; 69 | } 70 | 71 | .sectionTitle { 72 | padding: 10px 0 0 10px; 73 | font-size: 12px; 74 | color: #777; 75 | } 76 | 77 | .highlight { 78 | color: #ee0000; 79 | font-weight: 400; 80 | } 81 | -------------------------------------------------------------------------------- /demo/src/components/App/redux.js: -------------------------------------------------------------------------------- 1 | const UPDATE_INPUT_VALUE = 'UPDATE_INPUT_VALUE'; 2 | const HIDE_ITEMS = 'HIDE_ITEMS'; 3 | const UPDATE_HIGHLIGHTED_ITEM = 'UPDATE_HIGHLIGHTED_ITEM'; 4 | 5 | const initialState = { 6 | 0: { 7 | value: 'Items not displayed' 8 | }, 9 | 1: { 10 | value: 'No highlighted item' 11 | }, 12 | 2: { 13 | value: 'Highlighted item' 14 | }, 15 | 3: { 16 | value: 'Multi section - No highlighted item' 17 | }, 18 | 4: { 19 | value: 'Multi section - highlighted item' 20 | }, 21 | 5: { 22 | value: 'Hover and click items', 23 | highlightedSectionIndex: null, 24 | highlightedItemIndex: null 25 | }, 26 | 6: { 27 | value: 'Up/Down', 28 | highlightedSectionIndex: null, 29 | highlightedItemIndex: null 30 | }, 31 | 7: { 32 | value: 'Up/Down (with scrollbar)', 33 | highlightedSectionIndex: null, 34 | highlightedItemIndex: 7 35 | }, 36 | 8: { 37 | value: 'Multi section - Up/Down/Enter', 38 | highlightedSectionIndex: null, 39 | highlightedItemIndex: null 40 | }, 41 | 9: { 42 | value: '', 43 | items: [], 44 | highlightedSectionIndex: null, 45 | highlightedItemIndex: null 46 | }, 47 | 10: { 48 | value: '' 49 | }, 50 | 11: { 51 | value: '' 52 | }, 53 | 12: { 54 | value: '', 55 | highlightedSectionIndex: null, 56 | highlightedItemIndex: null 57 | } 58 | }; 59 | 60 | export function updateInputValue(exampleNumber, value, items) { 61 | return { 62 | type: UPDATE_INPUT_VALUE, 63 | exampleNumber, 64 | value, 65 | items 66 | }; 67 | } 68 | 69 | export function hideItems(exampleNumber) { 70 | return { 71 | type: HIDE_ITEMS, 72 | exampleNumber 73 | }; 74 | } 75 | 76 | export function updateHighlightedItem(exampleNumber, highlightedSectionIndex, highlightedItemIndex) { 77 | return { 78 | type: UPDATE_HIGHLIGHTED_ITEM, 79 | exampleNumber, 80 | highlightedSectionIndex, 81 | highlightedItemIndex 82 | }; 83 | } 84 | 85 | export default function(state = initialState, action) { 86 | switch (action.type) { 87 | case UPDATE_INPUT_VALUE: { 88 | const { exampleNumber, value, items } = action; 89 | 90 | return { 91 | ...state, 92 | [exampleNumber]: { 93 | ...state[exampleNumber], 94 | value, 95 | ...(items ? { items } : {}) 96 | } 97 | }; 98 | } 99 | 100 | case HIDE_ITEMS: { 101 | const { exampleNumber } = action; 102 | 103 | return { 104 | ...state, 105 | [exampleNumber]: { 106 | ...state[exampleNumber], 107 | items: [], 108 | highlightedSectionIndex: null, 109 | highlightedItemIndex: null 110 | } 111 | }; 112 | } 113 | 114 | case UPDATE_HIGHLIGHTED_ITEM: { 115 | const { exampleNumber, highlightedSectionIndex, highlightedItemIndex } = action; 116 | 117 | return { 118 | ...state, 119 | [exampleNumber]: { 120 | ...state[exampleNumber], 121 | highlightedSectionIndex: highlightedSectionIndex, 122 | highlightedItemIndex: highlightedItemIndex 123 | } 124 | }; 125 | } 126 | 127 | default: 128 | return state; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /demo/src/components/App/utils.js: -------------------------------------------------------------------------------- 1 | // https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions#Using_Special_Characters 2 | function escapeRegexCharacters(str) { 3 | return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 4 | } 5 | 6 | export { 7 | escapeRegexCharacters 8 | }; 9 | -------------------------------------------------------------------------------- /demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Autowhatever 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /demo/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createStore } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import appReducer from 'App/redux'; 6 | import App from 'App/App'; 7 | 8 | const store = createStore(appReducer); 9 | 10 | if (module.hot) { 11 | // Enable Webpack hot module replacement for reducers 12 | module.hot.accept('App/redux', () => { 13 | const nextRootReducer = require('App/redux'); 14 | 15 | store.replaceReducer(nextRootReducer); 16 | }); 17 | } 18 | 19 | function Demo() { 20 | return ( 21 |
22 | 23 | 24 | 25 |
26 | ); 27 | } 28 | 29 | ReactDOM.render( 30 | , 31 | document.getElementById('demo') 32 | ); 33 | -------------------------------------------------------------------------------- /demo/standalone/app.css: -------------------------------------------------------------------------------- 1 | .react-autowhatever__container { 2 | position: relative; 3 | } 4 | 5 | .react-autowhatever__input { 6 | width: 240px; 7 | height: 30px; 8 | padding: 10px 20px; 9 | font-family: "Helvetica Neue", Arial, sans-serif; 10 | font-weight: 300; 11 | font-size: 16px; 12 | border: 1px solid #aaa; 13 | border-radius: 4px; 14 | -webkit-appearance: none; 15 | } 16 | 17 | .react-autowhatever__input--focused { 18 | outline: none; 19 | } 20 | 21 | .react-autowhatever__input::-ms-clear { 22 | display: none; 23 | } 24 | 25 | .react-autowhatever__input--open { 26 | border-bottom-left-radius: 0; 27 | border-bottom-right-radius: 0; 28 | } 29 | 30 | .react-autowhatever__items-container { 31 | display: none; 32 | } 33 | 34 | .react-autowhatever__items-container--open { 35 | display: block; 36 | position: relative; 37 | top: -1px; 38 | width: 280px; 39 | border: 1px solid #aaa; 40 | background-color: #fff; 41 | font-family: 'Open Sans', sans-serif; 42 | font-weight: 300; 43 | font-size: 16px; 44 | border-bottom-left-radius: 4px; 45 | border-bottom-right-radius: 4px; 46 | z-index: 2; 47 | } 48 | 49 | .react-autowhatever__items-list { 50 | margin: 0; 51 | padding: 0; 52 | list-style-type: none; 53 | } 54 | 55 | .react-autowhatever__item { 56 | cursor: pointer; 57 | padding: 10px 20px; 58 | } 59 | 60 | .react-autowhatever__item--highlighted { 61 | background-color: #ddd; 62 | } 63 | -------------------------------------------------------------------------------- /demo/standalone/app.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/react-in-jsx-scope */ 2 | 3 | const items = [{ 4 | text: 'Apple' 5 | }, { 6 | text: 'Banana' 7 | }, { 8 | text: 'Cherry' 9 | }, { 10 | text: 'Grapefruit' 11 | }, { 12 | text: 'Lemon' 13 | }]; 14 | 15 | const renderItem = item => item.text; 16 | 17 | class App extends React.Component { // eslint-disable-line no-undef 18 | constructor() { 19 | super(); 20 | 21 | this.state = { 22 | value: '' 23 | }; 24 | } 25 | 26 | onChange = event => { 27 | this.setState({ 28 | value: event.target.value 29 | }); 30 | }; 31 | 32 | render() { 33 | const { value } = this.state; 34 | const inputProps = { 35 | placeholder: 'Type to filter fruits', 36 | value, 37 | onChange: this.onChange 38 | }; 39 | 40 | return ( 41 | 47 | ); 48 | } 49 | } 50 | 51 | ReactDOM.render(, document.getElementById('app')); // eslint-disable-line no-undef 52 | -------------------------------------------------------------------------------- /demo/standalone/compiled.app.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) 10 | /******/ return installedModules[moduleId].exports; 11 | 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ exports: {}, 15 | /******/ id: moduleId, 16 | /******/ loaded: false 17 | /******/ }; 18 | 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | 22 | /******/ // Flag the module as loaded 23 | /******/ module.loaded = true; 24 | 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | 29 | 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | 36 | /******/ // __webpack_public_path__ 37 | /******/ __webpack_require__.p = ""; 38 | 39 | /******/ // Load entry module and return exports 40 | /******/ return __webpack_require__(0); 41 | /******/ }) 42 | /************************************************************************/ 43 | /******/ ([ 44 | /* 0 */ 45 | /***/ (function(module, exports, __webpack_require__) { 46 | 47 | module.exports = __webpack_require__(1); 48 | 49 | 50 | /***/ }), 51 | /* 1 */ 52 | /***/ (function(module, exports) { 53 | 54 | 'use strict'; 55 | 56 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 57 | 58 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 59 | 60 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 61 | 62 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 63 | 64 | /* eslint-disable react/react-in-jsx-scope */ 65 | 66 | var items = [{ 67 | text: 'Apple' 68 | }, { 69 | text: 'Banana' 70 | }, { 71 | text: 'Cherry' 72 | }, { 73 | text: 'Grapefruit' 74 | }, { 75 | text: 'Lemon' 76 | }]; 77 | 78 | var renderItem = function renderItem(item) { 79 | return item.text; 80 | }; 81 | 82 | var App = function (_React$Component) { 83 | _inherits(App, _React$Component); 84 | 85 | // eslint-disable-line no-undef 86 | function App() { 87 | _classCallCheck(this, App); 88 | 89 | var _this = _possibleConstructorReturn(this, (App.__proto__ || Object.getPrototypeOf(App)).call(this)); 90 | 91 | _this.onChange = function (event) { 92 | _this.setState({ 93 | value: event.target.value 94 | }); 95 | }; 96 | 97 | _this.state = { 98 | value: '' 99 | }; 100 | return _this; 101 | } 102 | 103 | _createClass(App, [{ 104 | key: 'render', 105 | value: function render() { 106 | var value = this.state.value; 107 | 108 | var inputProps = { 109 | placeholder: 'Type to filter fruits', 110 | value: value, 111 | onChange: this.onChange 112 | }; 113 | 114 | return React.createElement(Autowhatever // eslint-disable-line react/jsx-no-undef 115 | , { items: items, 116 | renderItem: renderItem, 117 | inputProps: inputProps, 118 | highlightedItemIndex: 2 119 | }); 120 | } 121 | }]); 122 | 123 | return App; 124 | }(React.Component); 125 | 126 | ReactDOM.render(React.createElement(App, null), document.getElementById('app')); // eslint-disable-line no-undef 127 | 128 | /***/ }) 129 | /******/ ]); -------------------------------------------------------------------------------- /demo/standalone/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Autowhatever 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-autowhatever", 3 | "version": "10.2.1", 4 | "description": "Accessible rendering layer for Autosuggest and Autocomplete components", 5 | "main": "dist/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/moroshko/react-autowhatever.git" 9 | }, 10 | "author": "Misha Moroshko ", 11 | "scripts": { 12 | "start": "mkdir -p demo/dist && npm run copy-static-files && node server", 13 | "lint": "eslint src test demo/src demo/standalone/app.js server.js webpack.*.js", 14 | "test": "mocha \"test/**/*.test.js\" --compilers js:babel-register --require test/setup.js", 15 | "copy-static-files": "cp demo/src/index.html demo/dist/", 16 | "dist": "rm -rf dist && mkdir dist && babel src -d dist", 17 | "demo-dist": "rm -rf demo/dist && mkdir demo/dist && npm run copy-static-files && webpack --config webpack.gh-pages.config.js", 18 | "standalone": "webpack --config webpack.standalone.config.js && webpack --config webpack.standalone-demo.config.js", 19 | "prebuild": "npm run lint && npm test", 20 | "build": "npm run dist && npm run standalone", 21 | "gh-pages-build": "npm run prebuild && npm run demo-dist", 22 | "deploy": "./scripts/deploy-to-gh-pages.sh", 23 | "preversion": "npm run prebuild", 24 | "postversion": "git push && git push --tags", 25 | "prepublish": "npm run dist && npm run standalone" 26 | }, 27 | "dependencies": { 28 | "prop-types": "^15.5.8", 29 | "react-themeable": "^1.1.0", 30 | "section-iterator": "^2.0.0" 31 | }, 32 | "peerDependencies": { 33 | "react": ">=0.14.7" 34 | }, 35 | "devDependencies": { 36 | "autoprefixer": "^6.7.7", 37 | "autosuggest-highlight": "^3.1.0", 38 | "babel-cli": "^6.24.1", 39 | "babel-core": "^6.24.1", 40 | "babel-eslint": "^7.2.2", 41 | "babel-loader": "^6.4.1", 42 | "babel-preset-es2015": "^6.24.1", 43 | "babel-preset-react": "^6.24.1", 44 | "babel-preset-stage-0": "^6.24.1", 45 | "babel-register": "^6.24.1", 46 | "chai": "^3.5.0", 47 | "css-loader": "^0.28.0", 48 | "eslint": "^3.19.0", 49 | "eslint-plugin-react": "^6.10.3", 50 | "extract-text-webpack-plugin": "^1.0.1", 51 | "jsdom": "^9.12.0", 52 | "less": "^2.7.2", 53 | "less-loader": "^2.2.3", 54 | "mocha": "^3.2.0", 55 | "openurl": "^1.1.1", 56 | "postcss-loader": "^1.3.3", 57 | "react": "^15.5.4", 58 | "react-dom": "^15.5.4", 59 | "react-hot-loader": "^1.3.1", 60 | "react-isolated-scroll": "^0.1.0", 61 | "react-redux": "^4.4.8", 62 | "redux": "^3.6.0", 63 | "sinon": "^1.17.7", 64 | "sinon-chai": "^2.9.0", 65 | "style-loader": "^0.16.1", 66 | "webpack": "^1.13.2", 67 | "webpack-dev-server": "^1.16.2" 68 | }, 69 | "files": [ 70 | "dist" 71 | ], 72 | "keywords": [ 73 | "autosuggest", 74 | "autocomplete", 75 | "auto-suggest", 76 | "auto-complete", 77 | "auto suggest", 78 | "auto complete", 79 | "react autosuggest", 80 | "react autocomplete", 81 | "react auto-suggest", 82 | "react auto-complete", 83 | "react auto suggest", 84 | "react auto complete", 85 | "react-autosuggest", 86 | "react-autocomplete", 87 | "react-auto-suggest", 88 | "react-auto-complete", 89 | "react-component" 90 | ], 91 | "license": "MIT" 92 | } 93 | -------------------------------------------------------------------------------- /scripts/deploy-to-gh-pages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | git checkout gh-pages 6 | git pull origin gh-pages 7 | git merge master --no-edit 8 | npm run gh-pages-build 9 | cp demo/dist/*.* . 10 | git add app.css index.html index.js 11 | git commit -m 'Update gh-pages files' 12 | git push origin gh-pages 13 | git checkout master 14 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var WebpackDevServer = require('webpack-dev-server'); 3 | var openUrl = require('openurl'); 4 | 5 | var config = require('./webpack.dev.config'); 6 | var host = process.env.NODE_HOST || 'localhost'; 7 | var port = process.env.NODE_PORT || 3000; 8 | 9 | new WebpackDevServer(webpack(config), { 10 | publicPath: config.output.publicPath, 11 | hot: true, 12 | historyApiFallback: true 13 | }).listen(port, host, function(error) { 14 | if (error) { 15 | console.error(error); // eslint-disable-line no-console 16 | process.exit(1); 17 | } 18 | 19 | var url = `http://${host}:${port}/demo/dist/index.html`; 20 | 21 | console.log(`Demo is ready at ${url}`); // eslint-disable-line no-console 22 | 23 | openUrl.open(url); 24 | }); 25 | -------------------------------------------------------------------------------- /src/Autowhatever.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import createSectionIterator from 'section-iterator'; 4 | import themeable from 'react-themeable'; 5 | import SectionTitle from './SectionTitle'; 6 | import ItemsList from './ItemsList'; 7 | 8 | const emptyObject = {}; 9 | const defaultRenderInputComponent = props => ; 10 | const defaultRenderItemsContainer = 11 | ({ containerProps, children }) =>
{children}
; 12 | const defaultTheme = { 13 | container: 'react-autowhatever__container', 14 | containerOpen: 'react-autowhatever__container--open', 15 | input: 'react-autowhatever__input', 16 | inputOpen: 'react-autowhatever__input--open', 17 | inputFocused: 'react-autowhatever__input--focused', 18 | itemsContainer: 'react-autowhatever__items-container', 19 | itemsContainerOpen: 'react-autowhatever__items-container--open', 20 | itemsList: 'react-autowhatever__items-list', 21 | item: 'react-autowhatever__item', 22 | itemFirst: 'react-autowhatever__item--first', 23 | itemHighlighted: 'react-autowhatever__item--highlighted', 24 | sectionContainer: 'react-autowhatever__section-container', 25 | sectionContainerFirst: 'react-autowhatever__section-container--first', 26 | sectionTitle: 'react-autowhatever__section-title' 27 | }; 28 | 29 | export default class Autowhatever extends Component { 30 | static propTypes = { 31 | id: PropTypes.string, // Used in aria-* attributes. If multiple Autowhatever's are rendered on a page, they must have unique ids. 32 | multiSection: PropTypes.bool, // Indicates whether a multi section layout should be rendered. 33 | renderInputComponent: PropTypes.func, // When specified, it is used to render the input element. 34 | renderItemsContainer: PropTypes.func, // Renders the items container. 35 | items: PropTypes.array.isRequired, // Array of items or sections to render. 36 | renderItem: PropTypes.func, // This function renders a single item. 37 | renderItemData: PropTypes.object, // Arbitrary data that will be passed to renderItem() 38 | renderSectionTitle: PropTypes.func, // This function gets a section and renders its title. 39 | getSectionItems: PropTypes.func, // This function gets a section and returns its items, which will be passed into `renderItem` for rendering. 40 | containerProps: PropTypes.object, // Arbitrary container props 41 | inputProps: PropTypes.object, // Arbitrary input props 42 | itemProps: PropTypes.oneOfType([ // Arbitrary item props 43 | PropTypes.object, 44 | PropTypes.func 45 | ]), 46 | highlightedSectionIndex: PropTypes.number, // Section index of the highlighted item 47 | highlightedItemIndex: PropTypes.number, // Highlighted item index (within a section) 48 | theme: PropTypes.oneOfType([ // Styles. See: https://github.com/markdalgleish/react-themeable 49 | PropTypes.object, 50 | PropTypes.array 51 | ]) 52 | }; 53 | 54 | static defaultProps = { 55 | id: '1', 56 | multiSection: false, 57 | renderInputComponent: defaultRenderInputComponent, 58 | renderItemsContainer: defaultRenderItemsContainer, 59 | renderItem: () => { 60 | throw new Error('`renderItem` must be provided'); 61 | }, 62 | renderItemData: emptyObject, 63 | renderSectionTitle: () => { 64 | throw new Error('`renderSectionTitle` must be provided'); 65 | }, 66 | getSectionItems: () => { 67 | throw new Error('`getSectionItems` must be provided'); 68 | }, 69 | containerProps: emptyObject, 70 | inputProps: emptyObject, 71 | itemProps: emptyObject, 72 | highlightedSectionIndex: null, 73 | highlightedItemIndex: null, 74 | theme: defaultTheme 75 | }; 76 | 77 | constructor(props) { 78 | super(props); 79 | 80 | this.highlightedItem = null; 81 | 82 | this.state = { 83 | isInputFocused: false 84 | }; 85 | 86 | this.setSectionsItems(props); 87 | this.setSectionIterator(props); 88 | this.setTheme(props); 89 | } 90 | 91 | componentDidMount() { 92 | this.ensureHighlightedItemIsVisible(); 93 | } 94 | 95 | // eslint-disable-next-line camelcase, react/sort-comp 96 | UNSAFE_componentWillReceiveProps(nextProps) { 97 | if (nextProps.items !== this.props.items) { 98 | this.setSectionsItems(nextProps); 99 | } 100 | 101 | if (nextProps.items !== this.props.items || nextProps.multiSection !== this.props.multiSection) { 102 | this.setSectionIterator(nextProps); 103 | } 104 | 105 | if (nextProps.theme !== this.props.theme) { 106 | this.setTheme(nextProps); 107 | } 108 | } 109 | 110 | componentDidUpdate() { 111 | this.ensureHighlightedItemIsVisible(); 112 | } 113 | 114 | setSectionsItems(props) { 115 | if (props.multiSection) { 116 | this.sectionsItems = props.items.map(section => props.getSectionItems(section)); 117 | this.sectionsLengths = this.sectionsItems.map(items => items.length); 118 | this.allSectionsAreEmpty = this.sectionsLengths.every(itemsCount => itemsCount === 0); 119 | } 120 | } 121 | 122 | setSectionIterator(props) { 123 | this.sectionIterator = createSectionIterator({ 124 | multiSection: props.multiSection, 125 | data: props.multiSection ? this.sectionsLengths : props.items.length 126 | }); 127 | } 128 | 129 | setTheme(props) { 130 | this.theme = themeable(props.theme); 131 | } 132 | 133 | storeInputReference = input => { 134 | if (input !== null) { 135 | this.input = input; 136 | } 137 | 138 | const userRef = this.props.inputProps.ref; 139 | if (userRef) { 140 | if (typeof userRef === 'function') { 141 | userRef(input); 142 | } else if (typeof userRef === 'object' && userRef.hasOwnProperty('current')) { 143 | userRef.current = input; 144 | } 145 | } 146 | }; 147 | 148 | storeItemsContainerReference = itemsContainer => { 149 | if (itemsContainer !== null) { 150 | this.itemsContainer = itemsContainer; 151 | } 152 | }; 153 | 154 | onHighlightedItemChange = highlightedItem => { 155 | this.highlightedItem = highlightedItem; 156 | }; 157 | 158 | getItemId = (sectionIndex, itemIndex) => { 159 | if (itemIndex === null) { 160 | return null; 161 | } 162 | 163 | const { id } = this.props; 164 | const section = (sectionIndex === null ? '' : `section-${sectionIndex}`); 165 | 166 | return `react-autowhatever-${id}-${section}-item-${itemIndex}`; 167 | }; 168 | 169 | renderSections() { 170 | if (this.allSectionsAreEmpty) { 171 | return null; 172 | } 173 | 174 | const { theme } = this; 175 | const { 176 | id, items, renderItem, renderItemData, renderSectionTitle, 177 | highlightedSectionIndex, highlightedItemIndex, itemProps 178 | } = this.props; 179 | 180 | return items.map((section, sectionIndex) => { 181 | const keyPrefix = `react-autowhatever-${id}-`; 182 | const sectionKeyPrefix = `${keyPrefix}section-${sectionIndex}-`; 183 | const isFirstSection = (sectionIndex === 0); 184 | 185 | // `key` is provided by theme() 186 | /* eslint-disable react/jsx-key */ 187 | return ( 188 |
189 | 195 | 208 |
209 | ); 210 | /* eslint-enable react/jsx-key */ 211 | }); 212 | } 213 | 214 | renderItems() { 215 | const { items } = this.props; 216 | 217 | if (items.length === 0) { 218 | return null; 219 | } 220 | 221 | const { theme } = this; 222 | const { 223 | id, renderItem, renderItemData, highlightedSectionIndex, 224 | highlightedItemIndex, itemProps 225 | } = this.props; 226 | 227 | return ( 228 | 239 | ); 240 | } 241 | 242 | onFocus = event => { 243 | const { inputProps } = this.props; 244 | 245 | this.setState({ 246 | isInputFocused: true 247 | }); 248 | 249 | inputProps.onFocus && inputProps.onFocus(event); 250 | }; 251 | 252 | onBlur = event => { 253 | const { inputProps } = this.props; 254 | 255 | this.setState({ 256 | isInputFocused: false 257 | }); 258 | 259 | inputProps.onBlur && inputProps.onBlur(event); 260 | }; 261 | 262 | onKeyDown = event => { 263 | const { inputProps, highlightedSectionIndex, highlightedItemIndex } = this.props; 264 | 265 | switch (event.key) { 266 | case 'ArrowDown': 267 | case 'ArrowUp': { 268 | const nextPrev = (event.key === 'ArrowDown' ? 'next' : 'prev'); 269 | const [newHighlightedSectionIndex, newHighlightedItemIndex] = 270 | this.sectionIterator[nextPrev]([highlightedSectionIndex, highlightedItemIndex]); 271 | 272 | inputProps.onKeyDown(event, { newHighlightedSectionIndex, newHighlightedItemIndex }); 273 | break; 274 | } 275 | 276 | default: 277 | inputProps.onKeyDown(event, { highlightedSectionIndex, highlightedItemIndex }); 278 | } 279 | }; 280 | 281 | ensureHighlightedItemIsVisible() { 282 | const { highlightedItem } = this; 283 | 284 | if (!highlightedItem) { 285 | return; 286 | } 287 | 288 | const { itemsContainer } = this; 289 | const itemOffsetRelativeToContainer = 290 | highlightedItem.offsetParent === itemsContainer 291 | ? highlightedItem.offsetTop 292 | : highlightedItem.offsetTop - itemsContainer.offsetTop; 293 | 294 | let { scrollTop } = itemsContainer; // Top of the visible area 295 | 296 | if (itemOffsetRelativeToContainer < scrollTop) { 297 | // Item is off the top of the visible area 298 | scrollTop = itemOffsetRelativeToContainer; 299 | } else if (itemOffsetRelativeToContainer + highlightedItem.offsetHeight > scrollTop + itemsContainer.offsetHeight) { 300 | // Item is off the bottom of the visible area 301 | scrollTop = itemOffsetRelativeToContainer + highlightedItem.offsetHeight - itemsContainer.offsetHeight; 302 | } 303 | 304 | if (scrollTop !== itemsContainer.scrollTop) { 305 | itemsContainer.scrollTop = scrollTop; 306 | } 307 | } 308 | 309 | render() { 310 | const { theme } = this; 311 | const { 312 | id, multiSection, renderInputComponent, renderItemsContainer, 313 | highlightedSectionIndex, highlightedItemIndex 314 | } = this.props; 315 | const { isInputFocused } = this.state; 316 | const renderedItems = multiSection ? this.renderSections() : this.renderItems(); 317 | const isOpen = (renderedItems !== null); 318 | const ariaActivedescendant = this.getItemId(highlightedSectionIndex, highlightedItemIndex); 319 | const itemsContainerId = `react-autowhatever-${id}`; 320 | const containerProps = { 321 | role: 'combobox', 322 | 'aria-haspopup': 'listbox', 323 | 'aria-owns': itemsContainerId, 324 | 'aria-expanded': isOpen, 325 | ...theme( 326 | `react-autowhatever-${id}-container`, 327 | 'container', 328 | isOpen && 'containerOpen' 329 | ), 330 | ...this.props.containerProps 331 | }; 332 | const inputComponent = renderInputComponent({ 333 | type: 'text', 334 | value: '', 335 | autoComplete: 'off', 336 | 'aria-autocomplete': 'list', 337 | 'aria-controls': itemsContainerId, 338 | 'aria-activedescendant': ariaActivedescendant, 339 | ...theme( 340 | `react-autowhatever-${id}-input`, 341 | 'input', 342 | isOpen && 'inputOpen', 343 | isInputFocused && 'inputFocused' 344 | ), 345 | ...this.props.inputProps, 346 | onFocus: this.onFocus, 347 | onBlur: this.onBlur, 348 | onKeyDown: this.props.inputProps.onKeyDown && this.onKeyDown, 349 | ref: this.storeInputReference 350 | }); 351 | const itemsContainer = renderItemsContainer({ 352 | containerProps: { 353 | id: itemsContainerId, 354 | role: 'listbox', 355 | ...theme( 356 | `react-autowhatever-${id}-items-container`, 357 | 'itemsContainer', 358 | isOpen && 'itemsContainerOpen' 359 | ), 360 | ref: this.storeItemsContainerReference 361 | }, 362 | children: renderedItems 363 | }); 364 | 365 | return ( 366 |
367 | {inputComponent} 368 | {itemsContainer} 369 |
370 | ); 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /src/Item.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import compareObjects from './compareObjects'; 4 | 5 | export default class Item extends Component { 6 | static propTypes = { 7 | sectionIndex: PropTypes.number, 8 | isHighlighted: PropTypes.bool.isRequired, 9 | itemIndex: PropTypes.number.isRequired, 10 | item: PropTypes.any.isRequired, 11 | renderItem: PropTypes.func.isRequired, 12 | renderItemData: PropTypes.object.isRequired, 13 | onMouseEnter: PropTypes.func, 14 | onMouseLeave: PropTypes.func, 15 | onMouseDown: PropTypes.func, 16 | onClick: PropTypes.func 17 | }; 18 | 19 | shouldComponentUpdate(nextProps) { 20 | return compareObjects(nextProps, this.props, ['renderItemData']); 21 | } 22 | 23 | storeItemReference = item => { 24 | if (item !== null) { 25 | this.item = item; 26 | } 27 | }; 28 | 29 | onMouseEnter = event => { 30 | const { sectionIndex, itemIndex } = this.props; 31 | 32 | this.props.onMouseEnter(event, { sectionIndex, itemIndex }); 33 | }; 34 | 35 | onMouseLeave = event => { 36 | const { sectionIndex, itemIndex } = this.props; 37 | 38 | this.props.onMouseLeave(event, { sectionIndex, itemIndex }); 39 | }; 40 | 41 | onMouseDown = event => { 42 | const { sectionIndex, itemIndex } = this.props; 43 | 44 | this.props.onMouseDown(event, { sectionIndex, itemIndex }); 45 | }; 46 | 47 | onClick = event => { 48 | const { sectionIndex, itemIndex } = this.props; 49 | 50 | this.props.onClick(event, { sectionIndex, itemIndex }); 51 | }; 52 | 53 | render() { 54 | const { isHighlighted, item, renderItem, renderItemData, ...restProps } = this.props; 55 | 56 | delete restProps.sectionIndex; 57 | delete restProps.itemIndex; 58 | 59 | if (typeof restProps.onMouseEnter === 'function') { 60 | restProps.onMouseEnter = this.onMouseEnter; 61 | } 62 | 63 | if (typeof restProps.onMouseLeave === 'function') { 64 | restProps.onMouseLeave = this.onMouseLeave; 65 | } 66 | 67 | if (typeof restProps.onMouseDown === 'function') { 68 | restProps.onMouseDown = this.onMouseDown; 69 | } 70 | 71 | if (typeof restProps.onClick === 'function') { 72 | restProps.onClick = this.onClick; 73 | } 74 | 75 | return ( 76 |
  • 77 | {renderItem(item, { isHighlighted, ...renderItemData })} 78 |
  • 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/ItemsList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Item from './Item'; 4 | import compareObjects from './compareObjects'; 5 | 6 | export default class ItemsList extends Component { 7 | static propTypes = { 8 | items: PropTypes.array.isRequired, 9 | itemProps: PropTypes.oneOfType([ 10 | PropTypes.object, 11 | PropTypes.func 12 | ]), 13 | renderItem: PropTypes.func.isRequired, 14 | renderItemData: PropTypes.object.isRequired, 15 | sectionIndex: PropTypes.number, 16 | highlightedItemIndex: PropTypes.number, 17 | onHighlightedItemChange: PropTypes.func.isRequired, 18 | getItemId: PropTypes.func.isRequired, 19 | theme: PropTypes.func.isRequired, 20 | keyPrefix: PropTypes.string.isRequired 21 | }; 22 | 23 | static defaultProps = { 24 | sectionIndex: null 25 | }; 26 | 27 | shouldComponentUpdate(nextProps) { 28 | return compareObjects(nextProps, this.props, ['itemProps']); 29 | } 30 | 31 | storeHighlightedItemReference = highlightedItem => { 32 | this.props.onHighlightedItemChange(highlightedItem === null ? null : highlightedItem.item); 33 | }; 34 | 35 | render() { 36 | const { 37 | items, itemProps, renderItem, renderItemData, sectionIndex, 38 | highlightedItemIndex, getItemId, theme, keyPrefix 39 | } = this.props; 40 | const sectionPrefix = (sectionIndex === null ? keyPrefix : `${keyPrefix}section-${sectionIndex}-`); 41 | const isItemPropsFunction = (typeof itemProps === 'function'); 42 | 43 | return ( 44 |
      45 | { 46 | items.map((item, itemIndex) => { 47 | const isFirst = (itemIndex === 0); 48 | const isHighlighted = (itemIndex === highlightedItemIndex); 49 | const itemKey = `${sectionPrefix}item-${itemIndex}`; 50 | const itemPropsObj = isItemPropsFunction ? itemProps({ sectionIndex, itemIndex }) : itemProps; 51 | const allItemProps = { 52 | id: getItemId(sectionIndex, itemIndex), 53 | 'aria-selected': isHighlighted, 54 | ...theme(itemKey, 'item', isFirst && 'itemFirst', isHighlighted && 'itemHighlighted'), 55 | ...itemPropsObj 56 | }; 57 | 58 | if (isHighlighted) { 59 | allItemProps.ref = this.storeHighlightedItemReference; 60 | } 61 | 62 | // `key` is provided by theme() 63 | /* eslint-disable react/jsx-key */ 64 | return ( 65 | 74 | ); 75 | /* eslint-enable react/jsx-key */ 76 | }) 77 | } 78 |
    79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/SectionTitle.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import compareObjects from './compareObjects'; 4 | 5 | export default class SectionTitle extends Component { 6 | static propTypes = { 7 | section: PropTypes.any.isRequired, 8 | renderSectionTitle: PropTypes.func.isRequired, 9 | theme: PropTypes.func.isRequired, 10 | sectionKeyPrefix: PropTypes.string.isRequired 11 | }; 12 | 13 | shouldComponentUpdate(nextProps) { 14 | return compareObjects(nextProps, this.props); 15 | } 16 | 17 | render() { 18 | const { section, renderSectionTitle, theme, sectionKeyPrefix } = this.props; 19 | const sectionTitle = renderSectionTitle(section); 20 | 21 | if (!sectionTitle) { 22 | return null; 23 | } 24 | 25 | return ( 26 |
    27 | {sectionTitle} 28 |
    29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/compareObjects.js: -------------------------------------------------------------------------------- 1 | export default function compareObjects(objA, objB, keys = []) { 2 | if (objA === objB) { 3 | return false; 4 | } 5 | 6 | const aKeys = Object.keys(objA); 7 | const bKeys = Object.keys(objB); 8 | 9 | if (aKeys.length !== bKeys.length) { 10 | return true; 11 | } 12 | 13 | const keysMap = {}; 14 | let i, len; 15 | 16 | for (i = 0, len = keys.length; i < len; i++) { 17 | keysMap[keys[i]] = true; 18 | } 19 | 20 | for (i = 0, len = aKeys.length; i < len; i++) { 21 | let key = aKeys[i]; 22 | const aValue = objA[key]; 23 | const bValue = objB[key]; 24 | 25 | if (aValue === bValue) { 26 | continue; 27 | } 28 | 29 | if (!keysMap[key] || aValue === null || bValue === null || 30 | typeof aValue !== 'object' || typeof bValue !== 'object') { 31 | return true; 32 | } 33 | 34 | const aValueKeys = Object.keys(aValue); 35 | const bValueKeys = Object.keys(bValue); 36 | 37 | if (aValueKeys.length !== bValueKeys.length) { 38 | return true; 39 | } 40 | 41 | for (let n = 0, length = aValueKeys.length; n < length; n++) { 42 | const aValueKey = aValueKeys[n]; 43 | 44 | if (aValue[aValueKey] !== bValue[aValueKey]) { 45 | return true; 46 | } 47 | } 48 | } 49 | 50 | return false; 51 | } 52 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./Autowhatever').default; 2 | -------------------------------------------------------------------------------- /test/compareObjects.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import compareObjects from '../src/compareObjects'; 3 | 4 | const obj = { a: 1 }; 5 | const noop = () => {}; 6 | 7 | const cases = [ 8 | { 9 | objA: {}, 10 | objB: {}, 11 | result: false 12 | }, 13 | { 14 | objA: obj, 15 | objB: obj, 16 | result: false 17 | }, 18 | { 19 | objA: { a: 5 }, 20 | objB: { a: 5 }, 21 | result: false 22 | }, 23 | { 24 | objA: { a: 1 }, 25 | objB: { b: 1 }, 26 | result: true 27 | }, 28 | { 29 | objA: { a: 1 }, 30 | objB: { a: 1, b: 2 }, 31 | result: true 32 | }, 33 | { 34 | objA: { a: 1, b: 2 }, 35 | objB: { a: 1 }, 36 | result: true 37 | }, 38 | { 39 | objA: { a: 5, b: false, c: 'hey' }, 40 | objB: { a: 5, b: false, c: 'hey' }, 41 | result: false 42 | }, 43 | { 44 | objA: { a: noop }, 45 | objB: { a: noop }, 46 | result: false 47 | }, 48 | { 49 | objA: { a: noop }, 50 | objB: { a: noop }, 51 | keys: ['a'], 52 | result: false 53 | }, 54 | { 55 | objA: { a: () => {} }, 56 | objB: { a: () => {} }, 57 | result: true 58 | }, 59 | { 60 | objA: { a: () => {} }, 61 | objB: { a: () => {} }, 62 | keys: ['a'], 63 | result: true 64 | }, 65 | { 66 | objA: { a: { b: 9 } }, 67 | objB: { a: { b: 9 } }, 68 | result: true 69 | }, 70 | { 71 | objA: { a: { b: 9 } }, 72 | objB: { a: { b: 9 } }, 73 | keys: ['a'], 74 | result: false 75 | }, 76 | { 77 | objA: { a: { b: 9 } }, 78 | objB: { a: null }, 79 | keys: ['a'], 80 | result: true 81 | }, 82 | { 83 | objA: { a: null }, 84 | objB: { a: { b: 9 } }, 85 | keys: ['a'], 86 | result: true 87 | }, 88 | { 89 | objA: { a: { b: 9, c: 'hi' } }, 90 | objB: { a: { b: 9 } }, 91 | keys: ['a'], 92 | result: true 93 | }, 94 | { 95 | objA: { a: { b: 9 } }, 96 | objB: { a: { b: 9, c: 'hi' } }, 97 | keys: ['a'], 98 | result: true 99 | }, 100 | { 101 | objA: { a: { b: 9 }, c: { d: null } }, 102 | objB: { a: { b: 9 }, c: { d: null } }, 103 | keys: ['a', 'c'], 104 | result: false 105 | }, 106 | { 107 | objA: { a: { b: 9 }, c: { d: {} } }, 108 | objB: { a: { b: 9 }, c: { d: {} } }, 109 | keys: ['a', 'c'], 110 | result: true 111 | } 112 | ]; 113 | 114 | describe('compareObjects', () => { 115 | cases.forEach(({ objA, objB, keys, result }) => { 116 | it(`should return ${result} for ${JSON.stringify(objA)} and ${JSON.stringify(objB)}${keys ? ` with keys ${keys}` : ''}`, () => { 117 | expect(compareObjects(objA, objB, keys)).to.equal(result); 118 | }); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import sinon from 'sinon'; 3 | import sinonChai from 'sinon-chai'; 4 | import SyntheticEvent from 'react-dom/lib/SyntheticEvent'; 5 | import TestUtils, { Simulate } from 'react-dom/test-utils'; 6 | 7 | chai.use(sinonChai); 8 | 9 | let app, container, input, itemsContainer; 10 | 11 | export const init = application => { 12 | app = application; 13 | container = TestUtils.findRenderedDOMComponentWithClass(app, 'react-autowhatever__container'); 14 | input = TestUtils.findRenderedDOMComponentWithTag(app, 'input'); 15 | itemsContainer = TestUtils.findRenderedDOMComponentWithClass(app, 'react-autowhatever__items-container'); 16 | }; 17 | 18 | export const eventMatcher = sinon.match.instanceOf(SyntheticEvent); 19 | export const childrenMatcher = sinon.match.any; 20 | export const containerPropsMatcher = sinon.match({ 21 | id: sinon.match.string, 22 | key: sinon.match.string, 23 | className: sinon.match.string, 24 | ref: sinon.match.func 25 | }); 26 | 27 | export const getElementWithClass = 28 | className => TestUtils.findRenderedDOMComponentWithClass(app, className); 29 | export const getStoredInput = () => app.autowhatever.input; 30 | export const getStoredItemsContainer = () => app.autowhatever.itemsContainer; 31 | export const getStoredHighlightedItem = () => app.autowhatever.highlightedItem; 32 | 33 | export const getContainerAttribute = attr => 34 | container.getAttribute(attr); 35 | 36 | export const getInputAttribute = attr => 37 | input.getAttribute(attr); 38 | 39 | export const getItemsContainerAttribute = attr => 40 | itemsContainer.getAttribute(attr); 41 | 42 | export const getItems = () => 43 | TestUtils.scryRenderedDOMComponentsWithClass(app, 'react-autowhatever__item'); 44 | 45 | export const getItem = itemIndex => { 46 | const items = getItems(); 47 | 48 | if (itemIndex >= items.length) { 49 | throw Error(`Cannot find item #${itemIndex}`); 50 | } 51 | 52 | return items[itemIndex]; 53 | }; 54 | 55 | export const mouseEnterItem = itemIndex => 56 | Simulate.mouseEnter(getItem(itemIndex)); 57 | 58 | export const mouseLeaveItem = itemIndex => 59 | Simulate.mouseLeave(getItem(itemIndex)); 60 | 61 | export const mouseDownItem = itemIndex => 62 | Simulate.mouseDown(getItem(itemIndex)); 63 | 64 | export const clickItem = itemIndex => 65 | Simulate.click(getItem(itemIndex)); 66 | 67 | export const clickUp = () => 68 | Simulate.keyDown(input, { key: 'ArrowUp' }); 69 | 70 | export const clickDown = () => 71 | Simulate.keyDown(input, { key: 'ArrowDown' }); 72 | 73 | export const clickEnter = () => 74 | Simulate.keyDown(input, { key: 'Enter' }); 75 | -------------------------------------------------------------------------------- /test/multi-section/Autowhatever.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react-dom/test-utils'; 3 | import { expect } from 'chai'; 4 | import sections from './sections'; 5 | import { 6 | init, 7 | eventMatcher, 8 | clickUp, 9 | clickDown, 10 | clickEnter 11 | } from '../helpers'; 12 | import AutowhateverApp, { 13 | getSectionItems, 14 | renderSectionTitle, 15 | onKeyDown 16 | } from './AutowhateverApp'; 17 | 18 | describe('Multi Section Autowhatever', () => { 19 | beforeEach(() => { 20 | getSectionItems.reset(); 21 | renderSectionTitle.reset(); 22 | onKeyDown.reset(); 23 | init(TestUtils.renderIntoDocument()); 24 | }); 25 | 26 | describe('renderSectionTitle', () => { 27 | it('should be called once for every section', () => { 28 | expect(renderSectionTitle).to.have.callCount(3); 29 | expect(renderSectionTitle.getCall(0).args[0]).to.deep.equal(sections[0]); 30 | expect(renderSectionTitle.getCall(1).args[0]).to.deep.equal(sections[1]); 31 | expect(renderSectionTitle.getCall(2).args[0]).to.deep.equal(sections[2]); 32 | }); 33 | 34 | it('should not be called when Down is pressed', () => { 35 | renderSectionTitle.reset(); 36 | clickDown(); 37 | expect(renderSectionTitle).not.to.have.been.called; 38 | }); 39 | }); 40 | 41 | describe('getSectionItems', () => { 42 | it('should be called once for every section', () => { 43 | expect(getSectionItems).to.have.callCount(3); 44 | expect(getSectionItems.getCall(0).args[0]).to.deep.equal(sections[0]); 45 | expect(getSectionItems.getCall(1).args[0]).to.deep.equal(sections[1]); 46 | expect(getSectionItems.getCall(2).args[0]).to.deep.equal(sections[2]); 47 | }); 48 | 49 | it('should not be called when Down is pressed', () => { 50 | getSectionItems.reset(); 51 | clickDown(); 52 | expect(getSectionItems).not.to.have.been.called; 53 | }); 54 | }); 55 | 56 | describe('inputProps.onKeyDown', () => { 57 | it('should be called with the right parameters when Up/Down is pressed', () => { 58 | clickDown(); 59 | expect(onKeyDown).to.be.calledOnce; 60 | expect(onKeyDown).to.be.calledWith(eventMatcher, { 61 | newHighlightedSectionIndex: 0, 62 | newHighlightedItemIndex: 0 63 | }); 64 | 65 | clickDown(); 66 | expect(onKeyDown).to.be.calledWith(eventMatcher, { 67 | newHighlightedSectionIndex: 0, 68 | newHighlightedItemIndex: 1 69 | }); 70 | 71 | clickDown(); 72 | expect(onKeyDown).to.be.calledWith(eventMatcher, { 73 | newHighlightedSectionIndex: 1, 74 | newHighlightedItemIndex: 0 75 | }); 76 | 77 | clickDown(); 78 | expect(onKeyDown).to.be.calledWith(eventMatcher, { 79 | newHighlightedSectionIndex: 2, 80 | newHighlightedItemIndex: 0 81 | }); 82 | 83 | clickDown(); 84 | expect(onKeyDown).to.be.calledWith(eventMatcher, { 85 | newHighlightedSectionIndex: null, 86 | newHighlightedItemIndex: null 87 | }); 88 | }); 89 | 90 | it('should be called with the right parameters when Enter is pressed', () => { 91 | clickEnter(); 92 | expect(onKeyDown).to.be.calledWith(eventMatcher, { 93 | highlightedSectionIndex: null, 94 | highlightedItemIndex: null 95 | }); 96 | 97 | clickUp(); 98 | clickEnter(); 99 | expect(onKeyDown).to.be.calledWith(eventMatcher, { 100 | highlightedSectionIndex: 2, 101 | highlightedItemIndex: 0 102 | }); 103 | }); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /test/multi-section/AutowhateverApp.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import sinon from 'sinon'; 3 | import Autowhatever from '../../src/Autowhatever'; 4 | import sections from './sections'; 5 | 6 | let app; 7 | 8 | export const renderSectionTitle = sinon.spy(section => ( 9 | {section.title} 10 | )); 11 | 12 | export const getSectionItems = sinon.spy(section => section.items); 13 | 14 | export const renderItem = sinon.spy(item => ( 15 | {item.text} 16 | )); 17 | 18 | export const onKeyDown = sinon.spy((event, { newHighlightedSectionIndex, newHighlightedItemIndex }) => { 19 | if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { 20 | app.setState({ 21 | highlightedSectionIndex: newHighlightedSectionIndex, 22 | highlightedItemIndex: newHighlightedItemIndex 23 | }); 24 | } 25 | }); 26 | 27 | export default class AutowhateverApp extends Component { 28 | constructor() { 29 | super(); 30 | 31 | app = this; 32 | 33 | this.state = { 34 | value: '', 35 | highlightedSectionIndex: null, 36 | highlightedItemIndex: null 37 | }; 38 | } 39 | 40 | storeAutowhateverReference = autowhatever => { 41 | if (autowhatever !== null) { 42 | this.autowhatever = autowhatever; 43 | } 44 | }; 45 | 46 | onChange = event => { 47 | this.setState({ 48 | value: event.target.value 49 | }); 50 | }; 51 | 52 | render() { 53 | const { value, highlightedSectionIndex, highlightedItemIndex } = this.state; 54 | const inputProps = { 55 | value, 56 | onChange: this.onChange, 57 | onKeyDown 58 | }; 59 | 60 | return ( 61 | 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/multi-section/sections.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | title: 'A', 4 | items: [ 5 | { 6 | text: 'Apple' 7 | }, 8 | { 9 | text: 'Apricot' 10 | } 11 | ] 12 | }, 13 | { 14 | title: 'B', 15 | items: [ 16 | { 17 | text: 'Banana' 18 | } 19 | ] 20 | }, 21 | { 22 | title: 'C', 23 | items: [ 24 | { 25 | text: 'Cherry' 26 | } 27 | ] 28 | } 29 | ]; 30 | -------------------------------------------------------------------------------- /test/plain-list/Autowhatever.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react-dom/test-utils'; 3 | import { expect } from 'chai'; 4 | import { 5 | init, 6 | getStoredInput, 7 | getStoredItemsContainer, 8 | getStoredHighlightedItem, 9 | getContainerAttribute, 10 | getInputAttribute, 11 | getItemsContainerAttribute, 12 | getItems, 13 | getItem, 14 | mouseEnterItem, 15 | mouseLeaveItem, 16 | mouseDownItem, 17 | clickItem 18 | } from '../helpers'; 19 | import AutowhateverApp, { 20 | renderItem 21 | } from './AutowhateverApp'; 22 | 23 | describe('Plain List Autowhatever', () => { 24 | beforeEach(() => { 25 | renderItem.reset(); 26 | init(TestUtils.renderIntoDocument()); 27 | }); 28 | 29 | describe('initially', () => { 30 | it('should set container\'s `aria-owns` to items container\'s `id`', () => { 31 | expect(getContainerAttribute('aria-owns')).to.equal(getItemsContainerAttribute('id')); 32 | }); 33 | 34 | it('should set input\'s `aria-controls` to items container\'s `id`', () => { 35 | expect(getInputAttribute('aria-controls')).to.equal(getItemsContainerAttribute('id')); 36 | }); 37 | 38 | it('should render all items', () => { 39 | expect(getItems()).to.be.of.length(5); 40 | }); 41 | 42 | it('should call `renderItem` exactly `items.length` times', () => { 43 | expect(renderItem).to.have.callCount(5); 44 | }); 45 | 46 | it('should store the input on the instance', () => { 47 | expect(getStoredInput().getAttribute('id')).to.equal('my-fancy-input'); 48 | }); 49 | 50 | it('should store the items container on the instance', () => { 51 | expect(getStoredItemsContainer().getAttribute('id')).to.equal('react-autowhatever-my-fancy-component'); 52 | }); 53 | 54 | it('should set the stored highlighted item on the instance to null', () => { 55 | expect(getStoredHighlightedItem()).to.equal(null); 56 | }); 57 | }); 58 | 59 | describe('hovering items', () => { 60 | it('should call `renderItem` once with the right parameters when item is entered', () => { 61 | renderItem.reset(); 62 | mouseEnterItem(0); 63 | expect(renderItem).to.have.been.calledOnce; 64 | expect(renderItem).to.be.calledWith({ text: 'Apple' }); 65 | }); 66 | 67 | it('should call `renderItem` twice when the highlighted item is changed', () => { 68 | mouseEnterItem(1); 69 | renderItem.reset(); 70 | mouseLeaveItem(1); 71 | mouseEnterItem(2); 72 | expect(renderItem).to.have.been.calledTwice; 73 | }); 74 | 75 | it('should call `renderItem` with `isHighlighted` flag', () => { 76 | renderItem.reset(); 77 | mouseEnterItem(0); 78 | expect(renderItem).to.have.been.calledOnce; 79 | expect(renderItem).to.be.calledWith({ text: 'Apple' }, { isHighlighted: true }); 80 | 81 | renderItem.reset(); 82 | mouseLeaveItem(0); 83 | expect(renderItem).to.have.been.calledOnce; 84 | expect(renderItem).to.be.calledWith({ text: 'Apple' }, { isHighlighted: false }); 85 | }); 86 | 87 | it('should set `aria-selected` to true on highlighted items', () => { 88 | renderItem.reset(); 89 | mouseEnterItem(0); 90 | expect(getItem(0).getAttribute('aria-selected')).to.equal('true'); 91 | 92 | renderItem.reset(); 93 | mouseLeaveItem(0); 94 | expect(getItem(0).getAttribute('aria-selected')).to.equal('false'); 95 | }); 96 | 97 | it('should call `renderItem` once when item is left', () => { 98 | mouseEnterItem(3); 99 | renderItem.reset(); 100 | mouseLeaveItem(3); 101 | expect(renderItem).to.have.been.calledOnce; 102 | }); 103 | 104 | it('should not call `renderItem` when item is clicked', () => { 105 | renderItem.reset(); 106 | mouseDownItem(4); 107 | clickItem(4); 108 | expect(renderItem).not.to.have.been.called; 109 | }); 110 | 111 | it('should store the highlighted item on the instance', () => { 112 | mouseEnterItem(2); 113 | expect(getStoredHighlightedItem().getAttribute('id')) 114 | .to.equal('react-autowhatever-my-fancy-component--item-2'); 115 | 116 | mouseLeaveItem(2); 117 | expect(getStoredHighlightedItem()).to.equal(null); 118 | 119 | mouseEnterItem(3); 120 | expect(getStoredHighlightedItem().getAttribute('id')) 121 | .to.equal('react-autowhatever-my-fancy-component--item-3'); 122 | }); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /test/plain-list/AutowhateverApp.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import sinon from 'sinon'; 3 | import Autowhatever from '../../src/Autowhatever'; 4 | import items from './items'; 5 | 6 | export const renderItem = sinon.spy(item => ( 7 | {item.text} 8 | )); 9 | 10 | export default class AutowhateverApp extends Component { 11 | constructor() { 12 | super(); 13 | 14 | this.state = { 15 | value: '', 16 | highlightedItemIndex: null 17 | }; 18 | } 19 | 20 | storeAutowhateverReference = autowhatever => { 21 | if (autowhatever !== null) { 22 | this.autowhatever = autowhatever; 23 | } 24 | }; 25 | 26 | onChange = event => { 27 | this.setState({ 28 | value: event.target.value 29 | }); 30 | }; 31 | 32 | onMouseEnter = (event, { itemIndex }) => { 33 | this.setState({ 34 | highlightedItemIndex: itemIndex 35 | }); 36 | }; 37 | 38 | onMouseLeave = () => { 39 | this.setState({ 40 | highlightedItemIndex: null 41 | }); 42 | }; 43 | 44 | onClick = (event, { itemIndex }) => { 45 | this.setState({ 46 | value: items[itemIndex].text 47 | }); 48 | }; 49 | 50 | render() { 51 | const { value, highlightedItemIndex } = this.state; 52 | const inputProps = { 53 | id: 'my-fancy-input', 54 | value, 55 | onChange: this.onChange 56 | }; 57 | const itemProps = { 58 | onMouseEnter: this.onMouseEnter, 59 | onMouseLeave: this.onMouseLeave, 60 | onClick: this.onClick 61 | }; 62 | 63 | return ( 64 | 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /test/plain-list/items.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | text: 'Apple' 4 | }, 5 | { 6 | text: 'Banana' 7 | }, 8 | { 9 | text: 'Cherry' 10 | }, 11 | { 12 | text: 'Grapefruit' 13 | }, 14 | { 15 | text: 'Lemon' 16 | } 17 | ]; 18 | -------------------------------------------------------------------------------- /test/render-input-component/Autowhatever.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react-dom/test-utils'; 3 | import { expect } from 'chai'; 4 | import { init, getStoredInput } from '../helpers'; 5 | import AutowhateverApp from './AutowhateverApp'; 6 | 7 | describe('Autowhatever with renderInputComponent', () => { 8 | beforeEach(() => { 9 | init(TestUtils.renderIntoDocument()); 10 | }); 11 | 12 | it('should store the input on the instance', () => { 13 | expect(getStoredInput().getAttribute('id')).to.equal('my-custom-input'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/render-input-component/AutowhateverApp.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Autowhatever from '../../src/Autowhatever'; 3 | import items from './items'; 4 | 5 | export const renderItem = item => item.text; 6 | 7 | export const renderInputComponent = props => ( 8 |
    9 | 10 |
    11 | ); 12 | 13 | export default class AutowhateverApp extends Component { 14 | constructor() { 15 | super(); 16 | 17 | this.state = { 18 | value: '' 19 | }; 20 | } 21 | 22 | storeAutowhateverReference = autowhatever => { 23 | if (autowhatever !== null) { 24 | this.autowhatever = autowhatever; // used by the getStoredInput() helper 25 | } 26 | }; 27 | 28 | onChange = event => { 29 | this.setState({ 30 | value: event.target.value 31 | }); 32 | }; 33 | 34 | render() { 35 | const { value } = this.state; 36 | const inputProps = { 37 | id: 'my-custom-input', 38 | value, 39 | onChange: this.onChange 40 | }; 41 | 42 | return ( 43 | 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/render-input-component/items.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | text: 'Apple' 4 | }, 5 | { 6 | text: 'Banana' 7 | }, 8 | { 9 | text: 'Cherry' 10 | }, 11 | { 12 | text: 'Grapefruit' 13 | }, 14 | { 15 | text: 'Lemon' 16 | } 17 | ]; 18 | -------------------------------------------------------------------------------- /test/render-items-container/Autowhatever.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react-dom/test-utils'; 3 | import { expect } from 'chai'; 4 | import { 5 | init, 6 | childrenMatcher, 7 | containerPropsMatcher, 8 | getElementWithClass, 9 | getItemsContainerAttribute 10 | } from '../helpers'; 11 | import AutowhateverApp, { 12 | renderItemsContainer 13 | } from './AutowhateverApp'; 14 | 15 | describe('Autowhatever with renderItemsContainer', () => { 16 | beforeEach(() => { 17 | renderItemsContainer.reset(); 18 | init(TestUtils.renderIntoDocument()); 19 | }); 20 | 21 | it('should set items container id properly', () => { 22 | expect(getItemsContainerAttribute('id')).to.equal('react-autowhatever-my-id'); 23 | }); 24 | 25 | it('should render whatever renderItemsContainer returns', () => { 26 | expect(getElementWithClass('my-items-container-footer')).not.to.equal(null); 27 | }); 28 | 29 | it('should call renderItemsContainer once with the right parameters', () => { 30 | expect(renderItemsContainer).to.have.been.calledOnce; 31 | expect(renderItemsContainer).to.be.calledWith({ 32 | children: childrenMatcher, 33 | containerProps: containerPropsMatcher 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/render-items-container/AutowhateverApp.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import sinon from 'sinon'; 3 | import Autowhatever from '../../src/Autowhatever'; 4 | import items from './items'; 5 | 6 | export const renderItem = item => item.text; 7 | 8 | export const renderItemsContainer = sinon.spy(({ containerProps, children }) => ( 9 |
    10 | {children} 11 |
    12 | Footer 13 |
    14 |
    15 | )); 16 | 17 | export default class AutowhateverApp extends Component { 18 | constructor() { 19 | super(); 20 | 21 | this.state = { 22 | value: '' 23 | }; 24 | } 25 | 26 | onChange = event => { 27 | this.setState({ 28 | value: event.target.value 29 | }); 30 | }; 31 | 32 | render() { 33 | const { value } = this.state; 34 | const inputProps = { 35 | id: 'my-custom-input', 36 | value, 37 | onChange: this.onChange 38 | }; 39 | 40 | return ( 41 | 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/render-items-container/items.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | text: 'Apple' 4 | }, 5 | { 6 | text: 'Banana' 7 | }, 8 | { 9 | text: 'Cherry' 10 | }, 11 | { 12 | text: 'Grapefruit' 13 | }, 14 | { 15 | text: 'Lemon' 16 | } 17 | ]; 18 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import { jsdom } from 'jsdom'; 2 | 3 | global.document = jsdom(''); 4 | global.window = global.document.defaultView; 5 | global.navigator = global.window.navigator; 6 | -------------------------------------------------------------------------------- /webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var autoprefixer = require('autoprefixer'); 4 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 5 | 6 | module.exports = { 7 | entry: [ 8 | 'webpack-dev-server/client?http://localhost:3000', 9 | 'webpack/hot/only-dev-server', 10 | './demo/src/index' 11 | ], 12 | 13 | output: { 14 | path: path.join(__dirname, 'dist'), // Must be an absolute path 15 | filename: 'index.js', 16 | publicPath: '/demo/dist' 17 | }, 18 | 19 | module: { 20 | loaders: [{ 21 | test: /\.js$/, 22 | loaders: ['react-hot', 'babel'], 23 | include: [ 24 | path.join(__dirname, 'src'), // Must be an absolute path 25 | path.join(__dirname, 'demo', 'src') // Must be an absolute path 26 | ] 27 | }, { 28 | test: /\.less$/, 29 | loader: ExtractTextPlugin.extract('style', 'css?modules&localIdentName=[name]__[local]___[hash:base64:5]!postcss!less'), 30 | exclude: /node_modules/ 31 | }] 32 | }, 33 | 34 | postcss: function() { 35 | return [autoprefixer]; 36 | }, 37 | 38 | resolve: { 39 | modulesDirectories: ['node_modules', 'components', 'src'] 40 | }, 41 | 42 | devtool: 'source-map', 43 | 44 | plugins: [ 45 | new webpack.HotModuleReplacementPlugin(), 46 | new ExtractTextPlugin('app.css') 47 | ] 48 | }; 49 | -------------------------------------------------------------------------------- /webpack.gh-pages.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var autoprefixer = require('autoprefixer'); 4 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 5 | 6 | module.exports = { 7 | entry: './demo/src/index', 8 | 9 | output: { 10 | filename: './demo/dist/index.js' 11 | }, 12 | 13 | module: { 14 | loaders: [{ 15 | test: /\.js$/, 16 | loaders: ['babel'], 17 | include: [ 18 | path.join(__dirname, 'src'), // Must be an absolute path 19 | path.join(__dirname, 'demo', 'src') // Must be an absolute path 20 | ] 21 | }, { 22 | test: /\.less$/, 23 | loader: ExtractTextPlugin.extract('style', 'css?modules&localIdentName=[name]__[local]___[hash:base64:5]!postcss!less'), 24 | exclude: /node_modules/ 25 | }] 26 | }, 27 | 28 | postcss: function() { 29 | return [autoprefixer]; 30 | }, 31 | 32 | resolve: { 33 | modulesDirectories: ['node_modules', 'components', 'src'] 34 | }, 35 | 36 | plugins: [ 37 | new ExtractTextPlugin('./demo/dist/app.css'), 38 | new webpack.DefinePlugin({ 39 | 'process.env': { 40 | NODE_ENV: JSON.stringify('production') 41 | } 42 | }), 43 | new webpack.optimize.UglifyJsPlugin({ 44 | output: { 45 | comments: false 46 | }, 47 | compress: { 48 | warnings: false 49 | } 50 | }) 51 | ] 52 | }; 53 | -------------------------------------------------------------------------------- /webpack.standalone-demo.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | entry: [ 5 | './demo/standalone/app' 6 | ], 7 | 8 | output: { 9 | filename: './demo/standalone/compiled.app.js' 10 | }, 11 | 12 | module: { 13 | loaders: [{ 14 | test: /\.js$/, 15 | loader: 'babel', 16 | include: [ 17 | path.join(__dirname, 'demo', 'standalone') // Must be an absolute path 18 | ] 19 | }] 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /webpack.standalone.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = [{ 5 | entry: './src/index.js', 6 | 7 | output: { 8 | filename: './dist/standalone/autowhatever.js', 9 | libraryTarget: 'umd', 10 | library: 'Autowhatever' 11 | }, 12 | 13 | module: { 14 | loaders: [{ 15 | test: /\.js$/, 16 | loader: 'babel', 17 | include: [ 18 | path.join(__dirname, 'src') // Must be an absolute path 19 | ] 20 | }] 21 | }, 22 | 23 | externals: { 24 | react: 'React' 25 | } 26 | }, { 27 | entry: './src/index.js', 28 | 29 | output: { 30 | filename: './dist/standalone/autowhatever.min.js', 31 | libraryTarget: 'umd', 32 | library: 'Autowhatever' 33 | }, 34 | 35 | module: { 36 | loaders: [{ 37 | test: /\.js$/, 38 | loader: 'babel', 39 | include: [ 40 | path.join(__dirname, 'src') // Must be an absolute path 41 | ] 42 | }] 43 | }, 44 | 45 | externals: { 46 | react: 'React' 47 | }, 48 | 49 | plugins: [ 50 | new webpack.DefinePlugin({ 51 | 'process.env': { 52 | NODE_ENV: JSON.stringify('production') 53 | } 54 | }), 55 | new webpack.optimize.UglifyJsPlugin({ 56 | output: { 57 | comments: false 58 | }, 59 | compress: { 60 | warnings: false 61 | } 62 | }) 63 | ] 64 | }]; 65 | --------------------------------------------------------------------------------