The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .babelrc
├── .editorconfig
├── .eslintrc.js
├── .github
    ├── CONTRIBUTING.md
    ├── ISSUE_TEMPLATE.md
    ├── PULL_REQUEST_TEMPLATE.md
    └── workflows
    │   └── build-and-test.yml
├── .gitignore
├── .mocharc.yaml
├── FAQ.md
├── LICENSE
├── README.md
├── demo
    ├── src
    │   ├── components
    │   │   ├── App
    │   │   │   ├── App.js
    │   │   │   ├── App.less
    │   │   │   └── components
    │   │   │   │   ├── Examples
    │   │   │   │       ├── Examples.js
    │   │   │   │       ├── Examples.less
    │   │   │   │       └── components
    │   │   │   │       │   ├── Basic
    │   │   │   │       │       ├── Basic.js
    │   │   │   │       │       ├── Basic.less
    │   │   │   │       │       ├── autosuggest.css
    │   │   │   │       │       └── languages.js
    │   │   │   │       │   ├── CustomRender
    │   │   │   │       │       ├── CustomRender.js
    │   │   │   │       │       ├── CustomRender.less
    │   │   │   │       │       ├── people.js
    │   │   │   │       │       ├── photos
    │   │   │   │       │       │   ├── dancounsell.jpg
    │   │   │   │       │       │   ├── ladylexy.jpg
    │   │   │   │       │       │   ├── mtnmissy.jpg
    │   │   │   │       │       │   └── steveodom.jpg
    │   │   │   │       │       └── theme.less
    │   │   │   │       │   ├── MultipleSections
    │   │   │   │       │       ├── MultipleSections.js
    │   │   │   │       │       ├── MultipleSections.less
    │   │   │   │       │       ├── languages.js
    │   │   │   │       │       └── theme.less
    │   │   │   │       │   └── ScrollableContainer
    │   │   │   │       │       ├── ScrollableContainer.js
    │   │   │   │       │       ├── ScrollableContainer.less
    │   │   │   │       │       ├── countries.js
    │   │   │   │       │       └── theme.less
    │   │   │   │   ├── Features
    │   │   │   │       ├── Features.js
    │   │   │   │       ├── Features.less
    │   │   │   │       ├── accessible.svg
    │   │   │   │       ├── customizable.svg
    │   │   │   │       └── mobile-friendly.svg
    │   │   │   │   ├── Footer
    │   │   │   │       ├── Footer.js
    │   │   │   │       └── Footer.less
    │   │   │   │   ├── GitHub
    │   │   │   │       ├── GitHub.js
    │   │   │   │       └── GitHub.less
    │   │   │   │   ├── Header
    │   │   │   │       ├── Header.js
    │   │   │   │       ├── Header.less
    │   │   │   │       ├── logo.svg
    │   │   │   │       ├── star.svg
    │   │   │   │       └── twitter.svg
    │   │   │   │   └── Link
    │   │   │   │       ├── Link.js
    │   │   │   │       └── Link.less
    │   │   └── utils
    │   │   │   └── utils.js
    │   ├── index.html
    │   ├── index.js
    │   └── variables.less
    └── standalone
    │   ├── app.css
    │   ├── app.js
    │   ├── compiled.app.js
    │   └── index.html
├── dom-structure.png
├── nyc.config.js
├── package-lock.json
├── package.json
├── scripts
    └── deploy-to-gh-pages.sh
├── server.js
├── src
    ├── Autosuggest.js
    ├── Autowhatever.js
    ├── Item.js
    ├── ItemList.js
    ├── SectionTitle.js
    ├── compareObjects.js
    ├── index.js
    └── theme.js
├── test
    ├── always-render-suggestions
    │   ├── AutosuggestApp.js
    │   └── AutosuggestApp.test.js
    ├── async-suggestions
    │   ├── AutosuggestApp.js
    │   └── AutosuggestApp.test.js
    ├── autowhatever
    │   ├── autowhatever-helpers.js
    │   ├── compareObjects.test.js
    │   ├── default-props
    │   │   ├── Autowhatever.test.js
    │   │   ├── AutowhateverApp.js
    │   │   └── sections.js
    │   ├── function-ref
    │   │   ├── Autowhatever.test.js
    │   │   ├── AutowhateverApp.js
    │   │   └── items.js
    │   ├── multi-section
    │   │   ├── Autowhatever.test.js
    │   │   ├── AutowhateverApp.js
    │   │   └── sections.js
    │   ├── plain-list
    │   │   ├── Autowhatever.test.js
    │   │   ├── AutowhateverApp.js
    │   │   └── items.js
    │   └── render-items-container
    │   │   ├── Autowhatever.test.js
    │   │   ├── AutowhateverApp.js
    │   │   └── items.js
    ├── do-not-focus-input-on-suggestion-click
    │   ├── AutosuggestApp.js
    │   └── AutosuggestApp.test.js
    ├── focus-first-suggestion-clear-on-enter
    │   ├── AutosuggestApp.js
    │   └── AutosuggestApp.test.js
    ├── focus-first-suggestion
    │   ├── AutosuggestApp.js
    │   └── AutosuggestApp.test.js
    ├── helpers.js
    ├── keep-suggestions-on-select
    │   ├── AutosuggestApp.js
    │   └── AutosuggestApp.test.js
    ├── multi-section
    │   ├── AutosuggestApp.js
    │   ├── AutosuggestApp.test.js
    │   └── languages.js
    ├── plain-list
    │   ├── AutosuggestApp.js
    │   ├── AutosuggestApp.test.js
    │   └── languages.js
    ├── render-input-component
    │   ├── AutosuggestApp.js
    │   └── AutosuggestApp.test.js
    ├── render-suggestions-container
    │   ├── AutosuggestApp.js
    │   └── AutosuggestApp.test.js
    ├── setup.js
    └── textarea
    │   ├── AutosuggestApp.js
    │   ├── AutosuggestApp.test.js
    │   └── languages.js
├── webpack.dev.config.js
├── webpack.gh-pages.config.js
├── webpack.standalone-demo.config.js
└── webpack.standalone.config.js


/.babelrc:
--------------------------------------------------------------------------------
 1 | {
 2 |   "presets": ["@babel/env", "@babel/preset-react"],
 3 |   "plugins": [
 4 |     "@babel/plugin-proposal-class-properties"
 5 |   ],
 6 |   "env": {
 7 |     "production": {
 8 |       "plugins": [
 9 |         [
10 |           "babel-plugin-transform-react-remove-prop-types", {
11 |             "mode": "wrap"
12 |           }
13 |         ]
14 |       ]
15 |     },
16 |     "test": {
17 |       "plugins": [
18 |         "istanbul",
19 |       ]
20 |     }
21 |   }
22 | }
23 | 


--------------------------------------------------------------------------------
/.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: ['react'],
 10 |   extends: ['eslint:recommended'],
 11 |   rules: {
 12 |     'array-callback-return': 2,
 13 |     camelcase: [2, { properties: 'always' }],
 14 |     'linebreak-style': [2, 'unix'],
 15 |     'no-cond-assign': [2, 'always'],
 16 |     'no-console': 2,
 17 |     'no-global-assign': 2,
 18 |     'no-restricted-properties': [
 19 |       2,
 20 |       {
 21 |         object: 'describe',
 22 |         property: 'only',
 23 |         message: 'Please run all tests!',
 24 |       },
 25 |       {
 26 |         object: 'describe',
 27 |         property: 'skip',
 28 |         message: 'Please run all tests!',
 29 |       },
 30 |       {
 31 |         object: 'it',
 32 |         property: 'only',
 33 |         message: 'Please run all tests!',
 34 |       },
 35 |       {
 36 |         object: 'it',
 37 |         property: 'skip',
 38 |         message: 'Please run all tests!',
 39 |       },
 40 |     ],
 41 |     'no-template-curly-in-string': 2,
 42 |     'no-unused-vars': 2,
 43 |     'padding-line-between-statements': [
 44 |       'error',
 45 |       { blankLine: 'always', prev: ['const', 'let', 'var'], next: '*' },
 46 |       {
 47 |         blankLine: 'any',
 48 |         prev: ['const', 'let', 'var'],
 49 |         next: ['const', 'let', 'var'],
 50 |       },
 51 |     ],
 52 |     'prefer-destructuring': [2, { array: false, object: true }],
 53 |     'prefer-rest-params': 2,
 54 | 
 55 |     'react/display-name': 0,
 56 |     'react/forbid-prop-types': 0,
 57 |     'react/no-comment-textnodes': 0,
 58 |     'react/no-danger': 2,
 59 |     'react/no-danger-with-children': 2,
 60 |     'react/no-deprecated': 2,
 61 |     'react/no-did-mount-set-state': 2,
 62 |     'react/no-did-update-set-state': 2,
 63 |     'react/no-direct-mutation-state': 2,
 64 |     'react/no-find-dom-node': 2,
 65 |     'react/no-is-mounted': 2,
 66 |     'react/no-multi-comp': [2, { ignoreStateless: true }],
 67 |     'react/no-render-return-value': 2,
 68 |     'react/no-set-state': 0,
 69 |     'react/no-string-refs': 2,
 70 |     'react/no-unknown-property': 2,
 71 |     'react/no-unused-prop-types': 0, // https://github.com/yannickcr/eslint-plugin-react/issues/944
 72 |     'react/prefer-es6-class': [2, 'always'],
 73 |     'react/prefer-stateless-function': 2,
 74 |     'react/prop-types': [2, { skipUndeclared: true }],
 75 |     'react/react-in-jsx-scope': 2,
 76 |     'react/require-optimization': 0,
 77 |     'react/require-render-return': 2,
 78 |     'react/self-closing-comp': 2,
 79 |     'react/sort-comp': 2,
 80 |     'react/sort-prop-types': 0,
 81 |     'react/style-prop-object': 2,
 82 | 
 83 |     'react/jsx-boolean-value': [2, 'always'],
 84 |     'react/jsx-filename-extension': [2, { extensions: ['.js'] }],
 85 |     'react/jsx-handler-names': 0,
 86 |     'react/jsx-key': 2,
 87 |     'react/jsx-max-props-per-line': 0,
 88 |     'react/jsx-no-bind': 2,
 89 |     'react/jsx-no-duplicate-props': 2,
 90 |     'react/jsx-no-literals': 0,
 91 |     'react/jsx-no-target-blank': 2,
 92 |     'react/jsx-no-undef': 2,
 93 |     'react/jsx-pascal-case': 2,
 94 |     'react/jsx-sort-props': 0,
 95 |     'react/jsx-uses-react': 2,
 96 |     'react/jsx-uses-vars': 2,
 97 |   },
 98 |   settings: {
 99 |     react: {
100 |       version: 'detect',
101 |     },
102 |   },
103 | };
104 | 


