├── .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 Escape 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 | 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 | 16 | ``` 17 | 18 | [Codepen example](http://codepen.io/moroshko/pen/WryOMP) 19 | 20 | 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 | 38 | {children} 39 | 40 | ); 41 | } 42 | 43 | 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 |
11 |
12 | 13 | 14 |
15 |
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 |
11 |

Examples

12 | 13 | 14 | 15 | 16 |
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 => {suggestion.name}; 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 |
66 |
67 |
Basic
68 |
69 | Let’s start simple. Here’s a plain list of suggestions. 70 |
71 | 76 | Codepen 77 | 78 |
79 |
80 | 90 |
91 |
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 | 37 | 38 | {parts.map((part, index) => { 39 | const className = part.highlight ? theme.highlight : null; 40 | 41 | return ( 42 | 43 | {part.text} 44 | 45 | ); 46 | })} 47 | 48 | 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 |
94 |
95 |
Custom render
96 |
97 | Apply any styling you wish. 98 |
99 | For example, render images and highlight the matching string. 100 |
101 | 106 | Codepen 107 | 108 |
109 |
110 | 121 |
122 |
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 => {suggestion.name}; 37 | 38 | const renderSectionTitle = section => {section.title}; 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 |
80 |
81 |
Multiple sections
82 |
83 | Suggestions can also be presented in multiple sections. Note that we 84 | highlight the first suggestion by default here. 85 |
86 | 91 | Codepen 92 | 93 |
94 |
95 | 110 |
111 |
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 |
105 |
106 |
Scrollable container
107 |
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 |
112 |
113 |
114 |
Where do you live?
115 |
{selected.name}
116 | 119 |
120 | 130 |
Please select a country:
131 |
132 | 143 |
144 | 147 |
148 |
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 |
8 |

Features

9 |
10 |
11 |
12 |
Accessible
13 |
14 | 18 | WAI-ARIA compliant 19 | 20 | , with support for ARIA attributes and keyboard interactions. 21 |
22 |
23 |
24 |
25 |
Mobile friendly
26 |
27 | Works well on those little devices you carry around in your hands. 28 |
29 |
30 |
31 |
32 |
Customizable
33 |
34 | Supports custom suggestion rendering, multiple sections, and more. 35 |
36 |
37 |
38 |
39 | {'Check out the '} 40 | 44 | GitHub page 45 | 46 | {' for a full list of features.'} 47 |
48 |
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 | 2 | 3 | 4 | icon_accessible 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /demo/src/components/App/components/Features/customizable.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | icon_customisable copy 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /demo/src/components/App/components/Features/mobile-friendly.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | icon_mobile-friendly 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 |
8 |
9 | {'Crafted with love by '} 10 | 15 | @moroshko 16 | 17 |
18 |
19 | {'Page design by '} 20 | 25 | @vedranio 26 | 27 |
28 |
29 | {'Licensed under '} 30 | 35 | MIT license 36 | 37 |
38 |
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 | 16 | 17 | 18 | 23 | 27 | 28 | 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 |
34 |
35 |

React Autosuggest

36 |
37 | WAI-ARIA compliant autosuggest component built in React 38 |
39 | 45 | Get started 46 | 47 |
48 | 53 | {stargazers} stargazers 54 | 55 | 60 | @moroshko 61 | 62 |
63 | 64 |
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 | 2 | 3 | 4 | icon_react-logo 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /demo/src/components/App/components/Header/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | star 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /demo/src/components/App/components/Header/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | icon_twitter 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /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 | 14 | {children} 15 | 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, '\\$&'); 3 | 4 | export { escapeRegexCharacters }; 5 | -------------------------------------------------------------------------------- /demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Autosuggest 6 | 7 | 8 | 9 | 10 | 11 | 12 | 24 | 25 | 26 |
27 | 28 | 29 | 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(, 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, '\\$&'); 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 => {suggestion.name}; 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 | 127 | ); 128 | } 129 | } 130 | 131 | ReactDOM.render(, 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 | 2 | 3 | 4 | 5 | React Autosuggest 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 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 ", 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 |
  • 83 | {renderItem(item, { isHighlighted, ...renderItemData })} 84 |
  • 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 |
      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 | 89 | ); 90 | /* eslint-enable react/jsx-key */ 91 | })} 92 |
    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 |
    27 | {sectionTitle} 28 |
    29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/compareObjects.js: -------------------------------------------------------------------------------- 1 | export default function compareObjects(objA, objB, keys = []) { 2 | if (objA === objB) { 3 | return false; 4 | } 5 | 6 | const aKeys = Object.keys(objA); 7 | const bKeys = Object.keys(objB); 8 | 9 | if (aKeys.length !== bKeys.length) { 10 | return true; 11 | } 12 | 13 | const keysMap = {}; 14 | let i, len; 15 | 16 | for (i = 0, len = keys.length; i < len; i++) { 17 | keysMap[keys[i]] = true; 18 | } 19 | 20 | for (i = 0, len = aKeys.length; i < len; i++) { 21 | let key = aKeys[i]; 22 | const aValue = objA[key]; 23 | const bValue = objB[key]; 24 | 25 | if (aValue === bValue) { 26 | continue; 27 | } 28 | 29 | if ( 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 {suggestion.name}; 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 | 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()); 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 | 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()); 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(); 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 | 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()); 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 | 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()); 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 | {section.title} 10 | )); 11 | 12 | export const getSectionItems = sinon.spy((section) => section.items); 13 | 14 | export const renderItem = sinon.spy((item) => {item.text}); 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 | 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()); 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) => {item.text}); 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 | 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()); 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 |
    11 | {children} 12 |
    Footer
    13 |
    14 | ) 15 | ); 16 | 17 | export default class AutowhateverApp extends Component { 18 | constructor() { 19 | super(); 20 | 21 | this.state = { 22 | value: '', 23 | }; 24 | } 25 | 26 | onChange = (event) => { 27 | this.setState({ 28 | value: event.target.value, 29 | }); 30 | }; 31 | 32 | render() { 33 | const { value } = this.state; 34 | const inputProps = { 35 | id: 'my-custom-input', 36 | value, 37 | onChange: this.onChange, 38 | }; 39 | 40 | return ( 41 | 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/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 {suggestion.name}; 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 | 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()); 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 => {suggestion.name}; 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 | 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()); 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 {suggestion.name}; 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 | 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()); 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) => {suggestion.name}; 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 | 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()); 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 {suggestion.name}; 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 {section.title}; 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 |
    104 | 105 | 120 |
    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()); 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('C'); 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 | {part.text} 30 | ) : ( 31 | {part.text} 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 | 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 |
    39 | 40 |
    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 | 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()); 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 |
    41 | {children} 42 |
    43 | Press Enter to search {query} 44 |
    45 |
    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 | 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()); 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(``); 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 |