Skip to content

Commit

Permalink
Chain Label + Hint + State to avoid cutting announcements when using …
Browse files Browse the repository at this point in the history
…assertive

Read Implementation RCTParagraphComponentViewUnderstand logic behind announcement of the accessibilityLabel
--
Add announcement for Accessibility State. RCTViewComponent updateProps will save the accessibilityState in self.accessibilityTraits. Use self.accessibilityTraits to announce the change in value from updatePropsRead iOS ImplementationTest the announcement of self.accessibilityTraits with UIAccessibilityPostNotification
Read accessibilityValue logic in RCTComponentView
Chain Label + Hint + State to avoid cutting announcements when using assertiveCreate a local variables named label, hint, state, roleVerify order used on iOS to announce Label, Hint, State (selected my label Button my hint) => state label role hintAdd Label, Hint and State to accessibilityLiveRegionAnnouncement respecting correct orderTrigger announcement if accessibilityLiveRegionAnnouncement is not empty

"Read Implementation RCTParagraphComponentView
Understand logic behind announcement of the accessibilityLabel"
"Add announcement for Accessibility State. RCTViewComponent updateProps will save the accessibilityState in self.accessibilityTraits. Use self.accessibilityTraits to announce the change in value from updateProps
Read iOS Implementation
Test the announcement of self.accessibilityTraits with [UIAccessibilityPostNotification](https://github.com/fabriziobertoglio1987/react-native/blob/17af1f8489e202a65c1e20c440756db2be95a8b4/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm#L305)"
Read [accessibilityValue logic](https://github.com/fabriziobertoglio1987/react-native/blob/17af1f8489e202a65c1e20c440756db2be95a8b4/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm#L365) in RCTComponentView
"Chain Label + Hint + State to avoid cutting announcements when using assertive
Create a local variables named label, hint, state, role
Verify order used on iOS to announce Label, Hint, State (selected my label Button my hint) => state label role hint
Add Label, Hint and State to accessibilityLiveRegionAnnouncement respecting correct order
Trigger announcement if accessibilityLiveRegionAnnouncement is not empty "
  • Loading branch information
fabOnReact committed Nov 25, 2022
1 parent 17af1f8 commit 119892c
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 108 deletions.
180 changes: 108 additions & 72 deletions Libraries/Components/View/View.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import type {ViewProps} from './ViewPropTypes';

import flattenStyle from '../../StyleSheet/flattenStyle';
import TextAncestor from '../../Text/TextAncestor';
import {getAccessibilityRoleFromRole} from '../../Utilities/AcessibilityMapping';
import ViewNativeComponent from './ViewNativeComponent';
import * as React from 'react';

Expand Down Expand Up @@ -57,6 +56,7 @@ const View: React.AbstractComponent<
nativeID,
pointerEvents,
role,
style,
tabIndex,
...otherProps
}: ViewProps,
Expand All @@ -65,84 +65,120 @@ const View: React.AbstractComponent<
const _accessibilityLabelledBy =
ariaLabelledBy?.split(/\s*,\s*/g) ?? accessibilityLabelledBy;

let _accessibilityState;
if (
accessibilityState != null ||
ariaBusy != null ||
ariaChecked != null ||
ariaDisabled != null ||
ariaExpanded != null ||
ariaSelected != null
) {
_accessibilityState = {
busy: ariaBusy ?? accessibilityState?.busy,
checked: ariaChecked ?? accessibilityState?.checked,
disabled: ariaDisabled ?? accessibilityState?.disabled,
expanded: ariaExpanded ?? accessibilityState?.expanded,
selected: ariaSelected ?? accessibilityState?.selected,
};
}
let _accessibilityValue;
if (
accessibilityValue != null ||
ariaValueMax != null ||
ariaValueMin != null ||
ariaValueNow != null ||
ariaValueText != null
) {
_accessibilityValue = {
max: ariaValueMax ?? accessibilityValue?.max,
min: ariaValueMin ?? accessibilityValue?.min,
now: ariaValueNow ?? accessibilityValue?.now,
text: ariaValueText ?? accessibilityValue?.text,
};
}
const _accessibilityState = {
busy: ariaBusy ?? accessibilityState?.busy,
checked: ariaChecked ?? accessibilityState?.checked,
disabled: ariaDisabled ?? accessibilityState?.disabled,
expanded: ariaExpanded ?? accessibilityState?.expanded,
selected: ariaSelected ?? accessibilityState?.selected,
};

let style = flattenStyle(otherProps.style);
const _accessibilityValue = {
max: ariaValueMax ?? accessibilityValue?.max,
min: ariaValueMin ?? accessibilityValue?.min,
now: ariaValueNow ?? accessibilityValue?.now,
text: ariaValueText ?? accessibilityValue?.text,
};

const newPointerEvents = style?.pointerEvents || pointerEvents;
const defaultProps = {
...otherProps,
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,
pointerEvents: newPointerEvents,
ref: forwardedRef,
// Map role values to AccessibilityRole values
const roleToAccessibilityRoleMapping = {
alert: 'alert',
alertdialog: undefined,
application: undefined,
article: undefined,
banner: undefined,
button: 'button',
cell: undefined,
checkbox: 'checkbox',
columnheader: undefined,
combobox: 'combobox',
complementary: undefined,
contentinfo: undefined,
definition: undefined,
dialog: undefined,
directory: undefined,
document: undefined,
feed: undefined,
figure: undefined,
form: undefined,
grid: 'grid',
group: undefined,
heading: 'header',
img: 'image',
link: 'link',
list: 'list',
listitem: undefined,
log: undefined,
main: undefined,
marquee: undefined,
math: undefined,
menu: 'menu',
menubar: 'menubar',
menuitem: 'menuitem',
meter: undefined,
navigation: undefined,
none: 'none',
note: undefined,
presentation: 'none',
progressbar: 'progressbar',
radio: 'radio',
radiogroup: 'radiogroup',
region: undefined,
row: undefined,
rowgroup: undefined,
rowheader: undefined,
scrollbar: 'scrollbar',
searchbox: 'search',
separator: undefined,
slider: 'adjustable',
spinbutton: 'spinbutton',
status: undefined,
summary: 'summary',
switch: 'switch',
tab: 'tab',
table: undefined,
tablist: 'tablist',
tabpanel: undefined,
term: undefined,
timer: 'timer',
toolbar: 'toolbar',
tooltip: undefined,
tree: undefined,
treegrid: undefined,
treeitem: undefined,
};

// add accessibilityLiveRegion to focusable children on iOS
// move this to separate PR if can not be done clean
// requires additional Platform check for iOS
// should also check if children is focusable and add only
// liveRegion to first children
// a simple loop that retrieves the first focusable children
// for now just search for first Text component which is focusable as that is the
// iOS exception not covered
if (accessibilityLiveRegion != null && accessibilityLabel == null) {
return (
<TextAncestor.Provider value={false}>
<ViewNativeComponent {...defaultProps}>
{React.cloneElement(otherProps.children, {
accessibilityLiveRegion,
})}
</ViewNativeComponent>
</TextAncestor.Provider>
);
}
const flattenedStyle = flattenStyle(style);
const newPointerEvents = flattenedStyle?.pointerEvents || pointerEvents;

return (
<TextAncestor.Provider value={false}>
<ViewNativeComponent {...defaultProps} />
<ViewNativeComponent
{...otherProps}
accessibilityLiveRegion={
ariaLive === 'off' ? 'none' : ariaLive ?? accessibilityLiveRegion
}
accessibilityLabel={ariaLabel ?? accessibilityLabel}
focusable={tabIndex !== undefined ? !tabIndex : focusable}
accessibilityState={_accessibilityState}
accessibilityRole={
role ? roleToAccessibilityRoleMapping[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}
/>
</TextAncestor.Provider>
);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,20 +101,18 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
- (void)updateState:(State::Shared const &)state oldState:(State::Shared const &)oldState
{
_state = std::static_pointer_cast<ParagraphShadowNode::ConcreteState const>(state);
// STEP 1 - log the value
// NSLog(@"TESTING _state", _state.fragments_[0].string);
auto &data = _state->getData();
NSString *string = RCTNSStringFromStringNilIfEmpty(data.attributedString.getString());
auto const &viewProps = *std::static_pointer_cast<ViewProps const>(_props);
auto const &paragraphProps = *std::static_pointer_cast<ParagraphProps const>(_props);
auto const &accessibilityProps = *std::static_pointer_cast<AccessibilityProps const>(_props);
if (accessibilityProps.accessibilityLiveRegion == AccessibilityLiveRegion::Polite) {
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, string);
if (accessibilityProps.accessibilityLiveRegion == AccessibilityLiveRegion::None) {
// NSLog(@"TESTING None");
}
if (_state && paragraphProps.accessible && accessibilityProps.accessibilityLiveRegion == AccessibilityLiveRegion::Polite) {
auto &data = _state->getData();
if (![_accessibilityProvider isUpToDate:data.attributedString]) {
NSString *string = RCTNSStringFromStringNilIfEmpty(data.attributedString.getString());
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, string);
}
}
// STEP 3 - check that the value changed
// if ([string isEqualToString:@"my text"]) {
// NSLog(@"TESTING it is my text");
// }
// STEP 4 - call the announcement
[self setNeedsDisplay];
}

Expand Down
84 changes: 60 additions & 24 deletions React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -295,30 +295,6 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
self.accessibilityElement.accessibilityLabel = RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityLabel);
}