--------------------------------------------------------------------------------
/.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-autosuggest` 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 the [Basic example](http://codepen.io/moroshko/pen/LGNJMy).
 4 | 
 5 | * Provide the steps to reproduce the issue, e.g.:
 6 |   1. Focus on the input field
 7 |   2. Type `c`, and wait for suggestions to appear
 8 |   3. Press <kbd>Escape</kbd>
 9 | 
10 |   **Observed behaviour:** Suggestions stay open
11 | 
12 |   **Expected behaviour:** Suggestions should be closed
13 | 
14 | ## Are you making a feature request?
15 | 
16 | * Please describe your use case from user journey point of view, e.g.:
17 | 
18 |   > In my application, when user highlights suggestions (using the mouse or keyboard), I'd like to display additional information about the highlighted suggestion alongside the Autosuggest.
19 | 
20 | * If you have ideas how to extend the Autosuggest API to support your new feature, please share!
21 | 
22 | * If you know any examples online that already implement such functionality, please share a link.
23 | 


--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | Thanks a lot for contributing to react-autosuggest :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 | 


--------------------------------------------------------------------------------
/.github/workflows/build-and-test.yml:
--------------------------------------------------------------------------------
 1 | name: PR status checks
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: [ master ]
 6 |   pull_request:
 7 |     branches: [ master ]
 8 | 
 9 | jobs:
10 |   build:
11 | 
12 |     runs-on: ubuntu-latest
13 | 
14 |     strategy:
15 |       matrix:
16 |         node-version: [12.x]
17 | 
18 |     steps:
19 |     - uses: actions/checkout@v2
20 |     - name: Use Node.js ${{ matrix.node-version }}
21 |       uses: actions/setup-node@v1
22 |       with:
23 |         node-version: ${{ matrix.node-version }}
24 |     - run: npm ci
25 |     - run: npm run lint
26 |     - run: npm test
27 |       env:
28 |         CI: true
29 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | node_modules
 2 | coverage
 3 | .nyc_output
 4 | dist
 5 | demo/dist
 6 | npm-debug.log
 7 | yarn.lock
 8 | *.log
 9 | .DS
10 | 


--------------------------------------------------------------------------------
/.mocharc.yaml:
--------------------------------------------------------------------------------
1 | require: '@babel/register'
2 | file:
3 |   - ./test/setup.js
4 | 


--------------------------------------------------------------------------------
/FAQ.md:
--------------------------------------------------------------------------------
 1 | <a name="gettingTheInputElement"></a>
 2 | ### How do I get the input element?
 3 | 
 4 | The input element is available on the Autosuggest instance as `input`.
 5 | 
 6 | You could store the input element like this:
 7 | 
 8 | ```js
 9 | function storeInputReference(autosuggest) {
10 |   if (autosuggest !== null) {
11 |     this.input = autosuggest.input;
12 |   }
13 | }
14 | 
15 | <Autosuggest ref={storeInputReference} ... />
16 | ```
17 | 
18 | [Codepen example](http://codepen.io/moroshko/pen/WryOMP)
19 | 
20 | <a name="limitSuggestionsContainerScrolling"></a>
21 | ### How do I limit the scrolling of the suggestions container to the container itself?
22 | 
23 | When the suggestions container has a scroll bar, and you scroll beyond the start/end of the container, the page starts scrolling. To stop that, you can use [`react-isolated-scroll`](https://github.com/markdalgleish/react-isolated-scroll):
24 | 
25 | ```js
26 | import IsolatedScroll from 'react-isolated-scroll';
27 | 
28 | function renderSuggestionsContainer({ containerProps, children }) {
29 |   const { ref, ...restContainerProps } = containerProps;
30 |   const callRef = isolatedScroll => {
31 |     if (isolatedScroll !== null) {
32 |       ref(isolatedScroll.component);
33 |     }
34 |   };
35 | 
36 |   return (
37 |     <IsolatedScroll ref={callRef} {...restContainerProps}>
38 |       {children}
39 |     </IsolatedScroll>
40 |   );
41 | }
42 | 
43 | <Autosuggest renderSuggestionsContainer={renderSuggestionsContainer} ... />
44 | ```
45 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | The MIT License (MIT)
 2 | 
 3 | Copyright © 2017 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 | 


--------------------------------------------------------------------------------
/demo/src/components/App/App.js:
--------------------------------------------------------------------------------
 1 | import styles from './App.less';
 2 | 
 3 | import React from 'react';
 4 | import Header from 'Header/Header';
 5 | import Features from 'Features/Features';
 6 | import Examples from 'Examples/Examples';
 7 | import Footer from 'Footer/Footer';
 8 | 
 9 | const App = () => (
10 |   <div className={styles.container}>
11 |     <Header />
12 |     <Features />
13 |     <Examples />
14 |     <Footer />
15 |   </div>
16 | );
17 | 
18 | export default App;
19 | 


--------------------------------------------------------------------------------
/demo/src/components/App/App.less:
--------------------------------------------------------------------------------
 1 | body {
 2 |   margin: 0;
 3 | 
 4 |   // https://github.com/reactjs/react-modal/issues/191#issuecomment-230459026
 5 |   :global(&.ReactModal__Body--open) {
 6 |     overflow-y: hidden;
 7 |   }
 8 | }
 9 | 
10 | .container {
11 |   font-family: 'Open Sans', sans-serif;
12 |   font-weight: 300;
13 | }
14 | 
15 | .examplesContainer {
16 |   display: flex;
17 |   flex-direction: column;
18 | }
19 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/Examples/Examples.js:
--------------------------------------------------------------------------------
 1 | import styles from './Examples.less';
 2 | 
 3 | import React from 'react';
 4 | import Basic from 'Basic/Basic';
 5 | import MultipleSections from 'MultipleSections/MultipleSections';
 6 | import CustomRender from 'CustomRender/CustomRender';
 7 | import ScrollableContainer from 'ScrollableContainer/ScrollableContainer';
 8 | 
 9 | const Examples = () => (
10 |   <div className={styles.container}>
11 |     <h2 className={styles.header}>Examples</h2>
12 |     <Basic />
13 |     <MultipleSections />
14 |     <CustomRender />
15 |     <ScrollableContainer />
16 |   </div>
17 | );
18 | 
19 | export default Examples;
20 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/Examples/Examples.less:
--------------------------------------------------------------------------------
 1 | @import 'variables';
 2 | 
 3 | .container {
 4 |   display: flex;
 5 |   flex-direction: column;
 6 |   align-items: center;
 7 |   padding: (11 * @rows) 0;
 8 |   color: #212121;
 9 | 
10 |   @media @small {
11 |     padding: (7 * @rows) 0;
12 |   }
13 | }
14 | 
15 | .header {
16 |   margin: 0;
17 |   font-size: 50px;
18 |   font-weight: 300;
19 | 
20 |   @media @small {
21 |     font-size: 34px;
22 |   }
23 | }
24 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/Examples/components/Basic/Basic.js:
--------------------------------------------------------------------------------
 1 | import styles from './Basic.less';
 2 | 
 3 | import React, { Component } from 'react';
 4 | import isMobile from 'ismobilejs';
 5 | import Link from 'Link/Link';
 6 | import Autosuggest from 'Autosuggest';
 7 | import languages from './languages';
 8 | import { escapeRegexCharacters } from 'utils/utils';
 9 | 
10 | const focusInputOnSuggestionClick = !isMobile.any;
11 | 
12 | const getSuggestions = value => {
13 |   const escapedValue = escapeRegexCharacters(value.trim());
14 | 
15 |   if (escapedValue === '') {
16 |     return [];
17 |   }
18 | 
19 |   const regex = new RegExp('^' + escapedValue, 'i');
20 | 
21 |   return languages.filter(language => regex.test(language.name));
22 | };
23 | 
24 | const getSuggestionValue = suggestion => suggestion.name;
25 | 
26 | const renderSuggestion = suggestion => <span>{suggestion.name}</span>;
27 | 
28 | export default class Basic extends Component {
29 |   constructor() {
30 |     super();
31 | 
32 |     this.state = {
33 |       value: '',
34 |       suggestions: []
35 |     };
36 |   }
37 | 
38 |   onChange = (event, { newValue }) => {
39 |     this.setState({
40 |       value: newValue
41 |     });
42 |   };
43 | 
44 |   onSuggestionsFetchRequested = ({ value }) => {
45 |     this.setState({
46 |       suggestions: getSuggestions(value)
47 |     });
48 |   };
49 | 
50 |   onSuggestionsClearRequested = () => {
51 |     this.setState({
52 |       suggestions: []
53 |     });
54 |   };
55 | 
56 |   render() {
57 |     const { value, suggestions } = this.state;
58 |     const inputProps = {
59 |       placeholder: "Type 'c'",
60 |       value,
61 |       onChange: this.onChange
62 |     };
63 | 
64 |     return (
65 |       <div id="basic-example" className={styles.container}>
66 |         <div className={styles.textContainer}>
67 |           <div className={styles.title}>Basic</div>
68 |           <div className={styles.description}>
69 |             Let’s start simple. Here’s a plain list of suggestions.
70 |           </div>
71 |           <Link
72 |             className={styles.codepenLink}
73 |             href="http://codepen.io/moroshko/pen/LGNJMy"
74 |             underline={false}
75 |           >
76 |             Codepen
77 |           </Link>
78 |         </div>
79 |         <div className={styles.autosuggest}>
80 |           <Autosuggest
81 |             suggestions={suggestions}
82 |             onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
83 |             onSuggestionsClearRequested={this.onSuggestionsClearRequested}
84 |             getSuggestionValue={getSuggestionValue}
85 |             renderSuggestion={renderSuggestion}
86 |             inputProps={inputProps}
87 |             focusInputOnSuggestionClick={focusInputOnSuggestionClick}
88 |             id="basic-example"
89 |           />
90 |         </div>
91 |       </div>
92 |     );
93 |   }
94 | }
95 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/Examples/components/Basic/Basic.less:
--------------------------------------------------------------------------------
 1 | @import 'variables';
 2 | 
 3 | .container {
 4 |   display: flex;
 5 |   justify-content: space-between;
 6 |   width: 34 * @columns;
 7 |   margin: (11 * @rows) (0.5 * @column) 0;
 8 | 
 9 |   @media @examples-vertical {
10 |     flex-direction: column;
11 |     width: 14 * @columns;
12 |     margin-top: 9 * @rows;
13 |   }
14 | 
15 |   @media @small {
16 |     margin-top: 7 * @rows;
17 |   }
18 | }
19 | 
20 | .textContainer {
21 |   display: flex;
22 |   flex-direction: column;
23 |   width: 15 * @columns;
24 | }
25 | 
26 | .title {
27 |   font-size: 30px;
28 |   font-weight: 400;
29 |   line-height: 5 * @rows;
30 | 
31 |   @media @small {
32 |     font-size: 25px;
33 |   }
34 | }
35 | 
36 | .description {
37 |   margin-top: @row;
38 |   font-size: 20px;
39 | }
40 | 
41 | .codepenLink {
42 |   margin-top: @row;
43 |   font-size: 20px;
44 |   color: #209FD3;
45 |   align-self: flex-start;
46 | }
47 | 
48 | .autosuggest {
49 |   margin-top: 6 * @rows;
50 | 
51 |   @media @examples-vertical {
52 |     margin-top: 3 * @rows;
53 |     margin-left: 0;
54 |   }
55 | }
56 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/Examples/components/Basic/autosuggest.css:
--------------------------------------------------------------------------------
 1 | .react-autosuggest__container {
 2 |   position: relative;
 3 | }
 4 | 
 5 | .react-autosuggest__input {
 6 |   width: 240px;
 7 |   height: 30px;
 8 |   padding: 10px 20px;
 9 |   font-family: 'Open Sans', 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-autosuggest__input--focused {
18 |   outline: none;
19 | }
20 | 
21 | .react-autosuggest__input::-ms-clear {
22 |   display: none;
23 | }
24 | 
25 | .react-autosuggest__input--open {
26 |   border-bottom-left-radius: 0;
27 |   border-bottom-right-radius: 0;
28 | }
29 | 
30 | .react-autosuggest__suggestions-container {
31 |   display: none;
32 | }
33 | 
34 | .react-autosuggest__suggestions-container--open {
35 |   display: block;
36 |   position: absolute;
37 |   top: 51px;
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-autosuggest__suggestions-list {
50 |   margin: 0;
51 |   padding: 0;
52 |   list-style-type: none;
53 | }
54 | 
55 | .react-autosuggest__suggestion {
56 |   cursor: pointer;
57 |   padding: 10px 20px;
58 | }
59 | 
60 | .react-autosuggest__suggestion--highlighted {
61 |   background-color: #ddd;
62 | }
63 | 
64 | .react-autosuggest__section-container {
65 |   border-top: 1px dashed #ccc;
66 | }
67 | 
68 | .react-autosuggest__section-container--first {
69 |   border-top: 0;
70 | }
71 | 
72 | .react-autosuggest__section-title {
73 |   padding: 10px 0 0 10px;
74 |   font-size: 12px;
75 |   color: #777;
76 | }
77 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/Examples/components/Basic/languages.js:
--------------------------------------------------------------------------------
 1 | export default [
 2 |   {
 3 |     name: 'C',
 4 |     year: 1972
 5 |   },
 6 |   {
 7 |     name: 'C#',
 8 |     year: 2000
 9 |   },
10 |   {
11 |     name: 'C++',
12 |     year: 1983
13 |   },
14 |   {
15 |     name: 'Clojure',
16 |     year: 2007
17 |   },
18 |   {
19 |     name: 'Elm',
20 |     year: 2012
21 |   },
22 |   {
23 |     name: 'Go',
24 |     year: 2009
25 |   },
26 |   {
27 |     name: 'Haskell',
28 |     year: 1990
29 |   },
30 |   {
31 |     name: 'Java',
32 |     year: 1995
33 |   },
34 |   {
35 |     name: 'JavaScript',
36 |     year: 1995
37 |   },
38 |   {
39 |     name: 'Perl',
40 |     year: 1987
41 |   },
42 |   {
43 |     name: 'PHP',
44 |     year: 1995
45 |   },
46 |   {
47 |     name: 'Python',
48 |     year: 1991
49 |   },
50 |   {
51 |     name: 'Ruby',
52 |     year: 1995
53 |   },
54 |   {
55 |     name: 'Scala',
56 |     year: 2003
57 |   }
58 | ];
59 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/Examples/components/CustomRender/CustomRender.js:
--------------------------------------------------------------------------------
  1 | import styles from './CustomRender.less';
  2 | import theme from './theme.less';
  3 | 
  4 | import React, { Component } from 'react';
  5 | import isMobile from 'ismobilejs';
  6 | import match from 'autosuggest-highlight/match';
  7 | import parse from 'autosuggest-highlight/parse';
  8 | import Link from 'Link/Link';
  9 | import Autosuggest from 'Autosuggest';
 10 | import people from './people';
 11 | import { escapeRegexCharacters } from 'utils/utils';
 12 | 
 13 | const focusInputOnSuggestionClick = !isMobile.any;
 14 | 
 15 | const getSuggestions = value => {
 16 |   const escapedValue = escapeRegexCharacters(value.trim());
 17 | 
 18 |   if (escapedValue === '') {
 19 |     return [];
 20 |   }
 21 | 
 22 |   const regex = new RegExp('\\b' + escapedValue, 'i');
 23 | 
 24 |   return people.filter(person => regex.test(getSuggestionValue(person)));
 25 | };
 26 | 
 27 | const getSuggestionValue = suggestion =>
 28 |   `${suggestion.first} ${suggestion.last}`;
 29 | 
 30 | const renderSuggestion = (suggestion, { query }) => {
 31 |   const suggestionText = `${suggestion.first} ${suggestion.last}`;
 32 |   const matches = match(suggestionText, query);
 33 |   const parts = parse(suggestionText, matches);
 34 | 
 35 |   return (
 36 |     <span className={theme.suggestionContent + ' ' + theme[suggestion.twitter]}>
 37 |       <span className={theme.name}>
 38 |         {parts.map((part, index) => {
 39 |           const className = part.highlight ? theme.highlight : null;
 40 | 
 41 |           return (
 42 |             <span className={className} key={index}>
 43 |               {part.text}
 44 |             </span>
 45 |           );
 46 |         })}
 47 |       </span>
 48 |     </span>
 49 |   );
 50 | };
 51 | 
 52 | export default class CustomRender extends Component {
 53 |   constructor() {
 54 |     super();
 55 | 
 56 |     this.state = {
 57 |       value: '',
 58 |       suggestions: []
 59 |     };
 60 |   }
 61 | 
 62 |   onChange = (event, { newValue }) => {
 63 |     this.setState({
 64 |       value: newValue
 65 |     });
 66 |   };
 67 | 
 68 |   onSuggestionsFetchRequested = ({ value }) => {
 69 |     setTimeout(() => {
 70 |       if (value === this.state.value) {
 71 |         this.setState({
 72 |           suggestions: getSuggestions(value)
 73 |         });
 74 |       }
 75 |     }, 200);
 76 |   };
 77 | 
 78 |   onSuggestionsClearRequested = () => {
 79 |     this.setState({
 80 |       suggestions: []
 81 |     });
 82 |   };
 83 | 
 84 |   render() {
 85 |     const { value, suggestions } = this.state;
 86 |     const inputProps = {
 87 |       placeholder: "Type 'c'",
 88 |       value,
 89 |       onChange: this.onChange
 90 |     };
 91 | 
 92 |     return (
 93 |       <div id="custom-render-example" className={styles.container}>
 94 |         <div className={styles.textContainer}>
 95 |           <div className={styles.title}>Custom render</div>
 96 |           <div className={styles.description}>
 97 |             Apply any styling you wish.
 98 |             <br />
 99 |             For example, render images and highlight the matching string.
100 |           </div>
101 |           <Link
102 |             className={styles.codepenLink}
103 |             href="http://codepen.io/moroshko/pen/PZWbzK"
104 |             underline={false}
105 |           >
106 |             Codepen
107 |           </Link>
108 |         </div>
109 |         <div className={styles.autosuggest}>
110 |           <Autosuggest
111 |             suggestions={suggestions}
112 |             onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
113 |             onSuggestionsClearRequested={this.onSuggestionsClearRequested}
114 |             getSuggestionValue={getSuggestionValue}
115 |             renderSuggestion={renderSuggestion}
116 |             inputProps={inputProps}
117 |             focusInputOnSuggestionClick={focusInputOnSuggestionClick}
118 |             theme={theme}
119 |             id="custom-render-example"
120 |           />
121 |         </div>
122 |       </div>
123 |     );
124 |   }
125 | }
126 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/Examples/components/CustomRender/CustomRender.less:
--------------------------------------------------------------------------------
 1 | @import 'variables';
 2 | 
 3 | .container {
 4 |   display: flex;
 5 |   justify-content: space-between;
 6 |   width: 34 * @columns;
 7 |   margin: (16 * @rows) (0.5 * @column) 0;
 8 | 
 9 |   @media @examples-vertical {
10 |     flex-direction: column;
11 |     width: 14 * @columns;
12 |   }
13 | 
14 |   @media @small {
15 |     margin-top: 12 * @rows;
16 |   }
17 | }
18 | 
19 | .textContainer {
20 |   display: flex;
21 |   flex-direction: column;
22 |   width: 15 * @columns;
23 | }
24 | 
25 | .title {
26 |   font-size: 30px;
27 |   font-weight: 400;
28 |   line-height: 5 * @rows;
29 | 
30 |   @media @small {
31 |     font-size: 25px;
32 |   }
33 | }
34 | 
35 | .description {
36 |   margin-top: @row;
37 |   font-size: 20px;
38 | }
39 | 
40 | .codepenLink {
41 |   margin-top: @row;
42 |   font-size: 20px;
43 |   color: #209FD3;
44 |   align-self: flex-start;
45 | }
46 | 
47 | .autosuggest {
48 |   margin-top: 6 * @rows;
49 | 
50 |   @media @examples-vertical {
51 |     margin-top: 3 * @rows;
52 |     margin-left: 0;
53 |   }
54 | }
55 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/Examples/components/CustomRender/people.js:
--------------------------------------------------------------------------------
 1 | export default [
 2 |   {
 3 |     first: 'Charlie',
 4 |     last: 'Brown',
 5 |     twitter: 'dancounsell'
 6 |   },
 7 |   {
 8 |     first: 'Charlotte',
 9 |     last: 'White',
10 |     twitter: 'mtnmissy'
11 |   },
12 |   {
13 |     first: 'Chloe',
14 |     last: 'Jones',
15 |     twitter: 'ladylexy'
16 |   },
17 |   {
18 |     first: 'Cooper',
19 |     last: 'King',
20 |     twitter: 'steveodom'
21 |   }
22 | ];
23 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/Examples/components/CustomRender/photos/dancounsell.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moroshko/react-autosuggest/e2c84d906afc7b6723d29d1c2283c3f322ee2552/demo/src/components/App/components/Examples/components/CustomRender/photos/dancounsell.jpg


--------------------------------------------------------------------------------
/demo/src/components/App/components/Examples/components/CustomRender/photos/ladylexy.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moroshko/react-autosuggest/e2c84d906afc7b6723d29d1c2283c3f322ee2552/demo/src/components/App/components/Examples/components/CustomRender/photos/ladylexy.jpg


--------------------------------------------------------------------------------
/demo/src/components/App/components/Examples/components/CustomRender/photos/mtnmissy.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moroshko/react-autosuggest/e2c84d906afc7b6723d29d1c2283c3f322ee2552/demo/src/components/App/components/Examples/components/CustomRender/photos/mtnmissy.jpg


--------------------------------------------------------------------------------
/demo/src/components/App/components/Examples/components/CustomRender/photos/steveodom.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moroshko/react-autosuggest/e2c84d906afc7b6723d29d1c2283c3f322ee2552/demo/src/components/App/components/Examples/components/CustomRender/photos/steveodom.jpg


--------------------------------------------------------------------------------
/demo/src/components/App/components/Examples/components/CustomRender/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-family: 'Open Sans', sans-serif;
 13 |   font-weight: 300;
 14 |   font-size: 16px;
 15 |   border: 1px solid @border-color;
 16 |   border-radius: @border-radius;
 17 |   -webkit-appearance: none;
 18 | 
 19 |   &::-ms-clear {
 20 |     display: none;
 21 |   }
 22 | }
 23 | 
 24 | .inputOpen {
 25 |   border-bottom-left-radius: 0;
 26 |   border-bottom-right-radius: 0;
 27 | }
 28 | 
 29 | .inputFocused {
 30 |   outline: none;
 31 | }
 32 | 
 33 | .suggestionsContainer {
 34 |   display: none;
 35 | }
 36 | 
 37 | .suggestionsContainerOpen {
 38 |   display: block;
 39 |   position: absolute;
 40 |   top: 51px;
 41 |   width: 280px;
 42 |   border: 1px solid @border-color;
 43 |   background-color: #fff;
 44 |   font-family: 'Open Sans', sans-serif;
 45 |   font-weight: 300;
 46 |   font-size: 20px;
 47 |   border-bottom-left-radius: @border-radius;
 48 |   border-bottom-right-radius: @border-radius;
 49 |   z-index: 2;
 50 | }
 51 | 
 52 | .suggestionsList {
 53 |   margin: 0;
 54 |   padding: 0;
 55 |   list-style-type: none;
 56 | }
 57 | 
 58 | .suggestion {
 59 |   cursor: pointer;
 60 |   padding: 10px 20px;
 61 |   height: 48px;
 62 |   border-top: 1px solid #ddd;
 63 | }
 64 | 
 65 | .suggestionFirst {
 66 |   border-top: 0;
 67 | }
 68 | 
 69 | .suggestionHighlighted {
 70 |   background-color: #0C7EAF;
 71 |   color: #fff;
 72 | }
 73 | 
 74 | .suggestionContent {
 75 |   display: flex;
 76 |   align-items: center;
 77 |   background-repeat: no-repeat;
 78 | 
 79 |   &.dancounsell {
 80 |     background-image: url(../CustomRender/photos/dancounsell.jpg);
 81 |   }
 82 | 
 83 |   &.ladylexy {
 84 |     background-image: url(../CustomRender/photos/ladylexy.jpg);
 85 |   }
 86 | 
 87 |   &.mtnmissy {
 88 |     background-image: url(../CustomRender/photos/mtnmissy.jpg);
 89 |   }
 90 | 
 91 |   &.steveodom {
 92 |     background-image: url(../CustomRender/photos/steveodom.jpg);
 93 |   }
 94 | }
 95 | 
 96 | .name {
 97 |   margin-left: 68px;
 98 |   line-height: 45px;
 99 | }
100 | 
101 | .highlight {
102 |   color: #ee0000;
103 |   font-weight: 400;
104 | 
105 |   .suggestionHighlighted & {
106 |     color: #120000;
107 |   }
108 | }
109 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/Examples/components/MultipleSections/MultipleSections.js:
--------------------------------------------------------------------------------
  1 | import styles from './MultipleSections.less';
  2 | import theme from './theme.less';
  3 | 
  4 | import React, { Component } from 'react';
  5 | import isMobile from 'ismobilejs';
  6 | import Link from 'Link/Link';
  7 | import Autosuggest from 'Autosuggest';
  8 | import languages from './languages';
  9 | import { escapeRegexCharacters } from 'utils/utils';
 10 | 
 11 | const focusInputOnSuggestionClick = !isMobile.any;
 12 | 
 13 | const getSuggestions = value => {
 14 |   const escapedValue = escapeRegexCharacters(value.trim());
 15 | 
 16 |   if (escapedValue === '') {
 17 |     return [];
 18 |   }
 19 | 
 20 |   const regex = new RegExp('^' + escapedValue, 'i');
 21 | 
 22 |   return languages
 23 |     .map(section => {
 24 |       return {
 25 |         title: section.title,
 26 |         languages: section.languages.filter(language =>
 27 |           regex.test(language.name)
 28 |         )
 29 |       };
 30 |     })
 31 |     .filter(section => section.languages.length > 0);
 32 | };
 33 | 
 34 | const getSuggestionValue = suggestion => suggestion.name;
 35 | 
 36 | const renderSuggestion = suggestion => <span>{suggestion.name}</span>;
 37 | 
 38 | const renderSectionTitle = section => <strong>{section.title}</strong>;
 39 | 
 40 | const getSectionSuggestions = section => section.languages;
 41 | 
 42 | export default class MultipleSections extends Component {
 43 |   constructor() {
 44 |     super();
 45 | 
 46 |     this.state = {
 47 |       value: '',
 48 |       suggestions: []
 49 |     };
 50 |   }
 51 | 
 52 |   onChange = (event, { newValue }) => {
 53 |     this.setState({
 54 |       value: newValue
 55 |     });
 56 |   };
 57 | 
 58 |   onSuggestionsFetchRequested = ({ value }) => {
 59 |     this.setState({
 60 |       suggestions: getSuggestions(value)
 61 |     });
 62 |   };
 63 | 
 64 |   onSuggestionsClearRequested = () => {
 65 |     this.setState({
 66 |       suggestions: []
 67 |     });
 68 |   };
 69 | 
 70 |   render() {
 71 |     const { value, suggestions } = this.state;
 72 |     const inputProps = {
 73 |       placeholder: "Type 'c'",
 74 |       value,
 75 |       onChange: this.onChange
 76 |     };
 77 | 
 78 |     return (
 79 |       <div id="multiple-sections-example" className={styles.container}>
 80 |         <div className={styles.textContainer}>
 81 |           <div className={styles.title}>Multiple sections</div>
 82 |           <div className={styles.description}>
 83 |             Suggestions can also be presented in multiple sections. Note that we
 84 |             highlight the first suggestion by default here.
 85 |           </div>
 86 |           <Link
 87 |             className={styles.codepenLink}
 88 |             href="http://codepen.io/moroshko/pen/qbRNjV"
 89 |             underline={false}
 90 |           >
 91 |             Codepen
 92 |           </Link>
 93 |         </div>
 94 |         <div className={styles.autosuggest}>
 95 |           <Autosuggest
 96 |             multiSection={true}
 97 |             suggestions={suggestions}
 98 |             onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
 99 |             onSuggestionsClearRequested={this.onSuggestionsClearRequested}
100 |             getSuggestionValue={getSuggestionValue}
101 |             renderSuggestion={renderSuggestion}
102 |             renderSectionTitle={renderSectionTitle}
103 |             getSectionSuggestions={getSectionSuggestions}
104 |             inputProps={inputProps}
105 |             highlightFirstSuggestion={true}
106 |             focusInputOnSuggestionClick={focusInputOnSuggestionClick}
107 |             theme={theme}
108 |             id="multiple-sections-example"
109 |           />
110 |         </div>
111 |       </div>
112 |     );
113 |   }
114 | }
115 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/Examples/components/MultipleSections/MultipleSections.less:
--------------------------------------------------------------------------------
 1 | @import 'variables';
 2 | 
 3 | .container {
 4 |   display: flex;
 5 |   justify-content: space-between;
 6 |   width: 34 * @columns;
 7 |   margin: (16 * @rows) (0.5 * @column) 0;
 8 | 
 9 |   @media @examples-vertical {
10 |     flex-direction: column;
11 |     width: 14 * @columns;
12 |   }
13 | 
14 |   @media @small {
15 |     margin-top: 12 * @rows;
16 |   }
17 | }
18 | 
19 | .textContainer {
20 |   display: flex;
21 |   flex-direction: column;
22 |   width: 15 * @columns;
23 | }
24 | 
25 | .title {
26 |   font-size: 30px;
27 |   font-weight: 400;
28 |   line-height: 5 * @rows;
29 | 
30 |   @media @small {
31 |     font-size: 25px;
32 |   }
33 | }
34 | 
35 | .description {
36 |   margin-top: @row;
37 |   font-size: 20px;
38 | }
39 | 
40 | .codepenLink {
41 |   margin-top: @row;
42 |   font-size: 20px;
43 |   color: #209FD3;
44 |   align-self: flex-start;
45 | }
46 | 
47 | .autosuggest {
48 |   margin-top: 6 * @rows;
49 | 
50 |   @media @examples-vertical {
51 |     margin-top: 3 * @rows;
52 |     margin-left: 0;
53 |   }
54 | }
55 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/Examples/components/MultipleSections/languages.js:
--------------------------------------------------------------------------------
 1 | export default [
 2 |   {
 3 |     title: '1970s',
 4 |     languages: [
 5 |       {
 6 |         name: 'C',
 7 |         year: 1972
 8 |       }
 9 |     ]
10 |   },
11 |   {
12 |     title: '1980s',
13 |     languages: [
14 |       {
15 |         name: 'C++',
16 |         year: 1983
17 |       },
18 |       {
19 |         name: 'Perl',
20 |         year: 1987
21 |       }
22 |     ]
23 |   },
24 |   {
25 |     title: '1990s',
26 |     languages: [
27 |       {
28 |         name: 'Haskell',
29 |         year: 1990
30 |       },
31 |       {
32 |         name: 'Python',
33 |         year: 1991
34 |       },
35 |       {
36 |         name: 'Java',
37 |         year: 1995
38 |       },
39 |       {
40 |         name: 'JavaScript',
41 |         year: 1995
42 |       },
43 |       {
44 |         name: 'PHP',
45 |         year: 1995
46 |       },
47 |       {
48 |         name: 'Ruby',
49 |         year: 1995
50 |       }
51 |     ]
52 |   },
53 |   {
54 |     title: '2000s',
55 |     languages: [
56 |       {
57 |         name: 'C#',
58 |         year: 2000
59 |       },
60 |       {
61 |         name: 'Scala',
62 |         year: 2003
63 |       },
64 |       {
65 |         name: 'Clojure',
66 |         year: 2007
67 |       },
68 |       {
69 |         name: 'Go',
70 |         year: 2009
71 |       }
72 |     ]
73 |   },
74 |   {
75 |     title: '2010s',
76 |     languages: [
77 |       {
78 |         name: 'Elm',
79 |         year: 2012
80 |       }
81 |     ]
82 |   }
83 | ];
84 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/Examples/components/MultipleSections/theme.less:
--------------------------------------------------------------------------------
 1 | @font-size: 16px;
 2 | @border-color: #aaa;
 3 | @border-radius: 4px;
 4 | 
 5 | .container {
 6 |   position: relative;
 7 | }
 8 | 
 9 | .input {
10 |   width: 240px;
11 |   height: 30px;
12 |   padding: 10px 20px;
13 |   font-family: 'Open Sans', sans-serif;
14 |   font-weight: 300;
15 |   font-size: @font-size;
16 |   border: 1px solid @border-color;
17 |   border-radius: @border-radius;
18 |   -webkit-appearance: none;
19 | 
20 |   &::-ms-clear {
21 |     display: none;
22 |   }
23 | }
24 | 
25 | .inputOpen {
26 |   border-bottom-left-radius: 0;
27 |   border-bottom-right-radius: 0;
28 | }
29 | 
30 | .inputFocused {
31 |   outline: none;
32 | }
33 | 
34 | .suggestionsContainer {
35 |   display: none;
36 | }
37 | 
38 | .suggestionsContainerOpen {
39 |   display: block;
40 |   position: absolute;
41 |   top: 51px;
42 |   width: 280px;
43 |   border: 1px solid @border-color;
44 |   background-color: #fff;
45 |   font-family: 'Open Sans', sans-serif;
46 |   font-weight: 300;
47 |   font-size: @font-size;
48 |   border-bottom-left-radius: @border-radius;
49 |   border-bottom-right-radius: @border-radius;
50 |   z-index: 2;
51 | }
52 | 
53 | .suggestionsList {
54 |   margin: 0;
55 |   padding: 0;
56 |   list-style-type: none;
57 | }
58 | 
59 | .suggestion {
60 |   cursor: pointer;
61 |   padding: 10px 20px;
62 | }
63 | 
64 | .suggestionHighlighted {
65 |   background-color: #ddd;
66 | }
67 | 
68 | .sectionContainer {
69 |   border-top: 1px dashed #ccc;
70 | }
71 | 
72 | .sectionContainerFirst {
73 |   border-top: 0;
74 | }
75 | 
76 | .sectionTitle {
77 |   padding: 10px 0 0 10px;
78 |   font-size: 12px;
79 |   color: #777;
80 | }
81 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/Examples/components/ScrollableContainer/ScrollableContainer.js:
--------------------------------------------------------------------------------
  1 | import styles from './ScrollableContainer.less';
  2 | import theme from './theme.less';
  3 | 
  4 | import React, { Component } from 'react';
  5 | import Modal from 'react-modal';
  6 | import Autosuggest from 'Autosuggest';
  7 | import countries from './countries';
  8 | import { escapeRegexCharacters } from 'utils/utils';
  9 | 
 10 | const getSuggestions = value => {
 11 |   const escapedValue = escapeRegexCharacters(value.trim());
 12 | 
 13 |   if (escapedValue === '') {
 14 |     return countries;
 15 |   }
 16 | 
 17 |   const regex = new RegExp('^' + escapedValue, 'i');
 18 | 
 19 |   return countries.filter(country => regex.test(country.name));
 20 | };
 21 | 
 22 | const getSuggestionValue = suggestion => suggestion.name;
 23 | 
 24 | const renderSuggestion = suggestion => suggestion.name;
 25 | 
 26 | const modalStyle = {
 27 |   overlay: {
 28 |     position: 'fixed',
 29 |     top: 0,
 30 |     left: 0,
 31 |     right: 0,
 32 |     bottom: 0,
 33 |     backgroundColor: '#fff',
 34 |     fontFamily: '"Open Sans", sans-serif'
 35 |   },
 36 |   content: {
 37 |     position: 'absolute',
 38 |     top: 0,
 39 |     left: 0,
 40 |     right: 0,
 41 |     bottom: 0,
 42 |     border: 0,
 43 |     padding: 0,
 44 |     overflow: 'hidden',
 45 |     outline: 'none',
 46 |     height: '100%'
 47 |   }
 48 | };
 49 | 
 50 | export default class ScrollableContainer extends Component {
 51 |   constructor() {
 52 |     super();
 53 | 
 54 |     this.state = {
 55 |       isModalOpen: false,
 56 |       selected: countries.filter(country => country.name === 'Australia')[0],
 57 |       value: '',
 58 |       suggestions: countries
 59 |     };
 60 |   }
 61 | 
 62 |   openModal = () => {
 63 |     this.setState({
 64 |       isModalOpen: true,
 65 |       value: '',
 66 |       suggestions: countries
 67 |     });
 68 |   };
 69 | 
 70 |   closeModal = () => {
 71 |     this.setState({
 72 |       isModalOpen: false
 73 |     });
 74 |   };
 75 | 
 76 |   onChange = (event, { newValue }) => {
 77 |     this.setState({
 78 |       value: newValue
 79 |     });
 80 |   };
 81 | 
 82 |   onSuggestionsFetchRequested = ({ value }) => {
 83 |     this.setState({
 84 |       suggestions: getSuggestions(value)
 85 |     });
 86 |   };
 87 | 
 88 |   onSuggestionSelected = (event, { suggestion }) => {
 89 |     this.setState({
 90 |       isModalOpen: false,
 91 |       selected: suggestion
 92 |     });
 93 |   };
 94 | 
 95 |   render() {
 96 |     const { isModalOpen, selected, value, suggestions } = this.state;
 97 |     const inputProps = {
 98 |       placeholder: 'Type to filter',
 99 |       value,
100 |       onChange: this.onChange
101 |     };
102 | 
103 |     return (
104 |       <div id="scrollable-container-example" className={styles.container}>
105 |         <div className={styles.textContainer}>
106 |           <div className={styles.title}>Scrollable container</div>
107 |           <div className={styles.description}>
108 |             When the suggestions list is long, you may want to make it
109 |             scrollable. Note that the suggestions are rendered even when the
110 |             input field is not focused.
111 |           </div>
112 |         </div>
113 |         <div className={styles.demoContainer}>
114 |           <div className={styles.question}>Where do you live?</div>
115 |           <div className={styles.answer}>{selected.name}</div>
116 |           <button className={styles.editButton} onClick={this.openModal}>
117 |             Edit
118 |           </button>
119 |         </div>
120 |         <Modal
121 |           isOpen={isModalOpen}
122 |           contentLabel="Modal"
123 |           onRequestClose={this.closeModal}
124 |           shouldCloseOnOverlayClick={false}
125 |           closeTimeoutMS={
126 |             1 /* otherwise the modal is not closed when suggestion is selected by pressing Enter */
127 |           }
128 |           style={modalStyle}
129 |         >
130 |           <div className={styles.modalTitle}>Please select a country:</div>
131 |           <div className={styles.modalBody}>
132 |             <Autosuggest
133 |               suggestions={suggestions}
134 |               onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
135 |               onSuggestionSelected={this.onSuggestionSelected}
136 |               getSuggestionValue={getSuggestionValue}
137 |               renderSuggestion={renderSuggestion}
138 |               inputProps={inputProps}
139 |               alwaysRenderSuggestions={true}
140 |               theme={theme}
141 |               id="scrollable-container-example"
142 |             />
143 |           </div>
144 |           <button className={styles.cancelButton} onClick={this.closeModal}>
145 |             Cancel
146 |           </button>
147 |         </Modal>
148 |       </div>
149 |     );
150 |   }
151 | }
152 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/Examples/components/ScrollableContainer/ScrollableContainer.less:
--------------------------------------------------------------------------------
 1 | @import 'variables';
 2 | 
 3 | .container {
 4 |   display: flex;
 5 |   justify-content: space-between;
 6 |   width: 34 * @columns;
 7 |   margin: (16 * @rows) (0.5 * @column) 0;
 8 | 
 9 |   @media @examples-vertical {
10 |     flex-direction: column;
11 |     width: 14 * @columns;
12 |   }
13 | 
14 |   @media @small {
15 |     margin-top: 12 * @rows;
16 |   }
17 | }
18 | 
19 | .textContainer {
20 |   display: flex;
21 |   flex-direction: column;
22 |   width: 15 * @columns;
23 | }
24 | 
25 | .title {
26 |   font-size: 30px;
27 |   font-weight: 400;
28 |   line-height: 5 * @rows;
29 | 
30 |   @media @small {
31 |     font-size: 25px;
32 |   }
33 | }
34 | 
35 | .description {
36 |   margin-top: @row;
37 |   font-size: 20px;
38 | }
39 | 
40 | .demoContainer {
41 |   margin-top: 6 * @rows;
42 |   width: 14 * @columns;
43 | 
44 |   @media @examples-vertical {
45 |     margin-top: 3 * @rows;
46 |     margin-left: 0;
47 |   }
48 | }
49 | 
50 | .question {
51 |   font-size: 20px;
52 |   font-weight: 400;
53 | }
54 | 
55 | .answer {
56 |   margin-top: 2px;
57 |   line-height: 3 * @rows;
58 | }
59 | 
60 | .button {
61 |   font-family: inherit;
62 |   font-size: 16px;
63 |   font-weight: 300;
64 |   color: #209FD3;
65 |   background: none;
66 |   border: 0;
67 |   padding: 0;
68 |   cursor: pointer;
69 | 
70 |   &:hover {
71 |     text-decoration: underline;
72 |   }
73 | }
74 | 
75 | .editButton {
76 |   composes: button;
77 |   margin-top: 11px;
78 | }
79 | 
80 | .modalTitle {
81 |   margin: 5 * @rows auto 0;
82 |   width: 282px;
83 |   font-size: 20px;
84 |   font-weight: 400;
85 | }
86 | 
87 | .modalBody {
88 |   margin-top: 2 * @rows;
89 |   height: calc(~"100%" - 16 * @rows);
90 | }
91 | 
92 | .cancelButton {
93 |   composes: button;
94 |   position: absolute;
95 |   top: 2 * @rows;
96 |   right: @column;
97 | }
98 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/Examples/components/ScrollableContainer/theme.less:
--------------------------------------------------------------------------------
 1 | @font-size: 16px;
 2 | @border-color: #aaa;
 3 | @border-radius: 4px;
 4 | 
 5 | .container {
 6 |   position: relative;
 7 |   height: 100%;
 8 |   width: 282px;
 9 |   margin: 0 auto;
10 | }
11 | 
12 | .input {
13 |   width: 240px;
14 |   height: 30px;
15 |   padding: 10px 20px;
16 |   font-family: 'Open Sans', sans-serif;
17 |   font-weight: 300;
18 |   font-size: @font-size;
19 |   border: 1px solid @border-color;
20 |   border-radius: @border-radius;
21 |   -webkit-appearance: none;
22 | 
23 |   &::-ms-clear {
24 |     display: none;
25 |   }
26 | }
27 | 
28 | .inputOpen {
29 |   border-bottom-left-radius: 0;
30 |   border-bottom-right-radius: 0;
31 | }
32 | 
33 | .inputFocused {
34 |   outline: none;
35 | }
36 | 
37 | .suggestionsContainer {
38 |   display: none;
39 | }
40 | 
41 | .suggestionsContainerOpen {
42 |   display: block;
43 |   position: absolute;
44 |   top: 51px;
45 |   width: 280px;
46 |   border: 1px solid @border-color;
47 |   background-color: #fff;
48 |   font-family: 'Open Sans', sans-serif;
49 |   font-weight: 300;
50 |   font-size: @font-size;
51 |   border-bottom-left-radius: @border-radius;
52 |   border-bottom-right-radius: @border-radius;
53 |   z-index: 2;
54 |   max-height: calc(~"100%" - 52px);
55 |   overflow-y: auto;
56 | }
57 | 
58 | .suggestionsList {
59 |   margin: 0;
60 |   padding: 0;
61 |   list-style-type: none;
62 | }
63 | 
64 | .suggestion {
65 |   cursor: pointer;
66 |   padding: 10px 20px;
67 | }
68 | 
69 | .suggestionHighlighted {
70 |   background-color: #ddd;
71 | }
72 | 
73 | .sectionContainer {
74 |   border-top: 1px dashed #ccc;
75 | }
76 | 
77 | .sectionContainerFirst {
78 |   border-top: 0;
79 | }
80 | 
81 | .sectionTitle {
82 |   padding: 10px 0 0 10px;
83 |   font-size: 12px;
84 |   color: #777;
85 | }
86 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/Features/Features.js:
--------------------------------------------------------------------------------
 1 | import styles from './Features.less';
 2 | 
 3 | import React from 'react';
 4 | import Link from 'Link/Link';
 5 | 
 6 | const Features = () => (
 7 |   <div className={styles.container}>
 8 |     <h2 className={styles.header}>Features</h2>
 9 |     <div className={styles.content}>
10 |       <div className={styles.feature}>
11 |         <div className={styles.accessibleIcon} />
12 |         <div className={styles.featureName}>Accessible</div>
13 |         <div className={styles.featureDescription}>
14 |           <Link
15 |             className={styles.link}
16 |             href="https://rawgit.com/w3c/aria-practices/master/aria-practices-DeletedSectionsArchive.html#autocomplete"
17 |           >
18 |             WAI-ARIA compliant
19 |           </Link>
20 |           , with support for ARIA attributes and keyboard interactions.
21 |         </div>
22 |       </div>
23 |       <div className={styles.feature}>
24 |         <div className={styles.mobileFriendlyIcon} />
25 |         <div className={styles.featureName}>Mobile friendly</div>
26 |         <div className={styles.featureDescription}>
27 |           Works well on those little devices you carry around in your hands.
28 |         </div>
29 |       </div>
30 |       <div className={styles.feature}>
31 |         <div className={styles.customizableIcon} />
32 |         <div className={styles.featureName}>Customizable</div>
33 |         <div className={styles.featureDescription}>
34 |           Supports custom suggestion rendering, multiple sections, and more.
35 |         </div>
36 |       </div>
37 |     </div>
38 |     <div className={styles.footer}>
39 |       {'Check out the '}
40 |       <Link
41 |         className={styles.link}
42 |         href="https://github.com/moroshko/react-autosuggest#features"
43 |       >
44 |         GitHub page
45 |       </Link>
46 |       {' for a full list of features.'}
47 |     </div>
48 |   </div>
49 | );
50 | 
51 | export default Features;
52 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/Features/Features.less:
--------------------------------------------------------------------------------
  1 | @import 'variables';
  2 | 
  3 | @vertical: ~"(max-width: 859px)";
  4 | 
  5 | .container {
  6 |   display: flex;
  7 |   flex-direction: column;
  8 |   align-items: center;
  9 |   padding: (11 * @rows) 0;
 10 |   color: #fff;
 11 |   background-color: #009EDC;
 12 | 
 13 |   @media @small {
 14 |     padding: (7 * @rows) 0;
 15 |   }
 16 | }
 17 | 
 18 | .header {
 19 |   margin: 0;
 20 |   font-size: 50px;
 21 |   font-weight: 300;
 22 | 
 23 |   @media @small {
 24 |     font-size: 34px;
 25 |   }
 26 | }
 27 | 
 28 | .content {
 29 |   display: flex;
 30 |   margin-top: 8 * @rows;
 31 | 
 32 |   @media @vertical {
 33 |     flex-direction: column;
 34 |     margin-top: 0;
 35 |     width: 60%;
 36 |   }
 37 | 
 38 |   @media @small {
 39 |     width: 90%;
 40 |   }
 41 | }
 42 | 
 43 | .feature {
 44 |   display: flex;
 45 |   flex-direction: column;
 46 |   align-items: center;
 47 |   width: 14.5 * @columns;
 48 |   text-align: center;
 49 | 
 50 |   @media @vertical {
 51 |     margin-top: 16 * @rows;
 52 |     width: auto;
 53 | 
 54 |     &:first-child {
 55 |       margin-top: 9 * @rows;
 56 |     }
 57 |   }
 58 | 
 59 |   @media @small {
 60 |     margin-top: 12 * @rows;
 61 | 
 62 |     &:first-child {
 63 |       margin-top: 7 * @rows;
 64 |     }
 65 |   }
 66 | }
 67 | 
 68 | .icon {
 69 |   background-repeat: no-repeat;
 70 |   background-size: 100%;
 71 | }
 72 | 
 73 | .accessibleIcon {
 74 |   @width: 98px;
 75 |   @height: 130px;
 76 |   @xOffset: 3px;
 77 |   @yOffset: 5px;
 78 | 
 79 |   composes: icon;
 80 |   width: @width;
 81 |   height: @height;
 82 |   background-image: url(../Features/accessible.svg); // https://github.com/webpack/css-loader/issues/74
 83 |   transform: translateX(@xOffset) translateY(@yOffset);
 84 | 
 85 |   @media @small {
 86 |     width: @width * 0.7;
 87 |     height: @height * 0.7;
 88 |     transform: translateX(@xOffset * 0.7) translateY(@yOffset * 0.7);
 89 |   }
 90 | }
 91 | 
 92 | .mobileFriendlyIcon {
 93 |   @width: 105px;
 94 |   @height: 130px;
 95 |   @offset: 25px;
 96 | 
 97 |   composes: icon;
 98 |   width: @width;
 99 |   height: @height;
100 |   background-image: url(../Features/mobile-friendly.svg); // https://github.com/webpack/css-loader/issues/74
101 |   transform: translateX(@offset);
102 | 
103 |   @media @small {
104 |     width: @width * 0.7;
105 |     height: @height * 0.7;
106 |     transform: translateX(@offset * 0.7);
107 |   }
108 | }
109 | 
110 | .customizableIcon {
111 |   @width: 108px;
112 |   @height: 130px;
113 |   @offset: 9px;
114 | 
115 |   composes: icon;
116 |   width: @width;
117 |   height: @height;
118 |   background-image: url(../Features/customizable.svg); // https://github.com/webpack/css-loader/issues/74
119 |   transform: translateX(@offset);
120 | 
121 |   @media @small {
122 |     width: @width * 0.7;
123 |     height: @height * 0.7;
124 |     transform: translateX(@offset * 0.7);
125 |   }
126 | }
127 | 
128 | .featureName {
129 |   font-size: 30px;
130 |   font-weight: 400;
131 |   line-height: 5 * @rows;
132 | 
133 |   @media @small {
134 |     font-size: 25px;
135 |   }
136 | }
137 | 
138 | .link {
139 |   color: #fff;
140 | }
141 | 
142 | .featureDescription {
143 |   margin: @row 18px 0;
144 |   font-size: 20px;
145 |   max-width: 100%; // IE11 fix. See: http://stackoverflow.com/q/35111090/247243
146 | }
147 | 
148 | .footer {
149 |   margin: (8 * @rows) (0.5 * @column) 0;
150 |   font-size: 20px;
151 |   text-align: center;
152 | }
153 | 
154 | .footerLink {
155 |   color: #fff;
156 | }
157 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/Features/accessible.svg:
--------------------------------------------------------------------------------
 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 2 | <svg width="98px" height="92px" viewBox="0 0 98 92" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
 3 |     <!-- Generator: Sketch 3.4.3 (16044) - http://www.bohemiancoding.com/sketch -->
 4 |     <title>icon_accessible</title>
 5 |     <desc>Created with Sketch.</desc>
 6 |     <defs>
 7 |         <linearGradient x1="8.98232292%" y1="22.4745086%" x2="55.800063%" y2="63.2242015%" id="linearGradient-1">
 8 |             <stop stop-color="#000000" offset="0%"></stop>
 9 |             <stop stop-color="#000000" stop-opacity="0" offset="100%"></stop>
10 |         </linearGradient>
11 |     </defs>
12 |     <g id="icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
13 |         <g sketch:type="MSArtboardGroup" transform="translate(-355.000000, -419.000000)" id="icon_accessible">
14 |             <g sketch:type="MSLayerGroup" transform="translate(300.000000, 419.000000)">
15 |                 <rect id="background-padding" fill-opacity="0" fill="#D8D8D8" sketch:type="MSShapeGroup" x="0" y="0" width="200" height="145"></rect>
16 |                 <path d="M91,60.5 L140.200012,107 L171.200012,76 L123.050018,28 C123.050018,28 91.0200184,60.051 91,60.5 L91,60.5 Z" id="shadow" opacity="0.12" fill="url(#linearGradient-1)" sketch:type="MSShapeGroup"></path>
17 |                 <path d="M100.043628,0 C106.140281,0 111.901985,1.1559375 117.328738,3.470625 C122.752677,5.7825 127.501086,8.9465625 131.576781,12.9628125 C135.64966,16.97625 138.892202,21.67875 141.295961,27.0703125 C143.69972,32.4590625 144.929747,38.233125 144.99167,44.3840625 C145.107073,50.5940625 144.023411,56.4525 141.73787,61.9621875 C139.449514,67.4690625 136.297043,72.2728125 132.280457,76.37625 C128.261057,80.476875 123.529536,83.7421875 118.074635,86.1778125 C112.62255,88.605 106.787664,89.8790625 100.572793,90 L100.30821,90 L100.043628,90 C93.8878656,90 88.0952004,88.84125 82.6712617,86.529375 C77.2445083,84.211875 72.496099,81.0478125 68.4232193,77.0371875 C64.3475248,73.0209375 61.1049833,68.3184375 58.704039,62.9296875 C56.2974652,57.538125 55.0674387,51.766875 55.0083298,45.6159375 C54.8929269,39.403125 55.9765887,33.5446875 58.2621301,28.0378125 C60.5504862,22.528125 63.7001424,17.7103125 67.7195427,13.57875 C71.7361284,9.4471875 76.4704641,6.165 81.9253646,3.735 C87.3774503,1.3021875 93.2095215,0.05625 99.4272073,0 L99.6917897,0 L100.043628,0 L100.043628,0 L100.043628,0 Z M100.04774,3.2734375 L99.7254756,3.2734375 L99.4948642,3.2734375 C96.6595264,3.33251714 93.8862762,3.65745519 91.1780703,4.24234366 C88.4669079,4.82427815 85.8207897,5.67207104 83.2338026,6.77981436 C80.7680344,7.82552406 78.4441808,9.11641428 76.2563289,10.658393 C74.068477,12.1944637 72.0225398,13.9491291 70.1126042,15.9194353 C68.2026686,17.8277078 66.5085616,19.9043572 65.0302833,22.1523377 C63.552005,24.3944102 62.3191208,26.7812278 61.3345875,29.3039286 C60.2879664,31.888663 59.5015223,34.535431 58.9811684,37.2442327 C58.4549013,39.9500804 58.2242899,42.7179617 58.2863776,45.5508306 C58.2863776,48.3216659 58.5790767,51.0600074 59.1644749,53.7688091 C59.7469165,56.4746568 60.5954483,59.088931 61.7041571,61.6145858 C62.7507781,64.075253 64.0309671,66.397083 65.538811,68.5830298 C67.0466549,70.7689766 68.7555447,72.8131323 70.6684368,74.7214048 C72.5754159,76.5676437 74.6390924,78.2307356 76.8565099,79.7077267 C79.0739274,81.1847178 81.4125637,82.4460682 83.878332,83.4917779 C86.465319,84.5374876 89.099611,85.3380168 91.7782513,85.8933655 C94.4598482,86.4457601 97.2124025,86.7234344 100.04774,86.7234344 L100.278352,86.7234344 L100.603573,86.7234344 C103.373866,86.6584468 106.114594,86.3364628 108.825757,85.7545283 C111.533963,85.1696398 114.183038,84.3218469 116.770025,83.2141036 C119.232836,82.1063603 121.574429,80.7977462 123.791847,79.2912153 C126.009264,77.7846843 128.043375,76.0447888 129.891223,74.0774367 C131.798202,72.1071306 133.495266,70.0157112 134.973544,67.8002245 C136.451822,65.5847379 137.68175,63.2126902 138.66924,60.6899894 C139.712904,58.105255 140.499348,55.4584869 141.025615,52.7526392 C141.545969,50.0438376 141.776581,47.2730023 141.71745,44.4430873 C141.71745,41.6102184 141.421794,38.860061 140.839352,36.1837531 C140.253954,33.5044913 139.408379,30.9020329 138.29967,28.3822861 C137.250093,25.918665 135.969904,23.596835 134.465016,21.4138421 C132.954216,19.2249413 131.245326,17.1807857 129.33539,15.2725132 C127.425455,13.4262743 125.364735,11.7661363 123.147317,10.2891452 C120.9299,8.81215411 118.588307,7.54784973 116.125495,6.50509402 C113.538508,5.45643034 110.904216,4.65885515 108.225576,4.1035065 C105.546936,3.55111183 102.82099,3.2734375 100.04774,3.2734375 L100.04774,3.2734375 L100.04774,3.2734375 Z M125.720225,30.8671875 L122.850004,28 L93.4729163,57.44125 L78.8702209,43.7303125 L76,46.6875 L93.5601722,63.0884375 L125.720225,30.8671875 L125.720225,30.8671875 Z" id="Icon/Seek-icons/Circle---tick" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
18 |             </g>
19 |         </g>
20 |     </g>
21 | </svg>


--------------------------------------------------------------------------------
/demo/src/components/App/components/Features/customizable.svg:
--------------------------------------------------------------------------------
 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 2 | <svg width="108px" height="116px" viewBox="0 0 108 116" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
 3 |     <!-- Generator: Sketch 3.4.3 (16044) - http://www.bohemiancoding.com/sketch -->
 4 |     <title>icon_customisable copy</title>
 5 |     <desc>Created with Sketch.</desc>
 6 |     <defs>
 7 |         <linearGradient x1="-20.0229943%" y1="23.5002149%" x2="66.6460222%" y2="78.3019983%" id="linearGradient-1">
 8 |             <stop stop-color="#000000" offset="0%"></stop>
 9 |             <stop stop-color="#000000" stop-opacity="0" offset="100%"></stop>
10 |         </linearGradient>
11 |     </defs>
12 |     <g id="icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
13 |         <g sketch:type="MSArtboardGroup" transform="translate(-605.000000, -419.000000)" id="icon_customisable-copy">
14 |             <g sketch:type="MSLayerGroup" transform="translate(550.000000, 419.000000)">
15 |                 <rect id="background-padding" fill-opacity="0" fill="#D8D8D8" sketch:type="MSShapeGroup" x="0" y="0" width="200" height="146"></rect>
16 |                 <path d="M131,87.5 L113.058156,64 C113.061709,64.005244 102.415698,64.1955307 102.415698,64.1955307 L112,66 L112,73 C112,73 106.505084,75.3072626 104.650335,75.3072626 L87,75.3072626 L131,132 L131,87.5 Z" id="Shape" opacity="0.1" fill="url(#linearGradient-1)" sketch:type="MSShapeGroup"></path>
17 |                 <path d="M162.700012,62.7999878 L144.758169,39.2999878 C144.761721,39.3052318 134.115711,39.4955185 134.115711,39.4955185 L143.700012,41.2999878 L143.700012,48.2999878 C143.700012,48.2999878 138.205096,50.6072504 136.350347,50.6072504 L118.700012,50.6072504 L162.700012,107.299988 L162.700012,62.7999878 Z" id="Shape-Copy-2" opacity="0.1" fill="url(#linearGradient-1)" sketch:type="MSShapeGroup"></path>
18 |                 <path d="M99,38.5 L81.0581564,15 C81.0617088,15.005244 70.4156983,15.1955307 70.4156983,15.1955307 L79.9999998,17 L79.9999998,24 C79.9999998,24 74.5050838,26.3072626 72.6503352,26.3072626 L55,26.3072626 L99,83 L99,38.5 Z" id="Shape" opacity="0.1" fill="url(#linearGradient-1)" sketch:type="MSShapeGroup"></path>
19 |                 <path d="M78.5,17.75 L78.5,23.5 L57.75,23.5 L57.75,17.75 L78.5,17.75 L78.5,17.75 Z M77.5,15 L58.75,15 L55,15 L55,18.75 L55,22.5 L55,26.25 L58.75,26.25 L77.5,26.25 L81.25,26.25 L81.25,22.5 L81.25,18.75 L81.25,15 L77.5,15 Z M110.375,66.5 L110.375,72.25 L89.625,72.25 L89.625,66.5 L110.375,66.5 L110.375,66.5 Z M109.375,63.75 L90.625,63.75 L86.875,63.75 L86.875,67.5 L86.875,71.25 L86.875,75 L90.625,75 L109.375,75 L113.125,75 L113.125,71.25 L113.125,67.5 L113.125,63.75 L109.375,63.75 Z M142.25,42.125 L142.25,47.875 L121.5,47.875 L121.5,42.125 L142.25,42.125 L142.25,42.125 Z M145,39.375 L141.25,39.375 L122.5,39.375 L118.75,39.375 L118.75,43.125 L118.75,46.875 L118.75,50.625 L122.5,50.625 L141.25,50.625 L145,50.625 L145,46.875 L145,43.125 L145,39.375 L145,39.375 L145,39.375 Z M67.25,90 L70.25,90 L70.25,26.25 L67.25,26.25 L67.25,90 L67.25,90 Z M67.25,0 L67.25,15 L70.25,15 L70.25,0 L67.25,0 L67.25,0 Z M99.125,90 L102.125,90 L102.125,75 L99.125,75 L99.125,90 L99.125,90 Z M99.125,0 L99.125,63.75 L102.125,63.75 L102.125,0 L99.125,0 L99.125,0 Z M131,89.9999771 L134,89.9999771 L134,50.5999756 L131,50.5999756 L131,89.9999771 L131,89.9999771 Z M131,0 L131,39.4000015 L134,39.4000015 L134,0 L131,0 L131,0 Z" id="Shape" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
20 |             </g>
21 |         </g>
22 |     </g>
23 | </svg>


--------------------------------------------------------------------------------
/demo/src/components/App/components/Features/mobile-friendly.svg:
--------------------------------------------------------------------------------
 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 2 | <svg width="105px" height="130px" viewBox="0 0 105 130" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
 3 |     <!-- Generator: Sketch 3.4.3 (16044) - http://www.bohemiancoding.com/sketch -->
 4 |     <title>icon_mobile-friendly</title>
 5 |     <desc>Created with Sketch.</desc>
 6 |     <defs>
 7 |         <linearGradient x1="20.017619%" y1="32.5244759%" x2="61.6842857%" y2="60.1523072%" id="linearGradient-1">
 8 |             <stop stop-color="#000000" offset="0%"></stop>
 9 |             <stop stop-color="#000000" stop-opacity="0" offset="100%"></stop>
10 |         </linearGradient>
11 |     </defs>
12 |     <g id="icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
13 |         <g sketch:type="MSArtboardGroup" transform="translate(-122.000000, -419.000000)" id="icon_mobile-friendly">
14 |             <g sketch:type="MSLayerGroup" transform="translate(50.000000, 419.000000)">
15 |                 <rect id="background-padding" fill-opacity="0" fill="#D8D8D8" sketch:type="MSShapeGroup" x="0" y="0" width="200" height="146"></rect>
16 |                 <path d="M126.458,2 L126.095543,87.6174044 L76,88.259201 L73,86.2744889 L131.968857,146 L196,70.2592 L126.458,2 Z" id="shadow" opacity="0.1" fill="url(#linearGradient-1)" sketch:type="MSShapeGroup"></path>
17 |                 <path d="M108.606,5.613 L91.494,5.613 C90.713,5.613 90.079,6.247 90.079,7.027 C90.079,7.807 90.713,8.441 91.494,8.441 L108.606,8.441 C109.387,8.441 110.021,7.807 110.021,7.027 C110.021,6.247 109.387,5.613 108.606,5.613 L108.606,5.613 L108.606,5.613 Z M78.257,90 C74.807,90 72,87.193 72,83.743 L72,6.257 C72,2.807 74.807,0 78.257,0 L121.844,0 C125.294,0 128.101,2.807 128.101,6.257 L128.101,83.743 C128.101,87.194 125.294,90 121.844,90 L78.257,90 L78.257,90 Z M74.828,11.139 L125.273,11.139 L125.273,6.257 C125.273,4.366 123.735,2.828 121.844,2.828 L78.257,2.828 C76.366,2.828 74.828,4.366 74.828,6.257 L74.828,11.139 L74.828,11.139 L74.828,11.139 Z M74.828,69.576 L125.273,69.576 L125.273,13.967 L74.828,13.967 L74.828,69.576 L74.828,69.576 L74.828,69.576 Z M78.257,87.172 L121.844,87.172 C123.735,87.172 125.273,85.634 125.273,83.743 L125.273,72.404 L74.828,72.404 L74.828,83.743 C74.828,85.635 76.366,87.172 78.257,87.172 L78.257,87.172 L78.257,87.172 Z M104.855,79.203 C104.855,81.852 102.7,84.007 100.051,84.007 C97.402,84.007 95.247,81.852 95.247,79.203 C95.247,76.554 97.402,74.399 100.051,74.399 C102.7,74.399 104.855,76.554 104.855,79.203 L104.855,79.203 L104.855,79.203 Z M102.026,79.203 C102.026,78.113 101.14,77.227 100.05,77.227 C98.96,77.227 98.074,78.113 98.074,79.203 C98.074,80.292 98.96,81.179 100.05,81.179 C101.14,81.179 102.026,80.292 102.026,79.203 L102.026,79.203 L102.026,79.203 Z" id="Shape" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
18 |             </g>
19 |         </g>
20 |     </g>
21 | </svg>


--------------------------------------------------------------------------------
/demo/src/components/App/components/Footer/Footer.js:
--------------------------------------------------------------------------------
 1 | import styles from './Footer.less';
 2 | 
 3 | import React from 'react';
 4 | import Link from 'Link/Link';
 5 | 
 6 | const Footer = () => (
 7 |   <div className={styles.container}>
 8 |     <div>
 9 |       {'Crafted with love by '}
10 |       <Link
11 |         className={styles.link}
12 |         href="https://twitter.com/moroshko"
13 |         underline={false}
14 |       >
15 |         @moroshko
16 |       </Link>
17 |     </div>
18 |     <div className={styles.pageDesign}>
19 |       {'Page design by '}
20 |       <Link
21 |         className={styles.link}
22 |         href="https://twitter.com/vedranio"
23 |         underline={false}
24 |       >
25 |         @vedranio
26 |       </Link>
27 |     </div>
28 |     <div className={styles.licensed}>
29 |       {'Licensed under '}
30 |       <Link
31 |         className={styles.link}
32 |         href="http://moroshko.mit-license.org/"
33 |         underline={false}
34 |       >
35 |         MIT license
36 |       </Link>
37 |     </div>
38 |   </div>
39 | );
40 | 
41 | export default Footer;
42 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/Footer/Footer.less:
--------------------------------------------------------------------------------
 1 | @import 'variables';
 2 | 
 3 | .container {
 4 |   display: flex;
 5 |   flex-direction: column;
 6 |   align-items: center;
 7 |   padding: (11 * @rows) (0.5 * @column);
 8 |   font-size: 15px;
 9 |   color: #fff;
10 |   background-color: #212121;
11 | }
12 | 
13 | .link {
14 |   color: #fff;
15 | }
16 | 
17 | .pageDesign {
18 |   margin-top: 2 * @row;
19 | }
20 | 
21 | .licensed {
22 |   margin-top: 4 * @rows;
23 | }
24 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/GitHub/GitHub.js:
--------------------------------------------------------------------------------
 1 | import styles from './GitHub.less';
 2 | 
 3 | import React from 'react';
 4 | import PropTypes from 'prop-types';
 5 | 
 6 | const GitHub = props => {
 7 |   const { user, repo } = props;
 8 | 
 9 |   return (
10 |     <a
11 |       className={styles.corner}
12 |       href={`https://github.com/${user}/${repo}`}
13 |       target="_blank"
14 |       rel="noopener noreferrer"
15 |     >
16 |       <svg className={styles.svg} width="80" height="80" viewBox="0 0 250 250">
17 |         <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z" />
18 |         <path
19 |           className={styles.octoArm}
20 |           d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
21 |           fill="currentColor"
22 |         />
23 |         <path
24 |           d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
25 |           fill="currentColor"
26 |         />
27 |       </svg>
28 |     </a>
29 |   );
30 | };
31 | 
32 | GitHub.propTypes = {
33 |   user: PropTypes.string.isRequired,
34 |   repo: PropTypes.string.isRequired
35 | };
36 | 
37 | export default GitHub;
38 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/GitHub/GitHub.less:
--------------------------------------------------------------------------------
 1 | .octoArm {
 2 |   transform-origin: 130px 106px;
 3 | }
 4 | 
 5 | .corner:hover .octoArm {
 6 |   animation: octocat-wave 560ms ease-in-out
 7 | }
 8 | 
 9 | .svg {
10 |   position: absolute;
11 |   top: 0;
12 |   border: 0;
13 |   right: 0;
14 |   fill: #212121;
15 |   color: #0C7EAF;
16 | }
17 | 
18 | @keyframes octocat-wave {
19 |   0%, 100% {
20 |     transform: rotate(0)
21 |   }
22 |   20%, 60% {
23 |     transform: rotate(-25deg)
24 |   }
25 |   40%, 80% {
26 |     transform: rotate(10deg)
27 |   }
28 | }
29 | 
30 | @media (max-width: 500px) {
31 |   .corner:hover .octoArm {
32 |     animation: none
33 |   }
34 | 
35 |   .corner .octoArm {
36 |     animation: octocat-wave 560ms ease-in-out
37 |   }
38 | }
39 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/Header/Header.js:
--------------------------------------------------------------------------------
 1 | import styles from './Header.less';
 2 | 
 3 | import React, { Component } from 'react';
 4 | import fetch from 'isomorphic-fetch';
 5 | import Link from 'Link/Link';
 6 | import GitHub from 'GitHub/GitHub';
 7 | 
 8 | export default class Header extends Component {
 9 |   constructor() {
10 |     super();
11 | 
12 |     this.state = {
13 |       stargazers: '3754'
14 |     };
15 |   }
16 | 
17 |   componentDidMount() {
18 |     fetch('https://api.github.com/repos/moroshko/react-autosuggest')
19 |       .then(response => response.json())
20 |       .then(response => {
21 |         if (response.stargazers_count) {
22 |           this.setState({
23 |             stargazers: String(response.stargazers_count)
24 |           });
25 |         }
26 |       });
27 |   }
28 | 
29 |   render() {
30 |     const { stargazers } = this.state;
31 | 
32 |     return (
33 |       <div className={styles.container}>
34 |         <div className={styles.logo} />
35 |         <h1 className={styles.header}>React Autosuggest</h1>
36 |         <div className={styles.subHeader}>
37 |           WAI-ARIA compliant autosuggest component built in React
38 |         </div>
39 |         <a
40 |           className={styles.button}
41 |           href="https://github.com/moroshko/react-autosuggest#installation"
42 |           target="_blank"
43 |           rel="noopener noreferrer"
44 |         >
45 |           Get started
46 |         </a>
47 |         <div className={styles.socialLinks}>
48 |           <Link
49 |             className={styles.stargazersLink}
50 |             href="https://github.com/moroshko/react-autosuggest/stargazers"
51 |             underline={false}
52 |           >
53 |             {stargazers} stargazers
54 |           </Link>
55 |           <Link
56 |             className={styles.twitterLink}
57 |             href="https://twitter.com/moroshko"
58 |             underline={false}
59 |           >
60 |             @moroshko
61 |           </Link>
62 |         </div>
63 |         <GitHub user="moroshko" repo="react-autosuggest" />
64 |       </div>
65 |     );
66 |   }
67 | }
68 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/Header/Header.less:
--------------------------------------------------------------------------------
 1 | @import 'variables';
 2 | 
 3 | .container {
 4 |   position: relative;
 5 |   display: flex;
 6 |   flex-direction: column;
 7 |   align-items: center;
 8 |   padding: (11 * @rows) 0;
 9 |   color: #fff;
10 |   background-color: #0C7EAF;
11 | 
12 |   @media @small {
13 |     padding: (8 * @rows) 0;
14 |   }
15 | }
16 | 
17 | .logo {
18 |   @logoWidth: 199px;
19 |   @logoHeight: 176px;
20 | 
21 |   width: @logoWidth;
22 |   height: @logoHeight;
23 |   background-image: url(../Header/logo.svg); // https://github.com/webpack/css-loader/issues/74
24 |   background-repeat: no-repeat;
25 |   background-size: 100%;
26 | 
27 |   @media @small {
28 |     width: @logoWidth * 0.7;
29 |     height: @logoHeight * 0.7;
30 |   }
31 | }
32 | 
33 | .header {
34 |   margin: (3 * @rows) (0.5 * @column);
35 |   font-size: 50px;
36 |   font-weight: 300;
37 | 
38 |   @media @small {
39 |     font-size: 34px;
40 |   }
41 | }
42 | 
43 | .subHeader {
44 |   margin: 0 (0.5 * @column);
45 |   text-align: center;
46 |   font-size: 20px;
47 | }
48 | 
49 | .button {
50 |   display: inline-block;
51 |   margin-top: 5 * @rows;
52 |   width: 9 * @columns;
53 |   text-align: center;
54 |   font-size: 20px;
55 |   line-height: 5 * @rows;
56 |   color: #0C7EAF;
57 |   background-color: #fff;
58 |   border-radius: 3px;
59 |   text-decoration: none;
60 |   box-shadow: 2px 2px 2px 0 rgba(0, 0, 0, 0.3);
61 | 
62 |   &:hover {
63 |     box-shadow: 4px 4px 4px 0 rgba(0, 0, 0, 0.3);
64 |     transform: translateY(-1px);
65 |   }
66 | }
67 | 
68 | .socialLinks {
69 |   margin-top: 5 * @rows;
70 |   font-size: 15px;
71 | }
72 | 
73 | .socialLink {
74 |   display: inline-block;
75 |   line-height: 3 * @rows;
76 |   padding-left: 1.75 * @columns;
77 |   color: #fff;
78 |   background-repeat: no-repeat;
79 | }
80 | 
81 | .stargazersLink {
82 |   composes: socialLink;
83 |   background-image: url(../Header/star.svg); // https://github.com/webpack/css-loader/issues/74
84 |   background-position: 0 -1px;
85 | }
86 | 
87 | .twitterLink {
88 |   composes: socialLink;
89 |   background-image: url(../Header/twitter.svg); // https://github.com/webpack/css-loader/issues/74
90 |   background-position: 0 4px;
91 |   margin-left: 2 * @columns;
92 | }
93 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/Header/logo.svg:
--------------------------------------------------------------------------------
 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 2 | <svg width="196px" height="174px" viewBox="0 0 196 174" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
 3 |     <!-- Generator: Sketch 3.4.3 (16044) - http://www.bohemiancoding.com/sketch -->
 4 |     <title>icon_react-logo</title>
 5 |     <desc>Created with Sketch.</desc>
 6 |     <defs>
 7 |         <linearGradient x1="-38.6399819%" y1="72.3378885%" x2="44.9918262%" y2="50%" id="linearGradient-1">
 8 |             <stop stop-color="#000000" stop-opacity="0.1" offset="0%"></stop>
 9 |             <stop stop-color="#000000" stop-opacity="0" offset="100%"></stop>
