{"version":3,"file":"announcer.cjs","names":[],"sources":["../../src/core/announcer.ts"],"sourcesContent":["// ── Live-Region Announcer ─────────────────────────────────────────────────────\n// Programmatic announcements to screen readers without requiring a visible\n// DOM element. Used for status messages, selection feedback, and count updates\n// that live in components where the visible element is hidden or absent.\n//\n// Two visually-hidden live regions are lazily created in `<body>` — one polite,\n// one assertive. The clear-then-set pattern forces AT to re-read even identical\n// consecutive messages.\n//\n// Usage:\n// ```ts\n// import { announce } from './announcer';\n// announce(`${selectedCount} items selected`);\n// announce('Error: field is required', { politeness: 'assertive' });\n// ```\n\nimport { SR_ONLY_INLINE_STYLE } from '../styles';\n\n// ── Visually-hidden style ─────────────────────────────────────────────────────\n// Shared with the CSS-side `.sr-only` mixin — see `src/styles/mixins/accessibility.css.ts`.\n\n// ── Per-document live regions (WeakMap for automatic GC + test isolation) ────\n\ntype DocumentRegions = { assertive?: HTMLElement; polite?: HTMLElement };\n\nconst _regions = new WeakMap<Document, DocumentRegions>();\nconst _timers = new WeakMap<HTMLElement, ReturnType<typeof setTimeout>>();\n\n/** Creates and mounts a visually-hidden live region with the given politeness. */\nconst createRegion = (politeness: 'assertive' | 'polite', doc: Document): HTMLElement => {\n  const el = doc.createElement('div');\n\n  el.setAttribute('aria-live', politeness);\n  el.setAttribute('aria-atomic', 'true');\n  el.setAttribute('data-block-announcer', politeness);\n  el.style.cssText = SR_ONLY_INLINE_STYLE;\n\n  doc.body.appendChild(el);\n\n  return el;\n};\n\n/**\n * Returns the live region for the requested politeness in the given document,\n * creating it on first access. Returns `null` outside a browser context.\n */\nconst getRegion = (politeness: 'assertive' | 'polite', doc: Document): HTMLElement | null => {\n  let regions = _regions.get(doc);\n\n  // Re-create if this specific region was removed from the DOM (e.g. by test teardown).\n  if (regions?.[politeness] && !doc.body.contains(regions[politeness])) {\n    regions[politeness] = undefined;\n  }\n\n  if (!regions?.[politeness]) {\n    if (!regions) {\n      regions = {};\n      _regions.set(doc, regions);\n    }\n\n    regions[politeness] = createRegion(politeness, doc);\n  }\n\n  return regions[politeness] ?? null;\n};\n\n// ── Public API ────────────────────────────────────────────────────────────────\n\nexport type AnnouncePoliteness = 'assertive' | 'polite';\n\nexport type AnnounceOptions = {\n  /**\n   * `'polite'` — queues the announcement; does not interrupt ongoing speech.\n   * `'assertive'` — interrupts immediately. Use sparingly; reserve for errors.\n   *\n   * Default: `'polite'`.\n   */\n  politeness?: AnnouncePoliteness;\n};\n\n/**\n * Announces a message to screen readers via a singleton visually-hidden\n * live region. Safe to call outside a component lifecycle (no cleanup needed).\n *\n * The clear-then-set pattern (50 ms delay) forces AT to re-read even when the\n * message text hasn't changed.\n *\n * @example\n * ```ts\n * announce('3 results found');\n * announce('Session expired', { politeness: 'assertive' });\n * ```\n */\nexport const announce = (message: string, options: AnnounceOptions = {}): void => {\n  if (typeof document === 'undefined') return;\n\n  const politeness = options.politeness ?? 'polite';\n  const region = getRegion(politeness, document);\n\n  if (!region) return;\n\n  // Clear immediately so AT registers a content change, then set after a short\n  // delay. The 50 ms window is the established minimum for most AT/browser combos.\n  region.textContent = '';\n\n  // Use a per-element timer so concurrent polite + assertive announcements don't\n  // clobber each other's pending writes.\n  const existing = _timers.get(region);\n\n  if (existing !== undefined) clearTimeout(existing);\n\n  const timer = setTimeout(() => {\n    region.textContent = message;\n    _timers.delete(region);\n  }, 50);\n\n  _timers.set(region, timer);\n};\n"],"mappings":"0DAyBA,IAAM,EAAW,IAAI,QACf,EAAU,IAAI,QAGd,GAAgB,EAAoC,IAA+B,CACvF,IAAM,EAAK,EAAI,cAAc,KAAK,EASlC,OAPA,EAAG,aAAa,YAAa,CAAU,EACvC,EAAG,aAAa,cAAe,MAAM,EACrC,EAAG,aAAa,uBAAwB,CAAU,EAClD,EAAG,MAAM,QAAU,EAAA,qBAEnB,EAAI,KAAK,YAAY,CAAE,EAEhB,CACT,EAMM,GAAa,EAAoC,IAAsC,CAC3F,IAAI,EAAU,EAAS,IAAI,CAAG,EAgB9B,OAbI,IAAU,IAAe,CAAC,EAAI,KAAK,SAAS,EAAQ,EAAW,IACjE,EAAQ,GAAc,IAAA,IAGnB,IAAU,KACR,IACH,EAAU,CAAC,EACX,EAAS,IAAI,EAAK,CAAO,GAG3B,EAAQ,GAAc,EAAa,EAAY,CAAG,GAG7C,EAAQ,IAAe,IAChC,EA6Ba,GAAY,EAAiB,EAA2B,CAAC,IAAY,CAChF,GAAI,OAAO,SAAa,IAAa,OAGrC,IAAM,EAAS,EADI,EAAQ,YAAc,SACJ,QAAQ,EAE7C,GAAI,CAAC,EAAQ,OAIb,EAAO,YAAc,GAIrB,IAAM,EAAW,EAAQ,IAAI,CAAM,EAE/B,IAAa,IAAA,IAAW,aAAa,CAAQ,EAEjD,IAAM,EAAQ,eAAiB,CAC7B,EAAO,YAAc,EACrB,EAAQ,OAAO,CAAM,CACvB,EAAG,EAAE,EAEL,EAAQ,IAAI,EAAQ,CAAK,CAC3B"}