[add] Picker and Picker.Item components

Close #705
This commit is contained in:
Kenneth Kufluk
2017-11-06 14:26:53 -08:00
committed by Nicolas Gallagher
parent 02e62ad5d6
commit b7e970f4e6
7 changed files with 392 additions and 4 deletions

View File

@@ -0,0 +1,113 @@
/* eslint-disable react/jsx-sort-props, react/jsx-no-bind, no-alert */
/**
* @flow
*/
import React from 'react';
import PickerExample from './examples/PickerExample';
import UIExplorer, {
AppText,
Description,
DocItem,
Section,
StyleList,
storiesOf
} from '../../ui-explorer';
import { View } from 'react-native';
const PickerScreen = () => (
<View>
<UIExplorer title="Picker">
<Description>
<AppText>Renders the native &lt;select&gt; component.</AppText>
</Description>
<Section title="Props">
<DocItem
name="children"
typeInfo="?Array<Picker.Item>"
description="The items to display in the picker."
example={{
code: `<Picker>
<Picker.Item label="Goblet of Fire" />
<Picker.Item label="Order of the Phoenix" />
</Picker>`
}}
/>
<DocItem
name="enabled"
typeInfo="?boolean"
description="If set to false, the picker will be disabled, i.e., the user will not be able to make a selection."
example={{
render: () => <PickerExample enabled={false} />
}}
/>
<DocItem
name="onValueChange"
typeInfo="?(itemValue, itemIndex) => void"
description="Callback for when an item is selected. This is called with the value and index prop of the item that was selected."
example={{
render: () => (
<PickerExample
onValueChange={(itemValue, itemPosition) => {
window.alert(`itemValue: ${itemValue}, itemPosition: ${itemPosition}`);
}}
/>
)
}}
/>
<DocItem
name="selectedValue"
typeInfo="?string"
description="Select the item with the matching value."
example={{
render: () => <PickerExample selectedValue="book-3" />
}}
/>
<DocItem
name="style"
typeInfo="?style"
description={
<StyleList
stylePropTypes={[
{
name: '…View#style'
},
{
name: 'color',
typeInfo: 'color'
}
]}
/>
}
/>
<DocItem
name="testID"
typeInfo="?string"
description="Used to locate this view in end-to-end tests."
/>
</Section>
</UIExplorer>
<UIExplorer title="Picker.Item" url="1-components/Picker">
<Description>Individual selectable item in a Picker.</Description>
<Section title="Props">
<DocItem name="label" typeInfo="string" description="Text to display for this item" />
<DocItem name="testID" typeInfo="?string" />
<DocItem
name="value"
typeInfo="?number | string"
description="The value to be passed to the picker's 'onValueChange' callback when this item is selected."
/>
</Section>
</UIExplorer>
</View>
);
storiesOf('Components', module).add('Picker', PickerScreen);

View File

@@ -0,0 +1,28 @@
/**
* @flow
*/
import React from 'react';
import { Picker, StyleSheet, View } from 'react-native';
const PickerExample = props => (
<View style={styles.root}>
<Picker {...props}>
<Picker.Item label="Sourcerer's Stone" value="book-1" />
<Picker.Item label="Chamber of Secrets" value="book-2" />
<Picker.Item label="Prisoner of Azkaban" value="book-3" />
<Picker.Item label="Goblet of Fire" value="book-4" />
<Picker.Item label="Order of the Phoenix" value="book-5" />
<Picker.Item label="Half-Blood Prince" value="book-6" />
<Picker.Item label="Deathly Hallows" value="book-7" />
</Picker>
</View>
);
const styles = StyleSheet.create({
rootl: {
alignItems: 'flex-start'
}
});
export default PickerExample;

View File