10 |         </linearGradient>
11 |         <linearGradient x1="7.72673931%" y1="22.4745086%" x2="55.9776075%" y2="63.2242015%" id="linearGradient-2">
12 |             <stop stop-color="#000000" offset="0%"></stop>
13 |             <stop stop-color="#000000" stop-opacity="0" offset="100%"></stop>
14 |         </linearGradient>
15 |         <linearGradient x1="10.3481493%" y1="24.688389%" x2="55.9776075%" y2="63.2242015%" id="linearGradient-3">
16 |             <stop stop-color="#000000" offset="0%"></stop>
17 |             <stop stop-color="#000000" stop-opacity="0" offset="100%"></stop>
18 |         </linearGradient>
19 |     </defs>
20 |     <g id="Welcome" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
21 |         <g id="v3" sketch:type="MSArtboardGroup" transform="translate(-610.000000, -138.000000)">
22 |             <g id="Header" sketch:type="MSLayerGroup">
23 |                 <g id="icon_react-logo" transform="translate(611.000000, 140.000000)" sketch:type="MSShapeGroup">
24 |                     <path d="M96.7035678,48.5883575 C120.720144,48.5883575 143.030929,52.0260019 159.853158,57.8029516 C180.121556,64.7632477 192.583391,75.3141304 192.583391,84.8676872 C192.583391,94.8235177 179.375986,106.031696 157.610741,113.225318 C141.155057,118.664017 119.501772,121.502696 96.7035678,121.502696 C73.3295158,121.502696 51.1955858,118.837944 34.5584124,113.165209 C13.5048646,105.986881 0.823744389,94.6332291 0.823744389,84.8676872 C0.823744389,75.3916685 12.7222115,64.9225922 32.7057173,57.9726108 C49.5907012,52.1006946 72.4523738,48.5883575 96.7035678,48.5883575 L96.7035678,48.5883575 Z" id="Shape" stroke="#F7F7F7" stroke-width="3" fill-opacity="0" fill="#000000"></path>
25 |                     <path d="M104.593211,16.8667076 C120.763288,2.82945251 136.151425,-2.66828864 144.447905,2.1042216 C153.093816,7.07769088 156.22942,24.0902067 151.603031,46.4916257 C148.10516,63.4283818 139.753056,83.5577188 128.363759,103.258462 C116.686717,123.456801 103.315293,141.251812 90.0772234,152.794685 C73.3255936,167.401739 57.1309135,172.687495 48.6500911,167.808993 C40.4210015,163.074896 37.2736305,147.563348 41.2214827,126.822953 C44.5571183,109.298615 52.9281196,87.7888845 65.0433763,66.8325922 C77.0413241,46.0793929 91.1725812,28.517352 104.593211,16.8667076 Z" id="Shape" stroke="#F7F7F7" stroke-width="3" fill-opacity="0" fill="#000000"></path>
26 |                     <path d="M41.4293582,44.1775736 C37.3132089,23.1928268 40.2234662,7.14811546 48.5121017,2.36173371 C57.1494547,-2.62631844 73.4910383,3.15987896 90.6363123,18.3459795 C103.599473,29.8273203 116.910638,47.0980596 128.333095,66.7799516 C140.044011,86.9587281 148.821492,107.401775 152.235571,124.607069 C156.556031,146.379291 153.058873,163.014786 144.586251,167.907516 C136.365006,172.655128 121.320951,167.628663 105.279236,153.858523 C91.725252,142.223885 77.2242403,124.247121 65.073684,103.311102 C53.0411497,82.5778212 44.8455768,61.5944972 41.4293582,44.1775736 Z" id="Shape" stroke="#F7F7F7" stroke-width="3" fill-opacity="0" fill="#000000"></path>
27 |                     <g id="magnifying-lens" transform="translate(76.000000, 65.000000)">
28 |                         <g id="icon">
29 |                             <g id="shadows">
30 |                                 <rect id="Rectangle-5" fill="url(#linearGradient-1)" transform="translate(47.030330, 44.030330) rotate(-315.000000) translate(-47.030330, -44.030330) " x="42.8305826" y="42.5303301" width="8.39949494" height="3"></rect>
31 |                                 <path d="M6,32 L53,77 L77,50 L31,5 L33,19 C33,20.66 29.66,30 28,30 C28,30 24.8292327,32.5426918 23,33 C20.8292327,33.5426918 16,33 16,33 C15.77,33 6.22,32.051 6,32 Z" id="Shape" opacity="0.12" fill="url(#linearGradient-2)"></path>
32 |                                 <path d="M0,23 L12,34 L34,10 L22,0 L7,2 L1,12 L0,23 Z" id="Shape-Copy" opacity="0.12" fill="url(#linearGradient-3)"></path>
33 |                             </g>
34 |                             <rect id="Rectangle-4" fill="#FFFFFF" transform="translate(38.131728, 35.131728) rotate(-315.000000) translate(-38.131728, -35.131728) " x="28.131728" y="33.631728" width="20" height="3"></rect>
35 |                             <circle id="Oval-1" stroke="#FFFFFF" stroke-width="3" cx="17" cy="17" r="17"></circle>
36 |                         </g>
37 |                     </g>
38 |                 </g>
39 |             </g>
40 |         </g>
41 |     </g>
42 | </svg>


