Skip to content

Commit cd5d10c

Browse files
fix: LiveRegion text extraction (#3417)
Co-authored-by: Boris Serdiuk <[email protected]>
1 parent 6b44f71 commit cd5d10c

File tree

3 files changed

+54
-4
lines changed

3 files changed

+54
-4
lines changed

src/live-region/__integ__/live-region.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ describe('Live region', () => {
2626
setupTest(async page => {
2727
// Wait for live region to debounce after page load
2828
await new Promise(resolve => setTimeout(resolve, 2000));
29-
await expect(page.getInnerHTML('[aria-live]')).resolves.toBe('&lt;p&gt;Testing&lt;/p&gt;Testing');
29+
await expect(page.getInnerHTML('[aria-live]')).resolves.toBe('&lt;p&gt;Testing&lt;/p&gt; Testing');
3030
})
3131
);
3232
});

src/live-region/__tests__/live-region.test.tsx

+35-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
import React, { createRef } from 'react';
44
import { render, waitFor } from '@testing-library/react';
55

6-
import InternalLiveRegion, { InternalLiveRegionRef } from '../../../lib/components/live-region/internal.js';
6+
import InternalLiveRegion, {
7+
extractTextContent,
8+
InternalLiveRegionRef,
9+
} from '../../../lib/components/live-region/internal.js';
710

811
import styles from '../../../lib/components/live-region/test-classes/styles.css.js';
912

@@ -128,3 +131,34 @@ describe('LiveRegion', () => {
128131
expect(politeRegion).toHaveTextContent('Announcement');
129132
});
130133
});
134+
135+
describe('text extractor', () => {
136+
it('extracts text from an empty element', () => {
137+
const el = document.createElement('div');
138+
expect(extractTextContent(el)).toBe('');
139+
});
140+
141+
it('extracts text from an empty element with a comment', () => {
142+
const el = document.createElement('div');
143+
el.innerHTML = '<!-- comment -->';
144+
expect(extractTextContent(el)).toBe('');
145+
});
146+
147+
it('extracts text from a single element', () => {
148+
const el = document.createElement('div');
149+
el.textContent = 'Hello';
150+
expect(extractTextContent(el)).toBe('Hello');
151+
});
152+
153+
it('extracts text from nested elements', () => {
154+
const el = document.createElement('article');
155+
el.innerHTML = `
156+
<h1>Hello</h1>
157+
<p>World</p>
158+
<span>inline</span>
159+
<span>content</span>
160+
<span></span>
161+
`;
162+
expect(extractTextContent(el)).toBe('Hello World inline content');
163+
});
164+
});

src/live-region/internal.tsx

+18-2
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,28 @@ export default React.forwardRef(function InternalLiveRegion(
116116
);
117117
});
118118

119-
function extractTextContent(node: HTMLElement): string {
119+
const processNode = (childNode: Node): string => {
120+
if (childNode.nodeType === Node.TEXT_NODE) {
121+
return childNode.textContent || '';
122+
}
123+
124+
if (childNode.nodeType === Node.ELEMENT_NODE) {
125+
return extractTextContent(childNode as HTMLElement);
126+
}
127+
128+
return '';
129+
};
130+
131+
export function extractTextContent(node: HTMLElement): string {
120132
// We use the text content of the node as the announcement text.
121133
// This only extracts text content from the node including all its children which is enough for now.
122134
// To make it more powerful, it is possible to create a more sophisticated extractor with respect to
123135
// ARIA properties to ignore aria-hidden nodes and read ARIA labels from the live content.
124-
return (node.textContent || '').replace(/\s+/g, ' ').trim();
136+
if (!node || !node?.childNodes?.length) {
137+
return '';
138+
}
139+
140+
return Array.from(node.childNodes, processNode).join(' ').replace(/\s+/g, ' ').trim();
125141
}
126142

127143
function getSourceContent(source: ReadonlyArray<string | React.RefObject<HTMLElement> | undefined>): string {

0 commit comments

Comments
 (0)