Skip to content

Commit ed1fde7

Browse files
authored
Merge pull request #86499 from Expensify/claude-fixAccordionButtonA11yState
Add aria-expanded to ButtonWithDropdownMenu for screen reader accessibility
2 parents ae6fbc2 + 175103f commit ed1fde7

5 files changed

Lines changed: 38 additions & 1 deletion

File tree

patches/react-native/details.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,3 +252,9 @@
252252
- Upstream PR/issue: 🛑
253253
- E/App issue: https://github.com/Expensify/App/issues/85877
254254
- PR introducing patch: 🛑
255+
256+
### [react-native+0.83.1+034+fix-fabric-collapsed-accessibility-announcement.patch](react-native+0.83.1+034+fix-fabric-collapsed-accessibility-announcement.patch)
257+
258+
- Reason: Fixes a Fabric regression where VoiceOver on iOS only announces "expanded" but never "collapsed" for elements with `accessibilityState.expanded`. In `RCTViewComponentView.mm`, the code uses `value_or(false)` which skips the announcement entirely when `expanded` is `false`. This patch changes the logic to use `has_value()` and correctly announce both "expanded" and "collapsed" states, matching the old architecture (Paper) behavior.
259+
- Upstream PR/issue: https://github.com/facebook/react-native/issues/56296
260+
- E/App issue: [#76929](https://github.com/Expensify/App/issues/76929)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm
2+
--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm
3+
+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm
4+
@@ -1440,9 +1440,16 @@
5+
addObject:RCTLocalizedString(
6+
"mixed", "a checkbox, radio button, or other widget which is both checked and unchecked")];
7+
}
8+
- if (accessibilityState.expanded.value_or(false)) {
9+
- [valueComponents
10+
- addObject:RCTLocalizedString("expanded", "a menu, dialog, accordian panel, or other widget which is expanded")];
11+
+ if (accessibilityState.expanded.has_value()) {
12+
+ if (accessibilityState.expanded.value()) {
13+
+ [valueComponents
14+
+ addObject:RCTLocalizedString(
15+
+ "expanded", "a menu, dialog, accordian panel, or other widget which is expanded")];
16+
+ } else {
17+
+ [valueComponents
18+
+ addObject:RCTLocalizedString(
19+
+ "collapsed", "a menu, dialog, accordian panel, or other widget which is collapsed")];
20+
+ }
21+
}
22+
23+
if (accessibilityState.busy) {

src/components/Button/index.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {useIsFocused} from '@react-navigation/native';
22
import type {ForwardedRef} from 'react';
33
import React, {useCallback, useMemo, useState} from 'react';
4-
import type {GestureResponderEvent, LayoutChangeEvent, StyleProp, TextStyle, ViewStyle} from 'react-native';
4+
import type {AccessibilityState, GestureResponderEvent, LayoutChangeEvent, StyleProp, TextStyle, ViewStyle} from 'react-native';
55
import {StyleSheet, View} from 'react-native';
66
import ActivityIndicator from '@components/ActivityIndicator';
77
import Icon from '@components/Icon';
@@ -149,6 +149,9 @@ type ButtonProps = Partial<ChildrenProps> &
149149
/** Accessibility label for the component */
150150
accessibilityLabel?: string;
151151

152+
/** Accessibility state to pass to the pressable */
153+
accessibilityState?: AccessibilityState;
154+
152155
/** The text for the button label */
153156
text?: string;
154157

@@ -291,6 +294,7 @@ function Button({
291294
secondLineText = '',
292295
shouldBlendOpacity = false,
293296
shouldStayNormalOnDisable = false,
297+
accessibilityState,
294298
sentryLabel,
295299
ref,
296300
...rest
@@ -527,6 +531,7 @@ function Button({
527531
id={id}
528532
testID={testID}
529533
accessibilityLabel={accessibilityLabel}
534+
accessibilityState={accessibilityState}
530535
role={getButtonRole(isNested)}
531536
hoverDimmingValue={1}
532537
onHoverIn={!isDisabled || !shouldStayNormalOnDisable ? () => setIsHovered(true) : undefined}

src/components/ButtonWithDropdownMenu/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ function ButtonWithDropdownMenu<IValueType>({ref, ...props}: ButtonWithDropdownM
172172
ref={dropdownButtonRef}
173173
onPress={handlePress}
174174
text={customText ?? selectedItem?.text ?? ''}
175+
accessibilityState={!isSplitButton ? {expanded: isMenuVisible} : undefined}
175176
isDisabled={isDisabled || areAllOptionsDisabled}
176177
shouldStayNormalOnDisable={shouldStayNormalOnDisable}
177178
isLoading={isLoading}
@@ -202,6 +203,7 @@ function ButtonWithDropdownMenu<IValueType>({ref, ...props}: ButtonWithDropdownM
202203
ref={dropdownAnchor}
203204
success={success}
204205
isDisabled={isDisabled}
206+
accessibilityState={{expanded: isMenuVisible}}
205207
shouldStayNormalOnDisable={shouldStayNormalOnDisable}
206208
style={[styles.pl0]}
207209
onPress={() => setIsMenuVisible(!isMenuVisible)}

src/components/Pressable/GenericPressable/implementation/BaseGenericPressable.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ function GenericPressable({
202202
aria-disabled={isDisabled}
203203
aria-checked={accessibilityState?.checked}
204204
aria-selected={accessibilityState?.selected}
205+
aria-expanded={accessibilityState?.expanded}
205206
aria-keyshortcuts={keyboardShortcut && `${keyboardShortcut.modifiers.join('')}+${keyboardShortcut.shortcutKey}`}
206207
// ios-only form of inputs
207208
onMagicTap={!isDisabled ? voidOnPressHandler : undefined}

0 commit comments

Comments
 (0)