--------------------------------------------------------------------------------
/demo/src/components/App/components/Header/star.svg:
--------------------------------------------------------------------------------
 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 2 | <svg width="25px" height="24px" viewBox="0 0 25 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
 3 |     <!-- Generator: Sketch 3.4.3 (16044) - http://www.bohemiancoding.com/sketch -->
 4 |     <title>star</title>
 5 |     <desc>Created with Sketch.</desc>
 6 |     <defs></defs>
 7 |     <g id="icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
 8 |         <g sketch:type="MSArtboardGroup" transform="translate(-140.000000, -591.000000)" id="star" fill="#FFFFFF">
 9 |             <g sketch:type="MSLayerGroup" transform="translate(140.000000, 591.000000)">
10 |                 <path d="M12.469053,0.280606061 L15.5237405,8.90560606 L24.687803,8.90560606 L17.4557405,14.5434811 L20.0834905,23.2130436 L12.5143343,18.1142311 L4.98974053,23.2806061 L7.54992803,14.5880436 L0.25030303,8.90560606 L9.41436553,8.90560606 L12.469053,0.280606061" sketch:type="MSShapeGroup"></path>
11 |             </g>
12 |         </g>
13 |     </g>
14 | </svg>


--------------------------------------------------------------------------------
/demo/src/components/App/components/Header/twitter.svg:
--------------------------------------------------------------------------------
 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 2 | <svg width="25px" height="21px" viewBox="0 0 25 21" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
 3 |     <!-- Generator: Sketch 3.4.3 (16044) - http://www.bohemiancoding.com/sketch -->
 4 |     <title>icon_twitter</title>
 5 |     <desc>Created with Sketch.</desc>
 6 |     <defs></defs>
 7 |     <g id="icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
 8 |         <g sketch:type="MSArtboardGroup" transform="translate(-219.000000, -594.000000)" id="icon_twitter" fill="#FFFFFF">
 9 |             <g sketch:type="MSLayerGroup" transform="translate(219.000000, 594.000000)">
10 |                 <path d="M25,2.40552389 C24.3038204,3.44104744 23.4497202,4.32378161 22.4376996,5.05372641 C22.4476911,5.24949393 22.4526868,5.47076132 22.4526868,5.71752859 C22.4526868,7.08861879 22.25279,8.46136746 21.8529965,9.83577461 C21.4532029,11.2101953 20.8425125,12.5257571 20.0209252,13.78246 C19.1993379,15.039163 18.2206763,16.1524926 17.0849404,17.1224489 C15.9492044,18.0924052 14.5839967,18.8657953 12.9893173,19.442619 C11.3946378,20.0194427 9.68557109,20.3078546 7.86211712,20.3078546 C5.01631261,20.3078546 2.3956069,19.5395889 0,18.0030575 C0.425191895,18.0503748 0.832986746,18.0740334 1.22338455,18.0740334 C3.60082271,18.0740334 5.72425725,17.3417532 7.59368816,15.8771928 C6.4851918,15.8568715 5.49245009,15.5156389 4.61546303,14.8534952 C3.73847598,14.1913651 3.13495422,13.3452865 2.80489776,12.3152596 C3.13113635,12.3774016 3.45312383,12.4084726 3.77086021,12.4084726 C4.22821971,12.4084726 4.67825486,12.3488217 5.12096565,12.22952 C3.93799368,11.9925952 2.95661759,11.4033973 2.17683737,10.4619261 C1.39708423,9.5204684 1.00720766,8.43384357 1.00720766,7.20205158 L1.00720766,7.13856929 C1.73332104,7.54061025 2.50783476,7.75446529 3.33074882,7.78013439 C2.62996608,7.31294589 2.0743505,6.70371763 1.66390209,5.95244961 C1.25342661,5.20118159 1.04818887,4.38817775 1.04818887,3.5134381 C1.04818887,2.59088027 1.27936648,1.73180472 1.74172171,0.936211468 C3.0281824,2.51490189 4.58699822,3.77664117 6.41816918,4.72142931 C8.24936721,5.666231 10.2138929,6.19094482 12.3117464,6.29557078 C12.2222701,5.92267833 12.1774507,5.53354638 12.1772882,5.12817493 C12.1772882,3.71277303 12.6781742,2.50422674 13.6799461,1.50253604 C14.6817316,0.500845347 15.8903252,0 17.3057271,0 C18.7876033,0 20.0358041,0.539585926 21.0503293,1.61875778 C22.2094734,1.3891574 23.2948121,0.973868962 24.3063453,0.372892453 C23.916638,1.59935026 23.1657016,2.54547872 22.0535363,3.21127783 C23.0757244,3.08980991 24.0578654,2.82122526 24.9999594,2.40552389 L25,2.40552389 Z" id="twitter-icon" sketch:type="MSShapeGroup"></path>
11 |             </g>
12 |         </g>
13 |     </g>
14 | </svg>


--------------------------------------------------------------------------------
/demo/src/components/App/components/Link/Link.js:
--------------------------------------------------------------------------------
 1 | import styles from './Link.less';
 2 | 
 3 | import React from 'react';
 4 | import PropTypes from 'prop-types';
 5 | 
 6 | const Link = props => {
 7 |   const { className, href, underline, children } = props;
 8 |   const klass =
 9 |     (className === null ? '' : className + ' ') +
10 |     (underline ? styles.linkWithUnderline : styles.linkWithoutUnderline);
11 | 
12 |   return (
13 |     <a className={klass} href={href} target="_blank" rel="noopener noreferrer">
14 |       {children}
15 |     </a>
16 |   );
17 | };
18 | 
19 | Link.propTypes = {
20 |   className: PropTypes.string,
21 |   href: PropTypes.string.isRequired,
22 |   underline: PropTypes.bool.isRequired,
23 |   children: PropTypes.node
24 | };
25 | 
26 | Link.defaultProps = {
27 |   className: null,
28 |   underline: true
29 | };
30 | 
31 | export default Link;
32 | 


--------------------------------------------------------------------------------
/demo/src/components/App/components/Link/Link.less:
--------------------------------------------------------------------------------
 1 | .link {
 2 |   white-space: nowrap;
 3 | }
 4 | 
 5 | .linkWithUnderline {
 6 |   composes: link;
 7 | }
 8 | 
 9 | .linkWithoutUnderline {
10 |   composes: link;
11 |   text-decoration: none;
12 | 
13 |   &:hover {
14 |     text-decoration: underline;
15 |   }
16 | }
17 | 


--------------------------------------------------------------------------------
/demo/src/components/utils/utils.js:
--------------------------------------------------------------------------------
1 | // https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions#Using_special_characters
2 | const escapeRegexCharacters = str => str.replace(/[.*+?^${}()|[\]\\]/g, '\\
amp;');
3 | 
4 | export { escapeRegexCharacters };
5 | 


--------------------------------------------------------------------------------
/demo/src/index.html:
--------------------------------------------------------------------------------
 1 | <!DOCTYPE html>
 2 | <html lang="en">
 3 | <head>
 4 |   <meta charset="utf-8">
 5 |   <title>React Autosuggest</title>
 6 |   <meta name="description" content="Accessible, mobile friendly, and customizable React autosuggest component">
 7 |   <meta name="author" content="Misha Moroshko">
 8 |   <meta name="viewport" content="width=device-width, initial-scale=1">
 9 |   <link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Open+Sans:300|Open+Sans:400">
10 |   <link rel="stylesheet" type="text/css" href="autosuggest.css">
11 |   <link rel="stylesheet" type="text/css" href="app.css">
12 |   <script>
13 |     if (location.host === 'react-autosuggest.js.org') {
14 |       (function(h,o,t,j,a,r){
15 |         h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
16 |         h._hjSettings={hjid:59014,hjsv:5};
17 |         a=o.getElementsByTagName('head')[0];
18 |         r=o.createElement('script');r.async=1;
19 |         r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
20 |         a.appendChild(r);
21 |       })(window,document,'//static.hotjar.com/c/hotjar-','.js?sv=');
22 |     }
23 |   </script>
24 | </head>
25 | <body>
26 |   <div id="demo"></div>
27 |   <script src="index.js"></script>
28 | </body>
29 | </html>
30 | 


--------------------------------------------------------------------------------
/demo/src/index.js:
--------------------------------------------------------------------------------
1 | import es6promise from 'es6-promise';
2 | import React from 'react';
3 | import { render } from 'react-dom';
4 | import App from 'App/App';
5 | 
6 | es6promise.polyfill(); // Required, because `Promise` is undefined in IE.
7 | 
8 | render(<App />, document.getElementById('demo'));
9 | 


--------------------------------------------------------------------------------
/demo/src/variables.less:
--------------------------------------------------------------------------------
1 | @row: 9px;
2 | @rows: @row;
3 | @column: 20px;
4 | @columns: @column;
5 | 
6 | @small: ~"(max-width: 499px)";
7 | @examples-vertical: ~"(max-width: 719px)";
8 | 


--------------------------------------------------------------------------------
/demo/standalone/app.css:
--------------------------------------------------------------------------------
 1 | .react-autosuggest__container {
 2 |   position: relative;
 3 | }
 4 | 
 5 | .react-autosuggest__input {
 6 |   width: 240px;
 7 |   height: 30px;
 8 |   padding: 10px 20px;
 9 |   font-family: 'Open Sans', 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-autosuggest__input--focused {
18 |   outline: none;
19 | }
20 | 
21 | .react-autosuggest__input::-ms-clear {
22 |   display: none;
23 | }
24 | 
25 | .react-autosuggest__input--open {
26 |   border-bottom-left-radius: 0;
27 |   border-bottom-right-radius: 0;
28 | }
29 | 
30 | .react-autosuggest__suggestions-container {
31 |   display: none;
32 | }
33 | 
34 | .react-autosuggest__suggestions-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-autosuggest__suggestions-list {
50 |   margin: 0;
51 |   padding: 0;
52 |   list-style-type: none;
53 | }
54 | 
55 | .react-autosuggest__suggestion {
56 |   cursor: pointer;
57 |   padding: 10px 20px;
58 | }
59 | 
60 | .react-autosuggest__suggestion--highlighted {
61 |   background-color: #ddd;
62 | }
63 | 


--------------------------------------------------------------------------------
/demo/standalone/app.js:
--------------------------------------------------------------------------------
  1 | /* eslint-disable react/react-in-jsx-scope */
  2 | 
  3 | const languages = [
  4 |   {
  5 |     name: 'C',
  6 |     year: 1972
  7 |   },
  8 |   {
  9 |     name: 'C#',
 10 |     year: 2000
 11 |   },
 12 |   {
 13 |     name: 'C++',
 14 |     year: 1983
 15 |   },
 16 |   {
 17 |     name: 'Clojure',
 18 |     year: 2007
 19 |   },
 20 |   {
 21 |     name: 'Elm',
 22 |     year: 2012
 23 |   },
 24 |   {
 25 |     name: 'Go',
 26 |     year: 2009
 27 |   },
 28 |   {
 29 |     name: 'Haskell',
 30 |     year: 1990
 31 |   },
 32 |   {
 33 |     name: 'Java',
 34 |     year: 1995
 35 |   },
 36 |   {
 37 |     name: 'JavaScript',
 38 |     year: 1995
 39 |   },
 40 |   {
 41 |     name: 'Perl',
 42 |     year: 1987
 43 |   },
 44 |   {
 45 |     name: 'PHP',
 46 |     year: 1995
 47 |   },
 48 |   {
 49 |     name: 'Python',
 50 |     year: 1991
 51 |   },
 52 |   {
 53 |     name: 'Ruby',
 54 |     year: 1995
 55 |   },
 56 |   {
 57 |     name: 'Scala',
 58 |     year: 2003
 59 |   }
 60 | ];
 61 | 
 62 | // https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions#Using_special_characters
 63 | const escapeRegexCharacters = str => str.replace(/[.*+?^${}()|[\]\\]/g, '\\
amp;');
 64 | 
 65 | const getSuggestions = value => {
 66 |   const escapedValue = escapeRegexCharacters(value.trim());
 67 | 
 68 |   if (escapedValue === '') {
 69 |     return [];
 70 |   }
 71 | 
 72 |   const regex = new RegExp('^' + escapedValue, 'i');
 73 | 
 74 |   return languages.filter(language => regex.test(language.name));
 75 | };
 76 | 
 77 | const getSuggestionValue = suggestion => suggestion.name;
 78 | 
 79 | const renderSuggestion = suggestion => <span>{suggestion.name}</span>;
 80 | 
 81 | // prettier-ignore
 82 | class App extends React.Component { // eslint-disable-line no-undef
 83 |   constructor() {
 84 |     super();
 85 | 
 86 |     this.state = {
 87 |       value: '',
 88 |       suggestions: getSuggestions('')
 89 |     };
 90 |   }
 91 | 
 92 |   onChange = (event, { newValue }) => {
 93 |     this.setState({
 94 |       value: newValue
 95 |     });
 96 |   };
 97 | 
 98 |   onSuggestionsFetchRequested = ({ value }) => {
 99 |     this.setState({
100 |       suggestions: getSuggestions(value)
101 |     });
102 |   };
103 | 
104 |   onSuggestionsClearRequested = () => {
105 |     this.setState({
106 |       suggestions: []
107 |     });
108 |   };
109 | 
110 |   render() {
111 |     const { value, suggestions } = this.state;
112 |     const inputProps = {
113 |       placeholder: "Type 'c'",
114 |       value,
115 |       onChange: this.onChange
116 |     };
117 | 
118 |     return (
119 |       <Autosuggest // eslint-disable-line react/jsx-no-undef
120 |         suggestions={suggestions}
121 |         onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
122 |         onSuggestionsClearRequested={this.onSuggestionsClearRequested}
123 |         getSuggestionValue={getSuggestionValue}
124 |         renderSuggestion={renderSuggestion}
125 |         inputProps={inputProps}
126 |       />
127 |     );
128 |   }
129 | }
130 | 
131 | ReactDOM.render(<App />, document.getElementById('app')); // eslint-disable-line no-undef
132 | 


--------------------------------------------------------------------------------
/demo/standalone/compiled.app.js:
--------------------------------------------------------------------------------
 1 | /******/ (function(modules) {
 2 |   // webpackBootstrap
 3 |   /******/ // The module cache
 4 |   /******/ var installedModules = {}; // The require function
 5 | 
 6 |   /******/ /******/ function __webpack_require__(moduleId) {
 7 |     /******/ // Check if module is in cache
 8 |     /******/ if (installedModules[moduleId])
 9 |       /******/ return installedModules[moduleId].exports; // Create a new module (and put it into the cache)
10 | 
11 |     /******/ /******/ var module = (installedModules[moduleId] = {
12 |       /******/ exports: {},
13 |       /******/ id: moduleId,
14 |       /******/ loaded: false
15 |       /******/
16 |     }); // Execute the module function
17 | 
18 |     /******/ /******/ modules[moduleId].call(
19 |       module.exports,
20 |       module,
21 |       module.exports,
22 |       __webpack_require__
23 |     ); // Flag the module as loaded
24 | 
25 |     /******/ /******/ module.loaded = true; // Return the exports of the module
26 | 
27 |     /******/ /******/ return module.exports;
28 |     /******/
29 |   } // expose the modules object (__webpack_modules__)
30 | 
31 |   /******/ /******/ __webpack_require__.m = modules; // expose the module cache
32 | 
33 |   /******/ /******/ __webpack_require__.c = installedModules; // __webpack_public_path__
34 | 
35 |   /******/ /******/ __webpack_require__.p = ''; // Load entry module and return exports
36 | 
37 |   /******/ /******/ return __webpack_require__(0);
38 |   /******/
39 | })(
40 |   /************************************************************************/
41 |   /******/ [
42 |     /* 0 */
43 |     /***/ function(module, exports, __webpack_require__) {
44 |       (function webpackMissingModule() {
45 |         throw new Error('Cannot find module "./demo/standalone/app"');
46 |       })();
47 | 
48 |       /***/
49 |     }
50 |     /******/
51 |   ]
52 | );
53 | 


--------------------------------------------------------------------------------
/demo/standalone/index.html:
--------------------------------------------------------------------------------
 1 | <!DOCTYPE html>
 2 | <html lang="en">
 3 | <head>
 4 |   <meta charset="utf-8">
 5 |   <title>React Autosuggest</title>
 6 |   <meta name="description" content="Accessible, mobile friendly, and customizable React autosuggest component">
 7 |   <meta name="author" content="Misha Moroshko">
 8 |   <meta name="viewport" content="width=device-width, initial-scale=1">
 9 |   <link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Open+Sans:300|Open+Sans:400">
10 |   <link rel="stylesheet" type="text/css" href="app.css">
11 | </head>
12 | <body>
13 |   <div id="app"></div>
14 |   <script src="../../node_modules/react/dist/react.min.js"></script>
15 |   <script src="../../node_modules/react-dom/dist/react-dom.min.js"></script>
16 |   <script src="../../dist/standalone/autosuggest.min.js"></script>
17 |   <script src="compiled.app.js"></script>
18 | </body>
19 | </html>
20 | 


--------------------------------------------------------------------------------
/dom-structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moroshko/react-autosuggest/e2c84d906afc7b6723d29d1c2283c3f322ee2552/dom-structure.png


--------------------------------------------------------------------------------
/nyc.config.js:
--------------------------------------------------------------------------------
 1 | const babelConfig = require('@istanbuljs/nyc-config-babel');
 2 | 
 3 | module.exports = {
 4 |   ...babelConfig,
 5 |   statements: 95,
 6 |   branches: 91,
 7 |   functions: 100,
 8 |   lines: 95,
 9 |   include: ['src/*.js'],
10 |   exclude: ['test/**/*.js'],
11 |   reporter: ['lcov', 'text-summary'],
12 |   all: true,
13 |   'check-coverage': true
14 | };
15 | 


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "name": "react-autosuggest",
  3 |   "version": "10.1.0",
  4 |   "description": "WAI-ARIA compliant React autosuggest component",
  5 |   "main": "dist/index.js",
  6 |   "repository": {
  7 |     "type": "git",
  8 |     "url": "https://github.com/moroshko/react-autosuggest.git"
  9 |   },
 10 |   "author": "Misha Moroshko <michael.moroshko@gmail.com>",
 11 |   "scripts": {
 12 |     "start": "mkdir -p demo/dist && npm run copy-static-files && node server",
 13 |     "prettier": "prettier --single-quote --write",
 14 |     "prettier:src": "npm run prettier -- \"src/**/*.js\"",
 15 |     "prettier:all": "npm run prettier -- \".*.js\" \"*.js\" \"demo/src/**/*.js\" \"demo/standalone/app.js\" \"src/**/*.js\" \"test/**/*.js\"",
 16 |     "lint": "eslint src test demo/src demo/standalone/app.js server.js webpack.*.js",
 17 |     "test": "cross-env NODE_ENV=test nyc mocha \"test/**/*.test.js\"",
 18 |     "copy-static-files": "cp demo/src/index.html demo/src/components/App/components/Examples/components/Basic/autosuggest.css demo/dist/",
 19 |     "dist": "rm -rf dist && mkdir dist && babel src -d dist",
 20 |     "demo-dist": "rm -rf demo/dist && mkdir demo/dist && npm run copy-static-files && cross-env BABEL_ENV=production webpack --config webpack.gh-pages.config.js",
 21 |     "standalone": "cross-env BABEL_ENV=production webpack --config webpack.standalone.config.js && webpack --config webpack.standalone-demo.config.js",
 22 |     "prebuild": "npm run prettier:src && npm run lint && npm test",
 23 |     "build": "npm run dist && npm run standalone",
 24 |     "gh-pages-build": "npm run prebuild && npm run demo-dist",
 25 |     "postversion": "git push && git push --tags",
 26 |     "prepublish": "npm run dist && npm run standalone",
 27 |     "precommit": "lint-staged"
 28 |   },
 29 |   "dependencies": {
 30 |     "es6-promise": "^4.2.8",
 31 |     "prop-types": "^15.7.2",
 32 |     "react-themeable": "^1.1.0",
 33 |     "section-iterator": "^2.0.0",
 34 |     "shallow-equal": "^1.2.1"
 35 |   },
 36 |   "peerDependencies": {
 37 |     "react": ">=16.3.0"
 38 |   },
 39 |   "devDependencies": {
 40 |     "@babel/cli": "^7.8.4",
 41 |     "@babel/core": "^7.9.0",
 42 |     "@babel/plugin-proposal-class-properties": "^7.8.3",
 43 |     "@babel/preset-env": "^7.9.0",
 44 |     "@babel/preset-react": "^7.9.4",
 45 |     "@babel/register": "^7.9.0",
 46 |     "@istanbuljs/nyc-config-babel": "^3.0.0",
 47 |     "autoprefixer": "^9.7.5",
 48 |     "autosuggest-highlight": "^3.1.1",
 49 |     "babel-eslint": "^10.1.0",
 50 |     "babel-loader": "^8.1.0",
 51 |     "babel-plugin-istanbul": "^6.0.0",
 52 |     "babel-plugin-transform-react-remove-prop-types": "^0.4.24",
 53 |     "chai": "^4.2.0",
 54 |     "cross-env": "^7.0.2",
 55 |     "css-loader": "^3.4.2",
 56 |     "eslint": "^6.8.0",
 57 |     "eslint-plugin-react": "^7.19.0",
 58 |     "file-loader": "^6.0.0",
 59 |     "husky": "^4.2.3",
 60 |     "ismobilejs": "^1.0.3",
 61 |     "isomorphic-fetch": "^2.2.1",
 62 |     "jsdom": "15.1.1",
 63 |     "less": "^3.11.1",
 64 |     "less-loader": "^5.0.0",
 65 |     "lint-staged": "^10.0.10",
 66 |     "mini-css-extract-plugin": "^0.9.0",
 67 |     "mocha": "^7.1.1",
 68 |     "nyc": "^15.0.0",
 69 |     "openurl": "^1.1.1",
 70 |     "postcss-loader": "^3.0.0",
 71 |     "prettier": "^2.0.2",
 72 |     "react": "^16.13.1",
 73 |     "react-dom": "^16.13.1",
 74 |     "react-modal": "^3.11.2",
 75 |     "react-transform-hmr": "^1.0.4",
 76 |     "sinon": "^9.0.1",
 77 |     "sinon-chai": "^3.5.0",
 78 |     "style-loader": "^1.1.3",
 79 |     "svgo": "^1.3.2",
 80 |     "svgo-loader": "^2.2.1",
 81 |     "terser-webpack-plugin": "^2.3.5",
 82 |     "url-loader": "^4.0.0",
 83 |     "webpack": "^4.42.1",
 84 |     "webpack-cli": "^3.3.11",
 85 |     "webpack-dev-server": "^3.10.3"
 86 |   },
 87 |   "files": [
 88 |     "dist"
 89 |   ],
 90 |   "husky": {
 91 |     "hooks": {
 92 |       "pre-commit": "lint-staged"
 93 |     }
 94 |   },
 95 |   "lint-staged": {
 96 |     "*.js": [
 97 |       "npm run prettier:src"
 98 |     ]
 99 |   },
100 |   "keywords": [
101 |     "autosuggest",
102 |     "autocomplete",
103 |     "auto-suggest",
104 |     "auto-complete",
105 |     "auto suggest",
106 |     "auto complete",
107 |     "react autosuggest",
108 |     "react autocomplete",
109 |     "react auto-suggest",
110 |     "react auto-complete",
111 |     "react auto suggest",
112 |     "react auto complete",
113 |     "react-autosuggest",
114 |     "react-autocomplete",
115 |     "react-auto-suggest",
116 |     "react-auto-complete",
117 |     "react-component"
118 |   ],
119 |   "license": "MIT"
120 | }
121 | 


--------------------------------------------------------------------------------
/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 autosuggest.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 | }).listen(port, host, function(error) {
12 |   if (error) {
13 |     console.error(error); // eslint-disable-line no-console
14 |     process.exit(1);
15 |   }
16 | 
17 |   var url = `http://${host}:${port}/demo/dist/index.html`;
18 | 
19 |   console.log('Demo is ready at ' + url); // eslint-disable-line no-console
20 | 
21 |   openUrl.open(url);
22 | });
23 | 


--------------------------------------------------------------------------------
/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 {
55 |       isHighlighted,
56 |       item,
57 |       renderItem,
58 |       renderItemData,
59 |       ...restProps
60 |     } = this.props;
61 | 
62 |     delete restProps.sectionIndex;
63 |     delete restProps.itemIndex;
64 | 
65 |     if (typeof restProps.onMouseEnter === 'function') {
66 |       restProps.onMouseEnter = this.onMouseEnter;
67 |     }
68 | 
69 |     if (typeof restProps.onMouseLeave === 'function') {
70 |       restProps.onMouseLeave = this.onMouseLeave;
71 |     }
72 | 
73 |     if (typeof restProps.onMouseDown === 'function') {
74 |       restProps.onMouseDown = this.onMouseDown;
75 |     }
76 | 
77 |     if (typeof restProps.onClick === 'function') {
78 |       restProps.onClick = this.onClick;
79 |     }
80 | 
81 |     return (
82 |       <li role="option" {...restProps} ref={this.storeItemReference}>
83 |         {renderItem(item, { isHighlighted, ...renderItemData })}
84 |       </li>
85 |     );
86 |   }
87 | }
88 | 


--------------------------------------------------------------------------------
/src/ItemList.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([PropTypes.object, PropTypes.func]),
10 |     renderItem: PropTypes.func.isRequired,
11 |     renderItemData: PropTypes.object.isRequired,
12 |     sectionIndex: PropTypes.number,
13 |     highlightedItemIndex: PropTypes.number,
14 |     onHighlightedItemChange: PropTypes.func.isRequired,
15 |     getItemId: PropTypes.func.isRequired,
16 |     theme: PropTypes.func.isRequired,
17 |     keyPrefix: PropTypes.string.isRequired,
18 |   };
19 | 
20 |   static defaultProps = {
21 |     sectionIndex: null,
22 |   };
23 | 
24 |   shouldComponentUpdate(nextProps) {
25 |     return compareObjects(nextProps, this.props, ['itemProps']);
26 |   }
27 | 
28 |   storeHighlightedItemReference = (highlightedItem) => {
29 |     this.props.onHighlightedItemChange(
30 |       highlightedItem === null ? null : highlightedItem.item
31 |     );
32 |   };
33 | 
34 |   render() {
35 |     const {
36 |       items,
37 |       itemProps,
38 |       renderItem,
39 |       renderItemData,
40 |       sectionIndex,
41 |       highlightedItemIndex,
42 |       getItemId,
43 |       theme,
44 |       keyPrefix,
45 |     } = this.props;
46 |     const sectionPrefix =
47 |       sectionIndex === null
48 |         ? keyPrefix
49 |         : `${keyPrefix}section-${sectionIndex}-`;
50 |     const isItemPropsFunction = typeof itemProps === 'function';
51 | 
52 |     return (
53 |       <ul role="listbox" {...theme(`${sectionPrefix}items-list`, 'itemsList')}>
54 |         {items.map((item, itemIndex) => {
55 |           const isFirst = itemIndex === 0;
56 |           const isHighlighted = itemIndex === highlightedItemIndex;
57 |           const itemKey = `${sectionPrefix}item-${itemIndex}`;
58 |           const itemPropsObj = isItemPropsFunction
59 |             ? itemProps({ sectionIndex, itemIndex })
60 |             : itemProps;
61 |           const allItemProps = {
62 |             id: getItemId(sectionIndex, itemIndex),
63 |             'aria-selected': isHighlighted,
64 |             ...theme(
65 |               itemKey,
66 |               'item',
67 |               isFirst && 'itemFirst',
68 |               isHighlighted && 'itemHighlighted'
69 |             ),
70 |             ...itemPropsObj,
71 |           };
72 | 
73 |           if (isHighlighted) {
74 |             allItemProps.ref = this.storeHighlightedItemReference;
75 |           }
76 | 
77 |           // `key` is provided by theme()
78 |           /* eslint-disable react/jsx-key */
79 |           return (
80 |             <Item
81 |               {...allItemProps}
82 |               sectionIndex={sectionIndex}
83 |               isHighlighted={isHighlighted}
84 |               itemIndex={itemIndex}
85 |               item={item}
86 |               renderItem={renderItem}
87 |               renderItemData={renderItemData}
88 |             />
89 |           );
90 |           /* eslint-enable react/jsx-key */
91 |         })}
92 |       </ul>
93 |     );
94 |   }
95 | }
96 | 


--------------------------------------------------------------------------------
/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 |       <div {...theme(`${sectionKeyPrefix}title`, 'sectionTitle')}>
27 |         {sectionTitle}
28 |       </div>
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 (
30 |       !keysMap[key] ||
31 |       aValue === null ||
32 |       bValue === null ||
33 |       typeof aValue !== 'object' ||
34 |       typeof bValue !== 'object'
35 |     ) {
36 |       return true;
37 |     }
38 | 
39 |     const aValueKeys = Object.keys(aValue);
40 |     const bValueKeys = Object.keys(bValue);
41 | 
42 |     if (aValueKeys.length !== bValueKeys.length) {
43 |       return true;
44 |     }
45 | 
46 |     for (let n = 0, { length } = aValueKeys; n < length; n++) {
47 |       const aValueKey = aValueKeys[n];
48 | 
49 |       if (aValue[aValueKey] !== bValue[aValueKey]) {
50 |         return true;
51 |       }
52 |     }
53 |   }
54 | 
55 |   return false;
56 | }
57 | 


