Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding iOS support for accessibilityLiveRegion #35432

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
972a40a
Draft solution for iOS accessibilityLiveRegion
fabOnReact Nov 22, 2022
53f7b76
Trigger polite/assertive announcement with hint/label
fabOnReact Nov 22, 2022
85b1c1f
draft - accessibilityLiveRegion automatic content grouping announcement
fabOnReact Nov 24, 2022
f46b35b
Implement JavaScript or iOS functionality to selectively pass accessi…
fabOnReact Nov 24, 2022
17af1f8
adding comments for future review
fabOnReact Nov 24, 2022
119892c
Chain Label + Hint + State to avoid cutting announcements when using …
fabOnReact Nov 25, 2022
788f811
Add accessibilityValue to announcement
fabOnReact Nov 25, 2022
ed4c0a1
draft solution - announce change to Text attributedString
fabOnReact Nov 25, 2022
92119b8
not working - moving logic to attributedString will announce previous…
fabOnReact Nov 25, 2022
744408a
remove changes from View
fabOnReact Nov 25, 2022
a296e86
draft commit to be reverted
fabOnReact Nov 28, 2022
a60b07f
Merge branch 'main' into accessibility-live-region-ios
fabOnReact Nov 29, 2022
a7cc3f9
test - moving logic to Text.js from Paragraph.mm
fabOnReact Nov 29, 2022
5cdc7c8
draft
fabOnReact Nov 30, 2022
1f88f8f
adding dispatch_after to UIAccessibilityPostNotification to avoid cancel
fabOnReact Dec 1, 2022
8b3390c
removing change to podfile.lock
fabOnReact Dec 1, 2022
4bd8387
clean diff
fabOnReact Dec 1, 2022
e1478fe
clean diff
fabOnReact Dec 1, 2022
f609df6
moving acceLiveRegion example to AccessibilityEx
fabOnReact Dec 1, 2022
9826b5d
add prop type accLiveRegion to iOS
fabOnReact Dec 1, 2022
3b51a4b
adding more examples
fabOnReact Dec 1, 2022
1b1bd7e
fix circleci
fabOnReact Dec 1, 2022
b888fca
add back examples
fabOnReact Dec 1, 2022
d2039eb
remove setPressed
fabOnReact Dec 1, 2022
ceb9557
avoid double announcements on first page render
fabOnReact Dec 2, 2022
6171f4b
avoid repeating announcement 2 times with navigation
fabOnReact Dec 2, 2022
764378a
Revert "avoid double announcements on first page render"
fabOnReact Dec 2, 2022
7ba8c6f
remove dispatch_after
fabOnReact Dec 2, 2022
9003546
pass accessibilityLiveRegion to children on iOS
fabOnReact Dec 2, 2022
4bcff7e
set accLiveRegion polite/assertive in child view
fabOnReact Dec 2, 2022
2d7e799
fix announcement space cause by appending empty space
fabOnReact Dec 2, 2022
8c9ee23
minor refactoring, removing boolean for accessibilityLiveRegionToChil…
fabOnReact Dec 2, 2022
b505039
Merge branch 'main' into accessibility-live-region-ios
fabOnReact Dec 2, 2022
fdcd053
not working - text in parent view accessibilityLiveRegion
fabOnReact Dec 2, 2022
ccfef6a
Revert "not working - text in parent view accessibilityLiveRegion"
fabOnReact Dec 2, 2022
ec0fb52
minor change
fabOnReact Dec 2, 2022
746118c
update docs
fabOnReact Dec 2, 2022
01ca179
documentation and delete not used variable
fabOnReact Dec 2, 2022
66d0d8e
mark it as needs trigger re layout and in relevant callback trigger a…
fabOnReact Dec 5, 2022
c342429
fix - don't trigger announcement in update props, instead mark it as …
fabOnReact Dec 5, 2022
32dd4f0
fix - Any change of state will trigger the same announcement
fabOnReact Dec 5, 2022
ba55553
fix - should trigger reset of child instance variable when changes fr…
fabOnReact Dec 5, 2022
b9a4b93
_needsInvalidateLayer should be removed because triggers re-render of…
fabOnReact Dec 5, 2022
b47643e
Check diff between the old and the newViewProps field accessibilityPr…
fabOnReact Dec 5, 2022
b5157d1
Use an array newAccessibilityLiveRegionAnnouncement [“accessibilitySt…
fabOnReact Dec 6, 2022
3785f60
minor fix
fabOnReact Dec 6, 2022
171e23d
Trigger announcement if child or parent prop accLiveRegion is enabled
fabOnReact Dec 6, 2022
cac1e69
Trigger the announcement if changes from previous announcement. Avoid…
fabOnReact Dec 6, 2022
ba2cfaf
Update documentation (link)
fabOnReact Dec 6, 2022
059e906
Merge branch 'main' into accessibility-live-region-ios
fabOnReact Dec 6, 2022
7737502
Support for accessibilityValue with accessibilityLiveRegion
fabOnReact Dec 7, 2022
1565a8d
Text, Slider component will default with accessibilityLiveRegion != “…
fabOnReact Dec 7, 2022
8b7d4e0
avoid announcing not accessible elements
fabOnReact Dec 7, 2022
f38af2c
Move the logic from RCTParagraph updateState to RCTParagraphCompAccPr…
fabOnReact Dec 7, 2022
6c76096
Append accessibilityState checked and other field to accessibilityValue
fabOnReact Dec 7, 2022
0893d7e
Trigger VoiceOver announcement when adding children (for ex. chatroom…
fabOnReact Dec 8, 2022
fbf5344
mountChildComponentView is called after updateProps. This may cause n…
fabOnReact Dec 8, 2022
978c9bf
Copy TextAnchestor and hasTextAncestor as hasLiveRegion and implement…
fabOnReact Dec 9, 2022
061b76e
minor changes with code review
fabOnReact Dec 9, 2022
ca9b7ef
move RCTParagraphLogic to finalizeUpdates
fabOnReact Dec 9, 2022
48a415e
Create a method sendAccessibilityAnnouncement in RCTViewComponentView
fabOnReact Dec 9, 2022
cb53eb5
update state - do not return
fabOnReact Dec 9, 2022
75d5038
Verify issue with checked and unchecked. The expected behaviour, if a…
fabOnReact Dec 9, 2022
32e6df2
Text changes are not announced after last refactor. Adapt logic in RC…
fabOnReact Dec 9, 2022
fc778a7
minor change
fabOnReact Dec 9, 2022
ed596c4
fixing selected/disabled announcements
fabOnReact Dec 9, 2022
ef60511
Text does not announce other changes (for ex. accessibilityState
fabOnReact Dec 12, 2022
43ff153
remove space announcement
fabOnReact Dec 12, 2022
3cfdef4
adding ViewAncestor to TextInput
fabOnReact Dec 12, 2022
86d470a
adding accessibilityLiveRegion to Switch/Image
fabOnReact Dec 12, 2022
28039d9
6 | bug | Use correct default in useContext (link)
fabOnReact Dec 12, 2022
847e1aa
move accLiveRegion prop to separate variable while keeping compatabil…
fabOnReact Dec 12, 2022
7032cd2
React Hook "useContext" is called conditionally. React Hooks must be …
fabOnReact Dec 12, 2022
0940a54
pass aria-label from parent to child view
fabOnReact Dec 12, 2022
a5da98d
fixing issue with Android TextInput not passing accLiveRegion prop
fabOnReact Dec 12, 2022
1ae6a08
update snapshot
fabOnReact Dec 12, 2022
88cbc2c
not required anymore to fix issue with Text iOS not announcing dimmed
fabOnReact Dec 12, 2022
868f598
circleci eslint warnings
fabOnReact Dec 13, 2022
133f440
remove change to AnimatedValue
fabOnReact Dec 13, 2022
134b13b
update example
fabOnReact Dec 13, 2022
59a23ba
minor changes
fabOnReact Dec 14, 2022
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
10 changes: 10 additions & 0 deletions Libraries/Components/Switch/Switch.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import type {ViewProps} from '../View/ViewPropTypes';
import StyleSheet from '../../StyleSheet/StyleSheet';
import Platform from '../../Utilities/Platform';
import useMergeRefs from '../../Utilities/useMergeRefs';
import ViewAncestor from '../View/ViewAncestor';
import AndroidSwitchNativeComponent, {
Commands as AndroidSwitchCommands,
} from './AndroidSwitchNativeComponent';
import SwitchNativeComponent, {
Commands as SwitchCommands,
} from './SwitchNativeComponent';
import * as React from 'react';
import {useContext} from 'react';

type SwitchChangeEvent = SyntheticEvent<
$ReadOnly<{|
Expand Down Expand Up @@ -184,6 +186,13 @@ const SwitchWithForwardedRef: React.AbstractComponent<
}
}, [value, native]);

const parentLiveRegion = useContext(ViewAncestor);
const _accessibilityLiveRegion =
props.accessibilityLiveRegion == null &&
(parentLiveRegion === 'assertive' || parentLiveRegion === 'polite')
? parentLiveRegion
: props.accessibilityLiveRegion;

if (Platform.OS === 'android') {
const {accessibilityState} = restProps;
const _disabled =
Expand Down Expand Up @@ -220,6 +229,7 @@ const SwitchWithForwardedRef: React.AbstractComponent<
const platformProps = {
disabled,
onTintColor: trackColorForTrue,
accessibilityLiveRegion: _accessibilityLiveRegion,
style: StyleSheet.compose(
{height: 31, width: 51},
StyleSheet.compose(
Expand Down
25 changes: 24 additions & 1 deletion Libraries/Components/TextInput/TextInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,18 @@ import Text from '../../Text/Text';
import TextAncestor from '../../Text/TextAncestor';
import Platform from '../../Utilities/Platform';
import useMergeRefs from '../../Utilities/useMergeRefs';
import ViewAncestor from '../View/ViewAncestor';
import TextInputState from './TextInputState';
import invariant from 'invariant';
import nullthrows from 'nullthrows';
import * as React from 'react';
import {useCallback, useLayoutEffect, useRef, useState} from 'react';
import {
useCallback,
useContext,
useLayoutEffect,
useRef,
useState,
} from 'react';

type ReactRefSetter<T> = {current: null | T, ...} | ((ref: null | T) => mixed);
type TextInputInstance = React.ElementRef<HostComponent<mixed>> & {
Expand Down Expand Up @@ -1074,7 +1081,9 @@ function InternalTextInput(props: Props): React.Node {
'aria-disabled': ariaDisabled,
'aria-expanded': ariaExpanded,
'aria-selected': ariaSelected,
'aria-live': ariaLive,
accessibilityState,
accessibilityLiveRegion,
id,
tabIndex,
...otherProps
Expand Down Expand Up @@ -1420,6 +1429,8 @@ function InternalTextInput(props: Props): React.Node {

let style = flattenStyle(props.style);

const parentLiveRegion = useContext(ViewAncestor);

if (Platform.OS === 'ios') {
const RCTTextInputView =
props.multiline === true
Expand All @@ -1432,13 +1443,22 @@ function InternalTextInput(props: Props): React.Node {
(props.unstable_onChangeSync || props.unstable_onChangeTextSync) &&
!(props.onChange || props.onChangeText);

const _accessibilityLiveRegion =
accessibilityLiveRegion == null &&
(parentLiveRegion === 'assertive' || parentLiveRegion === 'polite')
? parentLiveRegion
: accessibilityLiveRegion;

textInput = (
<RCTTextInputView
// $FlowFixMe[incompatible-type] - Figure out imperative + forward refs.
ref={ref}
{...otherProps}
{...eventHandlers}
accessibilityState={_accessibilityState}
accessibilityLiveRegion={
ariaLive === 'off' ? 'none' : ariaLive ?? _accessibilityLiveRegion
}
accessible={accessible}
submitBehavior={submitBehavior}
caretHidden={caretHidden}
Expand Down Expand Up @@ -1490,6 +1510,9 @@ function InternalTextInput(props: Props): React.Node {
{...otherProps}
{...eventHandlers}
accessibilityState={_accessibilityState}
accessibilityLiveRegion={
ariaLive === 'off' ? 'none' : ariaLive ?? accessibilityLiveRegion
}
accessibilityLabelledBy={_accessibilityLabelledBy}
accessible={accessible}
autoCapitalize={autoCapitalize}
Expand Down
2 changes: 1 addition & 1 deletion Libraries/Components/TextInput/__tests__/TextInput-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ describe('TextInput compat with web', () => {

expect(instance.toJSON()).toMatchInlineSnapshot(`
<RCTSinglelineTextInputView
accessibilityLiveRegion="polite"
accessibilityState={
Object {
"busy": true,
Expand Down Expand Up @@ -345,7 +346,6 @@ describe('TextInput compat with web', () => {
aria-label="label"
aria-labelledby="labelledby"
aria-level={3}
aria-live="polite"
aria-modal={true}
aria-multiline={true}
aria-multiselectable={true}
Expand Down
62 changes: 36 additions & 26 deletions Libraries/Components/View/View.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import type {ViewProps} from './ViewPropTypes';
import flattenStyle from '../../StyleSheet/flattenStyle';
import TextAncestor from '../../Text/TextAncestor';
import {getAccessibilityRoleFromRole} from '../../Utilities/AcessibilityMapping';
import ViewAncestor from './ViewAncestor';
import ViewNativeComponent from './ViewNativeComponent';
import * as React from 'react';
import {useContext} from 'react';

export type Props = ViewProps;

Expand Down Expand Up @@ -102,34 +104,42 @@ const View: React.AbstractComponent<

const newPointerEvents = style?.pointerEvents || pointerEvents;

const parentLiveRegion = useContext(ViewAncestor);
const _accessibilityLiveRegion =
accessibilityLiveRegion == null &&
(parentLiveRegion === 'assertive' || parentLiveRegion === 'polite')
? parentLiveRegion
: accessibilityLiveRegion;
return (
<TextAncestor.Provider value={false}>
<ViewNativeComponent
{...otherProps}
accessibilityLiveRegion={
ariaLive === 'off' ? 'none' : ariaLive ?? accessibilityLiveRegion
}
accessibilityLabel={ariaLabel ?? accessibilityLabel}
focusable={tabIndex !== undefined ? !tabIndex : focusable}
accessibilityState={_accessibilityState}
accessibilityRole={
role ? getAccessibilityRoleFromRole(role) : accessibilityRole
}
accessibilityElementsHidden={
ariaHidden ?? accessibilityElementsHidden
}
accessibilityLabelledBy={_accessibilityLabelledBy}
accessibilityValue={_accessibilityValue}
importantForAccessibility={
ariaHidden === true
? 'no-hide-descendants'
: importantForAccessibility
}
nativeID={id ?? nativeID}
style={style}
pointerEvents={newPointerEvents}
ref={forwardedRef}
/>
<ViewAncestor.Provider value={ariaLive ?? accessibilityLiveRegion}>
<ViewNativeComponent
{...otherProps}
accessibilityLabel={ariaLabel ?? accessibilityLabel}
accessibilityLiveRegion={
ariaLive === 'off' ? 'none' : ariaLive ?? _accessibilityLiveRegion
}
focusable={tabIndex !== undefined ? !tabIndex : focusable}
accessibilityState={_accessibilityState}
accessibilityRole={
role ? getAccessibilityRoleFromRole(role) : accessibilityRole
}
accessibilityElementsHidden={
ariaHidden ?? accessibilityElementsHidden
}
accessibilityLabelledBy={_accessibilityLabelledBy}
accessibilityValue={_accessibilityValue}
importantForAccessibility={
ariaHidden === true
? 'no-hide-descendants'
: importantForAccessibility
}
nativeID={id ?? nativeID}
style={style}
pointerEvents={newPointerEvents}
ref={forwardedRef}
/>
</ViewAncestor.Provider>
</TextAncestor.Provider>
);
},
Expand Down
17 changes: 9 additions & 8 deletions Libraries/Components/View/ViewAccessibility.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ export interface AccessibilityProps
*/
accessible?: boolean | undefined;

/**
* Indicates to accessibility services whether the user should be notified when this view changes.
* Works for Android API >= 19 and iOS 11.
* For android: see http://developer.android.com/reference/android/view/View.html#attr_android:accessibilityLiveRegion for references.
* For iOS: It does not support accessibilityValue and children can not over-ride the value inherited from parent accessibilityLiveRegion.
* see https://bit.ly/3P2jItf for more info
*/
fabOnReact marked this conversation as resolved.
Show resolved Hide resolved
accessibilityLiveRegion?: 'none' | 'polite' | 'assertive' | undefined;

/**
* Provides an array of custom actions available for accessibility.
*/
Expand Down Expand Up @@ -226,14 +235,6 @@ export type AccessibilityRole =
| 'toolbar';

export interface AccessibilityPropsAndroid {
/**
* Indicates to accessibility services whether the user should be notified when this view changes.
* Works for Android API >= 19 only.
* See http://developer.android.com/reference/android/view/View.html#attr_android:accessibilityLiveRegion for references.
* @platform android
*/
accessibilityLiveRegion?: 'none' | 'polite' | 'assertive' | undefined;

/**
* Controls how view is important for accessibility which is if it fires accessibility events
* and if it is reported to accessibility services that query the screen.
Expand Down
24 changes: 24 additions & 0 deletions Libraries/Components/View/ViewAncestor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* 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
* @format
*/

'use strict';

const React = require('react');

/**
* Used to retrieve parent view accessibilityLiveRegion
*/
const ViewAncestorContext = (React.createContext(
'none',
): React$Context<$FlowFixMe>);
if (__DEV__) {
ViewAncestorContext.displayName = 'ViewAncestorContext';
}
module.exports = ViewAncestorContext;
18 changes: 4 additions & 14 deletions Libraries/Components/View/ViewPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,23 +284,13 @@ type AndroidViewProps = $ReadOnly<{|
needsOffscreenAlphaCompositing?: ?boolean,

/**
* Indicates to accessibility services whether the user should be notified
* when this view changes. Works for Android API >= 19 only.
* Indicates to accessibility services whether the user should be notified when this view changes.
* Works for Android API >= 19 and iOS 11.
*
* @platform android
*
* See https://reactnative.dev/docs/view#accessibilityliveregion
* For android see https://bit.ly/3HukgWQ
* For iOS see https://bit.ly/3P2jItf
*/
accessibilityLiveRegion?: ?('none' | 'polite' | 'assertive'),

/**
* Indicates to accessibility services whether the user should be notified
* when this view changes. Works for Android API >= 19 only.
*
* @platform android
*
* See https://reactnative.dev/docs/view#accessibilityliveregion
*/
'aria-live'?: ?('polite' | 'assertive' | 'off'),

/**
Expand Down
13 changes: 13 additions & 0 deletions Libraries/Image/Image.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {RootTag} from '../Types/RootTagTypes';
import type {ImageIOS} from './Image.flow';
import type {ImageProps as ImagePropsType} from './ImageProps';

import ViewAncestor from '../Components/View/ViewAncestor';
import flattenStyle from '../StyleSheet/flattenStyle';
import StyleSheet from '../StyleSheet/StyleSheet';
import ImageAnalyticsTagContext from './ImageAnalyticsTagContext';
Expand All @@ -23,6 +24,7 @@ import ImageViewNativeComponent from './ImageViewNativeComponent';
import NativeImageLoaderIOS from './NativeImageLoaderIOS';
import resolveAssetSource from './resolveAssetSource';
import * as React from 'react';
import {useContext} from 'react';

function getSize(
uri: string,
Expand Down Expand Up @@ -147,6 +149,7 @@ const BaseImage = (props: ImagePropsType, forwardedRef) => {
'aria-checked': ariaChecked,
'aria-disabled': ariaDisabled,
'aria-expanded': ariaExpanded,
'aria-live': ariaLive,
'aria-selected': ariaSelected,
height,
src,
Expand All @@ -163,12 +166,22 @@ const BaseImage = (props: ImagePropsType, forwardedRef) => {
};
const accessibilityLabel = props['aria-label'] ?? props.accessibilityLabel;

const parentLiveRegion = useContext(ViewAncestor);
const _accessibilityLiveRegion =
props.accessibilityLiveRegion == null &&
(parentLiveRegion === 'assertive' || parentLiveRegion === 'polite')
? parentLiveRegion
: props.accessibilityLiveRegion;

return (
<ImageAnalyticsTagContext.Consumer>
{analyticTag => {
return (
<ImageViewNativeComponent
accessibilityState={_accessibilityState}
accessibilityLiveRegion={
ariaLive === 'off' ? 'none' : ariaLive ?? _accessibilityLiveRegion
}
{...restProps}
accessible={props.alt !== undefined ? true : props.accessible}
accessibilityLabel={accessibilityLabel ?? props.alt}
Expand Down
1 change: 1 addition & 0 deletions Libraries/NativeComponent/BaseViewConfig.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ const validAttributesForNonEventProps = {
accessible: true,
accessibilityActions: true,
accessibilityLabel: true,
accessibilityLiveRegion: true,
accessibilityHint: true,
accessibilityLanguage: true,
accessibilityValue: true,
Expand Down
Loading