From 04eb05d068d51a80be12cc70d4739169d5fd7099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Wed, 17 Jun 2026 15:50:07 +0200 Subject: [PATCH 1/5] refactor: migrate accessibility props to aria props --- src/components/ActivityIndicator.tsx | 4 +- src/components/Appbar/AppbarAction.tsx | 6 +- src/components/Appbar/AppbarBackAction.tsx | 6 +- src/components/Appbar/AppbarContent.tsx | 22 +-- src/components/Banner.tsx | 4 +- .../BottomNavigation/BottomNavigation.tsx | 2 +- .../BottomNavigation/BottomNavigationBar.tsx | 16 ++- src/components/Button/Button.tsx | 15 +- src/components/Checkbox/Checkbox.tsx | 14 +- src/components/Checkbox/CheckboxItem.tsx | 14 +- src/components/Chip/Chip.tsx | 23 ++-- .../DataTable/DataTablePagination.tsx | 22 +-- src/components/Dialog/DialogTitle.tsx | 2 +- src/components/Drawer/DrawerCollapsedItem.tsx | 13 +- src/components/Drawer/DrawerItem.tsx | 10 +- src/components/FAB/Extended.tsx | 36 +++-- src/components/FAB/FAB.tsx | 34 +++-- src/components/FAB/Menu.tsx | 33 +++-- src/components/FAB/Shell.tsx | 36 +++-- src/components/IconButton/IconButton.tsx | 13 +- src/components/List/ListAccordion.tsx | 10 +- src/components/Menu/Menu.tsx | 6 +- src/components/Menu/MenuItem.tsx | 39 ++++-- src/components/Modal.tsx | 8 +- src/components/ProgressBar.tsx | 12 +- .../RadioButton/RadioButtonAndroid.tsx | 7 +- .../RadioButton/RadioButtonGroup.tsx | 2 +- src/components/RadioButton/RadioButtonIOS.tsx | 7 +- .../RadioButton/RadioButtonItem.tsx | 14 +- src/components/Searchbar.tsx | 14 +- .../SegmentedButtons/SegmentedButtonItem.tsx | 11 +- src/components/Snackbar.tsx | 6 +- src/components/Switch/Switch.tsx | 14 +- src/components/TextInput/TextInput.tsx | 2 +- src/components/TextInput/utils.ts | 9 +- src/components/ToggleButton/ToggleButton.tsx | 9 +- src/components/Tooltip/Tooltip.tsx | 2 +- .../__tests__/Appbar/Appbar.test.tsx | 22 +-- .../Appbar/__snapshots__/Appbar.test.tsx.snap | 21 +-- .../__tests__/Checkbox/CheckboxItem.test.tsx | 2 +- .../__snapshots__/Checkbox.test.tsx.snap | 14 +- .../__snapshots__/CheckboxItem.test.tsx.snap | 4 +- src/components/__tests__/DataTable.test.tsx | 6 +- src/components/__tests__/FAB.test.tsx | 6 +- src/components/__tests__/FABExtended.test.tsx | 4 +- src/components/__tests__/MenuItem.test.tsx | 8 +- src/components/__tests__/ProgressBar.test.tsx | 7 +- .../__snapshots__/RadioButton.test.tsx.snap | 8 +- .../RadioButtonGroup.test.tsx.snap | 4 +- .../RadioButtonItem.test.tsx.snap | 16 +-- src/components/__tests__/Switch.test.tsx | 23 ++-- src/components/__tests__/TextInput.test.tsx | 6 +- .../ActivityIndicator.test.tsx.snap | 32 ++--- .../__snapshots__/Banner.test.tsx.snap | 32 ++--- .../BottomNavigation.test.tsx.snap | 130 +++++++++--------- .../__snapshots__/Button.test.tsx.snap | 36 +++-- .../__snapshots__/Chip.test.tsx.snap | 16 +-- .../__snapshots__/DataTable.test.tsx.snap | 72 ++++------ .../__snapshots__/DrawerItem.test.tsx.snap | 6 +- .../__tests__/__snapshots__/FAB.test.tsx.snap | 28 ++-- .../__snapshots__/FABExtended.test.tsx.snap | 24 ++-- .../__snapshots__/FABMenu.test.tsx.snap | 86 ++++++------ .../__snapshots__/IconButton.test.tsx.snap | 25 +--- .../__snapshots__/ListAccordion.test.tsx.snap | 10 +- .../__snapshots__/ListItem.test.tsx.snap | 2 +- .../__snapshots__/Menu.test.tsx.snap | 22 +-- .../__snapshots__/MenuItem.test.tsx.snap | 10 +- .../__snapshots__/ProgressBar.test.tsx.snap | 63 +++------ .../__snapshots__/Searchbar.test.tsx.snap | 34 ++--- .../SegmentedButton.test.tsx.snap | 4 +- .../__snapshots__/Snackbar.test.tsx.snap | 10 +- .../__snapshots__/Switch.test.tsx.snap | 12 +- .../__snapshots__/TextInput.test.tsx.snap | 80 +++-------- .../__snapshots__/ToggleButton.test.tsx.snap | 22 +-- 74 files changed, 641 insertions(+), 763 deletions(-) diff --git a/src/components/ActivityIndicator.tsx b/src/components/ActivityIndicator.tsx index 9436979208..5b1d8ff77e 100644 --- a/src/components/ActivityIndicator.tsx +++ b/src/components/ActivityIndicator.tsx @@ -146,8 +146,8 @@ const ActivityIndicator = ({ style={[styles.container, style]} {...rest} accessible - accessibilityRole="progressbar" - accessibilityState={{ busy: animating }} + role="progressbar" + aria-busy={animating} > & { /** * Accessibility label for the button. This is read by the screen reader when the user taps the button. */ - accessibilityLabel?: string; + 'aria-label'?: string; /** * Function to execute on press. */ @@ -79,7 +79,7 @@ const AppbarAction = ({ icon, disabled, onPress, - accessibilityLabel, + 'aria-label': ariaLabel, isLeading, theme: themeOverrides, ref, @@ -101,7 +101,7 @@ const AppbarAction = ({ iconColor={actionIconColor} icon={icon} disabled={disabled} - accessibilityLabel={accessibilityLabel} + aria-label={ariaLabel} animated ref={ref} {...rest} diff --git a/src/components/Appbar/AppbarBackAction.tsx b/src/components/Appbar/AppbarBackAction.tsx index 80c7df2f95..2835c27c91 100644 --- a/src/components/Appbar/AppbarBackAction.tsx +++ b/src/components/Appbar/AppbarBackAction.tsx @@ -31,7 +31,7 @@ export type Props = $Omit< /** * Accessibility label for the button. This is read by the screen reader when the user taps the button. */ - accessibilityLabel?: string; + 'aria-label'?: string; /** * Function to execute on press. */ @@ -58,12 +58,12 @@ export type Props = $Omit< * ``` */ const AppbarBackAction = ({ - accessibilityLabel = 'Back', + 'aria-label': ariaLabel = 'Back', ref, ...rest }: Props) => ( @@ -164,11 +155,8 @@ const AppbarContent = ({ return ( // eslint-disable-next-line react-native-a11y/has-accessibility-props {children} diff --git a/src/components/BottomNavigation/BottomNavigation.tsx b/src/components/BottomNavigation/BottomNavigation.tsx index 1f18b17aae..29fab333d3 100644 --- a/src/components/BottomNavigation/BottomNavigation.tsx +++ b/src/components/BottomNavigation/BottomNavigation.tsx @@ -523,7 +523,7 @@ const BottomNavigation = ({ ({ ), getLabelText = ({ route }: { route: Route }) => route.title, getBadge = ({ route }: { route: Route }) => route.badge, - getAccessibilityLabel = ({ route }: { route: Route }) => - route.accessibilityLabel, + getAccessibilityLabel = ({ route }: { route: Route }) => route['aria-label'], getTestID = ({ route }: { route: Route }) => route.testID, activeColor, inactiveColor, @@ -507,7 +509,7 @@ const BottomNavigationBar = ({ maxWidth: maxTabBarWidth, }, ]} - accessibilityRole={'tablist'} + role={'tablist'} testID={`${testID}-content-wrapper`} > {routes.map((route, index) => { @@ -585,9 +587,9 @@ const BottomNavigationBar = ({ onPress: () => onTabPress(eventForIndex(index)), onLongPress: () => onTabLongPress?.(eventForIndex(index)), testID: getTestID({ route }), - accessibilityLabel: getAccessibilityLabel({ route }), - accessibilityRole: Platform.OS === 'ios' ? 'button' : 'tab', - accessibilityState: { selected: focused }, + 'aria-label': getAccessibilityLabel({ route }), + role: Platform.OS === 'ios' ? 'button' : 'tab', + 'aria-selected': focused, style: [styles.item, styles.v3Item], children: ( , 'mode'> & { /** * Accessibility label for the button. This is read by the screen reader when the user taps the button. */ - accessibilityLabel?: string; + 'aria-label'?: string; /** * Accessibility hint for the button. This is read by the screen reader when the user taps the button. */ @@ -88,7 +87,7 @@ export type Props = $Omit, 'mode'> & { /** * Accessibility role for the button. The "button" role is set by default. */ - accessibilityRole?: AccessibilityRole; + role?: string; /** * Function to execute on press. */ @@ -169,9 +168,9 @@ const Button = ({ buttonColor: customButtonColor, textColor: customTextColor, children, - accessibilityLabel, + 'aria-label': ariaLabel, accessibilityHint, - accessibilityRole = 'button', + role = 'button', hitSlop, onPress, onPressIn, @@ -352,10 +351,10 @@ const Button = ({ onPressIn={hasPassedTouchHandler ? handlePressIn : undefined} onPressOut={hasPassedTouchHandler ? handlePressOut : undefined} delayLongPress={delayLongPress} - accessibilityLabel={accessibilityLabel} + aria-label={ariaLabel} accessibilityHint={accessibilityHint} - accessibilityRole={accessibilityRole} - accessibilityState={{ disabled }} + role={role} + aria-disabled={disabled} accessible={accessible} hitSlop={hitSlop} disabled={disabled} diff --git a/src/components/Checkbox/Checkbox.tsx b/src/components/Checkbox/Checkbox.tsx index 7173e64501..021c3af904 100644 --- a/src/components/Checkbox/Checkbox.tsx +++ b/src/components/Checkbox/Checkbox.tsx @@ -247,14 +247,12 @@ const Checkbox = ({ rest.accessible === false ? {} : { - accessibilityRole: 'checkbox' as const, - accessibilityState: { - disabled: !!disabled, - checked: (status === 'indeterminate' - ? 'mixed' - : status === 'checked') as boolean | 'mixed', - }, - accessibilityLiveRegion: 'polite' as const, + role: 'checkbox' as const, + 'aria-disabled': !!disabled, + 'aria-checked': (status === 'indeterminate' + ? 'mixed' + : status === 'checked') as boolean | 'mixed', + 'aria-live': 'polite' as const, }; return ( diff --git a/src/components/Checkbox/CheckboxItem.tsx b/src/components/Checkbox/CheckboxItem.tsx index 50e758a412..e8bd23a59b 100644 --- a/src/components/Checkbox/CheckboxItem.tsx +++ b/src/components/Checkbox/CheckboxItem.tsx @@ -44,7 +44,7 @@ export type Props = { /** * Accessibility label for the touchable. This is read by the screen reader when the user taps the touchable. */ - accessibilityLabel?: string; + 'aria-label'?: string; /** * Custom color for unchecked checkbox. */ @@ -129,7 +129,7 @@ const CheckboxItem = ({ theme: themeOverrides, testID, position = 'trailing', - accessibilityLabel = label, + 'aria-label': ariaLabel = label, disabled, labelVariant = 'bodyLarge', labelMaxFontSizeMultiplier = 1.5, @@ -154,12 +154,10 @@ const CheckboxItem = ({ return ( , 'mode'> & { /** * Accessibility label for the chip. This is read by the screen reader when the user taps the chip. */ - accessibilityLabel?: string; + 'aria-label'?: string; /** * Accessibility label for the close icon. This is read by the screen reader when the user taps the close icon. */ @@ -178,8 +177,8 @@ const Chip = ({ selected = false, disabled = false, background, - accessibilityLabel, - accessibilityRole = 'button', + 'aria-label': ariaLabel, + role = 'button', closeIconAccessibilityLabel = 'Close', onPress, onLongPress, @@ -263,11 +262,6 @@ const Chip = ({ disabled, }); - const accessibilityState: AccessibilityState = { - selected, - disabled, - }; - const elevationStyle = elevation; const multiplier = compact ? 1.5 : 2; const labelSpacings = { @@ -312,9 +306,10 @@ const Chip = ({ onPressOut={hasPassedTouchHandler ? handlePressOut : undefined} delayLongPress={delayLongPress} disabled={disabled} - accessibilityLabel={accessibilityLabel} - accessibilityRole={accessibilityRole} - accessibilityState={accessibilityState} + aria-label={ariaLabel} + role={role} + aria-selected={selected} + aria-disabled={disabled} testID={testID} theme={theme} hitSlop={hitSlop} @@ -401,8 +396,8 @@ const Chip = ({ {closeIcon ? ( diff --git a/src/components/DataTable/DataTablePagination.tsx b/src/components/DataTable/DataTablePagination.tsx index 862185e9c5..32f4b8b4fb 100644 --- a/src/components/DataTable/DataTablePagination.tsx +++ b/src/components/DataTable/DataTablePagination.tsx @@ -29,7 +29,7 @@ export type Props = ViewProps & /** * AccessibilityLabel for `label`. */ - accessibilityLabel?: string; + 'aria-label'?: string; style?: StyleProp; /** * @optional @@ -106,7 +106,7 @@ const PaginationControls = ({ iconColor={textColor} disabled={page === 0} onPress={() => onPageChange(0)} - accessibilityLabel="page-first" + aria-label="page-first" theme={theme} /> ) : null} @@ -122,7 +122,7 @@ const PaginationControls = ({ iconColor={textColor} disabled={page === 0} onPress={() => onPageChange(page - 1)} - accessibilityLabel="chevron-left" + aria-label="chevron-left" theme={theme} /> onPageChange(page + 1)} - accessibilityLabel="chevron-right" + aria-label="chevron-right" theme={theme} /> {showFastPaginationControls ? ( @@ -153,7 +153,7 @@ const PaginationControls = ({ iconColor={textColor} disabled={numberOfPages === 0 || page === numberOfPages - 1} onPress={() => onPageChange(numberOfPages - 1)} - accessibilityLabel="page-last" + aria-label="page-last" theme={theme} /> ) : null} @@ -266,7 +266,7 @@ const PaginationDropdown = ({ */ const DataTablePagination = ({ label, - accessibilityLabel, + 'aria-label': accessibilityLabel, page, numberOfPages, onPageChange, @@ -287,19 +287,21 @@ const DataTablePagination = ({ {numberOfItemsPerPageList && numberOfItemsPerPage && onItemsPerPageChange && ( {label} diff --git a/src/components/Dialog/DialogTitle.tsx b/src/components/Dialog/DialogTitle.tsx index f869887205..bdf5021379 100644 --- a/src/components/Dialog/DialogTitle.tsx +++ b/src/components/Dialog/DialogTitle.tsx @@ -63,7 +63,7 @@ const DialogTitle = ({ return ( diff --git a/src/components/Drawer/DrawerCollapsedItem.tsx b/src/components/Drawer/DrawerCollapsedItem.tsx index 3f79fce421..66346e92bb 100644 --- a/src/components/Drawer/DrawerCollapsedItem.tsx +++ b/src/components/Drawer/DrawerCollapsedItem.tsx @@ -54,7 +54,7 @@ export type Props = ViewProps & { /** * Accessibility label for the button. This is read by the screen reader when the user taps the button. */ - accessibilityLabel?: string; + 'aria-label'?: string; style?: StyleProp; /** * @optional @@ -102,7 +102,7 @@ const DrawerCollapsedItem = ({ style, onPress, disabled, - accessibilityLabel, + 'aria-label': ariaLabel, badge = false, testID = 'drawer-collapsed-item', labelMaxFontSizeMultiplier, @@ -169,12 +169,9 @@ const DrawerCollapsedItem = ({ onPress={onPress} onPressOut={onPress ? handlePressOut : undefined} disabled={disabled} - // @ts-expect-error We keep old a11y props for backwards compat with old RN versions - accessibilityTraits={active ? ['button', 'selected'] : 'button'} - accessibilityComponentType="button" - accessibilityRole="button" - accessibilityState={{ selected: active }} - accessibilityLabel={accessibilityLabel} + role="button" + aria-selected={active} + aria-label={ariaLabel} testID={testID} > diff --git a/src/components/Drawer/DrawerItem.tsx b/src/components/Drawer/DrawerItem.tsx index 95ef989248..88535ba162 100644 --- a/src/components/Drawer/DrawerItem.tsx +++ b/src/components/Drawer/DrawerItem.tsx @@ -46,7 +46,7 @@ export type Props = ViewProps & { /** * Accessibility label for the button. This is read by the screen reader when the user taps the button. */ - accessibilityLabel?: string; + 'aria-label'?: string; /** * Callback which returns a React element to display on the right side. For instance a Badge. */ @@ -94,7 +94,7 @@ const DrawerItem = ({ style, onPress, background, - accessibilityLabel, + 'aria-label': ariaLabel, right, labelMaxFontSizeMultiplier, hitSlop, @@ -124,9 +124,9 @@ const DrawerItem = ({ { backgroundColor, borderRadius }, style, ]} - accessibilityRole="button" - accessibilityState={{ selected: active }} - accessibilityLabel={accessibilityLabel} + role="button" + aria-selected={active} + aria-label={ariaLabel} theme={theme} hitSlop={hitSlop} > diff --git a/src/components/FAB/Extended.tsx b/src/components/FAB/Extended.tsx index d15fe6a6fa..06a67cab1f 100644 --- a/src/components/FAB/Extended.tsx +++ b/src/components/FAB/Extended.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { StyleSheet, View } from 'react-native'; import type { - AccessibilityState, ColorValue, GestureResponderEvent, PressableAndroidRippleConfig, @@ -71,11 +70,24 @@ export type Props = { /** * Accessibility label. Falls back to `label` if unset. */ - accessibilityLabel?: string; + 'aria-label'?: string; /** - * Accessibility state forwarded to the underlying button. + * Indicates whether the element is checked. Accepts `true`, `false`, + * or `'mixed'` for an indeterminate state. */ - accessibilityState?: AccessibilityState; + 'aria-checked'?: boolean | 'mixed'; + /** + * Indicates whether the element is selected. + */ + 'aria-selected'?: boolean; + /** + * Indicates whether the element is currently busy (e.g. loading). + */ + 'aria-busy'?: boolean; + /** + * Indicates whether the element's controlled content is expanded. + */ + 'aria-expanded'?: boolean; /** * Specifies the largest possible scale a label font can reach. */ @@ -148,8 +160,11 @@ const Extended = ({ expanded, visible = true, onPress, - accessibilityLabel = label, - accessibilityState, + 'aria-label': ariaLabel = label, + 'aria-checked': ariaChecked, + 'aria-selected': ariaSelected, + 'aria-busy': ariaBusy, + 'aria-expanded': ariaExpanded, labelMaxFontSizeMultiplier, background, style, @@ -239,8 +254,11 @@ const Extended = ({ size={size} visible={visible} onPress={onPress} - accessibilityLabel={accessibilityLabel} - accessibilityState={accessibilityState} + aria-label={ariaLabel} + aria-checked={ariaChecked} + aria-selected={ariaSelected} + aria-busy={ariaBusy} + aria-expanded={ariaExpanded} background={background} widthShared={widthValue} labelMaxFontSizeMultiplier={labelMaxFontSizeMultiplier} @@ -253,7 +271,7 @@ const Extended = ({ ref={offscreenLabelRef} style={styles.offscreenMeasure} importantForAccessibility="no-hide-descendants" - accessibilityElementsHidden + aria-hidden > void; - accessibilityLabel?: string; + /** + * Accessibility label for the trigger FAB. + */ + 'aria-label'?: string; testID?: string; }; @@ -206,7 +209,7 @@ const AnimatedItem = ({ expanded ? styles.pointerEventsAuto : styles.pointerEventsNone, ]} importantForAccessibility={expanded ? 'yes' : 'no-hide-descendants'} - accessibilityElementsHidden={!expanded} + aria-hidden={!expanded} > {children} @@ -219,7 +222,10 @@ type ItemProps = { variant: Variant; theme: InternalTheme; onPress: (e: GestureResponderEvent) => void; - accessibilityLabel?: string; + /** + * Accessibility label. Falls back to `label`. + */ + 'aria-label'?: string; testID?: string; }; @@ -235,7 +241,7 @@ const MenuItem = ({ variant, theme, onPress, - accessibilityLabel, + 'aria-label': ariaLabel, testID, }: ItemProps) => { const colors = resolveColors({ theme, variant }); @@ -261,8 +267,8 @@ const MenuItem = ({ onPress={onPress} onFocus={onFocus} onBlur={onBlur} - accessibilityRole="button" - accessibilityLabel={accessibilityLabel ?? label} + role="button" + aria-label={ariaLabel ?? label} style={[ { borderRadius }, Platform.OS === 'web' ? webNoOutline : null, @@ -309,7 +315,10 @@ type MorphingTriggerProps = { visible: boolean; alignment: 'start' | 'center' | 'end'; onPress?: (e: GestureResponderEvent) => void; - accessibilityLabel?: string; + /** + * Accessibility label for the trigger button. + */ + 'aria-label'?: string; theme: InternalTheme; testID?: string; }; @@ -326,7 +335,7 @@ const MorphingTrigger = ({ visible, alignment, onPress, - accessibilityLabel, + 'aria-label': ariaLabel, theme, testID, }: MorphingTriggerProps) => { @@ -446,7 +455,7 @@ const MorphingTrigger = ({ contentColor={triggerContentColor} visible={visible} onPress={onPress} - accessibilityLabel={accessibilityLabel} + aria-label={ariaLabel} widthShared={widthShared} heightShared={heightShared} borderRadiusShared={borderRadiusShared} @@ -616,7 +625,7 @@ const Menu = ({ label={item.label} variant={itemsVariant} theme={theme} - accessibilityLabel={item.accessibilityLabel ?? item.label} + aria-label={item['aria-label'] ?? item.label} onPress={handleItemPress(item)} testID={item.testID} /> @@ -636,7 +645,7 @@ const Menu = ({ visible={triggerVisible} alignment={alignment} onPress={effectiveExpanded ? onDismiss : openOnPress} - accessibilityLabel={trigger.accessibilityLabel} + aria-label={trigger['aria-label']} theme={theme} testID={trigger.testID} /> diff --git a/src/components/FAB/Shell.tsx b/src/components/FAB/Shell.tsx index 4f9b9b5da9..2d2d60e4cd 100644 --- a/src/components/FAB/Shell.tsx +++ b/src/components/FAB/Shell.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { Platform, StyleSheet, View } from 'react-native'; import type { - AccessibilityState, ColorValue, GestureResponderEvent, PressableAndroidRippleConfig, @@ -92,11 +91,24 @@ export type ShellProps = { /** * Accessibility label. Falls back to `label` if unset. */ - accessibilityLabel?: string; + 'aria-label'?: string; /** - * Accessibility state forwarded to the underlying button. + * Indicates whether the element is checked. Accepts `true`, `false`, + * or `'mixed'` for an indeterminate state. */ - accessibilityState?: AccessibilityState; + 'aria-checked'?: boolean | 'mixed'; + /** + * Indicates whether the element is selected. + */ + 'aria-selected'?: boolean; + /** + * Indicates whether the element is currently busy (e.g. loading). + */ + 'aria-busy'?: boolean; + /** + * Indicates whether the element's controlled content is expanded. + */ + 'aria-expanded'?: boolean; /** * Largest scale the label font can reach (auto-built content only). */ @@ -182,8 +194,11 @@ const Shell = ({ elevation = Tokens.stateElevation.enabled, visible = true, onPress, - accessibilityLabel = label, - accessibilityState, + 'aria-label': ariaLabel = label, + 'aria-checked': ariaChecked, + 'aria-selected': ariaSelected, + 'aria-busy': ariaBusy, + 'aria-expanded': ariaExpanded, labelMaxFontSizeMultiplier, labelAnimatedStyle, background, @@ -290,9 +305,12 @@ const Shell = ({ onPress={onPress} onFocus={onFocus} onBlur={onBlur} - accessibilityLabel={accessibilityLabel} - accessibilityRole="button" - accessibilityState={accessibilityState} + aria-label={ariaLabel} + role="button" + aria-checked={ariaChecked} + aria-selected={ariaSelected} + aria-busy={ariaBusy} + aria-expanded={ariaExpanded} testID={testID} style={[ children ? styles.fill : null, diff --git a/src/components/IconButton/IconButton.tsx b/src/components/IconButton/IconButton.tsx index 899d1e5a46..52a64e98d0 100644 --- a/src/components/IconButton/IconButton.tsx +++ b/src/components/IconButton/IconButton.tsx @@ -59,7 +59,7 @@ export type Props = Omit<$RemoveChildren, 'style'> & { /** * Accessibility label for the button. This is read by the screen reader when the user taps the button. */ - accessibilityLabel?: string; + 'aria-label'?: string; /** * Style of button's inner content. * Use this prop to apply custom height and width or to set a custom padding`. @@ -112,7 +112,7 @@ const IconButton = ({ iconColor: customIconColor, containerColor: customContainerColor, size = 24, - accessibilityLabel, + 'aria-label': ariaLabel, disabled, onPress, selected = false, @@ -188,13 +188,10 @@ const IconButton = ({ borderless centered onPress={onPress} - accessibilityLabel={accessibilityLabel} + aria-label={ariaLabel} style={[styles.touchable, contentStyle]} - // @ts-expect-error We keep old a11y props for backwards compat with old RN versions - accessibilityTraits={disabled ? ['button', 'disabled'] : 'button'} - accessibilityComponentType="button" - accessibilityRole="button" - accessibilityState={{ disabled }} + role="button" + aria-disabled={disabled} disabled={disabled} hitSlop={ TouchableRipple.supported diff --git a/src/components/List/ListAccordion.tsx b/src/components/List/ListAccordion.tsx index 5dbca2ee60..97730d58e6 100644 --- a/src/components/List/ListAccordion.tsx +++ b/src/components/List/ListAccordion.tsx @@ -120,7 +120,7 @@ export type Props = { /** * Accessibility label for the TouchableRipple. This is read by the screen reader when the user taps the touchable. */ - accessibilityLabel?: string; + 'aria-label'?: string; /** * `pointerEvents` passed to the `View` container */ @@ -190,7 +190,7 @@ const ListAccordion = ({ onLongPress, delayLongPress, expanded: expandedProp, - accessibilityLabel, + 'aria-label': ariaLabel, pointerEvents = 'none', titleMaxFontSizeMultiplier, descriptionMaxFontSizeMultiplier, @@ -249,9 +249,9 @@ const ListAccordion = ({ onPress={handlePress} onLongPress={onLongPress} delayLongPress={delayLongPress} - accessibilityRole="button" - accessibilityState={{ expanded: isExpanded }} - accessibilityLabel={accessibilityLabel} + role="button" + aria-expanded={isExpanded} + aria-label={ariaLabel} testID={testID} theme={theme} background={background} diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx index 4c3d6fe65e..97fc7748cd 100644 --- a/src/components/Menu/Menu.tsx +++ b/src/components/Menu/Menu.tsx @@ -642,8 +642,8 @@ const Menu = ({ {rendered ? ( diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index a8412da806..3c66c885b3 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -182,15 +182,15 @@ function Modal({ return ( diff --git a/src/components/RadioButton/RadioButtonAndroid.tsx b/src/components/RadioButton/RadioButtonAndroid.tsx index a907b46b09..5fcaf9f6e5 100644 --- a/src/components/RadioButton/RadioButtonAndroid.tsx +++ b/src/components/RadioButton/RadioButtonAndroid.tsx @@ -134,9 +134,10 @@ const RadioButtonAndroid = ({ }); } } - accessibilityRole="radio" - accessibilityState={{ disabled, checked }} - accessibilityLiveRegion="polite" + role="radio" + aria-disabled={disabled} + aria-checked={checked} + aria-live="polite" style={styles.container} testID={testID} theme={theme} diff --git a/src/components/RadioButton/RadioButtonGroup.tsx b/src/components/RadioButton/RadioButtonGroup.tsx index 2bdd596561..8522bf3fc4 100644 --- a/src/components/RadioButton/RadioButtonGroup.tsx +++ b/src/components/RadioButton/RadioButtonGroup.tsx @@ -56,7 +56,7 @@ export const RadioButtonContext = React.createContext( */ const RadioButtonGroup = ({ value, onValueChange, children }: Props) => ( - {children} + {children} ); diff --git a/src/components/RadioButton/RadioButtonIOS.tsx b/src/components/RadioButton/RadioButtonIOS.tsx index e2e5f06c7c..015356eea3 100644 --- a/src/components/RadioButton/RadioButtonIOS.tsx +++ b/src/components/RadioButton/RadioButtonIOS.tsx @@ -92,9 +92,10 @@ const RadioButtonIOS = ({ }); } } - accessibilityRole="radio" - accessibilityState={{ disabled, checked }} - accessibilityLiveRegion="polite" + role="radio" + aria-disabled={disabled} + aria-checked={checked} + aria-live="polite" style={styles.container} testID={testID} theme={theme} diff --git a/src/components/RadioButton/RadioButtonItem.tsx b/src/components/RadioButton/RadioButtonItem.tsx index 1591760980..e37ed435e2 100644 --- a/src/components/RadioButton/RadioButtonItem.tsx +++ b/src/components/RadioButton/RadioButtonItem.tsx @@ -49,7 +49,7 @@ export type Props = { /** * Accessibility label for the touchable. This is read by the screen reader when the user taps the touchable. */ - accessibilityLabel?: string; + 'aria-label'?: string; /** * Custom color for unchecked radio. */ @@ -149,7 +149,7 @@ const RadioButtonItem = ({ status, theme: themeOverrides, background, - accessibilityLabel = label, + 'aria-label': ariaLabel = label, testID, mode, position = 'trailing', @@ -204,12 +204,10 @@ const RadioButtonItem = ({ }) } onLongPress={onLongPress} - accessibilityLabel={accessibilityLabel} - accessibilityRole="radio" - accessibilityState={{ - checked, - disabled, - }} + aria-label={ariaLabel} + role="radio" + aria-checked={checked} + aria-disabled={disabled} testID={testID} disabled={disabled} background={background} diff --git a/src/components/Searchbar.tsx b/src/components/Searchbar.tsx index a081b62fc4..3cc733e126 100644 --- a/src/components/Searchbar.tsx +++ b/src/components/Searchbar.tsx @@ -245,7 +245,7 @@ const Searchbar = ({ theme={theme} > )} {shouldRenderTraileringIcon ? ( ) : null} diff --git a/src/components/SegmentedButtons/SegmentedButtonItem.tsx b/src/components/SegmentedButtons/SegmentedButtonItem.tsx index 0d30d8aee4..4f701f0b48 100644 --- a/src/components/SegmentedButtons/SegmentedButtonItem.tsx +++ b/src/components/SegmentedButtons/SegmentedButtonItem.tsx @@ -52,7 +52,7 @@ export type Props = { /** * Accessibility label for the `SegmentedButtonItem`. This is read by the screen reader when the user taps the button. */ - accessibilityLabel?: string; + 'aria-label'?: string; /** * Function to execute on press. */ @@ -102,7 +102,7 @@ export type Props = { const SegmentedButtonItem = ({ checked, - accessibilityLabel, + 'aria-label': ariaLabel, disabled, style, labelStyle, @@ -195,9 +195,10 @@ const SegmentedButtonItem = ({ diff --git a/src/components/Switch/Switch.tsx b/src/components/Switch/Switch.tsx index 1eb0c23de6..0bcc53083b 100644 --- a/src/components/Switch/Switch.tsx +++ b/src/components/Switch/Switch.tsx @@ -57,7 +57,10 @@ export type Props = { style?: StyleProp; testID?: string; theme?: ThemeProp; - accessibilityLabel?: string; + /** + * Accessibility label for the switch. This is read by the screen reader when the user focuses the switch. + */ + 'aria-label'?: string; }; const { @@ -132,7 +135,7 @@ const Switch = ({ style, testID, theme: themeOverrides, - accessibilityLabel, + 'aria-label': ariaLabel, }: Props) => { const theme = useInternalTheme(themeOverrides); const reduceMotion = useReduceMotion(); @@ -367,9 +370,10 @@ const Switch = ({ focusedSV.value = 0; }} android_ripple={{ color: 'transparent' }} - accessibilityRole="switch" - accessibilityState={{ disabled: isDisabled, checked }} - accessibilityLabel={accessibilityLabel} + role="switch" + aria-disabled={isDisabled} + aria-checked={checked} + aria-label={ariaLabel} testID={testID} style={[ styles.touchable, diff --git a/src/components/TextInput/TextInput.tsx b/src/components/TextInput/TextInput.tsx index 806dd4585a..5a7a65d974 100644 --- a/src/components/TextInput/TextInput.tsx +++ b/src/components/TextInput/TextInput.tsx @@ -57,7 +57,7 @@ export type TextInputColors = { }; export type GetAccessibilityDataReturn = { - input: AccessibilityProps; + input: AccessibilityProps & { 'aria-invalid'?: boolean }; supportingText: AccessibilityProps; counter: AccessibilityProps; }; diff --git a/src/components/TextInput/utils.ts b/src/components/TextInput/utils.ts index e8da1a4de3..ad81f55301 100644 --- a/src/components/TextInput/utils.ts +++ b/src/components/TextInput/utils.ts @@ -611,19 +611,14 @@ export const getAccessibilityData = ({ : `Characters entered ${inputLength} of ${maxLength}` : undefined; - const accessibilityState = { - disabled: isDisabled, - invalid: isInvalid, - ...props.accessibilityState, - } as const; - return { input: { 'aria-label': ariaLabel, 'aria-valuemax': isCounterReached ? maxLength : undefined, 'aria-valuenow': isCounterReached ? inputLength : undefined, + 'aria-disabled': isDisabled, + 'aria-invalid': isInvalid, accessibilityHint: hint, - accessibilityState, }, supportingText: { 'aria-hidden': isSupportingTextHidden, diff --git a/src/components/ToggleButton/ToggleButton.tsx b/src/components/ToggleButton/ToggleButton.tsx index 8d4cdd556f..22598c16b2 100644 --- a/src/components/ToggleButton/ToggleButton.tsx +++ b/src/components/ToggleButton/ToggleButton.tsx @@ -29,7 +29,7 @@ export type Props = { /** * Accessibility label for the `ToggleButton`. This is read by the screen reader when the user taps the button. */ - accessibilityLabel?: string; + 'aria-label'?: string; /** * Function to execute on press. */ @@ -88,7 +88,7 @@ const ToggleButton = ({ icon, size, theme: themeOverrides, - accessibilityLabel, + 'aria-label': ariaLabel, disabled, style, value, @@ -123,8 +123,9 @@ const ToggleButton = ({ } }} size={size} - accessibilityLabel={accessibilityLabel} - accessibilityState={{ disabled, selected: checked }} + aria-label={ariaLabel} + aria-disabled={disabled} + aria-selected={checked} disabled={disabled} style={[ styles.content, diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx index 36dc08971b..8d237d6991 100644 --- a/src/components/Tooltip/Tooltip.tsx +++ b/src/components/Tooltip/Tooltip.tsx @@ -215,7 +215,7 @@ const Tooltip = ({ testID="tooltip-container" > { ); }); - it('Is recognized as a header when no onPress callback has been pressed', () => { - const { getByRole } = render( + it('Is recognized as a heading when no onPress callback has been passed', () => { + const { getByTestId } = render( @@ -155,7 +155,7 @@ describe('renderAppbarContent', () => { ); - expect(getByRole('header')).toBeTruthy(); + expect(getByTestId('appbar-content-title-text').props.role).toBe('heading'); }); it('is recognized as a button when onPress callback has been passed', () => { const { getByTestId } = render( @@ -166,15 +166,11 @@ describe('renderAppbarContent', () => { ); - expect(getByTestId('appbar-content').props.accessibilityRole).toEqual( - 'button' - ); + expect(getByTestId('appbar-content').props.role).toEqual('button'); expect( getByTestId('appbar-content').props.accessibilityState || {} ).not.toMatchObject({ disabled: true }); - expect( - getByTestId('appbar-content-title-text').props.accessibilityRole - ).toEqual('none'); + expect(getByTestId('appbar-content-title-text').props.role).toEqual('none'); }); it('is recognized as a disabled button when onPress and disabled is passed', () => { const { getByTestId } = render( @@ -185,15 +181,11 @@ describe('renderAppbarContent', () => { ); - expect(getByTestId('appbar-content').props.accessibilityRole).toEqual( - 'button' - ); + expect(getByTestId('appbar-content').props.role).toEqual('button'); expect( getByTestId('appbar-content').props.accessibilityState ).toMatchObject({ disabled: true }); - expect( - getByTestId('appbar-content-title-text').props.accessibilityRole - ).toEqual('none'); + expect(getByTestId('appbar-content-title-text').props.role).toEqual('none'); }); }); diff --git a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap index 7dbcf9f0a5..a5d9d95766 100644 --- a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap +++ b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap @@ -122,9 +122,7 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = ` testID="search-bar-icon-container" > { ); diff --git a/src/components/__tests__/Checkbox/__snapshots__/Checkbox.test.tsx.snap b/src/components/__tests__/Checkbox/__snapshots__/Checkbox.test.tsx.snap index 1b21c11b68..54f2e4f7a4 100644 --- a/src/components/__tests__/Checkbox/__snapshots__/Checkbox.test.tsx.snap +++ b/src/components/__tests__/Checkbox/__snapshots__/Checkbox.test.tsx.snap @@ -3,7 +3,6 @@ exports[`renders Checkbox with custom testID 1`] = ` { }); it('renders data table pagination with options select', () => { - const { getByLabelText, toJSON } = render( + const { getByTestId, toJSON } = render( { /> ); - expect(getByLabelText('Options Select')).toBeTruthy(); - expect(getByLabelText('selectPageDropdownLabel')).toBeTruthy(); + expect(getByTestId('options-select')).toBeTruthy(); + expect(getByTestId('select-page-dropdown-label')).toBeTruthy(); expect(toJSON()).toMatchSnapshot(); }); diff --git a/src/components/__tests__/FAB.test.tsx b/src/components/__tests__/FAB.test.tsx index 38ee6308b6..65e2f6418a 100644 --- a/src/components/__tests__/FAB.test.tsx +++ b/src/components/__tests__/FAB.test.tsx @@ -56,10 +56,8 @@ it('renders FAB with containerColor and contentColor overrides', () => { expect(tree).toMatchSnapshot(); }); -it('renders FAB with accessibilityLabel', () => { - const tree = render( - - ).toJSON(); +it('renders FAB with aria-label', () => { + const tree = render().toJSON(); expect(tree).toMatchSnapshot(); }); diff --git a/src/components/__tests__/FABExtended.test.tsx b/src/components/__tests__/FABExtended.test.tsx index e3c1490f65..689b25eb8f 100644 --- a/src/components/__tests__/FABExtended.test.tsx +++ b/src/components/__tests__/FABExtended.test.tsx @@ -61,13 +61,13 @@ it('uses label as default accessibilityLabel', () => { ); }); -it('respects explicit accessibilityLabel', () => { +it('respects explicit aria-label', () => { const { getByTestId } = render( ); diff --git a/src/components/__tests__/MenuItem.test.tsx b/src/components/__tests__/MenuItem.test.tsx index db7b30bc21..d240247bef 100644 --- a/src/components/__tests__/MenuItem.test.tsx +++ b/src/components/__tests__/MenuItem.test.tsx @@ -50,13 +50,9 @@ describe('Menu Item', () => { ); }); - it('accepts different values for accessibilityState', () => { + it('accepts aria-checked prop', () => { const { getByTestId } = render( - + ); expect(getByTestId('touchable').props.accessibilityState).toMatchObject({ diff --git a/src/components/__tests__/ProgressBar.test.tsx b/src/components/__tests__/ProgressBar.test.tsx index c3a89c8749..1759a008d9 100644 --- a/src/components/__tests__/ProgressBar.test.tsx +++ b/src/components/__tests__/ProgressBar.test.tsx @@ -22,12 +22,11 @@ const styles = StyleSheet.create({ }, }); -const a11yRole = 'progressbar'; const triggerLayout = async ( tree: ReturnType ): Promise => { await act(async () => { - tree.getByRole(a11yRole).props.onLayout(layoutEvent); + tree.getByTestId('progress-bar').props.onLayout(layoutEvent); }); }; @@ -49,7 +48,7 @@ it('renders progress bar with animated value', async () => { tree.update(); - expect(tree.getByRole(a11yRole)).toBeTruthy(); + expect(tree.getByTestId('progress-bar')).toBeTruthy(); }); it('renders progress bar with specific progress', async () => { @@ -84,7 +83,7 @@ it('renders progress bar with full height on web', () => { Platform.OS = 'web'; const tree = render(); - expect(tree.getByRole(a11yRole)).toHaveStyle({ + expect(tree.getByTestId('progress-bar')).toHaveStyle({ width: '100%', height: '100%', }); diff --git a/src/components/__tests__/RadioButton/__snapshots__/RadioButton.test.tsx.snap b/src/components/__tests__/RadioButton/__snapshots__/RadioButton.test.tsx.snap index 5560a80f36..c20910f20e 100644 --- a/src/components/__tests__/RadioButton/__snapshots__/RadioButton.test.tsx.snap +++ b/src/components/__tests__/RadioButton/__snapshots__/RadioButton.test.tsx.snap @@ -3,7 +3,6 @@ exports[`RadioButton RadioButton with custom testID renders properly 1`] = ` { describe('Switch interaction', () => { it('toggles to true when off and pressed', () => { const onValueChange = jest.fn(); - const { getByRole } = render( - + const { getByTestId } = render( + ); - fireEvent.press(getByRole('switch')); + fireEvent.press(getByTestId('switch')); expect(onValueChange).toHaveBeenCalledWith(true); }); it('toggles to false when on and pressed', () => { const onValueChange = jest.fn(); - const { getByRole } = render( - + const { getByTestId } = render( + ); - fireEvent.press(getByRole('switch')); + fireEvent.press(getByTestId('switch')); expect(onValueChange).toHaveBeenCalledWith(false); }); it('does not fire onValueChange when disabled', () => { const onValueChange = jest.fn(); - const { getByRole } = render( - + const { getByTestId } = render( + ); - fireEvent.press(getByRole('switch')); + fireEvent.press(getByTestId('switch')); expect(onValueChange).not.toHaveBeenCalled(); }); }); diff --git a/src/components/__tests__/TextInput.test.tsx b/src/components/__tests__/TextInput.test.tsx index 14ded21b21..b0dffa2c2e 100644 --- a/src/components/__tests__/TextInput.test.tsx +++ b/src/components/__tests__/TextInput.test.tsx @@ -248,7 +248,7 @@ it('uses polite aria-live on error supporting text', () => { ); expect(getByText('Invalid').props['aria-live']).toBe('polite'); - expect(getByTestId('tf-input').props.accessibilityState?.invalid).toBe(true); + expect(getByTestId('tf-input').props['aria-invalid']).toBe(true); }); it('marks the input invalid when error is true without supporting text', () => { @@ -262,7 +262,7 @@ it('marks the input invalid when error is true without supporting text', () => { /> ); - expect(getByTestId('tf-input').props.accessibilityState?.invalid).toBe(true); + expect(getByTestId('tf-input').props['aria-invalid']).toBe(true); expect(getByTestId('tf-input').props.accessibilityHint).toBeUndefined(); }); @@ -322,7 +322,7 @@ it('marks the input as disabled in accessibilityState when disabled is true', () /> ); - expect(getByTestId('tf-input').props.accessibilityState?.disabled).toBe(true); + expect(getByTestId('tf-input').props['aria-disabled']).toBe(true); }); it('renders the input via render with merged props', () => { diff --git a/src/components/__tests__/__snapshots__/ActivityIndicator.test.tsx.snap b/src/components/__tests__/__snapshots__/ActivityIndicator.test.tsx.snap index 43b4eac37e..7e684841e8 100644 --- a/src/components/__tests__/__snapshots__/ActivityIndicator.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/ActivityIndicator.test.tsx.snap @@ -2,13 +2,9 @@ exports[`renders colored indicator 1`] = `