--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./Autosuggest').default;
2 | 


--------------------------------------------------------------------------------
/src/theme.js:
--------------------------------------------------------------------------------
 1 | export const defaultTheme = {
 2 |   container: 'react-autosuggest__container',
 3 |   containerOpen: 'react-autosuggest__container--open',
 4 |   input: 'react-autosuggest__input',
 5 |   inputOpen: 'react-autosuggest__input--open',
 6 |   inputFocused: 'react-autosuggest__input--focused',
 7 |   suggestionsContainer: 'react-autosuggest__suggestions-container',
 8 |   suggestionsContainerOpen: 'react-autosuggest__suggestions-container--open',
 9 |   suggestionsList: 'react-autosuggest__suggestions-list',
10 |   suggestion: 'react-autosuggest__suggestion',
11 |   suggestionFirst: 'react-autosuggest__suggestion--first',
12 |   suggestionHighlighted: 'react-autosuggest__suggestion--highlighted',
13 |   sectionContainer: 'react-autosuggest__section-container',
14 |   sectionContainerFirst: 'react-autosuggest__section-container--first',
15 |   sectionTitle: 'react-autosuggest__section-title',
16 | };
17 | 
18 | export const mapToAutowhateverTheme = (theme) => {
19 |   let result = {};
20 | 
21 |   for (const key in theme) {
22 |     switch (key) {
23 |       case 'suggestionsContainer':
24 |         result['itemsContainer'] = theme[key];
25 |         break;
26 | 
27 |       case 'suggestionsContainerOpen':
28 |         result['itemsContainerOpen'] = theme[key];
29 |         break;
30 | 
31 |       case 'suggestion':
32 |         result['item'] = theme[key];
33 |         break;
34 | 
35 |       case 'suggestionFirst':
36 |         result['itemFirst'] = theme[key];
37 |         break;
38 | 
39 |       case 'suggestionHighlighted':
40 |         result['itemHighlighted'] = theme[key];
41 |         break;
42 | 
43 |       case 'suggestionsList':
44 |         result['itemsList'] = theme[key];
45 |         break;
46 | 
47 |       default:
48 |         result[key] = theme[key];
49 |     }
50 |   }
51 | 
52 |   return result;
53 | };
54 | 


--------------------------------------------------------------------------------
/test/always-render-suggestions/AutosuggestApp.js:
--------------------------------------------------------------------------------
 1 | import React, { Component } from 'react';
 2 | import sinon from 'sinon';
 3 | import Autosuggest from '../../src/Autosuggest';
 4 | import languages from '../plain-list/languages';
 5 | import { escapeRegexCharacters } from '../../demo/src/components/utils/utils.js';
 6 | 
 7 | const getMatchingLanguages = value => {
 8 |   const escapedValue = escapeRegexCharacters(value.trim());
 9 |   const regex = new RegExp('^' + escapedValue, 'i');
10 | 
11 |   return languages.filter(language => regex.test(language.name));
12 | };
13 | 
14 | let app = null;
15 | 
16 | export const getSuggestionValue = sinon.spy(suggestion => {
17 |   return suggestion.name;
18 | });
19 | 
20 | export const renderSuggestion = sinon.spy(suggestion => {
21 |   return <span>{suggestion.name}</span>;
22 | });
23 | 
24 | export const onChange = sinon.spy((event, { newValue }) => {
25 |   app.setState({
26 |     value: newValue
27 |   });
28 | });
29 | 
30 | export const onSuggestionsFetchRequested = sinon.spy(({ value }) => {
31 |   app.setState({
32 |     suggestions: getMatchingLanguages(value)
33 |   });
34 | });
35 | 
36 | export const onSuggestionSelected = sinon.spy();
37 | 
38 | export default class AutosuggestApp extends Component {
39 |   constructor() {
40 |     super();
41 | 
42 |     app = this;
43 | 
44 |     this.state = {
45 |       value: '',
46 |       suggestions: getMatchingLanguages('')
47 |     };
48 |   }
49 | 
50 |   render() {
51 |     const { value, suggestions } = this.state;
52 |     const inputProps = {
53 |       value,
54 |       onChange
55 |     };
56 | 
57 |     return (
58 |       <Autosuggest
59 |         suggestions={suggestions}
60 |         onSuggestionsFetchRequested={onSuggestionsFetchRequested}
61 |         onSuggestionSelected={onSuggestionSelected}
62 |         getSuggestionValue={getSuggestionValue}
63 |         renderSuggestion={renderSuggestion}
64 |         inputProps={inputProps}
65 |         alwaysRenderSuggestions={true}
66 |       />
67 |     );
68 |   }
69 | }
70 | 


--------------------------------------------------------------------------------
/test/always-render-suggestions/AutosuggestApp.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 |   expectInputValue,
  7 |   expectSuggestions,
  8 |   expectHighlightedSuggestion,
  9 |   clickSuggestion,
 10 |   focusInput,
 11 |   blurInput,
 12 |   clickEscape,
 13 |   clickEnter,
 14 |   clickCombinedCharacterEnter,
 15 |   clickDown,
 16 |   clickUp,
 17 |   focusAndSetInputValue
 18 | } from '../helpers';
 19 | import AutosuggestApp, { onSuggestionsFetchRequested } from './AutosuggestApp';
 20 | 
 21 | const allSuggestions = [
 22 |   'C',
 23 |   'C#',
 24 |   'C++',
 25 |   'Clojure',
 26 |   'Elm',
 27 |   'Go',
 28 |   'Haskell',
 29 |   'Java',
 30 |   'JavaScript',
 31 |   'Perl',
 32 |   'PHP',
 33 |   'Python',
 34 |   'Ruby',
 35 |   'Scala'
 36 | ];
 37 | 
 38 | describe('Autosuggest with alwaysRenderSuggestions={true}', () => {
 39 |   beforeEach(() => {
 40 |     init(TestUtils.renderIntoDocument(<AutosuggestApp />));
 41 |   });
 42 | 
 43 |   describe('initially', () => {
 44 |     it('should render suggestions', () => {
 45 |       expectSuggestions(allSuggestions);
 46 |     });
 47 |   });
 48 | 
 49 |   describe('when empty input is focused', () => {
 50 |     it('should render suggestions', () => {
 51 |       focusInput();
 52 |       expectSuggestions(allSuggestions);
 53 |     });
 54 |   });
 55 | 
 56 |   describe('when empty input is blurred', () => {
 57 |     it('should render suggestions', () => {
 58 |       focusInput();
 59 |       blurInput();
 60 |       expectSuggestions(allSuggestions);
 61 |     });
 62 |   });
 63 | 
 64 |   describe('when typing and matches exist', () => {
 65 |     beforeEach(() => {
 66 |       focusAndSetInputValue('e');
 67 |     });
 68 | 
 69 |     it('should render suggestions', () => {
 70 |       expectSuggestions(['Elm']);
 71 |     });
 72 | 
 73 |     it('should render suggestions when input is blurred', () => {
 74 |       blurInput();
 75 |       expectSuggestions(['Elm']);
 76 |     });
 77 |   });
 78 | 
 79 |   describe('when pressing Down', () => {
 80 |     it('should highlight the first suggestion', () => {
 81 |       focusAndSetInputValue('p');
 82 |       clickDown();
 83 |       expectHighlightedSuggestion('Perl');
 84 |     });
 85 |   });
 86 | 
 87 |   describe('when pressing Up', () => {
 88 |     it('should highlight the last suggestion', () => {
 89 |       focusAndSetInputValue('p');
 90 |       clickUp();
 91 |       expectHighlightedSuggestion('Python');
 92 |     });
 93 |   });
 94 | 
 95 |   describe('when pressing Enter', () => {
 96 |     beforeEach(() => {
 97 |       focusAndSetInputValue('p');
 98 |     });
 99 | 
100 |     it('should update suggestions if there is a highlighted suggestion', () => {
101 |       clickDown();
102 |       clickEnter();
103 |       expectSuggestions(['Perl']);
104 |     });
105 | 
106 |     it('should not hide suggestions if there is no highlighted suggestion', () => {
107 |       clickEnter();
108 |       expectSuggestions(['Perl', 'PHP', 'Python']);
109 |     });
110 | 
111 |     it('should not hide suggestions if enter event for combined character', () => {
112 |       clickCombinedCharacterEnter();
113 |       expectSuggestions(['Perl', 'PHP', 'Python']);
114 |     });
115 |   });
116 | 
117 |   describe('when pressing Escape', () => {
118 |     beforeEach(() => {
119 |       focusAndSetInputValue('p');
120 |     });
121 | 
122 |     describe('without prior Up/Down interaction', () => {
123 |       it('should clear the input', () => {
124 |         clickEscape();
125 |         expectInputValue('');
126 |       });
127 |     });
128 | 
129 |     describe('after Up/Down interaction', () => {
130 |       beforeEach(() => {
131 |         clickDown();
132 |         clickEscape();
133 |       });
134 | 
135 |       it('should revert input value', () => {
136 |         expectInputValue('p');
137 |       });
138 | 
139 |       it('should reset the highlighted suggestion', () => {
140 |         expectHighlightedSuggestion(null);
141 |       });
142 | 
143 |       it('should clear the input when Escape is pressed again', () => {
144 |         clickEscape();
145 |         expectInputValue('');
146 |       });
147 |     });
148 |   });
149 | 
150 |   describe('when suggestion is clicked', () => {
151 |     it('should not hide suggestions', () => {
152 |       focusAndSetInputValue('p');
153 |       clickSuggestion(1);
154 |       expectSuggestions(['PHP']);
155 |     });
156 | 
157 |     it('should reset the highlighted suggestion', () => {
158 |       focusAndSetInputValue('j');
159 |       clickSuggestion(1);
160 |       clickDown();
161 |       expectHighlightedSuggestion('JavaScript');
162 |     });
163 |   });
164 | 
165 |   describe('onSuggestionsFetchRequested', () => {
166 |     it('should be called once with the right parameters when suggestion is selected', () => {
167 |       focusAndSetInputValue('j');
168 |       onSuggestionsFetchRequested.resetHistory();
169 |       clickSuggestion(1);
170 |       expect(onSuggestionsFetchRequested).to.have.been.calledOnce;
171 |       expect(onSuggestionsFetchRequested).to.have.been.calledWithExactly({
172 |         value: 'JavaScript',
173 |         reason: 'suggestion-selected'
174 |       });
175 |     });
176 |   });
177 | });
178 | 


--------------------------------------------------------------------------------
/test/async-suggestions/AutosuggestApp.js:
--------------------------------------------------------------------------------
 1 | import React, { Component } from 'react';
 2 | import sinon from 'sinon';
 3 | import Autosuggest from '../../src/Autosuggest';
 4 | import languages from '../plain-list/languages';
 5 | import { escapeRegexCharacters } from '../../demo/src/components/utils/utils.js';
 6 | 
 7 | const getMatchingLanguages = value => {
 8 |   const escapedValue = escapeRegexCharacters(value.trim());
 9 | 
10 |   if (escapedValue === '') {
11 |     return [];
12 |   }
13 | 
14 |   const regex = new RegExp('^' + escapedValue, 'i');
15 | 
16 |   return languages.filter(language => regex.test(language.name));
17 | };
18 | 
19 | let app = null;
20 | 
21 | export const getSuggestionValue = sinon.spy(suggestion => suggestion.name);
22 | 
23 | export const renderSuggestion = sinon.spy(suggestion => suggestion.name);
24 | 
25 | export const onChange = sinon.spy((event, { newValue }) => {
26 |   app.setState({
27 |     value: newValue
28 |   });
29 | });
30 | 
31 | const loadSuggestions = value => {
32 |   setTimeout(() => {
33 |     if (value === app.state.value) {
34 |       app.setState({
35 |         suggestions: getMatchingLanguages(value)
36 |       });
37 |     }
38 |   }, 100);
39 | };
40 | 
41 | export const onSuggestionsFetchRequested = sinon.spy(({ value }) => {
42 |   loadSuggestions(value);
43 | });
44 | 
45 | export const onSuggestionsClearRequested = sinon.spy(() => {
46 |   app.setState({
47 |     suggestions: []
48 |   });
49 | });
50 | 
51 | export default class AutosuggestApp extends Component {
52 |   constructor() {
53 |     super();
54 | 
55 |     app = this;
56 | 
57 |     this.state = {
58 |       value: '',
59 |       suggestions: []
60 |     };
61 |   }
62 | 
63 |   render() {
64 |     const { value, suggestions } = this.state;
65 |     const inputProps = {
66 |       value,
67 |       onChange
68 |     };
69 | 
70 |     return (
71 |       <Autosuggest
72 |         suggestions={suggestions}
73 |         onSuggestionsFetchRequested={onSuggestionsFetchRequested}
74 |         onSuggestionsClearRequested={onSuggestionsClearRequested}
75 |         getSuggestionValue={getSuggestionValue}
76 |         renderSuggestion={renderSuggestion}
77 |         inputProps={inputProps}
78 |         highlightFirstSuggestion={true}
79 |       />
80 |     );
81 |   }
82 | }
83 | 


--------------------------------------------------------------------------------
/test/async-suggestions/AutosuggestApp.test.js:
--------------------------------------------------------------------------------
 1 | import React from 'react';
 2 | import TestUtils from 'react-dom/test-utils';
 3 | import {
 4 |   init,
 5 |   tick,
 6 |   expectSuggestions,
 7 |   clickSuggestion,
 8 |   clickEnter,
 9 |   clickDown,
10 |   setInputValue,
11 |   focusAndSetInputValue
12 | } from '../helpers';
13 | import AutosuggestApp from './AutosuggestApp';
14 | 
15 | describe('Autosuggest that gets suggestions asynchronously', () => {
16 |   beforeEach(() => {
17 |     init(TestUtils.renderIntoDocument(<AutosuggestApp />));
18 |   });
19 | 
20 |   describe('when typing and matches exist', () => {
21 |     beforeEach(() => {
22 |       focusAndSetInputValue('j');
23 |       tick(100);
24 |     });
25 | 
26 |     it('should show suggestions when they arrive', () => {
27 |       expectSuggestions(['Java', 'JavaScript']);
28 |     });
29 | 
30 |     it('should not show previous suggestions when revealing suggestions', () => {
31 |       setInputValue('');
32 |       setInputValue('p');
33 |       expectSuggestions([]);
34 |     });
35 |   });
36 | 
37 |   describe('when suggestion is clicked', () => {
38 |     beforeEach(() => {
39 |       focusAndSetInputValue('p');
40 |       tick(100);
41 |     });
42 | 
43 |     it('should hide suggestions', () => {
44 |       clickSuggestion(1);
45 |       tick(500);
46 |       expectSuggestions([]);
47 |     });
48 |   });
49 | 
50 |   describe('when pressing Enter', () => {
51 |     beforeEach(() => {
52 |       focusAndSetInputValue('p');
53 |       tick(100);
54 |     });
55 | 
56 |     it('should hide suggestions', () => {
57 |       clickDown();
58 |       clickEnter();
59 |       tick(500);
60 |       expectSuggestions([]);
61 |     });
62 | 
63 |     it('should not error if suggestions were cleared after having suggestions', () => {
64 |       focusAndSetInputValue('pz');
65 |       tick(100);
66 |       clickEnter();
67 |       expectSuggestions([]);
68 |     });
69 |   });
70 | });
71 | 


--------------------------------------------------------------------------------
/test/autowhatever/autowhatever-helpers.js:
--------------------------------------------------------------------------------
 1 | import chai from 'chai';
 2 | import sinonChai from 'sinon-chai';
 3 | import TestUtils, { Simulate } from 'react-dom/test-utils';
 4 | 
 5 | chai.use(sinonChai);
 6 | 
 7 | let app, container, input, itemsContainer;
 8 | 
 9 | export const init = (application) => {
10 |   app = application;
11 |   container = TestUtils.findRenderedDOMComponentWithClass(
12 |     app,
13 |     'react-autowhatever__container'
14 |   );
15 |   input = TestUtils.findRenderedDOMComponentWithTag(app, 'input');
16 |   itemsContainer = TestUtils.findRenderedDOMComponentWithClass(
17 |     app,
18 |     'react-autowhatever__items-container'
19 |   );
20 | };
21 | 
22 | export const getElementWithClass = (className) =>
23 |   TestUtils.findRenderedDOMComponentWithClass(app, className);
24 | 
25 | export const getStoredInput = () => app.autowhatever.input;
26 | export const getStoredItemsContainer = () => app.autowhatever.itemsContainer;
27 | export const getStoredHighlightedItem = () => app.autowhatever.highlightedItem;
28 | export const getInputRef = () => app.inputRef;
29 | 
30 | export const getContainerAttribute = (attr) => container.getAttribute(attr);
31 | 
32 | export const getInputAttribute = (attr) => input.getAttribute(attr);
33 | 
34 | export const getItemsContainerAttribute = (attr) =>
35 |   itemsContainer.getAttribute(attr);
36 | 
37 | export const getItems = () =>
38 |   TestUtils.scryRenderedDOMComponentsWithClass(app, 'react-autowhatever__item');
39 | 
40 | export const getItem = (itemIndex) => {
41 |   const items = getItems();
42 | 
43 |   if (itemIndex >= items.length) {
44 |     throw Error(`Cannot find item #${itemIndex}`);
45 |   }
46 | 
47 |   return items[itemIndex];
48 | };
49 | 
50 | export const mouseEnterItem = (itemIndex) =>
51 |   Simulate.mouseEnter(getItem(itemIndex));
52 | 
53 | export const mouseLeaveItem = (itemIndex) =>
54 |   Simulate.mouseLeave(getItem(itemIndex));
55 | 
56 | export const mouseDownItem = (itemIndex) =>
57 |   Simulate.mouseDown(getItem(itemIndex));
58 | 
59 | export const clickItem = (itemIndex) => Simulate.click(getItem(itemIndex));
60 | 
61 | export const clickUp = () =>
62 |   Simulate.keyDown(input, { key: 'ArrowUp', keyCode: 38 });
63 | 
64 | export const clickDown = () =>
65 |   Simulate.keyDown(input, { key: 'ArrowDown', keyCode: 40 });
66 | 
67 | export const clickEnter = () =>
68 |   Simulate.keyDown(input, { key: 'Enter', keyCode: 13 });
69 | 


