├── .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 | [](https://codeship.com/projects/96953)
2 |
3 | [](https://npmjs.org/package/react-autowhatever)
4 | [](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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------