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

Added support for importantForAccessibility for some components #31296

Closed
13 changes: 13 additions & 0 deletions Libraries/Components/Button.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ type ButtonProps = $ReadOnly<{|
* Accessibility props.
*/
accessibilityState?: ?AccessibilityState,

/**
* [Android] Controlling if a view fires accessibility events and if it is reported to accessibility services.
*/
importantForAccessibility?: ?('auto' | 'yes' | 'no' | 'no-hide-descendants'),
|}>;

/**
Expand Down Expand Up @@ -256,6 +261,7 @@ class Button extends React.Component<ButtonProps> {
render(): React.Node {
const {
accessibilityLabel,
importantForAccessibility,
color,
onPress,
touchSoundDisabled,
Expand Down Expand Up @@ -302,11 +308,18 @@ class Button extends React.Component<ButtonProps> {
const Touchable =
Platform.OS === 'android' ? TouchableNativeFeedback : TouchableOpacity;

// If `no` is specified for `importantForAccessibility`, it will be changed to `no-hide-descendants` because the text inside should not be focused.
const _importantForAccessibility =
importantForAccessibility === 'no'
? 'no-hide-descendants'
: importantForAccessibility;

return (
<Touchable
accessibilityLabel={accessibilityLabel}
accessibilityRole="button"
accessibilityState={accessibilityState}
importantForAccessibility={_importantForAccessibility}
hasTVPreferredFocus={hasTVPreferredFocus}
nextFocusDown={nextFocusDown}
nextFocusForward={nextFocusForward}
Expand Down
4 changes: 4 additions & 0 deletions Libraries/Components/Picker/Picker.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ type PickerProps = $ReadOnly<{|
* The string used for the accessibility label. Will be read once focused on the picker but not on change.
*/
accessibilityLabel?: ?string,
/**
* [Android] Controlling if a view fires accessibility events and if it is reported to accessibility services.
*/
importantForAccessibility?: ?('auto' | 'yes' | 'no' | 'no-hide-descendants'),
|}>;