--------------------------------------------------------------------------------
/test/autowhatever/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(
117 |       objA
118 |     )} and ${JSON.stringify(objB)}${keys ? ` with keys ${keys}` : ''}`, () => {
119 |       expect(compareObjects(objA, objB, keys)).to.equal(result);
120 |     });
121 |   });
122 | });
123 | 


--------------------------------------------------------------------------------
/test/autowhatever/default-props/Autowhatever.test.js:
--------------------------------------------------------------------------------
 1 | import React from 'react';
 2 | import TestUtils from 'react-dom/test-utils';
 3 | import { expect } from 'chai';
 4 | import sinon from 'sinon';
 5 | import AutowhateverApp from './AutowhateverApp';
 6 | 
 7 | describe('Autowhatever default props', () => {
 8 |   const render = (props) => {
 9 |     TestUtils.renderIntoDocument(<AutowhateverApp {...props} />);
10 |   };
11 | 
12 |   describe('should throw', () => {
13 |     sinon.stub(console, 'error');
14 | 
15 |     it('if renderItem is not provided', () => {
16 |       const renderWithoutRenderItems = () =>
17 |         render({
18 |           renderSectionTitle: () => 'Section',
19 |           getSectionItems: () => ['item'],
20 |         });
21 | 
22 |       expect(renderWithoutRenderItems).to.throw('renderItem');
23 |     });
24 | 
25 |     it('if renderSectionTitle is not provided', () => {
26 |       const renderWithoutRenderItems = () =>
27 |         render({ getSectionItems: () => ['item'], renderItem: () => null });
28 | 
29 |       expect(renderWithoutRenderItems).to.throw('renderSectionTitle');
30 |     });
31 | 
32 |     it('if getSectionItems is not provided', () => {
33 |       const renderWithoutRenderItems = () =>
34 |         render({ renderSectionTitle: () => 'Section' });
35 | 
36 |       expect(renderWithoutRenderItems).to.throw('getSectionItems');
37 |     });
38 |   });
39 | });
40 | 


--------------------------------------------------------------------------------
/test/autowhatever/default-props/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 onKeyDown = sinon.spy(
 9 |   (event, { newHighlightedSectionIndex, newHighlightedItemIndex }) => {
10 |     if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
11 |       app.setState({
12 |         highlightedSectionIndex: newHighlightedSectionIndex,
13 |         highlightedItemIndex: newHighlightedItemIndex,
14 |       });
15 |     }
16 |   }
17 | );
18 | 
19 | export default class AutowhateverApp extends Component {
20 |   static getDerivedStateFromError() {}
21 | 
22 |   constructor() {
23 |     super();
24 | 
25 |     app = this;
26 | 
27 |     this.state = {
28 |       value: '',
29 |       highlightedSectionIndex: null,
30 |       highlightedItemIndex: null,
31 |     };
32 |   }
33 | 
34 |   storeAutowhateverReference = (autowhatever) => {
35 |     if (autowhatever !== null) {
36 |       this.autowhatever = autowhatever;
37 |     }
38 |   };
39 | 
40 |   onChange = (event) => {
41 |     this.setState({
42 |       value: event.target.value,
43 |     });
44 |   };
45 | 
46 |   render() {
47 |     const { renderSectionTitle, getSectionItems, renderItem } = this.props;
48 |     const { value, highlightedSectionIndex, highlightedItemIndex } = this.state;
49 |     const inputProps = {
50 |       value,
51 |       onChange: this.onChange,
52 |       onKeyDown,
53 |     };
54 | 
55 |     return (
56 |       <Autowhatever
57 |         multiSection={true}
58 |         items={sections}
59 |         inputProps={inputProps}
60 |         highlightedSectionIndex={highlightedSectionIndex}
61 |         highlightedItemIndex={highlightedItemIndex}
62 |         ref={this.storeAutowhateverReference}
63 |         renderSectionTitle={renderSectionTitle}
64 |         getSectionItems={getSectionItems}
65 |         renderItem={renderItem}
66 |       />
67 |     );
68 |   }
69 | }
70 | 


--------------------------------------------------------------------------------
/test/autowhatever/default-props/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/autowhatever/function-ref/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, getInputRef } from '../autowhatever-helpers';
 5 | import AutowhateverApp from './AutowhateverApp';
 6 | 
 7 | describe('Autowhatever with functional ref', () => {
 8 |   beforeEach(() => {
 9 |     init(TestUtils.renderIntoDocument(<AutowhateverApp />));
10 |   });
11 | 
12 |   it('should store the ref', () => {
13 |     expect(getStoredInput()).to.equal(getInputRef());
14 |   });
15 | });
16 | 


--------------------------------------------------------------------------------
/test/autowhatever/function-ref/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 default class AutowhateverApp extends Component {
 8 |   constructor() {
 9 |     super();
10 | 
11 |     this.state = {
12 |       value: '',
13 |     };
14 |   }
15 | 
16 |   storeAutowhateverReference = (autowhatever) => {
17 |     if (autowhatever !== null) {
18 |       this.autowhatever = autowhatever; // used by the getStoredInput() helper
19 |     }
20 |   };
21 | 
22 |   storeInputReference = (input) => {
23 |     this.inputRef = input;
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 |       ref: this.storeInputReference,
39 |     };
40 | 
41 |     return (
42 |       <Autowhatever
43 |         items={items}
44 |         renderItem={renderItem}
45 |         inputProps={inputProps}
46 |         ref={this.storeAutowhateverReference}
47 |       />
48 |     );
49 |   }
50 | }
51 | 


--------------------------------------------------------------------------------
/test/autowhatever/function-ref/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/autowhatever/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 { init, clickUp, clickDown, clickEnter } from '../autowhatever-helpers';
  6 | import { syntheticEventMatcher } from '../../helpers';
  7 | import AutowhateverApp, {
  8 |   getSectionItems,
  9 |   renderSectionTitle,
 10 |   onKeyDown,
 11 | } from './AutowhateverApp';
 12 | 
 13 | describe('Multi Section Autowhatever', () => {
 14 |   beforeEach(() => {
 15 |     getSectionItems.resetHistory();
 16 |     renderSectionTitle.resetHistory();
 17 |     onKeyDown.resetHistory();
 18 |     init(TestUtils.renderIntoDocument(<AutowhateverApp />));
 19 |   });
 20 | 
 21 |   describe('renderSectionTitle', () => {
 22 |     it('should be called once for every section', () => {
 23 |       expect(renderSectionTitle).to.have.callCount(3);
 24 |       expect(renderSectionTitle.getCall(0).args[0]).to.deep.equal(sections[0]);
 25 |       expect(renderSectionTitle.getCall(1).args[0]).to.deep.equal(sections[1]);
 26 |       expect(renderSectionTitle.getCall(2).args[0]).to.deep.equal(sections[2]);
 27 |     });
 28 | 
 29 |     it('should not be called when Down is pressed', () => {
 30 |       renderSectionTitle.resetHistory();
 31 |       clickDown();
 32 |       expect(renderSectionTitle).not.to.have.been.called;
 33 |     });
 34 |   });
 35 | 
 36 |   describe('getSectionItems', () => {
 37 |     it('should be called once for every section', () => {
 38 |       expect(getSectionItems).to.have.callCount(3);
 39 |       expect(getSectionItems.getCall(0).args[0]).to.deep.equal(sections[0]);
 40 |       expect(getSectionItems.getCall(1).args[0]).to.deep.equal(sections[1]);
 41 |       expect(getSectionItems.getCall(2).args[0]).to.deep.equal(sections[2]);
 42 |     });
 43 | 
 44 |     it('should not be called when Down is pressed', () => {
 45 |       getSectionItems.resetHistory();
 46 |       clickDown();
 47 |       expect(getSectionItems).not.to.have.been.called;
 48 |     });
 49 |   });
 50 | 
 51 |   describe('inputProps.onKeyDown', () => {
 52 |     it('should be called with the right parameters when Up/Down is pressed', () => {
 53 |       clickDown();
 54 |       expect(onKeyDown).to.be.calledOnce;
 55 |       expect(onKeyDown).to.be.calledWith(syntheticEventMatcher, {
 56 |         newHighlightedSectionIndex: 0,
 57 |         newHighlightedItemIndex: 0,
 58 |       });
 59 | 
 60 |       clickDown();
 61 |       expect(onKeyDown).to.be.calledWith(syntheticEventMatcher, {
 62 |         newHighlightedSectionIndex: 0,
 63 |         newHighlightedItemIndex: 1,
 64 |       });
 65 | 
 66 |       clickDown();
 67 |       expect(onKeyDown).to.be.calledWith(syntheticEventMatcher, {
 68 |         newHighlightedSectionIndex: 1,
 69 |         newHighlightedItemIndex: 0,
 70 |       });
 71 | 
 72 |       clickDown();
 73 |       expect(onKeyDown).to.be.calledWith(syntheticEventMatcher, {
 74 |         newHighlightedSectionIndex: 2,
 75 |         newHighlightedItemIndex: 0,
 76 |       });
 77 | 
 78 |       clickDown();
 79 |       expect(onKeyDown).to.be.calledWith(syntheticEventMatcher, {
 80 |         newHighlightedSectionIndex: null,
 81 |         newHighlightedItemIndex: null,
 82 |       });
 83 |     });
 84 | 
 85 |     it('should be called with the right parameters when Enter is pressed', () => {
 86 |       clickEnter();
 87 |       expect(onKeyDown).to.be.calledWith(syntheticEventMatcher, {
 88 |         highlightedSectionIndex: null,
 89 |         highlightedItemIndex: null,
 90 |       });
 91 | 
 92 |       clickUp();
 93 |       clickEnter();
 94 |       expect(onKeyDown).to.be.calledWith(syntheticEventMatcher, {
 95 |         highlightedSectionIndex: 2,
 96 |         highlightedItemIndex: 0,
 97 |       });
 98 |     });
 99 |   });
100 | });
101 | 


--------------------------------------------------------------------------------
/test/autowhatever/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 |   <strong>{section.title}</strong>
10 | ));
11 | 
12 | export const getSectionItems = sinon.spy((section) => section.items);
13 | 
14 | export const renderItem = sinon.spy((item) => <span>{item.text}</span>);
15 | 
16 | export const onKeyDown = sinon.spy(
17 |   (event, { newHighlightedSectionIndex, newHighlightedItemIndex }) => {
18 |     if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
19 |       app.setState({
20 |         highlightedSectionIndex: newHighlightedSectionIndex,
21 |         highlightedItemIndex: newHighlightedItemIndex,
22 |       });
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 |       <Autowhatever
62 |         multiSection={true}
63 |         items={sections}
64 |         renderSectionTitle={renderSectionTitle}
65 |         getSectionItems={getSectionItems}
66 |         renderItem={renderItem}
67 |         inputProps={inputProps}
68 |         highlightedSectionIndex={highlightedSectionIndex}
69 |         highlightedItemIndex={highlightedItemIndex}
70 |         ref={this.storeAutowhateverReference}
71 |       />
72 |     );
73 |   }
74 | }
75 | 


--------------------------------------------------------------------------------
/test/autowhatever/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/autowhatever/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 |   getInputRef,
 19 | } from '../autowhatever-helpers';
 20 | import AutowhateverApp, { renderItem } from './AutowhateverApp';
 21 | 
 22 | describe('Plain List Autowhatever', () => {
 23 |   beforeEach(() => {
 24 |     renderItem.resetHistory();
 25 |     init(TestUtils.renderIntoDocument(<AutowhateverApp />));
 26 |   });
 27 | 
 28 |   describe('initially', () => {
 29 |     it("should set container's `aria-owns` to items container's `id`", () => {
 30 |       expect(getContainerAttribute('aria-owns')).to.equal(
 31 |         getItemsContainerAttribute('id')
 32 |       );
 33 |     });
 34 | 
 35 |     it("should set input's `aria-controls` to items container's `id`", () => {
 36 |       expect(getInputAttribute('aria-controls')).to.equal(
 37 |         getItemsContainerAttribute('id')
 38 |       );
 39 |     });
 40 | 
 41 |     it('should render all items', () => {
 42 |       expect(getItems()).to.be.of.length(5);
 43 |     });
 44 | 
 45 |     it('should call `renderItem` exactly `items.length` times', () => {
 46 |       expect(renderItem).to.have.callCount(5);
 47 |     });
 48 | 
 49 |     it('should store the input on the instance', () => {
 50 |       expect(getStoredInput().getAttribute('id')).to.equal('my-fancy-input');
 51 |     });
 52 | 
 53 |     it('should store the items container on the instance', () => {
 54 |       expect(getStoredItemsContainer().getAttribute('id')).to.equal(
 55 |         'react-autowhatever-my-fancy-component'
 56 |       );
 57 |     });
 58 | 
 59 |     it('should set the stored highlighted item on the instance to null', () => {
 60 |       expect(getStoredHighlightedItem()).to.equal(null);
 61 |     });
 62 | 
 63 |     it('should set current input ref', () => {
 64 |       expect(getStoredInput()).to.equal(getInputRef().current);
 65 |     });
 66 |   });
 67 | 
 68 |   describe('hovering items', () => {
 69 |     it('should call `renderItem` once with the right parameters when item is entered', () => {
 70 |       renderItem.resetHistory();
 71 |       mouseEnterItem(0);
 72 |       expect(renderItem).to.have.been.calledOnce;
 73 |       expect(renderItem).to.be.calledWith({ text: 'Apple' });
 74 |     });
 75 | 
 76 |     it('should call `renderItem` twice when the highlighted item is changed', () => {
 77 |       mouseEnterItem(1);
 78 |       renderItem.resetHistory();
 79 |       mouseLeaveItem(1);
 80 |       mouseEnterItem(2);
 81 |       expect(renderItem).to.have.been.calledTwice;
 82 |     });
 83 | 
 84 |     it('should call `renderItem` with `isHighlighted` flag', () => {
 85 |       renderItem.resetHistory();
 86 |       mouseEnterItem(0);
 87 |       expect(renderItem).to.have.been.calledOnce;
 88 |       expect(renderItem).to.be.calledWith(
 89 |         { text: 'Apple' },
 90 |         { isHighlighted: true }
 91 |       );
 92 | 
 93 |       renderItem.resetHistory();
 94 |       mouseLeaveItem(0);
 95 |       expect(renderItem).to.have.been.calledOnce;
 96 |       expect(renderItem).to.be.calledWith(
 97 |         { text: 'Apple' },
 98 |         { isHighlighted: false }
 99 |       );
100 |     });
101 | 
102 |     it('should set `aria-selected` to true on highlighted items', () => {
103 |       renderItem.resetHistory();
104 |       mouseEnterItem(0);
105 |       expect(getItem(0).getAttribute('aria-selected')).to.equal('true');
106 | 
107 |       renderItem.resetHistory();
108 |       mouseLeaveItem(0);
109 |       expect(getItem(0).getAttribute('aria-selected')).to.equal('false');
110 |     });
111 | 
112 |     it('should call `renderItem` once when item is left', () => {
113 |       mouseEnterItem(3);
114 |       renderItem.resetHistory();
115 |       mouseLeaveItem(3);
116 |       expect(renderItem).to.have.been.calledOnce;
117 |     });
118 | 
119 |     it('should not call `renderItem` when item is clicked', () => {
120 |       renderItem.resetHistory();
121 |       mouseDownItem(4);
122 |       clickItem(4);
123 |       expect(renderItem).not.to.have.been.called;
124 |     });
125 | 
126 |     it('should store the highlighted item on the instance', () => {
127 |       mouseEnterItem(2);
128 |       expect(getStoredHighlightedItem().getAttribute('id')).to.equal(
129 |         'react-autowhatever-my-fancy-component--item-2'
130 |       );
131 | 
132 |       mouseLeaveItem(2);
133 |       expect(getStoredHighlightedItem()).to.equal(null);
134 | 
135 |       mouseEnterItem(3);
136 |       expect(getStoredHighlightedItem().getAttribute('id')).to.equal(
137 |         'react-autowhatever-my-fancy-component--item-3'
138 |       );
139 |     });
140 |   });
141 | });
142 | 


--------------------------------------------------------------------------------
/test/autowhatever/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) => <span>{item.text}</span>);
 7 | 
 8 | export default class AutowhateverApp extends Component {
 9 |   constructor() {
10 |     super();
11 | 
12 |     this.state = {
13 |       value: '',
14 |       highlightedItemIndex: null,
15 |     };
16 | 
17 |     this.inputRef = React.createRef();
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 |       ref: this.inputRef,
57 |     };
58 |     const itemProps = {
59 |       onMouseEnter: this.onMouseEnter,
60 |       onMouseLeave: this.onMouseLeave,
61 |       onClick: this.onClick,
62 |     };
63 | 
64 |     return (
65 |       <Autowhatever
66 |         id="my-fancy-component"
67 |         items={items}
68 |         renderItem={renderItem}
69 |         inputProps={inputProps}
70 |         itemProps={itemProps}
71 |         highlightedItemIndex={highlightedItemIndex}
72 |         ref={this.storeAutowhateverReference}
73 |       />
74 |     );
75 |   }
76 | }
77 | 


--------------------------------------------------------------------------------
/test/autowhatever/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/autowhatever/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 |   getItemsContainerAttribute,
 7 |   getElementWithClass,
 8 | } from '../autowhatever-helpers';
 9 | import { childrenMatcher, containerPropsMatcher } from '../../helpers';
10 | import AutowhateverApp, { renderItemsContainer } from './AutowhateverApp';
11 | 
12 | describe('Autowhatever with renderItemsContainer', () => {
13 |   beforeEach(() => {
14 |     renderItemsContainer.resetHistory();
15 |     init(TestUtils.renderIntoDocument(<AutowhateverApp />));
16 |   });
17 | 
18 |   it('should set items container id properly', () => {
19 |     expect(getItemsContainerAttribute('id')).to.equal(
20 |       'react-autowhatever-my-id'
21 |     );
22 |   });
23 | 
24 |   it('should render whatever renderItemsContainer returns', () => {
25 |     expect(getElementWithClass('my-items-container-footer')).not.to.equal(null);
26 |   });
27 | 
28 |   it('should call renderItemsContainer once with the right parameters', () => {
29 |     expect(renderItemsContainer).to.have.been.calledOnce;
30 |     expect(renderItemsContainer).to.be.calledWith({
31 |       children: childrenMatcher,
32 |       containerProps: containerPropsMatcher,
33 |     });
34 |   });
35 | });
36 | 


--------------------------------------------------------------------------------
/test/autowhatever/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(
 9 |   ({ containerProps, children }) => (
10 |     <div {...containerProps}>
11 |       {children}
12 |       <div className="my-items-container-footer">Footer</div>
13 |     </div>
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 |       <Autowhatever
42 |         id="my-id"
43 |         renderItemsContainer={renderItemsContainer}
44 |         items={items}
45 |         renderItem={renderItem}
46 |         inputProps={inputProps}
47 |       />
48 |     );
49 |   }
50 | }
51 | 


--------------------------------------------------------------------------------
/test/autowhatever/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/do-not-focus-input-on-suggestion-click/AutosuggestApp.js:
--------------------------------------------------------------------------------
 1 | import React, { Component } from 'react';
 2 | import sinon from 'sinon';
 3 | import Autosuggest from '../../src/Autosuggest';
 4 | import languages from '../plain-list/languages';
 5 | import { escapeRegexCharacters } from '../../demo/src/components/utils/utils.js';
 6 | 
 7 | const getMatchingLanguages = value => {
 8 |   const escapedValue = escapeRegexCharacters(value.trim());
 9 |   const regex = new RegExp('^' + escapedValue, 'i');
10 | 
11 |   return languages.filter(language => regex.test(language.name));
12 | };
13 | 
14 | let app = null;
15 | 
16 | export const getSuggestionValue = sinon.spy(suggestion => {
17 |   return suggestion.name;
18 | });
19 | 
20 | export const renderSuggestion = sinon.spy(suggestion => {
21 |   return <span>{suggestion.name}</span>;
22 | });
23 | 
24 | export const onChange = sinon.spy((event, { newValue }) => {
25 |   app.setState({
26 |     value: newValue
27 |   });
28 | });
29 | 
30 | export const onBlur = sinon.spy();
31 | 
32 | export const onSuggestionsFetchRequested = sinon.spy(({ value }) => {
33 |   app.setState({
34 |     suggestions: getMatchingLanguages(value)
35 |   });
36 | });
37 | 
38 | export const onSuggestionsClearRequested = sinon.spy(() => {
39 |   app.setState({
40 |     suggestions: []
41 |   });
42 | });
43 | 
44 | export const onSuggestionSelected = sinon.spy();
45 | 
46 | export default class AutosuggestApp extends Component {
47 |   constructor() {
48 |     super();
49 | 
50 |     app = this;
51 | 
52 |     this.state = {
53 |       value: '',
54 |       suggestions: []
55 |     };
56 |   }
57 | 
58 |   render() {
59 |     const { value, suggestions } = this.state;
60 |     const inputProps = {
61 |       value,
62 |       onChange,
63 |       onBlur
64 |     };
65 | 
66 |     return (
67 |       <Autosuggest
68 |         suggestions={suggestions}
69 |         onSuggestionsFetchRequested={onSuggestionsFetchRequested}
70 |         onSuggestionsClearRequested={onSuggestionsClearRequested}
71 |         getSuggestionValue={getSuggestionValue}
72 |         renderSuggestion={renderSuggestion}
73 |         inputProps={inputProps}
74 |         onSuggestionSelected={onSuggestionSelected}
75 |         focusInputOnSuggestionClick={false}
76 |       />
77 |     );
78 |   }
79 | }
80 | 


--------------------------------------------------------------------------------
/test/do-not-focus-input-on-suggestion-click/AutosuggestApp.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 |   syntheticEventMatcher,
 7 |   clickSuggestion,
 8 |   focusAndSetInputValue,
 9 |   isInputFocused,
10 |   mouseUpDocument
11 | } from '../helpers';
12 | import AutosuggestApp, {
13 |   onBlur,
14 |   onSuggestionsClearRequested
15 | } from './AutosuggestApp';
16 | 
17 | describe('Autosuggest with focusInputOnSuggestionClick={false}', () => {
18 |   beforeEach(() => {
19 |     init(TestUtils.renderIntoDocument(<AutosuggestApp />));
20 |   });
21 | 
22 |   describe('when suggestion is clicked', () => {
23 |     beforeEach(() => {
24 |       focusAndSetInputValue('p');
25 |       onBlur.resetHistory();
26 |       onSuggestionsClearRequested.resetHistory();
27 |       clickSuggestion(1);
28 |     });
29 | 
30 |     it('should not focus on input', () => {
31 |       expect(isInputFocused()).to.equal(false);
32 |     });
33 | 
34 |     it('should call onBlur once with the right parameters', () => {
35 |       expect(onBlur).to.have.been.calledOnce;
36 |       expect(onBlur).to.have.been.calledWithExactly(syntheticEventMatcher, {
37 |         highlightedSuggestion: { name: 'PHP', year: 1995 }
38 |       });
39 |     });
40 | 
41 |     it('should call onSuggestionsClearRequested once', () => {
42 |       expect(onSuggestionsClearRequested).to.have.been.calledOnce;
43 |     });
44 | 
45 |     it('should not focus input on document mouse up', () => {
46 |       mouseUpDocument();
47 |       expect(isInputFocused()).to.equal(false);
48 |     });
49 |   });
50 | });
51 | 


--------------------------------------------------------------------------------
/test/focus-first-suggestion-clear-on-enter/AutosuggestApp.js:
--------------------------------------------------------------------------------
 1 | import React, { Component } from 'react';
 2 | import sinon from 'sinon';
 3 | import Autosuggest from '../../src/Autosuggest';
 4 | import languages from '../plain-list/languages';
 5 | import { escapeRegexCharacters } from '../../demo/src/components/utils/utils.js';
 6 | import { addEvent } from '../helpers';
 7 | 
 8 | const getMatchingLanguages = value => {
 9 |   const escapedValue = escapeRegexCharacters(value.trim());
10 |   const regex = new RegExp('^' + escapedValue, 'i');
11 | 
12 |   return languages.filter(language => regex.test(language.name));
13 | };
14 | 
15 | let app = null;
16 | 
17 | export const getSuggestionValue = suggestion => suggestion.name;
18 | 
19 | export const renderSuggestion = suggestion => <span>{suggestion.name}</span>;
20 | 
21 | export const onChange = sinon.spy((event, { newValue }) => {
22 |   addEvent('onChange');
23 | 
24 |   app.setState({
25 |     value: newValue
26 |   });
27 | });
28 | 
29 | export const onSuggestionsFetchRequested = ({ value }) => {
30 |   app.setState({
31 |     suggestions: getMatchingLanguages(value)
32 |   });
33 | };
34 | 
35 | export const onSuggestionsClearRequested = () => {
36 |   app.setState({
37 |     suggestions: []
38 |   });
39 | };
40 | 
41 | export const onSuggestionSelected = sinon.spy(() => {
42 |   addEvent('onSuggestionSelected');
43 | 
44 |   app.setState({ value: '' });
45 | });
46 | 
47 | export default class AutosuggestApp extends Component {
48 |   constructor() {
49 |     super();
50 | 
51 |     app = this;
52 | 
53 |     this.state = {
54 |       value: '',
55 |       suggestions: []
56 |     };
57 |   }
58 | 
59 |   render() {
60 |     const { value, suggestions } = this.state;
61 |     const inputProps = {
62 |       value,
63 |       onChange
64 |     };
65 | 
66 |     return (
67 |       <Autosuggest
68 |         suggestions={suggestions.slice()}
69 |         onSuggestionsFetchRequested={onSuggestionsFetchRequested}
70 |         onSuggestionsClearRequested={onSuggestionsClearRequested}
71 |         onSuggestionSelected={onSuggestionSelected}
72 |         getSuggestionValue={getSuggestionValue}
73 |         renderSuggestion={renderSuggestion}
74 |         inputProps={inputProps}
75 |         highlightFirstSuggestion={true}
76 |       />
77 |     );
78 |   }
79 | }
80 | 


--------------------------------------------------------------------------------
/test/focus-first-suggestion-clear-on-enter/AutosuggestApp.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 |   expectInputValue,
 7 |   clickEnter,
 8 |   clickDown,
 9 |   focusAndSetInputValue,
10 |   clearEvents,
11 |   getEvents
12 | } from '../helpers';
13 | import AutosuggestApp, {
14 |   onChange,
15 |   onSuggestionSelected
16 | } from './AutosuggestApp';
17 | 
18 | describe('Autosuggest with highlightFirstSuggestion={true} and clear on Enter', () => {
19 |   beforeEach(() => {
20 |     init(TestUtils.renderIntoDocument(<AutosuggestApp />));
21 |   });
22 | 
23 |   describe('when pressing Enter', () => {
24 |     beforeEach(() => {
25 |       focusAndSetInputValue('c');
26 |     });
27 | 
28 |     it('should clear the input after selecting the first suggestion', () => {
29 |       clickEnter();
30 |       expectInputValue('');
31 |     });
32 | 
33 |     it('should clear the input after selecting the second suggestion', () => {
34 |       clickDown();
35 |       clickEnter();
36 |       expectInputValue('');
37 |     });
38 |   });
39 | 
40 |   describe('onSuggestionSelected', () => {
41 |     beforeEach(() => {
42 |       onSuggestionSelected.resetHistory();
43 |       focusAndSetInputValue('j');
44 |     });
45 | 
46 |     it('should be called after inputProps.onChange when Enter is pressed', () => {
47 |       onChange.resetHistory();
48 |       clearEvents();
49 |       clickEnter();
50 |       expect(getEvents()).to.deep.equal(['onChange', 'onSuggestionSelected']);
51 |     });
52 |   });
53 | });
54 | 


--------------------------------------------------------------------------------
/test/focus-first-suggestion/AutosuggestApp.js:
--------------------------------------------------------------------------------
 1 | import React, { Component } from 'react';
 2 | import sinon from 'sinon';
 3 | import Autosuggest from '../../src/Autosuggest';
 4 | import languages from '../plain-list/languages';
 5 | import { escapeRegexCharacters } from '../../demo/src/components/utils/utils.js';
 6 | 
 7 | const getMatchingLanguages = (value) => {
 8 |   const escapedValue = escapeRegexCharacters(value.trim());
 9 |   const regex = new RegExp('^' + escapedValue, 'i');
10 | 
11 |   return languages.filter((language) => regex.test(language.name));
12 | };
13 | 
14 | let app = null;
15 | 
16 | export const setHighlightFirstSuggestion = (value) => {
17 |   app.setState({
18 |     highlightFirstSuggestion: value
19 |   });
20 | };
21 | 
22 | export const getSuggestionValue = sinon.spy((suggestion) => {
23 |   return suggestion.name;
24 | });
25 | 
26 | export const renderSuggestion = sinon.spy((suggestion) => {
27 |   return <span>{suggestion.name}</span>;
28 | });
29 | 
30 | export const onChange = sinon.spy((event, { newValue }) => {
31 |   app.setState({
32 |     value: newValue,
33 |   });
34 | });
35 | 
36 | export const onSuggestionsFetchRequested = sinon.spy(({ value }) => {
37 |   app.setState({
38 |     suggestions: getMatchingLanguages(value),
39 |   });
40 | });
41 | 
42 | export const onSuggestionsClearRequested = sinon.spy(() => {
43 |   app.setState({
44 |     suggestions: [],
45 |   });
46 | });
47 | 
48 | export const onSuggestionSelected = sinon.spy();
49 | 
50 | export const onSuggestionHighlighted = sinon.spy(({ suggestion }) => {
51 |   app.setState({
52 |     highlightedSuggestion: suggestion,
53 |   });
54 | });
55 | 
56 | export default class AutosuggestApp extends Component {
57 |   constructor() {
58 |     super();
59 | 
60 |     app = this;
61 | 
62 |     this.state = {
63 |       value: '',
64 |       suggestions: [],
65 |       highlightedSuggestion: null,
66 |       highlightFirstSuggestion: false
67 |     };
68 |   }
69 | 
70 |   render() {
71 |     const { value, suggestions, highlightFirstSuggestion } = this.state;
72 |     const inputProps = {
73 |       value,
74 |       onChange,
75 |     };
76 | 
77 |     return (
78 |       <Autosuggest
79 |         suggestions={suggestions.slice()}
80 |         onSuggestionsFetchRequested={onSuggestionsFetchRequested}
81 |         onSuggestionsClearRequested={onSuggestionsClearRequested}
82 |         onSuggestionSelected={onSuggestionSelected}
83 |         onSuggestionHighlighted={onSuggestionHighlighted}
84 |         getSuggestionValue={getSuggestionValue}
85 |         renderSuggestion={renderSuggestion}
86 |         inputProps={inputProps}
87 |         highlightFirstSuggestion={highlightFirstSuggestion}
88 |       />
89 |     );
90 |   }
91 | }
92 | 


--------------------------------------------------------------------------------
/test/focus-first-suggestion/AutosuggestApp.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 |   syntheticEventMatcher,
  7 |   expectInputValue,
  8 |   expectSuggestions,
  9 |   expectHighlightedSuggestion,
 10 |   mouseEnterSuggestion,
 11 |   mouseLeaveSuggestion,
 12 |   focusInput,
 13 |   blurInput,
 14 |   clickEscape,
 15 |   clickEnter,
 16 |   clickDown,
 17 |   setInputValue,
 18 |   focusAndSetInputValue,
 19 | } from '../helpers';
 20 | import AutosuggestApp, {
 21 |   onChange,
 22 |   onSuggestionSelected,
 23 |   onSuggestionHighlighted,
 24 |   setHighlightFirstSuggestion,
 25 | } from './AutosuggestApp';
 26 | 
 27 | describe('Autosuggest with highlightFirstSuggestion={true}', () => {
 28 |   beforeEach(() => {
 29 |     init(TestUtils.renderIntoDocument(<AutosuggestApp />));
 30 |     setHighlightFirstSuggestion(true);
 31 |   });
 32 | 
 33 |   describe('when highlightFirstSuggestion changes from true to false', () => {
 34 |     it("should unhighlight the suggestion", () => {
 35 |       focusAndSetInputValue('j');
 36 |       expectHighlightedSuggestion('Java');
 37 | 
 38 |       setHighlightFirstSuggestion(false);
 39 |       expectHighlightedSuggestion(null);
 40 |     });
 41 | 
 42 |     it("should retain the selected suggestion if it was set manually", () => {
 43 |       focusAndSetInputValue('j');
 44 |       expectHighlightedSuggestion('Java');
 45 |       clickDown();
 46 |       expectHighlightedSuggestion('JavaScript');
 47 | 
 48 |       setHighlightFirstSuggestion(false);
 49 |       expectHighlightedSuggestion('JavaScript');
 50 |     });
 51 | 
 52 |     it("should re-highlight the suggestion if it becomes true again", () => {
 53 |       focusAndSetInputValue('j');
 54 |       expectHighlightedSuggestion('Java');
 55 | 
 56 |       setHighlightFirstSuggestion(false);
 57 |       expectHighlightedSuggestion(null);
 58 | 
 59 |       setHighlightFirstSuggestion(true);
 60 |       expectHighlightedSuggestion('Java');
 61 |     });
 62 |   });
 63 | 
 64 |   describe('when typing and matches exist', () => {
 65 |     beforeEach(() => {
 66 |       focusAndSetInputValue('j');
 67 |     });
 68 | 
 69 |     it('should highlight the first suggestion', () => {
 70 |       expectHighlightedSuggestion('Java');
 71 |     });
 72 | 
 73 |     it('should highlight the first suggestion when typing a character does not change the suggestions', () => {
 74 |       focusAndSetInputValue('ja');
 75 |       expectHighlightedSuggestion('Java');
 76 |     });
 77 | 
 78 |     it('should highlight the first suggestion when input is focused after it has been blurred', () => {
 79 |       blurInput();
 80 |       focusInput();
 81 |       expectHighlightedSuggestion('Java');
 82 |     });
 83 | 
 84 |     it('should highlight the first suggestion when same suggestions are shown again', () => {
 85 |       setInputValue('');
 86 |       setInputValue('j');
 87 |       expectHighlightedSuggestion('Java');
 88 |     });
 89 | 
 90 |     it('should highlight a suggestion when mouse enters it', () => {
 91 |       mouseEnterSuggestion(1);
 92 |       expectHighlightedSuggestion('JavaScript');
 93 |     });
 94 | 
 95 |     it('should not have highlighted suggestions when mouse leaves a suggestion', () => {
 96 |       mouseEnterSuggestion(1);
 97 |       mouseLeaveSuggestion(1);
 98 |       expectHighlightedSuggestion(null);
 99 |     });
100 |   });
101 | 
102 |   describe('when pressing Down', () => {
103 |     beforeEach(() => {
104 |       focusAndSetInputValue('j');
105 |     });
106 | 
107 |     it('should highlight the second suggestion', () => {
108 |       clickDown();
109 |       expectHighlightedSuggestion('JavaScript');
110 |     });
111 | 
112 |     it('should not highlight any suggestion after reaching the last suggestion', () => {
113 |       clickDown(2);
114 |       expectHighlightedSuggestion(null);
115 |     });
116 | 
117 |     it('should highlight the first suggestion when suggestions are revealed', () => {
118 |       clickEscape();
119 |       clickDown();
120 |       expectHighlightedSuggestion('Java');
121 |     });
122 |   });
123 | 
124 |   describe('when pressing Enter', () => {
125 |     it('should hide suggestions if the first suggestion was autohighlighted', () => {
126 |       focusAndSetInputValue('p');
127 |       clickEnter();
128 |       expectSuggestions([]);
129 |     });
130 | 
131 |     it('should hide suggestions if mouse entered another suggestion after autohighlight', () => {
132 |       focusAndSetInputValue('p');
133 |       mouseEnterSuggestion(2);
134 |       clickEnter();
135 |       expectSuggestions([]);
136 |     });
137 | 
138 |     it('should not error if there are no suggestions', () => {
139 |       focusAndSetInputValue('z');
140 |       clickEnter();
141 |       expectInputValue('z');
142 |     });
143 |   });
144 | 
145 |   describe('inputProps.onChange', () => {
146 |     it('should be called once with the right parameters when Enter is pressed after autohighlight', () => {
147 |       focusAndSetInputValue('p');
148 |       onChange.resetHistory();
149 |       clickEnter();
150 |       expect(onChange).to.have.been.calledOnce;
151 |       expect(onChange).to.be.calledWith(syntheticEventMatcher, {
152 |         newValue: 'Perl',
153 |         method: 'enter',
154 |       });
155 |     });
156 |   });
157 | 
158 |   describe('onSuggestionSelected', () => {
159 |     it('should be called once with the right parameters when Enter is pressed after autohighlight', () => {
160 |       focusAndSetInputValue('p');
161 |       onSuggestionSelected.resetHistory();
162 |       clickEnter();
163 |       expect(onSuggestionSelected).to.have.been.calledOnce;
164 |       expect(onSuggestionSelected).to.have.been.calledWithExactly(
165 |         syntheticEventMatcher,
166 |         {
167 |           suggestion: { name: 'Perl', year: 1987 },
168 |           suggestionValue: 'Perl',
169 |           suggestionIndex: 0,
170 |           sectionIndex: null,
171 |           method: 'enter',
172 |         }
173 |       );
174 |     });
175 |   });
176 | 
177 |   describe('onSuggestionHighlighted', () => {
178 |     it('should be called once with the highlighed suggestion when the first suggestion is autohighlighted', () => {
179 |       onSuggestionHighlighted.resetHistory();
180 |       focusAndSetInputValue('p');
181 |       expect(onSuggestionHighlighted).to.have.been.calledOnce;
182 |       expect(onSuggestionHighlighted).to.have.been.calledWithExactly({
183 |         suggestion: { name: 'Perl', year: 1987 },
184 |       });
185 |     });
186 | 
187 |     it('should be called once with the new suggestion when typing more changes the autohighlighted suggestion', () => {
188 |       focusAndSetInputValue('c');
189 |       onSuggestionHighlighted.resetHistory();
190 |       focusAndSetInputValue('c+');
191 |       expect(onSuggestionHighlighted).to.have.been.calledOnce;
192 |       expect(onSuggestionHighlighted).to.have.been.calledWithExactly({
193 |         suggestion: { name: 'C++', year: 1983 },
194 |       });
195 |     });
196 |   });
197 | });
198 | 


--------------------------------------------------------------------------------
/test/helpers.js:
--------------------------------------------------------------------------------
  1 | import chai, { expect } from 'chai';
  2 | import sinon from 'sinon';
  3 | import sinonChai from 'sinon-chai';
  4 | import ReactDom from 'react-dom';
  5 | import TestUtils, { Simulate } from 'react-dom/test-utils';
  6 | 
  7 | chai.use(sinonChai);
  8 | 
  9 | const clock = sinon.useFakeTimers();
 10 | 
 11 | let app, container, input, suggestionsContainer, clearButton;
 12 | let eventsArray = [];
 13 | 
 14 | export const { tick } = clock;
 15 | 
 16 | export const clearEvents = () => {
 17 |   eventsArray = [];
 18 | };
 19 | 
 20 | export const addEvent = event => {
 21 |   eventsArray.push(event);
 22 | };
 23 | 
 24 | export const getEvents = () => {
 25 |   return eventsArray;
 26 | };
 27 | 
 28 | export const init = application => {
 29 |   app = application;
 30 |   container = TestUtils.findRenderedDOMComponentWithClass(
 31 |     app,
 32 |     'react-autosuggest__container'
 33 |   );
 34 |   input = TestUtils.findRenderedDOMComponentWithClass(
 35 |     app,
 36 |     'react-autosuggest__input'
 37 |   );
 38 |   suggestionsContainer = TestUtils.findRenderedDOMComponentWithClass(
 39 |     app,
 40 |     'react-autosuggest__suggestions-container'
 41 |   );
 42 |   clearButton = TestUtils.scryRenderedDOMComponentsWithTag(app, 'button')[0];
 43 | };
 44 | 
 45 | // Since react-dom doesn't export SyntheticEvent anymore
 46 | export const syntheticEventMatcher = sinon.match(value => {
 47 |   if (typeof value !== 'object' || value === null) {
 48 |     return false;
 49 |   }
 50 | 
 51 |   const proto = Object.getPrototypeOf(value);
 52 | 
 53 |   if ('_dispatchListeners' in value && proto && proto.constructor.Interface) {
 54 |     return true;
 55 |   }
 56 |   return false;
 57 | }, 'of SyntheticEvent type');
 58 | export const childrenMatcher = sinon.match.any;
 59 | export const containerPropsMatcher = sinon.match({
 60 |   id: sinon.match.string,
 61 |   key: sinon.match.string,
 62 |   className: sinon.match.string,
 63 |   ref: sinon.match.func
 64 | });
 65 | 
 66 | const reactAttributesRegex = / data-react[-\w]+="[^"]+"/g;
 67 | 
 68 | // See: http://stackoverflow.com/q/28979533/247243
 69 | const stripReactAttributes = html => html.replace(reactAttributesRegex, '');
 70 | 
 71 | export const getInnerHTML = element => stripReactAttributes(element.innerHTML);
 72 | 
 73 | export const getElementWithClass = className =>
 74 |   TestUtils.findRenderedDOMComponentWithClass(app, className);
 75 | 
 76 | export const expectContainerAttribute = (attributeName, expectedValue) => {
 77 |   expect(container.getAttribute(attributeName)).to.equal(expectedValue);
 78 | };
 79 | 
 80 | export const expectInputAttribute = (attributeName, expectedValue) => {
 81 |   expect(input.getAttribute(attributeName)).to.equal(expectedValue);
 82 | };
 83 | 
 84 | export const getSuggestionsContainerAttribute = attributeName =>
 85 |   suggestionsContainer.getAttribute(attributeName);
 86 | 
 87 | export const expectInputValue = expectedValue => {
 88 |   expect(input.value).to.equal(expectedValue);
 89 | };
 90 | 
 91 | export const getSuggestionsList = () =>
 92 |   TestUtils.findRenderedDOMComponentWithClass(
 93 |     app,
 94 |     'react-autosuggest__suggestions-list'
 95 |   );
 96 | 
 97 | export const getSuggestions = () =>
 98 |   TestUtils.scryRenderedDOMComponentsWithClass(
 99 |     app,
100 |     'react-autosuggest__suggestion'
101 |   );
102 | 
103 | export const getSuggestion = suggestionIndex => {
104 |   const suggestions = getSuggestions();
105 | 
106 |   if (suggestionIndex >= suggestions.length) {
107 |     throw Error(
108 |       `
109 |       Cannot find suggestion #${suggestionIndex}.
110 |       ${
111 |         suggestions.length === 0
112 |           ? 'No suggestions found.'
113 |           : `Only ${suggestions.length} suggestion${
114 |               suggestions.length === 1 ? '' : 's'
115 |             } found.`
116 |       }
117 |     `
118 |     );
119 |   }
120 | 
121 |   return suggestions[suggestionIndex];
122 | };
123 | 
124 | export const expectSuggestionAttribute = (
125 |   suggestionIndex,
126 |   attributeName,
127 |   expectedValue
128 | ) => {
129 |   expect(getSuggestion(suggestionIndex).getAttribute(attributeName)).to.equal(
130 |     expectedValue
131 |   );
132 | };
133 | 
134 | export const getTitles = () =>
135 |   TestUtils.scryRenderedDOMComponentsWithClass(
136 |     app,
137 |     'react-autosuggest__section-title'
138 |   );
139 | 
140 | export const getTitle = titleIndex => {
141 |   const titles = getTitles();
142 | 
143 |   if (titleIndex >= titles.length) {
144 |     throw Error(`Cannot find title #${titleIndex}`);
145 |   }
146 | 
147 |   return titles[titleIndex];
148 | };
149 | 
150 | export const expectInputReferenceToBeSet = () => {
151 |   expect(app.input).to.equal(input);
152 | };
153 | 
154 | export const expectSuggestions = expectedSuggestions => {
155 |   const suggestions = getSuggestions().map(
156 |     suggestion => suggestion.textContent
157 |   );
158 | 
159 |   expect(suggestions).to.deep.equal(expectedSuggestions);
160 | };
161 | 
162 | export const expectHighlightedSuggestion = suggestion => {
163 |   const highlightedSuggestions = TestUtils.scryRenderedDOMComponentsWithClass(
164 |     app,
165 |     'react-autosuggest__suggestion--highlighted'
166 |   );
167 | 
168 |   if (suggestion === null) {
169 |     expect(highlightedSuggestions).to.have.length(0);
170 |   } else {
171 |     expect(highlightedSuggestions).to.have.length(1);
172 |     expect(highlightedSuggestions[0].textContent).to.equal(suggestion);
173 |   }
174 | };
175 | 
176 | export const saveKeyDown = (event) => {
177 |   let runBrowserDefault = event.defaultPrevented
178 |     ? 'defaultPrevented'
179 |     : 'runBrowserDefault';
180 | 
181 |   addEvent('onKeyDown-' + runBrowserDefault);
182 | };
183 | 
184 | export const expectLetBrowserHandleKeyDown = () => {
185 |   expect(getEvents()).to.not.deep.include('onKeyDown-defaultPrevented');
186 |   expect(getEvents()).to.deep.include('onKeyDown-runBrowserDefault');
187 | };
188 | 
189 | export const expectDontLetBrowserHandleKeyDown = () => {
190 |   expect(getEvents()).to.not.deep.include('onKeyDown-runBrowserDefault');
191 |   expect(getEvents()).to.deep.include('onKeyDown-defaultPrevented');
192 | };
193 | 
194 | export const mouseEnterSuggestion = suggestionIndex => {
195 |   Simulate.mouseEnter(getSuggestion(suggestionIndex));
196 | };
197 | 
198 | export const mouseLeaveSuggestion = suggestionIndex => {
199 |   Simulate.mouseLeave(getSuggestion(suggestionIndex));
200 | };
201 | 
202 | export const mouseDownSuggestion = suggestionIndex => {
203 |   Simulate.mouseDown(getSuggestion(suggestionIndex));
204 | };
205 | 
206 | const mouseDownDocument = target => {
207 |   document.dispatchEvent(
208 |     new window.CustomEvent('mousedown', {
209 |       detail: {
210 |         // must be 'detail' accoring to docs: https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events#Adding_custom_data_–_CustomEvent()
211 |         target
212 |       }
213 |     })
214 |   );
215 | };
216 | 
217 | export const mouseUpDocument = target => {
218 |   document.dispatchEvent(
219 |     new window.CustomEvent(
220 |       'mouseup',
221 |       target
222 |         ? {
223 |             detail: {
224 |               // must be 'detail' accoring to docs: https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events#Adding_custom_data_–_CustomEvent()
225 |               target
226 |             }
227 |           }
228 |         : null
229 |     )
230 |   );
231 | };
232 | 
233 | const touchStartSuggestion = suggestionIndex => {
234 |   Simulate.touchStart(getSuggestion(suggestionIndex));
235 | };
236 | 
237 | const touchMoveSuggestion = suggestionIndex => {
238 |   Simulate.touchMove(getSuggestion(suggestionIndex));
239 | };
240 | 
241 | // It doesn't feel right to emulate all the DOM events by copying the implementation.
242 | // Please show me a better way to emulate this.
243 | export const clickSuggestion = suggestionIndex => {
244 |   const suggestion = getSuggestion(suggestionIndex);
245 | 
246 |   mouseEnterSuggestion(suggestionIndex);
247 |   mouseDownDocument(suggestion);
248 |   mouseDownSuggestion(suggestionIndex);
249 |   mouseUpDocument(suggestion);
250 |   blurInput();
251 |   focusInput();
252 |   Simulate.click(suggestion);
253 |   clock.tick(1);
254 | };
255 | 
256 | // Simulates only mouse events since on touch devices dragging considered as a scroll and is a different case.
257 | export const dragSuggestionOut = suggestionIndex => {
258 |   const suggestion = getSuggestion(suggestionIndex);
259 | 
260 |   mouseEnterSuggestion(suggestionIndex);
261 |   mouseDownDocument(suggestion);
262 |   mouseDownSuggestion(suggestionIndex);
263 |   mouseLeaveSuggestion(suggestionIndex);
264 |   mouseUpDocument();
265 | };
266 | 
267 | export const dragSuggestionOutAndIn = suggestionIndex => {
268 |   const suggestion = getSuggestion(suggestionIndex);
269 | 
270 |   mouseEnterSuggestion(suggestionIndex);
271 |   mouseDownDocument(suggestion);
272 |   mouseDownSuggestion(suggestionIndex);
273 |   mouseLeaveSuggestion(suggestionIndex);
274 |   mouseEnterSuggestion(suggestionIndex);
275 |   mouseUpDocument();
276 |   blurInput();
277 |   focusInput();
278 |   Simulate.click(suggestion);
279 |   clock.tick(1);
280 | };
281 | 
282 | // Simulates mouse events as well as touch events since some browsers (chrome) mirror them and we should handle this.
283 | // Order of events is implemented according to docs: https://developer.mozilla.org/en-US/docs/Web/API/Touch_events/Supporting_both_TouchEvent_and_MouseEvent
284 | export const dragSuggestionOutTouch = suggestionIndex => {
285 |   touchStartSuggestion(suggestionIndex);
286 |   touchMoveSuggestion(suggestionIndex);
287 |   mouseDownSuggestion(suggestionIndex);
288 |   mouseUpDocument();
289 | };
290 | 
291 | export const clickSuggestionsContainer = () => {
292 |   mouseDownDocument(suggestionsContainer);
293 |   blurInput();
294 |   focusInput();
295 | };
296 | 
297 | export const focusInput = () => {
298 |   Simulate.focus(input);
299 | };
300 | 
301 | export const blurInput = () => {
302 |   Simulate.blur(input);
303 | };
304 | 
305 | export const clickEscape = () => {
306 |   Simulate.keyDown(input, { key: 'Escape', keyCode: 27 }); // throws if key is missing
307 | };
308 | 
309 | export const clickEnter = () => {
310 |   Simulate.keyDown(input, { key: 'Enter', keyCode: 13 }); // throws if key is missing
311 |   clock.tick(1);
312 | };
313 | 
314 | // See #388
315 | export const clickCombinedCharacterEnter = () => {
316 |   Simulate.keyDown(input, { key: 'Enter', keyCode: 229 }); // throws if key is missing
317 |   clock.tick(1);
318 | };
319 | 
320 | export const clickDown = (count = 1) => {
321 |   for (let i = 0; i < count; i++) {
322 |     Simulate.keyDown(input, { key: 'ArrowDown', keyCode: 40 }); // throws if key is missing
323 |   }
324 | };
325 | 
326 | export const clickUp = (count = 1) => {
327 |   for (let i = 0; i < count; i++) {
328 |     Simulate.keyDown(input, { key: 'ArrowUp', keyCode: 38 }); // throws if key is missing
329 |   }
330 | };
331 | 
332 | export const setInputValue = value => {
333 |   input.value = value;
334 |   Simulate.change(input);
335 | };
336 | 
337 | export const focusAndSetInputValue = value => {
338 |   focusInput();
339 |   setInputValue(value);
340 | };
341 | 
342 | export const isInputFocused = () => document.activeElement === input;
343 | 
344 | export const clickClearButton = () => {
345 |   if (clearButton) {
346 |     Simulate.mouseDown(clearButton);
347 |   } else {
348 |     throw new Error("Clear button doesn't exist");
349 |   }
350 | };
351 | 
352 | export const unmountApp = () => {
353 |   // eslint-disable-next-line react/no-find-dom-node
354 |   ReactDom.unmountComponentAtNode(ReactDom.findDOMNode(app).parentNode);
355 | };
356 | 


