@a11y_craft/auto-announce 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nidhi Gajera
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# @a11y_craft/auto-announce
|
|
2
|
+
|
|
3
|
+
> Zero-config screen reader announcements. Import once — VoiceOver, NVDA, TalkBack and Narrator just work.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@a11y_craft/auto-announce)
|
|
6
|
+
[](https://bundlephobia.com/package/@a11y_craft/auto-announce)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## The Problem
|
|
12
|
+
|
|
13
|
+
Most apps have toasts, banners, and notifications that sighted users can see — but screen reader users never hear. Making them accessible requires manually wiring up `aria-live` regions, managing politeness levels, and handling timing quirks across different screen readers.
|
|
14
|
+
|
|
15
|
+
That's a lot of work. Most teams skip it.
|
|
16
|
+
|
|
17
|
+
**`@a11y_craft/auto-announce` does it for you.**
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## How It Works
|
|
22
|
+
|
|
23
|
+
The package silently watches your DOM using a `MutationObserver`. When a notification, toast, banner, or alert appears, it automatically reads it aloud to screen reader users — with no extra code required from you or your team.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install @a11y_craft/auto-announce
|
|
31
|
+
# or
|
|
32
|
+
yarn add @a11y_craft/auto-announce
|
|
33
|
+
# or
|
|
34
|
+
pnpm add @a11y_craft/auto-announce
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
Add one import to your app entry point and you're done.
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
// main.js / index.js / App.tsx — just once, anywhere at the top
|
|
45
|
+
import '@a11y_craft/auto-announce';
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
That's it. No setup. No providers. No function calls. Your entire app is now more accessible.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## What Gets Announced Automatically
|
|
53
|
+
|
|
54
|
+
The package detects notifications by their ARIA roles, class names, and data attributes:
|
|
55
|
+
|
|
56
|
+
| Element | Detected by | Politeness |
|
|
57
|
+
|---------|------------|------------|
|
|
58
|
+
| `<div role="alert">` | ARIA role | Assertive (interrupts) |
|
|
59
|
+
| `<div role="status">` | ARIA role | Polite |
|
|
60
|
+
| `<div role="log">` | ARIA role | Polite |
|
|
61
|
+
| `<div class="toast">` | Class name | Polite |
|
|
62
|
+
| `<div class="notification">` | Class name | Polite |
|
|
63
|
+
| `<div class="banner">` | Class name | Polite |
|
|
64
|
+
| `<div class="snackbar">` | Class name | Polite |
|
|
65
|
+
| `<div class="flash">` | Class name | Polite |
|
|
66
|
+
| `<div data-announce>` | Data attribute | Polite |
|
|
67
|
+
|
|
68
|
+
Elements that already have `aria-live` set are skipped — no double-announcing.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Framework Examples
|
|
73
|
+
|
|
74
|
+
### React
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
// index.tsx
|
|
78
|
+
import '@a11y_craft/auto-announce';
|
|
79
|
+
import { createRoot } from 'react-dom/client';
|
|
80
|
+
import App from './App';
|
|
81
|
+
|
|
82
|
+
createRoot(document.getElementById('root')!).render(<App />);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Now every toast, alert, or notification in your React app is automatically announced.
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
// Somewhere in your app — no extra code needed
|
|
89
|
+
function saveFile() {
|
|
90
|
+
await upload();
|
|
91
|
+
showToast('File uploaded successfully.'); // ← screen readers hear this automatically
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Vue
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
// main.js
|
|
99
|
+
import '@a11y_craft/auto-announce';
|
|
100
|
+
import { createApp } from 'vue';
|
|
101
|
+
import App from './App.vue';
|
|
102
|
+
|
|
103
|
+
createApp(App).mount('#app');
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Svelte
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
// main.js
|
|
110
|
+
import '@a11y_craft/auto-announce';
|
|
111
|
+
import App from './App.svelte';
|
|
112
|
+
|
|
113
|
+
new App({ target: document.body });
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Vanilla JS / HTML
|
|
117
|
+
|
|
118
|
+
```html
|
|
119
|
+
<script type="module">
|
|
120
|
+
import '@a11y_craft/auto-announce';
|
|
121
|
+
</script>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Custom Configuration
|
|
127
|
+
|
|
128
|
+
No configuration is needed for most apps. For advanced use cases:
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
import { autoAnnounce } from '@a11y_craft/auto-announce';
|
|
132
|
+
|
|
133
|
+
autoAnnounce({
|
|
134
|
+
// Add your own selectors on top of the defaults
|
|
135
|
+
selectors: ['.my-custom-toast', '[data-notify]'],
|
|
136
|
+
|
|
137
|
+
// Never announce these, even if they match
|
|
138
|
+
ignore: ['.silent-banner', '.marketing-popup'],
|
|
139
|
+
|
|
140
|
+
// Override politeness for all announcements
|
|
141
|
+
politeness: 'assertive',
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Stopping the Observer
|
|
148
|
+
|
|
149
|
+
Useful for cleanup in tests or single-page app teardown:
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
import { stopAutoAnnounce } from '@a11y_craft/auto-announce';
|
|
153
|
+
|
|
154
|
+
stopAutoAnnounce();
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Screen Reader Support
|
|
160
|
+
|
|
161
|
+
Tested against the most widely used screen reader and browser combinations:
|
|
162
|
+
|
|
163
|
+
| Screen Reader | Browser | Support |
|
|
164
|
+
|---|---|---|
|
|
165
|
+
| NVDA | Firefox | ✅ |
|
|
166
|
+
| JAWS | Chrome | ✅ |
|
|
167
|
+
| VoiceOver | Safari (macOS) | ✅ |
|
|
168
|
+
| VoiceOver | Safari (iOS) | ✅ |
|
|
169
|
+
| TalkBack | Chrome (Android) | ✅ |
|
|
170
|
+
| Narrator | Edge (Windows) | ✅ |
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## WCAG Compliance
|
|
175
|
+
|
|
176
|
+
Helps satisfy [WCAG 2.2 — Success Criterion 4.1.3: Status Messages](https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html), which requires that status messages be announced to screen readers without receiving focus.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Zero Dependencies
|
|
181
|
+
|
|
182
|
+
No runtime dependencies. The package is self-contained and uses only native browser APIs (`MutationObserver`, `aria-live`).
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## TypeScript
|
|
187
|
+
|
|
188
|
+
Full TypeScript support out of the box:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import { autoAnnounce } from '@a11y_craft/auto-announce';
|
|
192
|
+
import type { AutoAnnounceOptions } from '@a11y_craft/auto-announce';
|
|
193
|
+
|
|
194
|
+
const options: AutoAnnounceOptions = {
|
|
195
|
+
selectors: ['.my-toast'],
|
|
196
|
+
ignore: ['.quiet'],
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
autoAnnounce(options);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
MIT © [Nidhi Gajera](https://github.com/nidhiG2610)
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var E=Object.defineProperty;var P=Object.getOwnPropertyDescriptor;var D=Object.getOwnPropertyNames;var R=Object.prototype.hasOwnProperty;var _=(e,t)=>{for(var o in t)E(e,o,{get:t[o],enumerable:!0})},H=(e,t,o,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of D(t))!R.call(e,r)&&r!==o&&E(e,r,{get:()=>t[r],enumerable:!(n=P(t,r))||n.enumerable});return e};var N=e=>H(E({},"__esModule",{value:!0}),e);var G={};_(G,{autoAnnounce:()=>B,stopAutoAnnounce:()=>C});module.exports=N(G);var k=500,h=1e4,c=null,d=null,u=new Map;function f(e){var n;let t=document.createElement("div");return t.setAttribute("role",e==="assertive"?"alert":"status"),t.setAttribute("aria-live",e),t.setAttribute("aria-atomic","true"),t.setAttribute("data-a11y-craft","live-region"),t.style.position="absolute",t.style.width="1px",t.style.height="1px",t.style.padding="0",t.style.margin="-1px",t.style.overflow="hidden",t.style.clip="rect(0,0,0,0)",t.style.whiteSpace="nowrap",t.style.border="0",((n=document.body)!=null?n:document.documentElement).appendChild(t),t}function U(){c||(c=f("polite")),d||(d=f("assertive"))}function F(e){let t=Date.now(),o=u.get(e);if(o!==void 0&&t-o<k)return!0;if(u.size>=200){let n=u.keys().next().value;n!==void 0&&u.delete(n)}return u.set(e,t),!1}function x(e,t="polite"){if(typeof document=="undefined"||!e||typeof e!="string")return;let o=e.length>h?e.slice(0,h):e;if(F(o))return;U();let n=t==="assertive"?d:c;(!n.ownerDocument||!n.ownerDocument.documentElement.contains(n))&&(t==="assertive"?(d=f("assertive"),n=d):(c=f("polite"),n=c)),n.textContent="",setTimeout(()=>{n.textContent=o},0)}var S=['[role="alert"]','[role="status"]','[role="log"]',".toast",".toast-message",".notification",".banner",".alert",".snackbar",".flash",".flash-message","[data-announce]","[data-notification]","[data-toast]"];function M(e){return e.getAttribute("role")==="alert"?"assertive":"polite"}function T(e){if(!(e instanceof HTMLElement))return!1;let t=window.getComputedStyle(e);return t.display!=="none"&&t.visibility!=="hidden"&&t.opacity!=="0"}function L(e){if(e.hasAttribute("aria-live"))return!0;let t=e.parentElement;for(;t;){if(t.hasAttribute("aria-live"))return!0;t=t.parentElement}return!1}function w(e){var n,r,i,s;let t=e.getAttribute("aria-label");if(t)return t.trim();let o=e.getAttribute("aria-labelledby");if(o){let l=document.getElementById(o);if(l&&e.contains(l))return(r=(n=l.textContent)==null?void 0:n.trim())!=null?r:""}return(s=(i=e.textContent)==null?void 0:i.trim())!=null?s:""}function g(e,t){var n,r,i;let o=(i=(n=e.matches)==null?void 0:n.bind(e))!=null?i:(r=e.msMatchesSelector)==null?void 0:r.bind(e);return o?t.some(s=>{try{return o(s)}catch(l){return!1}}):!1}var a=null;function O(e,t,o,n){if(L(e)||o.length>0&&g(e,o)||!g(e,t)||!T(e))return;let r=w(e);if(!r)return;let i=n!=null?n:M(e);x(r,i)}function j(e,t,o,n){if(e.nodeType!==Node.ELEMENT_NODE)return;let r=e;O(r,t,o,n);let i=r.querySelectorAll(t.join(","));for(let s=0;s<i.length;s++)O(i[s],t,o,n)}function m(e={}){var s,l,y;if(typeof document=="undefined"||a)return;let t=20,o=((s=e.selectors)!=null?s:[]).slice(0,t),n=[...S,...o],r=((l=e.ignore)!=null?l:[]).slice(0,t),i=e.politeness;a=new MutationObserver(v=>{for(let p=0;p<v.length;p++){let A=v[p].addedNodes;for(let b=0;b<A.length;b++)j(A[b],n,r,i)}}),a.observe((y=document.body)!=null?y:document.documentElement,{childList:!0,subtree:!0})}function C(){a&&(a.disconnect(),a=null)}function B(e={}){m(e)}typeof document!="undefined"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>m()):m());0&&(module.exports={autoAnnounce,stopAutoAnnounce});
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/announcer.ts","../src/detector.ts","../src/observer.ts"],"sourcesContent":["import { startObserver, stopObserver } from './observer';\nimport { AutoAnnounceOptions } from './types';\n\nexport type { AutoAnnounceOptions } from './types';\n\n/**\n * Manually configure and start auto-announce with custom options.\n * Only needed if you want to override defaults.\n *\n * @example\n * import { autoAnnounce } from '@a11y_craft/auto-announce';\n * autoAnnounce({ selectors: ['.my-toast'], ignore: ['.silent'] });\n */\nexport function autoAnnounce(options: AutoAnnounceOptions = {}): void {\n startObserver(options);\n}\n\n/**\n * Stop observing the DOM. Useful for cleanup in tests or unmounting.\n */\nexport { stopObserver as stopAutoAnnounce };\n\n// ─── Auto-initialize on import ───────────────────────────────────────────────\n// This is the magic — just importing this package starts the observer.\n// Works with DOMContentLoaded so it's safe to import at the top of any file.\nif (typeof document !== 'undefined') {\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', () => startObserver());\n } else {\n startObserver();\n }\n}\n","import { Politeness } from './types';\n\nconst DEDUPE_MS = 500;\nconst MAX_MESSAGE_LENGTH = 10_000;\n\nlet politeRegion: HTMLElement | null = null;\nlet assertiveRegion: HTMLElement | null = null;\nconst recentMessages = new Map<string, number>();\n\nfunction createRegion(politeness: Politeness): HTMLElement {\n const el = document.createElement('div');\n el.setAttribute('role', politeness === 'assertive' ? 'alert' : 'status');\n el.setAttribute('aria-live', politeness);\n el.setAttribute('aria-atomic', 'true');\n el.setAttribute('data-a11y-craft', 'live-region');\n el.style.position = 'absolute';\n el.style.width = '1px';\n el.style.height = '1px';\n el.style.padding = '0';\n el.style.margin = '-1px';\n el.style.overflow = 'hidden';\n el.style.clip = 'rect(0,0,0,0)';\n el.style.whiteSpace = 'nowrap';\n el.style.border = '0';\n const target = document.body ?? document.documentElement;\n target.appendChild(el);\n return el;\n}\n\nfunction ensureRegions(): void {\n if (!politeRegion) politeRegion = createRegion('polite');\n if (!assertiveRegion) assertiveRegion = createRegion('assertive');\n}\n\nfunction isDuplicate(message: string): boolean {\n const now = Date.now();\n const last = recentMessages.get(message);\n if (last !== undefined && now - last < DEDUPE_MS) return true;\n if (recentMessages.size >= 200) {\n const oldest = recentMessages.keys().next().value;\n if (oldest !== undefined) recentMessages.delete(oldest);\n }\n recentMessages.set(message, now);\n return false;\n}\n\nexport function announce(message: string, politeness: Politeness = 'polite'): void {\n if (typeof document === 'undefined') return;\n if (!message || typeof message !== 'string') return;\n\n const safe = message.length > MAX_MESSAGE_LENGTH ? message.slice(0, MAX_MESSAGE_LENGTH) : message;\n if (isDuplicate(safe)) return;\n\n ensureRegions();\n const el = politeness === 'assertive' ? assertiveRegion! : politeRegion!;\n el.textContent = '';\n setTimeout(() => { el.textContent = safe; }, 0);\n}\n","import { Politeness } from './types';\n\nexport const DEFAULT_SELECTORS = [\n '[role=\"alert\"]',\n '[role=\"status\"]',\n '[role=\"log\"]',\n '.toast',\n '.toast-message',\n '.notification',\n '.banner',\n '.alert',\n '.snackbar',\n '.flash',\n '.flash-message',\n '[data-announce]',\n '[data-notification]',\n '[data-toast]',\n];\n\nexport function getPoliteness(el: Element): Politeness {\n const role = el.getAttribute('role');\n return role === 'alert' ? 'assertive' : 'polite';\n}\n\nexport function isVisible(el: Element): boolean {\n if (!(el instanceof HTMLElement)) return false;\n const style = window.getComputedStyle(el);\n return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';\n}\n\nexport function alreadyHasLiveRegion(el: Element): boolean {\n // Skip if the element itself or any ancestor is already an aria-live region\n return el.hasAttribute('aria-live') || !!el.closest('[aria-live]');\n}\n\nexport function extractText(el: Element): string {\n // Prefer aria-label, then aria-labelledby, then textContent\n const label = el.getAttribute('aria-label');\n if (label) return label.trim();\n\n const labelledBy = el.getAttribute('aria-labelledby');\n if (labelledBy) {\n const labelEl = document.getElementById(labelledBy);\n if (labelEl) return labelEl.textContent?.trim() ?? '';\n }\n\n return el.textContent?.trim() ?? '';\n}\n\nexport function matchesSelectors(el: Element, selectors: string[]): boolean {\n return selectors.some((sel) => {\n try {\n return el.matches(sel);\n } catch {\n return false;\n }\n });\n}\n","import { announce } from './announcer';\nimport {\n DEFAULT_SELECTORS,\n alreadyHasLiveRegion,\n extractText,\n getPoliteness,\n isVisible,\n matchesSelectors,\n} from './detector';\nimport { AutoAnnounceOptions, Politeness } from './types';\n\nlet observer: MutationObserver | null = null;\n\nfunction checkElement(\n el: Element,\n selectors: string[],\n ignoreSelectors: string[],\n politenessOverride?: Politeness,\n): void {\n // Skip if already handled by aria-live\n if (alreadyHasLiveRegion(el)) return;\n\n // Skip ignored selectors\n if (ignoreSelectors.length > 0 && matchesSelectors(el, ignoreSelectors)) return;\n\n // Must match one of our selectors\n if (!matchesSelectors(el, selectors)) return;\n\n // Must be visible\n if (!isVisible(el)) return;\n\n const text = extractText(el);\n if (!text) return;\n\n const politeness = politenessOverride ?? getPoliteness(el);\n announce(text, politeness);\n}\n\nfunction walkTree(\n node: Node,\n selectors: string[],\n ignoreSelectors: string[],\n politenessOverride?: Politeness,\n): void {\n if (node.nodeType !== Node.ELEMENT_NODE) return;\n const el = node as Element;\n\n // Check the node itself\n checkElement(el, selectors, ignoreSelectors, politenessOverride);\n\n // Check its descendants\n const descendants = el.querySelectorAll(selectors.join(','));\n descendants.forEach((child) =>\n checkElement(child, selectors, ignoreSelectors, politenessOverride),\n );\n}\n\nexport function startObserver(options: AutoAnnounceOptions = {}): void {\n if (typeof document === 'undefined') return;\n if (observer) return; // already running\n\n const selectors = [...DEFAULT_SELECTORS, ...(options.selectors ?? [])];\n const ignoreSelectors = options.ignore ?? [];\n const politenessOverride = options.politeness;\n\n observer = new MutationObserver((mutations) => {\n for (const mutation of mutations) {\n mutation.addedNodes.forEach((node) =>\n walkTree(node, selectors, ignoreSelectors, politenessOverride),\n );\n }\n });\n\n observer.observe(document.body ?? document.documentElement, {\n childList: true,\n subtree: true,\n });\n}\n\nexport function stopObserver(): void {\n if (observer) {\n observer.disconnect();\n observer = null;\n }\n}\n"],"mappings":"yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,kBAAAE,EAAA,qBAAAC,IAAA,eAAAC,EAAAJ,GCEA,IAAMK,EAAY,IACZC,EAAqB,IAEvBC,EAAmC,KACnCC,EAAsC,KACpCC,EAAiB,IAAI,IAE3B,SAASC,EAAaC,EAAqC,CAT3D,IAAAC,EAUE,IAAMC,EAAK,SAAS,cAAc,KAAK,EACvC,OAAAA,EAAG,aAAa,OAAQF,IAAe,YAAc,QAAU,QAAQ,EACvEE,EAAG,aAAa,YAAaF,CAAU,EACvCE,EAAG,aAAa,cAAe,MAAM,EACrCA,EAAG,aAAa,kBAAmB,aAAa,EAChDA,EAAG,MAAM,SAAW,WACpBA,EAAG,MAAM,MAAQ,MACjBA,EAAG,MAAM,OAAS,MAClBA,EAAG,MAAM,QAAU,IACnBA,EAAG,MAAM,OAAS,OAClBA,EAAG,MAAM,SAAW,SACpBA,EAAG,MAAM,KAAO,gBAChBA,EAAG,MAAM,WAAa,SACtBA,EAAG,MAAM,OAAS,MACHD,EAAA,SAAS,OAAT,KAAAA,EAAiB,SAAS,iBAClC,YAAYC,CAAE,EACdA,CACT,CAEA,SAASC,GAAsB,CACxBP,IAAcA,EAAeG,EAAa,QAAQ,GAClDF,IAAiBA,EAAkBE,EAAa,WAAW,EAClE,CAEA,SAASK,EAAYC,EAA0B,CAC7C,IAAMC,EAAM,KAAK,IAAI,EACfC,EAAOT,EAAe,IAAIO,CAAO,EACvC,GAAIE,IAAS,QAAaD,EAAMC,EAAOb,EAAW,MAAO,GACzD,GAAII,EAAe,MAAQ,IAAK,CAC9B,IAAMU,EAASV,EAAe,KAAK,EAAE,KAAK,EAAE,MACxCU,IAAW,QAAWV,EAAe,OAAOU,CAAM,CACxD,CACA,OAAAV,EAAe,IAAIO,EAASC,CAAG,EACxB,EACT,CAEO,SAASG,EAASJ,EAAiBL,EAAyB,SAAgB,CAEjF,GADI,OAAO,UAAa,aACpB,CAACK,GAAW,OAAOA,GAAY,SAAU,OAE7C,IAAMK,EAAOL,EAAQ,OAASV,EAAqBU,EAAQ,MAAM,EAAGV,CAAkB,EAAIU,EAC1F,GAAID,EAAYM,CAAI,EAAG,OAEvBP,EAAc,EACd,IAAMD,EAAKF,IAAe,YAAcH,EAAmBD,EAC3DM,EAAG,YAAc,GACjB,WAAW,IAAM,CAAEA,EAAG,YAAcQ,CAAM,EAAG,CAAC,CAChD,CCvDO,IAAMC,EAAoB,CAC/B,iBACA,kBACA,eACA,SACA,iBACA,gBACA,UACA,SACA,YACA,SACA,iBACA,kBACA,sBACA,cACF,EAEO,SAASC,EAAcC,EAAyB,CAErD,OADaA,EAAG,aAAa,MAAM,IACnB,QAAU,YAAc,QAC1C,CAEO,SAASC,EAAUD,EAAsB,CAC9C,GAAI,EAAEA,aAAc,aAAc,MAAO,GACzC,IAAME,EAAQ,OAAO,iBAAiBF,CAAE,EACxC,OAAOE,EAAM,UAAY,QAAUA,EAAM,aAAe,UAAYA,EAAM,UAAY,GACxF,CAEO,SAASC,EAAqBH,EAAsB,CAEzD,OAAOA,EAAG,aAAa,WAAW,GAAK,CAAC,CAACA,EAAG,QAAQ,aAAa,CACnE,CAEO,SAASI,EAAYJ,EAAqB,CAnCjD,IAAAK,EAAAC,EAAAC,EAAAC,EAqCE,IAAMC,EAAQT,EAAG,aAAa,YAAY,EAC1C,GAAIS,EAAO,OAAOA,EAAM,KAAK,EAE7B,IAAMC,EAAaV,EAAG,aAAa,iBAAiB,EACpD,GAAIU,EAAY,CACd,IAAMC,EAAU,SAAS,eAAeD,CAAU,EAClD,GAAIC,EAAS,OAAOL,GAAAD,EAAAM,EAAQ,cAAR,YAAAN,EAAqB,SAArB,KAAAC,EAA+B,EACrD,CAEA,OAAOE,GAAAD,EAAAP,EAAG,cAAH,YAAAO,EAAgB,SAAhB,KAAAC,EAA0B,EACnC,CAEO,SAASI,EAAiBZ,EAAaa,EAA8B,CAC1E,OAAOA,EAAU,KAAMC,GAAQ,CAC7B,GAAI,CACF,OAAOd,EAAG,QAAQc,CAAG,CACvB,OAAQC,EAAA,CACN,MAAO,EACT,CACF,CAAC,CACH,CC9CA,IAAIC,EAAoC,KAExC,SAASC,EACPC,EACAC,EACAC,EACAC,EACM,CAWN,GATIC,EAAqBJ,CAAE,GAGvBE,EAAgB,OAAS,GAAKG,EAAiBL,EAAIE,CAAe,GAGlE,CAACG,EAAiBL,EAAIC,CAAS,GAG/B,CAACK,EAAUN,CAAE,EAAG,OAEpB,IAAMO,EAAOC,EAAYR,CAAE,EAC3B,GAAI,CAACO,EAAM,OAEX,IAAME,EAAaN,GAAA,KAAAA,EAAsBO,EAAcV,CAAE,EACzDW,EAASJ,EAAME,CAAU,CAC3B,CAEA,SAASG,EACPC,EACAZ,EACAC,EACAC,EACM,CACN,GAAIU,EAAK,WAAa,KAAK,aAAc,OACzC,IAAMb,EAAKa,EAGXd,EAAaC,EAAIC,EAAWC,EAAiBC,CAAkB,EAG3CH,EAAG,iBAAiBC,EAAU,KAAK,GAAG,CAAC,EAC/C,QAASa,GACnBf,EAAae,EAAOb,EAAWC,EAAiBC,CAAkB,CACpE,CACF,CAEO,SAASY,EAAcC,EAA+B,CAAC,EAAS,CAzDvE,IAAAC,EAAAC,EAAAC,EA2DE,GADI,OAAO,UAAa,aACpBrB,EAAU,OAEd,IAAMG,EAAY,CAAC,GAAGmB,EAAmB,IAAIH,EAAAD,EAAQ,YAAR,KAAAC,EAAqB,CAAC,CAAE,EAC/Df,GAAkBgB,EAAAF,EAAQ,SAAR,KAAAE,EAAkB,CAAC,EACrCf,EAAqBa,EAAQ,WAEnClB,EAAW,IAAI,iBAAkBuB,GAAc,CAC7C,QAAWC,KAAYD,EACrBC,EAAS,WAAW,QAAST,GAC3BD,EAASC,EAAMZ,EAAWC,EAAiBC,CAAkB,CAC/D,CAEJ,CAAC,EAEDL,EAAS,SAAQqB,EAAA,SAAS,OAAT,KAAAA,EAAiB,SAAS,gBAAiB,CAC1D,UAAW,GACX,QAAS,EACX,CAAC,CACH,CAEO,SAASI,GAAqB,CAC/BzB,IACFA,EAAS,WAAW,EACpBA,EAAW,KAEf,CHvEO,SAAS0B,EAAaC,EAA+B,CAAC,EAAS,CACpEC,EAAcD,CAAO,CACvB,CAUI,OAAO,UAAa,cAClB,SAAS,aAAe,UAC1B,SAAS,iBAAiB,mBAAoB,IAAME,EAAc,CAAC,EAEnEA,EAAc","names":["index_exports","__export","autoAnnounce","stopObserver","__toCommonJS","DEDUPE_MS","MAX_MESSAGE_LENGTH","politeRegion","assertiveRegion","recentMessages","createRegion","politeness","_a","el","ensureRegions","isDuplicate","message","now","last","oldest","announce","safe","DEFAULT_SELECTORS","getPoliteness","el","isVisible","style","alreadyHasLiveRegion","extractText","_a","_b","_c","_d","label","labelledBy","labelEl","matchesSelectors","selectors","sel","e","observer","checkElement","el","selectors","ignoreSelectors","politenessOverride","alreadyHasLiveRegion","matchesSelectors","isVisible","text","extractText","politeness","getPoliteness","announce","walkTree","node","child","startObserver","options","_a","_b","_c","DEFAULT_SELECTORS","mutations","mutation","stopObserver","autoAnnounce","options","startObserver","startObserver"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/announcer.ts","../src/detector.ts","../src/observer.ts"],"sourcesContent":["import { startObserver, stopObserver } from './observer';\nimport { AutoAnnounceOptions } from './types';\n\nexport type { AutoAnnounceOptions } from './types';\n\n/**\n * Manually configure and start auto-announce with custom options.\n * Only needed if you want to override defaults.\n *\n * @example\n * import { autoAnnounce } from '@a11y_craft/auto-announce';\n * autoAnnounce({ selectors: ['.my-toast'], ignore: ['.silent'] });\n */\nexport function autoAnnounce(options: AutoAnnounceOptions = {}): void {\n startObserver(options);\n}\n\n/**\n * Stop observing the DOM. Useful for cleanup in tests or unmounting.\n */\nexport { stopObserver as stopAutoAnnounce };\n\n// ─── Auto-initialize on import ───────────────────────────────────────────────\n// This is the magic — just importing this package starts the observer.\n// Works with DOMContentLoaded so it's safe to import at the top of any file.\nif (typeof document !== 'undefined') {\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', () => startObserver());\n } else {\n startObserver();\n }\n}\n","import { Politeness } from './types';\n\nconst DEDUPE_MS = 500;\nconst MAX_MESSAGE_LENGTH = 10_000;\n\nlet politeRegion: HTMLElement | null = null;\nlet assertiveRegion: HTMLElement | null = null;\nconst recentMessages = new Map<string, number>();\n\nfunction createRegion(politeness: Politeness): HTMLElement {\n const el = document.createElement('div');\n el.setAttribute('role', politeness === 'assertive' ? 'alert' : 'status');\n el.setAttribute('aria-live', politeness);\n el.setAttribute('aria-atomic', 'true');\n el.setAttribute('data-a11y-craft', 'live-region');\n el.style.position = 'absolute';\n el.style.width = '1px';\n el.style.height = '1px';\n el.style.padding = '0';\n el.style.margin = '-1px';\n el.style.overflow = 'hidden';\n el.style.clip = 'rect(0,0,0,0)';\n el.style.whiteSpace = 'nowrap';\n el.style.border = '0';\n const target = document.body ?? document.documentElement;\n target.appendChild(el);\n return el;\n}\n\nfunction ensureRegions(): void {\n if (!politeRegion) politeRegion = createRegion('polite');\n if (!assertiveRegion) assertiveRegion = createRegion('assertive');\n}\n\nfunction isDuplicate(message: string): boolean {\n const now = Date.now();\n const last = recentMessages.get(message);\n if (last !== undefined && now - last < DEDUPE_MS) return true;\n if (recentMessages.size >= 200) {\n const oldest = recentMessages.keys().next().value;\n if (oldest !== undefined) recentMessages.delete(oldest);\n }\n recentMessages.set(message, now);\n return false;\n}\n\nexport function announce(message: string, politeness: Politeness = 'polite'): void {\n if (typeof document === 'undefined') return;\n if (!message || typeof message !== 'string') return;\n\n const safe = message.length > MAX_MESSAGE_LENGTH ? message.slice(0, MAX_MESSAGE_LENGTH) : message;\n if (isDuplicate(safe)) return;\n\n ensureRegions();\n let el = politeness === 'assertive' ? assertiveRegion! : politeRegion!;\n\n // If the region was removed from the DOM (e.g. innerHTML reset), recreate it\n // Use ownerDocument.contains() instead of isConnected for broader browser support\n if (!el.ownerDocument || !el.ownerDocument.documentElement.contains(el)) {\n if (politeness === 'assertive') {\n assertiveRegion = createRegion('assertive');\n el = assertiveRegion;\n } else {\n politeRegion = createRegion('polite');\n el = politeRegion;\n }\n }\n\n el.textContent = '';\n setTimeout(() => { el.textContent = safe; }, 0);\n}\n","import { Politeness } from './types';\n\nexport const DEFAULT_SELECTORS = [\n '[role=\"alert\"]',\n '[role=\"status\"]',\n '[role=\"log\"]',\n '.toast',\n '.toast-message',\n '.notification',\n '.banner',\n '.alert',\n '.snackbar',\n '.flash',\n '.flash-message',\n '[data-announce]',\n '[data-notification]',\n '[data-toast]',\n];\n\nexport function getPoliteness(el: Element): Politeness {\n const role = el.getAttribute('role');\n return role === 'alert' ? 'assertive' : 'polite';\n}\n\nexport function isVisible(el: Element): boolean {\n if (!(el instanceof HTMLElement)) return false;\n const style = window.getComputedStyle(el);\n return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';\n}\n\nexport function alreadyHasLiveRegion(el: Element): boolean {\n // Skip if the element itself has aria-live\n if (el.hasAttribute('aria-live')) return true;\n // Walk up the tree manually — el.closest() not available in IE11\n let parent = el.parentElement;\n while (parent) {\n if (parent.hasAttribute('aria-live')) return true;\n parent = parent.parentElement;\n }\n return false;\n}\n\nexport function extractText(el: Element): string {\n // Prefer aria-label, then aria-labelledby, then textContent\n const label = el.getAttribute('aria-label');\n if (label) return label.trim();\n\n const labelledBy = el.getAttribute('aria-labelledby');\n if (labelledBy) {\n const labelEl = document.getElementById(labelledBy);\n // Only read from elements that are descendants of the notification itself\n // or siblings — never from arbitrary page elements to prevent announcing\n // unintended sensitive content\n if (labelEl && el.contains(labelEl)) {\n return labelEl.textContent?.trim() ?? '';\n }\n }\n\n return el.textContent?.trim() ?? '';\n}\n\nexport function matchesSelectors(el: Element, selectors: string[]): boolean {\n // Support IE11's vendor-prefixed version\n const matchFn: ((sel: string) => boolean) | undefined =\n el.matches?.bind(el) ??\n (el as Element & { msMatchesSelector?: (s: string) => boolean }).msMatchesSelector?.bind(el);\n\n if (!matchFn) return false;\n\n return selectors.some((sel) => {\n try {\n return matchFn(sel);\n } catch {\n return false;\n }\n });\n}\n","import { announce } from './announcer';\nimport {\n DEFAULT_SELECTORS,\n alreadyHasLiveRegion,\n extractText,\n getPoliteness,\n isVisible,\n matchesSelectors,\n} from './detector';\nimport { AutoAnnounceOptions, Politeness } from './types';\n\nlet observer: MutationObserver | null = null;\n\nfunction checkElement(\n el: Element,\n selectors: string[],\n ignoreSelectors: string[],\n politenessOverride?: Politeness,\n): void {\n // Skip if already handled by aria-live\n if (alreadyHasLiveRegion(el)) return;\n\n // Skip ignored selectors\n if (ignoreSelectors.length > 0 && matchesSelectors(el, ignoreSelectors)) return;\n\n // Must match one of our selectors\n if (!matchesSelectors(el, selectors)) return;\n\n // Must be visible\n if (!isVisible(el)) return;\n\n const text = extractText(el);\n if (!text) return;\n\n const politeness = politenessOverride ?? getPoliteness(el);\n announce(text, politeness);\n}\n\nfunction walkTree(\n node: Node,\n selectors: string[],\n ignoreSelectors: string[],\n politenessOverride?: Politeness,\n): void {\n if (node.nodeType !== Node.ELEMENT_NODE) return;\n const el = node as Element;\n\n // Check the node itself\n checkElement(el, selectors, ignoreSelectors, politenessOverride);\n\n // Check its descendants — use for loop instead of NodeList.forEach for broader compat\n const descendants = el.querySelectorAll(selectors.join(','));\n for (let i = 0; i < descendants.length; i++) {\n checkElement(descendants[i], selectors, ignoreSelectors, politenessOverride);\n }\n}\n\nexport function startObserver(options: AutoAnnounceOptions = {}): void {\n if (typeof document === 'undefined') return;\n if (observer) return; // already running\n\n const MAX_CUSTOM_SELECTORS = 20;\n const customSelectors = (options.selectors ?? []).slice(0, MAX_CUSTOM_SELECTORS);\n const selectors = [...DEFAULT_SELECTORS, ...customSelectors];\n const ignoreSelectors = (options.ignore ?? []).slice(0, MAX_CUSTOM_SELECTORS);\n const politenessOverride = options.politeness;\n\n observer = new MutationObserver((mutations) => {\n for (let i = 0; i < mutations.length; i++) {\n const added = mutations[i].addedNodes;\n for (let j = 0; j < added.length; j++) {\n walkTree(added[j], selectors, ignoreSelectors, politenessOverride);\n }\n }\n });\n\n observer.observe(document.body ?? document.documentElement, {\n childList: true,\n subtree: true,\n });\n}\n\nexport function stopObserver(): void {\n if (observer) {\n observer.disconnect();\n observer = null;\n }\n}\n"],"mappings":"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","names":["index_exports","__export","autoAnnounce","stopObserver","__toCommonJS","DEDUPE_MS","MAX_MESSAGE_LENGTH","politeRegion","assertiveRegion","recentMessages","createRegion","politeness","_a","el","ensureRegions","isDuplicate","message","now","last","oldest","announce","safe","DEFAULT_SELECTORS","getPoliteness","el","isVisible","style","alreadyHasLiveRegion","parent","extractText","_a","_b","_c","_d","label","labelledBy","labelEl","matchesSelectors","selectors","matchFn","sel","e","observer","checkElement","el","selectors","ignoreSelectors","politenessOverride","alreadyHasLiveRegion","matchesSelectors","isVisible","text","extractText","politeness","getPoliteness","announce","walkTree","node","descendants","i","startObserver","options","_a","_b","_c","MAX_CUSTOM_SELECTORS","customSelectors","DEFAULT_SELECTORS","mutations","added","j","stopObserver","autoAnnounce","options","startObserver","startObserver"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var
|
|
1
|
+
var O=500,A=1e4,c=null,d=null,u=new Map;function f(t){var n;let e=document.createElement("div");return e.setAttribute("role",t==="assertive"?"alert":"status"),e.setAttribute("aria-live",t),e.setAttribute("aria-atomic","true"),e.setAttribute("data-a11y-craft","live-region"),e.style.position="absolute",e.style.width="1px",e.style.height="1px",e.style.padding="0",e.style.margin="-1px",e.style.overflow="hidden",e.style.clip="rect(0,0,0,0)",e.style.whiteSpace="nowrap",e.style.border="0",((n=document.body)!=null?n:document.documentElement).appendChild(e),e}function C(){c||(c=f("polite")),d||(d=f("assertive"))}function P(t){let e=Date.now(),o=u.get(t);if(o!==void 0&&e-o<O)return!0;if(u.size>=200){let n=u.keys().next().value;n!==void 0&&u.delete(n)}return u.set(t,e),!1}function h(t,e="polite"){if(typeof document=="undefined"||!t||typeof t!="string")return;let o=t.length>A?t.slice(0,A):t;if(P(o))return;C();let n=e==="assertive"?d:c;(!n.ownerDocument||!n.ownerDocument.documentElement.contains(n))&&(e==="assertive"?(d=f("assertive"),n=d):(c=f("polite"),n=c)),n.textContent="",setTimeout(()=>{n.textContent=o},0)}var x=['[role="alert"]','[role="status"]','[role="log"]',".toast",".toast-message",".notification",".banner",".alert",".snackbar",".flash",".flash-message","[data-announce]","[data-notification]","[data-toast]"];function S(t){return t.getAttribute("role")==="alert"?"assertive":"polite"}function M(t){if(!(t instanceof HTMLElement))return!1;let e=window.getComputedStyle(t);return e.display!=="none"&&e.visibility!=="hidden"&&e.opacity!=="0"}function T(t){if(t.hasAttribute("aria-live"))return!0;let e=t.parentElement;for(;e;){if(e.hasAttribute("aria-live"))return!0;e=e.parentElement}return!1}function L(t){var n,r,i,s;let e=t.getAttribute("aria-label");if(e)return e.trim();let o=t.getAttribute("aria-labelledby");if(o){let l=document.getElementById(o);if(l&&t.contains(l))return(r=(n=l.textContent)==null?void 0:n.trim())!=null?r:""}return(s=(i=t.textContent)==null?void 0:i.trim())!=null?s:""}function E(t,e){var n,r,i;let o=(i=(n=t.matches)==null?void 0:n.bind(t))!=null?i:(r=t.msMatchesSelector)==null?void 0:r.bind(t);return o?e.some(s=>{try{return o(s)}catch(l){return!1}}):!1}var a=null;function w(t,e,o,n){if(T(t)||o.length>0&&E(t,o)||!E(t,e)||!M(t))return;let r=L(t);if(!r)return;let i=n!=null?n:S(t);h(r,i)}function D(t,e,o,n){if(t.nodeType!==Node.ELEMENT_NODE)return;let r=t;w(r,e,o,n);let i=r.querySelectorAll(e.join(","));for(let s=0;s<i.length;s++)w(i[s],e,o,n)}function m(t={}){var s,l,g;if(typeof document=="undefined"||a)return;let e=20,o=((s=t.selectors)!=null?s:[]).slice(0,e),n=[...x,...o],r=((l=t.ignore)!=null?l:[]).slice(0,e),i=t.politeness;a=new MutationObserver(y=>{for(let p=0;p<y.length;p++){let v=y[p].addedNodes;for(let b=0;b<v.length;b++)D(v[b],n,r,i)}}),a.observe((g=document.body)!=null?g:document.documentElement,{childList:!0,subtree:!0})}function R(){a&&(a.disconnect(),a=null)}function j(t={}){m(t)}typeof document!="undefined"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>m()):m());export{j as autoAnnounce,R as stopAutoAnnounce};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/announcer.ts","../src/detector.ts","../src/observer.ts","../src/index.ts"],"sourcesContent":["import { Politeness } from './types';\n\nconst DEDUPE_MS = 500;\nconst MAX_MESSAGE_LENGTH = 10_000;\n\nlet politeRegion: HTMLElement | null = null;\nlet assertiveRegion: HTMLElement | null = null;\nconst recentMessages = new Map<string, number>();\n\nfunction createRegion(politeness: Politeness): HTMLElement {\n const el = document.createElement('div');\n el.setAttribute('role', politeness === 'assertive' ? 'alert' : 'status');\n el.setAttribute('aria-live', politeness);\n el.setAttribute('aria-atomic', 'true');\n el.setAttribute('data-a11y-craft', 'live-region');\n el.style.position = 'absolute';\n el.style.width = '1px';\n el.style.height = '1px';\n el.style.padding = '0';\n el.style.margin = '-1px';\n el.style.overflow = 'hidden';\n el.style.clip = 'rect(0,0,0,0)';\n el.style.whiteSpace = 'nowrap';\n el.style.border = '0';\n const target = document.body ?? document.documentElement;\n target.appendChild(el);\n return el;\n}\n\nfunction ensureRegions(): void {\n if (!politeRegion) politeRegion = createRegion('polite');\n if (!assertiveRegion) assertiveRegion = createRegion('assertive');\n}\n\nfunction isDuplicate(message: string): boolean {\n const now = Date.now();\n const last = recentMessages.get(message);\n if (last !== undefined && now - last < DEDUPE_MS) return true;\n if (recentMessages.size >= 200) {\n const oldest = recentMessages.keys().next().value;\n if (oldest !== undefined) recentMessages.delete(oldest);\n }\n recentMessages.set(message, now);\n return false;\n}\n\nexport function announce(message: string, politeness: Politeness = 'polite'): void {\n if (typeof document === 'undefined') return;\n if (!message || typeof message !== 'string') return;\n\n const safe = message.length > MAX_MESSAGE_LENGTH ? message.slice(0, MAX_MESSAGE_LENGTH) : message;\n if (isDuplicate(safe)) return;\n\n ensureRegions();\n const el = politeness === 'assertive' ? assertiveRegion! : politeRegion!;\n el.textContent = '';\n setTimeout(() => { el.textContent = safe; }, 0);\n}\n","import { Politeness } from './types';\n\nexport const DEFAULT_SELECTORS = [\n '[role=\"alert\"]',\n '[role=\"status\"]',\n '[role=\"log\"]',\n '.toast',\n '.toast-message',\n '.notification',\n '.banner',\n '.alert',\n '.snackbar',\n '.flash',\n '.flash-message',\n '[data-announce]',\n '[data-notification]',\n '[data-toast]',\n];\n\nexport function getPoliteness(el: Element): Politeness {\n const role = el.getAttribute('role');\n return role === 'alert' ? 'assertive' : 'polite';\n}\n\nexport function isVisible(el: Element): boolean {\n if (!(el instanceof HTMLElement)) return false;\n const style = window.getComputedStyle(el);\n return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';\n}\n\nexport function alreadyHasLiveRegion(el: Element): boolean {\n // Skip if the element itself or any ancestor is already an aria-live region\n return el.hasAttribute('aria-live') || !!el.closest('[aria-live]');\n}\n\nexport function extractText(el: Element): string {\n // Prefer aria-label, then aria-labelledby, then textContent\n const label = el.getAttribute('aria-label');\n if (label) return label.trim();\n\n const labelledBy = el.getAttribute('aria-labelledby');\n if (labelledBy) {\n const labelEl = document.getElementById(labelledBy);\n if (labelEl) return labelEl.textContent?.trim() ?? '';\n }\n\n return el.textContent?.trim() ?? '';\n}\n\nexport function matchesSelectors(el: Element, selectors: string[]): boolean {\n return selectors.some((sel) => {\n try {\n return el.matches(sel);\n } catch {\n return false;\n }\n });\n}\n","import { announce } from './announcer';\nimport {\n DEFAULT_SELECTORS,\n alreadyHasLiveRegion,\n extractText,\n getPoliteness,\n isVisible,\n matchesSelectors,\n} from './detector';\nimport { AutoAnnounceOptions, Politeness } from './types';\n\nlet observer: MutationObserver | null = null;\n\nfunction checkElement(\n el: Element,\n selectors: string[],\n ignoreSelectors: string[],\n politenessOverride?: Politeness,\n): void {\n // Skip if already handled by aria-live\n if (alreadyHasLiveRegion(el)) return;\n\n // Skip ignored selectors\n if (ignoreSelectors.length > 0 && matchesSelectors(el, ignoreSelectors)) return;\n\n // Must match one of our selectors\n if (!matchesSelectors(el, selectors)) return;\n\n // Must be visible\n if (!isVisible(el)) return;\n\n const text = extractText(el);\n if (!text) return;\n\n const politeness = politenessOverride ?? getPoliteness(el);\n announce(text, politeness);\n}\n\nfunction walkTree(\n node: Node,\n selectors: string[],\n ignoreSelectors: string[],\n politenessOverride?: Politeness,\n): void {\n if (node.nodeType !== Node.ELEMENT_NODE) return;\n const el = node as Element;\n\n // Check the node itself\n checkElement(el, selectors, ignoreSelectors, politenessOverride);\n\n // Check its descendants\n const descendants = el.querySelectorAll(selectors.join(','));\n descendants.forEach((child) =>\n checkElement(child, selectors, ignoreSelectors, politenessOverride),\n );\n}\n\nexport function startObserver(options: AutoAnnounceOptions = {}): void {\n if (typeof document === 'undefined') return;\n if (observer) return; // already running\n\n const selectors = [...DEFAULT_SELECTORS, ...(options.selectors ?? [])];\n const ignoreSelectors = options.ignore ?? [];\n const politenessOverride = options.politeness;\n\n observer = new MutationObserver((mutations) => {\n for (const mutation of mutations) {\n mutation.addedNodes.forEach((node) =>\n walkTree(node, selectors, ignoreSelectors, politenessOverride),\n );\n }\n });\n\n observer.observe(document.body ?? document.documentElement, {\n childList: true,\n subtree: true,\n });\n}\n\nexport function stopObserver(): void {\n if (observer) {\n observer.disconnect();\n observer = null;\n }\n}\n","import { startObserver, stopObserver } from './observer';\nimport { AutoAnnounceOptions } from './types';\n\nexport type { AutoAnnounceOptions } from './types';\n\n/**\n * Manually configure and start auto-announce with custom options.\n * Only needed if you want to override defaults.\n *\n * @example\n * import { autoAnnounce } from '@a11y_craft/auto-announce';\n * autoAnnounce({ selectors: ['.my-toast'], ignore: ['.silent'] });\n */\nexport function autoAnnounce(options: AutoAnnounceOptions = {}): void {\n startObserver(options);\n}\n\n/**\n * Stop observing the DOM. Useful for cleanup in tests or unmounting.\n */\nexport { stopObserver as stopAutoAnnounce };\n\n// ─── Auto-initialize on import ───────────────────────────────────────────────\n// This is the magic — just importing this package starts the observer.\n// Works with DOMContentLoaded so it's safe to import at the top of any file.\nif (typeof document !== 'undefined') {\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', () => startObserver());\n } else {\n startObserver();\n }\n}\n"],"mappings":"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","names":["DEDUPE_MS","MAX_MESSAGE_LENGTH","politeRegion","assertiveRegion","recentMessages","createRegion","politeness","_a","el","ensureRegions","isDuplicate","message","now","last","oldest","announce","safe","DEFAULT_SELECTORS","getPoliteness","el","isVisible","style","alreadyHasLiveRegion","extractText","_a","_b","_c","_d","label","labelledBy","labelEl","matchesSelectors","selectors","sel","e","observer","checkElement","el","selectors","ignoreSelectors","politenessOverride","alreadyHasLiveRegion","matchesSelectors","isVisible","text","extractText","politeness","getPoliteness","announce","walkTree","node","child","startObserver","options","_a","_b","_c","DEFAULT_SELECTORS","mutations","mutation","stopObserver","autoAnnounce","options","startObserver","startObserver"]}
|
|
1
|
+
{"version":3,"sources":["../src/announcer.ts","../src/detector.ts","../src/observer.ts","../src/index.ts"],"sourcesContent":["import { Politeness } from './types';\n\nconst DEDUPE_MS = 500;\nconst MAX_MESSAGE_LENGTH = 10_000;\n\nlet politeRegion: HTMLElement | null = null;\nlet assertiveRegion: HTMLElement | null = null;\nconst recentMessages = new Map<string, number>();\n\nfunction createRegion(politeness: Politeness): HTMLElement {\n const el = document.createElement('div');\n el.setAttribute('role', politeness === 'assertive' ? 'alert' : 'status');\n el.setAttribute('aria-live', politeness);\n el.setAttribute('aria-atomic', 'true');\n el.setAttribute('data-a11y-craft', 'live-region');\n el.style.position = 'absolute';\n el.style.width = '1px';\n el.style.height = '1px';\n el.style.padding = '0';\n el.style.margin = '-1px';\n el.style.overflow = 'hidden';\n el.style.clip = 'rect(0,0,0,0)';\n el.style.whiteSpace = 'nowrap';\n el.style.border = '0';\n const target = document.body ?? document.documentElement;\n target.appendChild(el);\n return el;\n}\n\nfunction ensureRegions(): void {\n if (!politeRegion) politeRegion = createRegion('polite');\n if (!assertiveRegion) assertiveRegion = createRegion('assertive');\n}\n\nfunction isDuplicate(message: string): boolean {\n const now = Date.now();\n const last = recentMessages.get(message);\n if (last !== undefined && now - last < DEDUPE_MS) return true;\n if (recentMessages.size >= 200) {\n const oldest = recentMessages.keys().next().value;\n if (oldest !== undefined) recentMessages.delete(oldest);\n }\n recentMessages.set(message, now);\n return false;\n}\n\nexport function announce(message: string, politeness: Politeness = 'polite'): void {\n if (typeof document === 'undefined') return;\n if (!message || typeof message !== 'string') return;\n\n const safe = message.length > MAX_MESSAGE_LENGTH ? message.slice(0, MAX_MESSAGE_LENGTH) : message;\n if (isDuplicate(safe)) return;\n\n ensureRegions();\n let el = politeness === 'assertive' ? assertiveRegion! : politeRegion!;\n\n // If the region was removed from the DOM (e.g. innerHTML reset), recreate it\n // Use ownerDocument.contains() instead of isConnected for broader browser support\n if (!el.ownerDocument || !el.ownerDocument.documentElement.contains(el)) {\n if (politeness === 'assertive') {\n assertiveRegion = createRegion('assertive');\n el = assertiveRegion;\n } else {\n politeRegion = createRegion('polite');\n el = politeRegion;\n }\n }\n\n el.textContent = '';\n setTimeout(() => { el.textContent = safe; }, 0);\n}\n","import { Politeness } from './types';\n\nexport const DEFAULT_SELECTORS = [\n '[role=\"alert\"]',\n '[role=\"status\"]',\n '[role=\"log\"]',\n '.toast',\n '.toast-message',\n '.notification',\n '.banner',\n '.alert',\n '.snackbar',\n '.flash',\n '.flash-message',\n '[data-announce]',\n '[data-notification]',\n '[data-toast]',\n];\n\nexport function getPoliteness(el: Element): Politeness {\n const role = el.getAttribute('role');\n return role === 'alert' ? 'assertive' : 'polite';\n}\n\nexport function isVisible(el: Element): boolean {\n if (!(el instanceof HTMLElement)) return false;\n const style = window.getComputedStyle(el);\n return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';\n}\n\nexport function alreadyHasLiveRegion(el: Element): boolean {\n // Skip if the element itself has aria-live\n if (el.hasAttribute('aria-live')) return true;\n // Walk up the tree manually — el.closest() not available in IE11\n let parent = el.parentElement;\n while (parent) {\n if (parent.hasAttribute('aria-live')) return true;\n parent = parent.parentElement;\n }\n return false;\n}\n\nexport function extractText(el: Element): string {\n // Prefer aria-label, then aria-labelledby, then textContent\n const label = el.getAttribute('aria-label');\n if (label) return label.trim();\n\n const labelledBy = el.getAttribute('aria-labelledby');\n if (labelledBy) {\n const labelEl = document.getElementById(labelledBy);\n // Only read from elements that are descendants of the notification itself\n // or siblings — never from arbitrary page elements to prevent announcing\n // unintended sensitive content\n if (labelEl && el.contains(labelEl)) {\n return labelEl.textContent?.trim() ?? '';\n }\n }\n\n return el.textContent?.trim() ?? '';\n}\n\nexport function matchesSelectors(el: Element, selectors: string[]): boolean {\n // Support IE11's vendor-prefixed version\n const matchFn: ((sel: string) => boolean) | undefined =\n el.matches?.bind(el) ??\n (el as Element & { msMatchesSelector?: (s: string) => boolean }).msMatchesSelector?.bind(el);\n\n if (!matchFn) return false;\n\n return selectors.some((sel) => {\n try {\n return matchFn(sel);\n } catch {\n return false;\n }\n });\n}\n","import { announce } from './announcer';\nimport {\n DEFAULT_SELECTORS,\n alreadyHasLiveRegion,\n extractText,\n getPoliteness,\n isVisible,\n matchesSelectors,\n} from './detector';\nimport { AutoAnnounceOptions, Politeness } from './types';\n\nlet observer: MutationObserver | null = null;\n\nfunction checkElement(\n el: Element,\n selectors: string[],\n ignoreSelectors: string[],\n politenessOverride?: Politeness,\n): void {\n // Skip if already handled by aria-live\n if (alreadyHasLiveRegion(el)) return;\n\n // Skip ignored selectors\n if (ignoreSelectors.length > 0 && matchesSelectors(el, ignoreSelectors)) return;\n\n // Must match one of our selectors\n if (!matchesSelectors(el, selectors)) return;\n\n // Must be visible\n if (!isVisible(el)) return;\n\n const text = extractText(el);\n if (!text) return;\n\n const politeness = politenessOverride ?? getPoliteness(el);\n announce(text, politeness);\n}\n\nfunction walkTree(\n node: Node,\n selectors: string[],\n ignoreSelectors: string[],\n politenessOverride?: Politeness,\n): void {\n if (node.nodeType !== Node.ELEMENT_NODE) return;\n const el = node as Element;\n\n // Check the node itself\n checkElement(el, selectors, ignoreSelectors, politenessOverride);\n\n // Check its descendants — use for loop instead of NodeList.forEach for broader compat\n const descendants = el.querySelectorAll(selectors.join(','));\n for (let i = 0; i < descendants.length; i++) {\n checkElement(descendants[i], selectors, ignoreSelectors, politenessOverride);\n }\n}\n\nexport function startObserver(options: AutoAnnounceOptions = {}): void {\n if (typeof document === 'undefined') return;\n if (observer) return; // already running\n\n const MAX_CUSTOM_SELECTORS = 20;\n const customSelectors = (options.selectors ?? []).slice(0, MAX_CUSTOM_SELECTORS);\n const selectors = [...DEFAULT_SELECTORS, ...customSelectors];\n const ignoreSelectors = (options.ignore ?? []).slice(0, MAX_CUSTOM_SELECTORS);\n const politenessOverride = options.politeness;\n\n observer = new MutationObserver((mutations) => {\n for (let i = 0; i < mutations.length; i++) {\n const added = mutations[i].addedNodes;\n for (let j = 0; j < added.length; j++) {\n walkTree(added[j], selectors, ignoreSelectors, politenessOverride);\n }\n }\n });\n\n observer.observe(document.body ?? document.documentElement, {\n childList: true,\n subtree: true,\n });\n}\n\nexport function stopObserver(): void {\n if (observer) {\n observer.disconnect();\n observer = null;\n }\n}\n","import { startObserver, stopObserver } from './observer';\nimport { AutoAnnounceOptions } from './types';\n\nexport type { AutoAnnounceOptions } from './types';\n\n/**\n * Manually configure and start auto-announce with custom options.\n * Only needed if you want to override defaults.\n *\n * @example\n * import { autoAnnounce } from '@a11y_craft/auto-announce';\n * autoAnnounce({ selectors: ['.my-toast'], ignore: ['.silent'] });\n */\nexport function autoAnnounce(options: AutoAnnounceOptions = {}): void {\n startObserver(options);\n}\n\n/**\n * Stop observing the DOM. Useful for cleanup in tests or unmounting.\n */\nexport { stopObserver as stopAutoAnnounce };\n\n// ─── Auto-initialize on import ───────────────────────────────────────────────\n// This is the magic — just importing this package starts the observer.\n// Works with DOMContentLoaded so it's safe to import at the top of any file.\nif (typeof document !== 'undefined') {\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', () => startObserver());\n } else {\n startObserver();\n }\n}\n"],"mappings":"AAEA,IAAMA,EAAY,IACZC,EAAqB,IAEvBC,EAAmC,KACnCC,EAAsC,KACpCC,EAAiB,IAAI,IAE3B,SAASC,EAAaC,EAAqC,CAT3D,IAAAC,EAUE,IAAMC,EAAK,SAAS,cAAc,KAAK,EACvC,OAAAA,EAAG,aAAa,OAAQF,IAAe,YAAc,QAAU,QAAQ,EACvEE,EAAG,aAAa,YAAaF,CAAU,EACvCE,EAAG,aAAa,cAAe,MAAM,EACrCA,EAAG,aAAa,kBAAmB,aAAa,EAChDA,EAAG,MAAM,SAAW,WACpBA,EAAG,MAAM,MAAQ,MACjBA,EAAG,MAAM,OAAS,MAClBA,EAAG,MAAM,QAAU,IACnBA,EAAG,MAAM,OAAS,OAClBA,EAAG,MAAM,SAAW,SACpBA,EAAG,MAAM,KAAO,gBAChBA,EAAG,MAAM,WAAa,SACtBA,EAAG,MAAM,OAAS,MACHD,EAAA,SAAS,OAAT,KAAAA,EAAiB,SAAS,iBAClC,YAAYC,CAAE,EACdA,CACT,CAEA,SAASC,GAAsB,CACxBP,IAAcA,EAAeG,EAAa,QAAQ,GAClDF,IAAiBA,EAAkBE,EAAa,WAAW,EAClE,CAEA,SAASK,EAAYC,EAA0B,CAC7C,IAAMC,EAAM,KAAK,IAAI,EACfC,EAAOT,EAAe,IAAIO,CAAO,EACvC,GAAIE,IAAS,QAAaD,EAAMC,EAAOb,EAAW,MAAO,GACzD,GAAII,EAAe,MAAQ,IAAK,CAC9B,IAAMU,EAASV,EAAe,KAAK,EAAE,KAAK,EAAE,MACxCU,IAAW,QAAWV,EAAe,OAAOU,CAAM,CACxD,CACA,OAAAV,EAAe,IAAIO,EAASC,CAAG,EACxB,EACT,CAEO,SAASG,EAASJ,EAAiBL,EAAyB,SAAgB,CAEjF,GADI,OAAO,UAAa,aACpB,CAACK,GAAW,OAAOA,GAAY,SAAU,OAE7C,IAAMK,EAAOL,EAAQ,OAASV,EAAqBU,EAAQ,MAAM,EAAGV,CAAkB,EAAIU,EAC1F,GAAID,EAAYM,CAAI,EAAG,OAEvBP,EAAc,EACd,IAAID,EAAKF,IAAe,YAAcH,EAAmBD,GAIrD,CAACM,EAAG,eAAiB,CAACA,EAAG,cAAc,gBAAgB,SAASA,CAAE,KAChEF,IAAe,aACjBH,EAAkBE,EAAa,WAAW,EAC1CG,EAAKL,IAELD,EAAeG,EAAa,QAAQ,EACpCG,EAAKN,IAITM,EAAG,YAAc,GACjB,WAAW,IAAM,CAAEA,EAAG,YAAcQ,CAAM,EAAG,CAAC,CAChD,CCpEO,IAAMC,EAAoB,CAC/B,iBACA,kBACA,eACA,SACA,iBACA,gBACA,UACA,SACA,YACA,SACA,iBACA,kBACA,sBACA,cACF,EAEO,SAASC,EAAcC,EAAyB,CAErD,OADaA,EAAG,aAAa,MAAM,IACnB,QAAU,YAAc,QAC1C,CAEO,SAASC,EAAUD,EAAsB,CAC9C,GAAI,EAAEA,aAAc,aAAc,MAAO,GACzC,IAAME,EAAQ,OAAO,iBAAiBF,CAAE,EACxC,OAAOE,EAAM,UAAY,QAAUA,EAAM,aAAe,UAAYA,EAAM,UAAY,GACxF,CAEO,SAASC,EAAqBH,EAAsB,CAEzD,GAAIA,EAAG,aAAa,WAAW,EAAG,MAAO,GAEzC,IAAII,EAASJ,EAAG,cAChB,KAAOI,GAAQ,CACb,GAAIA,EAAO,aAAa,WAAW,EAAG,MAAO,GAC7CA,EAASA,EAAO,aAClB,CACA,MAAO,EACT,CAEO,SAASC,EAAYL,EAAqB,CA1CjD,IAAAM,EAAAC,EAAAC,EAAAC,EA4CE,IAAMC,EAAQV,EAAG,aAAa,YAAY,EAC1C,GAAIU,EAAO,OAAOA,EAAM,KAAK,EAE7B,IAAMC,EAAaX,EAAG,aAAa,iBAAiB,EACpD,GAAIW,EAAY,CACd,IAAMC,EAAU,SAAS,eAAeD,CAAU,EAIlD,GAAIC,GAAWZ,EAAG,SAASY,CAAO,EAChC,OAAOL,GAAAD,EAAAM,EAAQ,cAAR,YAAAN,EAAqB,SAArB,KAAAC,EAA+B,EAE1C,CAEA,OAAOE,GAAAD,EAAAR,EAAG,cAAH,YAAAQ,EAAgB,SAAhB,KAAAC,EAA0B,EACnC,CAEO,SAASI,EAAiBb,EAAac,EAA8B,CA7D5E,IAAAR,EAAAC,EAAAC,EA+DE,IAAMO,GACJP,GAAAF,EAAAN,EAAG,UAAH,YAAAM,EAAY,KAAKN,KAAjB,KAAAQ,GACCD,EAAAP,EAAgE,oBAAhE,YAAAO,EAAmF,KAAKP,GAE3F,OAAKe,EAEED,EAAU,KAAME,GAAQ,CAC7B,GAAI,CACF,OAAOD,EAAQC,CAAG,CACpB,OAAQC,EAAA,CACN,MAAO,EACT,CACF,CAAC,EARoB,EASvB,CCjEA,IAAIC,EAAoC,KAExC,SAASC,EACPC,EACAC,EACAC,EACAC,EACM,CAWN,GATIC,EAAqBJ,CAAE,GAGvBE,EAAgB,OAAS,GAAKG,EAAiBL,EAAIE,CAAe,GAGlE,CAACG,EAAiBL,EAAIC,CAAS,GAG/B,CAACK,EAAUN,CAAE,EAAG,OAEpB,IAAMO,EAAOC,EAAYR,CAAE,EAC3B,GAAI,CAACO,EAAM,OAEX,IAAME,EAAaN,GAAA,KAAAA,EAAsBO,EAAcV,CAAE,EACzDW,EAASJ,EAAME,CAAU,CAC3B,CAEA,SAASG,EACPC,EACAZ,EACAC,EACAC,EACM,CACN,GAAIU,EAAK,WAAa,KAAK,aAAc,OACzC,IAAMb,EAAKa,EAGXd,EAAaC,EAAIC,EAAWC,EAAiBC,CAAkB,EAG/D,IAAMW,EAAcd,EAAG,iBAAiBC,EAAU,KAAK,GAAG,CAAC,EAC3D,QAASc,EAAI,EAAGA,EAAID,EAAY,OAAQC,IACtChB,EAAae,EAAYC,CAAC,EAAGd,EAAWC,EAAiBC,CAAkB,CAE/E,CAEO,SAASa,EAAcC,EAA+B,CAAC,EAAS,CAzDvE,IAAAC,EAAAC,EAAAC,EA2DE,GADI,OAAO,UAAa,aACpBtB,EAAU,OAEd,IAAMuB,EAAuB,GACvBC,IAAmBJ,EAAAD,EAAQ,YAAR,KAAAC,EAAqB,CAAC,GAAG,MAAM,EAAGG,CAAoB,EACzEpB,EAAY,CAAC,GAAGsB,EAAmB,GAAGD,CAAe,EACrDpB,IAAmBiB,EAAAF,EAAQ,SAAR,KAAAE,EAAkB,CAAC,GAAG,MAAM,EAAGE,CAAoB,EACtElB,EAAqBc,EAAQ,WAEnCnB,EAAW,IAAI,iBAAkB0B,GAAc,CAC7C,QAAST,EAAI,EAAGA,EAAIS,EAAU,OAAQT,IAAK,CACzC,IAAMU,EAAQD,EAAUT,CAAC,EAAE,WAC3B,QAASW,EAAI,EAAGA,EAAID,EAAM,OAAQC,IAChCd,EAASa,EAAMC,CAAC,EAAGzB,EAAWC,EAAiBC,CAAkB,CAErE,CACF,CAAC,EAEDL,EAAS,SAAQsB,EAAA,SAAS,OAAT,KAAAA,EAAiB,SAAS,gBAAiB,CAC1D,UAAW,GACX,QAAS,EACX,CAAC,CACH,CAEO,SAASO,GAAqB,CAC/B7B,IACFA,EAAS,WAAW,EACpBA,EAAW,KAEf,CC1EO,SAAS8B,EAAaC,EAA+B,CAAC,EAAS,CACpEC,EAAcD,CAAO,CACvB,CAUI,OAAO,UAAa,cAClB,SAAS,aAAe,UAC1B,SAAS,iBAAiB,mBAAoB,IAAME,EAAc,CAAC,EAEnEA,EAAc","names":["DEDUPE_MS","MAX_MESSAGE_LENGTH","politeRegion","assertiveRegion","recentMessages","createRegion","politeness","_a","el","ensureRegions","isDuplicate","message","now","last","oldest","announce","safe","DEFAULT_SELECTORS","getPoliteness","el","isVisible","style","alreadyHasLiveRegion","parent","extractText","_a","_b","_c","_d","label","labelledBy","labelEl","matchesSelectors","selectors","matchFn","sel","e","observer","checkElement","el","selectors","ignoreSelectors","politenessOverride","alreadyHasLiveRegion","matchesSelectors","isVisible","text","extractText","politeness","getPoliteness","announce","walkTree","node","descendants","i","startObserver","options","_a","_b","_c","MAX_CUSTOM_SELECTORS","customSelectors","DEFAULT_SELECTORS","mutations","added","j","stopObserver","autoAnnounce","options","startObserver","startObserver"]}
|