@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
JavaScript
'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;
}