├── .eslintrc.js ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── cypress.json ├── demo ├── .browserslistrc ├── .gitignore ├── README.md ├── babel.config.js ├── package.json ├── public │ ├── favicon.ico │ └── index.html └── src │ ├── App.vue │ ├── assets │ └── vue-a11y-logo.png │ ├── components │ └── HelloWorld.vue │ └── main.js ├── jest.config.js ├── package-lock.json ├── package.json ├── rollup.config.dev.js ├── rollup.config.prod.js ├── src ├── FocusLoop.vue ├── index.js └── plugin.js └── test └── support ├── commands.js └── index.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | 'cypress/globals': true, 5 | browser: true, 6 | node: true 7 | }, 8 | plugins: [ 9 | 'cypress', 10 | 'vuejs-accessibility' 11 | ], 12 | extends: [ 13 | 'plugin:vue/recommended', 14 | '@vue/standard', 15 | 'plugin:vuejs-accessibility/recommended' 16 | ], 17 | parserOptions: { 18 | parser: 'babel-eslint' 19 | }, 20 | rules: { 21 | 'no-console': 'off' 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .temp 4 | demo/vue-focus-loop.js -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [0.2.0](https://github.com/vue-a11y/vue-focus-loop/compare/v0.1.2...v0.2.0) (2021-05-04) 6 | 7 | 8 | ### Features 9 | 10 | * **a11y:** Content outside inert for assistive technology users ([ca85261](https://github.com/vue-a11y/vue-focus-loop/commit/ca8526157dfc0ce9cd3d1fe6879b6113c890afbc)) 11 | * Add autoFocus props ([3716d1e](https://github.com/vue-a11y/vue-focus-loop/commit/3716d1ef0ab8dc4804bac7a0b4668d2fd05165b7)) 12 | 13 | ### [0.1.2](https://github.com/vue-a11y/vue-focus-loop/compare/v0.1.1...v0.1.2) (2020-08-24) 14 | 15 | 16 | ### Bug Fixes 17 | 18 | * Change prop from isActive to isVisible ([552bb44](https://github.com/vue-a11y/vue-focus-loop/commit/552bb44f19b3e3dba56af5896ab1ebcdcae3c1e8)) 19 | 20 | ### [0.1.1](https://github.com/vue-a11y/vue-focus-loop/compare/v0.1.0...v0.1.1) (2020-08-24) 21 | 22 | 23 | ### Features 24 | 25 | * Set focus in first element when wrapper visible ([897777e](https://github.com/vue-a11y/vue-focus-loop/commit/897777e88cc8370dd945d1cb856c90d8a156cc00)) 26 | 27 | ## 0.1.0 (2020-07-28) 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Vue A11Y 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [@vue-a11y/focus-loop](https://focus-loop.vue-a11y.com) 2 | 3 | --- 4 | 🔥 HEADS UP! You are in the Vue 2 compatible branch, [check the "next" branch for Vue3 support](https://github.com/vue-a11y/vue-focus-loop/tree/next). 5 | 6 | --- 7 | ## Introduction 8 | 9 | Vue component that helps you to to trap focus in an element. 10 | 11 | When developing accessible components, in certain behaviors it is important to trap focus on the element. 12 | 13 | For example, when opening a modal, it is recommended that the focus is only on the tabbable elements of that modal and only release the focus, when the modal is closed. 14 | 15 | - Focus is trapped: Tab and Shift+Tab will cycle through the focusable nodes within `` without returning to the main document below; 16 | - Automatic focus on the first focusable element, with the option to disable; 17 | - Restoring focus to the last activeElement; 18 | - Hides the document from screen readers when `` is visible and enabled. 19 | 20 | ## Installation 21 | 22 | Add `@vue-a11y/focus-loop` in your Vue project. 23 | 24 | ```sh 25 | npm install -S @vue-a11y/focus-loop 26 | # or 27 | yarn add @vue-a11y/focus-loop 28 | ``` 29 | 30 | Or via CDN 31 | 32 | ```html 33 | 34 | ``` 35 | 36 | ## Usage 37 | 38 | You can use it globally in your main.js. 39 | 40 | ```js 41 | import Vue from 'vue' 42 | import VueFocusLoop from '@vue-a11y/focus-loop' 43 | 44 | Vue.use(VueFocusLoop) 45 | ``` 46 | 47 | Or you can import into your component. 48 | 49 | ```js 50 | import { FocusLoop } from '@vue-a11y/focus-loop' 51 | 52 | export default { 53 | components: { 54 | FocusLoop 55 | } 56 | } 57 | ``` 58 | 59 | **Example** of use on your single file component. 60 | 61 | ```vue 62 | 95 | ``` 96 | 97 | ## Make the focus-loop container visible and rendered 98 | 99 | prop | type | default 100 | ---------- | --------- | ------------ 101 | isVisible | `Boolean` | `false` 102 | 103 | ```html 104 | 105 | 106 | 107 | ``` 108 | 109 | ## Disable loop 110 | 111 | You can disable the focus trap and activate it only when you really need it. 112 | 113 | prop | type | default 114 | ---------- | --------- | ------------ 115 | disabled | `Boolean` | `false` 116 | 117 | For example: 118 | 119 | ```html 120 | 121 | 122 | 123 | ``` 124 | 125 | ## Disable autofocus on the first element 126 | 127 | When activating the ``, the first element receives the focus automatically, however, if you want to disable this behavior, just disable it through the `autoFocus` prop. 128 | 129 | prop | type | default 130 | ----------- | --------- | ------------ 131 | autoFocus | `Boolean` | `true` 132 | 133 | For example: 134 | 135 | ```html 136 | 137 | 138 | 139 | ``` 140 | 141 | ## Keyboard support 142 | 143 | Keyboard users will use `Tab` and `Shift + Tab` to navigate tabbable elements. 144 | 145 | According to the Modal Dialog Example in WAI-ARIA Authoring Practices specification, when the focus is on the last focusable element, you must move the focus to the first element and vice versa. 146 | 147 | Key | Function 148 | ------------ | ------------ 149 | Tab | ▸ Moves focus to next focusable element inside the dialog.
▸ When focus is on the last focusable element in the dialog, moves focus to the first focusable element in the dialog. 150 | Shift + Tab | ▸ Moves focus to previous focusable element inside the dialog.
▸ When focus is on the first focusable element in the dialog, moves focus to the last focusable element in the dialog. 151 | 152 | ## Links 153 | - [Demo](https://vue-focus-loop.surge.sh) 154 | 155 | ## Articles that served as inspiration: 156 | - [Using JavaScript to trap focus in an element](https://hiddedevries.nl/en/blog/2017-01-29-using-javascript-to-trap-focus-in-an-element) 157 | 158 | ## Other options 159 | - [focus-trap](https://github.com/davidtheclark/focus-trap) 160 | - [vue-focus-lock](https://github.com/theKashey/vue-focus-lock) 161 | 162 | ## Contributing 163 | - From typos in documentation to coding new features; 164 | - Check the open issues or open a new issue to start a discussion around your feature idea or the bug you found; 165 | - Fork repository, make changes and send a pull request; 166 | 167 | Follow us on Twitter [@vue_a11y](https://twitter.com/vue_a11y) 168 | 169 | **Thank you** -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:8080", 3 | "fixturesFolder": "tests/e2e/fixtures", 4 | "screenshotsFolder": "tests/e2e/screenshots", 5 | "integrationFolder": "tests/e2e/integration", 6 | "fileServerFolder": "demo", 7 | "pluginsFile": false, 8 | "supportFile": "tests/e2e/support" 9 | } 10 | -------------------------------------------------------------------------------- /demo/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | package-lock.json 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # demo 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Customize configuration 19 | See [Configuration Reference](https://cli.vuejs.org/config/). 20 | -------------------------------------------------------------------------------- /demo/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build" 8 | }, 9 | "dependencies": { 10 | "core-js": "^3.6.5", 11 | "vue": "^2.6.11" 12 | }, 13 | "devDependencies": { 14 | "@vue/cli-plugin-babel": "~4.4.0", 15 | "@vue/cli-service": "~4.4.0", 16 | "vue-template-compiler": "^2.6.11" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vue-a11y/vue-focus-loop/17c7f4794ab8e277324a9f5407d072ff836b1d34/demo/public/favicon.ico -------------------------------------------------------------------------------- /demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /demo/src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | 22 | 51 | -------------------------------------------------------------------------------- /demo/src/assets/vue-a11y-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vue-a11y/vue-focus-loop/17c7f4794ab8e277324a9f5407d072ff836b1d34/demo/src/assets/vue-a11y-logo.png -------------------------------------------------------------------------------- /demo/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 70 | 71 | 72 | 89 | -------------------------------------------------------------------------------- /demo/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import VueFocusLoop from '../../src' 4 | 5 | Vue.config.productionTip = false 6 | Vue.use(VueFocusLoop) 7 | 8 | new Vue({ 9 | render: h => h(App) 10 | }).$mount('#app') 11 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleNameMapper: { 3 | '^@/(.*)$': '/$1', 4 | '^vue$': 'vue/dist/vue.common.js' 5 | }, 6 | moduleFileExtensions: ['js', 'vue', 'json'], 7 | testMatch: [ 8 | '**/(*.)test.(js|jsx|ts|tsx)' 9 | ], 10 | transform: { 11 | '^.+\\.js$': 'babel-jest', 12 | '.*\\.(vue)$': 'vue-jest' 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vue-a11y/focus-loop", 3 | "version": "0.2.0", 4 | "description": "Vue component that helps you to to trap focus in an element.", 5 | "main": "dist/vue-focus-loop.ssr.js", 6 | "browser": "dist/vue-focus-loop.esm.js", 7 | "module": "dist/vue-focus-loop.esm.js", 8 | "unpkg": "dist/vue-focus-loop.min.js", 9 | "scripts": { 10 | "dev": "rollup --config rollup.config.dev.js --watch", 11 | "build": "npm run build:ssr & npm run build:es & npm run build:unpkg", 12 | "build:ssr": "rollup --config rollup.config.prod.js --format cjs --file dist/vue-focus-loop.ssr.js", 13 | "build:es": "rollup --config rollup.config.prod.js --format es --file dist/vue-focus-loop.esm.js", 14 | "build:unpkg": "rollup --config rollup.config.prod.js --format iife --file dist/vue-focus-loop.min.js", 15 | "docs:dev": "vuepress dev docs --no-cache", 16 | "docs:build": "vuepress build docs --no-cache && echo darkmode.vue-a11y.com >> docs/.vuepress/dist/CNAME", 17 | "docs:publish": "gh-pages -d docs/.vuepress/dist", 18 | "demo:dev": "cd demo && npm run serve", 19 | "demo:build": "cd demo && npm run build", 20 | "demo:publish": "cp ./demo/dist/index.html ./demo/dist/200.html && surge ./demo/dist https://vue-focus-loop.surge.sh/", 21 | "release": "standard-version", 22 | "test:unit": "jest", 23 | "test:e2e": "node_modules/.bin/cypress run --headless", 24 | "test:e2e:open": "node_modules/.bin/cypress open ", 25 | "project:publish": "git push --tags origin master && npm run build && npm publish --access public" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/vue-a11y/vue-focus-loop.git" 30 | }, 31 | "keywords": [ 32 | "vue", 33 | "component", 34 | "a11y", 35 | "accessibility", 36 | "focus", 37 | "tab", 38 | "tab-loop", 39 | "focus trap", 40 | "focus loop", 41 | "vue-a11y", 42 | "vue.js" 43 | ], 44 | "author": "Alan Ktquez (https://github.com/ktquez)", 45 | "license": "MIT", 46 | "bugs": { 47 | "url": "https://github.com/vue-a11y/vue-focus-loop/issues" 48 | }, 49 | "homepage": "https://github.com/vue-a11y/vue-focus-loop#readme", 50 | "devDependencies": { 51 | "@rollup/plugin-buble": "^0.21.3", 52 | "@rollup/plugin-commonjs": "^14.0.0", 53 | "@rollup/plugin-node-resolve": "^8.4.0", 54 | "@rollup/plugin-replace": "^2.4.2", 55 | "@vue/eslint-config-standard": "^5.1.2", 56 | "@vue/test-utils": "^1.3.6", 57 | "@vuepress/plugin-register-components": "^1.9.10", 58 | "@vuepress/theme-default": "^1.9.10", 59 | "babel-eslint": "^10.1.0", 60 | "babel-jest": "^26.6.3", 61 | "chokidar": "^3.6.0", 62 | "cypress": "^4.12.1", 63 | "eslint": "^7.32.0", 64 | "eslint-plugin-cypress": "^2.15.2", 65 | "eslint-plugin-import": "^2.30.0", 66 | "eslint-plugin-node": "^11.1.0", 67 | "eslint-plugin-promise": "^4.3.1", 68 | "eslint-plugin-standard": "^4.1.0", 69 | "eslint-plugin-vue": "^6.2.2", 70 | "eslint-plugin-vue-a11y": "0.0.31", 71 | "eslint-plugin-vuejs-accessibility": "^0.3.1", 72 | "gh-pages": "^3.2.3", 73 | "jest": "^26.6.3", 74 | "portal-vue": "^2.1.7", 75 | "rollup": "^2.79.2", 76 | "rollup-plugin-eslint": "^7.0.0", 77 | "rollup-plugin-terser": "^6.1.0", 78 | "rollup-plugin-vue": "^5.1.9", 79 | "standard-version": "^8.0.2", 80 | "vue": "^2.7.16", 81 | "vue-template-compiler": "^2.7.16", 82 | "vuepress": "^1.9.10", 83 | "vuepress-theme-default-vue-a11y": "^0.1.15" 84 | }, 85 | "dependencies": {} 86 | } 87 | -------------------------------------------------------------------------------- /rollup.config.dev.js: -------------------------------------------------------------------------------- 1 | import buble from '@rollup/plugin-buble' 2 | import resolve from '@rollup/plugin-node-resolve' 3 | import chokidar from 'chokidar' 4 | import { eslint } from 'rollup-plugin-eslint' 5 | import vue from 'rollup-plugin-vue' 6 | 7 | export default { 8 | input: 'src/index.js', 9 | watch: { 10 | chokidar, 11 | include: ['src/**'] 12 | }, 13 | plugins: [ 14 | resolve(), 15 | eslint({ 16 | include: './src/**' 17 | }), 18 | vue({ 19 | css: true, 20 | compileTemplate: true 21 | }), 22 | buble() 23 | ], 24 | output: [ 25 | { 26 | name: 'VueFocusLoop', 27 | file: 'demo/vue-focus-loop.js', 28 | format: 'umd', 29 | exports: 'named' 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /rollup.config.prod.js: -------------------------------------------------------------------------------- 1 | import buble from '@rollup/plugin-buble' 2 | import commonjs from '@rollup/plugin-commonjs' 3 | import replace from '@rollup/plugin-replace' 4 | import { terser } from 'rollup-plugin-terser' 5 | import vue from 'rollup-plugin-vue' 6 | 7 | export default commandLineArgs => { 8 | return { 9 | input: 'src/index.js', 10 | plugins: [ 11 | commonjs(), 12 | replace({ 13 | preventAssignment: true, 14 | 'process.env.NODE_ENV': JSON.stringify('production') 15 | }), 16 | vue({ 17 | css: true, 18 | compileTemplate: true, 19 | template: { 20 | isProduction: true, 21 | optimizeSSR: commandLineArgs.format === 'cjs' 22 | } 23 | }), 24 | buble(), 25 | commandLineArgs.format === 'iife' && terser() 26 | ], 27 | output: { 28 | name: 'VueFocusLoop', 29 | exports: 'named' 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/FocusLoop.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 146 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import VueFocusLoop from './plugin' 2 | 3 | export default VueFocusLoop 4 | export { default as FocusLoop } from './FocusLoop.vue' 5 | -------------------------------------------------------------------------------- /src/plugin.js: -------------------------------------------------------------------------------- 1 | import FocusLoop from './FocusLoop.vue' 2 | 3 | export default function install (Vue) { 4 | if (install.installed) return 5 | install.installed = true 6 | Vue.component('FocusLoop', FocusLoop) 7 | } 8 | 9 | // auto install 10 | if (typeof window !== 'undefined' && typeof window.Vue !== 'undefined') { 11 | window.Vue.use(install) 12 | } 13 | -------------------------------------------------------------------------------- /test/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) -------------------------------------------------------------------------------- /test/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | --------------------------------------------------------------------------------