Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 27 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export default class RNPickerSelect extends PureComponent {
this.scrollToInput = this.scrollToInput.bind(this);
this.togglePicker = this.togglePicker.bind(this);
this.renderInputAccessoryView = this.renderInputAccessoryView.bind(this);
this.androidPickerRef = React.createRef();
}

componentDidUpdate = (prevProps, prevState) => {
Expand Down Expand Up @@ -579,16 +580,41 @@ export default class RNPickerSelect extends PureComponent {
const { selectedItem } = this.state;

const Component = fixAndroidTouchableBug ? View : TouchableOpacity;
const pickerRef = (pickerProps && pickerProps.ref) || this.androidPickerRef;

const handleAccessibilityAction = (event) => {
if (disabled) {
return;
}
if (event.nativeEvent.actionName === 'activate') {
if (pickerRef && pickerRef.current && pickerRef.current.focus) {
pickerRef.current.focus();
}
}
};

const accessibilityLabel = selectedItem.inputLabel || selectedItem.label;

Choose a reason for hiding this comment

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

Let's just pass whatever accessibilityLabel we pass to RNPickerSelect manually

    <RNPickerSelect
      pickerProps={{
        accessibilityLabel: selectedItem.title,
      }}
    >


return (
<Component
testID="android_touchable_wrapper"
onPress={onOpen}
activeOpacity={1}
{...touchableWrapperProps}
accessible
accessibilityRole="combobox"
accessibilityLabel={accessibilityLabel}
accessibilityState={{ disabled }}
onAccessibilityAction={handleAccessibilityAction}
accessibilityActions={[{ name: 'activate' }]}

Choose a reason for hiding this comment

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

Should we add support for 'escape' action too?

>
<View style={style.headlessAndroidContainer}>
<View
style={style.headlessAndroidContainer}
importantForAccessibility="no-hide-descendants"
>
{this.renderTextInputOrChildren()}
<Picker
ref={pickerRef}
style={[
Icon ? { backgroundColor: 'transparent' } : {}, // to hide native icon
defaultStyles.headlessAndroidPicker,
Expand Down
150 changes: 150 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,156 @@ describe('RNPickerSelect', () => {
expect(touchable.type().displayName).toEqual('View');
});

describe('Android headless mode accessibility', () => {
beforeEach(() => {
Platform.OS = 'android';
});

it('should have accessibility props on the wrapper (Android headless)', () => {
const wrapper = shallow(
<RNPickerSelect
items={selectItems}
onValueChange={noop}
useNativeAndroidPickerStyle={false}
/>
);

const touchable = wrapper.find('[testID="android_touchable_wrapper"]');

expect(touchable.props().accessible).toEqual(true);
expect(touchable.props().accessibilityRole).toEqual('combobox');
// Default placeholder label is "Select an item..."
expect(touchable.props().accessibilityLabel).toEqual('Select an item...');
expect(touchable.props().accessibilityState).toEqual({ disabled: false });
expect(touchable.props().accessibilityActions).toEqual([{ name: 'activate' }]);
expect(touchable.props().onAccessibilityAction).toBeDefined();
});

it('should use selectedItem label as accessibilityLabel (Android headless)', () => {
const wrapper = shallow(
<RNPickerSelect
items={selectItems}
placeholder={{}}
onValueChange={noop}
useNativeAndroidPickerStyle={false}
value="orange"
/>
);

const touchable = wrapper.find('[testID="android_touchable_wrapper"]');

expect(touchable.props().accessibilityLabel).toEqual('Orange');
});

it('should use inputLabel as accessibilityLabel when provided (Android headless)', () => {
const itemsWithInputLabel = [{ label: 'Red', value: 'red', inputLabel: 'RED COLOR' }];
const wrapper = shallow(
<RNPickerSelect
items={itemsWithInputLabel}
placeholder={{}}
onValueChange={noop}
useNativeAndroidPickerStyle={false}
value="red"
/>
);

const touchable = wrapper.find('[testID="android_touchable_wrapper"]');

expect(touchable.props().accessibilityLabel).toEqual('RED COLOR');
});

it('should have importantForAccessibility on inner container (Android headless)', () => {
const wrapper = shallow(
<RNPickerSelect
items={selectItems}
onValueChange={noop}
useNativeAndroidPickerStyle={false}
/>
);

const touchable = wrapper.find('[testID="android_touchable_wrapper"]');
const innerContainer = touchable.children().first();

expect(innerContainer.props().importantForAccessibility).toEqual('no-hide-descendants');
});

it('should not trigger picker when disabled and accessibility action is called (Android headless)', () => {
const wrapper = shallow(
<RNPickerSelect
items={selectItems}
onValueChange={noop}
useNativeAndroidPickerStyle={false}
disabled
/>
);

const touchable = wrapper.find('[testID="android_touchable_wrapper"]');
const onAccessibilityAction = touchable.props().onAccessibilityAction;

// This should not throw and should be a no-op when disabled
expect(() => {
onAccessibilityAction({ nativeEvent: { actionName: 'activate' } });
}).not.toThrow();
});

it('should set accessibilityState.disabled to true when disabled (Android headless)', () => {
const wrapper = shallow(
<RNPickerSelect
items={selectItems}
onValueChange={noop}
useNativeAndroidPickerStyle={false}
disabled
/>
);

const touchable = wrapper.find('[testID="android_touchable_wrapper"]');

expect(touchable.props().accessibilityState).toEqual({ disabled: true });
});

it('should call pickerRef.focus() when accessibility action "activate" is triggered (Android headless)', () => {
const mockFocus = jest.fn();
const mockRef = { current: { focus: mockFocus } };

const wrapper = shallow(
<RNPickerSelect
items={selectItems}
onValueChange={noop}
useNativeAndroidPickerStyle={false}
pickerProps={{ ref: mockRef }}
/>
);

const touchable = wrapper.find('[testID="android_touchable_wrapper"]');
const onAccessibilityAction = touchable.props().onAccessibilityAction;

onAccessibilityAction({ nativeEvent: { actionName: 'activate' } });

expect(mockFocus).toHaveBeenCalledTimes(1);
});

it('should not call pickerRef.focus() for non-activate actions (Android headless)', () => {
const mockFocus = jest.fn();
const mockRef = { current: { focus: mockFocus } };

const wrapper = shallow(
<RNPickerSelect
items={selectItems}
onValueChange={noop}
useNativeAndroidPickerStyle={false}
pickerProps={{ ref: mockRef }}
/>
);

const touchable = wrapper.find('[testID="android_touchable_wrapper"]');
const onAccessibilityAction = touchable.props().onAccessibilityAction;

onAccessibilityAction({ nativeEvent: { actionName: 'longpress' } });

expect(mockFocus).not.toHaveBeenCalled();
});
});

it('should call the onClose callback when set', () => {
Platform.OS = 'ios';
const onCloseSpy = jest.fn();
Expand Down
Loading