From b7e970f4e6b48ee2c063d01914df1df4556c7d94 Mon Sep 17 00:00:00 2001 From: Kenneth Kufluk Date: Mon, 6 Nov 2017 14:26:53 -0800 Subject: [PATCH] [add] Picker and Picker.Item components Close #705 --- .../1-components/Picker/PickerScreen.js | 113 +++++++++++++++++ .../Picker/examples/PickerExample.js | 28 +++++ src/components/Picker/PickerItemPropType.js | 26 ++++ src/components/Picker/PickerStylePropTypes.js | 20 +++ .../__snapshots__/index-test.js.snap | 17 +++ src/components/Picker/__tests__/index-test.js | 74 +++++++++++ src/components/Picker/index.js | 118 +++++++++++++++++- 7 files changed, 392 insertions(+), 4 deletions(-) create mode 100644 docs/storybook/1-components/Picker/PickerScreen.js create mode 100644 docs/storybook/1-components/Picker/examples/PickerExample.js create mode 100644 src/components/Picker/PickerItemPropType.js create mode 100644 src/components/Picker/PickerStylePropTypes.js create mode 100644 src/components/Picker/__tests__/__snapshots__/index-test.js.snap create mode 100644 src/components/Picker/__tests__/index-test.js diff --git a/docs/storybook/1-components/Picker/PickerScreen.js b/docs/storybook/1-components/Picker/PickerScreen.js new file mode 100644 index 00000000..3943bdd3 --- /dev/null +++ b/docs/storybook/1-components/Picker/PickerScreen.js @@ -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 = () => ( + + + + Renders the native <select> component. + +
+ + + +` + }} + /> + + + }} + /> + + ( + { + window.alert(`itemValue: ${itemValue}, itemPosition: ${itemPosition}`); + }} + /> + ) + }} + /> + + + }} + /> + + + } + /> + + +
+
+ + + Individual selectable item in a Picker. + +
+ + + +
+
+
+); + +storiesOf('Components', module).add('Picker', PickerScreen); diff --git a/docs/storybook/1-components/Picker/examples/PickerExample.js b/docs/storybook/1-components/Picker/examples/PickerExample.js new file mode 100644 index 00000000..df785fe3 --- /dev/null +++ b/docs/storybook/1-components/Picker/examples/PickerExample.js @@ -0,0 +1,28 @@ +/** + * @flow + */ + +import React from 'react'; +import { Picker, StyleSheet, View } from 'react-native'; + +const PickerExample = props => ( + + + + + + + + + + + +); + +const styles = StyleSheet.create({ + rootl: { + alignItems: 'flex-start' + } +}); + +export default PickerExample; diff --git a/src/components/Picker/PickerItemPropType.js b/src/components/Picker/PickerItemPropType.js new file mode 100644 index 00000000..531a2bf0 --- /dev/null +++ b/src/components/Picker/PickerItemPropType.js @@ -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; diff --git a/src/components/Picker/PickerStylePropTypes.js b/src/components/Picker/PickerStylePropTypes.js new file mode 100644 index 00000000..85f35f64 --- /dev/null +++ b/src/components/Picker/PickerStylePropTypes.js @@ -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; diff --git a/src/components/Picker/__tests__/__snapshots__/index-test.js.snap b/src/components/Picker/__tests__/__snapshots__/index-test.js.snap new file mode 100644 index 00000000..8a110714 --- /dev/null +++ b/src/components/Picker/__tests__/__snapshots__/index-test.js.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/Picker prop "children" renders items 1`] = ` + +`; diff --git a/src/components/Picker/__tests__/index-test.js b/src/components/Picker/__tests__/index-test.js new file mode 100644 index 00000000..11677f44 --- /dev/null +++ b/src/components/Picker/__tests__/index-test.js @@ -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 = ( + + + + + ); + const component = shallow(picker); + expect(component).toMatchSnapshot(); + }); + }); + + describe('prop "enabled"', () => { + test('picker is disabled if false', () => { + const 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 = ( + + + + + ); + 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 = ( + + + + + ); + const component = shallow(picker); + expect(component.find('select').prop('value')).toBe('value-2'); + }); + + test('selects the correct item (number)', () => { + const picker = ( + + + + + ); + const component = shallow(picker); + expect(component.find('select').prop('value')).toBe(22); + }); + }); +}); diff --git a/src/components/Picker/index.js b/src/components/Picker/index.js index 909a0b90..f6c51d45 100644 --- a/src/components/Picker/index.js +++ b/src/components/Picker/index.js @@ -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 { + 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, + enabled?: boolean, + onValueChange?: Function, + selectedValue?: number | string, + style?: pickerStyleType, + testID?: string, + /* compat */ + itemStyle?: itemStylePropType, + mode?: string, + prompt?: string +}; + +class Picker extends Component { + 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);