@a11y_craft/auto-announce 1.0.0
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/dist/index.d.mts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +51 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
type Politeness = 'polite' | 'assertive';
|
|
2
|
+
interface AutoAnnounceOptions {
|
|
3
|
+
/** Additional CSS selectors to watch. Merged with defaults. */
|
|
4
|
+
selectors?: string[];
|
|
5
|
+
/** Elements matching these selectors will never be announced. */
|
|
6
|
+
ignore?: string[];
|
|
7
|
+
/** Override politeness for all announcements. */
|
|
8
|
+
politeness?: Politeness;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare function stopObserver(): void;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Manually configure and start auto-announce with custom options.
|
|
15
|
+
* Only needed if you want to override defaults.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* import { autoAnnounce } from '@a11y_craft/auto-announce';
|
|
19
|
+
* autoAnnounce({ selectors: ['.my-toast'], ignore: ['.silent'] });
|
|
20
|
+
*/
|
|
21
|
+
declare function autoAnnounce(options?: AutoAnnounceOptions): void;
|
|
22
|
+
|
|
23
|
+
export { type AutoAnnounceOptions, autoAnnounce, stopObserver as stopAutoAnnounce };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
type Politeness = 'polite' | 'assertive';
|
|
2
|
+
interface AutoAnnounceOptions {
|
|
3
|
+
/** Additional CSS selectors to watch. Merged with defaults. */
|
|
4
|
+
selectors?: string[];
|
|
5
|
+
/** Elements matching these selectors will never be announced. */
|
|
6
|
+
ignore?: string[];
|
|
7
|
+
/** Override politeness for all announcements. */
|
|
8
|
+
politeness?: Politeness;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare function stopObserver(): void;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Manually configure and start auto-announce with custom options.
|
|
15
|
+
* Only needed if you want to override defaults.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* import { autoAnnounce } from '@a11y_craft/auto-announce';
|
|
19
|
+
* autoAnnounce({ selectors: ['.my-toast'], ignore: ['.silent'] });
|
|
20
|
+
*/
|
|
21
|
+
declare function autoAnnounce(options?: AutoAnnounceOptions): void;
|
|
22
|
+
|
|
23
|
+
export { type AutoAnnounceOptions, autoAnnounce, stopObserver as stopAutoAnnounce };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var d=Object.defineProperty;var w=Object.getOwnPropertyDescriptor;var O=Object.getOwnPropertyNames;var P=Object.prototype.hasOwnProperty;var C=(t,e)=>{for(var n in e)d(t,n,{get:e[n],enumerable:!0})},D=(t,e,n,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of O(e))!P.call(t,r)&&r!==n&&d(t,r,{get:()=>e[r],enumerable:!(o=w(e,r))||o.enumerable});return t};var R=t=>D(d({},"__esModule",{value:!0}),t);var B={};C(B,{autoAnnounce:()=>U,stopAutoAnnounce:()=>T});module.exports=R(B);var H=500,b=1e4,f=null,m=null,a=new Map;function E(t){var o;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",((o=document.body)!=null?o:document.documentElement).appendChild(e),e}function _(){f||(f=E("polite")),m||(m=E("assertive"))}function N(t){let e=Date.now(),n=a.get(t);if(n!==void 0&&e-n<H)return!0;if(a.size>=200){let o=a.keys().next().value;o!==void 0&&a.delete(o)}return a.set(t,e),!1}function y(t,e="polite"){if(typeof document=="undefined"||!t||typeof t!="string")return;let n=t.length>b?t.slice(0,b):t;if(N(n))return;_();let o=e==="assertive"?m:f;o.textContent="",setTimeout(()=>{o.textContent=n},0)}var g=['[role="alert"]','[role="status"]','[role="log"]',".toast",".toast-message",".notification",".banner",".alert",".snackbar",".flash",".flash-message","[data-announce]","[data-notification]","[data-toast]"];function v(t){return t.getAttribute("role")==="alert"?"assertive":"polite"}function A(t){if(!(t instanceof HTMLElement))return!1;let e=window.getComputedStyle(t);return e.display!=="none"&&e.visibility!=="hidden"&&e.opacity!=="0"}function x(t){return t.hasAttribute("aria-live")||!!t.closest("[aria-live]")}function h(t){var o,r,i,s;let e=t.getAttribute("aria-label");if(e)return e.trim();let n=t.getAttribute("aria-labelledby");if(n){let u=document.getElementById(n);if(u)return(r=(o=u.textContent)==null?void 0:o.trim())!=null?r:""}return(s=(i=t.textContent)==null?void 0:i.trim())!=null?s:""}function p(t,e){return e.some(n=>{try{return t.matches(n)}catch(o){return!1}})}var l=null;function L(t,e,n,o){if(x(t)||n.length>0&&p(t,n)||!p(t,e)||!A(t))return;let r=h(t);if(!r)return;let i=o!=null?o:v(t);y(r,i)}function k(t,e,n,o){if(t.nodeType!==Node.ELEMENT_NODE)return;let r=t;L(r,e,n,o),r.querySelectorAll(e.join(",")).forEach(s=>L(s,e,n,o))}function c(t={}){var r,i,s;if(typeof document=="undefined"||l)return;let e=[...g,...(r=t.selectors)!=null?r:[]],n=(i=t.ignore)!=null?i:[],o=t.politeness;l=new MutationObserver(u=>{for(let M of u)M.addedNodes.forEach(S=>k(S,e,n,o))}),l.observe((s=document.body)!=null?s:document.documentElement,{childList:!0,subtree:!0})}function T(){l&&(l.disconnect(),l=null)}function U(t={}){c(t)}typeof document!="undefined"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>c()):c());0&&(module.exports={autoAnnounce,stopAutoAnnounce});
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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":"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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","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"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var M=500,p=1e4,d=null,f=null,a=new Map;function b(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 S(){d||(d=b("polite")),f||(f=b("assertive"))}function w(t){let e=Date.now(),o=a.get(t);if(o!==void 0&&e-o<M)return!0;if(a.size>=200){let n=a.keys().next().value;n!==void 0&&a.delete(n)}return a.set(t,e),!1}function E(t,e="polite"){if(typeof document=="undefined"||!t||typeof t!="string")return;let o=t.length>p?t.slice(0,p):t;if(w(o))return;S();let n=e==="assertive"?f:d;n.textContent="",setTimeout(()=>{n.textContent=o},0)}var y=['[role="alert"]','[role="status"]','[role="log"]',".toast",".toast-message",".notification",".banner",".alert",".snackbar",".flash",".flash-message","[data-announce]","[data-notification]","[data-toast]"];function g(t){return t.getAttribute("role")==="alert"?"assertive":"polite"}function v(t){if(!(t instanceof HTMLElement))return!1;let e=window.getComputedStyle(t);return e.display!=="none"&&e.visibility!=="hidden"&&e.opacity!=="0"}function A(t){return t.hasAttribute("aria-live")||!!t.closest("[aria-live]")}function x(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 u=document.getElementById(o);if(u)return(r=(n=u.textContent)==null?void 0:n.trim())!=null?r:""}return(s=(i=t.textContent)==null?void 0:i.trim())!=null?s:""}function m(t,e){return e.some(o=>{try{return t.matches(o)}catch(n){return!1}})}var l=null;function h(t,e,o,n){if(A(t)||o.length>0&&m(t,o)||!m(t,e)||!v(t))return;let r=x(t);if(!r)return;let i=n!=null?n:g(t);E(r,i)}function O(t,e,o,n){if(t.nodeType!==Node.ELEMENT_NODE)return;let r=t;h(r,e,o,n),r.querySelectorAll(e.join(",")).forEach(s=>h(s,e,o,n))}function c(t={}){var r,i,s;if(typeof document=="undefined"||l)return;let e=[...y,...(r=t.selectors)!=null?r:[]],o=(i=t.ignore)!=null?i:[],n=t.politeness;l=new MutationObserver(u=>{for(let L of u)L.addedNodes.forEach(T=>O(T,e,o,n))}),l.observe((s=document.body)!=null?s:document.documentElement,{childList:!0,subtree:!0})}function P(){l&&(l.disconnect(),l=null)}function k(t={}){c(t)}typeof document!="undefined"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>c()):c());export{k as autoAnnounce,P as stopAutoAnnounce};
|
|
2
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +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"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@a11y_craft/auto-announce",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zero-config screen reader announcements — import once, works everywhere automatically",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"accessibility",
|
|
7
|
+
"a11y",
|
|
8
|
+
"aria",
|
|
9
|
+
"aria-live",
|
|
10
|
+
"screen-reader",
|
|
11
|
+
"wcag",
|
|
12
|
+
"voiceover",
|
|
13
|
+
"nvda",
|
|
14
|
+
"announcer",
|
|
15
|
+
"auto"
|
|
16
|
+
],
|
|
17
|
+
"author": "Nidhi Gajera",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/nidhiG2610/auto-announce.git"
|
|
22
|
+
},
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"module": "./dist/index.mjs",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.mjs",
|
|
30
|
+
"require": "./dist/index.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist"
|
|
35
|
+
],
|
|
36
|
+
"sideEffects": true,
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsup",
|
|
39
|
+
"test": "jest",
|
|
40
|
+
"test:watch": "jest --watch",
|
|
41
|
+
"prepublishOnly": "npm run build && npm test"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/jest": "^29.5.12",
|
|
45
|
+
"jest": "^29.7.0",
|
|
46
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
47
|
+
"ts-jest": "^29.1.4",
|
|
48
|
+
"tsup": "^8.0.2",
|
|
49
|
+
"typescript": "^5.4.5"
|
|
50
|
+
}
|
|
51
|
+
}
|