├── .editorconfig
├── .eslintignore
├── .eslintrc.cjs
├── .gitattributes
├── .github
├── CODE_OF_CONDUCT.md
├── FUNDING.yml
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── .gitignore
├── .sasslintrc
├── LICENSE
├── README.md
├── deploy.docs.sh
├── docs
├── .vitepress
│ ├── components
│ │ └── interactive-code
│ │ │ ├── interactive-code.sass
│ │ │ ├── interactive-code.vue
│ │ │ └── transform-code-for-codepen.js
│ ├── config.js
│ └── theme
│ │ ├── custom.sass
│ │ └── index.js
├── examples
│ ├── autofocus-on-activation.vue
│ ├── autofocus-on-mount.vue
│ ├── index.md
│ ├── roving-grid.vue
│ ├── roving-gridcell.vue
│ ├── roving-nested.vue
│ ├── roving-rtl.vue
│ ├── roving-simple.vue
│ ├── trap-escexits.vue
│ ├── trap-escrefocus.vue
│ ├── trap-rtl.vue
│ ├── trap-simple.vue
│ └── use-as-composable.vue
├── guide
│ └── index.md
├── index.md
├── links
│ └── index.md
├── public
│ ├── favicon.ico
│ ├── googlec0cade17e5e27188.html
│ ├── icons
│ │ ├── apple-launch-1125x2436.png
│ │ ├── apple-launch-1170x2532.png
│ │ ├── apple-launch-1242x2208.png
│ │ ├── apple-launch-1242x2688.png
│ │ ├── apple-launch-1284x2778.png
│ │ ├── apple-launch-1536x2048.png
│ │ ├── apple-launch-1620x2160.png
│ │ ├── apple-launch-1668x2224.png
│ │ ├── apple-launch-1668x2388.png
│ │ ├── apple-launch-2048x2732.png
│ │ ├── apple-launch-750x1334.png
│ │ ├── apple-launch-828x1792.png
│ │ ├── favicon-128x128.png
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ └── favicon-96x96.png
│ ├── logo.png
│ └── sitemap.xml
└── vite.config.js
├── gitpkg.config.cjs
├── index.html
├── jsconfig.json
├── package.json
├── pnpm-lock.yaml
├── public
├── favicon.ico
└── icons
│ ├── apple-launch-1125x2436.png
│ ├── apple-launch-1170x2532.png
│ ├── apple-launch-1242x2208.png
│ ├── apple-launch-1242x2688.png
│ ├── apple-launch-1284x2778.png
│ ├── apple-launch-1536x2048.png
│ ├── apple-launch-1620x2160.png
│ ├── apple-launch-1668x2224.png
│ ├── apple-launch-1668x2388.png
│ ├── apple-launch-2048x2732.png
│ ├── apple-launch-750x1334.png
│ ├── apple-launch-828x1792.png
│ ├── favicon-128x128.png
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ └── favicon-96x96.png
├── src
├── App.vue
├── assets
│ └── logo.png
├── directives
│ └── keyboard-trap
│ │ ├── directive.js
│ │ ├── helpers.js
│ │ ├── index.js
│ │ └── options.js
├── exports.js
├── main.js
└── public
│ ├── styles
│ ├── index.css
│ └── index.sass
│ ├── types
│ └── index.d.ts
│ └── web-types
│ └── index.json
├── vite.dev.config.js
└── vite.src.config.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /dist
2 | /node_modules
3 | *.html
4 |
5 | !/docs/.vitepress
6 | /docs/.vitepress/dist
7 | /docs/.vitepress/cache
8 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | browser: true,
5 | es2021: true,
6 | node: true,
7 | 'vue/setup-compiler-macros': true,
8 | },
9 | extends: [
10 | 'plugin:vue/vue3-essential',
11 | 'airbnb-base',
12 | ],
13 | parserOptions: {
14 | ecmaVersion: 'latest',
15 | sourceType: 'module',
16 | },
17 | plugins: [
18 | 'vue',
19 | ],
20 | rules: {
21 | 'import/first': 'off',
22 | 'import/named': 'error',
23 | 'import/namespace': 'error',
24 | 'import/default': 'error',
25 | 'import/export': 'error',
26 | 'import/extensions': 'off',
27 | 'import/no-unresolved': 'off',
28 | 'import/no-extraneous-dependencies': 'off',
29 | 'import/no-dynamic-require': 'off',
30 | 'import/prefer-default-export': 'off',
31 | 'prefer-promise-reject-errors': 'off',
32 |
33 | // allow console.log during development only
34 | 'no-console': process.env.NODE_ENV === 'production' ? ['error', { allow: ['info', 'warn', 'error'] }] : 'off',
35 | // allow debugger during development only
36 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
37 |
38 | 'linebreak-style': 'off',
39 | 'arrow-parens': ['error', 'always'],
40 | 'max-len': 'off',
41 | 'template-curly-spacing': ['error', 'always'],
42 | 'no-underscore-dangle': 'off',
43 | 'no-var': 'error',
44 | 'no-param-reassign': ['error', { props: false }],
45 | indent: 'off',
46 | 'indent-legacy': ['error', 2, {
47 | SwitchCase: 1,
48 | }],
49 | 'vue/script-indent': ['error', 2, {
50 | baseIndent: 0,
51 | switchCase: 1,
52 | ignores: [],
53 | }],
54 | 'vue/max-attributes-per-line': ['error', {
55 | singleline: 3,
56 | multiline: 1,
57 | }],
58 | },
59 | };
60 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 |
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Code of Conduct
2 |
3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4 |
5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.
6 |
7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
8 |
9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
10 |
11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
12 |
13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
14 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [pdanpdan]
4 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: "[BUG]:"
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # TypeScript v1 declaration files
46 | typings/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Microbundle cache
58 | .rpt2_cache/
59 | .rts2_cache_cjs/
60 | .rts2_cache_es/
61 | .rts2_cache_umd/
62 |
63 | # Optional REPL history
64 | .node_repl_history
65 |
66 | # Output of 'npm pack'
67 | *.tgz
68 |
69 | # Yarn Integrity file
70 | .yarn-integrity
71 |
72 | # dotenv environment variables file
73 | .env
74 | .env.test
75 |
76 | # parcel-bundler cache (https://parceljs.org/)
77 | .cache
78 |
79 | # Next.js build output
80 | .next
81 |
82 | # Nuxt.js build / generate output
83 | .nuxt
84 | dist
85 | dist-ssr
86 |
87 | # Gatsby files
88 | .cache/
89 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
90 | # https://nextjs.org/blog/next-9-1#public-directory-support
91 | # public
92 |
93 | # vuepress build output
94 | .vuepress/dist
95 |
96 | # vitepress build output
97 | docs/.vitepress/dist
98 | docs/.vitepress/cache
99 |
100 | # Serverless directories
101 | .serverless/
102 |
103 | # FuseBox cache
104 | .fusebox/
105 |
106 | # DynamoDB Local files
107 | .dynamodb/
108 |
109 | # TernJS port file
110 | .tern-port
111 |
112 | *.local
113 |
114 | .DS_STORE
115 | .temp
116 |
--------------------------------------------------------------------------------
/.sasslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "extends-before-mixins": 2,
4 | "extends-before-declarations": 2,
5 | "placeholder-in-extend": 2,
6 | "mixins-before-declarations": [
7 | 2,
8 | {
9 | "exclude": [
10 | "breakpoint",
11 | "mq"
12 | ]
13 | }
14 | ],
15 | "no-warn": 1,
16 | "no-debug": 1,
17 | "no-ids": 0,
18 | "no-important": 0,
19 | "hex-notation": 0,
20 | "indentation": [
21 | 2,
22 | {
23 | "size": 2
24 | }
25 | ],
26 | "class-name-format": 0,
27 | "no-color-literals": 0,
28 | "empty-line-between-blocks": 0,
29 | "single-line-per-selector": 1,
30 | "force-element-nesting": 0,
31 | "property-sort-order": 0,
32 | "variable-for-property": 0,
33 | "leading-zero": 0
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022-present Popescu Dan
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 | # VueKeyboardTrap (vue-keyboard-trap)
2 |
3 | [](https://opensource.org/licenses/MIT)
4 | [](https://bundlephobia.com/result?p=@pdanpdan/vue-keyboard-trap)
5 | 
6 | 
7 | 
8 | 
9 |
10 | ## Project description
11 |
12 | Vue directive and composable for keyboard navigation - roving movement and trapping inside container.
13 |
14 | Works both for Vue3 and Vue2, as a directive (`v-kbd-trap`) or as a composable (`useKeyboardTrap`).
15 |
16 | [Demo codepen](https://codepen.io/pdanpdan/pen/MWrzLdM)
17 |
18 | [Docs and examples](https://pdanpdan.github.io/vue-keyboard-trap/)
19 |
20 | [Source code, Issues, Discussions](https://github.com/pdanpdan/vue-keyboard-trap)
21 |
22 | ## Install
23 |
24 | ```bash
25 | pnpm add @pdanpdan/vue-keyboard-trap
26 | ```
27 | or
28 | ```bash
29 | yarn add @pdanpdan/vue-keyboard-trap
30 | ```
31 | or
32 | ```bash
33 | npm install @pdanpdan/vue-keyboard-trap
34 | ```
35 |
36 | ## Playground
37 |
38 | [Demo codepen](https://codepen.io/pdanpdan/pen/MWrzLdM)
39 |
40 | ## Usage
41 |
42 | ### Usage as ESM
43 |
44 | #### As composable (both Vue3 and Vue2)
45 |
46 | ```html
47 |
68 |
69 |
70 |
71 | ...
72 |
73 |
74 | ```
75 |
76 | #### As plugin on Vue3 - directive
77 |
78 | ```javascript
79 | import { createApp } from 'vue';
80 | import { VueKeyboardTrapDirectivePlugin } from '@pdanpdan/vue-keyboard-trap';
81 | import App from './App.vue';
82 |
83 | const app = createApp(App);
84 |
85 | app.use(VueKeyboardTrapDirectivePlugin, {
86 | // ...options if required
87 | });
88 |
89 | app.mount('#app');
90 | ```
91 |
92 | #### As plugin on Vue2 - directive
93 |
94 | ```javascript
95 | import Vue from 'vue';
96 | import { VueKeyboardTrapDirectivePlugin } from '@pdanpdan/vue-keyboard-trap';
97 | import App from './App.vue';
98 |
99 | Vue.use(VueKeyboardTrapDirectivePlugin, {
100 | // ...options if required
101 | });
102 |
103 | new Vue({
104 | el: '#app',
105 | });
106 | ```
107 |
108 | #### Included in specific components (Vue3 script setup) - directive
109 |
110 | ```html
111 |
118 | ```
119 |
120 | #### Included in specific components (Vue3 script) - directive
121 |
122 | ```html
123 |
137 | ```
138 |
139 | #### Included in specific components (Vue2) - directive
140 |
141 | ```html
142 |
155 | ```
156 |
157 | #### User hint styles (cosmetic)
158 |
159 | The directive does not require any CSS styles to work, but for cosmetic purposes (as user hints) some example styles are provided in `dist/styles/index.sass`.
160 |
161 | in Javascript
162 | ```javascript
163 | import '@pdanpdan/vue-keyboard-trap/styles';
164 | ```
165 |
166 | or in SASS
167 | ```sass
168 | @import '@pdanpdan/vue-keyboard-trap/styles'
169 | ```
170 |
171 | or (if the `/styles` export is not used by your bundler)
172 |
173 | in Javascript
174 | ```javascript
175 | import '@pdanpdan/vue-keyboard-trap/dist/styles/index.sass';
176 | ```
177 |
178 | or in SASS
179 | ```sass
180 | @import '@pdanpdan/vue-keyboard-trap/dist/styles/index.sass'
181 | ```
182 |
183 | ### Usage as UMD
184 |
185 | Load the javascript from [https://cdn.jsdelivr.net/gh/pdanpdan/vue-keyboard-trap/dist/index.umd.js](https://cdn.jsdelivr.net/gh/pdanpdan/vue-keyboard-trap/dist/index.umd.js).
186 |
187 | It will expose a global object `VueKeyboardTrap` with `VueKeyboardTrapDirectivePlugin` and `VueKeyboardTrapDirectiveFactory` keys.
188 |
189 | In order to work it requires that `VueDemi` is already loaded on the page. You can do it like this:
190 |
191 | ```html
192 |
193 |
194 |
195 |
196 | ```
197 |
198 | #### As composable (both Vue3 and Vue2)
199 |
200 | ```javascript
201 | const { ref } = Vue;
202 |
203 | const { useKeyboardTrapFactory } = VueKeyboardTrap;
204 | const useKeyboardTrap = useKeyboardTrapFactory({
205 | // ...options if required
206 | });
207 |
208 | const elRef = ref(null);
209 | useKeyboardTrap(
210 | // element (reactive)
211 | elRef,
212 | // modifiers (optional, reactive, default all modifiers are false)
213 | {
214 | roving: true,
215 | },
216 | // active (optional, reactive, default true)
217 | true
218 | );
219 | ```
220 |
221 | #### As plugin on Vue3 - directive
222 |
223 | ```javascript
224 | const { createApp } = Vue;
225 | const { VueKeyboardTrapDirectivePlugin } = VueKeyboardTrap;
226 |
227 | const app = createApp({});
228 |
229 | app.use(VueKeyboardTrapDirectivePlugin, {
230 | // ...options if required
231 | });
232 |
233 | app.mount('#app');
234 | ```
235 |
236 | #### As plugin on Vue2 - directive
237 |
238 | ```javascript
239 | const { VueKeyboardTrapDirectivePlugin } = VueKeyboardTrap;
240 |
241 | Vue.use(VueKeyboardTrapDirectivePlugin, {
242 | // ...options if required
243 | });
244 |
245 | new Vue({
246 | el: '#app',
247 | });
248 | ```
249 |
250 | #### As directive on Vue3 - directive
251 |
252 | ```javascript
253 | const { createApp } = Vue;
254 | const { VueKeyboardTrapDirectiveFactory } = VueKeyboardTrap;
255 |
256 | const app = createApp({});
257 |
258 | const { name, directive } = VueKeyboardTrapDirectiveFactory({
259 | // ...options if required
260 | });
261 |
262 | app.directive(name, directive);
263 |
264 | app.mount('#app');
265 | ```
266 |
267 | #### As directive on Vue2 - directive
268 |
269 | ```javascript
270 | const { VueKeyboardTrapDirectiveFactory } = VueKeyboardTrap;
271 |
272 | const { name, directive } = VueKeyboardTrapDirectiveFactory({
273 | // ...options if required
274 | });
275 |
276 | Vue.directive(name, directive);
277 | ```
278 |
279 | #### User hint styles (cosmetic)
280 |
281 | If you want you can access the CSS cosmetic style (user hints) from [https://cdn.jsdelivr.net/gh/pdanpdan/vue-keyboard-trap/dist/styles/index.css](https://cdn.jsdelivr.net/gh/pdanpdan/vue-keyboard-trap/dist/styles/index.css).
282 |
283 | ### Directive configuration options
284 |
285 | | Option | Description | Default |
286 | |--------|-------------|:-------:|
287 | | `name` | snake-case name of the directive (without `v-` prefix) | `kbd-trap` |
288 | | `datasetName` | camelCase name of the `data-attribute` to be set on element when trap is enabled | `v${PascalCase from name}` |
289 | | `focusableSelector` | CSS selector for focusable elements | [see here](#default-focusableselector) |
290 | | `rovingSkipSelector` | CSS selector for elements that should not respond to roving key navigation (input, textarea, ...) | [see here](#default-rovingskipselector) |
291 | | `gridSkipSelector` | CSS selector that will be applied in .roving.grid mode to exclude elements - must be a series of `:not()` selectors | [see here](#default-gridskipselector) |
292 | | `autofocusSelector` | CSS selector for the elements that should be autofocused | [see here](#default-autofocusselector) |
293 | | `trapTabIndex` | tabIndex value to be used when trap element has a tabIndex of -1 and has no `tabindex` attribute | -9999 |
294 |
295 | #### Default `focusableSelector`:
296 |
297 | ```css
298 | :focus,
299 | a[href]:not([tabindex^="-"]),
300 | area[href]:not([tabindex^="-"]),
301 | video[controls]:not([tabindex^="-"]),
302 | audio[controls]:not([tabindex^="-"]),
303 | iframe:not([tabindex^="-"]),
304 | [tabindex]:not(slot):not([tabindex^="-"]),
305 | [contenteditable]:not([contenteditable="false"]):not([tabindex^="-"]),
306 | details > summary:first-of-type:not([tabindex^="-"]),
307 | input:not([type="hidden"]):not(fieldset[disabled] input):not([disabled]):not([tabindex^="-"]),
308 | select:not(fieldset[disabled] input):not([disabled]):not([tabindex^="-"]),
309 | textarea:not(fieldset[disabled] input):not([disabled]):not([tabindex^="-"]),
310 | button:not(fieldset[disabled] input):not([disabled]):not([tabindex^="-"]),
311 | fieldset[disabled]:not(fieldset[disabled] fieldset) > legend input:not([type="hidden"]):not([disabled]):not([tabindex^="-"]),
312 | fieldset[disabled]:not(fieldset[disabled] fieldset) > legend select:not([disabled]):not([tabindex^="-"]),
313 | fieldset[disabled]:not(fieldset[disabled] fieldset) > legend textarea:not([disabled]):not([tabindex^="-"]),
314 | fieldset[disabled]:not(fieldset[disabled] fieldset) > legend button:not([disabled]):not([tabindex^="-"]),
315 | [class*="focusable"]:not([disabled]):not([tabindex^="-"])
316 | ```
317 |
318 | By default `a` tags without href are not focusable - add a `tabindex="0"` attribute on them to make them focusable.
319 | This can be done for all other elements if you want them to be focusable.
320 |
321 | #### Default `rovingSkipSelector`:
322 |
323 | ```css
324 | input:not([disabled]):not([type="button"]):not([type="checkbox"]):not([type="file"]):not([type="image"]):not([type="radio"]):not([type="reset"]):not([type="submit"]),
325 | select:not([disabled]),
326 | select:not([disabled]) *,
327 | textarea:not([disabled]),
328 | [contenteditable]:not([contenteditable="false"]),
329 | [contenteditable]:not([contenteditable="false"]) *
330 | ```
331 |
332 | #### Default `gridSkipSelector`:
333 |
334 | ```css
335 | :not([disabled]),
336 | :not([tabindex^="-"])
337 | ```
338 |
339 | #### Default `autofocusSelector`:
340 |
341 | ```css
342 | [autofocus]:not([disabled]):not([autofocus="false"]),
343 | [data-autofocus]:not([disabled]):not([data-autofocus="false"])
344 | ```
345 |
346 | ### Dynamic enable/disable
347 |
348 | Use the value of the directive (boolean) to enable/disable it.
349 |
350 | ```html
351 |
352 | ```
353 |
354 | The modifiers are reactive so if you use render functions you can dynamically change the behaviour.
355 |
356 | ### Directive modifiers
357 |
358 | | Modifier | Description |
359 | |----------|-------------|
360 | | `.autofocus` | autofocuses the first element that matches [autofocusSelector](#default-autofocusselector) or (if no such element is found) the first focusable child element **when the directive is mounted or enabled** (**only if it not covered by another element**) |
361 | | `.roving` or `.roving.vertical.horizontal` | allow roving navigation (`Home`, `End`, `ArrowKeys`) |
362 | | `.roving.vertical` | allow roving navigation (`Home`, `End`, `ArrowUp`, `ArrowDown`) |
363 | | `.roving.horizontal` | allow roving navigation (`Home`, `End`, `ArrowLeft`, `ArrowRight`) |
364 | | `.roving.grid` | allow roving navigation (`Home`, `End`, `ArrowKeys`) using dataset attrs on elements `[data-${camelCase from datasetName}-(row/col)]`; `[data-${camelCase from datasetName}-(row/col)~="*"]` is a catchall |
365 | | `.roving` used on an element with `[role="grid"]` | allow roving navigation (`Home`, `End`, `ArrowKeys`) using role attrs on elements `[role="row/gridcell"]` |
366 | | `.roving.tabinside` | `Tab` key navigates to next/prev element inside trap (by default `Tab` key navigates to next/prev element outside trap in roving mode) |
367 | | `.escrefocus` | refocus element that was in focus before activating the trap on `Esc` |
368 | | `.escexits` | refocus a parent trap on `Esc` (has priority over `.escrefocus`) |
369 | | `.indexorder` used without `.grid` modifier and on elements without `[role="grid"]` | force usage of order in `tabindex` (`tabindex` in ascending order and then DOM order) |
370 |
371 | ## Keyboard navigation
372 |
373 | - `TAB` / `SHIFT`+`TAB` key
374 | - moves to next / previous focusable element inside the trap group (moves from last one to first one or from first one to last one when no more focusable elements are available in the group)
375 | - if `.roving` modifier is used moves to next / previous trap group or focusable element outside the current trap group
376 | - if `.roving.tabinside` modifiers are used then move inside the trap group
377 | - if `.indexorder` modifier is used without `.grid` and on elements without `[role="grid"]` - the order of tabindex will be used
378 | - `ESC` key
379 | - disables / enables the current tab group
380 | - if `.escexits` modifier is used then refocus the last active focusable element in a parent trap group
381 | - if `.escrefocus` modifier is used then refocus the last focusable element that was active before the current trap group got focus
382 | - if `.escexits` or `.escrefocus` are used then press `SHIFT + ESC` to disable / enable the current tab group
383 | - `HOME` / `END` when `.roving` modifier is used
384 | - move to first / last focusable element in the current trap group
385 | - `ARROW_KEYS` when `.roving` modifier is used (`.roving.horizontal.vertical` is the same as `.roving`)
386 | - if only `.horizontal` modifier is used then only `ARROW_LEFT` / `ARROW_RIGHT` keys can be used
387 | - if only `.vertical` modifier is used then only `ARROW_UP` / `ARROW_DOWN` keys can be used
388 | - `ARROW_LEFT` / `ARROW_UP` move to the previous focusable element inside the trap group
389 | - `ARROW_RIGHT` / `ARROW_DOWN` move to the next focusable element inside the trap group
390 | - if `.indexorder` modifier is used without `.grid` and on elements without `[role="grid"]` - the order of tabindex will be used
391 | - `ARROW_KEYS` when `.roving.grid` modifiers are used or `.roving` modifier on a trap element with [role="grid"]
392 | - move in the grid inside the current trap group
393 |
394 | ### Keyboard navigation inside `.roving.grid` trap groups
395 |
396 | In order to specify the navigation pattern you must use 2 dataset attributes on the focusable elements inside the `.roving` trap group:
397 |
398 | - `data-v-kbd-trap-row` specifies the numeric identifier of the row the element belongs to (numbers need not be consecutive, but their natural order determines the navigation order)
399 | - `data-v-kbd-trap-col` specifies the numeric identifier of the column the element belongs to (numbers need not be consecutive, but their natural order determines the navigation order)
400 |
401 | Any or both attributes can have a value of `*` that means that it is an alement that can be focused from elements having any coresponding (row or col) attribute.
402 |
403 | #### Navigation rules
404 |
405 | - the first focusable element on the row / col (based on direction of movement) is focused
406 | - an element with `*` for row or col is considered to belong to any row / col
407 |
408 | ### Keyboard navigation inside `.roving` trap groups with `[role="grid"]`
409 |
410 | In order to specify the navigation pattern you must use role attributes `[role="row"]` and `[role="gridcell"]`.
411 |
412 | All focusable element must have `[role="gridcell"]` and must be inside `[role="row"]` elements inside `[role="grid"]` trap element.
413 |
414 | The `gridcell`s will be considered inline-start aligned in every row.
415 |
416 | #### Navigation rules
417 |
418 | - the first focusable element on the row / col (based on direction of movement) is focused
419 |
420 | ### RTL / LTR
421 |
422 | The directive checks the closest parent DOM Element of the active element that has a `[dir="rtl"]` or `[dir="ltr`]` attribute.
423 |
424 | If the direction is RTL the `ARROW_LEFT` and `ARROW_RIGHT` keys move in reverse (according to document order of the focusable elements) but consistent to the way the elements are ordered on screen.
425 |
426 | ## CSS (visual hints for users)
427 |
428 | The directive does not require any styles, but it might help the users to have visual hints for navigation.
429 |
430 | A default style is provided as SASS in `dist/styles/index.sass` (can be imported as `import '@pdapdan/vue-keyboard-trap/styles'`, as `import '@pdapdan/vue-keyboard-trap/dist/styles/index.sass'` (if the bundler does not use the `/styles` export) or included from [https://cdn.jsdelivr.net/gh/pdanpdan/vue-keyboard-trap/dist/styles/index.sass](https://cdn.jsdelivr.net/gh/pdanpdan/vue-keyboard-trap/dist/styles/index.sass)).
431 |
432 | The default style is also provided as CSS in `dist/styles/index.css` (can be imported as `import '@pdapdan/vue-keyboard-trap/dist/styles/index.css'` or included from [https://cdn.jsdelivr.net/gh/pdanpdan/vue-keyboard-trap/dist/styles/index.css](https://cdn.jsdelivr.net/gh/pdanpdan/vue-keyboard-trap/dist/styles/index.css)).
433 |
434 | There are some CSS variables that can be used to customize the aspect of the hints:
435 |
436 | | Variable | Role | Default |
437 | |----------|------|:-------:|
438 | | `--color-v-kbd-trap-enabled` | the text color when directive is enabled | `#c33`
■ |
439 | | `--color-v-kbd-trap-disabled` | the text color when directive is disabled | `#999`
■ |
440 | | `--color-v-kbd-trap-background` | the background color of the hint area | `#eeee`
■ |
441 | | `--text-v-kbd-trap-separator` | separator between elements | `/` |
442 | | `--text-v-kbd-trap-enabled` | indicator for enabled but not active trap | `Trap` |
443 | | `--text-v-kbd-trap-esc` | indicator for `Esc` key active | `Esc` |
444 | | `--text-v-kbd-trap-esc-refocus` | indicator for `Esc` key active when it refocuses | `Esc\2949` / `Esc⥉` |
445 | | `--text-v-kbd-trap-esc-exits` | indicator for `Esc` key active when it exits trap | `Esc\2923` / `Esc⤣` |
446 | | `--text-v-kbd-trap-tab` | indicator for `Tab` key active inside trap | `Tab` |
447 | | `--text-v-kbd-trap-tab-exits` | indicator for `Tab` key active when it exits trap | `Tab\21C5` / `Tab⇅` |
448 | | `--text-v-kbd-trap-grid` | indicator for grid mode active | `\229E` / `⊞` |
449 | | `--text-v-kbd-trap-arrows-all` | indicator for move keys active in roving mode | `\2962\2963\2965\2964` / `⥢⥣⥥⥤` |
450 | | `--text-v-kbd-trap-arrows-horizontal` | indicator for move keys active in roving mode horizontal | `\2962\2964` / `⥢⥤` |
451 | | `--text-v-kbd-trap-arrows-vertical` | indicator for move keys active in roving mode vertical | `\2963\2965` / `⥣⥥` |
452 |
453 | In the default style the hint is positioned on the top-right corner of the trap group.
454 |
455 | <<< @/../src/public/styles/index.sass
456 |
457 | ## Development
458 |
459 | ### Install the dependencies
460 |
461 | ```bash
462 | pnpm i
463 | ```
464 |
465 | ### Start development mode (hot-code reloading, error reporting, etc.)
466 |
467 | ```bash
468 | pnpm dev
469 | ```
470 |
471 | ### Lint the files
472 |
473 | ```bash
474 | pnpm lint
475 | ```
476 |
477 | ### Build for production
478 |
479 | ```bash
480 | pnpm build
481 | ```
482 |
483 | ## Source code, issues, bug reports, feature requests
484 |
485 | [Vue Keyboard Trap (vue-keyboard-trap)](https://github.com/pdanpdan/vue-keyboard-trap)
486 |
487 | ## Author
488 |
489 | * Name: Dan Popescu (PDan)
490 | * Email: [pdan.popescu@gmail.com](mailto:pdan.popescu@gmail.com)
491 | * Website: https://github.com/pdanpdan/
492 | * Github: [@pdanpdan](https://github.com/pdanpdan)
493 |
494 | ## License
495 |
496 | Copyright © 2022-present [Dan Popescu](https://github.com/pdanpdan).
497 |
498 | This application is distributed under [](https://opensource.org/licenses/MIT), see LICENSE for more information.
499 |
--------------------------------------------------------------------------------
/deploy.docs.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | set -e
3 |
4 | pnpm docs:build
5 |
6 | cd docs/.vitepress/dist
7 |
8 | git init
9 | git add -A
10 | git commit --allow-empty -m 'deploy'
11 | git push -f git@github.com:pdanpdan/vue-keyboard-trap.git main:gh-pages
12 |
13 | cd -
14 |
--------------------------------------------------------------------------------
/docs/.vitepress/components/interactive-code/interactive-code.sass:
--------------------------------------------------------------------------------
1 | $interactiveCodeBorderRadius: 6px !default
2 |
3 | .interactive-code
4 | color: var(--vp-c-text-code)
5 | background-color: var(--vp-c-bg)
6 | border: 1px solid var(--vp-c-divider)
7 | border-radius: var(--interactive-code-border-radius, $interactiveCodeBorderRadius)
8 | margin-block: 16px
9 | overflow: hidden
10 |
11 | &__header
12 | background-color: var(--vp-c-bg-alt)
13 | border-bottom: 1px dashed var(--vp-c-divider)
14 | padding: .5em 1em
15 |
16 | &-title
17 | font-size: 1.1em
18 | font-weight: bold
19 | color: var(--vp-c-text-1)
20 |
21 | &-desc
22 | font-size: .9em
23 | font-weight: light
24 | color: var(--vp-c-text-2)
25 |
26 | &__component
27 | padding: 1em
28 |
29 | &__source
30 | > div
31 | margin: 0 !important
32 | border-radius: 0 !important
33 |
34 | &__actions
35 | background-color: var(--vp-c-bg-alt)
36 | border-top: 1px dashed var(--vp-c-divider)
37 | padding: .35em
38 | display: flex
39 | flex-wrap: wrap
40 | max-width: 100%
41 | align-items: center
42 |
43 | > .spacer
44 | display: inline-block
45 | flex: 1 1 auto
46 |
47 | > .separator
48 | display: inline-block
49 | align-self: stretch
50 | width: 1px
51 | background-color: var(--vp-c-divider-light)
52 | margin-inline: .5em
53 | margin-block: .25em
54 |
55 | > button
56 | cursor: pointer
57 | color: var(--vp-c-text-2)
58 | background-color: var(--vp-button-alt-bg)
59 | border: 1px solid var(--vp-button-alt-border)
60 | padding: .3em .6em
61 | min-height: 36px
62 | border-radius: .2em
63 | outline: none
64 | font-size: 1em
65 | font-weight: 500
66 |
67 | &:active,
68 | &:focus-visible
69 | background-color: var(--vp-button-alt-active-bg)
70 | border: 1px solid var(--vp-button-alt-active-border)
71 | &:focus-visible
72 | outline: auto
73 | outline-offset: 1px
74 | &:hover
75 | background-color: var(--vp-button-alt-hover-bg)
76 | border: 1px solid var(--vp-button-alt-hover-border)
77 |
78 | &__action
79 | &--icon
80 | min-width: 36px
81 | background-position: 50%
82 | background-size: 28px
83 | background-repeat: no-repeat
84 |
85 | &--source
86 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='rgba(128,128,128,1)' height='20' width='20' stroke='rgba(128,128,128,1)' viewBox='0 0 32 32'%3E%3Cpath d='M14.194,27a1.2,1.2,0,0,1-.276-.033,1.388,1.388,0,0,1-.968-1.618L16.562,6.105a1.383,1.383,0,0,1,.592-.91,1.192,1.192,0,0,1,.928-.163,1.39,1.39,0,0,1,.969,1.617L15.436,25.9a1.378,1.378,0,0,1-.59.908A1.2,1.2,0,0,1,14.194,27Z'/%3E%3Cpath d='M21.437,24.273l-.091,0a1.242,1.242,0,0,1-.891-.5,1.461,1.461,0,0,1,.136-1.893L26.8,15.807l-6.185-5.652a1.462,1.462,0,0,1-.187-1.888,1.25,1.25,0,0,1,.881-.533,1.2,1.2,0,0,1,.945.316l7.294,6.668a1.463,1.463,0,0,1,.191,1.889,1.415,1.415,0,0,1-.189.218l-7.265,7.1A1.222,1.222,0,0,1,21.437,24.273Z'/%3E%3Cpath d='M10.563,24.277a1.219,1.219,0,0,1-.852-.355l-7.271-7.1a1.2,1.2,0,0,1-.182-.21,1.459,1.459,0,0,1,.188-1.886l7.3-6.67a1.175,1.175,0,0,1,.938-.317,1.254,1.254,0,0,1,.887.53,1.462,1.462,0,0,1-.187,1.89L5.2,15.809l6.212,6.069a1.457,1.457,0,0,1,.133,1.893,1.235,1.235,0,0,1-.893.5C10.622,24.275,10.593,24.277,10.563,24.277Z'/%3E%3C/svg%3E")
87 |
88 | &--copy
89 | position: relative
90 | background-image: var(--vp-icon-copy)
91 |
92 | &--copied
93 | background-image: var(--vp-icon-copied)
94 |
95 | &::before
96 | content: 'Copied'
97 | position: absolute
98 | top: -1px
99 | bottom: -1px
100 | left: -5em
101 | background-color: var(--vp-button-alt-bg)
102 | border: 1px solid var(--vp-button-alt-border)
103 | padding: .35em .6em
104 | border-radius: .2em
105 |
--------------------------------------------------------------------------------
/docs/.vitepress/components/interactive-code/interactive-code.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
14 |
15 |
16 | Open in CodePen
17 |
18 |
19 |
20 |
27 |
28 |
29 |
30 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
55 |
56 |
57 |
58 |
59 |
132 |
--------------------------------------------------------------------------------
/docs/.vitepress/components/interactive-code/transform-code-for-codepen.js:
--------------------------------------------------------------------------------
1 | const reHtml = /(?:^|[\n\r]+)
]*?(?:\s+lang="([^"]+)")?[^>]*?>[\r\n]*(.*?)[\r\n]*<\/template>/is;
2 | const reCss = /(?:^|[\n\r]+)
70 |
--------------------------------------------------------------------------------
/docs/examples/autofocus-on-mount.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ showContent === true ? 'Hide content' : 'Show content' }}
5 |
6 |
7 |
First
8 |
Second
9 |
Autofocus
10 |
11 |
12 |
13 |
14 |
39 |
40 |
70 |
--------------------------------------------------------------------------------
/docs/examples/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Examples
3 | ---
4 |
5 | ## Playground with all usage patterns
6 |
7 | [Demo codepen](https://codepen.io/pdanpdan/pen/MWrzLdM)
8 |
9 | ## Basic usage as directive
10 |
11 |
12 |
13 | <<< @/examples/trap-simple.vue{3,14-15,18-20}
14 |
15 |
16 |
17 |
18 |
19 | <<< @/examples/trap-rtl.vue{6-7,15,23,38-39,42-44}
20 |
21 |
22 |
23 | ## Basic usage as composable
24 |
25 |
26 |
27 | <<< @/examples/use-as-composable.vue{7,19-20,24,35}
28 |
29 |
30 |
31 | ## Modifiers
32 |
33 | ### `.autofocus`
34 |
35 | Focuses the first child element that matches `autofocusSelector` or (if no such element is found) the first focusable child element.
36 |
37 | Is only triggered on mount or on directive activation (changed value from false to true) and only if it not covered by another element.
38 |
39 | To check if an element is covered it will be scrolled into view (`el.scrollIntoView()`) so if you have fixed (or sticky) headers or footers set `scroll-padding` on `html` in CSS so that the elements scroll in a visible area.
40 |
41 |
42 |
43 | <<< @/examples/autofocus-on-mount.vue{6,9,18-19,22-24}
44 |
45 |
46 |
47 |
48 |
49 | <<< @/examples/autofocus-on-activation.vue{6,9,18-19,22-24}
50 |
51 |
52 |
53 | ### `.roving`
54 |
55 | Simplify `TAB` key navigation in large applications where lots of elements are focusable.
56 |
57 | `TAB` / `SHIFT` + `TAB` key navigates between trap groups, while navigation inside the `.roving` trap group is done using `ARROW_KEYS`.
58 |
59 | The last focusable element of each `.roving` trap group is remembered and refocused when the trap group is focused.
60 |
61 |
62 |
63 | <<< @/examples/roving-simple.vue{6,17,35-36,39-41}
64 |
65 |
66 |
67 |
68 |
69 | <<< @/examples/roving-nested.vue{6,16,44-45,48-50}
70 |
71 |
72 |
73 |
74 |
75 | <<< @/examples/roving-rtl.vue{6-7,15,23,38-39,42-44}
76 |
77 |
78 |
79 | ### `.roving.grid`
80 |
81 | In order to specify the navigation pattern you must use 2 dataset attributes on the focusable elements inside the `.roving` trap group:
82 |
83 | - `data-v-kbd-trap-row` specifies the numeric identifier of the row the element belongs to (numbers need not be consecutive, but their natural order determines the navigation order)
84 | - `data-v-kbd-trap-col` specifies the numeric identifier of the column the element belongs to (numbers need not be consecutive, but their natural order determines the navigation order)
85 |
86 | Any or both attributes can have a value of `*` that means that it is an alement that can be focused from elements having any coresponding (row or col) attribute.
87 |
88 | #### Navigation rules
89 |
90 | - the first focusable element on the row / col (based on direction of movement) is focused
91 | - an element with `*` for row or col is considered to belong to any row / col
92 |
93 |
94 |
95 | <<< @/examples/roving-grid.vue{6,12-13,31-32,35-37}
96 |
97 |
98 |
99 | ### `.roving` on `[role="grid"]` trap group element
100 |
101 | In order to specify the navigation pattern you must use role attributes `[role="row"]` and `[role="gridcell"]`.
102 |
103 | All focusable element must have `[role="gridcell"]` and must be inside `[role="row"]` elements inside `[role="grid"]` trap element.
104 |
105 | The `gridcell`s will be considered inline-start aligned in every row.
106 |
107 | #### Navigation rules
108 |
109 | - the first focusable element on the row / col (based on direction of movement) is focused
110 |
111 |
112 |
113 | <<< @/examples/roving-gridcell.vue{9,11,29,31-37,40,45,60-61,64-66}
114 |
115 |
116 |
117 | ### `.escrefocus`
118 |
119 | When pressing `Esc` key disable the active trap and refocus the element that was in focus before activating the trap.
120 |
121 | Press `Shift + Esc` to disable / enable the current tab group.
122 |
123 |
124 |
125 | <<< @/examples/trap-escrefocus.vue{3,8,21,41-42,45-47}
126 |
127 |
128 |
129 | ### `.escexits`
130 |
131 | When pressing `Esc` key disable the active trap and move focus in the parent trap (if it exists).
132 |
133 | Press `Shift + Esc` to disable / enable the current tab group.
134 |
135 | Has priority over `.escrefocus`.
136 |
137 |
138 |
139 | <<< @/examples/trap-escexits.vue{3,8,21,41-42,45-47}
140 |
141 |
142 |
--------------------------------------------------------------------------------
/docs/examples/roving-grid.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Before
5 |
6 |
7 |
8 |
19 | R:{{ i + 1 }}/C:{{ j + 1 }}
20 |
21 |
22 |
23 |
24 |
After
25 |
26 |
27 |
28 |
40 |
41 |
70 |
--------------------------------------------------------------------------------
/docs/examples/roving-gridcell.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Before
5 |
6 |
7 |
13 |
14 |
15 |
16 |
17 | Wk.
18 | Su
19 | Mo
20 | Tu
21 | We
22 | Th
23 | Fr
24 | Sa
25 |
26 |
27 |
28 |
29 |
30 | 13
31 | 27
32 | 28
33 | 29
34 | 30
35 | 31
36 | 1
37 | 2
38 |
39 |
40 |
41 | {{ 13 + m}}
42 | {{ 2 + (m - 1) * 7 + d }}
48 |
49 |
50 |
51 |
52 |
53 |
After
54 |
55 |
56 |
57 |
69 |
70 |
123 |
--------------------------------------------------------------------------------
/docs/examples/roving-nested.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Before
5 |
6 |
7 |
13 | Group 1 / {{ i }}
14 |
15 |
16 |
17 |
23 | Group 2 / {{ i }}
24 |
25 |
26 |
27 |
33 | Group 1 / {{ i + 2 }}
34 |
35 |
36 |
37 |
After
38 |
39 |
40 |
41 |
53 |
54 |
83 |
--------------------------------------------------------------------------------
/docs/examples/roving-rtl.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ rtl === true ? 'RTL - switch to LTR' : 'LTR - switch to RTL' }}
5 |
6 |
7 |
8 |
RTL Always
9 |
10 |
1
11 |
2
12 |
3
13 |
14 |
15 |
16 |
LTR Always
17 |
18 |
1
19 |
2
20 |
3
21 |
22 |
23 |
24 |
{{ rtl === true ? 'RTL' : 'LTR' }}
25 |
26 |
1
27 |
2
28 |
3
29 |
30 |
31 |
32 |
33 |
34 |
59 |
60 |
103 |
--------------------------------------------------------------------------------
/docs/examples/roving-simple.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Before
5 |
6 |
7 |
13 | Group 1 / {{ i }}
14 |
15 |
16 |
17 |
18 |
24 | Group 2 / {{ i }}
25 |
26 |
27 |
28 |
After
29 |
30 |
31 |
32 |
44 |
45 |
74 |
--------------------------------------------------------------------------------
/docs/examples/trap-escexits.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Outside trap
5 |
6 |
Before
7 |
8 |
9 |
Inside trap 1 - clik one element inside, then use Esc key
10 |
11 |
17 | Group 1 / {{ i }}
18 |
19 |
20 |
21 |
22 |
Inside trap 2 - click one element inside, then use Esc key
23 |
24 |
30 | Group 2 / {{ i }}
31 |
32 |
33 |
34 |
After
35 |
36 |
37 |
38 |
50 |
51 |
82 |
--------------------------------------------------------------------------------
/docs/examples/trap-escrefocus.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Outside trap
5 |
6 |
Before
7 |
8 |
9 |
Inside trap 1 - clik one element inside, then use Esc key
10 |
11 |
17 | Group 1 / {{ i }}
18 |
19 |
20 |
21 |
22 |
Inside trap 2 - click one element inside, then use Esc key
23 |
24 |
30 | Group 2 / {{ i }}
31 |
32 |
33 |
34 |
After
35 |
36 |
37 |
38 |
50 |
51 |
82 |
--------------------------------------------------------------------------------
/docs/examples/trap-rtl.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ rtl === true ? 'RTL - switch to LTR' : 'LTR - switch to RTL' }}
5 |
6 |
7 |
8 |
RTL Always
9 |
10 |
1
11 |
2
12 |
3
13 |
14 |
15 |
16 |
LTR Always
17 |
18 |
1
19 |
2
20 |
3
21 |
22 |
23 |
24 |
{{ rtl === true ? 'RTL' : 'LTR' }}
25 |
26 |
1
27 |
2
28 |
3
29 |
30 |
31 |
32 |
33 |
34 |
59 |
60 |
103 |
--------------------------------------------------------------------------------
/docs/examples/trap-simple.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
First
5 |
Second
6 |
Third (tabindex="-1")
7 |
Fourth
8 |
9 |
10 |
11 |
23 |
24 |
46 |
--------------------------------------------------------------------------------
/docs/examples/use-as-composable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ trapActive === true ? 'Deactivate trap' : 'Activate trap' }}
5 |
{{ trapModifiers.roving === true ? 'Arrow navigation' : 'Tab navigation' }}
6 |
7 |
8 |
First
9 |
Second
10 |
Autofocus
11 |
12 |
13 |
14 |
15 |
48 |
49 |
79 |
--------------------------------------------------------------------------------
/docs/guide/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Guide
3 | ---
4 |
5 | # VueKeyboardTrap (vue-keyboard-trap)
6 |
7 | [](https://opensource.org/licenses/MIT)
8 | [](https://bundlephobia.com/result?p=@pdanpdan/vue-keyboard-trap)
9 | 
10 | 
11 | 
12 | 
13 |
14 | ## Project description
15 |
16 | Vue directive and composable for keyboard navigation - roving movement and trapping inside container.
17 |
18 | Works both for Vue3 and Vue2, as a directive (`v-kbd-trap`) or as a composable (`useKeyboardTrap`).
19 |
20 | ## Install
21 |
22 | ::: code-group
23 |
24 | ```bash [pnpm] :no-line-numbers
25 | pnpm add @pdanpdan/vue-keyboard-trap
26 | ```
27 |
28 | ```bash [yarn] :no-line-numbers
29 | yarn add @pdanpdan/vue-keyboard-trap
30 | ```
31 |
32 | ```bash [npm] :no-line-numbers
33 | npm install @pdanpdan/vue-keyboard-trap
34 | ```
35 |
36 | :::
37 |
38 | ## Playground
39 |
40 | [Demo codepen](https://codepen.io/pdanpdan/pen/MWrzLdM)
41 |
42 | ## Usage
43 |
44 | ### Usage as ESM
45 |
46 | #### As composable (both Vue3 and Vue2)
47 |
48 | ::: code-group
49 |
50 | ```html{3,5-8,11-20,24} [Vue3 and Vue2]
51 |
72 |
73 |
74 | // [!code focus]
75 | ...
76 |
77 |
78 | ```
79 |
80 | :::
81 |
82 | #### As plugin
83 |
84 | ::: code-group
85 |
86 | ```javascript{2,7-9} [Vue3]
87 | import { createApp } from 'vue';
88 | import { VueKeyboardTrapDirectivePlugin } from '@pdanpdan/vue-keyboard-trap'; // [!code focus]
89 | import App from './App.vue';
90 |
91 | const app = createApp(App);
92 |
93 | app.use(VueKeyboardTrapDirectivePlugin, { // [!code focus:3]
94 | // ...options if required
95 | });
96 |
97 | app.mount('#app');
98 | ```
99 |
100 | ```javascript{2,5-7} [Vue2]
101 | import Vue from 'vue';
102 | import { VueKeyboardTrapDirectivePlugin } from '@pdanpdan/vue-keyboard-trap'; // [!code focus]
103 | import App from './App.vue';
104 |
105 | Vue.use(VueKeyboardTrapDirectivePlugin, { // [!code focus:3]
106 | // ...options if required
107 | });
108 |
109 | new Vue({
110 | el: '#app',
111 | });
112 | ```
113 |
114 | :::
115 |
116 | #### Include in specific components
117 |
118 | ::: code-group
119 |
120 | ```html{2,4-6} [Vue3 script setup]
121 |
128 | ```
129 |
130 | ```html{3,5-7,10-12} [Vue3 script]
131 |
145 | ```
146 |
147 | ```html{2,4-6,9-11} [Vue2]
148 |
161 | ```
162 |
163 | :::
164 |
165 | #### User hint styles (cosmetic)
166 |
167 | The directive does not require any CSS styles to work, but for cosmetic purposes (as user hints) some example styles are provided in `dist/styles/index.sass`.
168 |
169 | ::: code-group
170 |
171 | ```javascript [Javascript] :no-line-numbers
172 | import '@pdanpdan/vue-keyboard-trap/styles';
173 | ```
174 |
175 | ```sass [SASS] :no-line-numbers
176 | @import '@pdanpdan/vue-keyboard-trap/styles'
177 | ```
178 |
179 | :::
180 |
181 | If the `/styles` export is not used by your bundler:
182 |
183 | ::: code-group
184 |
185 | ```javascript [Javascript] :no-line-numbers
186 | import '@pdanpdan/vue-keyboard-trap/dist/styles/index.sass';
187 | ```
188 |
189 | ```sass [SASS] :no-line-numbers
190 | @import '@pdanpdan/vue-keyboard-trap/dist/styles/index.sass'
191 | ```
192 |
193 | :::
194 |
195 | ### Usage as UMD
196 |
197 | Load the javascript from [https://cdn.jsdelivr.net/gh/pdanpdan/vue-keyboard-trap/dist/index.umd.js](https://cdn.jsdelivr.net/gh/pdanpdan/vue-keyboard-trap/dist/index.umd.js).
198 |
199 | It will expose a global object `VueKeyboardTrap` with `VueKeyboardTrapDirectivePlugin` and `VueKeyboardTrapDirectiveFactory` keys.
200 |
201 | In order to work it requires that `VueDemi` is already loaded on the page. You can do it like this:
202 |
203 | ```html
204 |
205 |
206 |
207 |
208 | ```
209 |
210 | #### As composable
211 |
212 | ::: code-group
213 | ```javascript{3-6,9-18} [Vue3 and Vue2]
214 | const { ref } = Vue;
215 |
216 | const { useKeyboardTrapFactory } = VueKeyboardTrap; // [!code focus:3]
217 | const useKeyboardTrap = useKeyboardTrapFactory({ // [!code focus:3]
218 | // ...options if required
219 | }); // [!code focus]
220 |
221 | const elRef = ref(null);
222 | useKeyboardTrap( // [!code focus:3]
223 | // element (reactive)
224 | elRef, // [!code focus]
225 | // modifiers (optional, reactive, default all modifiers are false)
226 | {
227 | roving: true,
228 | },
229 | // active (optional, reactive, default true)
230 | true
231 | ); // [!code focus]
232 | ```
233 |
234 | :::
235 |
236 | #### As plugin
237 |
238 | ::: code-group
239 |
240 | ```javascript{2,6-8} [Vue3]
241 | const { createApp } = Vue;
242 | const { VueKeyboardTrapDirectivePlugin } = VueKeyboardTrap; // [!code focus]
243 |
244 | const app = createApp({});
245 |
246 | app.use(VueKeyboardTrapDirectivePlugin, { // [!code focus:3]
247 | // ...options if required
248 | });
249 |
250 | app.mount('#app');
251 | ```
252 |
253 | ```javascript{1,3-5} [Vue2]
254 | const { VueKeyboardTrapDirectivePlugin } = VueKeyboardTrap; // [!code focus]
255 |
256 | Vue.use(VueKeyboardTrapDirectivePlugin, { // [!code focus:3]
257 | // ...options if required
258 | });
259 |
260 | new Vue({
261 | el: '#app',
262 | });
263 | ```
264 |
265 | :::
266 |
267 | #### As directive
268 |
269 | ::: code-group
270 | ```javascript{2,6-8,10} [Vue3]
271 | const { createApp } = Vue;
272 | const { VueKeyboardTrapDirectiveFactory } = VueKeyboardTrap; // [!code focus]
273 |
274 | const app = createApp({});
275 |
276 | const { name, directive } = VueKeyboardTrapDirectiveFactory({ // [!code focus:3]
277 | // ...options if required
278 | });
279 |
280 | app.directive(name, directive); // [!code focus]
281 |
282 | app.mount('#app');
283 | ```
284 |
285 | ```javascript{1,3-5,7} [Vue2]
286 | const { VueKeyboardTrapDirectiveFactory } = VueKeyboardTrap; // [!code focus]
287 |
288 | const { name, directive } = VueKeyboardTrapDirectiveFactory({ // [!code focus:3]
289 | // ...options if required
290 | });
291 |
292 | Vue.directive(name, directive); // [!code focus]
293 | ```
294 |
295 | :::
296 |
297 | #### User hint styles (cosmetic)
298 |
299 | If you want you can access the CSS cosmetic style (user hints) from [https://cdn.jsdelivr.net/gh/pdanpdan/vue-keyboard-trap/dist/styles/index.css](https://cdn.jsdelivr.net/gh/pdanpdan/vue-keyboard-trap/dist/styles/index.css).
300 |
301 | ### Directive configuration options
302 |
303 | | Option | Description | Default |
304 | |--------|-------------|:-------:|
305 | | `name` | snake-case name of the directive (without `v-` prefix) | `kbd-trap` |
306 | | `datasetName` | camelCase name of the `data-attribute` to be set on element when trap is enabled | `v${PascalCase from name}` |
307 | | `focusableSelector` | CSS selector for focusable elements | [see here](#default-focusableselector) |
308 | | `rovingSkipSelector` | CSS selector for elements that should not respond to roving key navigation (input, textarea, ...) | [see here](#default-rovingskipselector) |
309 | | `gridSkipSelector` | CSS selector that will be applied in .roving.grid mode to exclude elements - must be a series of `:not()` selectors | [see here](#default-gridskipselector) |
310 | | `autofocusSelector` | CSS selector for the elements that should be autofocused | [see here](#default-autofocusselector) |
311 | | `trapTabIndex` | tabIndex value to be used when trap element has a tabIndex of -1 and has no `tabindex` attribute | -9999 |
312 |
313 | #### Default `focusableSelector`:
314 |
315 | ```css
316 | :focus,
317 | a[href]:not([tabindex^="-"]),
318 | area[href]:not([tabindex^="-"]),
319 | video[controls]:not([tabindex^="-"]),
320 | audio[controls]:not([tabindex^="-"]),
321 | iframe:not([tabindex^="-"]),
322 | [tabindex]:not(slot):not([tabindex^="-"]),
323 | [contenteditable]:not([contenteditable="false"]):not([tabindex^="-"]),
324 | details > summary:first-of-type:not([tabindex^="-"]),
325 | input:not([type="hidden"]):not(fieldset[disabled] input):not([disabled]):not([tabindex^="-"]),
326 | select:not(fieldset[disabled] input):not([disabled]):not([tabindex^="-"]),
327 | textarea:not(fieldset[disabled] input):not([disabled]):not([tabindex^="-"]),
328 | button:not(fieldset[disabled] input):not([disabled]):not([tabindex^="-"]),
329 | fieldset[disabled]:not(fieldset[disabled] fieldset) > legend input:not([type="hidden"]):not([disabled]):not([tabindex^="-"]),
330 | fieldset[disabled]:not(fieldset[disabled] fieldset) > legend select:not([disabled]):not([tabindex^="-"]),
331 | fieldset[disabled]:not(fieldset[disabled] fieldset) > legend textarea:not([disabled]):not([tabindex^="-"]),
332 | fieldset[disabled]:not(fieldset[disabled] fieldset) > legend button:not([disabled]):not([tabindex^="-"]),
333 | [class*="focusable"]:not([disabled]):not([tabindex^="-"])
334 | ```
335 |
336 | By default `a` tags without href are not focusable - add a `tabindex="0"` attribute on them to make them focusable.
337 | This can be done for all other elements if you want them to be focusable.
338 |
339 | #### Default `rovingSkipSelector`:
340 |
341 | ```css
342 | input:not([disabled]):not([type="button"]):not([type="checkbox"]):not([type="file"]):not([type="image"]):not([type="radio"]):not([type="reset"]):not([type="submit"]),
343 | select:not([disabled]),
344 | select:not([disabled]) *,
345 | textarea:not([disabled]),
346 | [contenteditable]:not([contenteditable="false"]),
347 | [contenteditable]:not([contenteditable="false"]) *
348 | ```
349 |
350 | #### Default `gridSkipSelector`:
351 |
352 | ```css
353 | :not([disabled]),
354 | :not([tabindex^="-"])
355 | ```
356 |
357 | #### Default `autofocusSelector`:
358 |
359 | ```css
360 | [autofocus]:not([disabled]):not([autofocus="false"]),
361 | [data-autofocus]:not([disabled]):not([data-autofocus="false"])
362 | ```
363 |
364 | ### Dynamic enable/disable
365 |
366 | Use the value of the directive (boolean) to enable/disable it.
367 |
368 | ```html
369 |
370 | ```
371 |
372 | The modifiers are reactive so if you use render functions you can dynamically change the behaviour.
373 |
374 | ### Directive modifiers
375 |
376 | | Modifier | Description |
377 | |----------|-------------|
378 | | `.autofocus` | autofocuses the first element that matches [autofocusSelector](#default-autofocusselector) or (if no such element is found) the first focusable child element **when the directive is mounted or enabled** (**only if it not covered by another element**) |
379 | | `.roving` or `.roving.vertical.horizontal` | allow roving navigation (`Home`, `End`, `ArrowKeys`) |
380 | | `.roving.vertical` | allow roving navigation (`Home`, `End`, `ArrowUp`, `ArrowDown`) |
381 | | `.roving.horizontal` | allow roving navigation (`Home`, `End`, `ArrowLeft`, `ArrowRight`) |
382 | | `.roving.grid` | allow roving navigation (`Home`, `End`, `ArrowKeys`) using dataset attrs on elements `[data-${camelCase from datasetName}-(row/col)]`; `[data-${camelCase from datasetName}-(row/col)~="*"]` is a catchall |
383 | | `.roving` used on an element with `[role="grid"]` | allow roving navigation (`Home`, `End`, `ArrowKeys`) using role attrs on elements `[role="row/gridcell"]` |
384 | | `.roving.tabinside` | `Tab` key navigates to next/prev element inside trap (by default `Tab` key navigates to next/prev element outside trap in roving mode) |
385 | | `.escrefocus` | refocus element that was in focus before activating the trap on `Esc` |
386 | | `.escexits` | refocus a parent trap on `Esc` (has priority over `.escrefocus`) |
387 | | `.indexorder` used without `.grid` modifier and on elements without `[role="grid"]` | force usage of order in `tabindex` (`tabindex` in ascending order and then DOM order) |
388 |
389 | ## Keyboard navigation
390 |
391 | - `TAB` / `SHIFT`+`TAB` key
392 | - moves to next / previous focusable element inside the trap group (moves from last one to first one or from first one to last one when no more focusable elements are available in the group)
393 | - if `.roving` modifier is used moves to next / previous trap group or focusable element outside the current trap group
394 | - if `.roving.tabinside` modifiers are used then move inside the trap group
395 | - if `.indexorder` modifier is used without `.grid` and on elements without `[role="grid"]` - the order of tabindex will be used
396 | - `ESC` key
397 | - disables / enables the current tab group
398 | - if `.escexits` modifier is used then refocus the last active focusable element in a parent trap group
399 | - if `.escrefocus` modifier is used then refocus the last focusable element that was active before the current trap group got focus
400 | - if `.escexits` or `.escrefocus` are used then press `SHIFT + ESC` to disable / enable the current tab group
401 | - `HOME` / `END` when `.roving` modifier is used
402 | - move to first / last focusable element in the current trap group
403 | - `ARROW_KEYS` when `.roving` modifier is used (`.roving.horizontal.vertical` is the same as `.roving`)
404 | - if only `.horizontal` modifier is used then only `ARROW_LEFT` / `ARROW_RIGHT` keys can be used
405 | - if only `.vertical` modifier is used then only `ARROW_UP` / `ARROW_DOWN` keys can be used
406 | - `ARROW_LEFT` / `ARROW_UP` move to the previous focusable element inside the trap group
407 | - `ARROW_RIGHT` / `ARROW_DOWN` move to the next focusable element inside the trap group
408 | - if `.indexorder` modifier is used without `.grid` and on elements without `[role="grid"]` - the order of tabindex will be used
409 | - `ARROW_KEYS` when `.roving.grid` modifiers are used or `.roving` modifier on a trap element with [role="grid"]
410 | - move in the grid inside the current trap group
411 |
412 | ### Keyboard navigation inside `.roving.grid` trap groups
413 |
414 | In order to specify the navigation pattern you must use 2 dataset attributes on the focusable elements inside the `.roving` trap group:
415 |
416 | - `data-v-kbd-trap-row` specifies the numeric identifier of the row the element belongs to (numbers need not be consecutive, but their natural order determines the navigation order)
417 | - `data-v-kbd-trap-col` specifies the numeric identifier of the column the element belongs to (numbers need not be consecutive, but their natural order determines the navigation order)
418 |
419 | Any or both attributes can have a value of `*` that means that it is an alement that can be focused from elements having any coresponding (row or col) attribute.
420 |
421 | #### Navigation rules
422 |
423 | - the first focusable element on the row / col (based on direction of movement) is focused
424 | - an element with `*` for row or col is considered to belong to any row / col
425 |
426 | ### Keyboard navigation inside `.roving` trap groups with `[role="grid"]`
427 |
428 | In order to specify the navigation pattern you must use role attributes `[role="row"]` and `[role="gridcell"]`.
429 |
430 | All focusable element must have `[role="gridcell"]` and must be inside `[role="row"]` elements inside `[role="grid"]` trap element.
431 |
432 | The `gridcell`s will be considered inline-start aligned in every row.
433 |
434 | #### Navigation rules
435 |
436 | - the first focusable element on the row / col (based on direction of movement) is focused
437 |
438 | ### RTL / LTR
439 |
440 | The directive checks the closest parent DOM Element of the active element that has a `[dir="rtl"]` or `[dir="ltr`]` attribute.
441 |
442 | If the direction is RTL the `ARROW_LEFT` and `ARROW_RIGHT` keys move in reverse (according to document order of the focusable elements) but consistent to the way the elements are ordered on screen.
443 |
444 | ## CSS (visual hints for users)
445 |
446 | The directive does not require any styles, but it might help the users to have visual hints for navigation.
447 |
448 | A default style is provided as SASS in `dist/styles/index.sass` (can be imported as `import '@pdapdan/vue-keyboard-trap/styles'`, as `import '@pdapdan/vue-keyboard-trap/dist/styles/index.sass'` (if the bundler does not use the `/styles` export) or included from [https://cdn.jsdelivr.net/gh/pdanpdan/vue-keyboard-trap/dist/styles/index.sass](https://cdn.jsdelivr.net/gh/pdanpdan/vue-keyboard-trap/dist/styles/index.sass)).
449 |
450 | The default style is also provided as CSS in `dist/styles/index.css` (can be imported as `import '@pdapdan/vue-keyboard-trap/dist/styles/index.css'` or included from [https://cdn.jsdelivr.net/gh/pdanpdan/vue-keyboard-trap/dist/styles/index.css](https://cdn.jsdelivr.net/gh/pdanpdan/vue-keyboard-trap/dist/styles/index.css)).
451 |
452 | There are some CSS variables that can be used to customize the aspect of the hints:
453 |
454 | | Variable | Role | Default |
455 | |----------|------|:-------:|
456 | | `--color-v-kbd-trap-enabled` | the text color when directive is enabled | `#c33`
■ |
457 | | `--color-v-kbd-trap-disabled` | the text color when directive is disabled | `#999`
■ |
458 | | `--color-v-kbd-trap-background` | the background color of the hint area | `#eeee`
■ |
459 | | `--text-v-kbd-trap-separator` | separator between elements | `/` |
460 | | `--text-v-kbd-trap-enabled` | indicator for enabled but not active trap | `Trap` |
461 | | `--text-v-kbd-trap-esc` | indicator for `Esc` key active | `Esc` |
462 | | `--text-v-kbd-trap-esc-refocus` | indicator for `Esc` key active when it refocuses | `Esc\2949` / `Esc⥉` |
463 | | `--text-v-kbd-trap-esc-exits` | indicator for `Esc` key active when it exits trap | `Esc\2923` / `Esc⤣` |
464 | | `--text-v-kbd-trap-tab` | indicator for `Tab` key active inside trap | `Tab` |
465 | | `--text-v-kbd-trap-tab-exits` | indicator for `Tab` key active when it exits trap | `Tab\21C5` / `Tab⇅` |
466 | | `--text-v-kbd-trap-grid` | indicator for grid mode active | `\229E` / `⊞` |
467 | | `--text-v-kbd-trap-arrows-all` | indicator for move keys active in roving mode | `\2962\2963\2965\2964` / `⥢⥣⥥⥤` |
468 | | `--text-v-kbd-trap-arrows-horizontal` | indicator for move keys active in roving mode horizontal | `\2962\2964` / `⥢⥤` |
469 | | `--text-v-kbd-trap-arrows-vertical` | indicator for move keys active in roving mode vertical | `\2963\2965` / `⥣⥥` |
470 |
471 | In the default style the hint is positioned on the top-right corner of the trap group.
472 |
473 | <<< @/../src/public/styles/index.sass
474 |
475 | ## Development
476 |
477 | ### Install the dependencies
478 |
479 | ```bash :no-line-numbers
480 | pnpm i
481 | ```
482 |
483 | ### Start development mode (hot-code reloading, error reporting, etc.)
484 |
485 | ```bash :no-line-numbers
486 | pnpm dev
487 | ```
488 |
489 | ### Lint the files
490 |
491 | ```bash :no-line-numbers
492 | pnpm lint
493 | ```
494 |
495 | ### Build for production
496 |
497 | ```bash :no-line-numbers
498 | pnpm build
499 | ```
500 |
501 | ## Source code, issues, bug reports, feature requests
502 |
503 | [Vue Keyboard Trap (vue-keyboard-trap)](https://github.com/pdanpdan/vue-keyboard-trap)
504 |
505 | ## Author
506 |
507 | * Name: Dan Popescu (PDan)
508 | * Email: [pdan.popescu@gmail.com](mailto:pdan.popescu@gmail.com)
509 | * Website: https://github.com/pdanpdan/
510 | * Github: [@pdanpdan](https://github.com/pdanpdan)
511 |
512 | ## License
513 |
514 | Copyright © 2022-present [Dan Popescu](https://github.com/pdanpdan).
515 |
516 | This application is distributed under [](https://opensource.org/licenses/MIT), see LICENSE for more information.
517 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: home
3 |
4 | title: Vue Keyboard Trap
5 | titleTemplate: Tab Keyboard Navigation Helper for Vue
6 |
7 | hero:
8 | name: Vue Keyboard Trap
9 | text: Tab Keyboard Navigation Helper for Vue
10 | tagline: Vue3 and Vue2 directive and composable for keyboard navigation - roving movement and trapping inside container
11 | image:
12 | src: /logo.png
13 | alt: Logo image
14 | actions:
15 | - theme: brand
16 | text: Get Started
17 | link: /guide/
18 | - theme: alt
19 | text: View on GitHub
20 | link: https://github.com/pdanpdan/vue-keyboard-trap
21 |
22 | features:
23 | - title: Versatile
24 | details: Traps keyboard focus in component. Roving mode for complex content. Grid mode for 4 way focus change.
25 | - title: Vue3 and Vue2
26 | details: Same directive and/or composable works in Vue3 and Vue2. No other dependencies.
27 | - title: A11y
28 | details: Add accessibility for keyboard navigation.
29 | ---
30 |
--------------------------------------------------------------------------------
/docs/links/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Links
3 | ---
4 |
5 | ## Vue Keyboard Trap
6 |
7 | [Release history](https://github.com/pdanpdan/vue-keyboard-trap/releases)
8 |
9 | [Bug reports, feature requests](https://github.com/pdanpdan/vue-keyboard-trap/issues)
10 |
11 | [Discussions](https://github.com/pdanpdan/vue-keyboard-trap/discussions)
12 |
13 | [Source code](https://github.com/pdanpdan/vue-keyboard-trap)
14 |
15 | [Demo codepen](https://codepen.io/pdanpdan/pen/MWrzLdM)
16 |
17 | ## Author
18 |
19 | * Name: Dan Popescu (PDan)
20 | * Email:
21 | * Website: https://github.com/pdanpdan/
22 | * Github: [@pdanpdan](https://github.com/pdanpdan)
23 |
--------------------------------------------------------------------------------
/docs/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/docs/public/favicon.ico
--------------------------------------------------------------------------------
/docs/public/googlec0cade17e5e27188.html:
--------------------------------------------------------------------------------
1 | google-site-verification: googlec0cade17e5e27188.html
--------------------------------------------------------------------------------
/docs/public/icons/apple-launch-1125x2436.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/docs/public/icons/apple-launch-1125x2436.png
--------------------------------------------------------------------------------
/docs/public/icons/apple-launch-1170x2532.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/docs/public/icons/apple-launch-1170x2532.png
--------------------------------------------------------------------------------
/docs/public/icons/apple-launch-1242x2208.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/docs/public/icons/apple-launch-1242x2208.png
--------------------------------------------------------------------------------
/docs/public/icons/apple-launch-1242x2688.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/docs/public/icons/apple-launch-1242x2688.png
--------------------------------------------------------------------------------
/docs/public/icons/apple-launch-1284x2778.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/docs/public/icons/apple-launch-1284x2778.png
--------------------------------------------------------------------------------
/docs/public/icons/apple-launch-1536x2048.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/docs/public/icons/apple-launch-1536x2048.png
--------------------------------------------------------------------------------
/docs/public/icons/apple-launch-1620x2160.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/docs/public/icons/apple-launch-1620x2160.png
--------------------------------------------------------------------------------
/docs/public/icons/apple-launch-1668x2224.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/docs/public/icons/apple-launch-1668x2224.png
--------------------------------------------------------------------------------
/docs/public/icons/apple-launch-1668x2388.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/docs/public/icons/apple-launch-1668x2388.png
--------------------------------------------------------------------------------
/docs/public/icons/apple-launch-2048x2732.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/docs/public/icons/apple-launch-2048x2732.png
--------------------------------------------------------------------------------
/docs/public/icons/apple-launch-750x1334.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/docs/public/icons/apple-launch-750x1334.png
--------------------------------------------------------------------------------
/docs/public/icons/apple-launch-828x1792.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/docs/public/icons/apple-launch-828x1792.png
--------------------------------------------------------------------------------
/docs/public/icons/favicon-128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/docs/public/icons/favicon-128x128.png
--------------------------------------------------------------------------------
/docs/public/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/docs/public/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/docs/public/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/docs/public/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/docs/public/icons/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/docs/public/icons/favicon-96x96.png
--------------------------------------------------------------------------------
/docs/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/docs/public/logo.png
--------------------------------------------------------------------------------
/docs/public/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | https://pdanpdan.github.io/vue-keyboard-trap/
5 | 2023-02-22T14:06:07+00:00
6 | monthly
7 | 1.00
8 |
9 |
10 | https://pdanpdan.github.io/vue-keyboard-trap/guide/
11 | 2023-02-22T14:06:07+00:00
12 | weekly
13 | 0.80
14 |
15 |
16 | https://pdanpdan.github.io/vue-keyboard-trap/examples/
17 | 2023-02-22T14:06:07+00:00
18 | weekly
19 | 0.80
20 |
21 |
22 | https://pdanpdan.github.io/vue-keyboard-trap/links/
23 | 2023-02-22T14:06:07+00:00
24 | monthly
25 | 0.80
26 |
27 |
28 |
--------------------------------------------------------------------------------
/docs/vite.config.js:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path';
2 | import { NodePackageImporter } from 'sass-embedded';
3 | import { defineConfig } from 'vite';
4 |
5 | export default defineConfig({
6 | resolve: {
7 | alias: {
8 | '@pdanpdan/vue-keyboard-trap/styles': resolve(__dirname, '../src/public/styles/index.sass'),
9 | '@pdanpdan/vue-keyboard-trap': resolve(__dirname, '../src/exports.js'),
10 | },
11 | },
12 | css: {
13 | // preprocessorOptions: {
14 | // sass: {
15 | // charset: false,
16 | // },
17 | // },
18 | preprocessorOptions: {
19 | sass: {
20 | api: 'modern',
21 | importers: [new NodePackageImporter()],
22 | },
23 | },
24 | postcss: {
25 | plugins: [
26 | {
27 | postcssPlugin: 'internal:charset-removal',
28 | AtRule: {
29 | charset: (atRule) => {
30 | if (atRule.name === 'charset') {
31 | atRule.remove();
32 | }
33 | },
34 | },
35 | },
36 | ],
37 | },
38 | },
39 | });
40 |
--------------------------------------------------------------------------------
/gitpkg.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = () => ({
2 | getTagName: (pkg) => pkg.version,
3 | });
4 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vue Keyboard Trap
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "allowJs": true,
5 | "target": "ES2015",
6 | "jsx": "preserve",
7 | "paths": {
8 | "src/*": [
9 | "src/*"
10 | ],
11 | "vue$": [
12 | "node_modules/vue/dist/vue.runtime.esm-bundler.js"
13 | ]
14 | }
15 | },
16 | "exclude": [
17 | "dist",
18 | "node_modules"
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@pdanpdan/vue-keyboard-trap",
3 | "version": "1.1.0",
4 | "description": "Vue3 and Vue2 directive for keyboard navigation - roving movement and trapping inside container",
5 | "productName": "Vue Keyboard Trap",
6 | "author": {
7 | "name": "Dan Popescu",
8 | "email": "pdan.popescu@gmail.com",
9 | "url": "https://github.com/pdanpdan"
10 | },
11 | "funding": {
12 | "type": "github",
13 | "url": "https://github.com/sponsors/pdanpdan"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/pdanpdan/vue-keyboard-trap.git"
18 | },
19 | "keywords": [
20 | "vue",
21 | "vue2",
22 | "vue3",
23 | "composable",
24 | "directive",
25 | "keyboard",
26 | "navigation",
27 | "trap",
28 | "roving",
29 | "grid",
30 | "gridcell",
31 | "a11y",
32 | "accessibility",
33 | "cycle",
34 | "tab",
35 | "tabindex",
36 | "use",
37 | "wai-aria"
38 | ],
39 | "license": "MIT",
40 | "bugs": {
41 | "url": "https://github.com/pdanpdan/vue-keyboard-trap/issues"
42 | },
43 | "homepage": "https://pdanpdan.github.io/vue-keyboard-trap",
44 | "type": "module",
45 | "main": "./dist/index.umd.js",
46 | "module": "./dist/index.es.js",
47 | "exports": {
48 | ".": {
49 | "require": "./dist/index.umd.js",
50 | "import": "./dist/index.es.js",
51 | "types": "./dist/types/index.d.ts",
52 | "style": "./dist/styles/index.css",
53 | "css": "./dist/styles/index.css",
54 | "sass": "./dist/styles/index.sass",
55 | "web-types": "./dist/web-types/index.json"
56 | },
57 | "./styles/css": "./dist/styles/index.css",
58 | "./styles/sass": "./dist/styles/index.sass",
59 | "./types": "./dist/types/index.d.ts"
60 | },
61 | "typings": "./dist/types/index.d.ts",
62 | "types": "./dist/types/index.d.ts",
63 | "web-types": "./dist/web-types/index.json",
64 | "files": [
65 | "dist/",
66 | "src/"
67 | ],
68 | "directories": {
69 | "doc": "docs"
70 | },
71 | "sideEffects": [
72 | "*.sass",
73 | "*.css"
74 | ],
75 | "scripts": {
76 | "dev": "vite --config ./vite.dev.config.js",
77 | "docs:dev": "vitepress dev docs",
78 | "docs:build": "vitepress build docs",
79 | "docs:deploy": "./deploy.docs.sh",
80 | "lint": "eslint --ext .js,.vue ./",
81 | "build": "vite build --config ./vite.src.config.js",
82 | "prepublishOnly": "pnpm lint && pnpm build"
83 | },
84 | "dependencies": {
85 | "vue-demi": "^0.14.10"
86 | },
87 | "peerDependencies": {
88 | "@vue/composition-api": ">=1.0.0",
89 | "vue": ">=2.0.0"
90 | },
91 | "peerDependenciesMeta": {
92 | "@vue/composition-api": {
93 | "optional": true
94 | }
95 | },
96 | "devDependencies": {
97 | "@vitejs/plugin-vue": "^5.1.4",
98 | "eslint": "^8.57.1",
99 | "eslint-config-airbnb-base": "^15.0.0",
100 | "eslint-plugin-import": "^2.30.0",
101 | "eslint-plugin-vue": "^9.28.0",
102 | "flexsearch": "^0.7.43",
103 | "markdown-it": "^14.1.0",
104 | "sass-embedded": "^1.79.1",
105 | "vite": "^5.4.6",
106 | "vitepress": "1.3.4",
107 | "vue": "^3.5.6"
108 | },
109 | "engines": {
110 | "node": ">= 12.22.0"
111 | },
112 | "packageManager": "pnpm@9.10.0"
113 | }
114 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/public/favicon.ico
--------------------------------------------------------------------------------
/public/icons/apple-launch-1125x2436.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/public/icons/apple-launch-1125x2436.png
--------------------------------------------------------------------------------
/public/icons/apple-launch-1170x2532.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/public/icons/apple-launch-1170x2532.png
--------------------------------------------------------------------------------
/public/icons/apple-launch-1242x2208.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/public/icons/apple-launch-1242x2208.png
--------------------------------------------------------------------------------
/public/icons/apple-launch-1242x2688.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/public/icons/apple-launch-1242x2688.png
--------------------------------------------------------------------------------
/public/icons/apple-launch-1284x2778.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/public/icons/apple-launch-1284x2778.png
--------------------------------------------------------------------------------
/public/icons/apple-launch-1536x2048.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/public/icons/apple-launch-1536x2048.png
--------------------------------------------------------------------------------
/public/icons/apple-launch-1620x2160.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/public/icons/apple-launch-1620x2160.png
--------------------------------------------------------------------------------
/public/icons/apple-launch-1668x2224.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/public/icons/apple-launch-1668x2224.png
--------------------------------------------------------------------------------
/public/icons/apple-launch-1668x2388.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/public/icons/apple-launch-1668x2388.png
--------------------------------------------------------------------------------
/public/icons/apple-launch-2048x2732.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/public/icons/apple-launch-2048x2732.png
--------------------------------------------------------------------------------
/public/icons/apple-launch-750x1334.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/public/icons/apple-launch-750x1334.png
--------------------------------------------------------------------------------
/public/icons/apple-launch-828x1792.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/public/icons/apple-launch-828x1792.png
--------------------------------------------------------------------------------
/public/icons/favicon-128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/public/icons/favicon-128x128.png
--------------------------------------------------------------------------------
/public/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/public/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/public/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/public/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/public/icons/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/public/icons/favicon-96x96.png
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
23 |
24 |
25 |
0
26 |
27 |
28 |
v-kbd-trap.roving
29 |
30 |
1
31 |
32 |
2
33 |
34 |
35 |
36 |
37 |
v-kbd-trap.roving.escrefocus
38 |
39 |
3.1
40 |
41 |
3.2
42 |
43 |
44 |
45 |
3.4
46 |
47 |
48 |
4
49 |
50 |
51 |
v-kbd-trap.{{ Object.keys(modifiers).filter((k) => modifiers[k]).join('.') }} - useKeyboardTrap
52 |
53 |
5.1
54 |
55 |
5.2
56 |
57 |
58 |
59 |
5.4
60 |
61 |
62 |
6
63 |
64 |
65 |
v-kbd-trap.autofocus {{ active2 }}
66 |
67 |
6.1
68 |
69 |
6.2
70 |
71 |
6.3
72 |
73 |
74 |
75 |
v-kbd-trap.roving.horizontal.tabinside.escrefocus
76 |
77 |
7.1
78 |
79 |
7.2
80 |
81 |
7.3
82 |
83 |
84 |
85 |
v-kbd-trap.roving.vertical.tabinside.escrefocus
86 |
87 |
8.1
88 |
89 |
8.2
90 |
91 |
8.3
92 |
93 |
94 |
95 |
96 |
v-kbd-trap.roving.grid
97 |
98 |
99 |
107 | 9 R:{{ i }}/C:{{ j }}
108 |
109 |
110 |
111 |
112 |
113 |
v-kbd-trap.roving.grid
114 |
115 |
116 |
124 | 10 R:{{ i }}/C:{{ j }}
125 |
126 |
127 |
128 |
129 |
130 |
v-kbd-trap.roving - Role grid test (roles: grid, row, gridcell)
131 |
132 |
138 |
139 |
140 |
141 |
142 | Wk.
143 | Su
144 | Mo
145 | Tu
146 | We
147 | Th
148 | Fr
149 | Sa
150 |
151 |
152 |
153 |
154 |
155 | 13
156 | 27
157 | 28
158 | 29
159 | 30
160 | 31
161 | 1
162 | 2
163 |
164 |
165 |
166 | {{ 13 + m}}
167 | {{ 2 + (m - 1) * 7 + d }}
173 |
174 |
175 |
176 |
177 |
178 |
11
179 |
180 |
181 |
RTL Always - v-kbd-trap.roving.horizontal
182 |
183 |
12.1
184 |
185 |
12.2
186 |
187 |
12.3
188 |
189 |
190 |
191 |
RTL Always - v-kbd-trap.roving.horizontal
192 |
193 |
12bis.1
194 |
195 |
12bis.2
196 |
197 |
12bis.3
198 |
199 |
200 |
201 |
LTR Always - v-kbd-trap.roving.horizontal
202 |
203 |
13.1
204 |
205 |
13.2
206 |
207 |
13.3
208 |
209 |
210 |
211 |
LTR Always - v-kbd-trap.roving.horizontal
212 |
213 |
13bis.1
214 |
215 |
13bis.2
216 |
217 |
13bis.3
218 |
219 |
220 |
221 |
Autofocus covered test
222 |
223 |
224 |
225 |
226 |
v-kbd-trap.autofocus {{ active3 }}
227 |
228 |
14.1
229 |
230 |
14.2
231 |
232 |
14.3
233 |
234 |
235 |
Cover
236 |
237 |
238 |
239 |
240 |
15
241 |
242 |
243 |
v-kbd-trap (Use DOM order)
244 |
245 |
16.1 (tabindex 0)
246 |
247 |
16.2 (tabindex -1)
248 |
249 |
16.3 (tabindex 4)
250 |
251 |
16.4 (tabindex 0)
252 |
253 |
16.5 (tabindex 1)
254 |
255 |
16.6 (tabindex 5)
256 |
257 |
16.7 (tabindex 0)
258 |
259 |
260 |
261 |
v-kbd-trap.indexorder (Force tabindex order)
262 |
263 |
16.1bis (tabindex 0)
264 |
265 |
16.2bis (tabindex -1)
266 |
267 |
16.3bis (tabindex 4)
268 |
269 |
16.4bis (tabindex 0)
270 |
271 |
16.5bis (tabindex 1)
272 |
273 |
16.6bis (tabindex 5)
274 |
275 |
16.7bis (tabindex 0)
276 |
277 |
278 |
279 |
v-kbd-trap.roving (Use DOM order)
280 |
281 |
17.1 (tabindex 0)
282 |
283 |
17.2 (tabindex -1)
284 |
285 |
17.3 (tabindex 4)
286 |
287 |
17.4 (tabindex 0)
288 |
289 |
17.5 (tabindex 1)
290 |
291 |
17.6 (tabindex 5)
292 |
293 |
17.7 (tabindex 0)
294 |
295 |
296 |
297 |
v-kbd-trap.roving.indexorder (Force tabindex order)
298 |
299 |
17.1bis (tabindex 0)
300 |
301 |
17.2bis (tabindex -1)
302 |
303 |
17.3bis (tabindex 4)
304 |
305 |
17.4bis (tabindex 0)
306 |
307 |
17.5bis (tabindex 1)
308 |
309 |
17.6bis (tabindex 5)
310 |
311 |
17.7bis (tabindex 0)
312 |
313 |
314 |
18
315 |
316 |
317 |
v-kbd-trap.roving - form elements
318 |
319 |
19.1
320 |
321 |
322 | Button 19.2
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 | Checkox 19.7.1
343 | Checkox 19.7.2
344 | Checkox 19.7.3
345 |
346 |
347 |
348 | Radio 19.8.1
349 | Radio 19.8.2
350 | Radio 19.8.3
351 |
352 |
353 |
354 |
360 |
361 |
362 |
369 |
370 |
371 |
Tab / Shift + Tab
372 |
373 |
374 |
375 |
376 |
377 |
Tab / Shift + Tab
378 |
379 |
380 |
381 |
382 |
383 |
Tab / Shift + Tab
384 |
385 |
386 | Option 1
387 | Option 2
388 | Option 3
389 |
390 |
391 |
392 |
393 |
Tab / Shift + Tab
394 |
395 |
396 | Option 1
397 | Option 2
398 | Option 3
399 |
400 |
401 |
402 |
403 | Tab / Shift + Tab [fieldset]
404 |
405 |
406 |
407 |
408 |
409 | Tab / Shift + Tab [disabled fieldset]
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 | Tab / Shift + Tab [disabled fieldset]
420 |
421 |
422 |
423 |
424 |
425 |
426 | Tab / Shift + Tab [fieldset in disabled fieldset]
427 |
428 |
429 |
430 |
431 |
432 |
19.16
433 |
434 |
435 |
20
436 |
437 |
438 |
444 | 21.{{ i }}
445 |
446 |
447 |
448 |
454 | 21.3.{{ i }}
455 |
456 |
457 |
458 |
464 | 21.{{ i + 3 }}
465 |
466 |
467 |
468 |
22
469 |
470 |
471 |
476 |
477 |
482 |
483 |
484 |
485 |
Close
486 |
487 |
488 |
489 |
490 |
520 |
521 |
662 |
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdanpdan/vue-keyboard-trap/b09921177d8012cf1887e305382c02250cc00d4b/src/assets/logo.png
--------------------------------------------------------------------------------
/src/directives/keyboard-trap/directive.js:
--------------------------------------------------------------------------------
1 | import {
2 | isVue3,
3 | computed,
4 | markRaw,
5 | unref,
6 | watch,
7 | getCurrentScope,
8 | onScopeDispose,
9 | } from 'vue-demi';
10 |
11 | import { createConfig } from './options';
12 | import {
13 | extractNumber,
14 | focus,
15 | visibleFocusCheckFn,
16 | dirIsRtl,
17 | } from './helpers';
18 |
19 | // options:
20 | // name: snake-case name of the directive (without `v-` prefix) - default `kbd-trap`
21 | // datasetName: camelCase name of the `data-attribute` to be set on element when trap is enabled - default `v${ PascalCase from name}`
22 | //
23 | // focusableSelector: CSS selector for focusable elements
24 | // rovingSkipSelector: CSS selector for elements that should not respond to roving key navigation (input, textarea, ...)
25 | // gridSkipSelector: CSS selector that will be applied in .roving.grid mode to exclude elements - must be a series of :not() selectors
26 | // autofocusSelector: CSS selector for the elements that should be autofocused
27 | // trapTabIndex: tabIndex value to be used when trap element has a tabIndex of -1 and has no `tabindex` attribute (default -9999)
28 | //
29 | //
30 | // value: false to disable
31 | //
32 | //
33 | // modifiers:
34 | // .autofocus - autofocuses the first element that matches `autofocusSelector` or (if no such element is found) the first focusable child element when the directive is mounted or enabled
35 | // .roving, .roving.vertical.horizontal - allow roving navigation (Home, End, ArrowKeys)
36 | // .roving.vertical - allow roving navigation (Home, End, ArrowUp, ArrowDown)
37 | // .roving.horizontal - allow roving navigation (Home, End, ArrowLeft, ArrowRight)
38 | // .roving.grid - allow roving navigation (Home, End, ArrowKeys) using dataset attrs on elements [data-${ camelCase from datasetName }-(row|col)]
39 | // [data-${ camelCase from datasetName }-(row|col)~="*"] is a catchall
40 | // .roving used on an element with [role="grid"] - allow roving navigation (Home, End, ArrowKeys) using role attrs on elements [role="row|gridcell"]
41 | // .roving.tabinside - Tab key navigates to next/prev element inside trap (by default Tab key navigates to next/prev element outside trap in roving mode)
42 | // .escrefocus - refocus element that was in focus before activating the trap on Esc
43 | // .escexits - refocus a parent trap on Esc (has priority over .escrefocus)
44 | // .indexorder used without .grid and not on elements with [role="grid"] - force usage of order in tabindex (tabindex in ascending order and then DOM order)
45 |
46 | let activeTrapEl = null;
47 |
48 | function setActiveTrapEl(newEl, config) {
49 | if (activeTrapEl !== newEl) {
50 | if (newEl != null) {
51 | newEl.dataset[config.datasetNameActive] = '';
52 | newEl.__vKbdTrapActiveClean = () => {
53 | delete newEl.dataset[config.datasetNameActive];
54 | newEl.__vKbdTrapActiveClean = undefined;
55 | };
56 | }
57 |
58 | if (activeTrapEl != null && typeof activeTrapEl.__vKbdTrapActiveClean === 'function') {
59 | activeTrapEl.__vKbdTrapActiveClean();
60 | }
61 |
62 | activeTrapEl = newEl;
63 | }
64 | }
65 |
66 | function getCtx(el) {
67 | const ctx = (el || {}).__vKbdTrap;
68 |
69 | return ctx === Object(ctx) ? ctx : null;
70 | }
71 |
72 | function setAttributes(el, disable, ctx, config) {
73 | if (disable === true) {
74 | delete el.dataset[config.datasetName];
75 |
76 | if (el.tabIndex === config.trapTabIndex) {
77 | el.removeAttribute('tabindex');
78 | }
79 | } else {
80 | el.dataset[config.datasetName] = Object.keys(ctx.modifiers)
81 | .filter((key) => ctx.modifiers[key] === true)
82 | .join(' ');
83 |
84 | if (el.tabIndex < 0 && el.getAttribute('tabindex') == null && el.matches('dialog') === false && el.matches('[popover]') === false) {
85 | el.tabIndex = config.trapTabIndex;
86 | }
87 | }
88 | }
89 |
90 | function createCtx(config, el, value, modifiers) {
91 | const ctx = {
92 | disable: value === false,
93 | modifiers,
94 |
95 | focusTarget: null,
96 | relatedFocusTarget: null,
97 |
98 | bind() {
99 | el.__vKbdTrap = ctx;
100 | el.addEventListener('keydown', ctx.trap);
101 | el.addEventListener('focusin', ctx.activate);
102 | el.addEventListener('focusout', ctx.deactivate);
103 | el.addEventListener('pointerdown', ctx.overwriteFocusTarget, { passive: true });
104 |
105 | if (ctx.disable === false) {
106 | setAttributes(el, ctx.disable, ctx, config);
107 | }
108 | },
109 |
110 | unbind() {
111 | delete el.__vKbdTrap;
112 | el.removeEventListener('keydown', ctx.trap);
113 | el.removeEventListener('focusin', ctx.activate);
114 | el.removeEventListener('focusout', ctx.deactivate);
115 | el.removeEventListener('pointerdown', ctx.overwriteFocusTarget);
116 | setAttributes(el, true, ctx, config);
117 | },
118 |
119 | activate(ev) {
120 | if (ctx.disable === true || ev.__vKbdTrap === true) {
121 | return;
122 | }
123 |
124 | ev.__vKbdTrap = true;
125 |
126 | const oldFocusedElement = ev.relatedTarget;
127 |
128 | if (
129 | oldFocusedElement != null
130 | && oldFocusedElement !== document.body
131 | && oldFocusedElement.closest(config.datasetNameSelector) !== el
132 | && oldFocusedElement.tabIndex !== config.trapTabIndex
133 | ) {
134 | ctx.relatedFocusTarget = oldFocusedElement;
135 | }
136 |
137 | if (
138 | activeTrapEl !== el
139 | && (
140 | oldFocusedElement == null
141 | || oldFocusedElement.closest(config.datasetNameSelector) !== el
142 | )
143 | ) {
144 | setActiveTrapEl(el, config);
145 |
146 | if (
147 | oldFocusedElement == null
148 | || oldFocusedElement.dataset[config.datasetNamePreventRefocus] === undefined
149 | || el.contains(oldFocusedElement) === false
150 | ) {
151 | ctx.refocus(ctx.modifiers.roving !== true);
152 | }
153 | }
154 | },
155 |
156 | deactivate(ev) {
157 | if (ctx.disable === true || ev.__vKbdTrap === true) {
158 | return;
159 | }
160 |
161 | ev.__vKbdTrap = true;
162 |
163 | const newFocusedElement = ev.relatedTarget;
164 |
165 | if (
166 | activeTrapEl === el
167 | && (
168 | newFocusedElement == null
169 | || newFocusedElement.closest(config.datasetNameSelector) !== el
170 | )
171 | ) {
172 | ctx.focusTarget = ev.target;
173 |
174 | if (newFocusedElement == null && ctx.relatedFocusTarget) {
175 | focus(ctx.relatedFocusTarget);
176 | }
177 | setActiveTrapEl(null, config);
178 | }
179 | },
180 |
181 | trap(ev) {
182 | if (ctx.disable === true || ev.__vKbdTrap === true) {
183 | return;
184 | }
185 |
186 | const { code, shiftKey } = ev;
187 | const { activeElement } = document;
188 |
189 | if (code === 'Escape') {
190 | ev.__vKbdTrap = true;
191 |
192 | if (activeTrapEl === el) {
193 | ctx.focusTarget = activeElement;
194 |
195 | if (shiftKey === true) {
196 | ev.preventDefault();
197 | } else {
198 | if (ctx.modifiers.escexits === true) {
199 | setActiveTrapEl(el.parentElement == null ? null : el.parentElement.closest(config.datasetNameSelector), config);
200 |
201 | const newCtx = getCtx(activeTrapEl);
202 |
203 | if (newCtx != null) {
204 | newCtx.refocus();
205 | }
206 |
207 | return;
208 | }
209 |
210 | if (ctx.modifiers.escrefocus === true && focus(ctx.relatedFocusTarget) === true) {
211 | return;
212 | }
213 | }
214 |
215 | const trapEl = el.parentElement && el.parentElement.closest(config.datasetNameSelector);
216 | setActiveTrapEl(trapEl || null, config);
217 | } else {
218 | setActiveTrapEl(el, config);
219 | }
220 |
221 | return;
222 | }
223 |
224 | if (activeTrapEl !== el) {
225 | return;
226 | }
227 |
228 | ev.__vKbdTrap = true;
229 |
230 | let step = 0;
231 | let indexSelector = (i) => i;
232 | let rovingExit = false;
233 | let rovingDirection = false;
234 |
235 | if (ctx.modifiers.roving === true) {
236 | const rovingSkipSelector = activeElement.matches(config.rovingSkipSelector);
237 |
238 | if (code !== 'Tab' && rovingSkipSelector === true) {
239 | return;
240 | }
241 |
242 | if (code === 'Tab') {
243 | if (rovingSkipSelector === false && ctx.modifiers.tabinside !== true) {
244 | rovingExit = el.parentElement.closest(config.datasetNameSelector);
245 |
246 | if (rovingExit != null) {
247 | ev.__vKbdTrap = undefined;
248 | }
249 |
250 | if (shiftKey === true) {
251 | step = 1;
252 | indexSelector = (_, iMax) => iMax;
253 | } else {
254 | step = -1;
255 | indexSelector = () => 0;
256 | }
257 | } else {
258 | step = shiftKey === true ? -1 : 1;
259 | }
260 | } else if (code === 'Home') {
261 | step = 1;
262 | indexSelector = (_, iMax) => iMax;
263 | } else if (code === 'End') {
264 | step = -1;
265 | indexSelector = () => 0;
266 | } else if (
267 | el.parentElement != null
268 | && (
269 | (
270 | ctx.modifiers.vertical === true
271 | && ctx.modifiers.horizontal !== true
272 | && (code === 'ArrowLeft' || code === 'ArrowRight')
273 | ) || (
274 | ctx.modifiers.horizontal === true
275 | && ctx.modifiers.vertical !== true
276 | && (code === 'ArrowUp' || code === 'ArrowDown')
277 | )
278 | )
279 | ) {
280 | const parentTrap = el.parentElement.closest(
281 | ctx.modifiers.vertical === true
282 | ? config.datasetNameSelectorRovingHorizontal
283 | : config.datasetNameSelectorRovingVertical,
284 | );
285 |
286 | if (parentTrap != null) {
287 | rovingExit = parentTrap;
288 |
289 | ev.__vKbdTrap = undefined;
290 |
291 | if (code === (dirIsRtl(activeElement, el) === true ? 'ArrowRight' : 'ArrowLeft') || code === 'ArrowUp') {
292 | step = 1;
293 | indexSelector = (_, iMax) => iMax;
294 | } else {
295 | step = -1;
296 | indexSelector = () => 0;
297 | }
298 | }
299 | } else {
300 | if (ctx.modifiers.vertical === true || ctx.modifiers.horizontal !== true) {
301 | if (code === 'ArrowUp') {
302 | step = -1;
303 | rovingDirection = 'v';
304 | } else if (code === 'ArrowDown') {
305 | step = 1;
306 | rovingDirection = 'v';
307 | }
308 | }
309 |
310 | if (ctx.modifiers.vertical !== true || ctx.modifiers.horizontal === true) {
311 | if (code === 'ArrowLeft') {
312 | step = -1;
313 | rovingDirection = 'h';
314 | } else if (code === 'ArrowRight') {
315 | step = 1;
316 | rovingDirection = 'h';
317 | }
318 |
319 | if (step !== 0 && rovingDirection === 'h' && dirIsRtl(activeElement, el) === true) {
320 | step *= -1;
321 | }
322 | }
323 | }
324 | } else if (code === 'Tab') {
325 | step = shiftKey === true ? -1 : 1;
326 | }
327 |
328 | if (step === 0) {
329 | return;
330 | }
331 |
332 | if (rovingExit === false) {
333 | ev.preventDefault();
334 | } else {
335 | ctx.focusTarget = activeElement;
336 | ctx.focusTarget.dataset[config.datasetNamePreventRefocus] = '';
337 |
338 | requestAnimationFrame(() => {
339 | if (ctx.focusTarget) {
340 | delete ctx.focusTarget.dataset[config.datasetNamePreventRefocus];
341 | }
342 | });
343 | }
344 |
345 | let focusableList = [];
346 |
347 | if (rovingDirection !== false) {
348 | let focusableMap;
349 |
350 | if (ctx.modifiers.grid === true) {
351 | const row = extractNumber(activeElement.dataset[config.datasetNameRow]);
352 | const col = extractNumber(activeElement.dataset[config.datasetNameCol]);
353 |
354 | const focusableSelector = rovingDirection === 'v' ? config.datasetNameColSelector(col) : config.datasetNameRowSelector(row);
355 | focusableList = Array.from(el.querySelectorAll(focusableSelector));
356 |
357 | focusableMap = new WeakMap(
358 | focusableList.map((o) => {
359 | const r = extractNumber(o.dataset[config.datasetNameRow]);
360 | const c = extractNumber(o.dataset[config.datasetNameCol]);
361 | let val;
362 |
363 | if (rovingDirection === 'v') {
364 | if (r !== row || c === col) {
365 | val = 1000 * r + 1 * c;
366 | }
367 | } else if (c !== col || r === row) {
368 | val = 1000 * c + 1 * r;
369 | }
370 |
371 | return [o, val];
372 | }),
373 | );
374 | } else if (el.matches('[role="grid"]') === true && activeElement.matches('[role="row"] [role="gridcell"]')) {
375 | const rows = Array.from(el.querySelectorAll('[role="row"]'));
376 | const elToRowCol = new WeakMap();
377 | const rowsCells = rows.map((r, rIndex) => {
378 | const cols = Array.from(r.querySelectorAll('[role="gridcell"]'));
379 |
380 | cols.forEach((o, cIndex) => {
381 | elToRowCol.set(o, [rIndex + 1, cIndex + 1]);
382 | });
383 |
384 | return cols;
385 | });
386 | const curRow = activeElement.closest('[role="row"]');
387 | const row = rows.indexOf(curRow) + 1;
388 | const col = rowsCells[row - 1].indexOf(activeElement) + 1;
389 |
390 | const { focusableSelector } = config;
391 | focusableList = Array.from(el.querySelectorAll(focusableSelector));
392 |
393 | focusableMap = new WeakMap(
394 | focusableList.map((o) => {
395 | const [r, c] = elToRowCol.get(o) || [null, null];
396 | let val;
397 |
398 | if (rovingDirection === 'v') {
399 | if (c === col) {
400 | val = 1 * r;
401 | }
402 | } else if (r === row) {
403 | val = 1 * c;
404 | }
405 |
406 | return [o, val];
407 | }),
408 | );
409 | }
410 |
411 | if (focusableMap != null && rovingExit == null) {
412 | focusableList = focusableList.filter((o) => focusableMap.get(o) !== undefined);
413 | focusableList.sort((el1, el2) => focusableMap.get(el1) - focusableMap.get(el2));
414 | }
415 | }
416 |
417 | if (focusableList.length === 0) {
418 | const { focusableSelector } = config;
419 | focusableList = Array.from(el.querySelectorAll(focusableSelector));
420 |
421 | if (modifiers.indexorder === true && rovingExit == null) {
422 | const tabindexOrder = new WeakMap(
423 | focusableList.map((o) => ([o, Math.max(o.tabIndex || 0, 0)])),
424 | );
425 |
426 | focusableList.sort((el1, el2) => tabindexOrder.get(el1) - tabindexOrder.get(el2));
427 | }
428 |
429 | if (el.matches(focusableSelector)) {
430 | focusableList.unshift(el);
431 | }
432 | }
433 |
434 | const focusableIndexLast = focusableList.length - 1;
435 |
436 | let focusableIndex = indexSelector(focusableList.indexOf(activeElement), focusableIndexLast);
437 |
438 | for (let i = 0; i < focusableIndexLast; i += 1) {
439 | focusableIndex += step;
440 |
441 | if (focusableIndex < 0) {
442 | focusableIndex = focusableIndexLast;
443 | } else if (focusableIndex > focusableIndexLast) {
444 | focusableIndex = 0;
445 | }
446 |
447 | if (focus(focusableList[focusableIndex]) === true) {
448 | if (rovingExit !== false) {
449 | setActiveTrapEl(rovingExit, config);
450 | }
451 |
452 | return;
453 | }
454 | }
455 | },
456 |
457 | overwriteFocusTarget(ev) {
458 | if (ctx.disable === false && ev.__vKbdTrap !== true) {
459 | ev.__vKbdTrap = true;
460 |
461 | ctx.focusTarget = ev.target;
462 | }
463 | },
464 |
465 | refocus(onlyIfTrapEl) {
466 | if (
467 | ctx.disable === false
468 | && activeTrapEl === el
469 | && ctx.focusTarget
470 | ) {
471 | let trapEl = ctx.focusTarget.closest(config.datasetNameSelector);
472 |
473 | while (trapEl && trapEl !== el) {
474 | const newCtx = getCtx(trapEl);
475 |
476 | if (newCtx !== null && newCtx.disable === false && newCtx.focusTarget) {
477 | setActiveTrapEl(trapEl, config);
478 | return newCtx.refocus(onlyIfTrapEl !== undefined ? newCtx.modifiers.roving !== true : undefined);
479 | }
480 |
481 | trapEl = trapEl.parentElement && trapEl.parentElement.closest(config.datasetNameSelector);
482 | }
483 |
484 | if (ctx.focusTarget.tabIndex === config.trapTabIndex || ctx.focusTarget.matches('dialog') === true || ctx.focusTarget.matches('[popover]') === true) {
485 | return (ctx.modifiers.autofocus === true && focus(el.querySelector(config.autofocusSelector)) === true)
486 | || focus(el.querySelector(config.focusableSelector)) === true
487 | || focus(ctx.focusTarget) === true;
488 | }
489 |
490 | return onlyIfTrapEl === true
491 | ? false
492 | : focus(ctx.focusTarget) === true
493 | || focus(el.querySelector(config.focusableSelector)) === true;
494 | }
495 |
496 | return false;
497 | },
498 |
499 | autofocus() {
500 | setActiveTrapEl(el, config);
501 |
502 | if (ctx.disable === false && focus(el.querySelector(config.autofocusSelector), visibleFocusCheckFn) === false) {
503 | focus(el.querySelector(config.focusableSelector), visibleFocusCheckFn);
504 | }
505 | },
506 | };
507 |
508 | return ctx;
509 | }
510 |
511 | function bindFn(config, el, value, modifiers) {
512 | const ctx = createCtx(config, el, value, modifiers);
513 |
514 | ctx.bind();
515 |
516 | if (modifiers.autofocus === true) {
517 | ctx.autofocus();
518 | }
519 | }
520 |
521 | function updateFn(config, ctx, el, value, modifiers) {
522 | const disable = value === false;
523 |
524 | ctx.modifiers = modifiers;
525 |
526 | setAttributes(el, disable, ctx, config);
527 |
528 | if (activeTrapEl === el) {
529 | if (disable === true) {
530 | setActiveTrapEl(null, config);
531 | } else {
532 | el.dataset[config.datasetNameActive] = '';
533 | }
534 | }
535 |
536 | if (ctx.disable !== disable) {
537 | ctx.disable = disable;
538 |
539 | if (modifiers.autofocus === true) {
540 | ctx.autofocus();
541 | } else if (disable === false && activeTrapEl !== el && el.contains(document.activeElement) === true) {
542 | setActiveTrapEl(el, config);
543 | }
544 | }
545 | }
546 |
547 | function unbindFn(config, el) {
548 | const ctx = getCtx(el);
549 |
550 | if (ctx !== null) {
551 | ctx.unbind();
552 | }
553 |
554 | if (activeTrapEl === el) {
555 | if (ctx.relatedFocusTarget) {
556 | focus(ctx.relatedFocusTarget);
557 | }
558 | setActiveTrapEl(null, config);
559 | }
560 | }
561 |
562 | export default function directiveFactory(options) {
563 | const config = createConfig(options);
564 |
565 | const mounted = (el, { value, modifiers }) => bindFn(config, el, value, modifiers);
566 |
567 | const updated = (el, { value, modifiers }) => {
568 | const ctx = getCtx(el);
569 |
570 | if (ctx !== null) {
571 | updateFn(config, ctx, el, value, modifiers);
572 | } else if (isVue3) {
573 | mounted(el, { value, modifiers });
574 | } else if (activeTrapEl === el) {
575 | setActiveTrapEl(null, config);
576 | }
577 | };
578 |
579 | const unmounted = (el) => unbindFn(config, el);
580 |
581 | return isVue3
582 | ? markRaw({
583 | name: config.name,
584 |
585 | directive: {
586 | mounted,
587 | updated,
588 | unmounted,
589 | getSSRProps() { },
590 | },
591 | })
592 | : {
593 | name: config.name,
594 |
595 | directive: {
596 | bind: mounted,
597 | update: updated,
598 | unbind: unmounted,
599 | },
600 | };
601 | }
602 |
603 | // helper because vue-demi does not have it
604 | function toValue(maybeRef) {
605 | return typeof maybeRef === 'function'
606 | ? maybeRef()
607 | : unref(maybeRef);
608 | }
609 |
610 | export function composableFactory(options) {
611 | const config = createConfig(options);
612 |
613 | return (maybeElementOrComponentRefOrComputed, modifiersRefOrComputed = {}, activeRefOrComputed = true) => {
614 | const elComputed = computed(() => {
615 | const elOrComponent = toValue(maybeElementOrComponentRefOrComputed);
616 | if (elOrComponent == null) {
617 | return null;
618 | }
619 | return markRaw('$el' in elOrComponent ? elOrComponent.$el : elOrComponent);
620 | });
621 |
622 | const unwatch = watch(() => [elComputed.value, toValue(activeRefOrComputed), toValue(modifiersRefOrComputed)], ([el, value, modifiers], [oldEl] = []) => {
623 | if (el == null && oldEl == null) {
624 | return;
625 | }
626 |
627 | if (oldEl == null && el != null) {
628 | bindFn(config, el, value, modifiers);
629 | } else if (el == null) {
630 | unbindFn(config, el);
631 | } else if (el !== oldEl) {
632 | unbindFn(config, oldEl);
633 | bindFn(config, el, value, modifiers);
634 | } else {
635 | updateFn(config, getCtx(el), el, value, modifiers);
636 | }
637 | }, { flush: 'sync', deep: true, immediate: true });
638 |
639 | if (getCurrentScope()) {
640 | onScopeDispose(() => {
641 | unwatch();
642 | if (elComputed.value != null) {
643 | unbindFn(config, elComputed.value);
644 | }
645 | });
646 | }
647 | };
648 | }
649 |
--------------------------------------------------------------------------------
/src/directives/keyboard-trap/helpers.js:
--------------------------------------------------------------------------------
1 | function defaultFocusCheckFn() {
2 | return true;
3 | }
4 |
5 | export function visibleFocusCheckFn(el, scrolled = false) {
6 | if (el.closest('dialog') != null) {
7 | return true;
8 | }
9 |
10 | const {
11 | left,
12 | right,
13 | top,
14 | bottom,
15 | } = el.getBoundingClientRect();
16 |
17 | if (left === right && top === bottom) {
18 | return true;
19 | }
20 |
21 | const posList = [
22 | [left, top],
23 | [left, (top + bottom) / 2],
24 | [left, bottom],
25 | [(left + right) / 2, top],
26 | [(left + right) / 2, (top + bottom) / 2],
27 | [(left + right) / 2, bottom],
28 | [right, top],
29 | [right, (top + bottom) / 2],
30 | [right, bottom],
31 | ];
32 |
33 | let elAtPosFound = false;
34 |
35 | for (let i = 0; i < 9; i += 1) {
36 | const elAtPos = document.elementFromPoint(...posList[i]);
37 |
38 | if (el.contains(elAtPos) === true) {
39 | return true;
40 | }
41 |
42 | if (elAtPos != null) {
43 | elAtPosFound = true;
44 | }
45 | }
46 |
47 | if (scrolled === true || typeof el.scrollIntoView !== 'function') {
48 | return !elAtPosFound;
49 | }
50 |
51 | const scrollPos = [];
52 | let parent = el.parentElement;
53 |
54 | while (parent != null) {
55 | scrollPos.push([parent, parent.scrollLeft, parent.scrollTop]);
56 | parent = parent.parentElement;
57 | }
58 |
59 | el.scrollIntoView();
60 |
61 | const visible = visibleFocusCheckFn(el, true);
62 |
63 | for (let i = scrollPos.length - 1; i >= 0; i -= 1) {
64 | const [scrollEl, scrollLeft, scrollTop] = scrollPos[i];
65 | scrollEl.scrollLeft = scrollLeft;
66 | scrollEl.scrollTop = scrollTop;
67 | }
68 |
69 | return visible;
70 | }
71 |
72 | let focusTargetEl;
73 | export function focus(el, checkFn = defaultFocusCheckFn) {
74 | if (el == null || typeof el.focus !== 'function' || checkFn(el) !== true) {
75 | return false;
76 | }
77 |
78 | focusTargetEl = el;
79 | el.focus();
80 |
81 | return [focusTargetEl, el].includes(document.activeElement)
82 | || (document.activeElement != null && [focusTargetEl, el].includes(document.activeElement.__focusTargetPlaceholder));
83 | }
84 |
85 | const reNumber = /(\d+)/;
86 |
87 | export function extractNumber(val) {
88 | const match = reNumber.exec(val);
89 |
90 | return match == null ? '' : match[1];
91 | }
92 |
93 | export function dirIsRtl(activeElement, currentTrapEl) {
94 | const dirEl = (
95 | activeElement && activeElement !== currentTrapEl
96 | ? activeElement.parentElement || currentTrapEl
97 | : currentTrapEl
98 | ).closest('[dir="rtl"],[dir="ltr"]');
99 |
100 | return dirEl && dirEl.matches('[dir="rtl"]');
101 | }
102 |
--------------------------------------------------------------------------------
/src/directives/keyboard-trap/index.js:
--------------------------------------------------------------------------------
1 | import directiveFactory, { composableFactory } from './directive';
2 |
3 | const VueKeyboardTrapDirectivePlugin = {
4 | install(app, options) {
5 | const { name, directive } = directiveFactory(options);
6 |
7 | app.directive(name, directive);
8 | },
9 | };
10 |
11 | export {
12 | VueKeyboardTrapDirectivePlugin,
13 | directiveFactory as VueKeyboardTrapDirectiveFactory,
14 | composableFactory as useKeyboardTrapFactory,
15 | };
16 |
17 | export default VueKeyboardTrapDirectivePlugin;
18 |
--------------------------------------------------------------------------------
/src/directives/keyboard-trap/options.js:
--------------------------------------------------------------------------------
1 | // options:
2 | // name: snake-case name of the directive (without `v-` prefix) - default `kbd-trap`
3 | // datasetName: camelCase name of the `data-attribute` to be set on element when trap is enabled - default `v${ PascalCase from name}`
4 | //
5 | // focusableSelector: CSS selector for focusable elements
6 | // rovingSkipSelector: CSS selector for elements that should not respond to roving key navigation (input, textarea, ...)
7 | // gridSkipSelector: CSS selector that will be applied in .roving.grid mode to exclude elements - must be a series of :not() selectors
8 | // autofocusSelector: CSS selector for the elements that should be autofocused
9 | // trapTabIndex: tabIndex value to be used when trap element has a tabIndex of -1 and has no `tabindex` attribute (default -9999)
10 |
11 | function createConfig(options) {
12 | const config = {
13 | name: 'kbd-trap',
14 |
15 | focusableSelector: [':focus']
16 | .concat(
17 | [
18 | 'a[href]',
19 | 'area[href]',
20 | 'audio[controls]',
21 | 'video[controls]',
22 | 'iframe',
23 | '[tabindex]:not(slot)',
24 | '[contenteditable]:not([contenteditable="false"])',
25 | 'details > summary:first-of-type',
26 | ].map((s) => `${ s }:not([tabindex^="-"])`),
27 | )
28 | .concat(
29 | [
30 | 'input:not([type="hidden"]):not(fieldset[disabled] input)',
31 | 'select:not(fieldset[disabled] select)',
32 | 'textarea:not(fieldset[disabled] textarea)',
33 | 'button:not(fieldset[disabled] button)',
34 | '[class*="focusable"]',
35 | ].map((s) => `${ s }:not([disabled]):not([tabindex^="-"])`),
36 | )
37 | .concat(
38 | [
39 | 'input:not([type="hidden"])',
40 | 'select',
41 | 'textarea',
42 | 'button',
43 | ].map((s) => `fieldset[disabled]:not(fieldset[disabled] fieldset) > legend ${ s }:not([disabled]):not([tabindex^="-"])`),
44 | )
45 | .join(','),
46 |
47 | rovingSkipSelector: [
48 | 'input:not([disabled]):not([type="button"]):not([type="checkbox"]):not([type="file"]):not([type="image"]):not([type="radio"]):not([type="reset"]):not([type="submit"])',
49 | 'select:not([disabled])',
50 | 'select:not([disabled]) *',
51 | 'textarea:not([disabled])',
52 | '[contenteditable]:not([contenteditable="false"])',
53 | '[contenteditable]:not([contenteditable="false"]) *',
54 | ].join(','),
55 |
56 | gridSkipSelector: [
57 | ':not([disabled])',
58 | ':not([tabindex^="-"])',
59 | ].join(''),
60 |
61 | autofocusSelector: [
62 | '[autofocus]:not([autofocus="false"])',
63 | '[data-autofocus]:not([data-autofocus="false"])',
64 | ].map((s) => `${ s }:not([disabled])`).join(','),
65 |
66 | trapTabIndex: -9999,
67 |
68 | ...options,
69 | };
70 |
71 | const pascalName = config.name
72 | .toLocaleLowerCase()
73 | .split(/[^a-z0-9]+/)
74 | .filter((t) => t.length > 0)
75 | .map((t) => `${ t[0].toLocaleUpperCase() }${ t.slice(1) }`)
76 | .join('');
77 |
78 | if (config.datasetName === undefined) {
79 | config.datasetName = `v${ pascalName }`;
80 | }
81 |
82 | config.datasetNameActive = `${ config.datasetName }Active`;
83 | config.datasetNamePreventRefocus = `${ config.datasetName }PreventRefocus`;
84 |
85 | if (typeof window === 'undefined') {
86 | return config;
87 | }
88 |
89 | const dsEl = document.createElement('span');
90 | dsEl.dataset[config.datasetName] = '';
91 | const datasetNameSnake = dsEl.getAttributeNames()[0];
92 |
93 | config.datasetNameSelector = `[${ datasetNameSnake }]`;
94 | config.datasetNameSelectorRovingHorizontal = `[${ datasetNameSnake }~="roving"][${ datasetNameSnake }~="horizontal"],[${ datasetNameSnake }~="roving"]:not([${ datasetNameSnake }~="vertical"])`;
95 | config.datasetNameSelectorRovingVertical = `[${ datasetNameSnake }~="roving"][${ datasetNameSnake }~="vertical"],[${ datasetNameSnake }~="roving"]:not([${ datasetNameSnake }~="horizontal"])`;
96 |
97 | config.datasetNameRow = `${ config.datasetName }Row`;
98 | config.datasetNameRowSelector = (i) => `:focus,[${ datasetNameSnake }-row~="${ i }"]${ config.gridSkipSelector },[${ datasetNameSnake }-row~="*"]${ config.gridSkipSelector }`;
99 |
100 | config.datasetNameCol = `${ config.datasetName }Col`;
101 | config.datasetNameColSelector = (i) => `:focus,[${ datasetNameSnake }-col~="${ i }"]${ config.gridSkipSelector },[${ datasetNameSnake }-col~="*"]${ config.gridSkipSelector }`;
102 |
103 | return config;
104 | }
105 |
106 | export { createConfig };
107 |
--------------------------------------------------------------------------------
/src/exports.js:
--------------------------------------------------------------------------------
1 | import {
2 | useKeyboardTrapFactory,
3 | VueKeyboardTrapDirectivePlugin,
4 | VueKeyboardTrapDirectiveFactory,
5 | } from './directives/keyboard-trap/index';
6 |
7 | export default VueKeyboardTrapDirectivePlugin;
8 |
9 | export {
10 | useKeyboardTrapFactory,
11 | VueKeyboardTrapDirectivePlugin,
12 | VueKeyboardTrapDirectiveFactory,
13 | };
14 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue';
2 | import {
3 | VueKeyboardTrapDirectivePlugin,
4 | // VueKeyboardTrapDirectiveFactory,
5 | } from './exports.js';
6 | import App from './App.vue';
7 |
8 | import './public/styles/index.sass';
9 |
10 | const app = createApp(App);
11 |
12 | app.use(VueKeyboardTrapDirectivePlugin);
13 |
14 | // const { name, directive } = VueKeyboardTrapDirectiveFactory();
15 | // app.directive(name, directive);
16 |
17 | app.mount('#app');
18 |
--------------------------------------------------------------------------------
/src/public/styles/index.css:
--------------------------------------------------------------------------------
1 | @charset "UTF-8";
2 |
3 | [data-v-kbd-trap] {
4 | --v-kbd-trap: var(--text-v-kbd-trap-enabled, "Trap");
5 | --v-kbd-trap-esc: "";
6 | --v-kbd-trap-tab: "";
7 | --v-kbd-trap-roving: "";
8 | }
9 |
10 | [data-v-kbd-trap]:where(:has(:focus-visible)) {
11 | --v-kbd-trap: var(--text-v-kbd-trap-enabled, "Trap") var(--text-v-kbd-trap-separator, "/");
12 | --v-kbd-trap-esc: var(--text-v-kbd-trap-esc, "Esc");
13 | }
14 |
15 | [data-v-kbd-trap]:where(:has(:focus-visible)):after {
16 | content: var(--v-kbd-trap, "") var(--v-kbd-trap-esc, "") var(--v-kbd-trap-tab, "") var(--v-kbd-trap-roving, "");
17 | pointer-events: none;
18 | position: absolute;
19 | top: 2px;
20 | right: 2px;
21 | font: italic small-caps bold 14px monospace;
22 | line-height: 1em;
23 | padding: 4px;
24 | background-color: var(--color-v-kbd-trap-background, rgba(238, 238, 238, 0.9333333333));
25 | border-radius: 2px;
26 | z-index: 1;
27 | }
28 |
29 | [data-v-kbd-trap]:where([tabindex="-9999"], dialog, [popover]) {
30 | outline: none;
31 | }
32 |
33 | [data-v-kbd-trap]:after {
34 | color: var(--color-v-kbd-trap-disabled, #999);
35 | }
36 |
37 | [data-v-kbd-trap]:where([data-v-kbd-trap-active]):after {
38 | color: var(--color-v-kbd-trap-enabled, #c33);
39 | }
40 |
41 | [data-v-kbd-trap]:where([data-v-kbd-trap-active]) {
42 | --v-kbd-trap: "";
43 | --v-kbd-trap-esc: var(--text-v-kbd-trap-esc, "Esc");
44 | --v-kbd-trap-tab: var(--text-v-kbd-trap-separator, "/") var(--text-v-kbd-trap-tab, "Tab");
45 | --v-kbd-trap-roving: "";
46 | }
47 |
48 | [data-v-kbd-trap]:where([data-v-kbd-trap-active]):where([data-v-kbd-trap~=roving]) {
49 | --v-kbd-trap-tab: var(--text-v-kbd-trap-separator, "/") var(--text-v-kbd-trap-tab-exits, "Tab⇅");
50 | --v-kbd-trap-roving: var(--text-v-kbd-trap-separator, "/") var(--text-v-kbd-trap-arrows-all, "⥢⥣⥥⥤");
51 | }
52 |
53 | [data-v-kbd-trap]:where([data-v-kbd-trap-active]):where([data-v-kbd-trap~=roving]):where([data-v-kbd-trap~=tabinside]) {
54 | --v-kbd-trap-tab: var(--text-v-kbd-trap-separator, "/") var(--text-v-kbd-trap-tab, "Tab");
55 | }
56 |
57 | [data-v-kbd-trap]:where([data-v-kbd-trap-active]):where([data-v-kbd-trap~=roving]):where([data-v-kbd-trap~=vertical]) {
58 | --v-kbd-trap-roving: var(--text-v-kbd-trap-separator, "/") var(--text-v-kbd-trap-arrows-vertical, "⥣⥥");
59 | }
60 |
61 | [data-v-kbd-trap]:where([data-v-kbd-trap-active]):where([data-v-kbd-trap~=roving]):where([data-v-kbd-trap~=horizontal]) {
62 | --v-kbd-trap-roving: var(--text-v-kbd-trap-separator, "/") var(--text-v-kbd-trap-arrows-horizontal, "⥢⥤");
63 | }
64 |
65 | [data-v-kbd-trap]:where([data-v-kbd-trap-active]):where([data-v-kbd-trap~=roving]):where([data-v-kbd-trap~=grid], [role=grid]) {
66 | --v-kbd-trap-roving: var(--text-v-kbd-trap-separator, "/") var(--text-v-kbd-trap-grid, "⊞");
67 | }
68 |
69 | [data-v-kbd-trap]:where([data-v-kbd-trap-active]):where([data-v-kbd-trap~=roving]):has(input:not([disabled]):not([type=button]):not([type=checkbox]):not([type=file]):not([type=image]):not([type=radio]):not([type=reset]):not([type=submit]):focus-visible, select:not([disabled]):focus-visible, select:not([disabled]) *:focus-visible, textarea:not([disabled]):focus-visible, [contenteditable]:not([contenteditable=false]):focus-visible, [contenteditable]:not([contenteditable=false]) *:focus-visible) {
70 | --v-kbd-trap-tab: var(--text-v-kbd-trap-separator, "/") var(--text-v-kbd-trap-tab, "Tab");
71 | --v-kbd-trap-roving: "";
72 | }
73 |
74 | [data-v-kbd-trap]:where([data-v-kbd-trap-active]):where([data-v-kbd-trap~=escrefocus]) {
75 | --v-kbd-trap-esc: var(--text-v-kbd-trap-esc-refocus, "Esc⥉");
76 | }
77 |
78 | [data-v-kbd-trap]:where([data-v-kbd-trap-active]):where([data-v-kbd-trap~=escexits]) {
79 | --v-kbd-trap-esc: var(--text-v-kbd-trap-esc-exits, "Esc⤣");
80 | }
81 |
82 | [data-v-kbd-trap]:where([data-v-kbd-trap~=roving][data-v-kbd-trap~=horizontal]):has([data-v-kbd-trap-active][data-v-kbd-trap~=roving][data-v-kbd-trap~=vertical]) {
83 | --v-kbd-trap-roving: var(--text-v-kbd-trap-separator, "/") var(--text-v-kbd-trap-arrows-horizontal, "⥢⥤");
84 | }
85 |
86 | [data-v-kbd-trap]:where([data-v-kbd-trap~=roving][data-v-kbd-trap~=horizontal]):has([data-v-kbd-trap-active][data-v-kbd-trap~=roving][data-v-kbd-trap~=vertical]):after {
87 | color: var(--color-v-kbd-trap-enabled, #c33);
88 | }
89 |
90 | [data-v-kbd-trap]:where([data-v-kbd-trap~=roving][data-v-kbd-trap~=vertical]):has([data-v-kbd-trap-active][data-v-kbd-trap~=roving][data-v-kbd-trap~=horizontal]) {
91 | --v-kbd-trap-roving: var(--text-v-kbd-trap-separator, "/") var(--text-v-kbd-trap-arrows-vertical, "⥣⥥");
92 | }
93 |
94 | [data-v-kbd-trap]:where([data-v-kbd-trap~=roving][data-v-kbd-trap~=vertical]):has([data-v-kbd-trap-active][data-v-kbd-trap~=roving][data-v-kbd-trap~=horizontal]):after {
95 | color: var(--color-v-kbd-trap-enabled, #c33);
96 | }
97 |
--------------------------------------------------------------------------------
/src/public/styles/index.sass:
--------------------------------------------------------------------------------
1 | @charset "UTF-8"
2 |
3 | $ColorVKeyboardTrapEnabled: #c33 !default
4 | $ColorVKeyboardTrapDisabled: #999 !default
5 | $ColorVKeyboardTrapBackground: #eeee !default
6 |
7 | $TextVKeyboardTrapSeparator: "/" !default
8 | $TextVKeyboardTrapEnabled: "Trap" !default
9 | $TextVKeyboardTrapEsc: "Esc" !default
10 | $TextVKeyboardTrapEscRefocus: "Esc\2949" !default
11 | $TextVKeyboardTrapEscExits: "Esc\2923" !default
12 | $TextVKeyboardTrapTab: "Tab" !default
13 | $TextVKeyboardTrapTabExits: "Tab\21C5" !default
14 | $TextVKeyboardTrapGrid: "\229E" !default
15 | $TextVKeyboardTrapArrowsAll: "\2962\2963\2965\2964" !default
16 | $TextVKeyboardTrapArrowsHorizontal: "\2962\2964" !default
17 | $TextVKeyboardTrapArrowsVertical: "\2963\2965" !default
18 |
19 | // :root
20 | // --color-v-kbd-trap-enabled: #c33
21 | // --color-v-kbd-trap-disabled: #999
22 | // --color-v-kbd-trap-background: #eeee
23 | // --text-v-kbd-trap-separator: "/"
24 | // --text-v-kbd-trap-enabled: "Trap"
25 | // --text-v-kbd-trap-esc: "Esc"
26 | // --text-v-kbd-trap-esc-refocus: "Esc\2949"
27 | // --text-v-kbd-trap-esc-exits: "Esc\2923"
28 | // --text-v-kbd-trap-tab: "Tab"
29 | // --text-v-kbd-trap-tab-exits: "Tab\21C5"
30 | // --text-v-kbd-trap-grid: "\229E"
31 | // --text-v-kbd-trap-arrows-all: "\2962\2963\2965\2964"
32 | // --text-v-kbd-trap-arrows-horizontal: "\2962\2964"
33 | // --text-v-kbd-trap-arrows-vertical: "\2963\2965"
34 |
35 | [data-v-kbd-trap]
36 | --v-kbd-trap: var(--text-v-kbd-trap-enabled, "#{$TextVKeyboardTrapEnabled}")
37 | --v-kbd-trap-esc: ""
38 | --v-kbd-trap-tab: ""
39 | --v-kbd-trap-roving: ""
40 |
41 | &:where(:has(:focus-visible))
42 | --v-kbd-trap: var(--text-v-kbd-trap-enabled, "#{$TextVKeyboardTrapEnabled}") var(--text-v-kbd-trap-separator, "#{$TextVKeyboardTrapSeparator}")
43 | --v-kbd-trap-esc: var(--text-v-kbd-trap-esc, "#{$TextVKeyboardTrapEsc}")
44 |
45 | &:after
46 | content: var(--v-kbd-trap, "") var(--v-kbd-trap-esc, "") var(--v-kbd-trap-tab, "") var(--v-kbd-trap-roving, "")
47 | pointer-events: none
48 | position: absolute
49 | top: 2px
50 | right: 2px
51 | font: italic small-caps bold 14px monospace
52 | line-height: 1em
53 | padding: 4px
54 | background-color: var(--color-v-kbd-trap-background, #{$ColorVKeyboardTrapBackground})
55 | border-radius: 2px
56 | z-index: 1
57 |
58 | &:where([tabindex="-9999"], dialog, [popover])
59 | outline: none
60 |
61 | &:after
62 | color: var(--color-v-kbd-trap-disabled, #{$ColorVKeyboardTrapDisabled})
63 |
64 | &:where([data-v-kbd-trap-active]):after
65 | color: var(--color-v-kbd-trap-enabled, #{$ColorVKeyboardTrapEnabled})
66 |
67 | &:where([data-v-kbd-trap-active])
68 | --v-kbd-trap: ""
69 | --v-kbd-trap-esc: var(--text-v-kbd-trap-esc, "#{$TextVKeyboardTrapEsc}")
70 | --v-kbd-trap-tab: var(--text-v-kbd-trap-separator, "#{$TextVKeyboardTrapSeparator}") var(--text-v-kbd-trap-tab, "#{$TextVKeyboardTrapTab}")
71 | --v-kbd-trap-roving: ""
72 |
73 | &:where([data-v-kbd-trap~=roving])
74 | --v-kbd-trap-tab: var(--text-v-kbd-trap-separator, "#{$TextVKeyboardTrapSeparator}") var(--text-v-kbd-trap-tab-exits, "#{$TextVKeyboardTrapTabExits}")
75 | --v-kbd-trap-roving: var(--text-v-kbd-trap-separator, "#{$TextVKeyboardTrapSeparator}") var(--text-v-kbd-trap-arrows-all, "#{$TextVKeyboardTrapArrowsAll}")
76 |
77 | &:where([data-v-kbd-trap~=tabinside])
78 | --v-kbd-trap-tab: var(--text-v-kbd-trap-separator, "#{$TextVKeyboardTrapSeparator}") var(--text-v-kbd-trap-tab, "#{$TextVKeyboardTrapTab}")
79 |
80 | &:where([data-v-kbd-trap~=vertical])
81 | --v-kbd-trap-roving: var(--text-v-kbd-trap-separator, "#{$TextVKeyboardTrapSeparator}") var(--text-v-kbd-trap-arrows-vertical, "#{$TextVKeyboardTrapArrowsVertical}")
82 |
83 | &:where([data-v-kbd-trap~=horizontal])
84 | --v-kbd-trap-roving: var(--text-v-kbd-trap-separator, "#{$TextVKeyboardTrapSeparator}") var(--text-v-kbd-trap-arrows-horizontal, "#{$TextVKeyboardTrapArrowsHorizontal}")
85 |
86 | &:where([data-v-kbd-trap~=grid], [role=grid])
87 | --v-kbd-trap-roving: var(--text-v-kbd-trap-separator, "#{$TextVKeyboardTrapSeparator}") var(--text-v-kbd-trap-grid, "#{$TextVKeyboardTrapGrid}")
88 |
89 | &:has(input:not([disabled]):not([type="button"]):not([type="checkbox"]):not([type="file"]):not([type="image"]):not([type="radio"]):not([type="reset"]):not([type="submit"]):focus-visible, select:not([disabled]):focus-visible, select:not([disabled]) *:focus-visible, textarea:not([disabled]):focus-visible, [contenteditable]:not([contenteditable="false"]):focus-visible, [contenteditable]:not([contenteditable="false"]) *:focus-visible)
90 | --v-kbd-trap-tab: var(--text-v-kbd-trap-separator, "#{$TextVKeyboardTrapSeparator}") var(--text-v-kbd-trap-tab, "#{$TextVKeyboardTrapTab}")
91 | --v-kbd-trap-roving: ""
92 |
93 | &:where([data-v-kbd-trap~=escrefocus])
94 | --v-kbd-trap-esc: var(--text-v-kbd-trap-esc-refocus, "#{$TextVKeyboardTrapEscRefocus}")
95 |
96 | &:where([data-v-kbd-trap~=escexits])
97 | --v-kbd-trap-esc: var(--text-v-kbd-trap-esc-exits, "#{$TextVKeyboardTrapEscExits}")
98 |
99 | &:where([data-v-kbd-trap~=roving][data-v-kbd-trap~=horizontal]):has([data-v-kbd-trap-active][data-v-kbd-trap~=roving][data-v-kbd-trap~=vertical])
100 | --v-kbd-trap-roving: var(--text-v-kbd-trap-separator, "#{$TextVKeyboardTrapSeparator}") var(--text-v-kbd-trap-arrows-horizontal, "#{$TextVKeyboardTrapArrowsHorizontal}")
101 | &:after
102 | color: var(--color-v-kbd-trap-enabled, #{$ColorVKeyboardTrapEnabled})
103 |
104 | &:where([data-v-kbd-trap~=roving][data-v-kbd-trap~=vertical]):has([data-v-kbd-trap-active][data-v-kbd-trap~=roving][data-v-kbd-trap~=horizontal])
105 | --v-kbd-trap-roving: var(--text-v-kbd-trap-separator, "#{$TextVKeyboardTrapSeparator}") var(--text-v-kbd-trap-arrows-vertical, "#{$TextVKeyboardTrapArrowsVertical}")
106 | &:after
107 | color: var(--color-v-kbd-trap-enabled, #{$ColorVKeyboardTrapEnabled})
108 |
--------------------------------------------------------------------------------
/src/public/types/index.d.ts:
--------------------------------------------------------------------------------
1 | import type { App, ComponentPublicInstance, MaybeRefOrGetter } from 'vue';
2 |
3 | export interface IVueKeyboardTrapDirectiveOptions {
4 | name?: string;
5 | datasetName?: string;
6 | focusableSelector?: string;
7 | rovingSkipSelector?: string;
8 | gridSkipSelector?: string;
9 | autofocusSelector?: string;
10 | trapTabIndex?: number;
11 | }
12 |
13 | export interface IUseKeyboardTrapModifiers {
14 | autofocus: boolean;
15 | escexits: boolean;
16 | escrefocus: boolean;
17 | grid: boolean;
18 | horizontal: boolean;
19 | indexorder: boolean;
20 | roving: boolean;
21 | tabinside: boolean;
22 | vertical: boolean;
23 | }
24 |
25 | type IVueDirectivePlugin = {
26 | install( app: App, options: IVueKeyboardTrapDirectiveOptions ): void,
27 | };
28 |
29 | export const VueKeyboardTrapDirectivePlugin: IVueDirectivePlugin;
30 | export function VueKeyboardTrapDirectiveFactory(options?: IVueKeyboardTrapDirectiveOptions): { name: string, directive: object; };
31 |
32 | export type IUseKeyboardTrap = (
33 | el: MaybeRefOrGetter,
34 | modifiers?: MaybeRefOrGetter>,
35 | active?: MaybeRefOrGetter,
36 | ) => void;
37 | export function useKeyboardTrapFactory(options?: IVueKeyboardTrapDirectiveOptions): IUseKeyboardTrap
38 |
39 | export default VueKeyboardTrapDirectivePlugin;
40 |
--------------------------------------------------------------------------------
/src/public/web-types/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/web-types",
3 | "framework": "vue",
4 | "name": "vue-pdan",
5 | "version": "1.0.0",
6 | "js-types-syntax": "typescript",
7 | "contributions": {
8 | "html": {
9 | "vue-directives": [
10 | {
11 | "name": "v-kbd-trap",
12 | "source": {
13 | "module": "vue-pdan",
14 | "symbol": "KbdTrap"
15 | },
16 | "required": false,
17 | "description": "VueKeyboardTrap - directive for keyboard navigation - roving movement and trapping inside container",
18 | "doc-url": "https://pdanpdan.github.io/vue-keyboard-trap/",
19 | "vue-modifiers": [
20 | {
21 | "name": "autofocus",
22 | "description": "Autofocuses the element with [autofocus] or [data-autofocus] attribute when the directive is mounted or enabled (only if it not covered by another element)",
23 | "doc-url": "https://pdanpdan.github.io/vue-keyboard-trap/"
24 | },
25 | {
26 | "name": "roving",
27 | "description": "Allow roving navigation (Home, End, ArrowKeys) - it's the same as using .roving.vertical.horizontal",
28 | "doc-url": "https://pdanpdan.github.io/vue-keyboard-trap/"
29 | },
30 | {
31 | "name": "vertical",
32 | "description": "Used with .roving - allow roving navigation (Home, End, ArrowUp, ArrowDown)",
33 | "doc-url": "https://pdanpdan.github.io/vue-keyboard-trap/"
34 | },
35 | {
36 | "name": "horizontal",
37 | "description": "Used with .roving - allow roving navigation (Home, End, ArrowLeft, ArrowRight)",
38 | "doc-url": "https://pdanpdan.github.io/vue-keyboard-trap/"
39 | },
40 | {
41 | "name": "grid",
42 | "description": "Used with .roving - allow roving navigation (Home, End, ArrowKeys) using dataset attrs on elements [data-${ camelCase from datasetName }-(row|col)]; [data-${ camelCase from datasetName }-(row|col)~=\"*\"] is a catchall",
43 | "doc-url": "https://pdanpdan.github.io/vue-keyboard-trap/"
44 | },
45 | {
46 | "name": "tabinside",
47 | "description": "Used with .roving - Tab key navigates to next/prev element inside trap (by default Tab key navigates to next/prev element outside trap in roving mode)",
48 | "doc-url": "https://pdanpdan.github.io/vue-keyboard-trap/"
49 | },
50 | {
51 | "name": "escrefocus",
52 | "description": "Refocus element that was in focus before activating the trap on Esc",
53 | "doc-url": "https://pdanpdan.github.io/vue-keyboard-trap/"
54 | },
55 | {
56 | "name": "escexits",
57 | "description": "Refocus a parent trap on Esc (has priority over .escrefocus)",
58 | "doc-url": "https://pdanpdan.github.io/vue-keyboard-trap/"
59 | },
60 | {
61 | "name": "indexorder",
62 | "description": "When used without .grid modifier and on elements without [role=\"grid\"]: force usage of order in tabindex (tabindex in ascending order and then DOM order)",
63 | "doc-url": "https://pdanpdan.github.io/vue-keyboard-trap/"
64 | }
65 | ],
66 | "value": {
67 | "kind": "expression",
68 | "type": "boolean",
69 | "description": "Disable directive if false"
70 | }
71 | }
72 | ]
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/vite.dev.config.js:
--------------------------------------------------------------------------------
1 | import { NodePackageImporter } from 'sass-embedded';
2 | import { defineConfig } from 'vite';
3 | import vue from '@vitejs/plugin-vue';
4 |
5 | export default defineConfig({
6 | plugins: [vue()],
7 |
8 | css: {
9 | preprocessorOptions: {
10 | sass: {
11 | api: 'modern',
12 | importers: [new NodePackageImporter()],
13 | },
14 | },
15 | },
16 | });
17 |
--------------------------------------------------------------------------------
/vite.src.config.js:
--------------------------------------------------------------------------------
1 | import { NodePackageImporter } from 'sass-embedded';
2 | import { defineConfig } from 'vite';
3 | import vue from '@vitejs/plugin-vue';
4 |
5 | export default defineConfig({
6 | plugins: [vue()],
7 |
8 | publicDir: './src/public/',
9 |
10 | css: {
11 | preprocessorOptions: {
12 | sass: {
13 | api: 'modern',
14 | importers: [new NodePackageImporter()],
15 | },
16 | },
17 | },
18 |
19 | build: {
20 | target: 'esnext',
21 | sourcemap: true,
22 | emptyOutDir: true,
23 | outDir: './dist',
24 | lib: {
25 | entry: './src/exports.js',
26 | name: 'VueKeyboardTrap',
27 | fileName: (format) => `index.${ format }.js`,
28 | },
29 | optimizeDeps: {
30 | exclude: ['vue-demi'],
31 | },
32 | rollupOptions: {
33 | external: [
34 | '@vue/composition-api',
35 | 'vue',
36 | 'vue-demi',
37 | ],
38 | output: {
39 | globals: {
40 | vue: 'Vue',
41 | 'vue-demi': 'VueDemi',
42 | },
43 | exports: 'named',
44 | sourcemapExcludeSources: true,
45 | },
46 | },
47 | },
48 | });
49 |
--------------------------------------------------------------------------------