--------------------------------------------------------------------------------
/test/keep-suggestions-on-select/AutosuggestApp.js:
--------------------------------------------------------------------------------
 1 | import React, { Component } from 'react';
 2 | import Autosuggest from '../../src/Autosuggest';
 3 | import { escapeRegexCharacters } from '../../demo/src/components/utils/utils';
 4 | import languages from '../plain-list/languages';
 5 | import sinon from 'sinon';
 6 | import { addEvent } from '../helpers';
 7 | 
 8 | const getMatchingLanguages = (value) => {
 9 |   const escapedValue = escapeRegexCharacters(value.trim());
10 |   const regex = new RegExp('^' + escapedValue, 'i');
11 | 
12 |   return languages.filter((language) => regex.test(language.name));
13 | };
14 | 
15 | let app = null;
16 | 
17 | export const getSuggestionValue = (suggestion) => suggestion.name;
18 | 
19 | export const renderSuggestion = (suggestion) => <span>{suggestion.name}</span>;
20 | 
21 | export const onChange = sinon.spy((event, { newValue }) => {
22 |   addEvent('onChange');
23 | 
24 |   app.setState({
25 |     value: newValue,
26 |   });
27 | });
28 | 
29 | export const onSuggestionsFetchRequested = ({ value }) => {
30 |   app.setState({
31 |     suggestions: getMatchingLanguages(value),
32 |   });
33 | };
34 | 
35 | export const onSuggestionsClearRequested = () => {
36 |   app.setState({
37 |     suggestions: [],
38 |   });
39 | };
40 | 
41 | export const shouldKeepSuggestionsOnSelect = (suggestion) => {
42 |   return suggestion.name.toLowerCase().startsWith('clo');
43 | };
44 | 
45 | export default class AutosuggestApp extends Component {
46 |   constructor() {
47 |     super();
48 | 
49 |     app = this;
50 | 
51 |     this.state = {
52 |       value: '',
53 |       suggestions: [],
54 |     };
55 |   }
56 | 
57 |   render() {
58 |     const { value, suggestions } = this.state;
59 |     const inputProps = {
60 |       value,
61 |       onChange,
62 |     };
63 | 
64 |     return (
65 |       <Autosuggest
66 |         suggestions={suggestions.slice()}
67 |         onSuggestionsFetchRequested={onSuggestionsFetchRequested}
68 |         onSuggestionsClearRequested={onSuggestionsClearRequested}
69 |         getSuggestionValue={getSuggestionValue}
70 |         renderSuggestion={renderSuggestion}
71 |         inputProps={inputProps}
72 |         shouldKeepSuggestionsOnSelect={shouldKeepSuggestionsOnSelect}
73 |       />
74 |     );
75 |   }
76 | }
77 | 


--------------------------------------------------------------------------------
/test/keep-suggestions-on-select/AutosuggestApp.test.js:
--------------------------------------------------------------------------------
 1 | import {
 2 |   clickSuggestion,
 3 |   focusAndSetInputValue,
 4 |   getSuggestions,
 5 |   init,
 6 | } from '../helpers';
 7 | import TestUtils from 'react-dom/test-utils';
 8 | import AutosuggestApp from './AutosuggestApp';
 9 | import { expect } from 'chai';
10 | import React from 'react';
11 | 
12 | describe('Autosuggest with custom shouldKeepSuggestionsOnSelect', () => {
13 |   beforeEach(() => {
14 |     init(TestUtils.renderIntoDocument(<AutosuggestApp />));
15 |   });
16 | 
17 |   describe('when keep opened for starts with `clo` suggestions', () => {
18 |     it('should keep suggestions on select', () => {
19 |       focusAndSetInputValue('clo');
20 |       clickSuggestion(0);
21 |       const suggestions = getSuggestions();
22 | 
23 |       expect(suggestions.length).to.equal(1);
24 |     });
25 | 
26 |     it('should not suggestions on select', () => {
27 |       focusAndSetInputValue('php');
28 |       clickSuggestion(0);
29 |       const suggestions = getSuggestions();
30 | 
31 |       expect(suggestions.length).to.equal(0);
32 |     });
33 |   });
34 | });
35 | 


--------------------------------------------------------------------------------
/test/multi-section/AutosuggestApp.js:
--------------------------------------------------------------------------------
  1 | import React, { Component } from 'react';
  2 | import sinon from 'sinon';
  3 | import Autosuggest from '../../src/Autosuggest';
  4 | import languages from './languages';
  5 | import { escapeRegexCharacters } from '../../demo/src/components/utils/utils.js';
  6 | 
  7 | const getMatchingLanguages = value => {
  8 |   const escapedValue = escapeRegexCharacters(value.trim());
  9 |   const regex = new RegExp('^' + escapedValue, 'i');
 10 | 
 11 |   return languages
 12 |     .map(section => {
 13 |       return {
 14 |         title: section.title,
 15 |         languages: section.languages.filter(language =>
 16 |           regex.test(language.name)
 17 |         )
 18 |       };
 19 |     })
 20 |     .filter(section => section.languages.length > 0);
 21 | };
 22 | 
 23 | let app = null;
 24 | 
 25 | export const getSuggestionValue = sinon.spy(suggestion => {
 26 |   return suggestion.name;
 27 | });
 28 | 
 29 | export const renderSuggestion = sinon.spy(suggestion => {
 30 |   return <span>{suggestion.name}</span>;
 31 | });
 32 | 
 33 | const alwaysTrue = () => true;
 34 | 
 35 | export const onChange = sinon.spy((event, { newValue }) => {
 36 |   app.setState({
 37 |     value: newValue
 38 |   });
 39 | });
 40 | 
 41 | export const onBlur = sinon.spy();
 42 | 
 43 | export const onSuggestionsFetchRequested = sinon.spy(({ value }) => {
 44 |   app.setState({
 45 |     suggestions: getMatchingLanguages(value)
 46 |   });
 47 | });
 48 | 
 49 | export const onSuggestionsClearRequested = sinon.spy(() => {
 50 |   app.setState({
 51 |     suggestions: []
 52 |   });
 53 | });
 54 | 
 55 | export const onSuggestionSelected = sinon.spy();
 56 | 
 57 | export const onSuggestionHighlighted = sinon.spy();
 58 | 
 59 | export const renderSectionTitle = sinon.spy(section => {
 60 |   return <strong>{section.title}</strong>;
 61 | });
 62 | 
 63 | export const getSectionSuggestions = sinon.spy(section => {
 64 |   return section.languages;
 65 | });
 66 | 
 67 | let highlightFirstSuggestion = false;
 68 | 
 69 | export const setHighlightFirstSuggestion = value => {
 70 |   highlightFirstSuggestion = value;
 71 | };
 72 | 
 73 | export default class AutosuggestApp extends Component {
 74 |   constructor() {
 75 |     super();
 76 | 
 77 |     app = this;
 78 | 
 79 |     this.state = {
 80 |       value: '',
 81 |       suggestions: []
 82 |     };
 83 |   }
 84 | 
 85 |   onClearMouseDown = event => {
 86 |     event.preventDefault();
 87 | 
 88 |     this.setState({
 89 |       value: '',
 90 |       suggestions: getMatchingLanguages('')
 91 |     });
 92 |   };
 93 | 
 94 |   render() {
 95 |     const { value, suggestions } = this.state;
 96 |     const inputProps = {
 97 |       value,
 98 |       onChange,
 99 |       onBlur
100 |     };
101 | 
102 |     return (
103 |       <div>
104 |         <button onMouseDown={this.onClearMouseDown}>Clear</button>
105 |         <Autosuggest
106 |           multiSection={true}
107 |           suggestions={suggestions}
108 |           onSuggestionsFetchRequested={onSuggestionsFetchRequested}
109 |           onSuggestionsClearRequested={onSuggestionsClearRequested}
110 |           onSuggestionSelected={onSuggestionSelected}
111 |           onSuggestionHighlighted={onSuggestionHighlighted}
112 |           getSuggestionValue={getSuggestionValue}
113 |           renderSuggestion={renderSuggestion}
114 |           inputProps={inputProps}
115 |           shouldRenderSuggestions={alwaysTrue}
116 |           renderSectionTitle={renderSectionTitle}
117 |           getSectionSuggestions={getSectionSuggestions}
118 |           highlightFirstSuggestion={highlightFirstSuggestion}
119 |         />
120 |       </div>
121 |     );
122 |   }
123 | }
124 | 


--------------------------------------------------------------------------------
/test/multi-section/AutosuggestApp.test.js:
--------------------------------------------------------------------------------
  1 | import React from 'react';
  2 | import TestUtils from 'react-dom/test-utils';
  3 | import { expect } from 'chai';
  4 | import sinon from 'sinon';
  5 | import {
  6 |   init,
  7 |   syntheticEventMatcher,
  8 |   getInnerHTML,
  9 |   expectContainerAttribute,
 10 |   expectInputAttribute,
 11 |   expectSuggestions,
 12 |   expectHighlightedSuggestion,
 13 |   expectSuggestionAttribute,
 14 |   getSuggestionsContainerAttribute,
 15 |   getTitle,
 16 |   clickSuggestion,
 17 |   focusInput,
 18 |   clickEscape,
 19 |   clickEnter,
 20 |   clickDown,
 21 |   clickUp,
 22 |   setInputValue,
 23 |   focusAndSetInputValue,
 24 |   clickClearButton
 25 | } from '../helpers';
 26 | import AutosuggestApp, {
 27 |   renderSuggestion,
 28 |   onSuggestionsFetchRequested,
 29 |   onSuggestionSelected,
 30 |   onSuggestionHighlighted,
 31 |   renderSectionTitle,
 32 |   getSectionSuggestions,
 33 |   setHighlightFirstSuggestion
 34 | } from './AutosuggestApp';
 35 | 
 36 | describe('Autosuggest with multiSection={true}', () => {
 37 |   beforeEach(() => {
 38 |     init(TestUtils.renderIntoDocument(<AutosuggestApp />));
 39 |   });
 40 | 
 41 |   describe('renderSuggestion', () => {
 42 |     beforeEach(() => {
 43 |       focusInput();
 44 |     });
 45 | 
 46 |     it('should be called once for every suggestion', () => {
 47 |       expect(renderSuggestion).to.have.callCount(14);
 48 |     });
 49 | 
 50 |     it('should be called with an empty query when input field is blank', () => {
 51 |       renderSuggestion.resetHistory();
 52 |       clickDown();
 53 |       expect(renderSuggestion.getCall(0).args).to.deep.equal([
 54 |         { name: 'C', year: 1972 },
 55 |         { query: '', isHighlighted: true }
 56 |       ]);
 57 |     });
 58 | 
 59 |     it('should trim the value before passing it to the query', () => {
 60 |       renderSuggestion.resetHistory();
 61 |       setInputValue(' ');
 62 |       clickDown();
 63 |       expect(renderSuggestion.getCall(0).args).to.deep.equal([
 64 |         { name: 'C', year: 1972 },
 65 |         { query: '', isHighlighted: true }
 66 |       ]);
 67 |     });
 68 |   });
 69 | 
 70 |   describe('shouldRenderSuggestions', () => {
 71 |     it('should show suggestions input is empty and true is returned', () => {
 72 |       focusInput();
 73 |       expectSuggestions([
 74 |         'C',
 75 |         'C#',
 76 |         'C++',
 77 |         'Clojure',
 78 |         'Elm',
 79 |         'Go',
 80 |         'Haskell',
 81 |         'Java',
 82 |         'JavaScript',
 83 |         'Perl',
 84 |         'PHP',
 85 |         'Python',
 86 |         'Ruby',
 87 |         'Scala'
 88 |       ]);
 89 |     });
 90 |   });
 91 | 
 92 |   describe('onSuggestionSelected', () => {
 93 |     beforeEach(() => {
 94 |       onSuggestionSelected.resetHistory();
 95 |       focusInput();
 96 |     });
 97 | 
 98 |     it('should be called with the right sectionIndex when suggestion is clicked', () => {
 99 |       clickSuggestion(4);
100 |       expect(onSuggestionSelected).to.have.been.calledWith(
101 |         syntheticEventMatcher,
102 |         sinon.match({
103 |           sectionIndex: 1
104 |         })
105 |       );
106 |     });
107 | 
108 |     it('should be called with the right sectionIndex when Enter is pressed and suggestion is highlighted', () => {
109 |       clickDown(6);
110 |       clickEnter();
111 |       expect(onSuggestionSelected).to.have.been.calledWith(
112 |         syntheticEventMatcher,
113 |         sinon.match({
114 |           sectionIndex: 2
115 |         })
116 |       );
117 |     });
118 |   });
119 | 
120 |   describe('onSuggestionHighlighted', () => {
121 |     it('should be called once with the suggestion that becomes highlighted', () => {
122 |       focusAndSetInputValue('c');
123 |       onSuggestionHighlighted.resetHistory();
124 |       clickDown();
125 |       expect(onSuggestionHighlighted).to.have.been.calledOnce;
126 |       expect(onSuggestionHighlighted).to.have.been.calledWithExactly({
127 |         suggestion: { name: 'C', year: 1972 }
128 |       });
129 |     });
130 | 
131 |     it('should be called once with null when there is no more highlighted suggestion', () => {
132 |       focusAndSetInputValue('c');
133 |       clickDown();
134 |       onSuggestionHighlighted.resetHistory();
135 |       clickUp();
136 |       expect(onSuggestionHighlighted).to.have.been.calledOnce;
137 |       expect(onSuggestionHighlighted).to.have.been.calledWithExactly({
138 |         suggestion: null
139 |       });
140 |     });
141 |   });
142 | 
143 |   describe('onSuggestionsFetchRequested', () => {
144 |     it('should be called once with the right parameters when input gets focus and shouldRenderSuggestions returns true', () => {
145 |       onSuggestionsFetchRequested.resetHistory();
146 |       focusInput();
147 |       expect(onSuggestionsFetchRequested).to.have.been.calledOnce;
148 |       expect(onSuggestionsFetchRequested).to.have.been.calledWithExactly({
149 |         value: '',
150 |         reason: 'input-focused'
151 |       });
152 |     });
153 | 
154 |     it('should be called once with the right parameters when Escape is pressed and suggestions are hidden and shouldRenderSuggestions returns true for empty value', () => {
155 |       focusAndSetInputValue('jr');
156 |       onSuggestionsFetchRequested.resetHistory();
157 |       clickEscape();
158 |       expect(onSuggestionsFetchRequested).to.have.been.calledOnce;
159 |       expect(onSuggestionsFetchRequested).to.have.been.calledWithExactly({
160 |         value: '',
161 |         reason: 'escape-pressed'
162 |       });
163 |     });
164 |   });
165 | 
166 |   describe('when input is cleared after suggestion is clicked', () => {
167 |     beforeEach(() => {
168 |       focusInput();
169 |       clickSuggestion(1);
170 |     });
171 | 
172 |     it('should show suggestions', () => {
173 |       clickClearButton();
174 |       expectSuggestions([
175 |         'C',
176 |         'C#',
177 |         'C++',
178 |         'Clojure',
179 |         'Elm',
180 |         'Go',
181 |         'Haskell',
182 |         'Java',
183 |         'JavaScript',
184 |         'Perl',
185 |         'PHP',
186 |         'Python',
187 |         'Ruby',
188 |         'Scala'
189 |       ]);
190 |     });
191 |   });
192 | 
193 |   describe('renderSectionTitle', () => {
194 |     beforeEach(() => {
195 |       focusInput();
196 |       renderSectionTitle.resetHistory();
197 |       setInputValue('c');
198 |     });
199 | 
200 |     it('should be called with the right parameters', () => {
201 |       expect(renderSectionTitle).to.have.been.calledWithExactly({
202 |         title: 'C',
203 |         languages: [
204 |           {
205 |             name: 'C',
206 |             year: 1972
207 |           },
208 |           {
209 |             name: 'C#',
210 |             year: 2000
211 |           },
212 |           {
213 |             name: 'C++',
214 |             year: 1983
215 |           },
216 |           {
217 |             name: 'Clojure',
218 |             year: 2007
219 |           }
220 |         ]
221 |       });
222 |     });
223 | 
224 |     it('should be called once per section', () => {
225 |       expect(renderSectionTitle).to.have.been.calledOnce;
226 |     });
227 | 
228 |     it('return value should be used to render titles', () => {
229 |       const firstTitle = getTitle(0);
230 | 
231 |       expect(getInnerHTML(firstTitle)).to.equal('<strong>C</strong>');
232 |     });
233 |   });
234 | 
235 |   describe('getSectionSuggestions', () => {
236 |     beforeEach(() => {
237 |       focusInput();
238 |       getSectionSuggestions.resetHistory();
239 |       setInputValue('j');
240 |     });
241 | 
242 |     it('should be called with the right parameters', () => {
243 |       expect(getSectionSuggestions).to.have.been.calledWithExactly({
244 |         title: 'J',
245 |         languages: [
246 |           {
247 |             name: 'Java',
248 |             year: 1995
249 |           },
250 |           {
251 |             name: 'JavaScript',
252 |             year: 1995
253 |           }
254 |         ]
255 |       });
256 |     });
257 | 
258 |     it('should be called once per section', () => {
259 |       expect(getSectionSuggestions).to.have.been.calledOnce;
260 |     });
261 |   });
262 | 
263 |   describe('default theme', () => {
264 |     it('should set the input class', () => {
265 |       expectInputAttribute('class', 'react-autosuggest__input');
266 |     });
267 | 
268 |     it('should add the open container class when suggestions are shown', () => {
269 |       focusAndSetInputValue('c');
270 |       expectContainerAttribute(
271 |         'class',
272 |         'react-autosuggest__container react-autosuggest__container--open'
273 |       );
274 |     });
275 | 
276 |     it('should remove the open container class when suggestions are hidden', () => {
277 |       focusAndSetInputValue('c');
278 |       clickEscape();
279 |       expectContainerAttribute('class', 'react-autosuggest__container');
280 |     });
281 | 
282 |     it('should set suggestions the container class', () => {
283 |       expect(getSuggestionsContainerAttribute('class')).to.equal(
284 |         'react-autosuggest__suggestions-container'
285 |       );
286 | 
287 |       focusAndSetInputValue('e');
288 |       expect(getSuggestionsContainerAttribute('class')).to.equal(
289 |         'react-autosuggest__suggestions-container react-autosuggest__suggestions-container--open'
290 |       );
291 |     });
292 | 
293 |     it('should add the first suggestion class only to the first suggestion', () => {
294 |       focusAndSetInputValue('c');
295 |       expectSuggestionAttribute(
296 |         0,
297 |         'class',
298 |         'react-autosuggest__suggestion react-autosuggest__suggestion--first'
299 |       );
300 |       expectSuggestionAttribute(1, 'class', 'react-autosuggest__suggestion');
301 |     });
302 | 
303 |     it('should add the highlighted suggestion class only to the highlighted suggestion', () => {
304 |       focusAndSetInputValue('c');
305 |       clickDown();
306 |       clickDown();
307 |       expectSuggestionAttribute(
308 |         1,
309 |         'class',
310 |         'react-autosuggest__suggestion react-autosuggest__suggestion--highlighted'
311 |       );
312 |       expectSuggestionAttribute(2, 'class', 'react-autosuggest__suggestion');
313 |     });
314 |   });
315 | 
316 |   describe('and highlightFirstSuggestion={true}', () => {
317 |     before(() => {
318 |       setHighlightFirstSuggestion(true);
319 |     });
320 | 
321 |     after(() => {
322 |       setHighlightFirstSuggestion(false);
323 |     });
324 | 
325 |     describe('when typing and matches exist', () => {
326 |       beforeEach(() => {
327 |         focusAndSetInputValue('p');
328 |       });
329 | 
330 |       it('should highlight the first suggestion', () => {
331 |         expectHighlightedSuggestion('Perl');
332 |       });
333 |     });
334 |   });
335 | });
336 | 


