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 a41a9b93c161b..771aa0b8442d9 100644 --- a/src/components/SelectionList/ListItem/BaseListItem.tsx +++ b/src/components/SelectionList/ListItem/BaseListItem.tsx @@ -47,6 +47,7 @@ function BaseListItem({ shouldDisableHoverStyle, shouldStopMouseLeavePropagation = true, shouldShowRightCaret = false, + accessibilityRole = getButtonRole(true), }: BaseListItemProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -85,6 +86,9 @@ function BaseListItem({ const shouldShowHiddenCheckmark = shouldShowRBRIndicator && !shouldShowCheckmark; + const accessibilityState = + accessibilityRole === CONST.ROLE.CHECKBOX || accessibilityRole === CONST.ROLE.RADIO ? {checked: !!item.isSelected, selected: !!isFocused} : {selected: !!isFocused}; + return ( onDismissError(item)} @@ -94,6 +98,7 @@ function BaseListItem({ 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={getButtonRole(true)} + role={accessibilityRole} + accessibilityState={accessibilityState} isNested hoverDimmingValue={1} pressDimmingValue={item.isInteractive === false ? 1 : variables.pressDimValue} diff --git a/src/components/SelectionList/ListItem/MultiSelectListItem.tsx b/src/components/SelectionList/ListItem/MultiSelectListItem.tsx index 960edc9e37cd4..10bf72dab7d0e 100644 --- a/src/components/SelectionList/ListItem/MultiSelectListItem.tsx +++ b/src/components/SelectionList/ListItem/MultiSelectListItem.tsx @@ -77,6 +77,7 @@ function MultiSelectListItem({ isDisabled={isDisabled} rightHandSideComponent={checkboxComponent} 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 e1a340e3d81d8..021c45e4e5dee 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, + accessibilityRole, }: RadioListItemProps) { const styles = useThemeStyles(); const fullTitle = isMultilineSupported ? item.text?.trimStart() : item.text; @@ -51,6 +52,7 @@ function RadioListItem({ shouldHighlightSelectedItem={shouldHighlightSelectedItem} shouldDisableHoverStyle={shouldDisableHoverStyle} shouldStopMouseLeavePropagation={shouldStopMouseLeavePropagation} + accessibilityRole={accessibilityRole} > <> {!!item.leftElement && item.leftElement} diff --git a/src/components/SelectionList/ListItem/types.ts b/src/components/SelectionList/ListItem/types.ts index 8301690764467..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; diff --git a/src/components/SelectionListWithSections/BaseListItem.tsx b/src/components/SelectionListWithSections/BaseListItem.tsx index efd068359c06a..80d1b5a95ed22 100644 --- a/src/components/SelectionListWithSections/BaseListItem.tsx +++ b/src/components/SelectionListWithSections/BaseListItem.tsx @@ -48,6 +48,7 @@ function BaseListItem({ shouldHighlightSelectedItem = true, shouldDisableHoverStyle, shouldStopMouseLeavePropagation = true, + accessibilityRole = getButtonRole(true), }: BaseListItemProps) { const icons = useMemoizedLazyExpensifyIcons(['ArrowRight']); const theme = useTheme(); @@ -83,6 +84,9 @@ function BaseListItem({ const defaultAccessibilityLabel = item.text === item.alternateText ? (item.text ?? '') : [item.text, item.alternateText].filter(Boolean).join(', '); const accessibilityLabel = item.accessibilityLabel ?? defaultAccessibilityLabel; + const accessibilityState = + accessibilityRole === CONST.ROLE.CHECKBOX || accessibilityRole === CONST.ROLE.RADIO ? {checked: !!item.isSelected, selected: !!isFocused} : {selected: !!isFocused}; + return ( onDismissError(item)} @@ -92,6 +96,7 @@ function BaseListItem({ contentContainerStyle={containerStyle} > ({ disabled={isDisabled && !item.isSelected} interactive={item.isInteractive} accessibilityLabel={accessibilityLabel} - role={getButtonRole(true)} + accessibilityState={accessibilityState} + 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 e1a340e3d81d8..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,6 +52,7 @@ function RadioListItem({ shouldHighlightSelectedItem={shouldHighlightSelectedItem} shouldDisableHoverStyle={shouldDisableHoverStyle} shouldStopMouseLeavePropagation={shouldStopMouseLeavePropagation} + 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 7badcd137a8c7..67375fa310acf 100644 --- a/src/components/SelectionListWithSections/types.ts +++ b/src/components/SelectionListWithSections/types.ts @@ -6,6 +6,7 @@ import type { LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, + Role, ScrollViewProps, SectionListData, StyleProp, @@ -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; 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}