39 |
40 |
41 |
58 |
59 |
85 |
--------------------------------------------------------------------------------
/docs/guide/accessibility.md:
--------------------------------------------------------------------------------
1 | # ARIA live regions
2 |
3 | > Using JavaScript, it is possible to dynamically change parts of a page without requiring the entire page to reload — for instance, to update a list of search results on the fly, or to display a discreet alert or notification which does not require user interaction. While these changes are usually visually apparent to users who can see the page, they may not be obvious to users of assistive technologies. ARIA live regions fill this gap and provide a way to programmatically expose dynamic content changes in a way that can be announced by assistive technologies.
4 | --- [ARIA live regions - Accessibility | MDN](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions)
5 |
6 | ## Politeness settings
7 |
8 | You can use the options `polite`, `assertive` and `off`, if no configuration is defined, the default is `off`.
9 |
10 | ### polite
11 | It is used in most situations that present new information to users.
12 | The notification will take place at the next available point, without interruptions.
13 |
14 | ::: tip Note
15 | In the [@vue-a11y/announcer plugin](https://github.com/vue-a11y/vue-announcer/blob/master/src/index.js#L8) the default is `polite`
16 | :::
17 |
18 | ### assertive
19 | It is used in situations where the notification is important enough to communicate it immediately, for example, error messages or alerts.
20 |
21 |
22 |
23 | ```javascript
24 | this.$announcer.set('My notification error', 'assertive')
25 | ```
26 |
27 | ### off
28 | Is the default and prevent assistive technology from keeping up with changes.
29 |
30 |
31 | ## Referencies
32 |
33 | - [ARIA live regions - Accessibility | MDN](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions)
34 | - [Using aria-live - Bitsofcode](https://bitsofco.de/using-aria-live/)
--------------------------------------------------------------------------------
/demo/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 |
4 | module.exports = {
5 | entry: './src/main.js',
6 | output: {
7 | path: path.resolve(__dirname, './dist'),
8 | publicPath: '/dist/',
9 | filename: 'build.js'
10 | },
11 | module: {
12 | rules: [
13 | {
14 | test: /\.css$/,
15 | use: [
16 | 'vue-style-loader',
17 | 'css-loader'
18 | ]
19 | }, {
20 | test: /\.vue$/,
21 | loader: 'vue-loader',
22 | options: {
23 | loaders: {
24 | }
25 | // other vue-loader options go here
26 | }
27 | },
28 | {
29 | test: /\.js$/,
30 | loader: 'babel-loader',
31 | exclude: /node_modules/
32 | },
33 | {
34 | test: /\.(png|jpg|gif|svg)$/,
35 | loader: 'file-loader',
36 | options: {
37 | name: '[name].[ext]?[hash]'
38 | }
39 | }
40 | ]
41 | },
42 | resolve: {
43 | alias: {
44 | vue$: 'vue/dist/vue.esm.js',
45 | '@': path.resolve(__dirname, 'src/')
46 | },
47 | extensions: ['*', '.js', '.vue', '.json']
48 | },
49 | devServer: {
50 | historyApiFallback: true,
51 | noInfo: true,
52 | overlay: true
53 | },
54 | performance: {
55 | hints: false
56 | },
57 | devtool: '#eval-source-map'
58 | }
59 |
60 | if (process.env.NODE_ENV === 'production') {
61 | module.exports.devtool = '#source-map'
62 | // http://vue-loader.vuejs.org/en/workflow/production.html
63 | module.exports.plugins = (module.exports.plugins || []).concat([
64 | new webpack.DefinePlugin({
65 | 'process.env': {
66 | NODE_ENV: '"production"'
67 | }
68 | }),
69 | new webpack.optimize.UglifyJsPlugin({
70 | sourceMap: true,
71 | compress: {
72 | warnings: false
73 | }
74 | }),
75 | new webpack.LoaderOptionsPlugin({
76 | minimize: true
77 | })
78 | ])
79 | }
80 |
--------------------------------------------------------------------------------
/docs/guide/announcer.md:
--------------------------------------------------------------------------------
1 | # Announce
2 |
3 | The `$announcer` is available on the property injected into the Vue instance, so it is available everywhere in your application. With it it is possible to announce any necessary information and in real time to a person with a screen reader.
4 |
5 | ## Methods
6 |
7 | ### `$announcer.set(message, politiness)`
8 |
9 | The `$announcer.set` method is directly responsible for making changes to the announcer.
10 |
11 | | Argument | Type | Description
12 | | ----------------- | --------- | ----------------------------------------------------
13 | | `message` | String | Text to be announced.
14 | | `politiness` | String | Defines the priority of how updates will be handled. ([read more about live regions](/guide/accessibility.html))
15 |
16 | ```vue
17 |
18 |
19 |
25 |
26 | {{ errorMessage }}
27 |
28 |
29 |
30 |
31 |
47 | ```
48 |
49 | ### `$announcer.polite(message)`
50 |
51 | The `$announcer.polite` is a wrapper of the "set" method that defines the politeness setting as `polite`
52 |
53 | ```javascript
54 | // e.g.
55 | this.$announcer.polite(this.message)
56 | ```
57 |
58 | ### `$announcer.assertive(message)`
59 |
60 | The `$announcer.assertive` is a wrapper of the "set" method that defines the politeness setting as `assertive`
61 |
62 | ```javascript
63 | // e.g.
64 | this.$announcer.assertive(this.errorMessage)
65 | ```
66 |
67 | [Learn more about the politeness settings and when to use.](/guide/accessibility.md)
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [@vue-a11y/announcer](https://github.com/vue-a11y/vue-announcer/)
2 |
3 | ---
4 | 🔥 HEADS UP! You are in the Vue 2 compatible branch, [check the "next" branch for Vue 3 support](https://github.com/vue-a11y/vue-announcer/tree/next).
5 |
6 | ---
7 | ## Introduction
8 |
9 | Imagine browsing pages (routes), receiving alerts and notifications, having a countdown timer on the page, a progress bar, a loading or a change of route in a SPA. Now imagine all this happening to people who have visual disabilities and who use screen readers.
10 |
11 | The [@vue-a11y/announcer](https://github.com/vue-a11y/vue-announcer) (v2) provides an easy way to really tell what’s going on in your application to people using screen readers.
12 |
13 | > For vue-announcer version 1.* you can access [this link](https://github.com/vue-a11y/vue-announcer/tree/v1.0.6)
14 |
15 | Inspired by others in the community like:
16 | - [Example of how creating an accessible single-page application](https://haltersweb.github.io/Accessibility/spa.html)
17 | - [Ember A11y community](https://github.com/ember-a11y/a11y-announcer)
18 |
19 | ## Links
20 |
21 | - [Documentation](https://announcer.vue-a11y.com/)
22 | - [Demos](https://vue-announcer-v2.surge.sh/demos/)
23 |
24 | ## Run the tests
25 | ```shell
26 | git clone https://github.com/vue-a11y/vue-announcer.git vue-announcer
27 |
28 | # Run plugin
29 | cd vue-announcer
30 | npm install
31 | npm run dev
32 |
33 | # Run example
34 | cd examples
35 | npm install
36 | npm run dev
37 | cd ..
38 |
39 | # Run Cypress testing
40 | npm run test
41 | ```
42 |
43 | Or run Cypress on interactive mode
44 | ```shell
45 | npm run test:open
46 | ```
47 |
48 | It is a simple webpack template already installed and configured.
49 | After the commands just access the http://localhost:8080/
50 |
51 |
52 | ## Contributing
53 | - From typos in documentation to coding new features;
54 | - Check the open issues or open a new issue to start a discussion around your feature idea or the bug you found;
55 | - Fork repository, make changes and send a pull request;
56 |
57 | Follow us on Twitter [@vue_a11y](https://twitter.com/vue_a11y)
58 |
59 | **Thank you**
60 |
--------------------------------------------------------------------------------
/demo/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
15 |
16 |
{{ msg }}
17 |
Essential Links
18 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
77 |
78 |
111 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import VueAnnouncer from './vue-announcer.vue'
2 | import { draf, defaultOptions } from './utils'
3 |
4 | const announcerPlugins = {}
5 |
6 | export default function install (Vue, options = {}, router = null) {
7 | if (install.installed) return
8 | install.installed = true
9 |
10 | // merge options
11 | options = {
12 | ...defaultOptions,
13 | ...options
14 | }
15 |
16 | // Register vue-announcer component
17 | Vue.component('VueAnnouncer', VueAnnouncer)
18 |
19 | Vue.prototype.$announcer = {
20 | data: null,
21 | options,
22 |
23 | set (message, politeness) {
24 | if (!this.data) return
25 | this.reset()
26 | draf(() => {
27 | this.data.politeness = politeness || this.data.politeness
28 | this.data.content = message
29 | })
30 | },
31 |
32 | polite (message) {
33 | return this.set(message, 'polite')
34 | },
35 |
36 | assertive (message) {
37 | return this.set(message, 'assertive')
38 | },
39 |
40 | reset () {
41 | this.data.content = ''
42 | this.data.politeness = this.options.politeness
43 | },
44 |
45 | plugins: announcerPlugins,
46 |
47 | setComplementRoute (complementRoute) {
48 | if (typeof complementRoute !== 'string') return
49 | options.complementRoute = complementRoute
50 | }
51 | }
52 |
53 | // Register plugins
54 | if (options.plugins.length) {
55 | options.plugins.forEach(({ name, handler }) => {
56 | announcerPlugins[name] = handler.bind(Vue.prototype.$announcer)
57 | })
58 | }
59 |
60 | // If set the router, will be announced the change of route
61 | if (router) {
62 | router.afterEach(to => {
63 | const announcer = to.meta.announcer || {}
64 |
65 | // Skip: Used, for example, when an async title exists, in which case the announcement is made manually by the set method.
66 | // It is also possible to achieve the same result, using politeness: 'off', but it will be necessary
67 | // to set the "assertive" or "polite" when using the set method.
68 | // for example: this.$announcer.set('my async title', 'polite')
69 | if (announcer.skip) return
70 |
71 | // draf: Resolves the problem of getting the correct document.title when the meta announcer is not passed
72 | // Tested on Vuepress and Nuxt
73 | if (Vue.prototype.$isServer) return
74 | setTimeout(() => {
75 | draf(() => {
76 | const msg = announcer.message || document.title.trim()
77 | const complement = announcer.complementRoute || options.complementRoute
78 | const politeness = announcer.politeness || null
79 | Vue.prototype.$announcer.set(`${msg} ${complement}`, politeness)
80 | })
81 | }, 500)
82 | })
83 | }
84 | }
85 |
86 | // Auto install
87 | if (typeof window !== 'undefined' && typeof window.Vue !== 'undefined') {
88 | window.Vue.use(install)
89 | }
90 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ## [2.1.0](https://github.com/vue-a11y/vue-announcer/compare/v2.0.2...v2.1.0) (2020-09-01)
6 |
7 |
8 | ### Features
9 |
10 | * Plugins ([3784fb9](https://github.com/vue-a11y/vue-announcer/commit/3784fb96332d8781a78df8fb2f2e7b6601e187ae))
11 |
12 |
13 | ### Bug Fixes
14 |
15 | * Add types in package.json ([c92bd47](https://github.com/vue-a11y/vue-announcer/commit/c92bd47a545e6a8601532b8a245dc7b8b4bd1872))
16 | * Run router.afterEach only client ([d0238b2](https://github.com/vue-a11y/vue-announcer/commit/d0238b2b3d2f4d4a7704747c0e3b3f0762699b84))
17 | * Wait for the message to reset before setting the new value ([9062bd3](https://github.com/vue-a11y/vue-announcer/commit/9062bd3c19537a45910a8dc3c3904e2cb7ab0f03))
18 |
19 | ### [2.0.2](https://github.com/vue-a11y/vue-announcer/compare/v2.0.1...v2.0.2) (2020-05-08)
20 |
21 |
22 | ### Bug Fixes
23 |
24 | * Adding templete option in plug-in vue rollup to build as production ([5c71b23](https://github.com/vue-a11y/vue-announcer/commit/5c71b2317a4bfd3503513fad43f2f975a2530365))
25 |
26 | ### [2.0.1](https://github.com/vue-a11y/vue-announcer/compare/v2.0.0...v2.0.1) (2020-05-01)
27 |
28 |
29 | ### Bug Fixes
30 |
31 | * Add setTimeout in afterEach to ensure the correct document.title ([50c3cae](https://github.com/vue-a11y/vue-announcer/commit/50c3cae08cab5616b8a77a07620c0c5b2989e5db))
32 |
33 | ## [2.0.0](https://github.com/vue-a11y/vue-announcer/compare/v1.0.6...v2.0.0) (2020-05-01)
34 |
35 |
36 | ## [1.0.6](https://github.com/vue-a11y/vue-announcer/compare/v1.0.5...v1.0.6) (2020-01-08)
37 |
38 |
39 |
40 |
41 | ## [1.0.5](https://github.com/vue-a11y/vue-announcer/compare/v1.0.4...v1.0.5) (2019-12-17)
42 |
43 |
44 |
45 |
46 | ## [1.0.4](https://github.com/vue-a11y/vue-announcer/compare/v1.0.3...v1.0.4) (2019-01-25)
47 |
48 |
49 |
50 |
51 | ## [1.0.3](https://github.com/vue-a11y/vue-announcer/compare/v1.0.2...v1.0.3) (2019-01-23)
52 |
53 |
54 |
55 |
56 | ## [1.0.2](https://github.com/vue-a11y/vue-announcer/compare/v1.0.1...v1.0.2) (2018-05-24)
57 |
58 |
59 |
60 |
61 | ## [1.0.1](https://github.com/vue-a11y/vue-announcer/compare/v1.0.0...v1.0.1) (2018-05-22)
62 |
63 |
64 |
65 |
66 | # [1.0.0](https://github.com/vue-a11y/vue-announcer/compare/v0.1.1...v1.0.0) (2018-05-22)
67 |
68 |
69 |
70 |
71 | ## [0.1.1](https://github.com/vue-a11y/vue-announcer/compare/v0.0.3...v0.1.1) (2018-05-22)
72 |
73 |
74 |
75 |
76 | # [0.1.0](https://github.com/vue-a11y/vue-announcer/compare/v0.0.3...v0.1.0) (2018-05-21)
77 |
78 |
79 |
80 |
81 | ## [0.0.3](https://github.com/vue-a11y/vue-a11y-announcer/compare/v0.0.2...v0.0.3) (2018-05-20)
82 |
83 |
84 |
85 |
86 | ## 0.0.2 (2018-05-19)
87 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@vue-a11y/announcer",
3 | "version": "2.1.0",
4 | "description": "A simple way with Vue to announce any information to the screen readers.",
5 | "main": "dist/vue-announcer.ssr.js",
6 | "module": "dist/vue-announcer.esm.js",
7 | "browser": "dist/vue-announcer.esm.js",
8 | "unpkg": "dist/vue-announcer.min.js",
9 | "types": "index.d.ts",
10 | "scripts": {
11 | "dev": "rollup --config rollup.config.dev.js --watch",
12 | "build": "npm run build:ssr & npm run build:es & npm run build:unpkg",
13 | "build:ssr": "rollup --config rollup.config.prod.js --format cjs --file dist/vue-announcer.ssr.js",
14 | "build:es": "rollup --config rollup.config.prod.js --format es --file dist/vue-announcer.esm.js",
15 | "build:unpkg": "rollup --config rollup.config.prod.js --format iife --file dist/vue-announcer.min.js",
16 | "docs:dev": "vuepress dev docs --no-cache",
17 | "docs:build": "vuepress build docs --no-cache && echo announcer.vue-a11y.com >> docs/.vuepress/dist/CNAME",
18 | "docs:publish": "gh-pages -d docs/.vuepress/dist",
19 | "demo:dev": "cd demo && npm run dev",
20 | "demo:build": "cd demo && npm run build",
21 | "demo:publish": "cp ./demo/dist/index.html ./demo/dist/200.html && surge ./demo/dist https://vue-announcer.surge.sh/",
22 | "release": "standard-version",
23 | "test": "cypress run",
24 | "test:open": "cypress open",
25 | "projectPublish": "git push --follow-tags origin master && npm run build && npm publish --access public"
26 | },
27 | "repository": {
28 | "type": "git",
29 | "url": "git+https://github.com/vue-a11y/vue-announcer.git"
30 | },
31 | "keywords": [
32 | "announcer",
33 | "Vue.js",
34 | "Vue",
35 | "accessibility",
36 | "a11y",
37 | "screen",
38 | "readers",
39 | "Vision",
40 | "Disability",
41 | "JAWS",
42 | "ChromeVox",
43 | "NVDA"
44 | ],
45 | "author": "Alan Ktquez (https://medium.com/@ktquez)",
46 | "license": "MIT",
47 | "bugs": {
48 | "url": "https://github.com/vue-a11y/vue-announcer/issues"
49 | },
50 | "homepage": "https://vue-a11y.github.io/vue-announcer/",
51 | "devDependencies": {
52 | "@rollup/plugin-buble": "^0.21.3",
53 | "@rollup/plugin-commonjs": "^13.0.2",
54 | "@rollup/plugin-node-resolve": "^7.1.3",
55 | "@rollup/plugin-replace": "^2.4.2",
56 | "@vue/eslint-config-standard": "^5.1.2",
57 | "@vue/test-utils": "^1.3.6",
58 | "@vuepress/theme-default": "^1.9.10",
59 | "babel-eslint": "^10.1.0",
60 | "babel-jest": "^25.5.1",
61 | "chokidar": "^3.6.0",
62 | "cypress": "^4.12.1",
63 | "eslint": "^6.8.0",
64 | "eslint-plugin-cypress": "^2.15.2",
65 | "eslint-plugin-import": "^2.30.0",
66 | "eslint-plugin-node": "^11.1.0",
67 | "eslint-plugin-promise": "^4.3.1",
68 | "eslint-plugin-standard": "^4.1.0",
69 | "eslint-plugin-vue": "^6.2.2",
70 | "eslint-plugin-vuejs-accessibility": "^0.1.3",
71 | "gh-pages": "^3.2.3",
72 | "jest": "^25.5.4",
73 | "jest-serializer-vue": "^2.0.2",
74 | "jest-transform-stub": "^2.0.0",
75 | "rollup": "^2.79.2",
76 | "rollup-plugin-eslint": "^7.0.0",
77 | "rollup-plugin-terser": "^6.1.0",
78 | "rollup-plugin-vue": "^5.1.9",
79 | "standard-version": "^8.0.2",
80 | "vue": "^2.7.16",
81 | "vue-template-compiler": "^2.7.16",
82 | "vuepress": "^1.9.10",
83 | "vuepress-theme-default-vue-a11y": "^0.1.15",
84 | "watchpack": "^1.7.5"
85 | },
86 | "directories": {
87 | "doc": "docs",
88 | "example": "example",
89 | "test": "tests"
90 | },
91 | "dependencies": {}
92 | }
93 |
--------------------------------------------------------------------------------
/docs/guide/announcer-router.md:
--------------------------------------------------------------------------------
1 | # Announce route changes
2 |
3 | For page changes (routes) to be announced automatically, you only need to pass the router object as a parameter at the time of installation.
4 |
5 | ## Install
6 |
7 | ```javascript
8 | import Vue from 'vue'
9 | import router from './router'
10 | import VueAnnouncer from '@vue-a11y/announcer'
11 |
12 | Vue.use(VueAnnouncer, {}, router)
13 | ```
14 |
15 | ## Options
16 | Key | Data Type | default |
17 | ------------------ | ---------- | ------------ |
18 | `politeness` | String | `polite` |
19 | `complementRoute` | String | `has loaded` |
20 |
21 |
22 | Example:
23 | ```javascript
24 | Vue.use(VueAnnouncer, {
25 | complementRoute: 'ha cargado' // e.g. in spanish
26 | }, router)
27 | ```
28 |
29 | ## Methods
30 | **Note: These methods are registered on the `$announcer` property injected into the Vue instance**
31 |
32 | #### `$announcer.setComplementRoute(complementRoute)`
33 |
34 | If you need to set the `complementRoute` option dynamically without reloading the application, for example if you're
35 | dynamically loading translations, you can use this method to update it.
36 |
37 | ```javascript
38 | export default {
39 | onTranslationsUpdated (translations) {
40 | /* 'Foi carregada' e.g. in portuguese */
41 | this.$announcer.setComplementRoute(translations.complementRouteKey)
42 | }
43 | }
44 | ```
45 |
46 | ## Custom announcer (object meta)
47 |
48 | You can customize the message by defining the announcer on the "meta" object for each specific route.
49 |
50 | ```javascript
51 | {
52 | name: 'home',
53 | path: '/',
54 | component: Home,
55 | meta: {
56 | announcer: {
57 | message: 'Página de inicio se'
58 | }
59 | }
60 | }
61 | ```
62 |
63 | When the page loads, the screen reader user will hear:
64 | ```shell
65 | Página de inicio se ha cargado
66 | ```
67 |
68 | In the "meta" object you can also modify the route complement and also the politeness settings.
69 |
70 | ### Announcer in object meta
71 |
72 | Key | Data Type | data | default |
73 | ------------------ | ---------- | ------------------------- | ----------------------------------- |
74 | `message` | String | | document.title |
75 | `politeness` | String | polite, assertive, off | polite |
76 | `skip` | Boolean | | false |
77 | `complementRoute` | String | | `has loaded` or set at installation |
78 |
79 |
80 | ::: tip Note
81 | - The plug-in checks whether the message to be announced has been defined in the meta.announcer object, otherwise the document title to be loaded will be announced.
82 | - The @vue-a11y/announcer uses the global after hooks `router.afterEach` to announce the route changes.
83 | :::
84 |
85 | ## Skip in specific route
86 |
87 | Necessary for dynamic content pages that require asynchronous data to compose the page title.
88 |
89 | The `skip` property on the `meta.announcer` object is used to skip the automatic announcement made on the `router.afterEach`, that way you can announce when the asynchronous data is available.
90 |
91 | For example:
92 |
93 | In you `routes.js`
94 | ```javascript
95 | {
96 | name: 'post',
97 | path: '/posts/:id',
98 | component: Post,
99 | meta: {
100 | announcer: {
101 | skip: true
102 | }
103 | }
104 | }
105 | ```
106 | See this example:
107 | [Example link](https://github.com/vue-a11y/vue-announcer/blob/master/example/src/router.js)
108 |
109 | ---
110 |
111 | In you `Post.vue`
112 | ```vue
113 |
114 |