├── demo ├── CNAME ├── src │ ├── assets │ │ └── logo.png │ ├── pages │ │ ├── Home.vue │ │ ├── Contact.vue │ │ ├── About.vue │ │ ├── Post.vue │ │ └── Login.vue │ ├── main.js │ ├── router.js │ └── App.vue ├── .babelrc ├── .gitignore ├── index.html ├── README.md ├── package.json └── webpack.config.js ├── docs ├── .vuepress │ ├── styles │ │ └── palette.styl │ ├── public │ │ ├── favicon-dark.svg │ │ └── favicon.svg │ └── config.js ├── guide │ ├── support.md │ ├── README.md │ ├── plugins.md │ ├── accessibility.md │ ├── announcer.md │ └── announcer-router.md ├── demos │ ├── README.md │ ├── vuepress.md │ └── nuxt.md └── README.md ├── .babelrc ├── .gitignore ├── src ├── plugins │ └── spell.js ├── utils.js ├── vue-announcer.vue └── index.js ├── cypress.json ├── .eslintrc.js ├── rollup.config.dev.js ├── index.d.ts ├── rollup.config.prod.js ├── LICENSE ├── tests └── e2e │ └── integration │ └── vue-announcer.test.js ├── README.md ├── CHANGELOG.md └── package.json /demo/CNAME: -------------------------------------------------------------------------------- 1 | vue-announcer.surge.sh -------------------------------------------------------------------------------- /docs/.vuepress/styles/palette.styl: -------------------------------------------------------------------------------- 1 | $contentMaxWidth = 960px -------------------------------------------------------------------------------- /demo/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vue-a11y/vue-announcer/HEAD/demo/src/assets/logo.png -------------------------------------------------------------------------------- /demo/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "modules": false }], 4 | "stage-3" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", {"modules": false}], 4 | "stage-2", 5 | "es2015-rollup" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .editorconfig 4 | .DS_Store 5 | demo/node_modules 6 | demo/dist 7 | demo/package-lock.json 8 | demo/vue-announcer.js 9 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | yarn-error.log 6 | 7 | # Editor directories and files 8 | .idea 9 | *.suo 10 | *.ntvs* 11 | *.njsproj 12 | *.sln 13 | -------------------------------------------------------------------------------- /src/plugins/spell.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'spell', 3 | handler (pass) { 4 | if (pass) pass.split('').forEach((char, index) => setTimeout(() => this.polite(char), index * 100)) 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export const draf = (cb) => requestAnimationFrame(() => requestAnimationFrame(cb)) 2 | 3 | export const defaultOptions = { 4 | politeness: 'polite', 5 | complementRoute: 'has loaded', 6 | plugins: [] 7 | } 8 | -------------------------------------------------------------------------------- /demo/src/pages/Home.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:8080", 3 | "fixturesFolder": "tests/e2e/fixtures", 4 | "screenshotsFolder": "tests/e2e/screenshots", 5 | "integrationFolder": "tests/e2e/integration", 6 | "fileServerFolder": "demo", 7 | "pluginsFile": false, 8 | "supportFile": false 9 | } -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vue a11y announcer example 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/guide/support.md: -------------------------------------------------------------------------------- 1 | # Browser Testing 2 | 3 | Vue Announcer was tested and works as expected in the latest versions of: 4 | 5 | - NVDA (Chrome) ✔️ 6 | - ChromeVox (Chrome extension) ✔️ 7 | 8 | ## To test 9 | 10 | - Android TalkBack 11 | - JAWS 12 | - iOS VoiceOver (Safari) 13 | 14 | -------------------------------------------------------------------------------- /demo/src/pages/Contact.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 21 | -------------------------------------------------------------------------------- /demo/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueHead from 'vue-head' 3 | import VueAnnouncer from '../vue-announcer' 4 | import App from './App.vue' 5 | import router from './router.js' 6 | 7 | Vue.use(VueHead) 8 | Vue.use(VueAnnouncer, {}, router) 9 | Vue.config.productionTip = false 10 | 11 | /* eslint-disable no-new */ 12 | new Vue({ 13 | el: '#app', 14 | router, 15 | render: h => h(App) 16 | }) 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | "cypress/globals": true, 5 | node: true, 6 | browser: true 7 | }, 8 | extends: [ 9 | 'plugin:vue/recommended', 10 | '@vue/standard', 11 | 'plugin:vuejs-accessibility/recommended' 12 | ], 13 | plugins: [ 14 | 'cypress', 15 | 'vuejs-accessibility' 16 | ], 17 | parserOptions: { 18 | parser: 'babel-eslint' 19 | }, 20 | rules: { 21 | 'no-console': 'off' 22 | } 23 | } -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # example-vue-a11y-announcer 2 | 3 | > Example of how to announce information useful for screen readers 4 | 5 | ## Build Setup 6 | 7 | ``` bash 8 | # install dependencies 9 | npm install 10 | 11 | # serve with hot reload at localhost:8080 12 | npm run dev 13 | 14 | # build for production with minification 15 | npm run build 16 | ``` 17 | 18 | For detailed explanation on how things work, consult the [docs for vue-loader](http://vuejs.github.io/vue-loader). 19 | -------------------------------------------------------------------------------- /docs/guide/README.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Package 4 | 5 | #### NPM 6 | ```shell 7 | npm install -S @vue-a11y/announcer 8 | ``` 9 | 10 | #### Yarn 11 | ```shell 12 | yarn add @vue-a11y/announcer 13 | ``` 14 | 15 | ## Basic usage 16 | 17 | ```javascript 18 | import Vue from 'vue' 19 | import VueAnnouncer from '@vue-a11y/announcer' 20 | 21 | Vue.use(VueAnnouncer) 22 | ``` 23 | 24 | In your `App.vue` 25 | ```vue 26 | 32 | ``` 33 | 34 | -------------------------------------------------------------------------------- /docs/demos/README.md: -------------------------------------------------------------------------------- 1 | # Vue app 2 | 3 | 10 | 11 | Open sandbox example -------------------------------------------------------------------------------- /docs/.vuepress/public/favicon-dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.vuepress/public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rollup.config.dev.js: -------------------------------------------------------------------------------- 1 | import buble from '@rollup/plugin-buble' 2 | import resolve from '@rollup/plugin-node-resolve' 3 | import chokidar from 'chokidar' 4 | import { eslint } from 'rollup-plugin-eslint' 5 | import vue from 'rollup-plugin-vue' 6 | 7 | export default { 8 | input: 'src/index.js', 9 | watch: { 10 | chokidar, 11 | include: ['src/**'] 12 | }, 13 | plugins: [ 14 | resolve(), 15 | eslint({ 16 | include: './src/**' 17 | }), 18 | vue({ 19 | compileTemplate: true 20 | }), 21 | buble({ 22 | objectAssign: 'Object.assign' 23 | }) 24 | ], 25 | output: [ 26 | { 27 | name: 'VueAnnouncer', 28 | file: 'demo/vue-announcer.js', 29 | format: 'umd', 30 | exports: 'named' 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { PluginFunction } from 'vue'; 2 | 3 | export interface AnnouncerPlugins { 4 | name: string, 5 | handler: any 6 | } 7 | export interface Announcer { 8 | data: Record; 9 | 10 | options: Record; 11 | 12 | plugins?: AnnouncerPlugins[]; 13 | 14 | set(message: string, politeness: string): void; 15 | 16 | polite(message: string): void; 17 | 18 | assertive(message: string): void; 19 | 20 | reset(): void; 21 | 22 | setComplementRoute(complementRoute: string): void; 23 | 24 | } 25 | 26 | declare module 'vue/types/vue' { 27 | interface Vue 28 | { 29 | $announcer: Announcer; 30 | } 31 | } 32 | 33 | declare class VueAnnouncer { 34 | static install: PluginFunction; 35 | } 36 | 37 | export default VueAnnouncer; 38 | -------------------------------------------------------------------------------- /src/vue-announcer.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 27 | 28 | 41 | -------------------------------------------------------------------------------- /demo/src/pages/About.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 44 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | 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. 4 | 5 | The [@vue-a11y/announcer](https://github.com/vue-a11y/vue-announcer) provides an easy way to really tell what’s going on in your application to people using screen readers. 6 | 7 | Inspired by others in the community like: 8 | - [Example of how creating an accessible single-page application](https://haltersweb.github.io/Accessibility/spa.html) 9 | - [Ember A11y community](https://github.com/ember-a11y/a11y-announcer) 10 | 11 | --- 12 | 13 | ## 🔥 Vue Announcer for Vue 3 14 | 15 | To use VueAnnouncer for Vue 3 you can learn more through the [README of the next branch](https://github.com/vue-a11y/vue-announcer/blob/next/README.md) -------------------------------------------------------------------------------- /rollup.config.prod.js: -------------------------------------------------------------------------------- 1 | import buble from '@rollup/plugin-buble' 2 | import commonjs from '@rollup/plugin-commonjs' 3 | import replace from '@rollup/plugin-replace' 4 | import { terser } from 'rollup-plugin-terser' 5 | import vue from 'rollup-plugin-vue' 6 | 7 | export default commandLineArgs => { 8 | return { 9 | input: 'src/index.js', 10 | plugins: [ 11 | commonjs(), 12 | replace({ 13 | 'process.env.NODE_ENV': JSON.stringify('production') 14 | }), 15 | vue({ 16 | css: true, 17 | compileTemplate: true, 18 | template: { 19 | isProduction: true, 20 | optimizeSSR: commandLineArgs.format === 'cjs' 21 | } 22 | }), 23 | buble({ 24 | objectAssign: 'Object.assign' 25 | }), 26 | commandLineArgs.format === 'iife' && terser() 27 | ], 28 | output: { 29 | name: 'VueAnnouncer', 30 | exports: 'named' 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /demo/src/pages/Post.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 41 | -------------------------------------------------------------------------------- /docs/guide/plugins.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | announcer: Announcer plugins page has loaded 4 | 5 | --- 6 | 7 | # Plugins 8 | 9 | Plugin is an interesting resource for you to create different ways to use the announcer and adapt to a specific problem in your app. 10 | 11 | 12 | ```javascript 13 | // e.g. plugins/announcer/myPlugin.js 14 | export default { 15 | name: 'myPlugin', 16 | handler () { 17 | console.log('myPlugin') 18 | } 19 | } 20 | ``` 21 | 22 | ::: warning 23 | The handler function takes `$announcer` as a context (this), so you can use `this.assertive('my text')` 24 | ::: 25 | 26 | ```javascript 27 | // src/main.js 28 | import Vue from 'vue' 29 | import VueAnnouncer from '@vue-a11y/announcer' 30 | 31 | import myPlugin from '@plugins/announcer/myPlugin' 32 | 33 | Vue.use(VueAnnouncer, { 34 | plugins: [myPlugin] 35 | }) 36 | ``` 37 | 38 | ```javascript 39 | // e.g. component.vue 40 | export default { 41 | name: 'myComponent' 42 | 43 | methods: { 44 | test () { 45 | this.$announcer.plugins.myPlugin() 46 | } 47 | } 48 | } 49 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 vue-a11y 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-vue-announcer", 3 | "description": "Example of how to announce information useful for screen readers", 4 | "version": "1.0.0", 5 | "author": "Alan Albuquerque (ktquez) ", 6 | "license": "MIT", 7 | "private": true, 8 | "scripts": { 9 | "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot", 10 | "build": "cross-env NODE_ENV=production webpack --progress --hide-modules" 11 | }, 12 | "dependencies": { 13 | "vue": "^2.5.11", 14 | "vue-head": "^2.0.12", 15 | "vue-router": "^3.0.1", 16 | "vue-toasted": "^1.1.24" 17 | }, 18 | "browserslist": [ 19 | "> 1%", 20 | "last 2 versions", 21 | "not ie <= 8" 22 | ], 23 | "devDependencies": { 24 | "babel-core": "^6.26.0", 25 | "babel-loader": "^7.1.2", 26 | "babel-preset-env": "^1.6.0", 27 | "babel-preset-stage-3": "^6.24.1", 28 | "cross-env": "^5.0.5", 29 | "css-loader": "^0.28.7", 30 | "file-loader": "^1.1.4", 31 | "vue-loader": "^13.0.5", 32 | "vue-template-compiler": "^2.4.4", 33 | "webpack": "^3.6.0", 34 | "webpack-dev-server": "^2.9.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /demo/src/router.js: -------------------------------------------------------------------------------- 1 | import About from '@/pages/About' 2 | import Contact from '@/pages/Contact' 3 | import Home from '@/pages/Home' 4 | import Post from '@/pages/Post' 5 | import Login from '@/pages/Login' 6 | import Vue from 'vue' 7 | import VueRouter from 'vue-router' 8 | 9 | Vue.use(VueRouter) 10 | 11 | const router = new VueRouter({ 12 | mode: 'history', 13 | routes: [ 14 | { 15 | name: 'home', 16 | path: '/', 17 | component: Home, 18 | meta: { 19 | announcer: { 20 | complementRoute: 'has loaded' 21 | } 22 | } 23 | }, 24 | { 25 | name: 'login', 26 | path: '/login', 27 | component: Login 28 | }, 29 | { 30 | name: 'about', 31 | path: '/about', 32 | component: About 33 | }, 34 | { 35 | name: 'post', 36 | path: '/posts/:id', 37 | component: Post, 38 | meta: { 39 | announcer: { 40 | skip: true 41 | } 42 | } 43 | }, 44 | { 45 | name: 'contact', 46 | path: '/contact', 47 | component: Contact, 48 | meta: { 49 | announcer: { 50 | message: 'contact page', 51 | politeness: 'assetive', 52 | complementRoute: 'has fully loaded' 53 | } 54 | } 55 | } 56 | ] 57 | }) 58 | 59 | export default router 60 | -------------------------------------------------------------------------------- /docs/demos/vuepress.md: -------------------------------------------------------------------------------- 1 | # Vuepress 2 | 3 | Add the vue-announcer to your `enhanceApp.js` 4 | 5 | ```javascript 6 | import VueAnnouncer from "@vue-a11y/announcer"; 7 | 8 | export default ({ Vue, router, isServer }) => { 9 | if (!isServer) { 10 | Vue.use(VueAnnouncer, {}, router); 11 | } 12 | }; 13 | ``` 14 | 15 | Ok, now just insert the component `` in your main layout. 16 | 17 | ```vue 18 | 26 | ``` 27 | 28 |
29 | 30 | --- 31 | 32 | ### Example using theme-default 33 | 34 |
35 | 36 | 43 | 44 | Open sandbox example -------------------------------------------------------------------------------- /tests/e2e/integration/vue-announcer.test.js: -------------------------------------------------------------------------------- 1 | describe('Announcer test', () => { 2 | before(() => { 3 | cy.visit('/') 4 | }) 5 | 6 | it('Element must be empty when page is loaded', () => { 7 | cy.get('[data-va="announcer"]').should('empty') 8 | }) 9 | 10 | it('Should contain the default complement when the route changes', () => { 11 | cy.get('a[href="/about"]').click() 12 | cy.get('[data-va="announcer"]').should('contain', 'has loaded') 13 | }) 14 | 15 | it('Should contain the custom complement when the route changes', () => { 16 | cy.get('a[href="/contact"]').click() 17 | cy.get('[data-va="announcer"]').should('contain', 'has fully loaded') 18 | }) 19 | 20 | it('Should handle setting the custom complement multiple times', () => { 21 | cy.get('a[href="/contact"]').click() 22 | cy.get('[data-va="announcer"]').should('contain', 'has fully loaded') 23 | 24 | cy.get('a[href="/about"]').click() 25 | cy.get('[data-va="announcer"]').should('contain', 'has loaded') 26 | }) 27 | 28 | it('Should be equal error message with the announced', () => { 29 | cy.get('a[href="/about"]').click() 30 | cy.get('[data-va="handler-error-button"]').click() 31 | cy.get('[data-va="msg-error"]').should('not.be.empty') 32 | 33 | cy.get('[data-va="msg-error"]') 34 | .then(el => { 35 | cy.get('[data-va="announcer"]').should('contain', el.text().trim()) 36 | cy.get('[data-va="announcer"]').should('have.attr', 'aria-live', 'assertive') 37 | }) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | theme: 'vuepress-theme-default-vue-a11y', 3 | title: 'Vue announcer for Vue 2', 4 | description: 'A simple way with Vue to announce any useful information for screen readers.', 5 | head: [ 6 | ['meta', { name: 'theme-color', content: '#fff' }], 7 | ['link', { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' }] 8 | ], 9 | serviceWorker: true, 10 | themeConfig: { 11 | home: false, 12 | repo: 'vue-a11y/vue-announcer', 13 | docsDir: 'docs', 14 | colorMode: { 15 | props: { 16 | modes: ['light', 'dark', 'system', 'sepia'] 17 | } 18 | }, 19 | locales: { 20 | '/': { 21 | editLinkText: 'Edit this page on GitHub', 22 | nav: [ 23 | { 24 | text: 'Guide', 25 | link: '/guide/' 26 | }, 27 | { 28 | text: 'Examples', 29 | link: '/demos/' 30 | } 31 | ], 32 | sidebar: [ 33 | '/', 34 | { 35 | title: 'Guide', 36 | collapsable: false, 37 | children: [ 38 | '/guide/', 39 | '/guide/announcer.md', 40 | '/guide/announcer-router.md', 41 | '/guide/plugins.md', 42 | '/guide/accessibility.md', 43 | '/guide/support.md', 44 | ] 45 | }, 46 | { 47 | title: 'Examples', 48 | collapsable: false, 49 | children: [ 50 | '/demos/', 51 | '/demos/nuxt.md', 52 | '/demos/vuepress.md' 53 | ] 54 | } 55 | ] 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /docs/demos/nuxt.md: -------------------------------------------------------------------------------- 1 | # Nuxt.js 2 | 3 | The first step is to create your plugin in Nuxt `plugins/vue-announcer.js` 4 | 5 | ```javascript 6 | import Vue from "vue"; 7 | import VueAnnouncer from "@vue-a11y/announcer"; 8 | 9 | export default ({ app }) => { 10 | Vue.use(VueAnnouncer, {}, app.router); 11 | }; 12 | ``` 13 | 14 | After creating the plugin you need to add it in the nuxt settings, in the `nuxt.config.js` file, you can [learn more about it here](https://nuxtjs.org/api/configuration-plugins#__layout). 15 | 16 | ```javascript 17 | export default { 18 | // ... 19 | plugins: [ 20 | { src: "~/plugins/vue-announcer.js", mode: "client" } 21 | ], 22 | } 23 | ``` 24 | 25 | Now just add `` in your default layout. 26 | 27 | In your `layouts/default.vue` 28 | ```vue 29 | 35 | ``` 36 | 37 |
38 | 39 | --- 40 | 41 | ### Demo 42 |
43 | 44 | 51 | 52 | Open sandbox example -------------------------------------------------------------------------------- /demo/src/pages/Login.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | 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 | 126 | 127 | 153 | ``` 154 | 155 | See this example: 156 | [Example link](https://github.com/vue-a11y/vue-announcer/blob/master/example/src/pages/Post.vue) --------------------------------------------------------------------------------