From 8175736abc8d468c669ee69cb427bb2114a9f3c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Thu, 12 Feb 2026 00:10:29 +0000 Subject: [PATCH 1/4] basic updates --- src/helpers/__tests__/accessiblity.test.tsx | 49 ++++++++++++++++++++- src/helpers/accessibility.ts | 27 +++++++++++- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/helpers/__tests__/accessiblity.test.tsx b/src/helpers/__tests__/accessiblity.test.tsx index 64331884d..9e9d7f97a 100644 --- a/src/helpers/__tests__/accessiblity.test.tsx +++ b/src/helpers/__tests__/accessiblity.test.tsx @@ -2,7 +2,12 @@ import React from 'react'; import { Pressable, Switch, Text, TextInput, TouchableOpacity, View } from 'react-native'; import { isHiddenFromAccessibility, isInaccessible, render, screen } from '../..'; -import { computeAriaDisabled, computeAriaLabel, isAccessibilityElement } from '../accessibility'; +import { + computeAccessibleName, + computeAriaDisabled, + computeAriaLabel, + isAccessibilityElement, +} from '../accessibility'; describe('isHiddenFromAccessibility', () => { test('returns false for accessible elements', async () => { @@ -476,3 +481,45 @@ describe('computeAriaDisabled', () => { expect(computeAriaDisabled(screen.getByText('ARIA Disabled Text'))).toBe(true); }); }); + +describe('computeAccessibleName', () => { + test('basic cases', async () => { + await render( + <> + + + + Text Content + + + , + ); + expect(computeAccessibleName(screen.getByTestId('aria-label'))).toBe('ARIA Label'); + expect(computeAccessibleName(screen.getByTestId('accessibility-label'))).toBe( + 'Accessibility Label', + ); + expect(computeAccessibleName(screen.getByTestId('text-content'))).toBe('Text Content'); + expect(computeAccessibleName(screen.getByTestId('text-input'))).toBe('Text Input'); + }); + + test('basic precedence', async () => { + await render( + <> + + Text Content + + + Text Content + + + Text Content + + , + ); + expect(computeAccessibleName(screen.getByTestId('aria-label'))).toBe('ARIA Label'); + expect(computeAccessibleName(screen.getByTestId('accessibility-label'))).toBe( + 'Accessibility Label', + ); + expect(computeAccessibleName(screen.getByTestId('text-content'))).toBe('Text Content'); + }); +}); diff --git a/src/helpers/accessibility.ts b/src/helpers/accessibility.ts index 02f5d7ee9..6c6a87198 100644 --- a/src/helpers/accessibility.ts +++ b/src/helpers/accessibility.ts @@ -6,7 +6,7 @@ import { getContainerElement, getHostSiblings, isHostElement } from './component import { findAll } from './find-all'; import { isHostImage, isHostSwitch, isHostText, isHostTextInput } from './host-component-names'; import { getTextContent } from './text-content'; -import { isEditableTextInput } from './text-input'; +import { getTextInputValue, isEditableTextInput } from './text-input'; type IsInaccessibleOptions = { cache?: WeakMap; @@ -245,7 +245,30 @@ export function computeAriaValue(element: HostElement): AccessibilityValue { } export function computeAccessibleName(element: HostElement): string | undefined { - return computeAriaLabel(element) ?? getTextContent(element); + const label = computeAriaLabel(element); + if (label) { + return label; + } + + if (isHostTextInput(element) && element.props.placeholder) { + return element.props.placeholder; + } + + const parts = []; + for (const child of element.children) { + if (typeof child === 'string') { + if (child) { + parts.push(child); + } + } else { + const childLabel = computeAccessibleName(child); + if (childLabel) { + parts.push(childLabel); + } + } + } + + return parts.join(' '); } type RoleSupportMap = Partial>; From ed48cfe209d957f49f76ef5ebe5cbd636bca772d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Thu, 12 Feb 2026 00:31:47 +0000 Subject: [PATCH 2/4] more details and tests --- src/helpers/__tests__/accessiblity.test.tsx | 69 +++++++++++++++++++++ src/helpers/accessibility.ts | 13 +++- src/queries/__tests__/role.test.tsx | 6 +- src/queries/role.ts | 8 +-- 4 files changed, 83 insertions(+), 13 deletions(-) diff --git a/src/helpers/__tests__/accessiblity.test.tsx b/src/helpers/__tests__/accessiblity.test.tsx index 9e9d7f97a..cdc2f6be0 100644 --- a/src/helpers/__tests__/accessiblity.test.tsx +++ b/src/helpers/__tests__/accessiblity.test.tsx @@ -522,4 +522,73 @@ describe('computeAccessibleName', () => { ); expect(computeAccessibleName(screen.getByTestId('text-content'))).toBe('Text Content'); }); + + test('concatenates children accessible names', async () => { + await render( + <> + + Hello + World + + + + Hello + + + World + + + + + World + + + + + Hello + + + World + + + + Ignored + + World + + + + Ignored + + World + + , + ); + expect(computeAccessibleName(screen.getByTestId('multiple-text'))).toBe('Hello World'); + expect(computeAccessibleName(screen.getByTestId('nested-views'))).toBe('Hello World'); + expect(computeAccessibleName(screen.getByTestId('child-with-label'))).toBe('Hello World'); + expect(computeAccessibleName(screen.getByTestId('deeply-nested'))).toBe('Hello World'); + expect(computeAccessibleName(screen.getByTestId('child-label-over-text'))).toBe('Hello World'); + expect(computeAccessibleName(screen.getByTestId('child-accessibility-label-over-text'))).toBe( + 'Hello World', + ); + }); + + test('TextInput placeholder is used only for the element itself', async () => { + await render( + <> + + + + Hello + + + + + , + ); + expect(computeAccessibleName(screen.getByTestId('text-input'))).toBe('Placeholder'); + expect(computeAccessibleName(screen.getByTestId('parent'))).toBe('Hello'); + expect(computeAccessibleName(screen.getByTestId('parent-no-text'))).toBe(''); + }); }); diff --git a/src/helpers/accessibility.ts b/src/helpers/accessibility.ts index 6c6a87198..679a1ba83 100644 --- a/src/helpers/accessibility.ts +++ b/src/helpers/accessibility.ts @@ -244,13 +244,20 @@ export function computeAriaValue(element: HostElement): AccessibilityValue { }; } -export function computeAccessibleName(element: HostElement): string | undefined { +type ComputeAccessibleNameOptions = { + root?: boolean; +}; + +export function computeAccessibleName( + element: HostElement, + options?: ComputeAccessibleNameOptions, +): string | undefined { const label = computeAriaLabel(element); if (label) { return label; } - if (isHostTextInput(element) && element.props.placeholder) { + if (isHostTextInput(element) && element.props.placeholder && options?.root !== false) { return element.props.placeholder; } @@ -261,7 +268,7 @@ export function computeAccessibleName(element: HostElement): string | undefined parts.push(child); } } else { - const childLabel = computeAccessibleName(child); + const childLabel = computeAccessibleName(child, { root: false }); if (childLabel) { parts.push(childLabel); } diff --git a/src/queries/__tests__/role.test.tsx b/src/queries/__tests__/role.test.tsx index 7a112b5bd..c07c4c92a 100644 --- a/src/queries/__tests__/role.test.tsx +++ b/src/queries/__tests__/role.test.tsx @@ -135,16 +135,16 @@ describe('supports name option', () => { expect(screen.getByRole('button', { name: 'Save' }).props.testID).toBe('target-button'); }); - test('returns an element that has the corresponding role when several children include the name', async () => { + test('returns an element that has the corresponding role when several children provide the name', async () => { await render( Save - Save + As , ); // assert on the testId to be sure that the returned element is the one with the accessibilityRole - expect(screen.getByRole('button', { name: 'Save' }).props.testID).toBe('target-button'); + expect(screen.getByRole('button', { name: 'Save As' }).props.testID).toBe('target-button'); }); test('returns an element that has the corresponding role and a children with a matching accessibilityLabel', async () => { diff --git a/src/queries/role.ts b/src/queries/role.ts index 8e199b36d..04b5ae865 100644 --- a/src/queries/role.ts +++ b/src/queries/role.ts @@ -40,14 +40,8 @@ export type ByRoleOptions = CommonQueryOptions & const matchAccessibleNameIfNeeded = (node: HostElement, name?: TextMatch) => { if (name == null) return true; - // TODO: rewrite computeAccessibleName for real world a11y compliance const accessibleName = computeAccessibleName(node); - if (matches(name, accessibleName)) { - return true; - } - - const { queryAllByText, queryAllByLabelText } = getQueriesForElement(node); - return queryAllByText(name).length > 0 || queryAllByLabelText(name).length > 0; + return matches(name, accessibleName); }; const matchAccessibleStateIfNeeded = (node: HostElement, options?: ByRoleOptions) => { From 42466d4c30ea3836d0754563e2fcd340199da4ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Thu, 12 Feb 2026 00:40:52 +0000 Subject: [PATCH 3/4] . --- src/helpers/__tests__/accessiblity.test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/helpers/__tests__/accessiblity.test.tsx b/src/helpers/__tests__/accessiblity.test.tsx index cdc2f6be0..b768c84be 100644 --- a/src/helpers/__tests__/accessiblity.test.tsx +++ b/src/helpers/__tests__/accessiblity.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Pressable, Switch, Text, TextInput, TouchableOpacity, View } from 'react-native'; +import { Image, Pressable, Switch, Text, TextInput, TouchableOpacity, View } from 'react-native'; import { isHiddenFromAccessibility, isInaccessible, render, screen } from '../..'; import { @@ -492,6 +492,7 @@ describe('computeAccessibleName', () => { Text Content + Image Alt , ); expect(computeAccessibleName(screen.getByTestId('aria-label'))).toBe('ARIA Label'); @@ -500,6 +501,7 @@ describe('computeAccessibleName', () => { ); expect(computeAccessibleName(screen.getByTestId('text-content'))).toBe('Text Content'); expect(computeAccessibleName(screen.getByTestId('text-input'))).toBe('Text Input'); + expect(computeAccessibleName(screen.getByTestId('image'))).toBe('Image Alt'); }); test('basic precedence', async () => { From adc7ef99faecd10eb6fee5f3fbc1bf9fa174fe77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Thu, 12 Feb 2026 00:45:29 +0000 Subject: [PATCH 4/4] fix ci --- package.json | 2 +- src/helpers/accessibility.ts | 2 +- src/queries/role.ts | 1 - yarn.lock | 4 ++-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 806b43ac7..6b75cc9fd 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "prettier": "^3.6.2", "react": "19.2.3", "react-native": "0.84.0", - "react-native-gesture-handler": "^2.29.1", + "react-native-gesture-handler": "^2.30.0", "release-it": "^19.0.6", "test-renderer": "0.14.0", "typescript": "^5.9.3", diff --git a/src/helpers/accessibility.ts b/src/helpers/accessibility.ts index 679a1ba83..2187d0c58 100644 --- a/src/helpers/accessibility.ts +++ b/src/helpers/accessibility.ts @@ -6,7 +6,7 @@ import { getContainerElement, getHostSiblings, isHostElement } from './component import { findAll } from './find-all'; import { isHostImage, isHostSwitch, isHostText, isHostTextInput } from './host-component-names'; import { getTextContent } from './text-content'; -import { getTextInputValue, isEditableTextInput } from './text-input'; +import { isEditableTextInput } from './text-input'; type IsInaccessibleOptions = { cache?: WeakMap; diff --git a/src/queries/role.ts b/src/queries/role.ts index 04b5ae865..f37cb79ee 100644 --- a/src/queries/role.ts +++ b/src/queries/role.ts @@ -17,7 +17,6 @@ import { matchAccessibilityValue } from '../helpers/matchers/match-accessibility import { matchStringProp } from '../helpers/matchers/match-string-prop'; import { matches, type TextMatch } from '../matches'; import type { StringWithAutocomplete } from '../types'; -import { getQueriesForElement } from '../within'; import type { FindAllByQuery, FindByQuery, diff --git a/yarn.lock b/yarn.lock index fa3a316d6..831217c5d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3008,7 +3008,7 @@ __metadata: pretty-format: "npm:^30.2.0" react: "npm:19.2.3" react-native: "npm:0.84.0" - react-native-gesture-handler: "npm:^2.29.1" + react-native-gesture-handler: "npm:^2.30.0" redent: "npm:^3.0.0" release-it: "npm:^19.0.6" test-renderer: "npm:0.14.0" @@ -9291,7 +9291,7 @@ __metadata: languageName: node linkType: hard -"react-native-gesture-handler@npm:^2.29.1": +"react-native-gesture-handler@npm:^2.30.0": version: 2.30.0 resolution: "react-native-gesture-handler@npm:2.30.0" dependencies: