├── .babelrc ├── .browserslistrc ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── build ├── base │ └── plugins │ │ └── index.js ├── rollup.config.dev.js └── rollup.config.prod.js ├── demo ├── App.vue ├── index.html └── index.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── components │ └── VueAccessibleSelect │ │ └── VueAccessibleSelect.vue ├── config │ └── index.js ├── index.js └── styles │ ├── core.scss │ └── themes │ └── default.scss └── types └── index.d.ts /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@babel/preset-env"]] 3 | } 4 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist/* 4 | demo/demo* -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "es5" 5 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Andrew Vasilchuk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-accessible-select 2 | 3 | > Vue.js accessible select component made according to [WAI-ARIA practices](https://www.w3.org/TR/wai-aria-practices/#Listbox). 4 | 5 | ## ✨ Features 6 | 7 | - fully accessible; 8 | - ⌨️ keyboard navigation (`Page Up/Down`, `Home`, `End`, `Esc`); 9 | - 🔣 type-ahead to select option that starts with typed symbols; 10 | - 💅 style agnostic, so you can style it whatever you like (but including `core.scss` is highly encouraged). 11 | 12 | ## Demo 13 | 14 | [![Edit vue-accessible-select](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/vue-accessible-select-qse3r?fontsize=14) 15 | 16 | ## 💿 Installation 17 | 18 | ### 📦 Via NPM 19 | 20 | ```bash 21 | $ npm install vue-accessible-select --save 22 | ``` 23 | 24 | ### 🧶 Via Yarn 25 | 26 | ```bash 27 | $ yarn add vue-accessible-select 28 | ``` 29 | 30 | ## Initialization 31 | 32 | ### As a plugin 33 | 34 | It must be called before `new Vue()`. 35 | 36 | ```js 37 | import Vue from 'vue' 38 | import VueAccessibleSelect from 'vue-accessible-select' 39 | 40 | Vue.use(VueAccessibleSelect) 41 | ``` 42 | 43 | ### As a global component 44 | 45 | ```javascript 46 | import Vue from 'vue' 47 | import { VueAccessibleSelect } from 'vue-accessible-select' 48 | 49 | Vue.component('VueAccessibleSelect', VueAccessibleSelect) 50 | ``` 51 | 52 | ### As a local component 53 | 54 | ```javascript 55 | import { VueAccessibleSelect } from 'vue-accessible-select' 56 | 57 | export default { 58 | name: 'YourAwesomeComponent', 59 | components: { 60 | VueAccessibleSelect, 61 | }, 62 | } 63 | ``` 64 | 65 | > ℹ️ Note to set global options (for example `transition` for each select component), you should do the following: 66 | 67 | ```js 68 | import { config } from 'vue-accessible-select' 69 | 70 | config.transition = { 71 | name: 'foo', 72 | } 73 | ``` 74 | 75 | > ⚠️ Options passed locally via `props` will always take precedence over global config options. 76 | 77 | Default `config.js`: 78 | 79 | ```js 80 | export default { 81 | transition: null, 82 | } 83 | ``` 84 | 85 | ## 🚀 Usage 86 | 87 | ### Template 88 | 89 | ```html 90 | 100 | ``` 101 | 102 | ### Script 103 | 104 | ```js 105 | export default { 106 | // ... 107 | data() { 108 | return { 109 | value: undefined, 110 | options: [ 111 | { 112 | value: 0, 113 | label: '🍇 Grape', 114 | }, 115 | { 116 | value: { foo: 'bar' }, 117 | label: '🍉 Watermelon', 118 | }, 119 | { 120 | value: { foo: 'bar' }, 121 | label: '🥝 Kiwi', 122 | }, 123 | { 124 | value: false, 125 | label: '🥭 Mango', 126 | }, 127 | { 128 | value: true, 129 | label: '🍓 Strawberry', 130 | }, 131 | { 132 | value: 'lemon', 133 | label: '🍋 Lemon', 134 | }, 135 | ], 136 | } 137 | }, 138 | // ... 139 | } 140 | ``` 141 | 142 | ### 🎨 Styles 143 | 144 | Then don't forget to include core styles. Also library is sipped with default theme styles you can use. 145 | 146 | `SASS`: 147 | 148 | ```scss 149 | // recommended 150 | @import 'vue-accessible-select/src/styles/core.scss'; 151 | 152 | // optional 153 | @import 'vue-accessible-select/src/styles/themes/default.scss'; 154 | ``` 155 | 156 | Or already compiled `CSS`: 157 | 158 | ```css 159 | /* recommended */ 160 | @import 'vue-accessible-select/dist/styles/core.scss'; 161 | 162 | /* optional */ 163 | @import 'vue-accessible-select/dist/styles/themes/default.scss'; 164 | ``` 165 | 166 | > ⚠️ Note that when you import already compiled CSS you don't have ability to override `SASS` variables during build process, so it is preferable to use `.scss` file. 167 | 168 | When importing `core.scss`, there are `SASS` variables you can override during build process: 169 | 170 | ```scss 171 | $v-select-menu-position-top: 100% !default; 172 | $v-select-arrow-size: 8px !default; 173 | ``` 174 | 175 | ### API 176 | 177 | #### ⚙️ Props 178 | 179 | `` accepts some `props`: 180 | 181 | | Prop | Description | 182 | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 183 | | `options: array` | `required` Array of select options. Should be an array of objects that match the following pattern `{ value: any, label: string }` | 184 | | `value: any` | `required` Current value of select. When value is `undefined`, it is considered that select has no current value. | 185 | | `label: string` | Select label | 186 | | `placeholder: string` | Select placeholder | 187 | | `disabled: boolean` | Whether select is disabled | 188 | | `transition: object` | Through this object you can configure the transition of `.v-select__menu` entrance and leave. Should match the following pattern `{ name: string, mode: string? }` | 189 | 190 | #### 🕳️ Slots 191 | 192 | `` provides you with some `slots` and `scopedSlots` you can use to fit your needs. 193 | 194 | | Slot | Scope | Description | 195 | | ------------- | ------------------- | ---------------- | 196 | | `label` | | Label slot | 197 | | `prepend` | | Prepend slot | 198 | | `placeholder` | `{ placeholder }` | Placeholder slot | 199 | | `selected` | `{ value, option }` | Selected slot | 200 | | `arrow` | | Arrow slot | 201 | | `option` | `{ value, option }` | Option slot | 202 | | `no-options` | | No options slot | 203 | 204 | #### Example of possible usage of `slots` and `scopedSlots` 205 | 206 | ```html 207 | 208 | 211 | 216 | 219 | 222 | 225 | 228 | 229 | ``` 230 | 231 | ## ⌨️ Keyboard shortcuts 232 | 233 | `` is fully accessible when it comes to keyboard interaction. 234 | 235 | Here is some useful keys and their appropriate actions: 236 | 237 | - `Down Arrow` – Moves focus and selection to the next option. 238 | - `Up Arrow` – Moves focus and selection to the previous option. 239 | - `Home` – Moves focus and selection to the first option. 240 | - `End` – Moves focus and selection to the last option. 241 | - `Esc` – Closes menu. 242 | 243 | Type ahead: 244 | 245 | - Type a character: focus and selection moves to the next option with a label that starts with the typed character; 246 | - Type multiple characters in rapid succession: focus and selection moves to the next option with a label that starts with the string of characters typed. 247 | 248 | ## Powered by 249 | 250 | - `Rollup` (and plugins); 251 | - `SASS` and `node-sass`; 252 | - `PostCSS`; 253 | - `Autoprefixer`. 254 | 255 | ## 🔒 License 256 | 257 | [MIT](http://opensource.org/licenses/MIT) 258 | -------------------------------------------------------------------------------- /build/base/plugins/index.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve' 2 | import commonjs from '@rollup/plugin-commonjs' 3 | import vue from 'rollup-plugin-vue' 4 | 5 | export default [resolve(), commonjs(), vue({ needMap: false })] 6 | -------------------------------------------------------------------------------- /build/rollup.config.dev.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import serve from 'rollup-plugin-serve' 3 | import livereload from 'rollup-plugin-livereload' 4 | import replace from '@rollup/plugin-replace' 5 | 6 | import plugins from './base/plugins/index' 7 | 8 | export default { 9 | input: path.join(__dirname, '../demo/index.js'), 10 | output: { 11 | file: path.join(__dirname, '../demo/demo.js'), 12 | format: 'iife', 13 | sourcemap: true, 14 | }, 15 | plugins: [ 16 | replace({ 17 | 'process.env.NODE_ENV': JSON.stringify('development'), 18 | }), 19 | serve({ 20 | open: true, 21 | contentBase: path.join(__dirname, '../demo'), 22 | port: 8080, 23 | }), 24 | livereload({ 25 | verbose: true, 26 | watch: path.join(__dirname, '../demo'), 27 | }), 28 | ].concat(plugins), 29 | } 30 | -------------------------------------------------------------------------------- /build/rollup.config.prod.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import replace from '@rollup/plugin-replace' 3 | import { babel } from '@rollup/plugin-babel' 4 | import { terser } from 'rollup-plugin-terser' 5 | 6 | import plugins from './base/plugins/index.js' 7 | 8 | const name = 'VueAccessibleSelect' 9 | 10 | export default [ 11 | { 12 | input: path.join(__dirname, '../src/index.js'), 13 | output: [ 14 | { 15 | file: 'dist/vue-accessible-select.js', 16 | format: 'umd', 17 | name, 18 | }, 19 | { 20 | file: 'dist/vue-accessible-select.common.js', 21 | format: 'cjs', 22 | }, 23 | { 24 | file: 'dist/vue-accessible-select.esm.js', 25 | format: 'esm', 26 | }, 27 | ], 28 | plugins: [ 29 | replace({ 30 | 'process.env.NODE_ENV': JSON.stringify('production'), 31 | }), 32 | ].concat(plugins), 33 | }, 34 | { 35 | input: path.join(__dirname, '../src/index.js'), 36 | output: { 37 | file: 'dist/vue-accessible-select.min.js', 38 | format: 'umd', 39 | name, 40 | }, 41 | plugins: [ 42 | replace({ 43 | 'process.env.NODE_ENV': JSON.stringify('production'), 44 | }), 45 | babel(), 46 | terser(), 47 | ].concat(plugins), 48 | }, 49 | ] 50 | -------------------------------------------------------------------------------- /demo/App.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 108 | 109 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | vue-accessible-select 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import VueAccessibleSelectPlugin from '../src' 4 | 5 | Vue.config.productionTip = false 6 | 7 | Vue.use(VueAccessibleSelectPlugin) 8 | 9 | new Vue({ 10 | el: '#app', 11 | render: h => h(App), 12 | }) 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-accessible-select", 3 | "version": "1.2.1", 4 | "private": false, 5 | "description": "Vue.js component for accessible selects", 6 | "keywords": [ 7 | "accessibility", 8 | "select", 9 | "vue", 10 | "vue-accessible-select", 11 | "vue-select" 12 | ], 13 | "homepage": "https://github.com/andrewvasilchuk/vue-accessible-select#readme", 14 | "bugs": { 15 | "url": "https://github.com/andrewvasilchuk/vue-accessible-select/issues" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/andrewvasilchuk/vue-accessible-select" 20 | }, 21 | "license": "MIT", 22 | "author": "Andrew Vasilchuk ", 23 | "files": [ 24 | "src", 25 | "dist", 26 | "types/*.d.ts" 27 | ], 28 | "main": "dist/vue-accessible-select.js", 29 | "unpkg": "dist/vue-accessible-select.min.js", 30 | "module": "dist/vue-accessible-select.esm.js", 31 | "types": "types/index.d.ts", 32 | "scripts": { 33 | "prepare": "npm run build", 34 | "build": "rimraf dist/* && rollup -c build/rollup.config.prod.js && npm run build:css && npm run postcss", 35 | "build:css": "node-sass ./src -o ./dist --output-style compressed -x", 36 | "dev": "rollup -c build/rollup.config.dev.js --watch", 37 | "postcss": "postcss ./dist/**/*.css -r --no-map", 38 | "test:unit": "jest" 39 | }, 40 | "devDependencies": { 41 | "@babel/core": "^7.16.7", 42 | "@babel/preset-env": "^7.16.7", 43 | "@rollup/plugin-babel": "^5.3.0", 44 | "@rollup/plugin-commonjs": "^21.0.1", 45 | "@rollup/plugin-node-resolve": "^13.1.3", 46 | "@rollup/plugin-replace": "^3.0.1", 47 | "autoprefixer": "^10.4.2", 48 | "babel-core": "^7.0.0-bridge.0", 49 | "node-sass": "^7.0.1", 50 | "postcss": "^8.4.5", 51 | "postcss-cli": "^9.1.0", 52 | "pug": "^3.0.2", 53 | "rimraf": "^3.0.2", 54 | "rollup": "^2.63.0", 55 | "rollup-plugin-livereload": "^2.0.5", 56 | "rollup-plugin-postcss": "^4.0.2", 57 | "rollup-plugin-serve": "^1.1.0", 58 | "rollup-plugin-terser": "^7.0.2", 59 | "rollup-plugin-vue": "^4.7.2", 60 | "vue": "^2.6.14", 61 | "vue-template-compiler": "^2.6.14" 62 | }, 63 | "peerDependencies": { 64 | "vue": "^2.6.14" 65 | }, 66 | "dependencies": { 67 | "keycode-js": "^3.1.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('autoprefixer')], 3 | } 4 | -------------------------------------------------------------------------------- /src/components/VueAccessibleSelect/VueAccessibleSelect.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | 332 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | transition: null, 3 | } 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import VueAccessibleSelect from './components/VueAccessibleSelect/VueAccessibleSelect.vue' 2 | 3 | import config from './config' 4 | 5 | const Plugin = { 6 | install(Vue) { 7 | // Make sure that plugin can be installed only once 8 | if (this.installed) { 9 | return 10 | } 11 | 12 | Vue.component('VueAccessibleSelect', VueAccessibleSelect) 13 | }, 14 | } 15 | 16 | export default Plugin 17 | 18 | export { VueAccessibleSelect, config } 19 | -------------------------------------------------------------------------------- /src/styles/core.scss: -------------------------------------------------------------------------------- 1 | // v-select__menu styles 2 | $v-select-menu-position-top: 100% !default; 3 | 4 | // v-select__arrow styles 5 | $v-select-arrow-size: 8px !default; 6 | 7 | .v-select { 8 | &__inner { 9 | position: relative; 10 | } 11 | 12 | &__menu { 13 | position: absolute; 14 | top: $v-select-menu-position-top; 15 | left: 0; 16 | width: 100%; 17 | } 18 | 19 | &__list { 20 | padding-left: 0; 21 | margin: { 22 | top: 0; 23 | bottom: 0; 24 | } 25 | list-style: none; 26 | overflow: auto; 27 | } 28 | 29 | &__arrow { 30 | margin-left: auto; 31 | 32 | svg { 33 | display: inline-block; 34 | width: $v-select-arrow-size; 35 | height: $v-select-arrow-size; 36 | vertical-align: middle; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/styles/themes/default.scss: -------------------------------------------------------------------------------- 1 | // v-select__label styles 2 | $v-select-label-display: inline-block !default; 3 | $v-select-label-margin-bottom: 8px !default; 4 | $v-select-label-font-size: 14px !default; 5 | 6 | // v-select__btn styles 7 | $v-select-btn-display: flex !default; 8 | $v-select-btn-width: 100% !default; 9 | $v-select-btn-height: 38px !default; 10 | $v-select-btn-horizontal-padding: 16px !default; 11 | $v-select-btn-vertical-padding: 0 !default; 12 | $v-select-btn-border-width: 1px !default; 13 | $v-select-btn-border-style: solid !default; 14 | $v-select-btn-border-color: #ced4da !default; 15 | $v-select-btn-border-radius: 4px !default; 16 | $v-select-btn-background-color: #fff !default; 17 | $v-select-btn-border-color-focus: #80bdff !default; 18 | $v-select-btn-box-shadow-focus: 0 0 0 4px rgba(0, 128, 255, 0.24) !default; 19 | $v-select-btn-font-size: 100% !default; 20 | 21 | // v-select__menu styles 22 | $v-select-menu-vertical-padding: 8px !default; 23 | $v-select-menu-border-width: 1px !default; 24 | $v-select-menu-border-style: solid !default; 25 | $v-select-menu-border-color: rgba(0, 0, 0, 0.15) !default; 26 | $v-select-menu-border-radius: 4px !default; 27 | $v-select-menu-background-color: #fff !default; 28 | 29 | // v-select__list styles 30 | $v-select-list-max-height: 160px !default; 31 | 32 | // v-select__option styles 33 | $v-select-option-padding: 4px 16px !default; 34 | $v-select-option-background-color-hover: #f8f9fa !default; 35 | $v-select-option-color-selected: #fff !default; 36 | $v-select-option-background-color-selected: #007bff !default; 37 | 38 | .v-select { 39 | $block: &; 40 | 41 | &__label { 42 | display: $v-select-label-display; 43 | margin-bottom: $v-select-label-margin-bottom; 44 | font-size: $v-select-label-font-size; 45 | } 46 | 47 | &__btn { 48 | display: $v-select-btn-display; 49 | width: $v-select-btn-width; 50 | height: $v-select-btn-height; 51 | padding: $v-select-btn-vertical-padding $v-select-btn-horizontal-padding; 52 | border: $v-select-btn-border-width $v-select-btn-border-style $v-select-btn-border-color; 53 | border-radius: $v-select-btn-border-radius; 54 | background-color: $v-select-btn-background-color; 55 | font-size: $v-select-btn-font-size; 56 | 57 | @if $v-select-btn-display == flex { 58 | align-items: center; 59 | } 60 | 61 | &:focus { 62 | outline: 0; 63 | border-color: $v-select-btn-border-color-focus; 64 | box-shadow: $v-select-btn-box-shadow-focus; 65 | } 66 | } 67 | 68 | &__menu { 69 | padding: { 70 | top: $v-select-menu-vertical-padding; 71 | bottom: $v-select-menu-vertical-padding; 72 | } 73 | border: $v-select-menu-border-width $v-select-menu-border-style $v-select-menu-border-color; 74 | border-radius: $v-select-menu-border-radius; 75 | background-color: $v-select-menu-background-color; 76 | } 77 | 78 | &__list { 79 | max-height: $v-select-list-max-height; 80 | 81 | &:focus { 82 | outline: 0; 83 | } 84 | } 85 | 86 | &__option { 87 | padding: $v-select-option-padding; 88 | cursor: pointer; 89 | 90 | &:hover:not(#{$block}__option--selected) { 91 | background-color: $v-select-option-background-color-hover; 92 | } 93 | 94 | &--selected { 95 | background-color: $v-select-option-background-color-selected; 96 | color: $v-select-option-color-selected; 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { PluginFunction } from 'vue' 2 | 3 | declare const VueAccessibleSelect: VueAccessibleSelect 4 | 5 | export default VueAccessibleSelect 6 | 7 | export interface VueAccessibleSelect { 8 | install: PluginFunction 9 | } 10 | 11 | export interface VueAccessibleSelectOption { 12 | value: any 13 | label: string 14 | } 15 | 16 | export type VueAccessibleSelectOptions = VueAccessibleSelectOption[] 17 | --------------------------------------------------------------------------------