Skip to content

Commit 3e090db

Browse files
authored
Merge pull request #22 from TaduJR/fix/ios-voiceover-picker-accessibility
fix(a11y): add iOS VoiceOver support to picker and Done button
2 parents acf8e54 + 3673c94 commit 3e090db

File tree

2 files changed

+113
-1
lines changed

2 files changed

+113
-1
lines changed

src/index.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,8 @@ export default class RNPickerSelect extends PureComponent {
439439
left: 4,
440440
}}
441441
{...touchableDoneProps}
442+
accessibilityRole="button"
443+
accessibilityLabel={doneText}
442444
>
443445
<View testID="needed_for_touchable">
444446
<Text
@@ -492,6 +494,7 @@ export default class RNPickerSelect extends PureComponent {
492494
<View pointerEvents="box-only" style={containerStyle}>
493495
<TextInput
494496
testID="text_input"
497+
pointerEvents="none"
495498
style={[
496499
Platform.OS === 'ios' ? style.inputIOS : style.inputAndroid,
497500
this.getPlaceholderStyle(),
@@ -507,9 +510,11 @@ export default class RNPickerSelect extends PureComponent {
507510
}
508511

509512
renderIOS() {
510-
const { style, modalProps, pickerProps, touchableWrapperProps } = this.props;
513+
const { disabled, style, modalProps, pickerProps, touchableWrapperProps } = this.props;
511514
const { animationType, orientation, selectedItem, showPicker } = this.state;
512515

516+
const accessibilityLabel = pickerProps && pickerProps.accessibilityLabel;
517+
513518
return (
514519
<View style={[defaultStyles.viewContainer, style.viewContainer]}>
515520
<TouchableOpacity
@@ -519,6 +524,11 @@ export default class RNPickerSelect extends PureComponent {
519524
}}
520525
activeOpacity={1}
521526
{...touchableWrapperProps}
527+
accessible
528+
// "combobox" has no effect on iOS (facebook/react-native#50123), use "button" instead
529+
accessibilityRole="button"
530+
accessibilityLabel={accessibilityLabel}
531+
accessibilityState={{ disabled, expanded: showPicker }}
522532
>
523533
{this.renderTextInputOrChildren()}
524534
</TouchableOpacity>

test/test.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,108 @@ describe("RNPickerSelect", () => {
657657
});
658658
});
659659

660+
describe("iOS mode accessibility", () => {
661+
beforeEach(() => {
662+
Platform.OS = "ios";
663+
});
664+
665+
it("should have accessibility props on the wrapper (iOS)", () => {
666+
const wrapper = shallow(
667+
<RNPickerSelect
668+
items={selectItems}
669+
onValueChange={noop}
670+
pickerProps={{
671+
accessibilityLabel: "Select a language",
672+
}}
673+
/>,
674+
);
675+
676+
const touchable = wrapper.find('[testID="ios_touchable_wrapper"]');
677+
678+
expect(touchable.props().accessible).toEqual(true);
679+
expect(touchable.props().accessibilityRole).toEqual("button");
680+
expect(touchable.props().accessibilityLabel).toEqual("Select a language");
681+
expect(touchable.props().accessibilityState).toEqual({ disabled: false, expanded: false });
682+
});
683+
684+
it("should use accessibilityLabel from pickerProps (iOS)", () => {
685+
const wrapper = shallow(
686+
<RNPickerSelect
687+
items={selectItems}
688+
onValueChange={noop}
689+
pickerProps={{
690+
accessibilityLabel: "Choose a color",
691+
}}
692+
/>,
693+
);
694+
695+
const touchable = wrapper.find('[testID="ios_touchable_wrapper"]');
696+
697+
expect(touchable.props().accessibilityLabel).toEqual("Choose a color");
698+
});
699+
700+
it("should have undefined accessibilityLabel when not provided via pickerProps (iOS)", () => {
701+
const wrapper = shallow(
702+
<RNPickerSelect items={selectItems} onValueChange={noop} />,
703+
);
704+
705+
const touchable = wrapper.find('[testID="ios_touchable_wrapper"]');
706+
707+
expect(touchable.props().accessibilityLabel).toBeUndefined();
708+
});
709+
710+
it("should set accessibilityState.expanded to true when picker is open (iOS)", () => {
711+
const wrapper = shallow(
712+
<RNPickerSelect items={selectItems} onValueChange={noop} />,
713+
);
714+
715+
wrapper.find('[testID="ios_touchable_wrapper"]').simulate("press");
716+
717+
const touchable = wrapper.find('[testID="ios_touchable_wrapper"]');
718+
719+
expect(touchable.props().accessibilityState).toEqual({ disabled: false, expanded: true });
720+
});
721+
722+
it("should set accessibilityState.disabled to true when disabled (iOS)", () => {
723+
const wrapper = shallow(
724+
<RNPickerSelect items={selectItems} onValueChange={noop} disabled />,
725+
);
726+
727+
const touchable = wrapper.find('[testID="ios_touchable_wrapper"]');
728+
729+
expect(touchable.props().accessibilityState).toEqual({ disabled: true, expanded: false });
730+
});
731+
732+
it("should have accessibilityRole button on Done button (iOS)", () => {
733+
const wrapper = shallow(
734+
<RNPickerSelect items={selectItems} onValueChange={noop} />,
735+
);
736+
737+
wrapper.find('[testID="ios_touchable_wrapper"]').simulate("press");
738+
739+
const doneButton = wrapper.find('[testID="done_button"]');
740+
741+
expect(doneButton.props().accessibilityRole).toEqual("button");
742+
expect(doneButton.props().accessibilityLabel).toEqual("Done");
743+
});
744+
745+
it("should use custom doneText as accessibilityLabel on Done button (iOS)", () => {
746+
const wrapper = shallow(
747+
<RNPickerSelect
748+
items={selectItems}
749+
onValueChange={noop}
750+
doneText="Confirm"
751+
/>,
752+
);
753+
754+
wrapper.find('[testID="ios_touchable_wrapper"]').simulate("press");
755+
756+
const doneButton = wrapper.find('[testID="done_button"]');
757+
758+
expect(doneButton.props().accessibilityLabel).toEqual("Confirm");
759+
});
760+
});
761+
660762
it("should call the onClose callback when set", () => {
661763
Platform.OS = "ios";
662764
const onCloseSpy = jest.fn();

0 commit comments

Comments
 (0)