if (oldViewProps.accessibilityLabel != newViewProps.accessibilityLabel && newViewProps.accessibilityLiveRegion != AccessibilityLiveRegion::None) {
if ([self.nativeId isEqualToString:@"1"]) {
if (@available(iOS 11.0, *)) {
NSMutableDictionary<NSString *, NSNumber *> *attrsDictionary = [NSMutableDictionary new];
attrsDictionary[UIAccessibilitySpeechAttributeQueueAnnouncement] = @(newViewProps.accessibilityLiveRegion == AccessibilityLiveRegion::Polite ? YES : NO);
NSAttributedString *announcementWithAttrs = [[NSAttributedString alloc] initWithString: self.accessibilityElement.accessibilityLabel
attributes:attrsDictionary];
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcementWithAttrs);
}
}
}

if (!newViewProps.accessibilityHint.empty() && oldViewProps.accessibilityHint != newViewProps.accessibilityHint && newViewProps.accessibilityLiveRegion != AccessibilityLiveRegion::None) {
if ([self.nativeId isEqualToString:@"1"]) {
if (@available(iOS 11.0, *)) {
NSMutableDictionary<NSString *, NSNumber *> *attrsDictionary = [NSMutableDictionary new];
attrsDictionary[UIAccessibilitySpeechAttributeQueueAnnouncement] = @(YES);
NSAttributedString *announcementWithAttrs = [[NSAttributedString alloc] initWithString: RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityHint)
attributes:attrsDictionary];
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcementWithAttrs);
}
}
}

// `accessibilityLanguage`
if (oldViewProps.accessibilityLanguage != newViewProps.accessibilityLanguage) {
self.accessibilityElement.accessibilityLanguage =
Expand All @@ -329,6 +305,40 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
if (oldViewProps.accessibilityHint != newViewProps.accessibilityHint) {
self.accessibilityElement.accessibilityHint = RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityHint);
}

BOOL accessibilityLiveRegionEnabled = newViewProps.accessibilityLiveRegion != AccessibilityLiveRegion::None;
if (accessibilityLiveRegionEnabled) {
if (@available(iOS 11.0, *)) {
NSMutableString *accessibilityLiveRegionAnnouncement = [[NSMutableString alloc] initWithString:@""];
if (oldViewProps.accessibilityState != newViewProps.accessibilityState) {
if (newViewProps.accessibilityState.selected && accessibilityLiveRegionEnabled) {
[accessibilityLiveRegionAnnouncement insertString:@"selected " atIndex:0];

}
if (newViewProps.accessibilityState.disabled && accessibilityLiveRegionEnabled) {
[accessibilityLiveRegionAnnouncement insertString:@"disabled " atIndex:0];
}
}

if (!newViewProps.accessibilityLabel.empty() && oldViewProps.accessibilityLabel != newViewProps.accessibilityLabel && accessibilityLiveRegionEnabled) {
[accessibilityLiveRegionAnnouncement appendString:RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityLabel)];
[accessibilityLiveRegionAnnouncement appendString:@" "];
}

if (!newViewProps.accessibilityHint.empty() && oldViewProps.accessibilityHint != newViewProps.accessibilityHint && accessibilityLiveRegionEnabled) {
[accessibilityLiveRegionAnnouncement appendString:RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityHint)];
[accessibilityLiveRegionAnnouncement appendString:@" "];
}

if ([accessibilityLiveRegionAnnouncement length] != 0) {
NSMutableDictionary<NSString *, NSNumber *> *attrsDictionary = [NSMutableDictionary new];
attrsDictionary[UIAccessibilitySpeechAttributeQueueAnnouncement] = @(newViewProps.accessibilityLiveRegion == AccessibilityLiveRegion::Polite ? YES : NO);
NSAttributedString *announcementWithAttrs = [[NSAttributedString alloc] initWithString: accessibilityLiveRegionAnnouncement
attributes:attrsDictionary];
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcementWithAttrs);
}
}
}

// `accessibilityViewIsModal`
if (oldViewProps.accessibilityViewIsModal != newViewProps.accessibilityViewIsModal) {
Expand Down Expand Up @@ -384,7 +394,33 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
if (oldViewProps.testId != newViewProps.testId) {
self.accessibilityIdentifier = RCTNSStringFromString(newViewProps.testId);
}

/*
if (oldViewProps.accessibilityLabel != newViewProps.accessibilityLabel && newViewProps.accessibilityLiveRegion != AccessibilityLiveRegion::None) {
if ([self.nativeId isEqualToString:@"1"]) {
if (@available(iOS 11.0, *)) {
NSMutableDictionary<NSString *, NSNumber *> *attrsDictionary = [NSMutableDictionary new];
attrsDictionary[UIAccessibilitySpeechAttributeQueueAnnouncement] = @(newViewProps.accessibilityLiveRegion == AccessibilityLiveRegion::Polite ? YES : NO);
NSAttributedString *announcementWithAttrs = [[NSAttributedString alloc] initWithString: self.accessibilityElement.accessibilityLabel
attributes:attrsDictionary];
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcementWithAttrs);
}
}
}
if (!newViewProps.accessibilityHint.empty() && oldViewProps.accessibilityHint != newViewProps.accessibilityHint && newViewProps.accessibilityLiveRegion != AccessibilityLiveRegion::None) {
if ([self.nativeId isEqualToString:@"1"]) {
if (@available(iOS 11.0, *)) {
NSMutableDictionary<NSString *, NSNumber *> *attrsDictionary = [NSMutableDictionary new];
attrsDictionary[UIAccessibilitySpeechAttributeQueueAnnouncement] = @(YES);
NSAttributedString *announcementWithAttrs = [[NSAttributedString alloc] initWithString: RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityHint)
attributes:attrsDictionary];
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcementWithAttrs);
}
}
}
*/

_needsInvalidateLayer = _needsInvalidateLayer || needsInvalidateLayer;

_props = std::static_pointer_cast<ViewProps const>(props);
Expand Down

0 comments on commit 119892c

Please sign in to comment.