├── tsconfig.eslint.json ├── .gitignore ├── .prettierignore ├── .eslintignore ├── .prettierrc.json ├── .eslintrc.json ├── LICENSE ├── package.json ├── README.md ├── tsconfig.json ├── src └── webSrAnnouncer.ts └── index.html /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["./src/**/*", "./example/**/*"], 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .rts2_cache_* 4 | dist 5 | .cache 6 | example-dist 7 | coverage 8 | .nyc_output 9 | .parcel-cache 10 | *.code-workspace 11 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | .github 4 | .rts2_cache_* 5 | dist 6 | example-dist 7 | LICENSE 8 | package-lock.json 9 | tsconfig.json 10 | tsconfig.*.json 11 | .*ignore 12 | *.code-workspace 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | .github 4 | .rts2_cache_* 5 | dist 6 | example-dist 7 | LICENSE 8 | package-lock.json 9 | tsconfig.json 10 | tsconfig.*.json 11 | .*ignore 12 | *.md 13 | *.json 14 | *.html 15 | *.code-workspace -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "semi": false, 4 | "trailingComma": "all", 5 | "proseWrap": "always", 6 | "arrowParens": "avoid", 7 | "overrides": [ 8 | { 9 | "files": ["README.md", "package.json"], 10 | "options": { 11 | "useTabs": false 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "project": "./tsconfig.eslint.json" 5 | }, 6 | "plugins": ["@typescript-eslint"], 7 | "extends": [ 8 | "plugin:@typescript-eslint/eslint-recommended", 9 | "plugin:@typescript-eslint/recommended" 10 | ], 11 | "rules": { 12 | "prefer-const": "off", 13 | "@typescript-eslint/explicit-function-return-type": "off", 14 | "@typescript-eslint/member-delimiter-style": "off" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 David CM 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web_sr_announcer", 3 | "version": "1.1.2", 4 | "description": "A small utility to send messages to screen readers using aria-live", 5 | "author": "David CM ", 6 | "license": "MIT", 7 | "repository": "davidacm/web_sr_announcer", 8 | "source": "src/webSrAnnouncer.ts", 9 | "main": "dist/webSrAnnouncer.js", 10 | "module": "dist/webSrAnnouncer.module.js", 11 | "unpkg": "dist/webSrAnnouncer.umd.js", 12 | "types": "dist/webSrAnnouncer.d.ts", 13 | "files": [ 14 | "dist" 15 | ], 16 | "exports": { 17 | ".": { 18 | "import": "./dist/webSrAnnouncer.module.js", 19 | "require": "./dist/webSrAnnouncer.js" 20 | } 21 | }, 22 | "keywords": [ 23 | "a11y", 24 | "accessibility", 25 | "react", 26 | "vue", 27 | "angular", 28 | "html", 29 | "screen reader", 30 | "aria", 31 | "live" 32 | ], 33 | "scripts": { 34 | "check": "npm run -s typecheck && npm run -s lint && npm run -s check:format", 35 | "typecheck": "tsc --noEmit", 36 | "lint": "eslint \"**\"", 37 | "format": "prettier --write \"**\"", 38 | "check:format": "prettier --check \"**\"", 39 | "build": "microbundle --inline none", 40 | "test": "echo \"Error: no test specified\" && exit 1" 41 | }, 42 | "author": "David CM", 43 | "license": "MIT", 44 | "devDependencies": { 45 | "@typescript-eslint/eslint-plugin": "^6.4.0", 46 | "microbundle": "^0.15.1", 47 | "prettier": "^3.0.1", 48 | "typescript": "^5.1.6" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web Sr Announcer 2 | 3 | A small utility to send messages to screen readers using aria-live 4 | 5 | > [See Demo](https://davidacm.github.io/web_sr_announcer/) 6 | 7 | ## Install 8 | 9 | ```sh 10 | npm install --save web_sr_announcer 11 | ``` 12 | 13 | Or for a CDN version, you can use it on 14 | [unpkg.com](https://unpkg.com/web_sr_announcer) 15 | 16 | ```html 17 | 18 | 21 | ``` 22 | 23 | ## Usage 24 | 25 | ```js 26 | import webSrAnnouncer from "web_sr_announcer" // or use the script tag using cdn. 27 | webSrAnnouncer.initialize() 28 | 29 | // just the message is needed. 30 | webSrAnnouncer.announce("this is a test with default params") 31 | 32 | // using all params. 33 | webSrAnnouncer.announce("this is a test", "polite", 3000) 34 | ``` 35 | 36 | ## functions. 37 | 38 | ### initialize. 39 | 40 | this function creates or initializes the containers for the aria-live regions. 41 | call this before use announce function, otherwise the first message can be lost. 42 | 43 | * nodeId: string, an optional param if you want to use your own html container for the aria live regions. 44 | 45 | ### announce. 46 | 47 | update the aria-live region with the specified message. If a screen reader is 48 | active, should speak the message. 49 | 50 | this function has 3 params: 51 | 52 | * text: string, the text to be announced. 53 | * politeness: 'assertive' or 'polite', 'assertive' by default. 54 | * timeout: number, the time the message is available for screen readers. Default is 1000 ms. 55 | 56 | ### terminate. 57 | 58 | removes the containers used for live regions. you usually don't need to use 59 | this. 60 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src/**/*"], 3 | "compilerOptions": { 4 | /* Visit https://aka.ms/tsconfig to read more about this file */ 5 | "target": "ES6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 6 | "module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 7 | "lib": ["DOM", "ES6"], /* Specify library files to be included in the compilation. */ 8 | "strict": true, /* Enable all strict type-checking options. */ 9 | "noUnusedLocals": true, /* Report errors on unused locals. */ 10 | "noUnusedParameters": true, /* Report errors on unused parameters. */ 11 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 12 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 13 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 14 | "types": [], /* Type declaration files to be included in compilation. */ 15 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 16 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 17 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/webSrAnnouncer.ts: -------------------------------------------------------------------------------- 1 | type POLITENESS_SETTING = "assertive" | "polite" 2 | const DELAY_CLEAR = 1000 3 | 4 | // div container for aria-live elements. 5 | let container: HTMLElement | null = null 6 | 7 | // containers for assertive and polite. 8 | let liveRegions: { [key: string]: null | HTMLDivElement } = { 9 | assertive: null, 10 | polite: null, 11 | } 12 | // timer to clear the regions: 13 | const timers: { [key: string]: null | number } = { 14 | assertive: null, 15 | polite: null, 16 | } 17 | 18 | function createAriaLive(type: POLITENESS_SETTING) { 19 | let node = document.createElement("div") 20 | node.setAttribute("aria-live", type) 21 | node.setAttribute("aria-atomic", "true") 22 | node.setAttribute("aria-relevant", "all") 23 | node.setAttribute("role", "status") 24 | return node 25 | } 26 | 27 | function setContent(node: HTMLDivElement, text: string) { 28 | if (container) { 29 | const oldContent = node.textContent 30 | if (text === oldContent) { 31 | // add a space character to the end to make the new text different. 32 | text += " " 33 | } 34 | node.innerHTML = text 35 | } 36 | } 37 | 38 | /** 39 | * this function creates or initializes the containers for the aria-live regions. 40 | * call this before use announce function, otherwise the first message can be lost. 41 | * @param nodeId - optional, if you want to use your own html container for the aria live regions. 42 | */ 43 | function initialize(nodeId?: string) { 44 | if (container) { 45 | return 46 | } 47 | if (nodeId) { 48 | container = document.getElementById(nodeId) 49 | if (!container) { 50 | console.error("unable to get the specified id element", nodeId) 51 | return 52 | } 53 | } else { 54 | container = document.createElement("div") 55 | document.body.prepend(container) 56 | } 57 | 58 | // hide the main container element from the screen. 59 | Object.assign(container.style, { 60 | border: 0, 61 | clip: "rect(0 0 0 0)", 62 | clipPath: "inset(50%)", 63 | height: "1px", 64 | margin: "-1px", 65 | overflow: "hidden", 66 | padding: 0, 67 | position: "absolute", 68 | width: "1px", 69 | whiteSpace: "nowrap", 70 | }) 71 | 72 | liveRegions.assertive = createAriaLive("assertive") 73 | liveRegions.polite = createAriaLive("polite") 74 | container.appendChild(liveRegions.assertive) 75 | container.appendChild(liveRegions.polite) 76 | } 77 | 78 | /** 79 | * removes the containers used for live regions. 80 | * you usually don't need to use this 81 | */ 82 | function terminate() { 83 | if (container) { 84 | document.body.removeChild(container) 85 | container = null 86 | liveRegions = {} 87 | } 88 | } 89 | 90 | /** 91 | * update the aria-live region with the specified message. If a screen reader is active, should speak the message. 92 | * @param text - the text to be announced by an active screen reader. 93 | * @param politeness - assertive or polite,. Use the mdn documentation for more information. 94 | * @param timeout - optional. The delay time to clear the live region, so the message can't be seen using navigation functions. 95 | */ 96 | function announce( 97 | text: string, 98 | politeness: POLITENESS_SETTING = "assertive", 99 | timeout = DELAY_CLEAR, 100 | ) { 101 | if (!container) { 102 | initialize() 103 | } 104 | const option = politeness.toLowerCase() 105 | const node = liveRegions[option] 106 | if (node) { 107 | setContent(node, text) 108 | if (timers[option]) { 109 | clearTimeout(timers[option] as number) 110 | } 111 | timers[option] = setTimeout(() => { 112 | if (liveRegions[option]) { 113 | ;(liveRegions[option] as HTMLDivElement).innerHTML = "" 114 | } 115 | timers[option] = null 116 | }, timeout) 117 | } 118 | } 119 | 120 | const webSrAnnouncer = { 121 | initialize, 122 | terminate, 123 | announce, 124 | } 125 | export default webSrAnnouncer 126 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | test - web sr announcer 7 | 108 | 109 | 110 | 111 |

web sr announcer

112 |

113 | A small library for deal with hidden aria-lives to announce messages to 114 | users that are using screen readers. 115 |

116 | 117 | 122 | 135 | 136 | 137 |
npm install web_sr_announcer
138 | 139 |

Code Example

140 | 141 |
142 | 			import webSrAnnouncer from "web_sr_announcer"
143 | webSrAnnouncer.initialize()
144 | // just the message is needed.
145 | webSrAnnouncer.announce('this is a test with default params')
146 | 
147 | // using all params.
148 | webSrAnnouncer.announce('this is a test', 'polite', 3000)
149 | 
151 | 152 |

References

153 |

154 | Aria live documentation 158 |

159 | 160 |

demo

161 | 162 | 163 | 164 | 165 | 170 | 171 | 172 | 175 | 176 | 177 | --------------------------------------------------------------------------------