From 805526bf0696ed6b87330be1840eabce060bb6d1 Mon Sep 17 00:00:00 2001 From: ErfanBagheri404 Date: Sat, 15 Aug 2026 11:57:12 +0330 Subject: [PATCH 1/2] fix: remove duplicate tabstop in RadioButton.Item The item renders an outer TouchableRipple plus the inner RadioButton, both of which are focusable on web. This makes the item take two Tab presses to navigate past. Disable the inner radio button's focusability so only the outer touchable is a tabstop. Fixes #4775 --- src/components/RadioButton/RadioButtonItem.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/RadioButton/RadioButtonItem.tsx b/src/components/RadioButton/RadioButtonItem.tsx index e37ed435e2..11d077370a 100644 --- a/src/components/RadioButton/RadioButtonItem.tsx +++ b/src/components/RadioButton/RadioButtonItem.tsx @@ -165,7 +165,12 @@ const RadioButtonItem = ({ color, theme, uncheckedColor, - }; + // The outer TouchableRipple in RadioButtonItem already provides the + // interactive surface. Disable the inner RadioButton's focusability + // so there is only one tabstop per radio button item on web. + focusable: false, + tabIndex: -1, + } as const; const isLeading = position === 'leading'; let radioButton: any; From b3b6612f48338e595d25b65e5c04c9e45518f10e Mon Sep 17 00:00:00 2001 From: ErfanBagheri404 Date: Sat, 22 Aug 2026 09:05:49 +0330 Subject: [PATCH 2/2] fix: hide inner RadioButton from a11y tree, add regression test The inner RadioButton inside RadioButtonItem exposed its own role="radio" and aria-checked to screen readers, creating a duplicate control. Apply accessible={false} + aria-hidden={true} (matches Checkbox.Item), update snapshots, and add a regression test asserting getAllByRole('radio') returns exactly 1. Addresses review comments on #5050. --- .../RadioButton/RadioButtonItem.tsx | 8 ++++++-- .../RadioButton/RadioButtonItem.test.tsx | 13 ++++++++++++ .../RadioButtonItem.test.tsx.snap | 20 +++++++++++-------- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/components/RadioButton/RadioButtonItem.tsx b/src/components/RadioButton/RadioButtonItem.tsx index 11d077370a..0a737705ec 100644 --- a/src/components/RadioButton/RadioButtonItem.tsx +++ b/src/components/RadioButton/RadioButtonItem.tsx @@ -166,10 +166,14 @@ const RadioButtonItem = ({ theme, uncheckedColor, // The outer TouchableRipple in RadioButtonItem already provides the - // interactive surface. Disable the inner RadioButton's focusability - // so there is only one tabstop per radio button item on web. + // interactive surface. Hide the inner RadioButton from the + // accessibility tree so screen readers only encounter one control, + // and keep tabIndex: -1 so web keyboard users have a single + // tabstop per row (matches the approach used in Checkbox.Item). focusable: false, tabIndex: -1, + accessible: false, + 'aria-hidden': true, } as const; const isLeading = position === 'leading'; let radioButton: any; diff --git a/src/components/__tests__/RadioButton/RadioButtonItem.test.tsx b/src/components/__tests__/RadioButton/RadioButtonItem.test.tsx index 821d9ebc5b..272f99e011 100644 --- a/src/components/__tests__/RadioButton/RadioButtonItem.test.tsx +++ b/src/components/__tests__/RadioButton/RadioButtonItem.test.tsx @@ -84,3 +84,16 @@ it('should execute onLongPress', async () => { expect(onLongPress).toHaveBeenCalledTimes(1); }); + +it('exposes only one radio control to screen readers', async () => { + await render( + + ); + + expect(screen.getAllByRole('radio')).toHaveLength(1); +}); + diff --git a/src/components/__tests__/RadioButton/__snapshots__/RadioButtonItem.test.tsx.snap b/src/components/__tests__/RadioButton/__snapshots__/RadioButtonItem.test.tsx.snap index 5867b1408c..280c367331 100644 --- a/src/components/__tests__/RadioButton/__snapshots__/RadioButtonItem.test.tsx.snap +++ b/src/components/__tests__/RadioButton/__snapshots__/RadioButtonItem.test.tsx.snap @@ -74,9 +74,10 @@ exports[`can render leading radio button control 1`] = ` "text": undefined, } } - accessible={true} + accessible={false} + aria-hidden={true} collapsable={false} - focusable={true} + focusable={false} onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -288,9 +289,10 @@ exports[`can render the Android radio button on different platforms 1`] = ` "text": undefined, } } - accessible={true} + accessible={false} + aria-hidden={true} collapsable={false} - focusable={true} + focusable={false} onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -440,9 +442,10 @@ exports[`can render the iOS radio button on different platforms 1`] = ` "text": undefined, } } - accessible={true} + accessible={false} + aria-hidden={true} collapsable={false} - focusable={true} + focusable={false} onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -618,9 +621,10 @@ exports[`renders unchecked 1`] = ` "text": undefined, } } - accessible={true} + accessible={false} + aria-hidden={true} collapsable={false} - focusable={true} + focusable={false} onBlur={[Function]} onClick={[Function]} onFocus={[Function]}