{"version":3,"file":"useScreenReaderAnnounce.mjs","sources":["../../../src/lib/hooks/useScreenReaderAnnounce.ts"],"sourcesContent":["/**\n * @file Screen Reader Announcement Hook\n * @description Hook for announcing dynamic content changes to screen readers\n *\n * WCAG 2.1 Reference: Success Criterion 4.1.3 Status Messages (Level AA)\n * Uses ARIA live regions to announce content changes without moving focus.\n */\n\nimport { useCallback, useEffect, useRef } from 'react';\n\n/**\n * Announcement priority levels\n * - polite: Waits for user to finish current task before announcing\n * - assertive: Interrupts current speech immediately (use sparingly)\n */\nexport type AnnouncementPriority = 'polite' | 'assertive';\n\n/**\n * Options for announcements\n */\nexport interface AnnounceOptions {\n  /** Priority level - 'polite' (default) or 'assertive' */\n  priority?: AnnouncementPriority;\n  /** Clear the announcement after this many milliseconds */\n  clearAfter?: number;\n}\n\n/**\n * Create and manage a live region element\n */\nfunction createLiveRegion(priority: AnnouncementPriority): HTMLDivElement {\n  const region = document.createElement('div');\n  region.setAttribute('role', 'status');\n  region.setAttribute('aria-live', priority);\n  region.setAttribute('aria-atomic', 'true');\n  region.className = `live-region-${priority}`;\n\n  // Visually hidden but accessible to screen readers\n  Object.assign(region.style, {\n    position: 'absolute',\n    width: '1px',\n    height: '1px',\n    padding: '0',\n    margin: '-1px',\n    overflow: 'hidden',\n    clip: 'rect(0, 0, 0, 0)',\n    whiteSpace: 'nowrap',\n    border: '0',\n  });\n\n  return region;\n}\n\n/**\n * Get or create a shared live region\n */\nconst liveRegions: Record<AnnouncementPriority, HTMLDivElement | null> = {\n  polite: null,\n  assertive: null,\n};\n\nfunction getLiveRegion(priority: AnnouncementPriority): HTMLDivElement {\n  if (!liveRegions[priority]) {\n    liveRegions[priority] = createLiveRegion(priority);\n    document.body.appendChild(liveRegions[priority]);\n  }\n  return liveRegions[priority];\n}\n\n/**\n * Hook for announcing dynamic content to screen readers\n *\n * @example\n * ```tsx\n * function SearchResults({ results }) {\n *   const announce = useScreenReaderAnnounce();\n *\n *   useEffect(() => {\n *     announce(`Found ${results.length} results`);\n *   }, [results.length, announce]);\n *\n *   return <div>...</div>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Assertive announcement for errors\n * function ErrorBanner({ error }) {\n *   const announce = useScreenReaderAnnounce();\n *\n *   useEffect(() => {\n *     if (error) {\n *       announce(`Error: ${error.message}`, { priority: 'assertive' });\n *     }\n *   }, [error, announce]);\n * }\n * ```\n */\nexport function useScreenReaderAnnounce(): (message: string, options?: AnnounceOptions) => void {\n  const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  // Clear any pending timeouts on unmount\n  useEffect(() => {\n    return () => {\n      if (timeoutRef.current) {\n        clearTimeout(timeoutRef.current);\n      }\n    };\n  }, []);\n\n  return useCallback((message: string, options: AnnounceOptions = {}) => {\n    const { priority = 'polite', clearAfter = 1000 } = options;\n    const region = getLiveRegion(priority);\n\n    // Clear previous timeout\n    if (timeoutRef.current) {\n      clearTimeout(timeoutRef.current);\n    }\n\n    // Clear and then set the message (forces re-announcement)\n    region.textContent = '';\n\n    // Small delay to ensure the clear is processed\n    requestAnimationFrame(() => {\n      region.textContent = message;\n    });\n\n    // Optionally clear the message after a delay\n    if (clearAfter > 0) {\n      timeoutRef.current = setTimeout(() => {\n        region.textContent = '';\n      }, clearAfter);\n    }\n  }, []);\n}\n\n/**\n * Screen Reader Announcement Provider Component\n *\n * Add this to your app root if you prefer a component-based approach.\n *\n * @example\n * ```tsx\n * <ScreenReaderAnnouncementRegion />\n * ```\n */\nexport function ScreenReaderAnnouncementRegion(): null {\n  useEffect(() => {\n    // Initialize both live regions on mount\n    getLiveRegion('polite');\n    getLiveRegion('assertive');\n  }, []);\n\n  return null;\n}\n\n/**\n * Announce a message imperatively (not as a hook)\n * Useful for non-component code or callbacks\n *\n * @example\n * ```ts\n * // In an event handler\n * function handleSave() {\n *   saveData().then(() => {\n *     announceToScreenReader('Changes saved successfully');\n *   });\n * }\n * ```\n */\nexport function announceToScreenReader(message: string, options: AnnounceOptions = {}): void {\n  const { priority = 'polite', clearAfter = 1000 } = options;\n  const region = getLiveRegion(priority);\n\n  // Clear and then set the message\n  region.textContent = '';\n  requestAnimationFrame(() => {\n    region.textContent = message;\n  });\n\n  if (clearAfter > 0) {\n    setTimeout(() => {\n      region.textContent = '';\n    }, clearAfter);\n  }\n}\n"],"names":["createLiveRegion","priority","region","liveRegions","getLiveRegion","useScreenReaderAnnounce","timeoutRef","useRef","useEffect","useCallback","message","options","clearAfter","ScreenReaderAnnouncementRegion","announceToScreenReader"],"mappings":";AA8BA,SAASA,EAAiBC,GAAgD;AACxE,QAAMC,IAAS,SAAS,cAAc,KAAK;AAC3C,SAAAA,EAAO,aAAa,QAAQ,QAAQ,GACpCA,EAAO,aAAa,aAAaD,CAAQ,GACzCC,EAAO,aAAa,eAAe,MAAM,GACzCA,EAAO,YAAY,eAAeD,CAAQ,IAG1C,OAAO,OAAOC,EAAO,OAAO;AAAA,IAC1B,UAAU;AAAA,IACV,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,MAAM;AAAA,IACN,YAAY;AAAA,IACZ,QAAQ;AAAA,EAAA,CACT,GAEMA;AACT;AAKA,MAAMC,IAAmE;AAAA,EACvE,QAAQ;AAAA,EACR,WAAW;AACb;AAEA,SAASC,EAAcH,GAAgD;AACrE,SAAKE,EAAYF,CAAQ,MACvBE,EAAYF,CAAQ,IAAID,EAAiBC,CAAQ,GACjD,SAAS,KAAK,YAAYE,EAAYF,CAAQ,CAAC,IAE1CE,EAAYF,CAAQ;AAC7B;AAgCO,SAASI,IAAgF;AAC9F,QAAMC,IAAaC,EAA6C,IAAI;AAGpE,SAAAC,EAAU,MACD,MAAM;AACX,IAAIF,EAAW,WACb,aAAaA,EAAW,OAAO;AAAA,EAEnC,GACC,CAAA,CAAE,GAEEG,EAAY,CAACC,GAAiBC,IAA2B,CAAA,MAAO;AACrE,UAAM,EAAE,UAAAV,IAAW,UAAU,YAAAW,IAAa,QAASD,GAC7CT,IAASE,EAAcH,CAAQ;AAGrC,IAAIK,EAAW,WACb,aAAaA,EAAW,OAAO,GAIjCJ,EAAO,cAAc,IAGrB,sBAAsB,MAAM;AAC1B,MAAAA,EAAO,cAAcQ;AAAA,IACvB,CAAC,GAGGE,IAAa,MACfN,EAAW,UAAU,WAAW,MAAM;AACpC,MAAAJ,EAAO,cAAc;AAAA,IACvB,GAAGU,CAAU;AAAA,EAEjB,GAAG,CAAA,CAAE;AACP;AAYO,SAASC,IAAuC;AACrD,SAAAL,EAAU,MAAM;AAEd,IAAAJ,EAAc,QAAQ,GACtBA,EAAc,WAAW;AAAA,EAC3B,GAAG,CAAA,CAAE,GAEE;AACT;AAgBO,SAASU,EAAuBJ,GAAiBC,IAA2B,IAAU;AAC3F,QAAM,EAAE,UAAAV,IAAW,UAAU,YAAAW,IAAa,QAASD,GAC7CT,IAASE,EAAcH,CAAQ;AAGrC,EAAAC,EAAO,cAAc,IACrB,sBAAsB,MAAM;AAC1B,IAAAA,EAAO,cAAcQ;AAAA,EACvB,CAAC,GAEGE,IAAa,KACf,WAAW,MAAM;AACf,IAAAV,EAAO,cAAc;AAAA,EACvB,GAAGU,CAAU;AAEjB;"}