├── .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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
43 | );
44 |
45 | export default class AutosuggestApp extends Component {
46 | constructor() {
47 | super();
48 |
49 | app = this;
50 |
51 | this.state = {
52 | value: '',
53 | suggestions: []
54 | };
55 | }
56 |
57 | storeAutosuggestReference = autosuggest => {
58 | if (autosuggest !== null) {
59 | this.input = autosuggest.input;
60 | }
61 | };
62 |
63 | render() {
64 | const { value, suggestions } = this.state;
65 | const inputProps = {
66 | value,
67 | onChange,
68 | onKeyDown: saveKeyDown,
69 | };
70 |
71 | return (
72 |
83 | );
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/test/textarea/AutosuggestApp.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TestUtils from 'react-dom/test-utils';
3 | import { expect } from 'chai';
4 | import {
5 | init,
6 | focusAndSetInputValue,
7 | clickDown,
8 | clickEnter,
9 | expectInputValue,
10 | clearEvents,
11 | expectLetBrowserHandleKeyDown,
12 | expectDontLetBrowserHandleKeyDown
13 | } from '../helpers';
14 | import AutosuggestApp, { onSuggestionSelected } from './AutosuggestApp';
15 |
16 | describe('Autosuggest with textarea', () => {
17 | beforeEach(() => {
18 | init(TestUtils.renderIntoDocument( ));
19 |
20 | focusAndSetInputValue('p');
21 | clearEvents();
22 | });
23 |
24 | it("inserts a newline if you press enter without selecting a suggestion", () => {
25 | clickEnter();
26 |
27 | expectInputValue('p');
28 | expect(onSuggestionSelected).not.to.have.been.called;
29 | expectLetBrowserHandleKeyDown();
30 | });
31 |
32 | it("doesn't insert a newline if you select a suggestion with enter", () => {
33 | clickDown();
34 | clearEvents();
35 | clickEnter();
36 |
37 | expectInputValue('Perl');
38 | expect(onSuggestionSelected).to.have.been.calledOnce;
39 | expectDontLetBrowserHandleKeyDown();
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/test/textarea/languages.js:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | name: 'C',
4 | year: 1972
5 | },
6 | {
7 | name: 'C#',
8 | year: 2000
9 | },
10 | {
11 | name: 'C++',
12 | year: 1983
13 | },
14 | {
15 | name: 'Clojure',
16 | year: 2007
17 | },
18 | {
19 | name: 'Elm',
20 | year: 2012
21 | },
22 | {
23 | name: 'Go',
24 | year: 2009
25 | },
26 | {
27 | name: 'Haskell',
28 | year: 1990
29 | },
30 | {
31 | name: 'Java',
32 | year: 1995
33 | },
34 | {
35 | name: 'JavaScript',
36 | year: 1995
37 | },
38 | {
39 | name: 'Perl',
40 | year: 1987
41 | },
42 | {
43 | name: 'PHP',
44 | year: 1995
45 | },
46 | {
47 | name: 'Python',
48 | year: 1991
49 | },
50 | {
51 | name: 'Ruby',
52 | year: 1995
53 | },
54 | {
55 | name: 'Scala',
56 | year: 2003
57 | }
58 | ];
59 |
--------------------------------------------------------------------------------
/webpack.dev.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const autoprefixer = require('autoprefixer');
4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
5 |
6 | const host = process.env.NODE_HOST || 'localhost';
7 | const port = process.env.NODE_PORT || 3000;
8 |
9 | module.exports = {
10 | mode: 'development',
11 | entry: [
12 | `webpack-dev-server/client?http://${host}:${port}`,
13 | './demo/src/index'
14 | ],
15 |
16 | output: {
17 | path: path.join(__dirname, 'dist'), // Must be an absolute path
18 | filename: 'index.js',
19 | publicPath: '/demo/dist/'
20 | },
21 |
22 | module: {
23 | rules: [
24 | {
25 | test: /\.js$/,
26 | loader: 'babel-loader',
27 | include: [
28 | path.join(__dirname, 'src'), // Must be an absolute path
29 | path.join(__dirname, 'demo', 'src') // Must be an absolute path
30 | ]
31 | },
32 | {
33 | test: /\.less$/,
34 | use: [
35 | MiniCssExtractPlugin.loader,
36 | {
37 | loader: 'css-loader',
38 | options: {
39 | modules: {
40 | localIdentName: '[name]__[local]___[hash:base64:5]'
41 | }
42 | }
43 | },
44 | {
45 | loader: 'postcss-loader',
46 | options: {
47 | plugins: [autoprefixer()]
48 | }
49 | },
50 | {
51 | loader: 'less-loader',
52 | options: {
53 | paths: [path.resolve(__dirname, 'demo', 'src')]
54 | }
55 | }
56 | ],
57 | exclude: /node_modules/
58 | },
59 | {
60 | test: /\.jpg$/,
61 | loader: 'url-loader?limit=8192' // 8kb
62 | },
63 | {
64 | test: /\.svg$/,
65 | use: [
66 | 'url-loader?limit=8192!', // 8kb
67 | 'svgo-loader'
68 | ]
69 | }
70 | ]
71 | },
72 |
73 | resolve: {
74 | modules: [
75 | 'node_modules',
76 | 'components',
77 | 'src',
78 | path.join(__dirname, 'demo', 'src') // Must be an absolute path
79 | ]
80 | },
81 |
82 | devtool: 'source-map',
83 |
84 | plugins: [
85 | new webpack.HotModuleReplacementPlugin(),
86 | new MiniCssExtractPlugin({
87 | filename: 'app.css'
88 | })
89 | ]
90 | };
91 |
--------------------------------------------------------------------------------
/webpack.gh-pages.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const autoprefixer = require('autoprefixer');
4 | const TerserPlugin = require('terser-webpack-plugin');
5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
6 |
7 | module.exports = {
8 | entry: './demo/src/index',
9 | mode: 'production',
10 |
11 | output: {
12 | path: path.join(__dirname, 'demo', 'dist'),
13 | filename: 'index.js'
14 | },
15 |
16 | optimization: {
17 | minimizer: [
18 | new TerserPlugin({
19 | cache: true,
20 | parallel: true
21 | })
22 | ]
23 | },
24 |
25 | module: {
26 | rules: [
27 | {
28 | test: /\.js$/,
29 | loader: 'babel-loader',
30 | include: [
31 | path.join(__dirname, 'src'), // Must be an absolute path
32 | path.join(__dirname, 'demo', 'src') // Must be an absolute path
33 | ]
34 | },
35 | {
36 | test: /\.less$/,
37 | use: [
38 | MiniCssExtractPlugin.loader,
39 | {
40 | loader: 'css-loader',
41 | options: {
42 | modules: {
43 | localIdentName: '[name]__[local]___[hash:base64:5]'
44 | }
45 | }
46 | },
47 | {
48 | loader: 'postcss-loader',
49 | options: {
50 | plugins: [autoprefixer()]
51 | }
52 | },
53 | {
54 | loader: 'less-loader',
55 | options: {
56 | paths: [path.resolve(__dirname, 'demo', 'src')]
57 | }
58 | }
59 | ],
60 | exclude: /node_modules/
61 | },
62 | {
63 | test: /\.jpg$/,
64 | loader: 'url-loader?limit=8192' // 8kb
65 | },
66 | {
67 | test: /\.svg$/,
68 | use: [
69 | 'url-loader?limit=8192!', // 8kb
70 | 'svgo-loader'
71 | ]
72 | }
73 | ]
74 | },
75 |
76 | resolve: {
77 | modules: ['node_modules', 'components', 'src']
78 | },
79 |
80 | plugins: [
81 | new MiniCssExtractPlugin({
82 | filename: 'app.css'
83 | }),
84 | new webpack.DefinePlugin({
85 | 'process.env': {
86 | NODE_ENV: JSON.stringify('production')
87 | }
88 | })
89 | ]
90 | };
91 |
--------------------------------------------------------------------------------
/webpack.standalone-demo.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 |
3 | module.exports = {
4 | entry: ['./demo/standalone/app'],
5 | mode: 'production',
6 |
7 | output: {
8 | filename: './demo/standalone/compiled.app.js'
9 | },
10 |
11 | module: {
12 | rules: [
13 | {
14 | test: /\.js$/,
15 | loader: 'babel-loader',
16 | include: [
17 | path.join(__dirname, 'demo', 'standalone') // Must be an absolute path
18 | ]
19 | }
20 | ]
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/webpack.standalone.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const TerserPlugin = require('terser-webpack-plugin');
4 |
5 | module.exports = [
6 | {
7 | entry: './src/index.js',
8 | mode: 'production',
9 |
10 | output: {
11 | filename: './dist/standalone/autosuggest.js',
12 | libraryTarget: 'umd',
13 | library: 'Autosuggest'
14 | },
15 |
16 | optimization: {
17 | minimizer: [
18 | new TerserPlugin({
19 | cache: true,
20 | parallel: true
21 | })
22 | ]
23 | },
24 |
25 | module: {
26 | rules: [
27 | {
28 | test: /\.js$/,
29 | loader: 'babel-loader',
30 | include: [
31 | path.join(__dirname, 'src') // Must be an absolute path
32 | ]
33 | }
34 | ]
35 | },
36 |
37 | externals: {
38 | react: 'React'
39 | }
40 | },
41 | {
42 | entry: './src/index.js',
43 | mode: 'production',
44 |
45 | output: {
46 | filename: './dist/standalone/autosuggest.min.js',
47 | libraryTarget: 'umd',
48 | library: 'Autosuggest'
49 | },
50 |
51 | module: {
52 | rules: [
53 | {
54 | test: /\.js$/,
55 | loader: 'babel-loader',
56 | include: [
57 | path.join(__dirname, 'src') // Must be an absolute path
58 | ]
59 | }
60 | ]
61 | },
62 |
63 | externals: {
64 | react: 'React'
65 | },
66 |
67 | plugins: [
68 | new webpack.DefinePlugin({
69 | 'process.env': {
70 | NODE_ENV: JSON.stringify('production')
71 | }
72 | })
73 | ]
74 | }
75 | ];
76 |
--------------------------------------------------------------------------------