├── LICENSE ├── README.md ├── index.d.ts ├── index.js └── package.json /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Mayank 2 | 3 | The following license is modified from the MIT license. 4 | 5 | ANTI-FASCIST LICENSE: 6 | 7 | The following conditions must be met by any person obtaining a copy of this 8 | software: 9 | 10 | - You MAY NOT be a fascist. 11 | - You MUST not financially support fascists. 12 | - You MUST not publicly voice support for fascists. 13 | 14 | "Fascist" can be understood as any entity which supports radical authoritarian 15 | nationalism. For example: Donald Trump is a fascist; if you donated to his 16 | campaign then all rights provided by this license are not granted to you. 17 | 18 | MIT LICENSE: 19 | 20 | Permission is hereby granted, free of charge, to any person obtaining a copy 21 | of this software and associated documentation files (the "Software"), to deal 22 | in the Software without restriction, including without limitation the rights 23 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 24 | copies of the Software, and to permit persons to whom the Software is 25 | furnished to do so, subject to the following conditions: 26 | 27 | The above copyright notice and this permission notice shall be included in all 28 | copies or substantial portions of the Software. 29 | 30 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 31 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 32 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 33 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 34 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 35 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 36 | SOFTWARE. 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # live-announcer 2 | 3 | A web component that makes it easier to work with [live regions](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions). 4 | 5 | ``` 6 | npm add @acab/live-announcer 7 | ``` 8 | 9 | ## Usage 10 | 11 | 1. Set it up on page load. This should be done as early as possible. 12 | 13 | ```js 14 | import * as announcer from '@acab/live-announcer'; 15 | announcer.setup(); 16 | ``` 17 | 18 | 2. Announce notifications from anywhere on the page, in an [assertive](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-live#assertive) or [polite](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-live#polite) way. 19 | 20 | ```js 21 | announcer.notify('Something happened!', { priority: 'important' }); 22 | ``` 23 | 24 | ```js 25 | announcer.notify('Something happened, but it can wait.'); 26 | ``` 27 | 28 | ### Best practices 29 | 30 | 1. Keep the notification text short and concise. Don't use special characters or non-text content. 31 | 2. Set up the announcer as early as possible, before even sending any notifications. 32 | 3. Don't send too many notifications at the same time. Prefer static text. 33 | 34 | ### Dialogs and popout windows 35 | 36 | Calling `setup()` will automatically create an instance of the announcer and inject it into the page. Specifically, it will be appended into the `` element's shadow tree. 37 | 38 | In some cases, you might want to inject it somewhere else, for example, into a modal `` or a popout window. For such scenarios, use the constructor to create a separate instance and inject it wherever you'd like: 39 | 40 | ```js 41 | import { LiveAnnouncer } from '@acab/live-announcer'; 42 | 43 | const announcer = new LiveAnnouncer(); 44 | announcer.setup({ target: document.querySelector('dialog') }); 45 | 46 | announcer.notify('Something happened inside the dialog!'); 47 | ``` 48 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export class LiveAnnouncer extends HTMLElement { 2 | setup(options: { target: HTMLElement | ShadowRoot } = {}); 3 | notify(text: string, options: { priority: 'none' | 'important' } = {}) 4 | } 5 | 6 | export const setup: () => void; 7 | 8 | export const notify: (text: string, options: { priority: 'none' | 'important' } = {}) => void; 9 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const visuallyHiddenCss = `\ 2 | :host { 3 | clip-path: inset(50%) !important; 4 | height: 1px !important; 5 | overflow: hidden !important; 6 | position: absolute !important; 7 | white-space: nowrap !important; 8 | width: 1px !important; 9 | user-select: none !important; 10 | } 11 | `; 12 | 13 | class LiveAnnouncer extends HTMLElement { 14 | #assertiveRegion; 15 | #politeRegion; 16 | 17 | static register(tagName = 'live-announcer', registry) { 18 | if (typeof window !== 'undefined') { 19 | registry ||= customElements; 20 | if (registry.get(tagName) == undefined) { 21 | registry.define(tagName, LiveAnnouncer); 22 | } 23 | } 24 | } 25 | 26 | constructor() { 27 | super(); 28 | this.#assertiveRegion = this.ownerDocument.createElement('div'); 29 | this.#assertiveRegion.setAttribute('aria-live', 'assertive'); 30 | this.#politeRegion = this.ownerDocument.createElement('div'); 31 | this.#politeRegion.setAttribute('aria-live', 'polite'); 32 | 33 | this.attachShadow({ mode: 'open' }); 34 | this.shadowRoot?.replaceChildren(this.#assertiveRegion, this.#politeRegion); 35 | 36 | const stylesheet = new CSSStyleSheet(); 37 | stylesheet.replaceSync(visuallyHiddenCss); 38 | this.shadowRoot?.adoptedStyleSheets.push(stylesheet); 39 | } 40 | 41 | setup({ target = document.body } = {}) { 42 | target?.appendChild(this); 43 | } 44 | 45 | notify(text, { priority = 'none' } = {}) { 46 | const region = priority === 'important' ? this.#assertiveRegion : this.#politeRegion; 47 | region.textContent = text; 48 | setTimeout(() => { 49 | region.textContent = ''; 50 | }, 3_000); 51 | } 52 | } 53 | 54 | let _announcer; 55 | 56 | function setup() { 57 | LiveAnnouncer.register(); 58 | if (!document.body.shadowRoot) { 59 | const shadow = document.body.attachShadow({ mode: 'open' }); 60 | shadow.appendChild(document.createElement('slot')); 61 | } 62 | _announcer = new LiveAnnouncer(); 63 | _announcer.setup({ target: document.body.shadowRoot }); 64 | } 65 | 66 | function notify(...args) { 67 | _announcer.notify(...args); 68 | } 69 | 70 | export { setup, notify, LiveAnnouncer }; 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acab/live-announcer", 3 | "type": "module", 4 | "version": "0.2.2", 5 | "exports": { 6 | ".": "./index.js" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/mayank99/live-announcer.git" 11 | }, 12 | "author": "Mayank", 13 | "files": [ 14 | "index.js", 15 | "index.d.ts" 16 | ], 17 | "main": "index.js", 18 | "types": "index.d.ts" 19 | } 20 | --------------------------------------------------------------------------------