/**
* useLiveRegion composable
*
* Manages an ARIA live region for screen reader announcements.
* Creates/reuses a single visually-hidden
element
* in the DOM, and announces content when messages arrive.
*
* This matches the React reference implementation's approach to accessibility
* announcements for message components.
*/
import { onMounted, onUnmounted } from 'vue'
const LIVE_REGION_ID = 'webchat-live-region'
/**
* Get or create the shared live region element in the DOM
*/
function getOrCreateLiveRegion(): HTMLDivElement {
let region = document.getElementById(LIVE_REGION_ID) as HTMLDivElement | null
if (!region) {
region = document.createElement('div')
region.id = LIVE_REGION_ID
region.setAttribute('role', 'status')
region.setAttribute('aria-live', 'polite')
region.setAttribute('aria-atomic', 'true')
// Visually hidden but accessible to screen readers
Object.assign(region.style, {
position: 'absolute',
width: '1px',
height: '1px',
padding: '0',
margin: '-1px',
overflow: 'hidden',
clip: 'rect(0, 0, 0, 0)',
whiteSpace: 'nowrap',
borderWidth: '0',
})
document.body.appendChild(region)
}
return region
}
/**
* Track reference count so we only clean up when no components are using the region
*/
let refCount = 0
interface UseLiveRegionOptions {
/** Text to announce to screen readers */
announcement: string
/** Whether the announcement should be made (defaults to true) */
enabled?: boolean
}
/**
* Composable that announces content to screen readers via an ARIA live region.
*
* @example
* ```typescript
* // In a message component:
* useLiveRegion({
* announcement: `New message: ${messageText}`,
* enabled: !props.ignoreLiveRegion,
* })
* ```
*/
export function useLiveRegion(options: UseLiveRegionOptions): void {
onMounted(() => {
refCount++
if (options.enabled === false) return
if (!options.announcement) return
const region = getOrCreateLiveRegion()
// Clear then set to trigger screen reader re-announcement
region.textContent = ''
// Use requestAnimationFrame to ensure the empty text is processed first
requestAnimationFrame(() => {
region.textContent = options.announcement
})
})
onUnmounted(() => {
refCount--
// Clean up the live region when no components are using it
if (refCount <= 0) {
refCount = 0
const region = document.getElementById(LIVE_REGION_ID)
if (region) {
region.textContent = ''
}
}
})
}