From 71ace0700c0253fabf50731dee9a50fb9e83aafe Mon Sep 17 00:00:00 2001 From: ashu75575 Date: Sat, 31 Jan 2026 00:25:09 +0530 Subject: [PATCH 1/7] Fix accessibility roles and states for radio buttons, checkboxes, and tabs --- .../SelectionList/ListItem/BaseListItem.tsx | 31 +++++++++++++++++-- .../ListItem/MultiSelectListItem.tsx | 1 + .../SelectionList/ListItem/RadioListItem.tsx | 3 ++ .../BaseListItem.tsx | 19 +++++++++++- .../TabSelector/TabSelectorItem.tsx | 3 +- 5 files changed, 52 insertions(+), 5 deletions(-) diff --git a/src/components/SelectionList/ListItem/BaseListItem.tsx b/src/components/SelectionList/ListItem/BaseListItem.tsx index a41a9b93c161b..17aa780f5e2a3 100644 --- a/src/components/SelectionList/ListItem/BaseListItem.tsx +++ b/src/components/SelectionList/ListItem/BaseListItem.tsx @@ -1,4 +1,5 @@ import React, {useRef} from 'react'; +import type {Role} from 'react-native'; import {View} from 'react-native'; import {getButtonRole} from '@components/Button/utils'; import Icon from '@components/Icon'; @@ -68,7 +69,16 @@ function BaseListItem({ }; const rightHandSideComponentRender = () => { - if (canSelectMultiple || !rightHandSideComponent) { + if (!rightHandSideComponent) { + return null; + } + + // When canSelectMultiple is true, most components (like UserListItem) handle checkboxes in children, + // so rightHandSideComponent should be hidden. However, some components (like MultiSelectListItem) + // explicitly pass checkboxes as rightHandSideComponent and need them to show. + // We check if rightHandSideComponent exists and shouldUseDefaultRightHandSideCheckmark is false, + // which indicates the component is handling its own checkbox rendering and wants rightHandSideComponent to show. + if (canSelectMultiple && shouldUseDefaultRightHandSideCheckmark) { return null; } @@ -85,6 +95,21 @@ function BaseListItem({ const shouldShowHiddenCheckmark = shouldShowRBRIndicator && !shouldShowCheckmark; + // Use radio role for single-select (e.g. Date dropdown options), checkbox for multi-select, button otherwise + const isRadioOption = !canSelectMultiple && !!rightHandSideComponent; + const isCheckboxOption = canSelectMultiple; + let role: Role | undefined; + if (isCheckboxOption) { + role = CONST.ROLE.CHECKBOX; + } else if (isRadioOption) { + role = CONST.ROLE.RADIO; + } else { + role = getButtonRole(true) ?? CONST.ROLE.BUTTON; + } + const accessibilityState = isRadioOption || isCheckboxOption + ? {checked: !!item.isSelected, selected: !!isFocused} + : {selected: !!isFocused}; + return ( onDismissError(item)} @@ -113,7 +138,8 @@ function BaseListItem({ disabled={isDisabled && !item.isSelected} interactive={item.isInteractive} accessibilityLabel={item.accessibilityLabel ?? [item.text, item.text !== item.alternateText ? item.alternateText : undefined].filter(Boolean).join(', ')} - role={getButtonRole(true)} + role={role} + accessibilityState={accessibilityState} isNested hoverDimmingValue={1} pressDimmingValue={item.isInteractive === false ? 1 : variables.pressDimValue} @@ -140,7 +166,6 @@ function BaseListItem({ > ({ showTooltip={showTooltip} isDisabled={isDisabled} rightHandSideComponent={checkboxComponent} + canSelectMultiple onSelectRow={onSelectRow} onDismissError={onDismissError} shouldPreventEnterKeySubmit={shouldPreventEnterKeySubmit} diff --git a/src/components/SelectionList/ListItem/RadioListItem.tsx b/src/components/SelectionList/ListItem/RadioListItem.tsx index e1a340e3d81d8..b12666ede8b3f 100644 --- a/src/components/SelectionList/ListItem/RadioListItem.tsx +++ b/src/components/SelectionList/ListItem/RadioListItem.tsx @@ -26,6 +26,7 @@ function RadioListItem({ shouldHighlightSelectedItem = true, shouldDisableHoverStyle, shouldStopMouseLeavePropagation, + canSelectMultiple, }: RadioListItemProps) { const styles = useThemeStyles(); const fullTitle = isMultilineSupported ? item.text?.trimStart() : item.text; @@ -40,6 +41,7 @@ function RadioListItem({ isFocused={isFocused} isDisabled={isDisabled} showTooltip={showTooltip} + canSelectMultiple={canSelectMultiple} onSelectRow={onSelectRow} onDismissError={onDismissError} shouldPreventEnterKeySubmit={shouldPreventEnterKeySubmit} @@ -51,6 +53,7 @@ function RadioListItem({ shouldHighlightSelectedItem={shouldHighlightSelectedItem} shouldDisableHoverStyle={shouldDisableHoverStyle} shouldStopMouseLeavePropagation={shouldStopMouseLeavePropagation} + shouldUseDefaultRightHandSideCheckmark={!canSelectMultiple || !rightHandSideComponent} > <> {!!item.leftElement && item.leftElement} diff --git a/src/components/SelectionListWithSections/BaseListItem.tsx b/src/components/SelectionListWithSections/BaseListItem.tsx index efd068359c06a..7526b9ce4dcd2 100644 --- a/src/components/SelectionListWithSections/BaseListItem.tsx +++ b/src/components/SelectionListWithSections/BaseListItem.tsx @@ -1,4 +1,5 @@ import React, {useRef} from 'react'; +import type {Role} from 'react-native'; import {View} from 'react-native'; import {getButtonRole} from '@components/Button/utils'; import Icon from '@components/Icon'; @@ -83,6 +84,21 @@ function BaseListItem({ const defaultAccessibilityLabel = item.text === item.alternateText ? (item.text ?? '') : [item.text, item.alternateText].filter(Boolean).join(', '); const accessibilityLabel = item.accessibilityLabel ?? defaultAccessibilityLabel; + // Use radio role for single-select (e.g. Date dropdown options), checkbox for multi-select, button otherwise + const isRadioOption = !canSelectMultiple && !!rightHandSideComponent; + const isCheckboxOption = canSelectMultiple; + let role: Role | undefined; + if (isCheckboxOption) { + role = CONST.ROLE.CHECKBOX; + } else if (isRadioOption) { + role = CONST.ROLE.RADIO; + } else { + role = getButtonRole(true) ?? CONST.ROLE.BUTTON; + } + const accessibilityState = isRadioOption || isCheckboxOption + ? {checked: !!item.isSelected, selected: !!isFocused} + : {selected: !!isFocused}; + return ( onDismissError(item)} @@ -111,7 +127,8 @@ function BaseListItem({ disabled={isDisabled && !item.isSelected} interactive={item.isInteractive} accessibilityLabel={accessibilityLabel} - role={getButtonRole(true)} + accessibilityState={accessibilityState} + role={role} isNested hoverDimmingValue={1} pressDimmingValue={item.isInteractive === false ? 1 : variables.pressDimValue} diff --git a/src/components/TabSelector/TabSelectorItem.tsx b/src/components/TabSelector/TabSelectorItem.tsx index 55961c9e1ac8f..8373b1380d8bd 100644 --- a/src/components/TabSelector/TabSelectorItem.tsx +++ b/src/components/TabSelector/TabSelectorItem.tsx @@ -118,12 +118,13 @@ function TabSelectorItem({ setIsHovered(true)} onHoverOut={() => setIsHovered(false)} - role={CONST.ROLE.BUTTON} + role={CONST.ROLE.TAB} dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} testID={testID} ref={childRef} From e5efd1c2e3f6371f830f5f3fed772cf0898f0ee7 Mon Sep 17 00:00:00 2001 From: ashu75575 Date: Sat, 31 Jan 2026 01:15:19 +0530 Subject: [PATCH 2/7] code format --- src/components/SelectionList/ListItem/BaseListItem.tsx | 4 +--- src/components/SelectionListWithSections/BaseListItem.tsx | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/SelectionList/ListItem/BaseListItem.tsx b/src/components/SelectionList/ListItem/BaseListItem.tsx index 17aa780f5e2a3..badce03a17cc7 100644 --- a/src/components/SelectionList/ListItem/BaseListItem.tsx +++ b/src/components/SelectionList/ListItem/BaseListItem.tsx @@ -106,9 +106,7 @@ function BaseListItem({ } else { role = getButtonRole(true) ?? CONST.ROLE.BUTTON; } - const accessibilityState = isRadioOption || isCheckboxOption - ? {checked: !!item.isSelected, selected: !!isFocused} - : {selected: !!isFocused}; + const accessibilityState = isRadioOption || isCheckboxOption ? {checked: !!item.isSelected, selected: !!isFocused} : {selected: !!isFocused}; return ( ({ } else { role = getButtonRole(true) ?? CONST.ROLE.BUTTON; } - const accessibilityState = isRadioOption || isCheckboxOption - ? {checked: !!item.isSelected, selected: !!isFocused} - : {selected: !!isFocused}; + const accessibilityState = isRadioOption || isCheckboxOption ? {checked: !!item.isSelected, selected: !!isFocused} : {selected: !!isFocused}; return ( Date: Sat, 31 Jan 2026 15:31:53 +0530 Subject: [PATCH 3/7] fix ai comment --- .../SelectionList/ListItem/BaseListItem.tsx | 11 ++---- .../SelectionList/ListItem/RadioListItem.tsx | 1 + .../SelectionList/ListItem/types.ts | 3 ++ .../BaseListItem.tsx | 7 ++-- .../RadioListItem.tsx | 1 + .../SelectionListWithSections/types.ts | 39 +------------------ 6 files changed, 13 insertions(+), 49 deletions(-) diff --git a/src/components/SelectionList/ListItem/BaseListItem.tsx b/src/components/SelectionList/ListItem/BaseListItem.tsx index badce03a17cc7..7746acc82ba92 100644 --- a/src/components/SelectionList/ListItem/BaseListItem.tsx +++ b/src/components/SelectionList/ListItem/BaseListItem.tsx @@ -48,6 +48,7 @@ function BaseListItem({ shouldDisableHoverStyle, shouldStopMouseLeavePropagation = true, shouldShowRightCaret = false, + shouldUseRadioRole = false, }: BaseListItemProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -73,11 +74,6 @@ function BaseListItem({ return null; } - // When canSelectMultiple is true, most components (like UserListItem) handle checkboxes in children, - // so rightHandSideComponent should be hidden. However, some components (like MultiSelectListItem) - // explicitly pass checkboxes as rightHandSideComponent and need them to show. - // We check if rightHandSideComponent exists and shouldUseDefaultRightHandSideCheckmark is false, - // which indicates the component is handling its own checkbox rendering and wants rightHandSideComponent to show. if (canSelectMultiple && shouldUseDefaultRightHandSideCheckmark) { return null; } @@ -95,8 +91,7 @@ function BaseListItem({ const shouldShowHiddenCheckmark = shouldShowRBRIndicator && !shouldShowCheckmark; - // Use radio role for single-select (e.g. Date dropdown options), checkbox for multi-select, button otherwise - const isRadioOption = !canSelectMultiple && !!rightHandSideComponent; + const isRadioOption = shouldUseRadioRole; const isCheckboxOption = canSelectMultiple; let role: Role | undefined; if (isCheckboxOption) { @@ -104,7 +99,7 @@ function BaseListItem({ } else if (isRadioOption) { role = CONST.ROLE.RADIO; } else { - role = getButtonRole(true) ?? CONST.ROLE.BUTTON; + role = getButtonRole(true); } const accessibilityState = isRadioOption || isCheckboxOption ? {checked: !!item.isSelected, selected: !!isFocused} : {selected: !!isFocused}; diff --git a/src/components/SelectionList/ListItem/RadioListItem.tsx b/src/components/SelectionList/ListItem/RadioListItem.tsx index b12666ede8b3f..ef9dcca30dcbc 100644 --- a/src/components/SelectionList/ListItem/RadioListItem.tsx +++ b/src/components/SelectionList/ListItem/RadioListItem.tsx @@ -54,6 +54,7 @@ function RadioListItem({ shouldDisableHoverStyle={shouldDisableHoverStyle} shouldStopMouseLeavePropagation={shouldStopMouseLeavePropagation} shouldUseDefaultRightHandSideCheckmark={!canSelectMultiple || !rightHandSideComponent} + shouldUseRadioRole > <> {!!item.leftElement && item.leftElement} diff --git a/src/components/SelectionList/ListItem/types.ts b/src/components/SelectionList/ListItem/types.ts index 8301690764467..9f2ba7b5450c0 100644 --- a/src/components/SelectionList/ListItem/types.ts +++ b/src/components/SelectionList/ListItem/types.ts @@ -314,6 +314,9 @@ type BaseListItemProps = CommonListItemProps & { /** Whether to call stopPropagation on the mouseleave event in BaseListItem */ shouldStopMouseLeavePropagation?: boolean; + + /** Whether to expose this row as a radio option for screen readers (single-choice group). Set by RadioListItem. */ + shouldUseRadioRole?: boolean; }; type SplitListItemType = ListItem & diff --git a/src/components/SelectionListWithSections/BaseListItem.tsx b/src/components/SelectionListWithSections/BaseListItem.tsx index d10bfb0416c68..df29e2c5db96f 100644 --- a/src/components/SelectionListWithSections/BaseListItem.tsx +++ b/src/components/SelectionListWithSections/BaseListItem.tsx @@ -49,6 +49,7 @@ function BaseListItem({ shouldHighlightSelectedItem = true, shouldDisableHoverStyle, shouldStopMouseLeavePropagation = true, + shouldUseRadioRole = false, }: BaseListItemProps) { const icons = useMemoizedLazyExpensifyIcons(['ArrowRight']); const theme = useTheme(); @@ -84,8 +85,7 @@ function BaseListItem({ const defaultAccessibilityLabel = item.text === item.alternateText ? (item.text ?? '') : [item.text, item.alternateText].filter(Boolean).join(', '); const accessibilityLabel = item.accessibilityLabel ?? defaultAccessibilityLabel; - // Use radio role for single-select (e.g. Date dropdown options), checkbox for multi-select, button otherwise - const isRadioOption = !canSelectMultiple && !!rightHandSideComponent; + const isRadioOption = shouldUseRadioRole; const isCheckboxOption = canSelectMultiple; let role: Role | undefined; if (isCheckboxOption) { @@ -93,7 +93,7 @@ function BaseListItem({ } else if (isRadioOption) { role = CONST.ROLE.RADIO; } else { - role = getButtonRole(true) ?? CONST.ROLE.BUTTON; + role = getButtonRole(true); } const accessibilityState = isRadioOption || isCheckboxOption ? {checked: !!item.isSelected, selected: !!isFocused} : {selected: !!isFocused}; @@ -148,7 +148,6 @@ function BaseListItem({ > ({ shouldHighlightSelectedItem={shouldHighlightSelectedItem} shouldDisableHoverStyle={shouldDisableHoverStyle} shouldStopMouseLeavePropagation={shouldStopMouseLeavePropagation} + shouldUseRadioRole > <> {!!item.leftElement && item.leftElement} diff --git a/src/components/SelectionListWithSections/types.ts b/src/components/SelectionListWithSections/types.ts index 7badcd137a8c7..d24582162e3ea 100644 --- a/src/components/SelectionListWithSections/types.ts +++ b/src/components/SelectionListWithSections/types.ts @@ -33,15 +33,11 @@ import type { SearchCategoryGroup, SearchDataTypes, SearchMemberGroup, - SearchMerchantGroup, SearchMonthGroup, - SearchQuarterGroup, SearchTagGroup, SearchTask, SearchTransactionAction, - SearchWeekGroup, SearchWithdrawalIDGroup, - SearchYearGroup, } from '@src/types/onyx/SearchResults'; import type {ReceiptErrors} from '@src/types/onyx/Transaction'; import type Transaction from '@src/types/onyx/Transaction'; @@ -452,9 +448,6 @@ type TransactionReportGroupListItemType = TransactionGroupListItemType & {groupe /** The date the report was exported */ exported?: string; - /** Whether the status field should be shown in a pending state */ - shouldShowStatusAsPending?: boolean; - /** * Whether we should show the report year. * This is true if at least one report in the dataset was created in past years @@ -519,37 +512,11 @@ type TransactionCategoryGroupListItemType = TransactionGroupListItemType & {grou formattedCategory?: string; }; -type TransactionMerchantGroupListItemType = TransactionGroupListItemType & {groupedBy: typeof CONST.SEARCH.GROUP_BY.MERCHANT} & SearchMerchantGroup & { - /** Final and formatted "merchant" value used for displaying and sorting */ - formattedMerchant?: string; - }; - type TransactionTagGroupListItemType = TransactionGroupListItemType & {groupedBy: typeof CONST.SEARCH.GROUP_BY.TAG} & SearchTagGroup & { /** Final and formatted "tag" value used for displaying and sorting */ formattedTag?: string; }; -type TransactionWeekGroupListItemType = TransactionGroupListItemType & {groupedBy: typeof CONST.SEARCH.GROUP_BY.WEEK} & SearchWeekGroup & { - /** Final and formatted "week" value used for displaying */ - formattedWeek: string; - }; - -type TransactionYearGroupListItemType = TransactionGroupListItemType & {groupedBy: typeof CONST.SEARCH.GROUP_BY.YEAR} & SearchYearGroup & { - /** Final and formatted "year" value used for displaying */ - formattedYear: string; - - /** Key used for sorting */ - sortKey: number; - }; - -type TransactionQuarterGroupListItemType = TransactionGroupListItemType & {groupedBy: typeof CONST.SEARCH.GROUP_BY.QUARTER} & SearchQuarterGroup & { - /** Final and formatted "quarter" value used for displaying */ - formattedQuarter: string; - - /** Sort key for sorting */ - sortKey: number; - }; - type ListItemProps = CommonListItemProps & { /** The section list item */ item: TItem; @@ -619,6 +586,8 @@ type BaseListItemProps = CommonListItemProps & testID?: string; /** Whether to show the default right hand side checkmark */ shouldUseDefaultRightHandSideCheckmark?: boolean; + /** Whether to expose this row as a radio option for screen readers (single-choice group). Set by RadioListItem. */ + shouldUseRadioRole?: boolean; }; type UserListItemProps = ListItemProps & @@ -1193,11 +1162,7 @@ export type { TransactionCardGroupListItemType, TransactionWithdrawalIDGroupListItemType, TransactionCategoryGroupListItemType, - TransactionMerchantGroupListItemType, TransactionTagGroupListItemType, - TransactionWeekGroupListItemType, - TransactionYearGroupListItemType, - TransactionQuarterGroupListItemType, Section, SectionListDataType, SectionWithIndexOffset, From db43c0170b21145a598184aac4c31d86cd5675ab Mon Sep 17 00:00:00 2001 From: ashu75575 Date: Sun, 1 Feb 2026 23:43:56 +0530 Subject: [PATCH 4/7] fix type checks --- .../SelectionListWithSections/types.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/components/SelectionListWithSections/types.ts b/src/components/SelectionListWithSections/types.ts index d24582162e3ea..636b6ed3e60fc 100644 --- a/src/components/SelectionListWithSections/types.ts +++ b/src/components/SelectionListWithSections/types.ts @@ -33,11 +33,15 @@ import type { SearchCategoryGroup, SearchDataTypes, SearchMemberGroup, + SearchMerchantGroup, SearchMonthGroup, + SearchQuarterGroup, SearchTagGroup, SearchTask, SearchTransactionAction, + SearchWeekGroup, SearchWithdrawalIDGroup, + SearchYearGroup, } from '@src/types/onyx/SearchResults'; import type {ReceiptErrors} from '@src/types/onyx/Transaction'; import type Transaction from '@src/types/onyx/Transaction'; @@ -448,6 +452,9 @@ type TransactionReportGroupListItemType = TransactionGroupListItemType & {groupe /** The date the report was exported */ exported?: string; + /** Whether the status field should be shown in a pending state */ + shouldShowStatusAsPending?: boolean; + /** * Whether we should show the report year. * This is true if at least one report in the dataset was created in past years @@ -512,11 +519,37 @@ type TransactionCategoryGroupListItemType = TransactionGroupListItemType & {grou formattedCategory?: string; }; +type TransactionMerchantGroupListItemType = TransactionGroupListItemType & {groupedBy: typeof CONST.SEARCH.GROUP_BY.MERCHANT} & SearchMerchantGroup & { + /** Final and formatted "merchant" value used for displaying and sorting */ + formattedMerchant?: string; + }; + type TransactionTagGroupListItemType = TransactionGroupListItemType & {groupedBy: typeof CONST.SEARCH.GROUP_BY.TAG} & SearchTagGroup & { /** Final and formatted "tag" value used for displaying and sorting */ formattedTag?: string; }; +type TransactionWeekGroupListItemType = TransactionGroupListItemType & {groupedBy: typeof CONST.SEARCH.GROUP_BY.WEEK} & SearchWeekGroup & { + /** Final and formatted "week" value used for displaying */ + formattedWeek: string; + }; + +type TransactionYearGroupListItemType = TransactionGroupListItemType & {groupedBy: typeof CONST.SEARCH.GROUP_BY.YEAR} & SearchYearGroup & { + /** Final and formatted "year" value used for displaying */ + formattedYear: string; + + /** Key used for sorting */ + sortKey: number; + }; + +type TransactionQuarterGroupListItemType = TransactionGroupListItemType & {groupedBy: typeof CONST.SEARCH.GROUP_BY.QUARTER} & SearchQuarterGroup & { + /** Final and formatted "quarter" value used for displaying */ + formattedQuarter: string; + + /** Sort key for sorting */ + sortKey: number; + }; + type ListItemProps = CommonListItemProps & { /** The section list item */ item: TItem; @@ -1162,7 +1195,11 @@ export type { TransactionCardGroupListItemType, TransactionWithdrawalIDGroupListItemType, TransactionCategoryGroupListItemType, + TransactionMerchantGroupListItemType, TransactionTagGroupListItemType, + TransactionWeekGroupListItemType, + TransactionYearGroupListItemType, + TransactionQuarterGroupListItemType, Section, SectionListDataType, SectionWithIndexOffset, From a3d246b27bf38c564a3619ab917e789e5c465c83 Mon Sep 17 00:00:00 2001 From: ashu75575 Date: Sun, 1 Feb 2026 23:50:56 +0530 Subject: [PATCH 5/7] fix eslint --- src/CONST/index.ts | 6 ++++++ src/components/SelectionList/ListItem/BaseListItem.tsx | 1 + src/components/SelectionListWithSections/BaseListItem.tsx | 1 + 3 files changed, 8 insertions(+) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 08b1b94e0da68..c6455c4b54e58 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -8110,6 +8110,12 @@ const CONST = { LHN: { OPTION_ROW: 'LHN-OptionRow', }, + SELECTION_LIST: { + BASE_LIST_ITEM: 'SelectionList-BaseListItem', + }, + SELECTION_LIST_WITH_SECTIONS: { + BASE_LIST_ITEM: 'SelectionListWithSections-BaseListItem', + }, CONTEXT_MENU: { REPLY_IN_THREAD: 'ContextMenu-ReplyInThread', MARK_AS_UNREAD: 'ContextMenu-MarkAsUnread', diff --git a/src/components/SelectionList/ListItem/BaseListItem.tsx b/src/components/SelectionList/ListItem/BaseListItem.tsx index 7746acc82ba92..846b29f05eb1d 100644 --- a/src/components/SelectionList/ListItem/BaseListItem.tsx +++ b/src/components/SelectionList/ListItem/BaseListItem.tsx @@ -112,6 +112,7 @@ function BaseListItem({ contentContainerStyle={containerStyle} > ({ contentContainerStyle={containerStyle} > Date: Mon, 2 Feb 2026 00:19:25 +0530 Subject: [PATCH 6/7] fix tests --- src/components/SelectionList/ListItem/BaseListItem.tsx | 7 ++----- src/components/SelectionListWithSections/BaseListItem.tsx | 1 + 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/SelectionList/ListItem/BaseListItem.tsx b/src/components/SelectionList/ListItem/BaseListItem.tsx index 846b29f05eb1d..1958cc6c93bc3 100644 --- a/src/components/SelectionList/ListItem/BaseListItem.tsx +++ b/src/components/SelectionList/ListItem/BaseListItem.tsx @@ -70,11 +70,7 @@ function BaseListItem({ }; const rightHandSideComponentRender = () => { - if (!rightHandSideComponent) { - return null; - } - - if (canSelectMultiple && shouldUseDefaultRightHandSideCheckmark) { + if (canSelectMultiple || !rightHandSideComponent) { return null; } @@ -160,6 +156,7 @@ function BaseListItem({ > ({ > Date: Mon, 2 Feb 2026 02:49:26 +0530 Subject: [PATCH 7/7] Refactor accessibility roles in selection list items --- .../SelectionList/ListItem/BaseListItem.tsx | 19 ++++--------------- .../ListItem/MultiSelectListItem.tsx | 2 +- .../SelectionList/ListItem/RadioListItem.tsx | 6 ++---- .../SelectionList/ListItem/types.ts | 8 ++++---- .../BaseListItem.tsx | 17 +++-------------- .../RadioListItem.tsx | 3 ++- .../SingleSelectListItem.tsx | 2 ++ .../SelectionListWithSections/types.ts | 6 ++++-- 8 files changed, 22 insertions(+), 41 deletions(-) diff --git a/src/components/SelectionList/ListItem/BaseListItem.tsx b/src/components/SelectionList/ListItem/BaseListItem.tsx index 1958cc6c93bc3..889311c4db857 100644 --- a/src/components/SelectionList/ListItem/BaseListItem.tsx +++ b/src/components/SelectionList/ListItem/BaseListItem.tsx @@ -1,5 +1,4 @@ import React, {useRef} from 'react'; -import type {Role} from 'react-native'; import {View} from 'react-native'; import {getButtonRole} from '@components/Button/utils'; import Icon from '@components/Icon'; @@ -48,7 +47,7 @@ function BaseListItem({ shouldDisableHoverStyle, shouldStopMouseLeavePropagation = true, shouldShowRightCaret = false, - shouldUseRadioRole = false, + accessibilityRole = getButtonRole(true), }: BaseListItemProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -87,17 +86,8 @@ function BaseListItem({ const shouldShowHiddenCheckmark = shouldShowRBRIndicator && !shouldShowCheckmark; - const isRadioOption = shouldUseRadioRole; - const isCheckboxOption = canSelectMultiple; - let role: Role | undefined; - if (isCheckboxOption) { - role = CONST.ROLE.CHECKBOX; - } else if (isRadioOption) { - role = CONST.ROLE.RADIO; - } else { - role = getButtonRole(true); - } - const accessibilityState = isRadioOption || isCheckboxOption ? {checked: !!item.isSelected, selected: !!isFocused} : {selected: !!isFocused}; + const accessibilityState = + accessibilityRole === CONST.ROLE.CHECKBOX || accessibilityRole === CONST.ROLE.RADIO ? {checked: !!item.isSelected, selected: !!isFocused} : {selected: !!isFocused}; return ( ({ contentContainerStyle={containerStyle} > ({ disabled={isDisabled && !item.isSelected} interactive={item.isInteractive} accessibilityLabel={item.accessibilityLabel ?? [item.text, item.text !== item.alternateText ? item.alternateText : undefined].filter(Boolean).join(', ')} - role={role} + role={accessibilityRole } accessibilityState={accessibilityState} isNested hoverDimmingValue={1} diff --git a/src/components/SelectionList/ListItem/MultiSelectListItem.tsx b/src/components/SelectionList/ListItem/MultiSelectListItem.tsx index 659ca8937c549..10bf72dab7d0e 100644 --- a/src/components/SelectionList/ListItem/MultiSelectListItem.tsx +++ b/src/components/SelectionList/ListItem/MultiSelectListItem.tsx @@ -76,8 +76,8 @@ function MultiSelectListItem({ showTooltip={showTooltip} isDisabled={isDisabled} rightHandSideComponent={checkboxComponent} - canSelectMultiple onSelectRow={onSelectRow} + accessibilityRole={CONST.ROLE.CHECKBOX} onDismissError={onDismissError} shouldPreventEnterKeySubmit={shouldPreventEnterKeySubmit} isMultilineSupported={isMultilineSupported} diff --git a/src/components/SelectionList/ListItem/RadioListItem.tsx b/src/components/SelectionList/ListItem/RadioListItem.tsx index ef9dcca30dcbc..021c45e4e5dee 100644 --- a/src/components/SelectionList/ListItem/RadioListItem.tsx +++ b/src/components/SelectionList/ListItem/RadioListItem.tsx @@ -26,7 +26,7 @@ function RadioListItem({ shouldHighlightSelectedItem = true, shouldDisableHoverStyle, shouldStopMouseLeavePropagation, - canSelectMultiple, + accessibilityRole, }: RadioListItemProps) { const styles = useThemeStyles(); const fullTitle = isMultilineSupported ? item.text?.trimStart() : item.text; @@ -41,7 +41,6 @@ function RadioListItem({ isFocused={isFocused} isDisabled={isDisabled} showTooltip={showTooltip} - canSelectMultiple={canSelectMultiple} onSelectRow={onSelectRow} onDismissError={onDismissError} shouldPreventEnterKeySubmit={shouldPreventEnterKeySubmit} @@ -53,8 +52,7 @@ function RadioListItem({ shouldHighlightSelectedItem={shouldHighlightSelectedItem} shouldDisableHoverStyle={shouldDisableHoverStyle} shouldStopMouseLeavePropagation={shouldStopMouseLeavePropagation} - shouldUseDefaultRightHandSideCheckmark={!canSelectMultiple || !rightHandSideComponent} - shouldUseRadioRole + accessibilityRole={accessibilityRole} > <> {!!item.leftElement && item.leftElement} diff --git a/src/components/SelectionList/ListItem/types.ts b/src/components/SelectionList/ListItem/types.ts index 9f2ba7b5450c0..da7a08101efd2 100644 --- a/src/components/SelectionList/ListItem/types.ts +++ b/src/components/SelectionList/ListItem/types.ts @@ -1,5 +1,5 @@ import type {ReactElement, ReactNode} from 'react'; -import type {AccessibilityState, BlurEvent, NativeSyntheticEvent, StyleProp, TargetedEvent, TextStyle, ViewStyle} from 'react-native'; +import type {AccessibilityState, BlurEvent, NativeSyntheticEvent, Role, StyleProp, TargetedEvent, TextStyle, ViewStyle} from 'react-native'; import type {AnimatedStyle} from 'react-native-reanimated'; import type {ValueOf} from 'type-fest'; import type {ForwardedFSClassProps} from '@libs/Fullstory/types'; @@ -195,6 +195,9 @@ type CommonListItemProps = { /** Accessibility State tells a person using either VoiceOver on iOS or TalkBack on Android the state of the element currently focused on */ accessibilityState?: AccessibilityState; + /** Accessibility role for the list item (e.g. 'checkbox' for multi-select options so screen readers announce checked state) */ + accessibilityRole?: Role; + /** Whether to show the right caret icon */ shouldShowRightCaret?: boolean; } & TRightHandSideComponent; @@ -314,9 +317,6 @@ type BaseListItemProps = CommonListItemProps & { /** Whether to call stopPropagation on the mouseleave event in BaseListItem */ shouldStopMouseLeavePropagation?: boolean; - - /** Whether to expose this row as a radio option for screen readers (single-choice group). Set by RadioListItem. */ - shouldUseRadioRole?: boolean; }; type SplitListItemType = ListItem & diff --git a/src/components/SelectionListWithSections/BaseListItem.tsx b/src/components/SelectionListWithSections/BaseListItem.tsx index 5fd9817a42861..6087dcddd4457 100644 --- a/src/components/SelectionListWithSections/BaseListItem.tsx +++ b/src/components/SelectionListWithSections/BaseListItem.tsx @@ -1,5 +1,4 @@ import React, {useRef} from 'react'; -import type {Role} from 'react-native'; import {View} from 'react-native'; import {getButtonRole} from '@components/Button/utils'; import Icon from '@components/Icon'; @@ -49,7 +48,7 @@ function BaseListItem({ shouldHighlightSelectedItem = true, shouldDisableHoverStyle, shouldStopMouseLeavePropagation = true, - shouldUseRadioRole = false, + accessibilityRole = getButtonRole(true), }: BaseListItemProps) { const icons = useMemoizedLazyExpensifyIcons(['ArrowRight']); const theme = useTheme(); @@ -85,17 +84,7 @@ function BaseListItem({ const defaultAccessibilityLabel = item.text === item.alternateText ? (item.text ?? '') : [item.text, item.alternateText].filter(Boolean).join(', '); const accessibilityLabel = item.accessibilityLabel ?? defaultAccessibilityLabel; - const isRadioOption = shouldUseRadioRole; - const isCheckboxOption = canSelectMultiple; - let role: Role | undefined; - if (isCheckboxOption) { - role = CONST.ROLE.CHECKBOX; - } else if (isRadioOption) { - role = CONST.ROLE.RADIO; - } else { - role = getButtonRole(true); - } - const accessibilityState = isRadioOption || isCheckboxOption ? {checked: !!item.isSelected, selected: !!isFocused} : {selected: !!isFocused}; + const accessibilityState = accessibilityRole === CONST.ROLE.CHECKBOX || accessibilityRole === CONST.ROLE.RADIO ? {checked: !!item.isSelected, selected: !!isFocused} : {selected: !!isFocused}; return ( ({ interactive={item.isInteractive} accessibilityLabel={accessibilityLabel} accessibilityState={accessibilityState} - role={role} + role={accessibilityRole} isNested hoverDimmingValue={1} pressDimmingValue={item.isInteractive === false ? 1 : variables.pressDimValue} diff --git a/src/components/SelectionListWithSections/RadioListItem.tsx b/src/components/SelectionListWithSections/RadioListItem.tsx index 40500628e6bf1..021c45e4e5dee 100644 --- a/src/components/SelectionListWithSections/RadioListItem.tsx +++ b/src/components/SelectionListWithSections/RadioListItem.tsx @@ -26,6 +26,7 @@ function RadioListItem({ shouldHighlightSelectedItem = true, shouldDisableHoverStyle, shouldStopMouseLeavePropagation, + accessibilityRole, }: RadioListItemProps) { const styles = useThemeStyles(); const fullTitle = isMultilineSupported ? item.text?.trimStart() : item.text; @@ -51,7 +52,7 @@ function RadioListItem({ shouldHighlightSelectedItem={shouldHighlightSelectedItem} shouldDisableHoverStyle={shouldDisableHoverStyle} shouldStopMouseLeavePropagation={shouldStopMouseLeavePropagation} - shouldUseRadioRole + accessibilityRole={accessibilityRole} > <> {!!item.leftElement && item.leftElement} diff --git a/src/components/SelectionListWithSections/SingleSelectListItem.tsx b/src/components/SelectionListWithSections/SingleSelectListItem.tsx index f380fe9648d87..ec36edad11e0f 100644 --- a/src/components/SelectionListWithSections/SingleSelectListItem.tsx +++ b/src/components/SelectionListWithSections/SingleSelectListItem.tsx @@ -1,6 +1,7 @@ import React, {useCallback} from 'react'; import Checkbox from '@components/Checkbox'; import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; import RadioListItem from './RadioListItem'; import type {ListItem, SingleSelectListItemProps} from './types'; @@ -48,6 +49,7 @@ function SingleSelectListItem({ isDisabled={isDisabled} rightHandSideComponent={radioCheckboxComponent} onSelectRow={onSelectRow} + accessibilityRole={CONST.ROLE.RADIO} onDismissError={onDismissError} shouldPreventEnterKeySubmit={shouldPreventEnterKeySubmit} isMultilineSupported={isMultilineSupported} diff --git a/src/components/SelectionListWithSections/types.ts b/src/components/SelectionListWithSections/types.ts index 636b6ed3e60fc..82c1b35ad94ac 100644 --- a/src/components/SelectionListWithSections/types.ts +++ b/src/components/SelectionListWithSections/types.ts @@ -13,6 +13,7 @@ import type { TextInput, TextStyle, ViewStyle, + Role } from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {AnimatedStyle} from 'react-native-reanimated'; @@ -123,6 +124,9 @@ type CommonListItemProps = { /** Whether to call stopPropagation on the mouseleave event in BaseListItem */ shouldStopMouseLeavePropagation?: boolean; + + /** Accessibility role for the list item (e.g. 'checkbox' for multi-select options so screen readers announce checked state) */ + accessibilityRole?: Role; } & TRightHandSideComponent; type ListItemFocusEventHandler = (event: NativeSyntheticEvent) => void; @@ -619,8 +623,6 @@ type BaseListItemProps = CommonListItemProps & testID?: string; /** Whether to show the default right hand side checkmark */ shouldUseDefaultRightHandSideCheckmark?: boolean; - /** Whether to expose this row as a radio option for screen readers (single-choice group). Set by RadioListItem. */ - shouldUseRadioRole?: boolean; }; type UserListItemProps = ListItemProps &