Skip to content
Source
Sign inSign up

useLiveRegion.ts

46 lines1.5 KB

1import { useState } from "react";
2​
3// NVDA reads a polite region's leftover text again when focus returns from a
4// dialog, so an announcement is cleared once it has had time to be spoken.
5// Clearing doesn't cut off speech that already started.
6const CLEAR_AFTER_MS = 7_000;
7​
8/**
9 * A polite live region written straight to the DOM, so speaking never
10 * re-renders. `attach` is the region element's ref callback; writes while no
11 * region is attached are dropped.
12 */
13export function createLiveRegion() {
14 let region: HTMLElement | null = null;
15 let clearTimer: ReturnType<typeof setTimeout> | null = null;
16​
17 const cancelClear = () => {
18 if (clearTimer !== null) clearTimeout(clearTimer);
19 clearTimer = null;
20 };
21​
22 const clear = () => {
23 cancelClear();
24 if (region !== null) region.textContent = "";
25 };
26​
27 const announce = (text: string) => {
28 if (region === null) return;
29 cancelClear();
30 // Screen readers ignore a write that leaves the text unchanged, so a repeat
31 // gets a trailing non-breaking space to count as new.
32 region.textContent = region.textContent === text ? `${text}\u00A0` : text;
33 clearTimer = setTimeout(clear, CLEAR_AFTER_MS);
34 };
35​
36 const attach = (node: HTMLElement | null) => {
37 region = node;
38 };
39​
40 return { attach, announce, clear };
42​
43export function useLiveRegion() {
44 const [liveRegion] = useState(createLiveRegion);
45 return { regionRef: liveRegion.attach, announce: liveRegion.announce, clear: liveRegion.clear };