Skip to content

Commit 2c32ce7

Browse files
fix: accessible name calculation (#1910)
1 parent 7ce8b09 commit 2c32ce7

3 files changed

Lines changed: 83 additions & 4 deletions

File tree

src/helpers/__tests__/accessibility.test.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,37 @@ describe('computeAccessibleName', () => {
864864
);
865865
});
866866

867+
test('concatenates inline text children without extra spaces', async () => {
868+
const name = 'World';
869+
870+
await render(<Text testID="subject">Hello {name}!</Text>);
871+
872+
expect(computeAccessibleName(screen.getByTestId('subject'))).toBe('Hello World!');
873+
});
874+
875+
test('concatenates nested inline Text children without extra spaces', async () => {
876+
const name = 'World';
877+
878+
await render(
879+
<Text testID="subject">
880+
Hello <Text>{name}</Text>!
881+
</Text>,
882+
);
883+
884+
expect(computeAccessibleName(screen.getByTestId('subject'))).toBe('Hello World!');
885+
});
886+
887+
test('separates non-text accessible names inside Text from adjacent inline text', async () => {
888+
await render(
889+
<Text testID="subject">
890+
<View aria-label="icon" />
891+
<Text>label</Text>
892+
</Text>,
893+
);
894+
895+
expect(computeAccessibleName(screen.getByTestId('subject'))).toBe('icon label');
896+
});
897+
867898
test('TextInput placeholder is used only for the element itself', async () => {
868899
await render(
869900
<>

src/helpers/accessibility.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,11 @@ type ComputeAccessibleNameOptions = {
268268
root?: boolean;
269269
};
270270

271+
type AccessibleNamePart = {
272+
text: string;
273+
isInlineText: boolean;
274+
};
275+
271276
export function computeAccessibleName(
272277
instance: TestInstance,
273278
options?: ComputeAccessibleNameOptions,
@@ -281,21 +286,36 @@ export function computeAccessibleName(
281286
return instance.props.placeholder;
282287
}
283288

284-
const parts = [];
289+
const parts: AccessibleNamePart[] = [];
285290
for (const child of instance.children) {
286291
if (typeof child === 'string') {
287292
if (child) {
288-
parts.push(child);
293+
parts.push({ text: child, isInlineText: true });
289294
}
290295
} else {
291296
const childLabel = computeAccessibleName(child, { root: false });
292297
if (childLabel) {
293-
parts.push(childLabel);
298+
parts.push({ text: childLabel, isInlineText: isHostText(child) });
294299
}
295300
}
296301
}
297302

298-
return parts.join(' ');
303+
return joinAccessibleNameParts(parts, { inline: isHostText(instance) });
304+
}
305+
306+
function joinAccessibleNameParts(
307+
parts: AccessibleNamePart[],
308+
options: { inline: boolean },
309+
): string {
310+
return parts.reduce((accessibleName, part, index) => {
311+
if (index === 0) {
312+
return part.text;
313+
}
314+
315+
const previousPart = parts[index - 1];
316+
const separator = options.inline && previousPart.isInlineText && part.isInlineText ? '' : ' ';
317+
return `${accessibleName}${separator}${part.text}`;
318+
}, '');
299319
}
300320

301321
type RoleSupportMap = Partial<Record<Role | AccessibilityRole, true>>;

src/queries/__tests__/role.test.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,34 @@ describe('supports name option', () => {
207207
expect(screen.getByRole('header', { name: 'About' }).props.testID).toBe('target-header');
208208
});
209209

210+
test('returns an element when inline text children form the name', async () => {
211+
const name = 'World';
212+
213+
await render(
214+
<Text accessibilityRole="header" testID="target-header">
215+
Hello {name}!
216+
</Text>,
217+
);
218+
219+
expect(screen.getByRole('header', { name: 'Hello World!' })).toBe(
220+
screen.getByTestId('target-header'),
221+
);
222+
});
223+
224+
test('returns an element when nested inline Text children form the name', async () => {
225+
const name = 'World';
226+
227+
await render(
228+
<Text accessibilityRole="header" testID="target-header">
229+
Hello <Text>{name}</Text>!
230+
</Text>,
231+
);
232+
233+
expect(screen.getByRole('header', { name: 'Hello World!' })).toBe(
234+
screen.getByTestId('target-header'),
235+
);
236+
});
237+
210238
test('returns an element with nested Text as children', async () => {
211239
await render(
212240
<Text accessibilityRole="header" testID="parent">

0 commit comments

Comments
 (0)