From 1f37516f00318536e6d007115c9bb404bfc35a00 Mon Sep 17 00:00:00 2001 From: lazerg Date: Mon, 2 Nov 2026 15:50:26 +0500 Subject: [PATCH] Map aria props on iOS SafeAreaView --- .../Components/SafeAreaView/SafeAreaView.js | 100 +++++++++++++++++- .../__tests__/SafeAreaView-test.js | 53 ++++++++++ 2 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 packages/react-native/Libraries/Components/SafeAreaView/__tests__/SafeAreaView-test.js diff --git a/packages/react-native/Libraries/Components/SafeAreaView/SafeAreaView.js b/packages/react-native/Libraries/Components/SafeAreaView/SafeAreaView.js index 2b141c8c8494..5197f17adbbd 100644 --- a/packages/react-native/Libraries/Components/SafeAreaView/SafeAreaView.js +++ b/packages/react-native/Libraries/Components/SafeAreaView/SafeAreaView.js @@ -13,10 +13,108 @@ import type {ViewProps} from '../View/ViewPropTypes'; import Platform from '../../Utilities/Platform'; import View from '../View/View'; +import RCTSafeAreaViewNativeComponent from './RCTSafeAreaViewNativeComponent'; import * as React from 'react'; export type SafeAreaViewInstance = HostInstance; +component SafeAreaViewIOS( + ref?: React.RefSetter, + ...props: ViewProps +) { + const { + accessibilityState, + accessibilityValue, + 'aria-busy': ariaBusy, + 'aria-checked': ariaChecked, + 'aria-disabled': ariaDisabled, + 'aria-expanded': ariaExpanded, + 'aria-hidden': ariaHidden, + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledBy, + 'aria-live': ariaLive, + 'aria-modal': ariaModal, + 'aria-selected': ariaSelected, + 'aria-valuemax': ariaValueMax, + 'aria-valuemin': ariaValueMin, + 'aria-valuenow': ariaValueNow, + 'aria-valuetext': ariaValueText, + id, + tabIndex, + ...otherProps + } = props; + + const resolvedProps = otherProps as {...ViewProps}; + + const parsedAriaLabelledBy = ariaLabelledBy?.split(/\s*,\s*/g); + if (parsedAriaLabelledBy !== undefined) { + resolvedProps.accessibilityLabelledBy = parsedAriaLabelledBy; + } + + if (ariaLabel !== undefined) { + resolvedProps.accessibilityLabel = ariaLabel; + } + + if (ariaLive !== undefined) { + resolvedProps.accessibilityLiveRegion = + ariaLive === 'off' ? 'none' : ariaLive; + } + + if (ariaModal !== undefined) { + resolvedProps.accessibilityViewIsModal = ariaModal; + } + + if (ariaHidden !== undefined) { + resolvedProps.accessibilityElementsHidden = ariaHidden; + } + + if (id !== undefined) { + resolvedProps.nativeID = id; + } + + if (tabIndex !== undefined) { + resolvedProps.focusable = !tabIndex; + } + + if ( + accessibilityState != null || + ariaBusy != null || + ariaChecked != null || + ariaDisabled != null || + ariaExpanded != null || + ariaSelected != null + ) { + resolvedProps.accessibilityState = { + busy: ariaBusy ?? accessibilityState?.busy, + checked: ariaChecked ?? accessibilityState?.checked, + disabled: ariaDisabled ?? accessibilityState?.disabled, + expanded: ariaExpanded ?? accessibilityState?.expanded, + selected: ariaSelected ?? accessibilityState?.selected, + }; + } + + if ( + accessibilityValue != null || + ariaValueMax != null || + ariaValueMin != null || + ariaValueNow != null || + ariaValueText != null + ) { + resolvedProps.accessibilityValue = { + max: ariaValueMax ?? accessibilityValue?.max, + min: ariaValueMin ?? accessibilityValue?.min, + now: ariaValueNow ?? accessibilityValue?.now, + text: ariaValueText ?? accessibilityValue?.text, + }; + } + + return ref == null ? ( + + ) : ( + + ); +} + /** * Renders content within the safe area boundaries of a device. Currently only applicable to iOS devices with iOS version 11 or later. Automatically applies padding to reflect the portion of the view not covered by navigation bars, tab bars, toolbars, and other ancestor views. * @@ -28,7 +126,7 @@ const SafeAreaView: component( ref?: React.RefSetter, ...props: ViewProps ) = Platform.select({ - ios: require('./RCTSafeAreaViewNativeComponent').default, + ios: SafeAreaViewIOS, default: View, }); diff --git a/packages/react-native/Libraries/Components/SafeAreaView/__tests__/SafeAreaView-test.js b/packages/react-native/Libraries/Components/SafeAreaView/__tests__/SafeAreaView-test.js new file mode 100644 index 000000000000..d51a4a213951 --- /dev/null +++ b/packages/react-native/Libraries/Components/SafeAreaView/__tests__/SafeAreaView-test.js @@ -0,0 +1,53 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import {create} from '@react-native/jest-preset/jest/renderer'; +import * as React from 'react'; + +describe('SafeAreaView on iOS', () => { + beforeEach(() => { + jest.resetModules(); + jest.doMock('../../../Utilities/Platform', () => ({ + __esModule: true, + default: { + OS: 'ios', + select: (spec: {[string]: unknown}) => spec.ios ?? spec.default, + }, + })); + }); + + it('maps aria props to their accessibility counterparts', async () => { + const SafeAreaView = require('../SafeAreaView').default; + const component = await create( + , + ); + const props = component.toJSON()?.props; + + expect(props?.accessibilityElementsHidden).toBe(true); + expect(props?.accessibilityLabel).toBe('Label'); + expect(props?.accessibilityState).toEqual( + expect.objectContaining({busy: true}), + ); + expect(props?.accessibilityValue).toEqual( + expect.objectContaining({now: 3}), + ); + expect(props?.nativeID).toBe('safe'); + expect(props?.['aria-hidden']).toBeUndefined(); + expect(props?.['aria-label']).toBeUndefined(); + }); +});