/**
Expand Down
8 changes: 8 additions & 0 deletions Libraries/Components/Picker/PickerAndroid.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type PickerItemValue = number | string;

type Props = $ReadOnly<{|
accessibilityLabel?: ?Stringish,
importantForAccessibility?: ?('auto' | 'yes' | 'no' | 'no-hide-descendants'),
children?: React.Node,
style?: ?TextStyleProp,
backgroundColor?: ?ColorValue,
Expand Down Expand Up @@ -110,6 +111,12 @@ function PickerAndroid(props: Props): React.Node {
],
);

// If `no` is specified for `importantForAccessibility`, it will be changed to `no-hide-descendants` because the Picker.Item should not be focused.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey! I just wanted to ask why this is needed? What happens if someone specifies "no" without this code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. If set to no, the Picker will not focus, but it will focus on the TextView inside that.
picker

Copy link
Contributor

@blavalla blavalla Apr 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch @grgr-dkrk !

@kacieb , this is due to the fact that while some components like <Button> seem like atomic units, they actually end up rendering a hierarchy of components for example a wrapper <View> with a <Text> inside them.

By allowing those descendants to become focusable, it sort of breaks the model of these being a single component to consider, so I think forcing no-hide-descendants makes sense here.

The other option is to always render any descendants of these elements with importantForAccessibility="no", so that they can never be focusable on their own. This would have the same end result, but may potentially cause issues when the descendant content is supposed to automatically get moved up to the focusable ancestor to act as a label (which is what Talkback does with unfocusable text elements by default).

const _importantForAccessibility =
props.importantForAccessibility === 'no'
? 'no-hide-descendants'
: props.importantForAccessibility;

const rootProps = {
accessibilityLabel: props.accessibilityLabel,
enabled: props.enabled,
Expand All @@ -121,6 +128,7 @@ function PickerAndroid(props: Props): React.Node {
style: StyleSheet.compose(styles.pickerAndroid, props.style),
backgroundColor: props.backgroundColor,
testID: props.testID,
importantForAccessibility: _importantForAccessibility,
};
return props.mode === 'dropdown' ? (
<AndroidDropdownPickerNativeComponent {...rootProps} />
Expand Down
34 changes: 34 additions & 0 deletions Libraries/Components/Picker/__tests__/Picker-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,38 @@ describe('<Picker />', () => {
},
);
});
it('should be set importantForAccessibility={no-hide-descendants} when importantForAccessibility={no-hide-descendants}', () => {
ReactNativeTestTools.expectRendersMatchingSnapshot(
'Picker',
() => (
<Picker
importantForAccessibility={'no-hide-descendants'}
selectedValue="foo"
onValueChange={jest.fn()}>
<Picker.Item label="foo" value="foo" />
<Picker.Item label="bar" value="bar" />
</Picker>
),
() => {
jest.dontMock('../Picker');
},
);
});
it('should be set importantForAccessibility={no-hide-descendants} when importantForAccessibility={no}', () => {
ReactNativeTestTools.expectRendersMatchingSnapshot(
'Picker',
() => (
<Picker
importantForAccessibility={'no'}
selectedValue="foo"
onValueChange={jest.fn()}>
<Picker.Item label="foo" value="foo" />
<Picker.Item label="bar" value="bar" />
</Picker>
),
() => {
jest.dontMock('../Picker');
},
);
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,201 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<Picker /> should be set importantForAccessibility={no-hide-descendants} when importantForAccessibility={no}: should deep render when mocked (please verify output manually) 1`] = `
<View>
<RCTPicker
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see anything about the importantForAccessibility prop here in the snapshot tests, which is unexpected. Does this snapshot need to be updated? Or perhaps we should allow this to be mocked to ensure importantForAccessibility is being set correctly? It seems like right now this test will not fail if the internal behavior is changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kacieb
I was taking a snapshot of iOS, but since this feature is for Android only, I decided to add the test only to the Android components. Thanks!

items={
Array [
Object {
"label": "foo",
"textColor": undefined,
"value": "foo",
},
Object {
"label": "bar",
"textColor": undefined,
"value": "bar",
},
]
}
onChange={[Function]}
selectedIndex={0}
style={
Array [
Object {
"height": 216,
},
undefined,
]
}
/>
</View>
`;

exports[`<Picker /> should be set importantForAccessibility={no-hide-descendants} when importantForAccessibility={no}: should deep render when not mocked (please verify output manually) 1`] = `
<View>
<RCTPicker
items={
Array [
Object {
"label": "foo",
"textColor": undefined,
"value": "foo",
},
Object {
"label": "bar",
"textColor": undefined,
"value": "bar",
},
]
}
onChange={[Function]}
selectedIndex={0}
style={
Array [
Object {
"height": 216,
},
undefined,
]
}
/>
</View>
`;

exports[`<Picker /> should be set importantForAccessibility={no-hide-descendants} when importantForAccessibility={no}: should shallow render as <Picker /> when mocked 1`] = `
<Picker
importantForAccessibility="no"
mode="dialog"
onValueChange={[MockFunction]}
selectedValue="foo"
>
<PickerItem
label="foo"
value="foo"
/>
<PickerItem
label="bar"
value="bar"
/>
</Picker>
`;

exports[`<Picker /> should be set importantForAccessibility={no-hide-descendants} when importantForAccessibility={no}: should shallow render as <Picker /> when not mocked 1`] = `
<Picker
importantForAccessibility="no"
mode="dialog"
onValueChange={[MockFunction]}
selectedValue="foo"
>
<PickerItem
label="foo"
value="foo"
/>
<PickerItem
label="bar"
value="bar"
/>
</Picker>
`;

exports[`<Picker /> should be set importantForAccessibility={no-hide-descendants} when importantForAccessibility={no-hide-descendants}: should deep render when mocked (please verify output manually) 1`] = `
<View>
<RCTPicker
items={
Array [
Object {
"label": "foo",
"textColor": undefined,
"value": "foo",
},
Object {
"label": "bar",
"textColor": undefined,
"value": "bar",
},
]
}
onChange={[Function]}
selectedIndex={0}
style={
Array [
Object {
"height": 216,
},
undefined,
]
}
/>
</View>
`;

exports[`<Picker /> should be set importantForAccessibility={no-hide-descendants} when importantForAccessibility={no-hide-descendants}: should deep render when not mocked (please verify output manually) 1`] = `
<View>
<RCTPicker
items={
Array [
Object {
"label": "foo",
"textColor": undefined,
"value": "foo",
},
Object {
"label": "bar",
"textColor": undefined,
"value": "bar",
},
]
}
onChange={[Function]}
selectedIndex={0}
style={
Array [
Object {
"height": 216,
},
undefined,
]
}
/>
</View>
`;

exports[`<Picker /> should be set importantForAccessibility={no-hide-descendants} when importantForAccessibility={no-hide-descendants}: should shallow render as <Picker /> when mocked 1`] = `
<Picker
importantForAccessibility="no-hide-descendants"
mode="dialog"
onValueChange={[MockFunction]}
selectedValue="foo"
>
<PickerItem
label="foo"
value="foo"
/>
<PickerItem
label="bar"
value="bar"
/>
</Picker>
`;

exports[`<Picker /> should be set importantForAccessibility={no-hide-descendants} when importantForAccessibility={no-hide-descendants}: should shallow render as <Picker /> when not mocked 1`] = `
<Picker
importantForAccessibility="no-hide-descendants"
mode="dialog"
onValueChange={[MockFunction]}
selectedValue="foo"
>
<PickerItem
label="foo"
value="foo"
/>
<PickerItem
label="bar"
value="bar"
/>
</Picker>
`;

exports[`<Picker /> should render as expected: should deep render when mocked (please verify output manually) 1`] = `
<View>
<RCTPicker
Expand Down
10 changes: 10 additions & 0 deletions Libraries/Components/__tests__/Button-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,14 @@ describe('<Button />', () => {
expect(ReactTestRenderer.create( <Button title="Test Button" disabled={false} accessibilityState={{disabled: false}} />)
).toMatchSnapshot();
});

it('should be set importantForAccessibility={no-hide-descendants} when importantForAccessibility={no-hide-descendants}', () => {
expect(ReactTestRenderer.create( <Button title="Test Button" importantForAccessibility={'no-hide-descendants'} />)
).toMatchSnapshot();
});

it('should be set importantForAccessibility={no-hide-descendants} when importantForAccessibility={no}', () => {
expect(ReactTestRenderer.create( <Button title="Test Button" importantForAccessibility={'no'} />)
).toMatchSnapshot();
});
});
Loading