Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,18 @@ export type AccessibilityPropsIOS = $ReadOnly<{
*/
accessibilityElementsHidden?: ?boolean,

/**
* When true, indicates that this view should act as an accessibility
* container, allowing nested interactive elements to be individually
* accessible. Use this when you have nested Pressables or other
* interactive components that need to be separately accessible.
*
* When enabled, VoiceOver will be able to navigate to its accessible children.
*
* @platform ios
*/
accessibilityContainer?: ?boolean,

/**
* Indicates to the accessibility services that the UI component is in
* a specific language. The provided string should be formatted following
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ const validAttributesForNonEventProps = {
accessibilityValue: true,
accessibilityViewIsModal: true,
accessibilityElementsHidden: true,
accessibilityContainer: true,
accessibilityIgnoresInvertColors: true,
accessibilityShowsLargeContentViewer: true,
accessibilityLargeContentTitle: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ @implementation RCTViewComponentView {
BOOL _useCustomContainerView;
NSMutableSet<NSString *> *_accessibilityOrderNativeIDs;
RCTSwiftUIContainerViewWrapper *_swiftUIWrapper;
BOOL _isAccessibilityContainer;
NSArray *_accessibilityElements;
}

#ifdef RCT_DYNAMIC_FRAMEWORKS
Expand Down Expand Up @@ -343,6 +345,13 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
self.nativeId = RCTNSStringFromStringNilIfEmpty(newViewProps.nativeId);
}

// `accessibilityContainer` - needs to be handled before `accessible` so the setter override works
if (oldViewProps.accessibilityContainer != newViewProps.accessibilityContainer) {
_isAccessibilityContainer = newViewProps.accessibilityContainer;
// Clear cached accessibility elements when this changes
_accessibilityElements = nil;
}

// `accessible`
if (oldViewProps.accessible != newViewProps.accessible) {
self.accessibilityElement.isAccessibilityElement = newViewProps.accessible;
Expand Down Expand Up @@ -1353,13 +1362,29 @@ - (BOOL)wantsToCooptLabel

- (BOOL)isAccessibilityElement
{
// If this view is an accessibility container, it should not itself be an accessibility element
// This allows the accessibilityElements array to be used instead
if (_isAccessibilityContainer) {
return NO;
}

if (self.contentView != nil) {
return self.contentView.isAccessibilityElement;
}

return [super isAccessibilityElement];
}

- (void)setIsAccessibilityElement:(BOOL)isAccessibilityElement
{
// If this is an accessibility container, prevent setting isAccessibilityElement to YES
if (_isAccessibilityContainer) {
// Don't call super - we want to keep it as NO
return;
}
[super setIsAccessibilityElement:isAccessibilityElement];
}

- (NSString *)accessibilityValue
{
const auto &props = static_cast<const ViewProps &>(*_props);
Expand Down Expand Up @@ -1637,6 +1662,77 @@ - (BOOL)resignFirstResponder
return YES;
}

#pragma mark - UIAccessibilityContainer

- (BOOL)isAccessibilityContainer
{
return _isAccessibilityContainer;
}

- (NSArray *)accessibilityElements
{
if (!_isAccessibilityContainer) {
return [super accessibilityElements];
}
[self updateAccessibilityElementsIfNeeded];
return _accessibilityElements;
}

- (void)setAccessibilityElements:(NSArray *)accessibilityElements
{
// Allow setting accessibilityElements, but if we're managing it ourselves, ignore
if (!_isAccessibilityContainer) {
[super setAccessibilityElements:accessibilityElements];
}
}


- (void)updateAccessibilityElementsIfNeeded
{
if (_accessibilityElements != nil) {
return;
}

NSMutableArray *elements = [NSMutableArray new];

// First, add an accessibility element for the container itself
// This allows VoiceOver to access the container's action (e.g., an outer Pressable's onPress)
if (super.accessibilityLabel || super.accessibilityTraits != UIAccessibilityTraitNone) {
UIAccessibilityElement *containerElement = [[UIAccessibilityElement alloc] initWithAccessibilityContainer:self];
containerElement.accessibilityLabel = super.accessibilityLabel;
containerElement.accessibilityHint = super.accessibilityHint;
containerElement.accessibilityTraits = super.accessibilityTraits;
containerElement.accessibilityFrame = self.accessibilityFrame;
containerElement.accessibilityFrameInContainerSpace = self.bounds;
[elements addObject:containerElement];
}

// Then collect all accessible child elements
UIView *container = self.contentView ?: self;

for (UIView *subview in container.subviews) {
if (subview.isAccessibilityElement) {
[elements addObject:subview];
} else if ([subview respondsToSelector:@selector(accessibilityElements)] &&
[subview accessibilityElements]) {
// If the subview has its own accessibilityElements array, add those
NSArray *subElements = [subview accessibilityElements];
[elements addObjectsFromArray:subElements];
} else if ([subview respondsToSelector:@selector(accessibilityElementCount)] &&
[subview accessibilityElementCount] > 0) {
// Recursively collect accessibility elements from child containers
NSInteger count = [subview accessibilityElementCount];
for (NSInteger i = 0; i < count; i++) {
id element = [subview accessibilityElementAtIndex:i];
if (element) {
[elements addObject:element];
}
}
}
}
_accessibilityElements = [elements copy];
}

@end

#ifdef __cplusplus
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,15 @@ AccessibilityProps::AccessibilityProps(
"accessibilityRespondsToUserInteraction",
sourceProps.accessibilityRespondsToUserInteraction,
true)),
accessibilityContainer(
ReactNativeFeatureFlags::enableCppPropsIteratorSetter()
? sourceProps.accessibilityContainer
: convertRawProp(
context,
rawProps,
"accessibilityContainer",
sourceProps.accessibilityContainer,
false)),
onAccessibilityTap(
ReactNativeFeatureFlags::enableCppPropsIteratorSetter()
? sourceProps.onAccessibilityTap
Expand Down Expand Up @@ -280,6 +289,7 @@ void AccessibilityProps::setProp(
RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityElementsHidden);
RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityIgnoresInvertColors);
RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityRespondsToUserInteraction);
RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityContainer);
RAW_SET_PROP_SWITCH_CASE_BASIC(onAccessibilityTap);
RAW_SET_PROP_SWITCH_CASE_BASIC(onAccessibilityMagicTap);
RAW_SET_PROP_SWITCH_CASE_BASIC(onAccessibilityEscape);
Expand Down Expand Up @@ -327,6 +337,10 @@ SharedDebugStringConvertibleList AccessibilityProps::getDebugProps() const {
"accessibilityElementsHidden",
accessibilityElementsHidden,
defaultProps.accessibilityElementsHidden),
debugStringConvertibleItem(
"accessibilityContainer",
accessibilityContainer,
defaultProps.accessibilityContainer),
debugStringConvertibleItem(
"accessibilityHint",
accessibilityHint,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class AccessibilityProps {
// C++ because if not, it will default to false before render which prevents
// the view from being updated with the correct value.
bool accessibilityRespondsToUserInteraction{true};
bool accessibilityContainer{false};
bool onAccessibilityTap{};
bool onAccessibilityMagicTap{};
bool onAccessibilityEscape{};
Expand Down
46 changes: 46 additions & 0 deletions packages/rn-tester/js/examples/Pressable/PressableExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,52 @@ const examples = [
);
},
},
{
title: 'Nested Pressables with Accessibility Container (iOS)',
description: (
'Demonstrates accessibilityContainer prop for nested interactive elements': string
),
render: function NestedPressablesWithAccessibilityContainer(): React.Node {
const [outerPressCount, setOuterPressCount] = useState(0);
const [innerPressCount, setInnerPressCount] = useState(0);

return (
<View>
<View style={styles.row}>
<Text style={styles.text}>
Outer: {outerPressCount} | Inner: {innerPressCount}
</Text>
</View>
<View style={styles.row}>
<Pressable
accessibilityContainer={true}
accessibilityLabel="Outer card button"
accessibilityRole="button"
onPress={() => setOuterPressCount(outerPressCount + 1)}
style={{
backgroundColor: '#f9c2ff',
padding: 16,
borderRadius: 8,
}}>
<Text>Outer Pressable</Text>
<Pressable
accessibilityLabel="Inner button"
accessibilityRole="button"
onPress={() => setInnerPressCount(innerPressCount + 1)}
style={{
backgroundColor: '#61dafb',
padding: 12,
marginTop: 8,
borderRadius: 6,
}}>
<Text>Inner Pressable</Text>
</Pressable>
</Pressable>
</View>
</View>
);
},
},
...PressableExampleFbInternal.examples,
];

Expand Down