├── .prettierignore ├── .gitattributes ├── .storybook ├── addons.js ├── public │ ├── frodo.jpg │ ├── aragorn.jpg │ ├── gandalf.png │ └── samwise.jpg ├── config.js └── preview-head.html ├── .npmrc ├── docs ├── logo.png ├── storybook │ ├── favicon.ico │ ├── index.html │ └── iframe.html ├── index.js ├── webpack.config.js ├── index.html └── App.vue ├── .github ├── FUNDING.yml ├── workflows │ └── test.yml ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── __tests__ ├── test-compat-all.sh ├── test-compat.sh ├── autosuggest.test.js └── __snapshots__ │ └── autosuggest.test.js.snap ├── .prettierrc.js ├── .eslintrc.js ├── other ├── USERS.md ├── manual-releases.md ├── CODE_OF_CONDUCT.md └── MAINTAINING.md ├── src ├── utils.js ├── vue-autosuggest.js ├── parts │ └── DefaultSection.js ├── stories │ └── index.js └── Autosuggest.vue ├── .babelrc ├── codecov.yml ├── LICENSE ├── CONTRIBUTING.md ├── .all-contributorsrc ├── CHANGELOG.md ├── package.json └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | *.vue -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import "@storybook/addon-actions/register"; 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org/ 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrenjennings/vue-autosuggest/HEAD/docs/logo.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [darrenjennings] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | .opt-in 5 | .opt-out 6 | .DS_Store 7 | .eslintcache 8 | .idea 9 | -------------------------------------------------------------------------------- /docs/storybook/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrenjennings/vue-autosuggest/HEAD/docs/storybook/favicon.ico -------------------------------------------------------------------------------- /.storybook/public/frodo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrenjennings/vue-autosuggest/HEAD/.storybook/public/frodo.jpg -------------------------------------------------------------------------------- /.storybook/public/aragorn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrenjennings/vue-autosuggest/HEAD/.storybook/public/aragorn.jpg -------------------------------------------------------------------------------- /.storybook/public/gandalf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrenjennings/vue-autosuggest/HEAD/.storybook/public/gandalf.png -------------------------------------------------------------------------------- /.storybook/public/samwise.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrenjennings/vue-autosuggest/HEAD/.storybook/public/samwise.jpg -------------------------------------------------------------------------------- /__tests__/test-compat-all.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | __tests__/test-compat.sh "~2.5.0" 4 | __tests__/test-compat.sh "~2.6.0" 5 | 6 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | // .prettierrc.js 2 | module.exports = { 3 | printWidth: 100, 4 | singleQuote: false, 5 | semi: true 6 | }; 7 | -------------------------------------------------------------------------------- /__tests__/test-compat.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | echo "running unit tests with Vue $1" 4 | yarn add -W -D vue@$1 vue-template-compiler@$1 vue-server-renderer@$1 5 | yarn test 6 | -------------------------------------------------------------------------------- /docs/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from './App.vue'; 3 | 4 | // Vue.config.performance = true 5 | 6 | new Vue({ 7 | el: "#app", 8 | components: { 9 | App 10 | }, 11 | template: "" 12 | }); 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true 4 | }, 5 | extends: ["plugin:vue/recommended"], 6 | plugins: ["vue"], 7 | rules: { 8 | "vue/valid-v-if": "error", 9 | "no-console": ["warn", { allow: ["warn", "error"] }] 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/vue'; 2 | 3 | import Vue from 'vue'; 4 | import VueAutosuggest from "../src/Autosuggest.vue"; 5 | 6 | function loadStories() { 7 | // You can require as many stories as you need. 8 | require('../src/stories'); 9 | } 10 | 11 | configure(loadStories, module); 12 | -------------------------------------------------------------------------------- /other/USERS.md: -------------------------------------------------------------------------------- 1 | # Users 2 | 3 | If you or your company uses this project, add your name to this list! Eventually 4 | we may have a website to showcase these (wanna build it!?) 5 | 6 | > [Educents](https://www.educents.com) uses it on [Educents.com](https://www.educents.com) 7 | 8 | 13 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /** DOM Utilities */ 2 | function hasClass(el, className) { 3 | return !!el.className.match(new RegExp("(\\s|^)" + className + "(\\s|$)")); 4 | } 5 | 6 | function addClass(el, className) { 7 | if (!hasClass(el, className)) el.className += " " + className; 8 | } 9 | 10 | function removeClass(el, className) { 11 | if (el.classList) { 12 | el.classList.remove(className); 13 | } 14 | } 15 | 16 | export { addClass, removeClass }; 17 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "browsers": ["last 2 versions", "safari >= 7"], 8 | "uglify": true 9 | }, 10 | "modules": "umd" 11 | } 12 | ] 13 | ], 14 | "plugins": [ 15 | "transform-object-rest-spread", 16 | "transform-vue-jsx", 17 | [ 18 | "transform-runtime", 19 | { 20 | "polyfill": true, 21 | "regenerator": true 22 | } 23 | ] 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /src/vue-autosuggest.js: -------------------------------------------------------------------------------- 1 | import VueAutosuggest from "./Autosuggest.vue"; 2 | import DefaultSection from "./parts/DefaultSection.js"; 3 | 4 | const VueAutosuggestPlugin = { 5 | install(Vue) { 6 | Vue.component("vue-autosuggest-default-section", DefaultSection); 7 | Vue.component("vue-autosuggest", VueAutosuggest); 8 | } 9 | }; 10 | 11 | export default VueAutosuggestPlugin; 12 | export { VueAutosuggest, DefaultSection }; 13 | 14 | if (typeof window !== "undefined" && window.Vue) { 15 | window.Vue.use(VueAutosuggestPlugin); 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: "Test" 2 | 3 | on: push 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Set up Node 14 11 | uses: actions/setup-node@v2 12 | with: 13 | node-version: 14 14 | cache: "yarn" 15 | - name: Install dependencies 16 | run: yarn install 17 | - name: Run Tests 18 | run: yarn test-compat 19 | - name: Run Coverage 20 | run: yarn test:coverage 21 | - name: Run Build 22 | run: yarn build:umd 23 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: yes 3 | 4 | coverage: 5 | precision: 2 6 | round: down 7 | range: "70...100" 8 | status: 9 | project: 10 | default: 11 | threshold: 0.2 12 | if_not_found: success 13 | patch: 14 | default: 15 | enabled: no 16 | if_not_found: success 17 | changes: 18 | default: 19 | enabled: no 20 | if_not_found: success 21 | 22 | parsers: 23 | gcov: 24 | branch_detection: 25 | conditional: yes 26 | loop: yes 27 | method: no 28 | macro: no 29 | 30 | comment: false 31 | 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Educents 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | - `vue-autosuggest` version: 15 | - `node` version: 16 | - `npm` (or `yarn`) version: 17 | 18 | Relevant code or config 19 | 20 | ```javascript 21 | 22 | ``` 23 | 24 | What you did: 25 | 26 | 27 | 28 | What happened: 29 | 30 | 31 | 32 | Reproduction sandbox/repository: 33 | 34 | 41 | 42 | Problem description: 43 | 44 | 45 | 46 | Suggested solution: 47 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | **What**: 19 | 20 | 21 | **Why**: 22 | 23 | 24 | **How**: 25 | 26 | 27 | **Checklist**: 28 | 29 | 30 | - [ ] Documentation 31 | - [ ] Tests 32 | - [ ] Ready to be merged 33 | - [ ] Added myself to contributors table 34 | 35 | 36 | -------------------------------------------------------------------------------- /other/manual-releases.md: -------------------------------------------------------------------------------- 1 | # manual-releases 2 | 3 | This project has an automated release set up. So things are only released when there are 4 | useful changes in the code that justify a release. But sometimes things get messed up one way or another 5 | and we need to trigger the release ourselves. When this happens, simply bump the number below and commit 6 | that with the following commit message based on your needs: 7 | 8 | **Major** 9 | 10 | ``` 11 | fix(release): manually release a major version 12 | 13 | There was an issue with a major release, so this manual-releases.md 14 | change is to release a new major version. 15 | 16 | Reference: # 17 | 18 | BREAKING CHANGE: 19 | ``` 20 | 21 | **Minor** 22 | 23 | ``` 24 | feat(release): manually release a minor version 25 | 26 | There was an issue with a minor release, so this manual-releases.md 27 | change is to release a new minor version. 28 | 29 | Reference: # 30 | ``` 31 | 32 | **Patch** 33 | 34 | ``` 35 | fix(release): manually release a patch version 36 | 37 | There was an issue with a patch release, so this manual-releases.md 38 | change is to release a new patch version. 39 | 40 | Reference: # 41 | ``` 42 | 43 | The number of times we've had to do a manual release is: 0 44 | -------------------------------------------------------------------------------- /docs/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const eslintFriendlyFormatter = require("eslint-friendly-formatter"); 3 | 4 | module.exports = { 5 | context: __dirname, 6 | mode: process.env.NODE_ENV || 'development', 7 | module: { 8 | rules: [ 9 | { 10 | test: /\.js$/, 11 | exclude: /node_modules/, 12 | test: /\.(js|vue)$/, 13 | loader: "eslint-loader", 14 | enforce: "pre", 15 | options: { 16 | formatter: eslintFriendlyFormatter 17 | } 18 | }, 19 | { 20 | test: /\.js/, 21 | loader: "babel-loader", 22 | exclude: /node_modules/, 23 | options: { 24 | babelrc: true 25 | } 26 | }, 27 | { 28 | test: /\.vue$/, 29 | loaders: ["vue-loader"], 30 | exclude: /node_modules/ 31 | } 32 | ] 33 | }, 34 | 35 | resolve: { 36 | extensions: [".js", ".vue"], 37 | alias: { 38 | vue: "vue/dist/vue.js" 39 | } 40 | }, 41 | 42 | entry: "./index.js", 43 | 44 | output: { 45 | path: path.resolve(__dirname, "build"), 46 | filename: "app.js", 47 | publicPath: "/build/" 48 | }, 49 | 50 | devServer: { 51 | contentBase: __dirname, 52 | port: 2000 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /docs/storybook/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Storybook 10 | 39 | 40 | 41 | 42 |
43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-autosuggest 9 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for being willing to contribute! 4 | 5 | **Working on your first Pull Request?** You can learn how from this *free* series 6 | [How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github) 7 | 8 | ## Project setup 9 | 10 | 1. Fork and clone the repo 11 | 2. Run `npm run setup -s` to install dependencies and run validation 12 | 3. Create a branch for your PR with `git checkout -b pr/your-branch-name` 13 | 14 | > Tip: Keep your `master` branch pointing at the original repository and make 15 | > pull requests from branches on your fork. To do this, run: 16 | > 17 | > ``` 18 | > git remote add upstream https://github.com/educents/vue-autosuggest.git 19 | > git fetch upstream 20 | > git branch --set-upstream-to=upstream/master master 21 | > ``` 22 | > 23 | > This will add the original repository as a "remote" called "upstream," 24 | > Then fetch the git information from that remote, then set your local `master` 25 | > branch to use the upstream master branch whenever you run `git pull`. 26 | > Then you can make all of your pull request branches based on this `master` 27 | > branch. Whenever you want to update your version of `master`, do a regular 28 | > `git pull`. 29 | 30 | ## Add yourself as a contributor 31 | 32 | This project follows the [all contributors][all-contributors] specification. 33 | To add yourself to the table of contributors on the `README.md`, please use the 34 | automated script as part of your PR: 35 | 36 | ```console 37 | yarn add-contributor 38 | ``` 39 | 40 | Follow the prompt and commit `.all-contributorsrc` and `README.md` in the PR. 41 | If you've already added yourself to the list and are making 42 | a new type of contribution, you can run it again and select the added 43 | contribution type. 44 | 45 | ## Committing and Pushing changes 46 | 47 | Please make sure to run the tests before you commit your changes. You can run 48 | `npm run test:update` which will update any snapshots that need updating. 49 | Make sure to include those changes (if they exist) in your commit. 50 | 51 | ## Help needed 52 | 53 | Please checkout [the open issues][issues] 54 | 55 | Also, please watch the repo and respond to questions/bug reports/feature 56 | requests! Thanks! 57 | 58 | [issues]: https://github.com/educents/vue-autosuggest/issues 59 | [all-contributors]: https://github.com/kentcdodds/all-contributors 60 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "vue-autosuggest", 3 | "projectOwner": "darrenjennings", 4 | "files": [ 5 | "README.md" 6 | ], 7 | "imageSize": 100, 8 | "commit": false, 9 | "contributors": [ 10 | { 11 | "login": "darrenjennings", 12 | "name": "Darren Jennings", 13 | "avatar_url": "https://avatars.githubusercontent.com/u/5770711?v=4", 14 | "profile": "https://darrenjennings.github.io", 15 | "contributions": [ 16 | "code", 17 | "doc", 18 | "infra", 19 | "test", 20 | "design", 21 | "example" 22 | ] 23 | }, 24 | { 25 | "login": "ekulish", 26 | "name": "Evgeniy Kulish", 27 | "avatar_url": "https://avatars2.githubusercontent.com/u/411772?v=4", 28 | "profile": "https://github.com/ekulish", 29 | "contributions": [ 30 | "code", 31 | "design", 32 | "example", 33 | "test" 34 | ] 35 | }, 36 | { 37 | "login": "scottadamsmith", 38 | "name": "Scott Smith", 39 | "avatar_url": "https://avatars3.githubusercontent.com/u/1824850?v=4", 40 | "profile": "https://github.com/scottadamsmith", 41 | "contributions": [ 42 | "bug", 43 | "code", 44 | "test" 45 | ] 46 | }, 47 | { 48 | "login": "chuca", 49 | "name": "Fernando Machuca", 50 | "avatar_url": "https://avatars0.githubusercontent.com/u/864496?v=4", 51 | "profile": "https://github.com/chuca", 52 | "contributions": [ 53 | "design" 54 | ] 55 | }, 56 | { 57 | "login": "BerniML", 58 | "name": "BerniML", 59 | "avatar_url": "https://avatars1.githubusercontent.com/u/12657810?v=4", 60 | "profile": "https://github.com/BerniML", 61 | "contributions": [ 62 | "code", 63 | "test" 64 | ] 65 | }, 66 | { 67 | "login": "42tte", 68 | "name": "Kristoffer Nordström", 69 | "avatar_url": "https://avatars0.githubusercontent.com/u/8436510?v=4", 70 | "profile": "https://github.com/42tte", 71 | "contributions": [ 72 | "code", 73 | "test" 74 | ] 75 | }, 76 | { 77 | "login": "djw", 78 | "name": "Dan Wilson", 79 | "avatar_url": "https://avatars.githubusercontent.com/u/8142?v=4", 80 | "profile": "https://www.danjwilson.co.uk", 81 | "contributions": [ 82 | "code" 83 | ] 84 | } 85 | ] 86 | } 87 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 90 | -------------------------------------------------------------------------------- /other/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at devs@educents.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /other/MAINTAINING.md: -------------------------------------------------------------------------------- 1 | # Maintaining 2 | 3 | This is documentation for maintainers of this project. 4 | 5 | ## Code of Conduct 6 | 7 | Please review, understand, and be an example of it. Violations of the code of conduct are 8 | taken seriously, even (especially) for maintainers. 9 | 10 | ## Issues 11 | 12 | We want to support and build the community. We do that best by helping people learn to solve 13 | their own problems. We have an issue template and hopefully most folks follow it. If it's 14 | not clear what the issue is, invite them to create a minimal reproduction of what they're trying 15 | to accomplish or the bug they think they've found. 16 | 17 | Once it's determined that a code change is necessary, point people to 18 | [makeapullrequest.com](http://makeapullrequest.com) and invite them to make a pull request. 19 | If they're the one who needs the feature, they're the one who can build it. If they need 20 | some hand holding and you have time to lend a hand, please do so. It's an investment into 21 | another human being, and an investment into a potential maintainer. 22 | 23 | Remember that this is open source, so the code is not yours, it's ours. If someone needs a change 24 | in the codebase, you don't have to make it happen yourself. Commit as much time to the project 25 | as you want/need to. Nobody can ask any more of you than that. 26 | 27 | ## Pull Requests 28 | 29 | As a maintainer, you're fine to make your branches on the main repo or on your own fork. Either 30 | way is fine. 31 | 32 | When we receive a pull request, an Actions build is kicked off automatically (see `.github/workflows/test.yml` 33 | for what runs in the Actions build). We avoid merging anything that breaks the Actions build. 34 | 35 | Please review PRs and focus on the code rather than the individual. You never know when this is 36 | someone's first ever PR and we want their experience to be as positive as possible, so be 37 | uplifting and constructive. 38 | 39 | When you merge the pull request, 99% of the time you should use the 40 | [Squash and merge](https://help.github.com/articles/merging-a-pull-request/) feature. This keeps 41 | our git history clean, but more importantly, this allows us to make any necessary changes to the 42 | commit message so we release what we want to release. See the next section on Releases for more 43 | about that. 44 | 45 | ## Release 46 | 47 | Our releases are automatic. They happen whenever code lands into `master`. A travis build gets 48 | kicked off and if it's successful, a tool called 49 | [`semantic-release`](https://github.com/semantic-release/semantic-release) is used to 50 | automatically publish a new release to npm as well as a changelog to GitHub. It is only able to 51 | determine the version and whether a release is necessary by the git commit messages. With this 52 | in mind, **please brush up on [the commit message convention][commit] which drives our releases.** 53 | 54 | > One important note about this: Please make sure that commit messages do NOT contain the words 55 | > "BREAKING CHANGE" in them unless we want to push a major version. I've been burned by this 56 | > more than once where someone will include "BREAKING CHANGE: None" and it will end up releasing 57 | > a new major version. Not a huge deal honestly, but kind of annoying... 58 | 59 | ## Thanks! 60 | 61 | Thank you so much for helping to maintain this project! 62 | 63 | [commit]: https://github.com/conventional-changelog-archived-repos/conventional-changelog-angular/blob/ed32559941719a130bb0327f886d6a32a8cbc2ba/convention.md 64 | -------------------------------------------------------------------------------- /docs/storybook/iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 14 | Storybook 15 | 104 | 105 | 106 | 107 | 108 |
109 |
110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | Abridged versions of releases. See [release notes](https://github.com/Educents/vue-autosuggest/releases) for more details. 4 | 5 | * [2.0.4](https://github.com/darrenjennings/vue-autosuggest/releases/tag/v2.0.4) Bugfix 6 | * Fixes #124 7 | * [2.0.3](https://github.com/darrenjennings/vue-autosuggest/releases/tag/v2.0.3) Bugfix 8 | * Fixes #142 9 | * [2.0.2](https://github.com/darrenjennings/vue-autosuggest/releases/tag/v2.0.2) Bugfixes 10 | * Fixes #136, Fixes #135, Fixes #129 11 | * [2.0.1](https://github.com/darrenjennings/vue-autosuggest/releases/tag/v2.0.1) Bugfix 12 | * Fixes #129 where in some cases, re-renders would cause the default autocomplete="off" to be unset. 13 | * [2.0.0](https://github.com/darrenjennings/vue-autosuggest/releases/tag/v2.0.0) :sparkles: Major Release 14 | * See [full release notes](https://github.com/darrenjennings/vue-autosuggest/releases/tag/v2.0.0) 15 | * [1.8.3](https://github.com/darrenjennings/vue-autosuggest/tree/v1.8.3) Bugfix 16 | * Bugfix to support IE/Edge since they don't support scrollTo. #40 17 | * [1.8.2](https://github.com/darrenjennings/vue-autosuggest/tree/v1.8.2) Bugfix 18 | * Fixes #102 Clicking on the scroll bar has been a rough go. This release aims to rid us of the all the frustration. 19 | * [1.8.1](https://github.com/darrenjennings/vue-autosuggest/tree/v1.8.1) Bugfix 20 | * Fix incorrect scrollbar click calculation 21 | * [1.8.0-1](https://github.com/darrenjennings/vue-autosuggest/tree/v1.8.0-1) Bugfix 22 | * Removes form-control as an always present css class on the `` 23 | * Migration from 1.x -> 1.8.0-1 24 | * If you had class set on the inputProps property, then vue-autosuggest used to always append form-control. If class is 25 | empty, the form-control class is the default, but if you have it set then now you need to specifically add form 26 | control to `inputProps.class` if you are wanting it. 27 | * [1.7.3](https://github.com/darrenjennings/vue-autosuggest/tree/v1.7.3) Bugfix, multiple instances navigation fixes 28 | * From [#78](https://github.com/darrenjennings/vue-autosuggest/pull/78) 29 | * [1.7.1-2](https://github.com/darrenjennings/vue-autosuggest/tree/v1.7.1-2) Bugfix 30 | * fix(mousedown) check for presence of results 31 | * Fixes [#66](https://github.com/darrenjennings/vue-autosuggest/issues/66) 32 | * [1.7.1-1](https://github.com/darrenjennings/vue-autosuggest/releases/tag/v1.7.1-1) bugfixes scrollbar clicking and event management 33 | * fix(scroll) don't close autosuggest when clicking on scrollbar (#64) 34 | * destroy event listeners in beforeDestroy lifecycle 35 | * Fixes [#63](https://github.com/darrenjennings/vue-autosuggest/issues/63) 36 | * The biggest benefit for this release is not only the ability to click and drag the scroll bar, but the cleanup of event listeners, which would previously hang around even after the component would be destroyed. 37 | 38 | * [1.7.0](https://github.com/darrenjennings/vue-autosuggest/tree/v1.7.0) @suggestion and minify esm bundle 39 | * feat(events) add suggestion as event @suggestion 40 | * chore(eslint) add eslint vue/recommended configuration and have 41 | * autosuggest lib conform 42 | * chore(rollup) uglify esm module for smaller lib size 43 | * Migrating from 1.6-1.7: 44 | * The on-selected event is now @selected 45 | * Fixes [#58](https://github.com/darrenjennings/vue-autosuggest/issues/58), From #62 46 | 47 | * [1.6.0](https://github.com/darrenjennings/vue-autosuggest/tree/v1.6.0) - Attribute prop customization 48 | * Added component id and class customization props: 49 | * component-attr-id-autosuggest 50 | * component-attr-class-autosuggest-results-container 51 | * component-attr-class-autosuggest-results 52 | * [1.5.0](https://github.com/Educents/vue-autosuggest/releases/tag/v1.5.0) - Slots 53 | * Slots - header, footer, and suggestion items 54 | * [1.4.3](https://github.com/Educents/vue-autosuggest/releases/tag/1.4.3) - bugfix [46](https://github.com/Educents/vue-autosuggest/pull/46) 55 | * [1.4.1](https://github.com/Educents/vue-autosuggest/releases/tag/1.4.1) - Native events using transparent wrappers, bugfixes 56 | * [1.3.1](https://github.com/Educents/vue-autosuggest/releases/tag/v1.3.1) - various improvements 57 | * Blur and Focus events added to inputProps 58 | * passing in oldText for onInputChange(val, oldVal) watcher event 59 | * Ability to set input autocomplete value. Default = off 60 | * [1.1.2](https://github.com/Educents/vue-autosuggest/releases/tag/v1.1.2) - getSuggestionValue, renderSuggestion props, scrollTo behavior 61 | -------------------------------------------------------------------------------- /src/parts/DefaultSection.js: -------------------------------------------------------------------------------- 1 | const DefaultSection = { 2 | name: "default-section", 3 | props: { 4 | /** @type ResultSection */ 5 | section: { type: Object, required: true }, 6 | currentIndex: { type: [Number, String], required: false, default: Infinity }, 7 | renderSuggestion: { type: Function, required: false }, 8 | normalizeItemFunction: { type: Function, required: true }, 9 | componentAttrPrefix: { type: String, required: true }, 10 | componentAttrIdAutosuggest: { type: String, required: true } 11 | }, 12 | data: function () { 13 | return { 14 | /** @type Number */ 15 | _currentIndex: this.currentIndex 16 | } 17 | }, 18 | computed: { 19 | /** 20 | * Suggestions from the section 21 | */ 22 | list: function () { 23 | let { limit, data } = this.section; 24 | if (data.length < limit) { 25 | limit = data.length; 26 | } 27 | return data.slice(0, limit); 28 | } 29 | }, 30 | methods: { 31 | getItemIndex (i) { 32 | return this.section.start_index + i; 33 | }, 34 | getItemByIndex (i) { 35 | return this.section.data[i]; 36 | }, 37 | onMouseEnter (event) { 38 | const idx = parseInt(event.currentTarget.getAttribute("data-suggestion-index")) 39 | this._currentIndex = idx 40 | this.$emit('updateCurrentIndex', idx) 41 | }, 42 | onMouseLeave () { 43 | this.$emit('updateCurrentIndex', null) 44 | } 45 | }, 46 | // eslint-disable-next-line no-unused-vars 47 | render (h) { 48 | const componentAttrPrefix = this.componentAttrPrefix 49 | const slots = { 50 | beforeSection: this.$scopedSlots[`before-section-${this.section.name}`], 51 | afterSectionDefault: this.$scopedSlots[`after-section`], 52 | afterSectionNamed: this.$scopedSlots[`after-section-${this.section.name}`] 53 | } 54 | 55 | const beforeClassName = `${componentAttrPrefix}__results-before ${componentAttrPrefix}__results-before--${this.section.name}` 56 | const before = slots.beforeSection && slots.beforeSection({ 57 | section: this.section, 58 | className: beforeClassName 59 | }) || [] 60 | 61 | return h( 62 | "ul", 63 | { 64 | attrs: { role: "listbox", "aria-labelledby": this.section.label && `${this.componentAttrIdAutosuggest}-${this.section.label}` }, 65 | class: this.section.ulClass 66 | }, 67 | [ 68 | before[0] && before[0] || this.section.label &&
  • {this.section.label}
  • || '', 69 | this.list.map((val, key) => { 70 | const item = this.normalizeItemFunction(this.section.name, this.section.type, this.section.label, this.section.liClass, val) 71 | const itemIndex = this.getItemIndex(key) 72 | const isHighlighted = this._currentIndex === itemIndex || parseInt(this.currentIndex) === itemIndex 73 | 74 | return h( 75 | "li", 76 | { 77 | attrs: { 78 | role: "option", 79 | "data-suggestion-index": itemIndex, 80 | "data-section-name": item.name, 81 | id: `${componentAttrPrefix}__results-item--${itemIndex}`, 82 | ...item.liAttributes 83 | }, 84 | key: itemIndex, 85 | class: { 86 | [`${componentAttrPrefix}__results-item--highlighted`]: isHighlighted, 87 | [`${componentAttrPrefix}__results-item`]: true, 88 | ...item.liClass 89 | }, 90 | on: { 91 | mouseenter: this.onMouseEnter, 92 | mouseleave: this.onMouseLeave 93 | } 94 | }, 95 | [this.renderSuggestion ? this.renderSuggestion(item) 96 | : this.$scopedSlots.default && this.$scopedSlots.default({ 97 | _key: key, 98 | suggestion: item 99 | })] 100 | ); 101 | }), 102 | slots.afterSectionDefault && slots.afterSectionDefault({ 103 | section: this.section, 104 | className: `${componentAttrPrefix}__results-after ${componentAttrPrefix}__results-after--${this.section.name}` 105 | }), 106 | slots.afterSectionNamed && slots.afterSectionNamed({ 107 | section: this.section, 108 | className: `${componentAttrPrefix}__results_after ${componentAttrPrefix}__results-after--${this.section.name}` 109 | }) 110 | ] 111 | ); 112 | } 113 | }; 114 | 115 | export default DefaultSection; 116 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-autosuggest", 3 | "version": "2.2.0", 4 | "description": "Vue autosuggest component.", 5 | "engines": { 6 | "node": "> 4", 7 | "npm": "> 3" 8 | }, 9 | "scripts": { 10 | "commit": "git-cz", 11 | "start": "webpack-dev-server --config docs/webpack.config.js", 12 | "lint": "eslint --ext .js,.vue src", 13 | "build": "yarn build:docs && yarn build:esm && yarn build:umd && yarn storybook:build", 14 | "build:docs": "NODE_ENV=production webpack --config docs/webpack.config.js", 15 | "build:esm": "rollup -c ./build/rollup.esm.config.js", 16 | "build:umd": "rollup -c ./build/rollup.umd.config.js", 17 | "deploy": "yarn build && npm publish", 18 | "test": "yarn jest", 19 | "test-compat": "./__tests__/test-compat-all.sh", 20 | "report-coverage": "cat ./coverage/lcov.info | ./node_modules/.bin/codecov", 21 | "test:coverage": "yarn jest -- --coverage", 22 | "test:update": "yarn test --updateSnapshot", 23 | "add-contributor": "kcd-scripts contributors add", 24 | "setup": "yarn install", 25 | "precommit": "yarn test", 26 | "storybook": "start-storybook -p 9001 -c .storybook -s ./.storybook/public", 27 | "storybook:build": "build-storybook -c .storybook -o docs/storybook" 28 | }, 29 | "files": [ 30 | "dist", 31 | "src" 32 | ], 33 | "main": "dist/vue-autosuggest.js", 34 | "module": "dist/vue-autosuggest.esm.js", 35 | "keywords": [ 36 | "vue", 37 | "autosuggest", 38 | "autocomplete", 39 | "enhanced input", 40 | "typeahead", 41 | "dropdown", 42 | "select", 43 | "combobox", 44 | "accessibility", 45 | "WAI-ARIA", 46 | "multiselect", 47 | "multiple selection" 48 | ], 49 | "author": "Darren Jennings ", 50 | "license": "MIT", 51 | "eslintIgnore": [ 52 | "node_modules", 53 | "coverage", 54 | "dist", 55 | "docs" 56 | ], 57 | "repository": { 58 | "type": "git", 59 | "url": "https://github.com/darrenjennings/vue-autosuggest.git" 60 | }, 61 | "bugs": { 62 | "url": "https://github.com/darrenjennings/vue-autosuggest/issues" 63 | }, 64 | "homepage": "https://github.com/darrenjennings/vue-autosuggest#readme", 65 | "config": { 66 | "commitizen": { 67 | "path": "cz-conventional-changelog" 68 | } 69 | }, 70 | "peerDependencies": { 71 | "vue": ">= 2.5.0" 72 | }, 73 | "devDependencies": { 74 | "@rollup/plugin-buble": "^0.21.3", 75 | "@rollup/plugin-commonjs": "^21.0.2", 76 | "@rollup/plugin-json": "^4.1.0", 77 | "@rollup/plugin-node-resolve": "^13.1.3", 78 | "@rollup/plugin-replace": "^3.1.0", 79 | "@storybook/addon-actions": "^3.2.16", 80 | "@storybook/vue": "3.2.15", 81 | "@vue/test-utils": "1.0.0-beta.25", 82 | "babel-core": "6.26.0", 83 | "babel-eslint": "^8.0.2", 84 | "babel-jest": "22.4.0", 85 | "babel-loader": "7.1.2", 86 | "babel-plugin-syntax-jsx": "^6.18.0", 87 | "babel-plugin-transform-object-rest-spread": "6.26.0", 88 | "babel-plugin-transform-runtime": "6.23.0", 89 | "babel-plugin-transform-vue-jsx": "^3.5.0", 90 | "babel-preset-env": "1.6.0", 91 | "codecov": "3.0.0", 92 | "codecov.io": "0.1.6", 93 | "commitizen": "2.9.6", 94 | "css-loader": "0.28.7", 95 | "cz-conventional-changelog": "2.0.0", 96 | "diffable-html": "3.0.0", 97 | "eslint": "^5.6.0", 98 | "eslint-friendly-formatter": "^3.0.0", 99 | "eslint-loader": "^2.1.1", 100 | "eslint-plugin-vue": "^5.0.0-beta.3", 101 | "husky": "0.14.3", 102 | "jest": "23.6.0", 103 | "jest-serializer-vue": "^2.0.2", 104 | "kcd-scripts": "0.27.1", 105 | "prettier": "1.14.3", 106 | "rollup": "^2.68.0", 107 | "rollup-plugin-filesize": "^9.1.2", 108 | "rollup-plugin-uglify": "^6.0.4", 109 | "rollup-plugin-vue": "5", 110 | "sinon": "4.1.2", 111 | "vue": "2.5.18", 112 | "vue-jest": "2.6.0", 113 | "vue-loader": "14.2.2", 114 | "vue-server-renderer": "2.5.18", 115 | "vue-template-compiler": "2.5.18", 116 | "vue-template-es2015-compiler": "1.8.2", 117 | "webpack": "4.28.3", 118 | "webpack-cli": "^3.2.0", 119 | "webpack-dev-server": "3.1.14", 120 | "webpack-merge": "4.2.1" 121 | }, 122 | "jest": { 123 | "moduleFileExtensions": [ 124 | "js", 125 | "vue" 126 | ], 127 | "transform": { 128 | "^.+\\.js$": "/node_modules/babel-jest", 129 | ".*\\.(vue)$": "/node_modules/vue-jest" 130 | }, 131 | "snapshotSerializers": [ 132 | "jest-serializer-vue" 133 | ], 134 | "collectCoverageFrom": [ 135 | "src/**/*.{vue}", 136 | "!**/node_modules/**", 137 | "!**/.test.js" 138 | ], 139 | "testURL": "http://localhost" 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /docs/App.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 183 | 184 | 327 | -------------------------------------------------------------------------------- /src/stories/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import { action } from "@storybook/addon-actions"; 3 | import { storiesOf } from "@storybook/vue"; 4 | 5 | import Autosuggest from "../Autosuggest.vue"; 6 | 7 | const sharedData = { 8 | options: [ 9 | "Frodo", 10 | "Gandalf", 11 | "Samwise", 12 | "Aragorn", 13 | "Galadriel", 14 | "Sauron", 15 | "Gimli", 16 | "Legolas", 17 | "Saruman", 18 | "Elrond", 19 | "Gollum", 20 | "Bilbo" 21 | ], 22 | methods: { 23 | onInputChange(text) { 24 | action('onInputChange')(text) 25 | if (text === null) { 26 | return; 27 | } 28 | const filteredData = this.options[0].data.filter(item => { 29 | return item.toLowerCase().indexOf(text.toLowerCase()) > -1; 30 | }); 31 | this.filteredOptions = [{ data: filteredData }]; 32 | } 33 | } 34 | }; 35 | 36 | storiesOf("Vue-Autosuggest", module) 37 | .add("simplest", () => ({ 38 | components: { Autosuggest }, 39 | template: `
    40 |
    You have selected '{{selected}}'
    41 |
    42 | 43 |
    44 |
    `, 45 | data() { 46 | return { 47 | selected: "", 48 | filteredOptions: [], 49 | options: [{ data: sharedData.options.slice(0, 10) }], 50 | inputProps: { 51 | id: "autosuggest__input", 52 | placeholder: "Type 'e'" 53 | }, 54 | onSelected: item => { 55 | action("Selected")(item) 56 | this.selected = item; 57 | } 58 | }; 59 | }, 60 | methods: sharedData.methods 61 | })) 62 | .add("simple with multiple sections", () => ({ 63 | components: { Autosuggest }, 64 | template: `
    65 |
    You have selected {{selected}}
    66 |
    67 | 68 |
    69 |
    `, 70 | data() { 71 | return { 72 | selected: "", 73 | limit: 10, 74 | filteredOptions: [], 75 | options: [ 76 | { 77 | data: sharedData.options 78 | } 79 | ], 80 | inputProps: { 81 | id: "autosuggest__input", 82 | placeholder: "Type 'g'" 83 | } 84 | }; 85 | }, 86 | methods: { 87 | onSelected(item) { 88 | action('Selected')(item.item); 89 | }, 90 | onInputChange(text) { 91 | action('onInputChange')(text) 92 | if (text === null) { 93 | return; 94 | } 95 | const filtered = []; 96 | const suggestionsData = this.options[0].data.filter(item => { 97 | return item.toLowerCase().indexOf(text.toLowerCase()) > -1; 98 | }); 99 | 100 | suggestionsData.length > 0 && 101 | filtered.push( 102 | { 103 | label: "Section 1", 104 | data: suggestionsData 105 | }, 106 | { 107 | label: "Section 2", 108 | data: suggestionsData 109 | } 110 | ); 111 | this.filteredOptions = filtered; 112 | } 113 | } 114 | })) 115 | .add("selected", () => ({ 116 | components: { Autosuggest }, 117 | template: ` 118 |
    119 | 125 | 128 | 135 | 138 | 139 |
    140 | `, 141 | data() { 142 | return { 143 | selected: [], 144 | colors: [ 145 | '#1abc9c', 146 | '#2ecc71', 147 | '#3498db', 148 | '#9b59b6', 149 | '#34495e', 150 | '#f1c40f', 151 | '#e74c3c', 152 | '#7f8c8d', 153 | '#C4E538', 154 | '#0652DD', 155 | '#9980FA', 156 | '#EA2027', 157 | ], 158 | limit: 10, 159 | filteredOptions: [], 160 | options: [ 161 | { 162 | data: [...sharedData.options] 163 | } 164 | ], 165 | inputProps: { 166 | id: "autosuggest__input", 167 | onClick: this.onClick, 168 | placeholder: "Type 'g'" 169 | }, 170 | onSelected(i){ 171 | action('Selected')(i); 172 | } 173 | }; 174 | }, 175 | methods: { 176 | onInputChange: sharedData.methods.onInputChange 177 | } 178 | })) 179 | .add("with property: initial value", () => ({ 180 | components: { Autosuggest }, 181 | template: `
    182 |
    You have selected {{selected}}
    183 |
    184 | 185 |
    186 |
    `, 187 | data() { 188 | return { 189 | selected: "", 190 | limit: 10, 191 | query: 'Frodo', 192 | filteredOptions: [], 193 | options: [ 194 | { 195 | data: sharedData.options 196 | } 197 | ], 198 | sectionConfigs: { 199 | default: { 200 | limit: 6, 201 | onSelected: (item, originalInput) => { 202 | action('Selected')(item); 203 | } 204 | } 205 | }, 206 | inputProps: { 207 | id: "autosuggest__input", 208 | placeholder: "Type 'g'" 209 | } 210 | }; 211 | }, 212 | methods: sharedData.methods 213 | })) 214 | .add("render with advanced styling + images", () => ({ 215 | components: { Autosuggest }, 216 | template: `
    217 |
    You have selected '{{JSON.stringify(selected,null,2)}}'
    218 |
    219 | 226 |
    227 |
    `, 228 | data() { 229 | return { 230 | selected: "", 231 | filteredOptions: [], 232 | suggestions: [ 233 | { 234 | data: [ 235 | { id: 1, name: "Frodo", avatar: "https://upload.wikimedia.org/wikipedia/en/4/4e/Elijah_Wood_as_Frodo_Baggins.png" }, 236 | { id: 2, name: "Samwise", avatar: "https://pbs.twimg.com/media/Cdbyw8JUIAAXY8d.jpg" }, 237 | { id: 3, name: "Gandalf", avatar: "https://upload.wikimedia.org/wikipedia/en/e/e9/Gandalf600ppx.jpg" }, 238 | { id: 4, name: "Aragorn", avatar: "https://i.pinimg.com/236x/78/75/40/7875409365d056d145dfb4c32e413aad--viggo-mortensen-aragorn-lord-of-the-rings.jpg" } 239 | ] 240 | } 241 | ] 242 | }; 243 | }, 244 | methods: { 245 | onInputChange(item) { 246 | action('onInputChange')(item); 247 | if (item === null) { 248 | return; 249 | } 250 | const filteredData = this.suggestions[0].data.filter(option => { 251 | return option.name.toLowerCase().indexOf(item.toLowerCase()) > -1; 252 | }); 253 | this.filteredOptions = [{ data: filteredData }]; 254 | }, 255 | onSelected(item) { 256 | action("Selected")(item); 257 | this.selected = item; 258 | }, 259 | renderSuggestion(suggestion) { 260 | const item = suggestion.item; 261 | return ( 262 |
    268 | 277 | {item.name} 278 |
    279 | ); 280 | }, 281 | getSuggestionValue(suggestion) { 282 | return suggestion.item.name; 283 | } 284 | } 285 | })) 286 | .add("selecting from a list", () => ({ 287 | components: { Autosuggest }, 288 | template: `
    289 |
    290 | 291 |
    292 |
    293 |
    {{item}}
    294 |
    295 |
    296 |
    297 |
    298 |
    `, 299 | data() { 300 | return { 301 | selected: [], 302 | limit: 10, 303 | filteredOptions: [], 304 | options: [ 305 | { 306 | data: sharedData.options.slice(0, 5) 307 | } 308 | ], 309 | inputProps: { 310 | id: "autosuggest__input", 311 | placeholder: "Type 'g'" 312 | } 313 | }; 314 | }, 315 | methods: { 316 | onSelected(item) { 317 | action("Selected")(item); 318 | item && item.item && this.add(item.item); 319 | }, 320 | onClick() { 321 | this.onInputChange(""); 322 | }, 323 | onInputChange: sharedData.methods.onInputChange, 324 | add(item) { 325 | this.$refs.autosuggest.searchInput = ""; 326 | this.selected.push(item); 327 | this.options[0].data.splice(this.options[0].data.indexOf(item), 1); 328 | this.onInputChange(item); 329 | }, 330 | remove(item) { 331 | this.options[0].data.push(item); 332 | this.selected.splice(this.selected.indexOf(item), 1); 333 | this.onInputChange(item); 334 | } 335 | } 336 | })) 337 | .add("many many options", () => ({ 338 | components: { Autosuggest }, 339 | template: `
    340 |
    341 | 347 |
    348 |
    `, 349 | data() { 350 | return { 351 | selected: "", 352 | filteredOptions: [], 353 | suggestions: [], 354 | timeout: null 355 | }; 356 | }, 357 | created() { 358 | let options = []; 359 | for (let i = 0; i < 1000; ++i) { 360 | options.push(String(i)); 361 | } 362 | this.suggestions = [{ data: options }]; 363 | }, 364 | methods: { 365 | onInputChange(input) { 366 | action("onInputChange")(input); 367 | if (input === null) { 368 | return; 369 | } 370 | clearTimeout(this.timeout); 371 | this.timeout = setTimeout(() => { 372 | const filteredData = this.suggestions[0].data.filter(option => { 373 | return option.indexOf(input) > -1; 374 | }); 375 | this.filteredOptions = [{ data: filteredData }]; 376 | }, 100); 377 | }, 378 | onSelected(item) { 379 | action("Selected")(item); 380 | this.selected = item; 381 | }, 382 | renderSuggestion(suggestion) { 383 | return ( 384 |
    390 | {suggestion.item} 391 |
    392 | ); 393 | } 394 | } 395 | })) 396 | .add("Slots - Header, Footer, Suggestion", () => ({ 397 | components: { Autosuggest }, 398 | template: ` 399 |
    400 | 405 | 408 | 415 | 418 | 419 |
    420 | `, 421 | data() { 422 | return { 423 | selected: [], 424 | colors: [ 425 | '#1abc9c', 426 | '#2ecc71', 427 | '#3498db', 428 | '#9b59b6', 429 | '#34495e', 430 | '#f1c40f', 431 | '#e74c3c', 432 | '#7f8c8d', 433 | '#C4E538', 434 | '#0652DD', 435 | '#9980FA', 436 | '#EA2027', 437 | ], 438 | limit: 10, 439 | filteredOptions: [], 440 | options: [ 441 | { 442 | data: [...sharedData.options] 443 | } 444 | ], 445 | inputProps: { 446 | id: "autosuggest__input", 447 | onClick: this.onClick, 448 | placeholder: "Type 'g'" 449 | } 450 | }; 451 | }, 452 | methods: { 453 | onInputChange: sharedData.methods.onInputChange 454 | } 455 | })) 456 | .add("multiple instances", () => ({ 457 | components: { Autosuggest }, 458 | template: ` 459 |
    460 |
    461 |

    Tab throw each component and use arrow keys to test isolation of functionality.

    462 |
    463 |
    464 | 469 | 470 | 475 | 476 | 481 | 482 |
    483 |
    484 | `, 485 | data() { 486 | return { 487 | selected: [], 488 | limit: 10, 489 | filteredOptions: { 490 | 0: [], 491 | 1: [], 492 | 2: [] 493 | }, 494 | options: [ 495 | { 496 | data: [...sharedData.options] 497 | } 498 | ], 499 | inputProps: { 500 | id: "autosuggest__input", 501 | onClick: this.onClick, 502 | placeholder: "Type 'g'" 503 | } 504 | }; 505 | }, 506 | methods: { 507 | onInputChange(text, index) { 508 | action('onInputChange')(text) 509 | if (text === null) { 510 | return; 511 | } 512 | const filteredData = this.options[0].data.filter(item => { 513 | return item.toLowerCase().indexOf(text.toLowerCase()) > -1; 514 | }); 515 | 516 | this.filteredOptions[index] = [{ data: filteredData }]; 517 | } 518 | } 519 | })) 520 | .add("with section before/after slots", () => ({ 521 | components: { Autosuggest }, 522 | template: `
    523 |
    You have selected {{selected}}
    524 |
    525 | 531 | 534 | 539 | 540 |
    541 |
    `, 542 | data() { 543 | return { 544 | selected: "", 545 | limit: 10, 546 | query: '', 547 | options: [ 548 | { 549 | data: sharedData.options 550 | } 551 | ], 552 | inputProps: { 553 | id: "autosuggest__input", 554 | placeholder: "Type 'g'" 555 | } 556 | }; 557 | }, 558 | computed: { 559 | filteredOptions() { 560 | const suggestionsData = this.options[0].data.filter(item => { 561 | return item.toLowerCase().indexOf(this.query.toLowerCase()) > -1; 562 | }); 563 | 564 | return [ 565 | { 566 | label: "Section 1", 567 | data: suggestionsData, 568 | limit: 2 569 | }, 570 | { 571 | label: "Section 2", 572 | data: suggestionsData.map(a => `${a} ${this.rando()}` ) 573 | .concat(suggestionsData.map(a => `${a} ${this.rando()}`)) 574 | .concat(suggestionsData.map(a => `${a} ${this.rando()}`)) 575 | } 576 | ] 577 | } 578 | }, 579 | methods: { 580 | rando(){ 581 | return Math.floor(Math.random() * (100 - 1)) + 1 582 | }, 583 | onSelected(item) { 584 | action('Selected')(item.item) 585 | 586 | this.query = item.item 587 | }, 588 | onInputChange(text) { 589 | action('onInputChange')(text) 590 | if (text === null) { 591 | return; 592 | } 593 | this.query = text 594 | } 595 | } 596 | })); 597 | -------------------------------------------------------------------------------- /src/Autosuggest.vue: -------------------------------------------------------------------------------- 1 | 87 | 88 | 683 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
    2 |

    Vue logo

    3 | 4 |

    vue-autosuggest

    5 | 6 |

    🔍 Autosuggest component built for Vue.

    7 |
    8 | 9 |
    10 | 11 | [![Build Status][build-badge]][build] 12 | [![Code Coverage][coverage-badge]][coverage] 13 | [![version][version-badge]][package] 14 | [![downloads][downloads-badge]][npmtrends] 15 | [![MIT License][license-badge]][LICENSE] 16 | [![gzip size][size-badge]](https://unpkg.com/vue-autosuggest@latest) 17 | 18 | [![All Contributors](https://img.shields.io/badge/all_contributors-7-orange.svg?style=flat-square)](#contributors) 19 | [![PRs Welcome][prs-badge]][prs] 20 | [![Code of Conduct][coc-badge]][coc] 21 | 22 | [![Watch on GitHub][github-watch-badge]][github-watch] 23 | [![Star on GitHub][github-star-badge]][github-star] 24 | [![Tweet][twitter-badge]][twitter] 25 | 26 | ## Table of Contents 27 | 28 | * [Examples](#examples) 29 | * [Features](#features) 30 | * [Installation](#installation) 31 | * [Usage](#usage) 32 | * [Props](#props) 33 | * [Inspiration](#inspiration) 34 | * [Contributors](#contributors) 35 | * [LICENSE](#license) 36 | 37 | ## Examples 38 | 39 | * Demo 40 | * Storybook Helpful to see all 41 | variations of component's props.
    42 | * JSFiddle Helpful for playing around 43 | and sharing. 44 | * Codesandbox Demos: 45 | 46 | * [Deeply nested data objects as suggestions](https://codesandbox.io/s/vueautosuggest-api-data-objects-0nudq) 47 | * [Api Fetching suggestions with Multiple sections](https://codesandbox.io/s/vueautosuggest-api-fetching-57d4e) 48 | * [Form Validation with VeeValidate](https://codesandbox.io/s/vueautosuggest-vee-validate-ve13m) 49 | * [Multiple VueAutosuggest instances on same page](https://codesandbox.io/s/vueautosuggest-multiple-vue-autosuggests-545ee) 50 | * [Integration with Algolia](https://www.algolia.com/doc/guides/building-search-ui/resources/ui-and-ux-patterns/in-depth/autocomplete/vue/?language=vue#results-page-with-autocomplete) thanks to [@haroenv](https://github.com/haroenv)! 51 | 52 | ## Features 53 | 54 | * WAI-ARIA complete autosuggest component built with the power of Vue. 55 | * Full control over rendering with built in defaults or custom components for rendering. 56 | * Easily integrate AJAX data fetching for list presentation. 57 | * Supports multiple sections. 58 | * No opinions on CSS, full control over styling. 59 | * Rigorously tested. 60 | 61 | ## Installation 62 | 63 | This module is distributed via [npm][npm] which is bundled with [node][node] and 64 | should be installed as one of your project's `dependencies`: 65 | 66 | ``` 67 | npm install vue-autosuggest 68 | ``` 69 | 70 | or 71 | 72 | ``` 73 | yarn add vue-autosuggest 74 | ``` 75 | 76 | ## Usage 77 | 78 | Load VueAutosuggest into your vue app globally. 79 | 80 | ```js 81 | import VueAutosuggest from "vue-autosuggest"; 82 | Vue.use(VueAutosuggest); 83 | ``` 84 | 85 | or locally inside a component: 86 | 87 | ```js 88 | import { VueAutosuggest } from 'vue-autosuggest'; 89 | export default { 90 | ... 91 | components: { 92 | VueAutosuggest 93 | } 94 | ... 95 | }; 96 | ``` 97 | 98 | Place the component into your app! 99 | 100 | ```html 101 | 108 | 111 | 112 | 113 | ``` 114 | 115 | Advanced usage: 116 | 117 |
    Click to expand

    118 | 119 | ```html 120 | 143 | 144 | 201 | 202 | 241 | ``` 242 | 243 |

    244 | 245 | For more advanced usage, check out the examples below, and explore the 246 | properties you can use. 247 | 248 | ## [Slots](#slots) 249 | 250 | ### header/footer 251 | Slots for injecting content around the results/input. Useful for header/footer like slots or empty state. 252 | 253 | ```html 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | ``` 264 | 265 | ### Adding labels 266 | 267 | It is common in forms to add a label next to the `` tag for semantic html / accessibility. You can use the 268 | `before-input` slot to accomplish this in conjunction with the `inputProps.id`: 269 | 270 | ```html 271 | 272 | 275 | ... 276 | 277 | ``` 278 | 279 | ### suggestion item (i.e. default slot) 280 | Used to style each suggestion inside the `
  • ` tag. Using [scoped slots](https://vuejs.org/v2/guide/components-slots.html#Scoped-Slots) 281 | you have access to the `suggestion` item inside the `v-for` suggestions loop. This gives you the power of Vue templating, since 282 | vue-autosuggest does not have an opinion about how you render the items in your list. 283 | 284 | ```vue 285 | 286 | 294 | 295 | ``` 296 | 297 | > This slot will be overridden when the [`render-suggestion`](#renderSuggestion) prop is used. 298 | 299 | 300 | 301 | ## [Props](#props) 302 | 303 | | Prop | Type | Required | Description | 304 | | :------------------------------------------ | :------- | :------: | :-------------------------------------------------------- | 305 | | [`suggestions`](#suggestionsProp) | Array | ✓ | Array of sections, each containing suggestions to be rendered. e.g.`suggestions: [{data: ['harry','ron','hermione']}]` | 306 | | [`input-props`](#inputPropsTable) | Object | ✓ | Add props to the ``. | 307 | | [`section-configs`](#sectionConfigsProp) | Object | | Define multiple sections ``. | 308 | | [`render-suggestion`](#renderSuggestion) | Function | | Tell vue-autosuggest how to render inside the `
  • ` tag. Overrides what is inside the default suggestion template slot. | 309 | | [`get-suggestion-value`](#getSuggestionValue) | Function | | Tells vue-autosuggest what to put in the `` value | 310 | | [`should-render-suggestions`](#shouldRenderSuggestions) | Function | | Tell vue-autosuggest if it should render the suggestions results popover | 311 | | `component-attr-id-autosuggest` | String | | `id` of entire component | 312 | | `component-attr-class-autosuggest-results-container` | String | | `class` of container of results container | 313 | | `component-attr-class-autosuggest-results` | String | | `class` of results container | 314 | | `component-attr-prefix` | String | | prefix to be used for results item classes/ids. default: `autosuggest` | 315 | 316 | 317 | 318 | ### inputProps 319 | 320 | | Prop | Type | Required | Description | 321 | | :----------------------- | :------------------ | :--------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 322 | | [`id`](#inputPropsTable) | String | ✓ | id attribute on ``. | 323 | | Any DOM Props | \* | | You can add any props to `` as the component will `v-bind` inputProps. Similar to rest spread in JSX. See more details here: https://vuejs.org/v2/api/#v-bind. The `name` attribute is set to "`q`" by default. | 324 | 325 | 326 | 327 | ### sectionConfigs 328 | 329 | Multiple sections can be defined in the `sectionConfigs` prop which defines the control behavior for 330 | each section. 331 | 332 | | Prop | Type | Required | Description | 333 | | :----------- | :------- | :------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 334 | | `on-selected` | Function | ✓ | Determine behavior for what should happen when a suggestion is selected. e.g. Submit a form, open a link, update a vue model, tweet at Ken Wheeler etc. | 335 | | `limit` | Number | | Limit each section by some value. Default: `Infinity` | 336 | 337 | Below we have defined a `default` section and a `blog` section. The `blog` section has a component 338 | `type` of `url-section` which corresponds to which component the Autosuggest loads. When type is not 339 | defined, Vue-autosuggest will use a built in `DefaultSection.vue` component. 340 | 341 | ```js 342 | sectionConfigs: { 343 | 'default': { 344 | limit: 6, 345 | onSelected: function(item, originalInput) { 346 | console.log(item, originalInput, `Selected "${item.item}"`); 347 | } 348 | }, 349 | 'blog': { 350 | limit: 3, 351 | type: "url-section", 352 | onSelected: function() { 353 | console.log("url: " + item.item.url); 354 | } 355 | } 356 | } 357 | ``` 358 | 359 | 360 | 361 | ### renderSuggestion 362 | 363 | This function can be used to tell vue-autosuggest how to render the html inside the `
  • ` tag when you do not want to use the 364 | default template slot for suggestions but would rather have the power of javascript / jsx. 365 | 366 | In its most basic form it just returns an object property: 367 | 368 | ```js 369 | renderSuggestion(suggestion) { 370 | return suggestion.name; 371 | }, 372 | ``` 373 | 374 | But usually it returns a JSX fragment, which is transformed into a virtual node description with babel-plugin-transform-vue-jsx: 375 | 376 | ```jsx 377 | renderSuggestion(suggestion) { 378 | return
    {suggestion.name}
    ; 379 | }, 380 | ``` 381 | 382 | If you're not using babel-plugin-transform-vue-jsx, you can create the virtual node description yourself: 383 | 384 | ```js 385 | renderSuggestion(suggestion) { 386 | return this.$createElement('div', { 'style': { color: 'red'} }, suggestion.name); 387 | }, 388 | ``` 389 | 390 | 391 | 392 | ### getSuggestionValue 393 | 394 | This function will tell vue-autosuggest what to put in the `` as the value. 395 | 396 | ```js 397 | getSuggestionValue(suggestion) { 398 | return suggestion.item.name; 399 | }, 400 | ``` 401 | 402 | 403 | 404 | ### shouldRenderSuggestions 405 | 406 | This function will tell vue-autosuggest if it should display the suggestions popover 407 | 408 | ```js 409 | /** 410 | * @param {Array} size - total results displayed 411 | * @param {Boolean} loading - value that indicates if vue-autosuggest _thinks_ that the 412 | * the popover should be open (e.g. if user hit escape, or 413 | * user clicked away) 414 | * @returns {Boolean} 415 | */ 416 | shouldRenderSuggestions (size, loading) { 417 | // This is the default behavior 418 | return size >= 0 && !loading 419 | } 420 | ``` 421 | 422 | ## [Events](#events) 423 | 424 | Below are the list of supported events. `@` is short-hand for 425 | [v-on](https://vuejs.org/v2/guide/events.html#Listening-to-Events). 426 | 427 | | Prop | Returns | Description | 428 | | :-------------------------------- | :-------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 429 | | `@selected` | suggestionItem, index | suggestion select handler. equivalent to sectionConfigs `on-selected` but for all items | 430 | | `@input`, `@focus`, `@blur`, etc. | \* | there is a transparent wrapper on the underlying `` so vue-autosuggest will use any DOM event you pass it for listening. This is implemented using `v-on:`. | 431 | | `@opened`, `@closed` | \* | suggestions visibility handler, indicates when the suggestions are opened and closed. This is called alongside [shouldRenderSuggestions](#shouldRenderSuggestions). | 432 | | `@item-changed` | suggestionItem, index | when keying through the results, this event signals which item is highlighted before being selected. | 433 | 434 | ## Browser support 435 | 436 | For IE11 and below, some functionality may not work. For example, you will have to manually [polyfill](https://github.com/Financial-Times/polyfill-service/issues/177) `Node.prototype.contains` 437 | 438 | ## Inspiration 439 | 440 | * Misha Moroshko's react-autosuggest component inspired the api + WAI-ARIA completeness 441 | https://github.com/moroshko/react-autosuggest 442 | 443 | ## Contributors 444 | 445 | Thanks goes to these people ([emoji key][emojis]): 446 | 447 | 448 | | [
    Darren Jennings](https://darrenjennings.github.io)
    [💻](https://github.com/darrenjennings/vue-autosuggest/commits?author=darrenjennings "Code") [📖](https://github.com/darrenjennings/vue-autosuggest/commits?author=darrenjennings "Documentation") [🚇](#infra-darrenjennings "Infrastructure (Hosting, Build-Tools, etc)") [⚠️](https://github.com/darrenjennings/vue-autosuggest/commits?author=darrenjennings "Tests") [🎨](#design-darrenjennings "Design") [💡](#example-darrenjennings "Examples") | [
    Evgeniy Kulish](https://github.com/ekulish)
    [💻](https://github.com/darrenjennings/vue-autosuggest/commits?author=ekulish "Code") [🎨](#design-ekulish "Design") [💡](#example-ekulish "Examples") [⚠️](https://github.com/darrenjennings/vue-autosuggest/commits?author=ekulish "Tests") | [
    Scott Smith](https://github.com/scottadamsmith)
    [🐛](https://github.com/darrenjennings/vue-autosuggest/issues?q=author%3Ascottadamsmith "Bug reports") [💻](https://github.com/darrenjennings/vue-autosuggest/commits?author=scottadamsmith "Code") [⚠️](https://github.com/darrenjennings/vue-autosuggest/commits?author=scottadamsmith "Tests") | [
    Fernando Machuca](https://github.com/chuca)
    [🎨](#design-chuca "Design") | [
    BerniML](https://github.com/BerniML)
    [💻](https://github.com/darrenjennings/vue-autosuggest/commits?author=BerniML "Code") [⚠️](https://github.com/darrenjennings/vue-autosuggest/commits?author=BerniML "Tests") | [
    Kristoffer Nordström](https://github.com/42tte)
    [💻](https://github.com/darrenjennings/vue-autosuggest/commits?author=42tte "Code") [⚠️](https://github.com/darrenjennings/vue-autosuggest/commits?author=42tte "Tests") | [
    Dan Wilson](https://www.danjwilson.co.uk)
    [💻](https://github.com/darrenjennings/vue-autosuggest/commits?author=djw "Code") | 449 | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | 450 | 451 | 452 | Thanks to [@chuca](https://github.com/chuca) for the logo design. 453 | 454 | This project follows the [all-contributors][all-contributors] specification. Contributions of any 455 | kind welcome! 456 | 457 | ## LICENSE 458 | 459 | MIT 460 | 461 | [npm]: https://www.npmjs.com/ 462 | [node]: https://nodejs.org 463 | [build-badge]: https://img.shields.io/travis/darrenjennings/vue-autosuggest.svg?style=flat-square 464 | [build]: https://travis-ci.org/darrenjennings/vue-autosuggest 465 | [size-badge]: https://img.badgesize.io/https://unpkg.com/vue-autosuggest@latest/dist/vue-autosuggest.esm.js?compression=gzip&style=flat-square 466 | [coverage-badge]: https://img.shields.io/codecov/c/github/darrenjennings/vue-autosuggest.svg?style=flat-square 467 | [coverage]: https://codecov.io/github/darrenjennings/vue-autosuggest 468 | [version-badge]: https://img.shields.io/npm/v/vue-autosuggest.svg?style=flat-square 469 | [package]: https://www.npmjs.com/package/vue-autosuggest 470 | [downloads-badge]: https://img.shields.io/npm/dm/vue-autosuggest.svg?style=flat-square 471 | [npmtrends]: http://www.npmtrends.com/vue-autosuggest 472 | [license-badge]: https://img.shields.io/npm/l/vue-autosuggest.svg?style=flat-square 473 | [license]: https://github.com/darrenjennings/vue-autosuggest/blob/master/LICENSE 474 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square 475 | [prs]: http://makeapullrequest.com 476 | [coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square 477 | [coc]: https://github.com/darrenjennings/vue-autosuggest/blob/master/other/CODE_OF_CONDUCT.md 478 | [github-watch-badge]: https://img.shields.io/github/watchers/darrenjennings/vue-autosuggest.svg?style=social 479 | [github-watch]: https://github.com/darrenjennings/vue-autosuggest/watchers 480 | [github-star-badge]: https://img.shields.io/github/stars/darrenjennings/vue-autosuggest.svg?style=social 481 | [github-star]: https://github.com/darrenjennings/vue-autosuggest/stargazers 482 | [twitter]: https://twitter.com/intent/tweet?text=Check%20out%20vue-autosuggest%20by%20%40darrenjennings%20https%3A%2F%2Fgithub.com%2Fdarrenjennings%2Fvue-autosuggest%20%F0%9F%91%8D 483 | [twitter-badge]: https://img.shields.io/twitter/url/https/github.com/darrenjennings/vue-autosuggest.svg?style=social 484 | [emojis]: https://github.com/kentcdodds/all-contributors#emoji-key 485 | [all-contributors]: https://github.com/kentcdodds/all-contributors 486 | 487 | 488 | -------------------------------------------------------------------------------- /__tests__/autosuggest.test.js: -------------------------------------------------------------------------------- 1 | import { mount, shallowMount } from "@vue/test-utils"; 2 | import { createRenderer } from "vue-server-renderer"; 3 | 4 | import Autosuggest from "../src/Autosuggest.vue"; 5 | 6 | Element.prototype.scrollTo = () => {}; // https://github.com/vuejs/vue-test-utils/issues/319 7 | 8 | // Helper to call function x number of times 9 | const times = x => f => { 10 | if (x > 0) { 11 | f(); 12 | times(x - 1)(f); 13 | } 14 | }; 15 | 16 | describe("Autosuggest", () => { 17 | const id = `autosuggest__input`; 18 | const filteredOptions = [ 19 | { 20 | data: [ 21 | "clifford kits", 22 | "friendly chemistry", 23 | "phonics", 24 | "life of fred", 25 | "life of fred math", 26 | "magic school bus", 27 | "math mammoth light blue", 28 | "handwriting", 29 | "math", 30 | "minecraft", 31 | "free worksheets", 32 | "4th grade", 33 | "snap circuits", 34 | "bath toys", 35 | "channies", 36 | "fred", 37 | "lego", 38 | "math life of fred", 39 | "multiplication", 40 | "thinking tree" 41 | ] 42 | } 43 | ]; 44 | 45 | const defaultProps = { 46 | suggestions: filteredOptions, 47 | inputProps: { 48 | id, 49 | placeholder: "Type 'G'" 50 | }, 51 | sectionConfigs: { 52 | default: { 53 | limit: 5, 54 | onSelected: () => {} 55 | } 56 | } 57 | }; 58 | 59 | const defaultListeners = { 60 | click: () => {} 61 | }; 62 | 63 | it("can mount", () => { 64 | const props = Object.assign({}, defaultProps); 65 | props.inputProps = Object.assign({}, defaultProps.inputProps); 66 | 67 | props.suggestions = [filteredOptions[0]]; 68 | 69 | const wrapper = shallowMount(Autosuggest, { 70 | propsData: props 71 | }); 72 | 73 | const input = wrapper.find('input[type="text"]') 74 | input.setValue('q') 75 | 76 | const renderer = createRenderer(); 77 | renderer.renderToString(wrapper.vm, (err, str) => { 78 | if (err) throw new Error(err); 79 | expect(str).toMatchSnapshot(); 80 | }); 81 | }); 82 | 83 | it("can render suggestions", () => { 84 | const props = Object.assign({}, defaultProps); 85 | props.inputProps = Object.assign({}, defaultProps.inputProps); 86 | 87 | const wrapper = mount(Autosuggest, { 88 | propsData: props, 89 | attachToDocument: true 90 | }); 91 | 92 | const input = wrapper.find("input"); 93 | expect(input.attributes("id", defaultProps.inputProps.id)).toBeTruthy(); 94 | 95 | input.trigger("click"); 96 | input.setValue("G"); 97 | input.trigger("keydown.down"); 98 | 99 | expect(wrapper.findAll(`ul li`).length).toBeLessThanOrEqual( 100 | defaultProps.sectionConfigs.default.limit 101 | ); 102 | 103 | const renderer = createRenderer(); 104 | renderer.renderToString(wrapper.vm, (err, str) => { 105 | if (err) throw new Error(err); 106 | expect(str).toMatchSnapshot(); 107 | }); 108 | }); 109 | 110 | it("can use escape key to exit", async () => { 111 | const wrapper = mount(Autosuggest, { 112 | propsData: defaultProps, 113 | listeners: defaultListeners 114 | }); 115 | 116 | const input = wrapper.find("input"); 117 | input.trigger("click"); 118 | input.setValue("G"); 119 | input.trigger("keydown.up"); // Check it doesn't offset the selection by going up first when nothing is selected. 120 | 121 | // TODO: test these keys are actually returning early. 122 | input.trigger("keydown", { 123 | keyCode: 16 // Shift 124 | }); 125 | input.trigger("keydown", { 126 | keyCode: 9 // Tab 127 | }); 128 | input.trigger("keydown", { 129 | keyCode: 18 // alt/option 130 | }); 131 | input.trigger("keydown", { 132 | keyCode: 91 // OS Key 133 | }); 134 | input.trigger("keydown", { 135 | keyCode: 93 // Right OS Key 136 | }); 137 | 138 | input.trigger("keydown.down"); 139 | 140 | expect(wrapper.findAll(`ul li`).length).toBeLessThanOrEqual( 141 | defaultProps.sectionConfigs.default.limit 142 | ); 143 | 144 | input.trigger("keydown.esc"); 145 | expect(wrapper.findAll(`ul li`).length).toEqual(0); 146 | 147 | const renderer = createRenderer(); 148 | renderer.renderToString(wrapper.vm, (err, str) => { 149 | if (err) { 150 | return false; 151 | } 152 | expect(str).toMatchSnapshot(); 153 | }); 154 | }); 155 | 156 | it("can select from suggestions using keystroke", async () => { 157 | const wrapper = mount(Autosuggest, { 158 | propsData: defaultProps, 159 | attachToDocument: true 160 | }); 161 | 162 | const input = wrapper.find("input"); 163 | input.trigger("click"); 164 | input.setValue("G"); 165 | 166 | times(5)(() => { 167 | input.trigger("keydown.down"); 168 | }); 169 | 170 | times(5)(() => { 171 | input.trigger("keydown.up"); 172 | }); 173 | 174 | input.trigger("keydown.enter"); 175 | 176 | await wrapper.vm.$nextTick(() => {}); 177 | 178 | const renderer = createRenderer(); 179 | renderer.renderToString(wrapper.vm, (err, str) => { 180 | if (err) { 181 | return false; 182 | } 183 | expect(str).toMatchSnapshot(); 184 | }); 185 | }); 186 | 187 | it("can interact with results of specific instance when multiple instances exist", async () => { 188 | const multipleAutosuggest = { 189 | components: { 190 | Autosuggest 191 | }, 192 | data () { 193 | return { 194 | autosuggestProps: defaultProps, 195 | automatischsuchen: true 196 | } 197 | }, 198 | render(h) { 199 | return h( 200 | "div", 201 | [ 202 | h( 203 | Autosuggest, 204 | { 205 | props: this.autosuggestProps 206 | } 207 | ), 208 | h( 209 | Autosuggest, 210 | { 211 | props: this.autosuggestProps 212 | } 213 | ) 214 | ] 215 | ); 216 | } 217 | } 218 | const wrapper = mount(multipleAutosuggest, { 219 | attachToDocument: true 220 | }); 221 | 222 | const autosuggestInstances = wrapper.findAll(Autosuggest); 223 | 224 | const autosuggest1 = autosuggestInstances.at(0); 225 | const autosuggest2 = autosuggestInstances.at(1); 226 | const input1 = autosuggest1.find("input"); 227 | const input2 = autosuggest2.find("input"); 228 | 229 | input1.trigger("click"); 230 | input2.trigger("click"); 231 | 232 | expect(autosuggest1.findAll("li.autosuggest__results-item").length).toBe(5); 233 | expect(autosuggest1.findAll("li.autosuggest__results-item").length).toBe(5); 234 | 235 | times(2)(() => { 236 | input2.trigger("keydown.down"); 237 | }); 238 | 239 | expect(autosuggest1.findAll("li.autosuggest__results-item--highlighted").length).toBe(0); 240 | expect(autosuggest2.findAll("li.autosuggest__results-item--highlighted").length).toBe(1); 241 | expect(autosuggest2.findAll("li").at(1).classes()).toContain("autosuggest__results-item--highlighted"); 242 | 243 | input2.trigger("keydown.enter"); 244 | 245 | expect(input1.element.value).toBe(""); 246 | expect(input2.element.value).toBe("friendly chemistry"); 247 | }); 248 | 249 | it("can click outside document to trigger close", async () => { 250 | const props = Object.assign({}, defaultProps); 251 | 252 | const wrapper = mount(Autosuggest, { 253 | propsData: props, 254 | listeners: defaultListeners, 255 | attachToDocument: true 256 | }); 257 | 258 | const input = wrapper.find("input"); 259 | input.setValue("G"); 260 | 261 | input.trigger("click"); 262 | input.setValue("G"); 263 | window.document.dispatchEvent(new Event("mousedown")); 264 | window.document.dispatchEvent(new Event("mouseup")); 265 | 266 | await wrapper.vm.$nextTick(() => {}); 267 | 268 | const renderer = createRenderer(); 269 | renderer.renderToString(wrapper.vm, (err, str) => { 270 | if (err) { 271 | return false; 272 | } 273 | expect(str).toMatchSnapshot(); 274 | }); 275 | }); 276 | 277 | it("can display section header", async () => { 278 | const props = Object.assign({}, defaultProps); 279 | props.sectionConfigs = { 280 | default: { 281 | label: "Suggestions", 282 | limit: 5, 283 | onSelected: () => {} 284 | } 285 | }; 286 | const wrapper = mount(Autosuggest, { 287 | propsData: props, 288 | listeners: defaultListeners, 289 | attachToDocument: true 290 | }); 291 | 292 | const input = wrapper.find("input"); 293 | input.setValue("G"); 294 | 295 | input.trigger("click"); 296 | input.setValue("G"); 297 | expect(wrapper.find("ul li:nth-child(1)").element.innerHTML).toBe( 298 | props.sectionConfigs.default.label 299 | ); 300 | const renderer = createRenderer(); 301 | renderer.renderToString(wrapper.vm, (err, str) => { 302 | if (err) { 303 | return false; 304 | } 305 | expect(str).toMatchSnapshot(); 306 | }); 307 | }); 308 | 309 | it("is aria complete", async () => { 310 | const propsData = { 311 | ...defaultProps, 312 | sectionConfigs: { 313 | default: { 314 | label: "Suggestions", 315 | limit: 5, 316 | onSelected: () => {} 317 | } 318 | } 319 | } 320 | const wrapper = mount(Autosuggest, { propsData }); 321 | 322 | const combobox = wrapper.find("[role='combobox']"); 323 | expect(combobox.exists()).toBeTruthy(); 324 | expect(combobox.attributes()["aria-haspopup"]).toBe("listbox"); 325 | expect(combobox.attributes()["aria-owns"]).toBe("autosuggest-autosuggest__results"); 326 | 327 | const input = combobox.find("input"); 328 | expect(input.attributes()["aria-autocomplete"]).toBe("list"); 329 | expect(input.attributes()["aria-activedescendant"]).toBe(""); 330 | expect(input.attributes()["aria-controls"]).toBe("autosuggest-autosuggest__results"); 331 | 332 | // aria owns needs to be an "id", #191 333 | let results = wrapper.find(`#${combobox.attributes()["aria-owns"]}`) 334 | expect(results.exists()).toBeTruthy() 335 | results = wrapper.find(`#${input.attributes()["aria-controls"]}`) 336 | expect(results.exists()).toBeTruthy() 337 | 338 | // TODO: Make sure aria-completeness is actually 2legit2quit. 339 | 340 | input.trigger("click"); 341 | input.setValue("G"); 342 | 343 | expect(combobox.attributes()["aria-expanded"]).toBe("true"); 344 | 345 | // make sure aria-labeledby references the section config label, and that it's an "id" 346 | const ul = wrapper.find('ul') 347 | expect(ul.attributes()['aria-labelledby']).toBe('autosuggest-Suggestions') 348 | expect(ul.find(`#${ul.attributes()['aria-labelledby']}`).exists).toBeTruthy() 349 | 350 | const mouseDownTimes = 3; 351 | times(mouseDownTimes)(() => { 352 | input.trigger("keydown.down"); 353 | }); 354 | 355 | const activeDescendentString = input.attributes()["aria-activedescendant"]; 356 | expect(parseInt(activeDescendentString[activeDescendentString.length - 1])).toBe( 357 | mouseDownTimes - 1 358 | ); 359 | expect(input.element.value).toBe(filteredOptions[0].data[mouseDownTimes - 1]); 360 | 361 | const renderer = createRenderer(); 362 | renderer.renderToString(wrapper.vm, (err, str) => { 363 | if (err) { 364 | return false; 365 | } 366 | expect(str).toMatchSnapshot(); 367 | }); 368 | }); 369 | 370 | it("can render simplest component with single onSelected", async () => { 371 | const props = Object.assign({}, defaultProps); 372 | props.inputProps = Object.assign({}, defaultProps.inputProps); 373 | props.inputProps.class = "cool-class"; 374 | props.suggestions = filteredOptions; 375 | 376 | delete props.suggestions[0].name; // ensure empty component name is OK 377 | delete props.sectionConfigs; // ensure empty sectionConfigs is OK 378 | delete props.inputProps.onClick; // ensure empty onClick is OK 379 | 380 | props.onSelected = () => {}; 381 | 382 | const wrapper = mount(Autosuggest, { 383 | propsData: props, 384 | attachToDocument: true 385 | }); 386 | 387 | const input = wrapper.find("input"); 388 | input.trigger("click"); 389 | input.setValue("G"); 390 | 391 | times(3)(() => { 392 | input.trigger("keydown.down"); 393 | }); 394 | 395 | wrapper.find("li").trigger("mouseover"); 396 | wrapper.find("li").trigger("mouseenter"); 397 | wrapper.find("li").trigger("mouseleave"); 398 | 399 | const renderer = createRenderer(); 400 | renderer.renderToString(wrapper.vm, (err, str) => { 401 | if (err) { 402 | return false; 403 | } 404 | expect(str).toMatchSnapshot(); 405 | }); 406 | }); 407 | 408 | it("can render default suggestion value by property name", async () => { 409 | const props = Object.assign({}, defaultProps); 410 | props.inputProps = Object.assign({}, defaultProps.inputProps); 411 | props.inputProps.class = "cool-class"; 412 | props.suggestions = [ 413 | { 414 | data: [ 415 | { 416 | id: 1, 417 | name: "Frodo", 418 | avatar: 419 | "https://upload.wikimedia.org/wikipedia/en/4/4e/Elijah_Wood_as_Frodo_Baggins.png" 420 | } 421 | ] 422 | } 423 | ]; 424 | 425 | props.onSelected = () => {}; 426 | 427 | const wrapper = mount(Autosuggest, { 428 | propsData: props, 429 | attachToDocument: true 430 | }); 431 | 432 | const input = wrapper.find("input"); 433 | input.trigger("click"); 434 | input.setValue("F"); 435 | 436 | input.trigger("keydown.down"); 437 | input.trigger("keydown.enter"); 438 | 439 | await wrapper.vm.$nextTick(() => {}); 440 | 441 | expect(input.element.value).toBe("Frodo"); 442 | 443 | const renderer = createRenderer(); 444 | renderer.renderToString(wrapper.vm, (err, str) => { 445 | if (err) { 446 | return false; 447 | } 448 | expect(str).toMatchSnapshot(); 449 | }); 450 | }); 451 | 452 | it("changes input attributes", () => { 453 | const props = { ...defaultProps }; 454 | props.inputProps = { ...defaultProps.inputProps, name: "my-input" }; 455 | 456 | const wrapper = mount(Autosuggest, { 457 | propsData: props 458 | }); 459 | 460 | const input = wrapper.find("input"); 461 | expect(input.attributes()["name"]).toBe("my-input"); 462 | }); 463 | 464 | it("search input prop type handles string and integers only", async () => { 465 | let props = { 466 | ...defaultProps, 467 | inputProps: {...defaultProps.inputProps} 468 | }; 469 | 470 | const mockConsole = jest.fn(); 471 | console.error = mockConsole; 472 | 473 | const blurred = () => {}; 474 | props.inputProps.onBlur = blurred; 475 | 476 | const wrapper = mount(Autosuggest, { 477 | propsData: props 478 | }); 479 | 480 | const input = wrapper.find("input"); 481 | 482 | // Integers 483 | input.trigger("click"); 484 | input.setValue(1); 485 | await wrapper.vm.$nextTick(() => {}); 486 | input.trigger("blur"); 487 | 488 | // Strings 489 | input.trigger("click"); 490 | input.setValue("Hello"); 491 | await wrapper.vm.$nextTick(() => {}); 492 | input.trigger("blur"); 493 | 494 | // Should not throw any errors 495 | expect(mockConsole).toHaveBeenCalledTimes(0); 496 | 497 | // Functions 498 | input.trigger("click"); 499 | wrapper.setData({ searchInput: () => { /* BAD */ } }); 500 | await wrapper.vm.$nextTick(() => {}); 501 | input.trigger("blur"); 502 | 503 | // Should throw validation error 504 | expect(mockConsole).toHaveBeenCalled(); 505 | }); 506 | 507 | it("can render slots", async () => { 508 | const wrapper = mount(Autosuggest, { 509 | propsData: defaultProps, 510 | slots: { 511 | 'before-suggestions': '
    ', 512 | 'after-suggestions': '' 513 | }, 514 | scopedSlots: { 515 | default: ` 516 |

    {{ suggestion.item }}

    517 | ` 518 | }, 519 | attachToDocument: true 520 | }); 521 | 522 | const input = wrapper.find("input"); 523 | input.trigger("click"); 524 | input.setValue("G"); 525 | 526 | expect(wrapper.findAll('.header-dude').length).toEqual(1); 527 | expect(wrapper.findAll('#footer-dude span').length).toEqual(2); 528 | expect(wrapper.findAll('h1').length).toEqual(5); 529 | 530 | await wrapper.vm.$nextTick(() => {}); 531 | 532 | const renderer = createRenderer(); 533 | renderer.renderToString(wrapper.vm, (err, str) => { 534 | if (err) { 535 | return false; 536 | } 537 | expect(str).toMatchSnapshot(); 538 | }); 539 | }); 540 | 541 | it("can render section slots", async () => { 542 | const props = { ...defaultProps }; 543 | props.suggestions.push({ name: 'dogs', data: ['spike', 'bud', 'rover']}) 544 | props.suggestions.push({ name: 'cats', data: ['sassy', 'tuesday', 'church']}) 545 | props.suggestions.push({ name: 'zeu', data: ['elephant', 'lion']}) 546 | props.suggestions.push({ name: 'Uhh', data: ['something', 'something2']}) 547 | 548 | props.sectionConfigs = { 549 | default: { 550 | label: "Suggestions", 551 | limit: 5, 552 | onSelected: () => {} 553 | }, 554 | Uhh: { 555 | label: "uhh" 556 | }, 557 | }; 558 | const wrapper = mount(Autosuggest, { 559 | propsData: props, 560 | attachToDocument: true, 561 | scopedSlots: { 562 | 'before-section-dogs': `
  • The Dogs
  • `, 563 | 'before-section-cats': `
  • Moar Cats is good
  • `, 564 | 'before-section-zeu': `
  • zoo animals?
  • ` 565 | }, 566 | }); 567 | 568 | const input = wrapper.find("input"); 569 | input.setValue("G"); 570 | 571 | input.trigger("click"); 572 | input.setValue("G"); 573 | expect(wrapper.find("ul li:nth-child(1)").element.innerHTML).toBe( 574 | props.sectionConfigs.default.label 575 | ); 576 | const renderer = createRenderer(); 577 | renderer.renderToString(wrapper.vm, (err, str) => { 578 | if (err) { 579 | return false; 580 | } 581 | expect(str).toMatchSnapshot(); 582 | }); 583 | }); 584 | 585 | it("can customize ids and classes for container divs", async () => { 586 | const wrapper = mount(Autosuggest, { 587 | propsData: { 588 | ...defaultProps, 589 | class: "containerz", 590 | 'component-attr-id-autosuggest': "automatischsuchen", 591 | 'component-attr-class-autosuggest-results-container': 'resultz-containerz', 592 | 'component-attr-class-autosuggest-results': 'resultz' 593 | }, 594 | attachToDocument: true 595 | }); 596 | 597 | const input = wrapper.find("input"); 598 | input.trigger("click"); 599 | input.setValue("G"); 600 | 601 | expect(wrapper.find('#automatischsuchen').is('div')).toBe(true); 602 | expect(wrapper.find('.containerz').is('div')).toBe(true); 603 | expect(wrapper.find('.resultz-containerz').is('div')).toBe(true); 604 | expect(wrapper.find(`#${defaultProps.inputProps.id}`).is('input')).toBe(true); 605 | 606 | const renderer = createRenderer(); 607 | renderer.renderToString(wrapper.vm, (err, str) => { 608 | if (err) { 609 | return false; 610 | } 611 | expect(str).toMatchSnapshot(); 612 | }); 613 | }); 614 | 615 | it("can customize css prefix", async () => { 616 | const wrapper = mount(Autosuggest, { 617 | propsData: { 618 | ...defaultProps, 619 | class: "containerz", 620 | 'component-attr-prefix': 'v', 621 | 'component-attr-id-autosuggest': "the-whole-thing", 622 | 'component-attr-class-autosuggest-results-container': 'the-results-container', 623 | 'component-attr-class-autosuggest-results': 'the-results', 624 | inputProps: { 625 | ...defaultProps.inputProps, 626 | id: 'the-input-thing', 627 | } 628 | }, 629 | attachToDocument: true 630 | }); 631 | 632 | const input = wrapper.find("input"); 633 | input.trigger("click"); 634 | input.setValue("G"); 635 | 636 | // Make sure the prefixes still allow for custom css/id's 637 | expect(wrapper.find('#the-whole-thing').is('div')).toBe(true); 638 | expect(wrapper.find('#the-input-thing').is('input')).toBe(true); 639 | expect(wrapper.find('.the-results-container').is('div')).toBe(true); 640 | expect(wrapper.find('.the-results').is('div')).toBe(true); 641 | 642 | // Prefix checks 643 | expect(wrapper.find('#v__results-item--0').is('li')).toBeTruthy() 644 | expect(wrapper.find('.v__results-item').is('li')).toBeTruthy() 645 | 646 | const renderer = createRenderer(); 647 | renderer.renderToString(wrapper.vm, (err, str) => { 648 | if (err) { 649 | return false; 650 | } 651 | expect(str).toMatchSnapshot(); 652 | }); 653 | }); 654 | 655 | it("@click and @selected listener events works as expected", async () => { 656 | let props = Object.assign({}, defaultProps); 657 | 658 | delete props['sectionConfigs'] 659 | 660 | const mockFn = jest.fn(); 661 | const mockConsole = jest.fn(); 662 | 663 | console.warn = mockConsole; 664 | 665 | const wrapper = mount(Autosuggest, { 666 | propsData: props, 667 | listeners: { 668 | click: e => { 669 | mockFn(e); 670 | }, 671 | selected: e => { 672 | mockFn(e); 673 | } 674 | }, 675 | attachToDocument: true 676 | }); 677 | 678 | await wrapper.vm.$nextTick(() => {}); 679 | 680 | const input = wrapper.find("input"); 681 | input.trigger("click"); 682 | wrapper.setData({ searchInput: "F" }); 683 | 684 | input.trigger("keydown.down"); 685 | input.trigger("keydown.enter"); 686 | 687 | expect(input.element.value).toBe("clifford kits"); 688 | 689 | expect(mockConsole).toHaveBeenCalledTimes(0); 690 | expect(mockFn).toHaveBeenCalledTimes(2); 691 | 692 | const renderer = createRenderer(); 693 | 694 | renderer.renderToString(wrapper.vm, (err, str) => { 695 | if (err) { 696 | return false; 697 | } 698 | expect(str).toMatchSnapshot(); 699 | }); 700 | }); 701 | 702 | it("tears down event listeners", async () => { 703 | let props = {...defaultProps}; 704 | 705 | delete props['sectionConfigs'] 706 | 707 | const AEL = jest.fn(); 708 | const REL = jest.fn(); 709 | 710 | window.document.addEventListener = AEL 711 | window.document.removeEventListener = REL 712 | 713 | const wrapper = mount(Autosuggest, { 714 | propsData: props, 715 | attachToDocument: true 716 | }); 717 | 718 | wrapper.destroy() 719 | expect(AEL).toHaveBeenCalledTimes(2) 720 | expect(REL).toHaveBeenCalledTimes(2) 721 | }); 722 | 723 | it("can modify input type attribute", async () => { 724 | const props = Object.assign({}, defaultProps); 725 | props.inputProps = { 726 | ...defaultProps.inputProps, 727 | type: 'search' 728 | }; 729 | 730 | props.suggestions = [filteredOptions[0]]; 731 | 732 | const wrapper = shallowMount(Autosuggest, { 733 | propsData: props 734 | }); 735 | 736 | const input = wrapper.find('input[type="search"]') 737 | expect(input.is('input')).toBe(true) 738 | expect(input.attributes("type", 'search')).toBeTruthy(); 739 | 740 | const renderer = createRenderer(); 741 | renderer.renderToString(wrapper.vm, (err, str) => { 742 | if (err) throw new Error(err); 743 | expect(str).toMatchSnapshot(); 744 | }); 745 | }); 746 | 747 | it("can modify input props", async () => { 748 | const Parent = { 749 | template: `
    750 | 753 |
    754 | `, 755 | components: { Autosuggest }, 756 | data: () => { 757 | return { 758 | 'ph': 'Type here...' 759 | } 760 | } 761 | } 762 | 763 | const wrapper = mount(Parent); 764 | const input = wrapper.find('input[type="text"]') 765 | expect(input.attributes("placeholder")).toBe('Type here...'); 766 | 767 | wrapper.setData({ ph: 'Please type here...' }) 768 | expect(input.attributes("placeholder")).toBe('Please type here...') 769 | }); 770 | 771 | it("can handle null data", async () => { 772 | const props = {...defaultProps, suggestions: [{ data: null }]}; 773 | 774 | const wrapper = shallowMount(Autosuggest, { 775 | propsData: props 776 | }); 777 | 778 | const renderer = createRenderer(); 779 | renderer.renderToString(wrapper.vm, (err, str) => { 780 | if (err) throw new Error(err); 781 | expect(str).toMatchSnapshot(); 782 | }); 783 | }); 784 | 785 | it("highlights first option on keydown when previously closed", async () => { 786 | const props = { ...defaultProps }; 787 | props.inputProps = { ...defaultProps.inputProps }; 788 | 789 | const wrapper = mount(Autosuggest, { 790 | propsData: props, 791 | attachToDocument: true 792 | }); 793 | 794 | const input = wrapper.find("input"); 795 | expect(input.attributes("id", defaultProps.inputProps.id)).toBeTruthy(); 796 | 797 | input.trigger("click"); 798 | input.setValue("G"); 799 | input.trigger("keydown.down"); 800 | input.trigger("keydown.enter"); 801 | input.trigger("keydown.down"); 802 | 803 | expect(wrapper.findAll("li.autosuggest__results-item--highlighted")).toHaveLength(1) 804 | 805 | const item = wrapper.find("li.autosuggest__results-item--highlighted") 806 | expect(item.attributes('data-suggestion-index')).toBe('0') 807 | expect(input.attributes('aria-activedescendant')).toBe('autosuggest__results-item--0') 808 | 809 | const renderer = createRenderer(); 810 | renderer.renderToString(wrapper.vm, (err, str) => { 811 | if (err) { 812 | return false; 813 | } 814 | expect(str).toMatchSnapshot(); 815 | }); 816 | }); 817 | 818 | it("can display ul and li classNames", async () => { 819 | const props = { ...defaultProps }; 820 | props.sectionConfigs.default.liClass = { 'hello-li': true } 821 | props.sectionConfigs.default.ulClass = { 'hello-ul': true } 822 | 823 | const wrapper = mount(Autosuggest, { 824 | propsData: props, 825 | listeners: defaultListeners, 826 | attachToDocument: true 827 | }); 828 | 829 | const input = wrapper.find("input"); 830 | input.setValue("G"); 831 | 832 | input.trigger("click"); 833 | input.setValue("G"); 834 | 835 | const ul = wrapper.find("ul") 836 | const li = ul.find("li:nth-child(1)") 837 | 838 | expect(ul.classes()).toContain('hello-ul'); 839 | expect(li.classes()).toContain('hello-li'); 840 | 841 | const renderer = createRenderer(); 842 | renderer.renderToString(wrapper.vm, (err, str) => { 843 | if (err) { 844 | return false; 845 | } 846 | expect(str).toMatchSnapshot(); 847 | }); 848 | }); 849 | 850 | it("emits opened and closed events", async () => { 851 | const props = { ...defaultProps }; 852 | props.inputProps = { ...defaultProps.inputProps }; 853 | 854 | const wrapper = mount(Autosuggest, { 855 | propsData: props, 856 | }); 857 | 858 | const input = wrapper.find("input"); 859 | input.setValue("G"); 860 | input.trigger("keydown.down"); 861 | 862 | await wrapper.vm.$nextTick(() => {}) 863 | expect(wrapper.emitted().opened).toBeTruthy(); 864 | 865 | input.trigger("keydown.esc"); 866 | await wrapper.vm.$nextTick(() => {}) 867 | expect(wrapper.emitted().closed).toBeTruthy(); 868 | }); 869 | 870 | it("emits item-changed event", async () => { 871 | const props = { ...defaultProps }; 872 | props.inputProps = { ...defaultProps.inputProps }; 873 | 874 | const wrapper = mount(Autosuggest, { 875 | propsData: props, 876 | }); 877 | 878 | const input = wrapper.find("input"); 879 | input.setValue("G"); 880 | input.trigger("keydown.down"); 881 | input.trigger("keydown.down"); 882 | 883 | await wrapper.vm.$nextTick(() => {}) 884 | expect(wrapper.emitted()['item-changed']).toHaveLength(2); 885 | const itemChanged1 = wrapper.emitted()['item-changed'][0] 886 | const itemChanged2 = wrapper.emitted()['item-changed'][1] 887 | 888 | // Emits with item and index 889 | expect(itemChanged1[0].item).toBe('clifford kits'); 890 | expect(itemChanged1[1]).toBe(0); 891 | expect(itemChanged2[0].item).toBe('friendly chemistry'); 892 | expect(itemChanged2[1]).toBe(1); 893 | 894 | input.trigger("keydown.up"); 895 | await wrapper.vm.$nextTick(() => {}) 896 | input.trigger("keydown.up"); 897 | await wrapper.vm.$nextTick(() => {}) 898 | await wrapper.vm.$nextTick(() => {}) 899 | 900 | // Ensure empty item-changed is emitted when user keys back 901 | // to the input #177 902 | expect(wrapper.emitted()['item-changed']).toHaveLength(4) 903 | const itemChangedEmpty = wrapper.emitted()['item-changed'][3] 904 | expect(itemChangedEmpty[0]).toBeNull(); 905 | expect(itemChangedEmpty[1]).toBeNull(); 906 | }); 907 | 908 | it("current index resilient against many keyups #190", async () => { 909 | const props = { ...defaultProps }; 910 | props.inputProps = { ...defaultProps.inputProps }; 911 | 912 | const wrapper = mount(Autosuggest, { 913 | propsData: props, 914 | }); 915 | 916 | const input = wrapper.find("input"); 917 | input.setValue("G"); 918 | input.trigger("keydown.down"); 919 | await wrapper.vm.$nextTick(() => {}) 920 | expect(wrapper.vm.currentIndex).toBe(0) 921 | input.trigger("keydown.up"); 922 | expect(wrapper.vm.currentIndex).toBe(-1) 923 | 924 | // Go into the upside down, but make sure to come back unscathed 925 | await wrapper.vm.$nextTick(() => {}) 926 | input.trigger("keydown.up"); 927 | await wrapper.vm.$nextTick(() => {}) 928 | input.trigger("keydown.up"); 929 | await wrapper.vm.$nextTick(() => {}) 930 | 931 | expect(wrapper.vm.currentIndex).toBe(-1) 932 | }); 933 | }); 934 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/autosuggest.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Autosuggest @click and @selected listener events works as expected 1`] = ` 4 |
    5 | 6 |
    7 | 8 |
    9 |
    10 | `; 11 | 12 | exports[`Autosuggest can click outside document to trigger close 1`] = ` 13 |
    14 | 15 |
    16 | 17 |
    18 |
    19 | `; 20 | 21 | exports[`Autosuggest can customize css prefix 1`] = ` 22 |
    23 |
    24 |
    25 |
    26 |
      27 |
    • 28 | clifford kits 29 |
    • 30 |
    • 31 | friendly chemistry 32 |
    • 33 |
    • 34 | phonics 35 |
    • 36 |
    • 37 | life of fred 38 |
    • 39 |
    • 40 | life of fred math 41 |
    • 42 |
    43 |
      44 |
    • 45 | spike 46 |
    • 47 |
    • 48 | bud 49 |
    • 50 |
    • 51 | rover 52 |
    • 53 |
    54 |
      55 |
    • 56 | sassy 57 |
    • 58 |
    • 59 | tuesday 60 |
    • 61 |
    • 62 | church 63 |
    • 64 |
    65 |
      66 |
    • 67 | elephant 68 |
    • 69 |
    • 70 | lion 71 |
    • 72 |
    73 |
      74 |
    • 75 | something 76 |
    • 77 |
    • 78 | something2 79 |
    • 80 |
    81 |
    82 |
    83 |
    84 | `; 85 | 86 | exports[`Autosuggest can customize ids and classes for container divs 1`] = ` 87 |
    88 |
    89 |
    90 |
    91 |
      92 |
    • 93 | clifford kits 94 |
    • 95 |
    • 96 | friendly chemistry 97 |
    • 98 |
    • 99 | phonics 100 |
    • 101 |
    • 102 | life of fred 103 |
    • 104 |
    • 105 | life of fred math 106 |
    • 107 |
    108 |
      109 |
    • 110 | spike 111 |
    • 112 |
    • 113 | bud 114 |
    • 115 |
    • 116 | rover 117 |
    • 118 |
    119 |
      120 |
    • 121 | sassy 122 |
    • 123 |
    • 124 | tuesday 125 |
    • 126 |
    • 127 | church 128 |
    • 129 |
    130 |
      131 |
    • 132 | elephant 133 |
    • 134 |
    • 135 | lion 136 |
    • 137 |
    138 |
      139 |
    • 140 | something 141 |
    • 142 |
    • 143 | something2 144 |
    • 145 |
    146 |
    147 |
    148 |
    149 | `; 150 | 151 | exports[`Autosuggest can display section header 1`] = ` 152 |
    153 |
    154 |
    155 |
    156 |
      157 |
    • Suggestions
    • 158 |
    • 159 | clifford kits 160 |
    • 161 |
    • 162 | friendly chemistry 163 |
    • 164 |
    • 165 | phonics 166 |
    • 167 |
    • 168 | life of fred 169 |
    • 170 |
    • 171 | life of fred math 172 |
    • 173 |
    174 |
    175 |
    176 |
    177 | `; 178 | 179 | exports[`Autosuggest can display ul and li classNames 1`] = ` 180 |
    181 |
    182 |
    183 |
    184 |
      185 |
    • 186 | clifford kits 187 |
    • 188 |
    • 189 | friendly chemistry 190 |
    • 191 |
    • 192 | phonics 193 |
    • 194 |
    • 195 | life of fred 196 |
    • 197 |
    • 198 | life of fred math 199 |
    • 200 |
    201 |
      202 |
    • 203 | spike 204 |
    • 205 |
    • 206 | bud 207 |
    • 208 |
    • 209 | rover 210 |
    • 211 |
    212 |
      213 |
    • 214 | sassy 215 |
    • 216 |
    • 217 | tuesday 218 |
    • 219 |
    • 220 | church 221 |
    • 222 |
    223 |
      224 |
    • 225 | elephant 226 |
    • 227 |
    • 228 | lion 229 |
    • 230 |
    231 |
      232 |
    • 233 | something 234 |
    • 235 |
    • 236 | something2 237 |
    • 238 |
    239 |
    240 |
    241 |
    242 | `; 243 | 244 | exports[`Autosuggest can handle null data 1`] = ` 245 |
    246 | 247 |
    248 | 249 |
    250 |
    251 | `; 252 | 253 | exports[`Autosuggest can modify input type attribute 1`] = ` 254 |
    255 | 256 |
    257 | 258 |
    259 |
    260 | `; 261 | 262 | exports[`Autosuggest can mount 1`] = ` 263 |
    264 | 265 |
    266 | 267 |
    268 |
    269 | `; 270 | 271 | exports[`Autosuggest can render default suggestion value by property name 1`] = ` 272 |
    273 | 274 |
    275 | 276 |
    277 |
    278 | `; 279 | 280 | exports[`Autosuggest can render section slots 1`] = ` 281 |
    282 |
    283 |
    284 |
    285 |
      286 |
    • Suggestions
    • 287 |
    • 288 | clifford kits 289 |
    • 290 |
    • 291 | friendly chemistry 292 |
    • 293 |
    • 294 | phonics 295 |
    • 296 |
    • 297 | life of fred 298 |
    • 299 |
    • 300 | life of fred math 301 |
    • 302 |
    303 |
      304 |
    • The Dogs
    • 305 |
    • 306 | spike 307 |
    • 308 |
    • 309 | bud 310 |
    • 311 |
    • 312 | rover 313 |
    • 314 |
    315 |
      316 |
    • Moar Cats is good
    • 317 |
    • 318 | sassy 319 |
    • 320 |
    • 321 | tuesday 322 |
    • 323 |
    • 324 | church 325 |
    • 326 |
    327 |
      328 |
    • zoo animals?
    • 329 |
    • 330 | elephant 331 |
    • 332 |
    • 333 | lion 334 |
    • 335 |
    336 |
      337 |
    • uhh
    • 338 |
    • 339 | something 340 |
    • 341 |
    • 342 | something2 343 |
    • 344 |
    345 |
    346 |
    347 |
    348 | `; 349 | 350 | exports[`Autosuggest can render simplest component with single onSelected 1`] = ` 351 |
    352 |
    353 |
    354 |
    355 |
      356 |
    • 357 | clifford kits 358 |
    • 359 |
    • 360 | friendly chemistry 361 |
    • 362 |
    • 363 | phonics 364 |
    • 365 |
    • 366 | life of fred 367 |
    • 368 |
    • 369 | life of fred math 370 |
    • 371 |
    • 372 | magic school bus 373 |
    • 374 |
    • 375 | math mammoth light blue 376 |
    • 377 |
    • 378 | handwriting 379 |
    • 380 |
    • 381 | math 382 |
    • 383 |
    • 384 | minecraft 385 |
    • 386 |
    • 387 | free worksheets 388 |
    • 389 |
    • 390 | 4th grade 391 |
    • 392 |
    • 393 | snap circuits 394 |
    • 395 |
    • 396 | bath toys 397 |
    • 398 |
    • 399 | channies 400 |
    • 401 |
    • 402 | fred 403 |
    • 404 |
    • 405 | lego 406 |
    • 407 |
    • 408 | math life of fred 409 |
    • 410 |
    • 411 | multiplication 412 |
    • 413 |
    • 414 | thinking tree 415 |
    • 416 |
    417 |
    418 |
    419 |
    420 | `; 421 | 422 | exports[`Autosuggest can render slots 1`] = ` 423 |
    424 |
    425 |
    426 |
    427 |
    428 |
      429 |
    • 430 |

      clifford kits

      431 |
    • 432 |
    • 433 |

      friendly chemistry

      434 |
    • 435 |
    • 436 |

      phonics

      437 |
    • 438 |
    • 439 |

      life of fred

      440 |
    • 441 |
    • 442 |

      life of fred math

      443 |
    • 444 |
    445 | 446 |
    447 |
    448 |
    449 | `; 450 | 451 | exports[`Autosuggest can render suggestions 1`] = ` 452 |
    453 |
    454 |
    455 |
    456 |
      457 |
    • 458 | clifford kits 459 |
    • 460 |
    • 461 | friendly chemistry 462 |
    • 463 |
    • 464 | phonics 465 |
    • 466 |
    • 467 | life of fred 468 |
    • 469 |
    • 470 | life of fred math 471 |
    • 472 |
    473 |
    474 |
    475 |
    476 | `; 477 | 478 | exports[`Autosuggest can select from suggestions using keystroke 1`] = ` 479 |
    480 | 481 |
    482 | 483 |
    484 |
    485 | `; 486 | 487 | exports[`Autosuggest can use escape key to exit 1`] = ` 488 |
    489 | 490 |
    491 | 492 |
    493 |
    494 | `; 495 | 496 | exports[`Autosuggest highlights first option on keydown when previously closed 1`] = ` 497 |
    498 |
    499 |
    500 |
    501 |
      502 |
    • 503 | clifford kits 504 |
    • 505 |
    • 506 | friendly chemistry 507 |
    • 508 |
    • 509 | phonics 510 |
    • 511 |
    • 512 | life of fred 513 |
    • 514 |
    • 515 | life of fred math 516 |
    • 517 |
    518 |
      519 |
    • 520 | spike 521 |
    • 522 |
    • 523 | bud 524 |
    • 525 |
    • 526 | rover 527 |
    • 528 |
    529 |
      530 |
    • 531 | sassy 532 |
    • 533 |
    • 534 | tuesday 535 |
    • 536 |
    • 537 | church 538 |
    • 539 |
    540 |
      541 |
    • 542 | elephant 543 |
    • 544 |
    • 545 | lion 546 |
    • 547 |
    548 |
      549 |
    • 550 | something 551 |
    • 552 |
    • 553 | something2 554 |
    • 555 |
    556 |
    557 |
    558 |
    559 | `; 560 | 561 | exports[`Autosuggest is aria complete 1`] = ` 562 |
    563 |
    564 |
    565 |
    566 |
      567 |
    • Suggestions
    • 568 |
    • 569 | clifford kits 570 |
    • 571 |
    • 572 | friendly chemistry 573 |
    • 574 |
    • 575 | phonics 576 |
    • 577 |
    • 578 | life of fred 579 |
    • 580 |
    • 581 | life of fred math 582 |
    • 583 |
    584 |
    585 |
    586 |
    587 | `; 588 | --------------------------------------------------------------------------------