@@ -0,0 +1,26 @@
/**
* Copyright (c) 2017-present, Nicolas Gallagher.
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import React from 'react';
import Picker from './';
const PickerItemPropType = (props: Object, propName: string, componentName: string) => {
const prop = props[propName];
let error = null;
React.Children.forEach(prop, function(child) {
if (child.type !== Picker.Item) {
error = new Error('`Picker` children must be of type `Picker.Item`.');
}
});
return error;
};
export default PickerItemPropType;

View File

@@ -0,0 +1,20 @@
/**
* Copyright (c) 2017-present, Nicolas Gallagher.
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import ColorPropType from '../../propTypes/ColorPropType';
import ViewStylePropTypes from '../View/ViewStylePropTypes';
const PickerStylePropTypes = {
...ViewStylePropTypes,
color: ColorPropType
};
export default PickerStylePropTypes;

View File

@@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/Picker prop "children" renders items 1`] = `
<select
className="rn-fontFamily-poiln3 rn-fontSize-7cikom rn-marginTop-1mnahxq rn-marginRight-61z16t rn-marginBottom-p1pxzi rn-marginLeft-11wrixw"
onChange={[Function]}
>
<PickerItem
label="label-1"
value="value-1"
/>
<PickerItem
label="label-2"
value="value-2"
/>
</select>
`;

View File

@@ -0,0 +1,74 @@
/* eslint-env jasmine, jest */
import React from 'react';
import { shallow } from 'enzyme';
import Picker from '..';
describe('components/Picker', () => {
describe('prop "children"', () => {
test('renders items', () => {
const picker = (
<Picker>
<Picker.Item label="label-1" value="value-1" />
<Picker.Item label="label-2" value="value-2" />
</Picker>
);
const component = shallow(picker);
expect(component).toMatchSnapshot();
});
});
describe('prop "enabled"', () => {
test('picker is disabled if false', () => {
const picker = (
<Picker enabled={false}>
<Picker.Item label="label-1" value="value-1" />
<Picker.Item label="label-2" value="value-2" />
</Picker>
);
const component = shallow(picker);
expect(component.find('select').props().disabled).toBe(true);
});
});
describe('prop "onValueChange"', () => {
test('is called with (value, index)', () => {
const onValueChange = jest.fn();
const picker = (
<Picker onValueChange={onValueChange} selectedValue="value-1">
<Picker.Item label="label-1" value="value-1" />
<Picker.Item label="label-2" value="value-2" />
</Picker>
);
const component = shallow(picker);
component.find('select').simulate('change', {
target: { selectedIndex: '1', value: 'value-2' }
});
expect(onValueChange).toHaveBeenCalledWith('value-2', '1');
});
});
describe('prop "selectedValue"', () => {
test('selects the correct item (string)', () => {
const picker = (
<Picker selectedValue="value-2">
<Picker.Item label="label-1" value="value-1" />
<Picker.Item label="label-2" value="value-2" />
</Picker>
);
const component = shallow(picker);
expect(component.find('select').prop('value')).toBe('value-2');
});
test('selects the correct item (number)', () => {
const picker = (
<Picker selectedValue={22}>
<Picker.Item label="label-1" value={11} />
<Picker.Item label="label-2" value={22} />
</Picker>
);
const component = shallow(picker);
expect(component.find('select').prop('value')).toBe(22);
});
});
});

View File

@@ -1,4 +1,114 @@
import UnimplementedView from '../UnimplementedView';
const Picker = UnimplementedView;
Picker.Item = UnimplementedView;
export default Picker;
/**
* Copyright (c) 2017-present, Nicolas Gallagher.
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*
* @providesModule Picker
* @flow
*/
import applyNativeMethods from '../../modules/applyNativeMethods';
import { Component } from 'react';
import ColorPropType from '../../propTypes/ColorPropType';
import createElement from '../../modules/createElement';
import PickerItemPropType from './PickerItemPropType';
import PickerStylePropTypes from './PickerStylePropTypes';
import StyleSheetPropType from '../../propTypes/StyleSheetPropType';
import StyleSheet from '../../apis/StyleSheet';
import TextStylePropTypes from '../Text/TextStylePropTypes';
import { arrayOf, bool, func, number, oneOfType, string } from 'prop-types';
const pickerStyleType = StyleSheetPropType(PickerStylePropTypes);
const itemStylePropType = StyleSheetPropType(TextStylePropTypes);
type ItemProps = {
color?: ColorPropType,
label: string,
testID?: string,
value?: number | string
};
class PickerItem extends Component<ItemProps> {
static propTypes = {
color: ColorPropType,
label: string.isRequired,
testID: string,
value: oneOfType([number, string])
};
render() {
const { label, testID, value } = this.props;
return createElement('option', { label, testID, value });
}
}
type Props = {
children?: Array<typeof PickerItem>,
enabled?: boolean,
onValueChange?: Function,
selectedValue?: number | string,
style?: pickerStyleType,
testID?: string,
/* compat */
itemStyle?: itemStylePropType,
mode?: string,
prompt?: string
};
class Picker extends Component<Props> {
static propTypes = {
children: arrayOf(PickerItemPropType),
enabled: bool,
onValueChange: func,
selectedValue: oneOfType([number, string]),
style: pickerStyleType,
testID: string
};
static Item = PickerItem;
render() {
const {
children,
enabled,
selectedValue,
style,
testID,
/* eslint-disable */
itemStyle,
mode,
prompt
/* eslint-enable */
} = this.props;
return createElement('select', {
children,
disabled: enabled === false ? true : undefined,
onChange: this._handleChange,
style: [styles.initial, style],
testID,
value: selectedValue
});
}
_handleChange = (e: Object) => {
const { onValueChange } = this.props;
const { selectedIndex, value } = e.target;
if (onValueChange) {
onValueChange(value, selectedIndex);
}
};
}
const styles = StyleSheet.create({
initial: {
fontFamily: 'inherit',
fontSize: 'inherit',
margin: 0
}
});
export default applyNativeMethods(Picker);