fix: accessible name calculation (#1910)

This commit is contained in:
Maciej Jastrzebski
2026-05-28 17:41:06 +02:00
committed by GitHub
parent 7ce8b09123
commit 2c32ce75df
3 changed files with 83 additions and 4 deletions
@@ -864,6 +864,37 @@ describe('computeAccessibleName', () => {
);
});
test('concatenates inline text children without extra spaces', async () => {
const name = 'World';
await render(<Text testID="subject">Hello {name}!</Text>);
expect(computeAccessibleName(screen.getByTestId('subject'))).toBe('Hello World!');
});
test('concatenates nested inline Text children without extra spaces', async () => {
const name = 'World';
await render(
<Text testID="subject">
Hello <Text>{name}</Text>!
</Text>,
);
expect(computeAccessibleName(screen.getByTestId('subject'))).toBe('Hello World!');
});
test('separates non-text accessible names inside Text from adjacent inline text', async () => {
await render(
<Text testID="subject">
<View aria-label="icon" />
<Text>label</Text>
</Text>,
);
expect(computeAccessibleName(screen.getByTestId('subject'))).toBe('icon label');
});
test('TextInput placeholder is used only for the element itself', async () => {
await render(
<>
+24 -4
View File
@@ -268,6 +268,11 @@ type ComputeAccessibleNameOptions = {
root?: boolean;
};
type AccessibleNamePart = {
text: string;
isInlineText: boolean;
};
export function computeAccessibleName(
instance: TestInstance,
options?: ComputeAccessibleNameOptions,
@@ -281,21 +286,36 @@ export function computeAccessibleName(
return instance.props.placeholder;
}
const parts = [];
const parts: AccessibleNamePart[] = [];
for (const child of instance.children) {
if (typeof child === 'string') {
if (child) {
parts.push(child);
parts.push({ text: child, isInlineText: true });
}
} else {
const childLabel = computeAccessibleName(child, { root: false });
if (childLabel) {
parts.push(childLabel);
parts.push({ text: childLabel, isInlineText: isHostText(child) });
}
}
}
return parts.join(' ');
return joinAccessibleNameParts(parts, { inline: isHostText(instance) });
}
function joinAccessibleNameParts(
parts: AccessibleNamePart[],
options: { inline: boolean },
): string {
return parts.reduce((accessibleName, part, index) => {
if (index === 0) {
return part.text;
}
const previousPart = parts[index - 1];
const separator = options.inline && previousPart.isInlineText && part.isInlineText ? '' : ' ';
return `${accessibleName}${separator}${part.text}`;
}, '');
}
type RoleSupportMap = Partial<Record<Role | AccessibilityRole, true>>;
+28
View File
@@ -207,6 +207,34 @@ describe('supports name option', () => {
expect(screen.getByRole('header', { name: 'About' }).props.testID).toBe('target-header');
});
test('returns an element when inline text children form the name', async () => {
const name = 'World';
await render(
<Text accessibilityRole="header" testID="target-header">
Hello {name}!
</Text>,
);
expect(screen.getByRole('header', { name: 'Hello World!' })).toBe(
screen.getByTestId('target-header'),
);
});
test('returns an element when nested inline Text children form the name', async () => {
const name = 'World';
await render(
<Text accessibilityRole="header" testID="target-header">
Hello <Text>{name}</Text>!
</Text>,
);
expect(screen.getByRole('header', { name: 'Hello World!' })).toBe(
screen.getByTestId('target-header'),
);
});
test('returns an element with nested Text as children', async () => {
await render(
<Text accessibilityRole="header" testID="parent">