Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions patches/react-native/details.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,3 +252,10 @@
- Upstream PR/issue: 🛑
- E/App issue: https://github.com/Expensify/App/issues/85877
- PR introducing patch: 🛑


### [react-native+0.83.1+034+fix-fabric-collapsed-accessibility-announcement.patch](react-native+0.83.1+034+fix-fabric-collapsed-accessibility-announcement.patch)

- 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.
- Upstream PR/issue: 🛑

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please create a detailed upstream issue with a minimal reproduction (not this repo), and ideally a pull request as well. We can adjust the compensation accordingly, but I'm unwilling to merge a new patch to React Native without an upstream issue.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for reviewing! Created an upstream issue: react/react-native#56296. I'll try to open a PR later.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MelvinBot please include the upstream issue link react/react-native#56296 and remove the extra empty line at L256.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — added the upstream issue link (react/react-native#56296) and removed the extra blank line. Pushed in 175103f.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roryabraham, the upstream issue is still waiting for triage. Should I open a PR now?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say yes you can open a PR

@QichenZhu QichenZhu Apr 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR is open: react/react-native#56430.

Update: PR approved.

- E/App issue: [#76929](https://github.com/Expensify/App/issues/76929)
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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
--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm
+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm
@@ -1440,9 +1440,16 @@
addObject:RCTLocalizedString(
"mixed", "a checkbox, radio button, or other widget which is both checked and unchecked")];
}
- if (accessibilityState.expanded.value_or(false)) {
- [valueComponents
- addObject:RCTLocalizedString("expanded", "a menu, dialog, accordian panel, or other widget which is expanded")];
+ if (accessibilityState.expanded.has_value()) {
+ if (accessibilityState.expanded.value()) {
+ [valueComponents
+ addObject:RCTLocalizedString(
+ "expanded", "a menu, dialog, accordian panel, or other widget which is expanded")];
+ } else {
+ [valueComponents
+ addObject:RCTLocalizedString(
+ "collapsed", "a menu, dialog, accordian panel, or other widget which is collapsed")];
+ }
}

if (accessibilityState.busy) {
7 changes: 6 additions & 1 deletion src/components/Button/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {useIsFocused} from '@react-navigation/native';
import type {ForwardedRef} from 'react';
import React, {useCallback, useMemo, useState} from 'react';
import type {GestureResponderEvent, LayoutChangeEvent, StyleProp, TextStyle, ViewStyle} from 'react-native';
import type {AccessibilityState, GestureResponderEvent, LayoutChangeEvent, StyleProp, TextStyle, ViewStyle} from 'react-native';
import {StyleSheet, View} from 'react-native';
import ActivityIndicator from '@components/ActivityIndicator';
import Icon from '@components/Icon';
Expand Down Expand Up @@ -149,6 +149,9 @@ type ButtonProps = Partial<ChildrenProps> &
/** Accessibility label for the component */
accessibilityLabel?: string;

/** Accessibility state to pass to the pressable */
accessibilityState?: AccessibilityState;

/** The text for the button label */
text?: string;

Expand Down Expand Up @@ -291,6 +294,7 @@ function Button({
secondLineText = '',
shouldBlendOpacity = false,
shouldStayNormalOnDisable = false,
accessibilityState,
sentryLabel,
ref,
...rest
Expand Down Expand Up @@ -527,6 +531,7 @@ function Button({
id={id}
testID={testID}
accessibilityLabel={accessibilityLabel}
accessibilityState={accessibilityState}
role={getButtonRole(isNested)}
hoverDimmingValue={1}
onHoverIn={!isDisabled || !shouldStayNormalOnDisable ? () => setIsHovered(true) : undefined}
Expand Down
2 changes: 2 additions & 0 deletions src/components/ButtonWithDropdownMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ function ButtonWithDropdownMenu<IValueType>({ref, ...props}: ButtonWithDropdownM
ref={dropdownButtonRef}
onPress={handlePress}
text={customText ?? selectedItem?.text ?? ''}
accessibilityState={!isSplitButton ? {expanded: isMenuVisible} : undefined}
isDisabled={isDisabled || areAllOptionsDisabled}
shouldStayNormalOnDisable={shouldStayNormalOnDisable}
isLoading={isLoading}
Expand Down Expand Up @@ -202,6 +203,7 @@ function ButtonWithDropdownMenu<IValueType>({ref, ...props}: ButtonWithDropdownM
ref={dropdownAnchor}
success={success}
isDisabled={isDisabled}
accessibilityState={{expanded: isMenuVisible}}
shouldStayNormalOnDisable={shouldStayNormalOnDisable}
style={[styles.pl0]}
onPress={() => setIsMenuVisible(!isMenuVisible)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ function GenericPressable({
aria-disabled={isDisabled}
aria-checked={accessibilityState?.checked}
aria-selected={accessibilityState?.selected}
aria-expanded={accessibilityState?.expanded}
aria-keyshortcuts={keyboardShortcut && `${keyboardShortcut.modifiers.join('')}+${keyboardShortcut.shortcutKey}`}
// ios-only form of inputs
onMagicTap={!isDisabled ? voidOnPressHandler : undefined}
Expand Down
Loading