UNPKG

@base-ui/react

Version:

Base UI is a library of headless ('unstyled') React components and low-level hooks. You gain complete control over your app's CSS and accessibility features.

55 lines (52 loc) 1.79 kB
'use client'; import * as React from 'react'; import { isIOS } from '@base-ui/utils/detectBrowser'; import { useTimeout } from '@base-ui/utils/useTimeout'; // Word Joiner is invisible and zero-width, so it forces a text mutation without shifting layout. const LIVE_REGION_MARKER = '\u2060'; // Safari VoiceOver needed roughly 200ms to reliably notice the initial polite live-region change. export const INITIAL_LIVE_REGION_TEXT_MUTATION_RESET_DELAY = 200; function findLastTextNode(root) { const walker = root.ownerDocument.createTreeWalker(root, NodeFilter.SHOW_TEXT); let lastTextNode = null; while (walker.nextNode()) { const textNode = walker.currentNode; if (textNode.nodeValue !== '') { lastTextNode = textNode; } } return lastTextNode; } export function useInitialLiveRegionTextMutation() { const timeout = useTimeout(); const rootRef = React.useRef(null); // Only the initial mounted announcement needs the marker; later text updates announce naturally. React.useEffect(() => { if (isIOS) { return undefined; } const root = rootRef.current; if (root == null) { return undefined; } const textNode = findLastTextNode(root); if (textNode == null) { return undefined; } const originalValue = textNode.nodeValue ?? ''; const markedValue = `${originalValue}${LIVE_REGION_MARKER}`; textNode.nodeValue = markedValue; timeout.start(INITIAL_LIVE_REGION_TEXT_MUTATION_RESET_DELAY, () => { if (textNode.nodeValue === markedValue) { textNode.nodeValue = originalValue; } }); return () => { timeout.clear(); if (textNode.nodeValue === markedValue) { textNode.nodeValue = originalValue; } }; }, [rootRef, timeout]); return rootRef; }