--------------------------------------------------------------------------------
/test/multi-section/languages.js:
--------------------------------------------------------------------------------
 1 | export default [
 2 |   {
 3 |     title: 'C',
 4 |     languages: [
 5 |       {
 6 |         name: 'C',
 7 |         year: 1972
 8 |       },
 9 |       {
10 |         name: 'C#',
11 |         year: 2000
12 |       },
13 |       {
14 |         name: 'C++',
15 |         year: 1983
16 |       },
17 |       {
18 |         name: 'Clojure',
19 |         year: 2007
20 |       }
21 |     ]
22 |   },
23 |   {
24 |     title: 'E',
25 |     languages: [
26 |       {
27 |         name: 'Elm',
28 |         year: 2012
29 |       }
30 |     ]
31 |   },
32 |   {
33 |     title: 'G',
34 |     languages: [
35 |       {
36 |         name: 'Go',
37 |         year: 2009
38 |       }
39 |     ]
40 |   },
41 |   {
42 |     title: 'H',
43 |     languages: [
44 |       {
45 |         name: 'Haskell',
46 |         year: 1990
47 |       }
48 |     ]
49 |   },
50 |   {
51 |     title: 'J',
52 |     languages: [
53 |       {
54 |         name: 'Java',
55 |         year: 1995
56 |       },
57 |       {
58 |         name: 'JavaScript',
59 |         year: 1995
60 |       }
61 |     ]
62 |   },
63 |   {
64 |     title: 'P',
65 |     languages: [
66 |       {
67 |         name: 'Perl',
68 |         year: 1987
69 |       },
70 |       {
71 |         name: 'PHP',
72 |         year: 1995
73 |       },
74 |       {
75 |         name: 'Python',
76 |         year: 1991
77 |       }
78 |     ]
79 |   },
80 |   {
81 |     title: 'R',
82 |     languages: [
83 |       {
84 |         name: 'Ruby',
85 |         year: 1995
86 |       }
87 |     ]
88 |   },
89 |   {
90 |     title: 'S',
91 |     languages: [
92 |       {
93 |         name: 'Scala',
94 |         year: 2003
95 |       }
96 |     ]
97 |   }
98 | ];
99 | 


--------------------------------------------------------------------------------
/test/plain-list/AutosuggestApp.js:
--------------------------------------------------------------------------------
  1 | import React, { Component } from 'react';
  2 | import sinon from 'sinon';
  3 | import match from 'autosuggest-highlight/match';
  4 | import parse from 'autosuggest-highlight/parse';
  5 | import Autosuggest from '../../src/Autosuggest';
  6 | import languages from './languages';
  7 | import { escapeRegexCharacters } from '../../demo/src/components/utils/utils.js';
  8 | import { addEvent, saveKeyDown } from '../helpers';
  9 | 
 10 | const getMatchingLanguages = value => {
 11 |   const escapedValue = escapeRegexCharacters(value.trim());
 12 |   const regex = new RegExp('^' + escapedValue, 'i');
 13 | 
 14 |   return languages.filter(language => regex.test(language.name));
 15 | };
 16 | 
 17 | let app = null;
 18 | 
 19 | export const getSuggestionValue = sinon.spy(suggestion => {
 20 |   return suggestion.name;
 21 | });
 22 | 
 23 | export const renderSuggestion = sinon.spy((suggestion, { query }) => {
 24 |   const matches = match(suggestion.name, query);
 25 |   const parts = parse(suggestion.name, matches);
 26 | 
 27 |   return parts.map((part, index) => {
 28 |     return part.highlight ? (
 29 |       <strong key={index}>{part.text}</strong>
 30 |     ) : (
 31 |       <span key={index}>{part.text}</span>
 32 |     );
 33 |   });
 34 | });
 35 | 
 36 | export const onChange = sinon.spy((event, { newValue }) => {
 37 |   addEvent('onChange');
 38 | 
 39 |   app.setState({
 40 |     value: newValue
 41 |   });
 42 | });
 43 | 
 44 | export const onFocus = sinon.spy();
 45 | export const onBlur = sinon.spy();
 46 | 
 47 | export const defaultShouldRenderSuggestionsStub = (value) => {
 48 |   return value.trim().length > 0 && value[0] !== ' ';
 49 | };
 50 | export const shouldRenderSuggestions = sinon.stub().callsFake(defaultShouldRenderSuggestionsStub);
 51 | 
 52 | export const onSuggestionsFetchRequested = sinon.spy(({ value }) => {
 53 |   app.setState({
 54 |     suggestions: getMatchingLanguages(value)
 55 |   });
 56 | });
 57 | 
 58 | export const onSuggestionsClearRequested = sinon.spy(() => {
 59 |   app.setState({
 60 |     suggestions: []
 61 |   });
 62 | });
 63 | 
 64 | export const onSuggestionSelected = sinon.spy(() => {
 65 |   addEvent('onSuggestionSelected');
 66 | });
 67 | 
 68 | export const onSuggestionHighlighted = sinon.spy(() => {
 69 |   addEvent('onSuggestionHighlighted');
 70 | });
 71 | 
 72 | export default class AutosuggestApp extends Component {
 73 |   constructor() {
 74 |     super();
 75 | 
 76 |     app = this;
 77 | 
 78 |     this.state = {
 79 |       value: '',
 80 |       suggestions: []
 81 |     };
 82 |   }
 83 | 
 84 |   storeAutosuggestReference = autosuggest => {
 85 |     if (autosuggest !== null) {
 86 |       this.input = autosuggest.input;
 87 |     }
 88 |   };
 89 | 
 90 |   render() {
 91 |     const { value, suggestions } = this.state;
 92 |     const inputProps = {
 93 |       id: 'my-awesome-autosuggest',
 94 |       placeholder: 'Type a programming language',
 95 |       type: 'search',
 96 |       onKeyDown: saveKeyDown,
 97 |       value,
 98 |       onChange,
 99 |       onFocus,
100 |       onBlur
101 |     };
102 | 
103 |     return (
104 |       <Autosuggest
105 |         suggestions={suggestions}
106 |         onSuggestionsFetchRequested={onSuggestionsFetchRequested}
107 |         onSuggestionsClearRequested={onSuggestionsClearRequested}
108 |         onSuggestionSelected={onSuggestionSelected}
109 |         onSuggestionHighlighted={onSuggestionHighlighted}
110 |         getSuggestionValue={getSuggestionValue}
111 |         renderSuggestion={renderSuggestion}
112 |         inputProps={inputProps}
113 |         shouldRenderSuggestions={shouldRenderSuggestions}
114 |         ref={this.storeAutosuggestReference}
115 |       />
116 |     );
117 |   }
118 | }
119 | 


--------------------------------------------------------------------------------
/test/plain-list/languages.js:
--------------------------------------------------------------------------------
 1 | export default [
 2 |   {
 3 |     name: 'C',
 4 |     year: 1972
 5 |   },
 6 |   {
 7 |     name: 'C#',
 8 |     year: 2000
 9 |   },
10 |   {
11 |     name: 'C++',
12 |     year: 1983
13 |   },
14 |   {
15 |     name: 'Clojure',
16 |     year: 2007
17 |   },
18 |   {
19 |     name: 'Elm',
20 |     year: 2012
21 |   },
22 |   {
23 |     name: 'Go',
24 |     year: 2009
25 |   },
26 |   {
27 |     name: 'Haskell',
28 |     year: 1990
29 |   },
30 |   {
31 |     name: 'Java',
32 |     year: 1995
33 |   },
34 |   {
35 |     name: 'JavaScript',
36 |     year: 1995
37 |   },
38 |   {
39 |     name: 'Perl',
40 |     year: 1987
41 |   },
42 |   {
43 |     name: 'PHP',
44 |     year: 1995
45 |   },
46 |   {
47 |     name: 'Python',
48 |     year: 1991
49 |   },
50 |   {
51 |     name: 'Ruby',
52 |     year: 1995
53 |   },
54 |   {
55 |     name: 'Scala',
56 |     year: 2003
57 |   }
58 | ];
59 | 


--------------------------------------------------------------------------------
/test/render-input-component/AutosuggestApp.js:
--------------------------------------------------------------------------------
 1 | import React, { Component } from 'react';
 2 | import Autosuggest from '../../src/Autosuggest';
 3 | import languages from '../plain-list/languages';
 4 | import { escapeRegexCharacters } from '../../demo/src/components/utils/utils.js';
 5 | 
 6 | const getMatchingLanguages = value => {
 7 |   const escapedValue = escapeRegexCharacters(value.trim());
 8 |   const regex = new RegExp('^' + escapedValue, 'i');
 9 | 
10 |   return languages.filter(language => regex.test(language.name));
11 | };
12 | 
13 | let app = null;
14 | 
15 | const onChange = (event, { newValue }) => {
16 |   app.setState({
17 |     value: newValue
18 |   });
19 | };
20 | 
21 | const onSuggestionsFetchRequested = ({ value }) => {
22 |   app.setState({
23 |     suggestions: getMatchingLanguages(value)
24 |   });
25 | };
26 | 
27 | const onSuggestionsClearRequested = () => {
28 |   app.setState({
29 |     suggestions: []
30 |   });
31 | };
32 | 
33 | const getSuggestionValue = suggestion => suggestion.name;
34 | 
35 | const renderSuggestion = suggestion => suggestion.name;
36 | 
37 | const renderInputComponent = inputProps => (
38 |   <div>
39 |     <input id="my-custom-input" {...inputProps} />
40 |   </div>
41 | );
42 | 
43 | export default class AutosuggestApp extends Component {
44 |   constructor() {
45 |     super();
46 | 
47 |     app = this;
48 | 
49 |     this.state = {
50 |       value: '',
51 |       suggestions: []
52 |     };
53 |   }
54 | 
55 |   storeAutosuggestReference = autosuggest => {
56 |     if (autosuggest !== null) {
57 |       this.input = autosuggest.input;
58 |     }
59 |   };
60 | 
61 |   render() {
62 |     const { value, suggestions } = this.state;
63 |     const inputProps = {
64 |       value,
65 |       onChange
66 |     };
67 | 
68 |     return (
69 |       <Autosuggest
70 |         suggestions={suggestions}
71 |         onSuggestionsFetchRequested={onSuggestionsFetchRequested}
72 |         onSuggestionsClearRequested={onSuggestionsClearRequested}
73 |         getSuggestionValue={getSuggestionValue}
74 |         renderSuggestion={renderSuggestion}
75 |         renderInputComponent={renderInputComponent}
76 |         inputProps={inputProps}
77 |         ref={this.storeAutosuggestReference}
78 |       />
79 |     );
80 |   }
81 | }
82 | 


--------------------------------------------------------------------------------
/test/render-input-component/AutosuggestApp.test.js:
--------------------------------------------------------------------------------
 1 | import React from 'react';
 2 | import TestUtils from 'react-dom/test-utils';
 3 | import {
 4 |   init,
 5 |   expectInputAttribute,
 6 |   expectInputReferenceToBeSet
 7 | } from '../helpers';
 8 | import AutosuggestApp from './AutosuggestApp';
 9 | 
10 | describe('Autosuggest with renderInputComponent', () => {
11 |   beforeEach(() => {
12 |     init(TestUtils.renderIntoDocument(<AutosuggestApp />));
13 |   });
14 | 
15 |   describe('initially', () => {
16 |     it("should set input's id", () => {
17 |       expectInputAttribute('id', 'my-custom-input');
18 |     });
19 | 
20 |     it('should set the input reference', () => {
21 |       expectInputReferenceToBeSet();
22 |     });
23 |   });
24 | });
25 | 


--------------------------------------------------------------------------------
/test/render-suggestions-container/AutosuggestApp.js:
--------------------------------------------------------------------------------
 1 | import React, { Component } from 'react';
 2 | import sinon from 'sinon';
 3 | import Autosuggest from '../../src/Autosuggest';
 4 | import languages from '../plain-list/languages';
 5 | import { escapeRegexCharacters } from '../../demo/src/components/utils/utils.js';
 6 | 
 7 | const getMatchingLanguages = value => {
 8 |   const escapedValue = escapeRegexCharacters(value.trim());
 9 |   const regex = new RegExp('^' + escapedValue, 'i');
10 | 
11 |   return languages.filter(language => regex.test(language.name));
12 | };
13 | 
14 | let app = null;
15 | 
16 | const onChange = (event, { newValue }) => {
17 |   app.setState({
18 |     value: newValue
19 |   });
20 | };
21 | 
22 | const onSuggestionsFetchRequested = ({ value }) => {
23 |   app.setState({
24 |     suggestions: getMatchingLanguages(value)
25 |   });
26 | };
27 | 
28 | const onSuggestionsClearRequested = () => {
29 |   app.setState({
30 |     suggestions: []
31 |   });
32 | };
33 | 
34 | const getSuggestionValue = suggestion => suggestion.name;
35 | 
36 | const renderSuggestion = suggestion => suggestion.name;
37 | 
38 | export const renderSuggestionsContainer = sinon.spy(
39 |   ({ containerProps, children, query }) => (
40 |     <div {...containerProps}>
41 |       {children}
42 |       <div className="my-suggestions-container-footer">
43 |         Press Enter to search <strong className="my-query">{query}</strong>
44 |       </div>
45 |     </div>
46 |   )
47 | );
48 | 
49 | export default class AutosuggestApp extends Component {
50 |   constructor() {
51 |     super();
52 | 
53 |     app = this;
54 | 
55 |     this.state = {
56 |       value: '',
57 |       suggestions: []
58 |     };
59 |   }
60 | 
61 |   storeAutosuggestReference = autosuggest => {
62 |     if (autosuggest !== null) {
63 |       this.input = autosuggest.input;
64 |     }
65 |   };
66 | 
67 |   render() {
68 |     const { value, suggestions } = this.state;
69 |     const inputProps = {
70 |       value,
71 |       onChange
72 |     };
73 | 
74 |     return (
75 |       <Autosuggest
76 |         suggestions={suggestions}
77 |         onSuggestionsFetchRequested={onSuggestionsFetchRequested}
78 |         onSuggestionsClearRequested={onSuggestionsClearRequested}
79 |         getSuggestionValue={getSuggestionValue}
80 |         renderSuggestion={renderSuggestion}
81 |         renderSuggestionsContainer={renderSuggestionsContainer}
82 |         inputProps={inputProps}
83 |         ref={this.storeAutosuggestReference}
84 |       />
85 |     );
86 |   }
87 | }
88 | 


--------------------------------------------------------------------------------
/test/render-suggestions-container/AutosuggestApp.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 |   getInnerHTML,
 9 |   getElementWithClass,
10 |   setInputValue
11 | } from '../helpers';
12 | import AutosuggestApp, { renderSuggestionsContainer } from './AutosuggestApp';
13 | 
14 | describe('Autosuggest with renderSuggestionsContainer', () => {
15 |   beforeEach(() => {
16 |     init(TestUtils.renderIntoDocument(<AutosuggestApp />));
17 |     renderSuggestionsContainer.resetHistory();
18 |     setInputValue('c ');
19 |   });
20 | 
21 |   it('should render whatever renderSuggestionsContainer returns', () => {
22 |     expect(getElementWithClass('my-suggestions-container-footer')).not.to.equal(
23 |       null
24 |     );
25 |     expect(getInnerHTML(getElementWithClass('my-query'))).to.equal('c');
26 |   });
27 | 
28 |   it('should call renderSuggestionsContainer once with the right parameters', () => {
29 |     expect(renderSuggestionsContainer).to.have.been.calledOnce;
30 |     expect(renderSuggestionsContainer).to.be.calledWith({
31 |       containerProps: containerPropsMatcher,
32 |       children: childrenMatcher,
33 |       query: 'c'
34 |     });
35 |   });
36 | });
37 | 


--------------------------------------------------------------------------------
/test/setup.js:
--------------------------------------------------------------------------------
1 | import { JSDOM } from 'jsdom';
2 | 
3 | const dom = new JSDOM(`<!doctype html><html><body></body></html>`);
4 | 
5 | global.window = dom.window;
6 | global.document = dom.window.document;
7 | global.navigator = dom.window.navigator;
8 | 


--------------------------------------------------------------------------------
/test/textarea/AutosuggestApp.js:
--------------------------------------------------------------------------------
 1 | import React, { Component } from 'react';
 2 | import sinon from 'sinon';
 3 | import Autosuggest from '../../src/Autosuggest';
 4 | import languages from '../plain-list/languages';
 5 | import { escapeRegexCharacters } from '../../demo/src/components/utils/utils.js';
 6 | import { saveKeyDown } from '../helpers';
 7 | 
 8 | const getMatchingLanguages = value => {
 9 |   const escapedValue = escapeRegexCharacters(value.trim());
10 |   const regex = new RegExp('^' + escapedValue, 'i');
11 | 
12 |   return languages.filter(language => regex.test(language.name));
13 | };
14 | 
15 | let app = null;
16 | 
17 | const onChange = (event, { newValue }) => {
18 |   app.setState({
19 |     value: newValue
20 |   });
21 | };
22 | 
23 | const onSuggestionsFetchRequested = ({ value }) => {
24 |   app.setState({
25 |     suggestions: getMatchingLanguages(value)
26 |   });
27 | };
28 | 
29 | const onSuggestionsClearRequested = () => {
30 |   app.setState({
31 |     suggestions: []
32 |   });
33 | };
34 | 
35 | export const onSuggestionSelected = sinon.spy(() => {});
36 | 
37 | const getSuggestionValue = suggestion => suggestion.name;
38 | 
39 | const renderSuggestion = suggestion => suggestion.name;
40 | 
41 | const renderTextarea = inputProps => (
42 |   <textarea {...inputProps} />
43 | );
44 | 
45 | export default class AutosuggestApp extends Component {
46 |   constructor() {
47 |     super();
48 | 
49 |     app = this;
50 | 
51 |     this.state = {
52 |       value: '',
53 |       suggestions: []
54 |     };
55 |   }
56 | 
57 |   storeAutosuggestReference = autosuggest => {
58 |     if (autosuggest !== null) {
59 |       this.input = autosuggest.input;
60 |     }
61 |   };
62 | 
63 |   render() {
64 |     const { value, suggestions } = this.state;
65 |     const inputProps = {
66 |       value,
67 |       onChange,
68 |       onKeyDown: saveKeyDown,
69 |     };
70 | 
71 |     return (
72 |       <Autosuggest
73 |         suggestions={suggestions}
74 |         onSuggestionsFetchRequested={onSuggestionsFetchRequested}
75 |         onSuggestionsClearRequested={onSuggestionsClearRequested}
76 |         getSuggestionValue={getSuggestionValue}
77 |         onSuggestionSelected={onSuggestionSelected}
78 |         renderSuggestion={renderSuggestion}
79 |         renderInputComponent={renderTextarea}
80 |         inputProps={inputProps}
81 |         ref={this.storeAutosuggestReference}
82 |       />
83 |     );
84 |   }
85 | }
86 | 


--------------------------------------------------------------------------------
/test/textarea/AutosuggestApp.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 |   focusAndSetInputValue,
 7 |   clickDown,
 8 |   clickEnter,
 9 |   expectInputValue,
10 |   clearEvents,
11 |   expectLetBrowserHandleKeyDown,
12 |   expectDontLetBrowserHandleKeyDown
13 | } from '../helpers';
14 | import AutosuggestApp, { onSuggestionSelected } from './AutosuggestApp';
15 | 
16 | describe('Autosuggest with textarea', () => {
17 |   beforeEach(() => {
18 |     init(TestUtils.renderIntoDocument(<AutosuggestApp />));
19 | 
20 |     focusAndSetInputValue('p');
21 |     clearEvents();
22 |   });
23 | 
24 |   it("inserts a newline if you press enter without selecting a suggestion", () => {
25 |     clickEnter();
26 | 
27 |     expectInputValue('p');
28 |     expect(onSuggestionSelected).not.to.have.been.called;
29 |     expectLetBrowserHandleKeyDown();
30 |   });
31 | 
32 |   it("doesn't insert a newline if you select a suggestion with enter", () => {
33 |     clickDown();
34 |     clearEvents();
35 |     clickEnter();
36 | 
37 |     expectInputValue('Perl');
38 |     expect(onSuggestionSelected).to.have.been.calledOnce;
39 |     expectDontLetBrowserHandleKeyDown();
40 |   });
41 | });
42 | 


--------------------------------------------------------------------------------
/test/textarea/languages.js:
--------------------------------------------------------------------------------
 1 | export default [
 2 |   {
 3 |     name: 'C',
 4 |     year: 1972
 5 |   },
 6 |   {
 7 |     name: 'C#',
 8 |     year: 2000
 9 |   },
10 |   {
11 |     name: 'C++',
12 |     year: 1983
13 |   },
14 |   {
15 |     name: 'Clojure',
16 |     year: 2007
17 |   },
18 |   {
19 |     name: 'Elm',
20 |     year: 2012
21 |   },
22 |   {
23 |     name: 'Go',
24 |     year: 2009
25 |   },
26 |   {
27 |     name: 'Haskell',
28 |     year: 1990
29 |   },
30 |   {
31 |     name: 'Java',
32 |     year: 1995
33 |   },
34 |   {
35 |     name: 'JavaScript',
36 |     year: 1995
37 |   },
38 |   {
39 |     name: 'Perl',
40 |     year: 1987
41 |   },
42 |   {
43 |     name: 'PHP',
44 |     year: 1995
45 |   },
46 |   {
47 |     name: 'Python',
48 |     year: 1991
49 |   },
50 |   {
51 |     name: 'Ruby',
52 |     year: 1995
53 |   },
54 |   {
55 |     name: 'Scala',
56 |     year: 2003
57 |   }
58 | ];
59 | 


--------------------------------------------------------------------------------
/webpack.dev.config.js:
--------------------------------------------------------------------------------
 1 | const path = require('path');
 2 | const webpack = require('webpack');
 3 | const autoprefixer = require('autoprefixer');
 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
 5 | 
 6 | const host = process.env.NODE_HOST || 'localhost';
 7 | const port = process.env.NODE_PORT || 3000;
 8 | 
 9 | module.exports = {
10 |   mode: 'development',
11 |   entry: [
12 |     `webpack-dev-server/client?http://${host}:${port}`,
13 |     './demo/src/index'
14 |   ],
15 | 
16 |   output: {
17 |     path: path.join(__dirname, 'dist'), // Must be an absolute path
18 |     filename: 'index.js',
19 |     publicPath: '/demo/dist/'
20 |   },
21 | 
22 |   module: {
23 |     rules: [
24 |       {
25 |         test: /\.js$/,
26 |         loader: 'babel-loader',
27 |         include: [
28 |           path.join(__dirname, 'src'), // Must be an absolute path
29 |           path.join(__dirname, 'demo', 'src') // Must be an absolute path
30 |         ]
31 |       },
32 |       {
33 |         test: /\.less$/,
34 |         use: [
35 |           MiniCssExtractPlugin.loader,
36 |           {
37 |             loader: 'css-loader',
38 |             options: {
39 |               modules: {
40 |                 localIdentName: '[name]__[local]___[hash:base64:5]'
41 |               }
42 |             }
43 |           },
44 |           {
45 |             loader: 'postcss-loader',
46 |             options: {
47 |               plugins: [autoprefixer()]
48 |             }
49 |           },
50 |           {
51 |             loader: 'less-loader',
52 |             options: {
53 |               paths: [path.resolve(__dirname, 'demo', 'src')]
54 |             }
55 |           }
56 |         ],
57 |         exclude: /node_modules/
58 |       },
59 |       {
60 |         test: /\.jpg$/,
61 |         loader: 'url-loader?limit=8192' // 8kb
62 |       },
63 |       {
64 |         test: /\.svg$/,
65 |         use: [
66 |           'url-loader?limit=8192!', // 8kb
67 |           'svgo-loader'
68 |         ]
69 |       }
70 |     ]
71 |   },
72 | 
73 |   resolve: {
74 |     modules: [
75 |       'node_modules',
76 |       'components',
77 |       'src',
78 |       path.join(__dirname, 'demo', 'src') // Must be an absolute path
79 |     ]
80 |   },
81 | 
82 |   devtool: 'source-map',
83 | 
84 |   plugins: [
85 |     new webpack.HotModuleReplacementPlugin(),
86 |     new MiniCssExtractPlugin({
87 |       filename: 'app.css'
88 |     })
89 |   ]
90 | };
91 | 


--------------------------------------------------------------------------------
/webpack.gh-pages.config.js:
--------------------------------------------------------------------------------
 1 | const path = require('path');
 2 | const webpack = require('webpack');
 3 | const autoprefixer = require('autoprefixer');
 4 | const TerserPlugin = require('terser-webpack-plugin');
 5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
 6 | 
 7 | module.exports = {
 8 |   entry: './demo/src/index',
 9 |   mode: 'production',
10 | 
11 |   output: {
12 |     path: path.join(__dirname, 'demo', 'dist'),
13 |     filename: 'index.js'
14 |   },
15 | 
16 |   optimization: {
17 |     minimizer: [
18 |       new TerserPlugin({
19 |         cache: true,
20 |         parallel: true
21 |       })
22 |     ]
23 |   },
24 | 
25 |   module: {
26 |     rules: [
27 |       {
28 |         test: /\.js$/,
29 |         loader: 'babel-loader',
30 |         include: [
31 |           path.join(__dirname, 'src'), // Must be an absolute path
32 |           path.join(__dirname, 'demo', 'src') // Must be an absolute path
33 |         ]
34 |       },
35 |       {
36 |         test: /\.less$/,
37 |         use: [
38 |           MiniCssExtractPlugin.loader,
39 |           {
40 |             loader: 'css-loader',
41 |             options: {
42 |               modules: {
43 |                 localIdentName: '[name]__[local]___[hash:base64:5]'
44 |               }
45 |             }
46 |           },
47 |           {
48 |             loader: 'postcss-loader',
49 |             options: {
50 |               plugins: [autoprefixer()]
51 |             }
52 |           },
53 |           {
54 |             loader: 'less-loader',
55 |             options: {
56 |               paths: [path.resolve(__dirname, 'demo', 'src')]
57 |             }
58 |           }
59 |         ],
60 |         exclude: /node_modules/
61 |       },
62 |       {
63 |         test: /\.jpg$/,
64 |         loader: 'url-loader?limit=8192' // 8kb
65 |       },
66 |       {
67 |         test: /\.svg$/,
68 |         use: [
69 |           'url-loader?limit=8192!', // 8kb
70 |           'svgo-loader'
71 |         ]
72 |       }
73 |     ]
74 |   },
75 | 
76 |   resolve: {
77 |     modules: ['node_modules', 'components', 'src']
78 |   },
79 | 
80 |   plugins: [
81 |     new MiniCssExtractPlugin({
82 |       filename: 'app.css'
83 |     }),
84 |     new webpack.DefinePlugin({
85 |       'process.env': {
86 |         NODE_ENV: JSON.stringify('production')
87 |       }
88 |     })
89 |   ]
90 | };
91 | 


--------------------------------------------------------------------------------
/webpack.standalone-demo.config.js:
--------------------------------------------------------------------------------
 1 | var path = require('path');
 2 | 
 3 | module.exports = {
 4 |   entry: ['./demo/standalone/app'],
 5 |   mode: 'production',
 6 | 
 7 |   output: {
 8 |     filename: './demo/standalone/compiled.app.js'
 9 |   },
10 | 
11 |   module: {
12 |     rules: [
13 |       {
14 |         test: /\.js$/,
15 |         loader: 'babel-loader',
16 |         include: [
17 |           path.join(__dirname, 'demo', 'standalone') // Must be an absolute path
18 |         ]
19 |       }
20 |     ]
21 |   }
22 | };
23 | 


--------------------------------------------------------------------------------
/webpack.standalone.config.js:
--------------------------------------------------------------------------------
 1 | const path = require('path');
 2 | const webpack = require('webpack');
 3 | const TerserPlugin = require('terser-webpack-plugin');
 4 | 
 5 | module.exports = [
 6 |   {
 7 |     entry: './src/index.js',
 8 |     mode: 'production',
 9 | 
10 |     output: {
11 |       filename: './dist/standalone/autosuggest.js',
12 |       libraryTarget: 'umd',
13 |       library: 'Autosuggest'
14 |     },
15 | 
16 |     optimization: {
17 |       minimizer: [
18 |         new TerserPlugin({
19 |           cache: true,
20 |           parallel: true
21 |         })
22 |       ]
23 |     },
24 | 
25 |     module: {
26 |       rules: [
27 |         {
28 |           test: /\.js$/,
29 |           loader: 'babel-loader',
30 |           include: [
31 |             path.join(__dirname, 'src') // Must be an absolute path
32 |           ]
33 |         }
34 |       ]
35 |     },
36 | 
37 |     externals: {
38 |       react: 'React'
39 |     }
40 |   },
41 |   {
42 |     entry: './src/index.js',
43 |     mode: 'production',
44 | 
45 |     output: {
46 |       filename: './dist/standalone/autosuggest.min.js',
47 |       libraryTarget: 'umd',
48 |       library: 'Autosuggest'
49 |     },
50 | 
51 |     module: {
52 |       rules: [
53 |         {
54 |           test: /\.js$/,
55 |           loader: 'babel-loader',
56 |           include: [
57 |             path.join(__dirname, 'src') // Must be an absolute path
58 |           ]
59 |         }
60 |       ]
61 |     },
62 | 
63 |     externals: {
64 |       react: 'React'
65 |     },
66 | 
67 |     plugins: [
68 |       new webpack.DefinePlugin({
69 |         'process.env': {
70 |           NODE_ENV: JSON.stringify('production')
71 |         }
72 |       })
73 |     ]
74 |   }
75 | ];
76 | 


--------------------------------------------------------------------------------