From 6cc44d765a1127aa4998a490e3ff90b741ef9441 Mon Sep 17 00:00:00 2001 From: bacarybruno Date: Thu, 15 Aug 2024 13:38:15 -0700 Subject: [PATCH] feat(accessibility): support large content viewer on iOS (#45903) Summary: Referring to the [iOS Large Content Viewer](https://developer.apple.com/videos/play/wwdc2019/261/): iOS Tab Bars can't grow with dynamic text, but the Large Content Viewer helps them to be seen by people with low vision. Currently on React Native we don't expose the properties that can help implementing iOS [UILargeContentViewerItem](https://developer.apple.com/documentation/uikit/uilargecontentvieweritem) protocol. The goal of this PR is to expose the necessary props. In this PR, I'm exposing 2 props: - `accessibilityShowsLargeContentViewer`: to enable the large content viewer - `accessibilityLargeContentTitle`: to define the large content viewer title I plan to use this to open a PR on react-navigation so that bottom tabbars can implement largeContentViewer. Should fix https://github.com/facebook/react-native/issues/30892 ## Changelog: [IOS] [ADDED] - Support LargeContentViewer on iOS for better accessibility Pull Request resolved: https://github.com/facebook/react-native/pull/45903 Test Plan: Reviewed By: cipolleschi Differential Revision: D61148361 Pulled By: joevilches fbshipit-source-id: 86dd92f4f79534a58e6e015febdaf217ea291eb4 --- .../View/ReactNativeViewAttributes.js | 2 ++ .../Components/View/ViewAccessibility.d.ts | 15 +++++++++++++ .../Components/View/ViewPropTypes.js | 14 ++++++++++++ .../NativeComponent/BaseViewConfig.ios.js | 2 ++ .../__snapshots__/public-api-test.js.snap | 4 ++++ .../View/RCTViewComponentView.mm | 20 +++++++++++++++++ .../react-native/React/Views/RCTViewManager.m | 22 +++++++++++++++++++ .../components/view/AccessibilityProps.cpp | 20 +++++++++++++++++ .../components/view/AccessibilityProps.h | 2 ++ 9 files changed, 101 insertions(+) diff --git a/packages/react-native/Libraries/Components/View/ReactNativeViewAttributes.js b/packages/react-native/Libraries/Components/View/ReactNativeViewAttributes.js index 88dab482c47..d07a319837a 100644 --- a/packages/react-native/Libraries/Components/View/ReactNativeViewAttributes.js +++ b/packages/react-native/Libraries/Components/View/ReactNativeViewAttributes.js @@ -22,6 +22,8 @@ const UIView = { accessibilityValue: true, accessibilityHint: true, accessibilityLanguage: true, + accessibilityShowsLargeContentViewer: true, + accessibilityLargeContentTitle: true, importantForAccessibility: true, nativeID: true, testID: true, diff --git a/packages/react-native/Libraries/Components/View/ViewAccessibility.d.ts b/packages/react-native/Libraries/Components/View/ViewAccessibility.d.ts index 90cca7b66a1..bf4bee39538 100644 --- a/packages/react-native/Libraries/Components/View/ViewAccessibility.d.ts +++ b/packages/react-native/Libraries/Components/View/ViewAccessibility.d.ts @@ -319,6 +319,21 @@ export interface AccessibilityPropsIOS { * @platform ios */ accessibilityLanguage?: string | undefined; + + /** + * A Boolean value that indicates whether or not to show the item in the large content viewer. + * Available on iOS 13.0+ + * https://reactnative.dev/docs/accessibility#accessibilityshowslargecontentviewer + * @platform ios + */ + accessibilityShowsLargeContentViewer?: boolean | undefined; + + /** + * When `accessibilityShowsLargeContentViewer` is set, this string will be used as title for the large content viewer. + * https://reactnative.dev/docs/accessibility#accessibilitylargecontenttitle + * @platform ios + */ + accessibilityLargeContentTitle?: string | undefined; } export type Role = diff --git a/packages/react-native/Libraries/Components/View/ViewPropTypes.js b/packages/react-native/Libraries/Components/View/ViewPropTypes.js index 53dfa6a5cc3..00d90a87e30 100644 --- a/packages/react-native/Libraries/Components/View/ViewPropTypes.js +++ b/packages/react-native/Libraries/Components/View/ViewPropTypes.js @@ -415,6 +415,20 @@ type IOSViewProps = $ReadOnly<{| */ accessibilityViewIsModal?: ?boolean, + /** + * @platform ios + * + * See https://reactnative.dev/docs/view#accessibilityshowslargecontentviewer + */ + accessibilityShowsLargeContentViewer?: ?boolean, + + /** + * @platform ios + * + * See https://reactnative.dev/docs/view#accessibilitylargecontenttitle + */ + accessibilityLargeContentTitle?: ?string, + /** * The aria-modal attribute indicates content contained within a modal with aria-modal="true" * should be accessible to the user. diff --git a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js index 378b5c272dc..bcea251752d 100644 --- a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js @@ -193,6 +193,8 @@ const validAttributesForNonEventProps = { accessibilityViewIsModal: true, accessibilityElementsHidden: true, accessibilityIgnoresInvertColors: true, + accessibilityShowsLargeContentViewer: true, + accessibilityLargeContentTitle: true, testID: true, backgroundColor: {process: require('../StyleSheet/processColor').default}, backfaceVisibility: true, diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index beab205c536..ad9ca9e408a 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -3891,6 +3891,8 @@ exports[`public API should not change unintentionally Libraries/Components/View/ accessibilityValue: true, accessibilityHint: true, accessibilityLanguage: true, + accessibilityShowsLargeContentViewer: true, + accessibilityLargeContentTitle: true, importantForAccessibility: true, nativeID: true, testID: true, @@ -4177,6 +4179,8 @@ type AndroidViewProps = $ReadOnly<{| type IOSViewProps = $ReadOnly<{| accessibilityIgnoresInvertColors?: ?boolean, accessibilityViewIsModal?: ?boolean, + accessibilityShowsLargeContentViewer?: ?boolean, + accessibilityLargeContentTitle?: ?string, \\"aria-modal\\"?: ?boolean, accessibilityElementsHidden?: ?boolean, accessibilityLanguage?: ?Stringish, diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index ef5ef9b2d4a..883e91b5d13 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -347,6 +347,26 @@ using namespace facebook::react; self.accessibilityElement.accessibilityElementsHidden = newViewProps.accessibilityElementsHidden; } + // `accessibilityShowsLargeContentViewer` + if (oldViewProps.accessibilityShowsLargeContentViewer != newViewProps.accessibilityShowsLargeContentViewer) { + if (@available(iOS 13.0, *)) { + if (newViewProps.accessibilityShowsLargeContentViewer) { + self.showsLargeContentViewer = YES; + UILargeContentViewerInteraction *interaction = [[UILargeContentViewerInteraction alloc] init]; + [self addInteraction:interaction]; + } else { + self.showsLargeContentViewer = NO; + } + } + } + + // `accessibilityLargeContentTitle` + if (oldViewProps.accessibilityLargeContentTitle != newViewProps.accessibilityLargeContentTitle) { + if (@available(iOS 13.0, *)) { + self.largeContentTitle = RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityLargeContentTitle); + } + } + // `accessibilityTraits` if (oldViewProps.accessibilityTraits != newViewProps.accessibilityTraits) { self.accessibilityElement.accessibilityTraits = diff --git a/packages/react-native/React/Views/RCTViewManager.m b/packages/react-native/React/Views/RCTViewManager.m index a657565d7b8..223b89ecaeb 100644 --- a/packages/react-native/React/Views/RCTViewManager.m +++ b/packages/react-native/React/Views/RCTViewManager.m @@ -295,6 +295,28 @@ RCT_CUSTOM_VIEW_PROPERTY(accessibilityState, NSDictionary, RCTView) } } +RCT_CUSTOM_VIEW_PROPERTY(accessibilityShowsLargeContentViewer, BOOL, RCTView) +{ + if (@available(iOS 13.0, *)) { + BOOL showsLargeContentViewer = json ? [RCTConvert BOOL:json] : defaultView.showsLargeContentViewer; + + if (showsLargeContentViewer) { + view.showsLargeContentViewer = YES; + UILargeContentViewerInteraction *interaction = [[UILargeContentViewerInteraction alloc] init]; + [view addInteraction:interaction]; + } else { + view.showsLargeContentViewer = NO; + } + } +} + +RCT_CUSTOM_VIEW_PROPERTY(accessibilityLargeContentTitle, NSString, RCTView) +{ + if (@available(iOS 13.0, *)) { + view.largeContentTitle = json ? [RCTConvert NSString:json] : defaultView.largeContentTitle; + } +} + RCT_CUSTOM_VIEW_PROPERTY(nativeID, NSString *, RCTView) { view.nativeID = json ? [RCTConvert NSString:json] : defaultView.nativeID; diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.cpp index bb5e7a6f288..56b4c335ad8 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.cpp @@ -81,6 +81,15 @@ AccessibilityProps::AccessibilityProps( "accessibilityLanguage", sourceProps.accessibilityLanguage, "")), + accessibilityLargeContentTitle( + CoreFeatures::enablePropIteratorSetter + ? sourceProps.accessibilityLargeContentTitle + : convertRawProp( + context, + rawProps, + "accessibilityLargeContentTitle", + sourceProps.accessibilityLargeContentTitle, + "")), accessibilityValue( CoreFeatures::enablePropIteratorSetter ? sourceProps.accessibilityValue @@ -99,6 +108,15 @@ AccessibilityProps::AccessibilityProps( "accessibilityActions", sourceProps.accessibilityActions, {})), + accessibilityShowsLargeContentViewer( + CoreFeatures::enablePropIteratorSetter + ? sourceProps.accessibilityShowsLargeContentViewer + : convertRawProp( + context, + rawProps, + "accessibilityShowsLargeContentViewer", + sourceProps.accessibilityShowsLargeContentViewer, + false)), accessibilityViewIsModal( CoreFeatures::enablePropIteratorSetter ? sourceProps.accessibilityViewIsModal @@ -229,6 +247,8 @@ void AccessibilityProps::setProp( RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityLabelledBy); RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityHint); RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityLanguage); + RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityShowsLargeContentViewer); + RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityLargeContentTitle); RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityValue); RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityActions); RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityViewIsModal); diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.h b/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.h index a46c21412d5..77db5750114 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityProps.h @@ -41,8 +41,10 @@ class AccessibilityProps { std::string accessibilityRole{""}; std::string accessibilityHint{""}; std::string accessibilityLanguage{""}; + std::string accessibilityLargeContentTitle{""}; AccessibilityValue accessibilityValue; std::vector accessibilityActions{}; + bool accessibilityShowsLargeContentViewer{false}; bool accessibilityViewIsModal{false}; bool accessibilityElementsHidden{false}; bool accessibilityIgnoresInvertColors{false};