diff --git a/Examples/UIExplorer/PickerAndroidExample.js b/Examples/UIExplorer/PickerAndroidExample.js index 947a0129e..0f76f6e84 100644 --- a/Examples/UIExplorer/PickerAndroidExample.js +++ b/Examples/UIExplorer/PickerAndroidExample.js @@ -16,20 +16,21 @@ 'use strict'; const React = require('react-native'); +const StyleSheet = require('StyleSheet'); const UIExplorerBlock = require('UIExplorerBlock'); const UIExplorerPage = require('UIExplorerPage'); const { - PickerAndroid, + Picker, Text, TouchableWithoutFeedback, } = React; -const Item = PickerAndroid.Item; +const Item = Picker.Item; -const PickerAndroidExample = React.createClass({ +const PickerExample = React.createClass({ statics: { - title: '', + title: '', description: 'Provides multiple options to choose from, using either a dropdown menu or a dialog.', }, @@ -38,9 +39,8 @@ const PickerAndroidExample = React.createClass({ selected1: 'key1', selected2: 'key1', selected3: 'key1', - selected4: 'key1', color: 'red', - mode: PickerAndroid.MODE_DIALOG, + mode: Picker.MODE_DIALOG, }; }, @@ -48,93 +48,93 @@ const PickerAndroidExample = React.createClass({ render: function() { return ( - + - - - - + + + + - - - - + + + + - - - - - - - - - - - - Tap here to switch between dialog/dropdown. - + + + - - - - + + + - - - - + + + + - You can not change the value of this picker because it doesn't set a selected prop on - its items. + Cannot change the value of this picker because it doesn't update selectedValue. - - - - - - + + + + - - - - + + + + ); }, changeMode: function() { - const newMode = this.state.mode === PickerAndroid.MODE_DIALOG - ? PickerAndroid.MODE_DROPDOWN - : PickerAndroid.MODE_DIALOG; + const newMode = this.state.mode === Picker.MODE_DIALOG + ? Picker.MODE_DROPDOWN + : Picker.MODE_DIALOG; this.setState({mode: newMode}); }, - onSelect: function(key: string, value: string) { + onValueChange: function(key: string, value: string) { const newState = {}; newState[key] = value; this.setState(newState); }, }); -module.exports = PickerAndroidExample; +var styles = StyleSheet.create({ + picker: { + width: 100, + }, +}); + +module.exports = PickerExample; diff --git a/Libraries/Components/Picker/Picker.js b/Libraries/Components/Picker/Picker.js new file mode 100644 index 000000000..9db560fb9 --- /dev/null +++ b/Libraries/Components/Picker/Picker.js @@ -0,0 +1,152 @@ +/** + * 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. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule Picker + * @flow + */ + +'use strict'; + +var ColorPropType = require('ColorPropType'); +var PickerIOS = require('PickerIOS'); +var PickerAndroid = require('PickerAndroid'); +var Platform = require('Platform'); +var React = require('React'); +var StyleSheet = require('StyleSheet'); +var StyleSheetPropType = require('StyleSheetPropType'); +var TextStylePropTypes = require('TextStylePropTypes'); +var UnimplementedView = require('UnimplementedView'); +var View = require('View'); +var ViewStylePropTypes = require('ViewStylePropTypes'); + +var itemStylePropType = StyleSheetPropType(TextStylePropTypes); + +var pickerStyleType = StyleSheetPropType({ + ...ViewStylePropTypes, + color: ColorPropType, +}); + +var MODE_DIALOG = 'dialog'; +var MODE_DROPDOWN = 'dropdown'; + +/** + * Renders the native picker component on iOS and Android. Example: + * + * this.setState({language: lang})}> + * + * + * + */ +var Picker = React.createClass({ + + statics: { + /** + * On Android, display the options in a dialog. + */ + MODE_DIALOG: MODE_DIALOG, + /** + * On Android, display the options in a dropdown (this is the default). + */ + MODE_DROPDOWN: MODE_DROPDOWN, + }, + + getDefaultProps: function() { + return { + mode: MODE_DIALOG, + }; + }, + + propTypes: { + ...View.propTypes, + style: pickerStyleType, + /** + * Value matching value of one of the items. Can be a string or an integer. + */ + selectedValue: React.PropTypes.any, + /** + * Callback for when an item is selected. This is called with the following parameters: + * - `itemValue`: the `value` prop of the item that was selected + * - `itemPosition`: the index of the selected item in this picker + */ + onValueChange: React.PropTypes.func, + /** + * If set to false, the picker will be disabled, i.e. the user will not be able to make a + * selection. + * @platform android + */ + enabled: React.PropTypes.bool, + /** + * On Android, specifies how to display the selection items when the user taps on the picker: + * - 'dialog': Show a modal dialog. This is the default. + * - 'dropdown': Shows a dropdown anchored to the picker view + * + * @platform android + */ + mode: React.PropTypes.oneOf([MODE_DIALOG, MODE_DROPDOWN]), + /** + * Style to apply to each of the item labels. + * @platform ios + */ + itemStyle: itemStylePropType, + /** + * Prompt string for this picker, used on Android in dialog mode as the title of the dialog. + * @platform android + */ + prompt: React.PropTypes.string, + /** + * Used to locate this view in end-to-end tests. + */ + testID: React.PropTypes.string, + }, + + render: function() { + if (Platform.OS === 'ios') { + return {this.props.children}; + } else if (Platform.OS === 'android') { + return {this.props.children}; + } else { + return ; + } + }, +}); + +/** + * Individual selectable item in a Picker. + */ +Picker.Item = React.createClass({ + + propTypes: { + /** + * Text to display for this item. + */ + label: React.PropTypes.string.isRequired, + /** + * The value to be passed to picker's `onValueChange` callback when + * this item is selected. Can be a string or an integer. + */ + value: React.PropTypes.any, + /** + * Color of this item's text. + * @platform android + */ + color: ColorPropType, + /** + * Used to locate the item in end-to-end tests. + */ + testID: React.PropTypes.string, + }, + + render: function() { + // The items are not rendered directly + throw null; + }, +}); + +module.exports = Picker; diff --git a/Libraries/Components/Picker/PickerAndroid.android.js b/Libraries/Components/Picker/PickerAndroid.android.js new file mode 100644 index 000000000..2e95b32f1 --- /dev/null +++ b/Libraries/Components/Picker/PickerAndroid.android.js @@ -0,0 +1,141 @@ +/** + * 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. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule PickerAndroid + * @flow + */ + +'use strict'; + +var ColorPropType = require('ColorPropType'); +var React = require('React'); +var ReactChildren = require('ReactChildren'); +var ReactPropTypes = require('ReactPropTypes'); +var StyleSheet = require('StyleSheet'); +var StyleSheetPropType = require('StyleSheetPropType'); +var View = require('View'); +var ViewStylePropTypes = require('ViewStylePropTypes'); + +var processColor = require('processColor'); +var requireNativeComponent = require('requireNativeComponent'); + +var REF_PICKER = 'picker'; +var MODE_DIALOG = 'dialog'; +var MODE_DROPDOWN = 'dropdown'; + +var pickerStyleType = StyleSheetPropType({ + ...ViewStylePropTypes, + color: ColorPropType, +}); + +type Event = Object; + +/** + * Not exposed as a public API - use instead. + */ +var PickerAndroid = React.createClass({ + + propTypes: { + ...View.propTypes, + style: pickerStyleType, + selectedValue: React.PropTypes.any, + enabled: ReactPropTypes.bool, + mode: ReactPropTypes.oneOf(['dialog', 'dropdown']), + onValueChange: ReactPropTypes.func, + prompt: ReactPropTypes.string, + testID: ReactPropTypes.string, + }, + + getInitialState: function() { + return this._stateFromProps(this.props); + }, + + componentWillReceiveProps: function(nextProps) { + this.setState(this._stateFromProps(nextProps)); + }, + + // Translate prop and children into stuff that the native picker understands. + _stateFromProps: function(props) { + var selectedIndex = 0; + let items = ReactChildren.map(props.children, (child, index) => { + if (child.props.value === props.selectedValue) { + selectedIndex = index; + } + let childProps = { + value: child.props.value, + label: child.props.label, + }; + if (child.props.color) { + childProps.color = processColor(child.props.color); + } + return childProps; + }); + return {selectedIndex, items}; + }, + + render: function() { + var Picker = this.props.mode === MODE_DROPDOWN ? DropdownPicker : DialogPicker; + + var nativeProps = { + enabled: this.props.enabled, + items: this.state.items, + mode: this.props.mode, + onSelect: this._onChange, + prompt: this.props.prompt, + selected: this.state.selectedIndex, + testID: this.props.testID, + style: [styles.pickerAndroid, this.props.style], + }; + + return ; + }, + + _onChange: function(event: Event) { + if (this.props.onValueChange) { + var position = event.nativeEvent.position; + if (position >= 0) { + var value = this.props.children[position].props.value; + this.props.onValueChange(value); + } else { + this.props.onValueChange(null); + } + } + + // The picker is a controlled component. This means we expect the + // on*Change handlers to be in charge of updating our + // `selectedValue` prop. That way they can also + // disallow/undo/mutate the selection of certain values. In other + // words, the embedder of this component should be the source of + // truth, not the native component. + if (this.refs[REF_PICKER] && this.state.selectedIndex !== event.nativeEvent.position) { + this.refs[REF_PICKER].setNativeProps({selected: this.state.selectedIndex}); + } + }, +}); + +var styles = StyleSheet.create({ + pickerAndroid: { + // The picker will conform to whatever width is given, but we do + // have to set the component's height explicitly on the + // surrounding view to ensure it gets rendered. + // TODO would be better to export a native constant for this, + // like in iOS the RCTDatePickerManager.m + height: 50, + }, +}); + +var cfg = { + nativeOnly: { + items: true, + selected: true, + } +} +var DropdownPicker = requireNativeComponent('AndroidDropdownPicker', PickerAndroid, cfg); +var DialogPicker = requireNativeComponent('AndroidDialogPicker', PickerAndroid, cfg); + +module.exports = PickerAndroid; diff --git a/Libraries/Components/Picker/PickerAndroid.ios.js b/Libraries/Components/Picker/PickerAndroid.ios.js new file mode 100644 index 000000000..fc5898828 --- /dev/null +++ b/Libraries/Components/Picker/PickerAndroid.ios.js @@ -0,0 +1,13 @@ +/** + * 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. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule PickerAndroid + */ +'use strict'; + +module.exports = require('UnimplementedView'); diff --git a/Libraries/Components/PickerAndroid/PickerAndroid.js b/Libraries/Components/PickerAndroid/PickerAndroid.js deleted file mode 100644 index 1ec105ca4..000000000 --- a/Libraries/Components/PickerAndroid/PickerAndroid.js +++ /dev/null @@ -1,213 +0,0 @@ -/** - * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule PickerAndroid - * @flow - */ - -'use strict'; - -var ColorPropType = require('ColorPropType'); -var React = require('React'); -var ReactChildren = require('ReactChildren'); -var ReactPropTypes = require('ReactPropTypes'); -var StyleSheetPropType = require('StyleSheetPropType'); -var View = require('View'); -var ViewStylePropTypes = require('ViewStylePropTypes'); - -var processColor = require('processColor'); -var requireNativeComponent = require('requireNativeComponent'); - -var MODE_DIALOG = 'dialog'; -var MODE_DROPDOWN = 'dropdown'; -var REF_PICKER = 'picker'; - -var pickerStyleType = StyleSheetPropType({ - ...ViewStylePropTypes, - color: ColorPropType, -}); - -type Items = { - selected: number; - items: any[]; -}; - -type Event = Object; - -/** - * Individual selectable item in a Picker. - */ -var Item = React.createClass({ - - propTypes: { - /** - * Color of this item's text. - */ - color: ColorPropType, - /** - * Text to display for this item. - */ - text: ReactPropTypes.string.isRequired, - /** - * The value to be passed to picker's `onSelect` callback when this item is selected. - */ - value: ReactPropTypes.string, - /** - * If `true`, this item is selected and shown in the picker. - * Usually this is set based on state. - */ - selected: ReactPropTypes.bool, - /** - * Used to locate this view in end-to-end tests. - */ - testID: ReactPropTypes.string, - }, - - render: function() { - throw new Error('Picker items should never be rendered'); - }, - -}); - -/** - * - A React component that renders the native Picker widget on Android. The items - * that can be selected are specified as children views of type Item. Example usage: - * - * - * - * - * - */ -var PickerAndroid = React.createClass({ - - propTypes: { - ...View.propTypes, - style: pickerStyleType, - /** - * If set to false, the picker will be disabled, i.e. the user will not be able to make a - * selection. - */ - enabled: ReactPropTypes.bool, - /** - * Specifies how to display the selection items when the user taps on the picker: - * - * - dialog: Show a modal dialog - * - dropdown: Shows a dropdown anchored to the picker view - */ - mode: ReactPropTypes.oneOf([MODE_DIALOG, MODE_DROPDOWN]), - /** - * Callback for when an item is selected. This is called with the following parameters: - * - * - `itemValue`: the `value` prop of the item that was selected - * - `itemPosition`: the index of the selected item in this picker - */ - onSelect: ReactPropTypes.func, - /** - * Prompt string for this picker, currently only used in `dialog` mode as the title of the - * dialog. - */ - prompt: ReactPropTypes.string, - /** - * Used to locate this view in end-to-end tests. - */ - testID: ReactPropTypes.string, - }, - - statics: { - Item: Item, - MODE_DIALOG: MODE_DIALOG, - MODE_DROPDOWN: MODE_DROPDOWN, - }, - - getDefaultProps: function() { - return { - mode: MODE_DIALOG, - }; - }, - - render: function() { - var Picker = this.props.mode === MODE_DROPDOWN ? DropdownPicker : DialogPicker; - - var { selected, items } = this._getItems(); - - var nativeProps = { - enabled: this.props.enabled, - items: items, - mode: this.props.mode, - onSelect: this._onSelect, - prompt: this.props.prompt, - selected: selected, - style: this.props.style, - testID: this.props.testID, - }; - - return ; - }, - - /** - * Transform this view's children into an array of items to be passed to the native component. - * Since we're traversing the children, also determine the selected position. - * - * @returns an object with two keys: - * - * - `selected` (number) - the index of the selected item - * - `items` (array) - the items of this picker, as an array of strings - */ - _getItems: function(): Items { - var items = []; - var selected = 0; - ReactChildren.forEach(this.props.children, function(child, index) { - var childProps = Object.assign({}, child.props); - if (childProps.color) { - childProps.color = processColor(childProps.color); - } - items.push(childProps); - if (childProps.selected) { - selected = index; - } - }); - return { - selected: selected, - items: items, - }; - }, - - _onSelect: function(event: Event) { - if (this.props.onSelect) { - var position = event.nativeEvent.position; - if (position >= 0) { - var value = this.props.children[position].props.value; - this.props.onSelect(value, position); - } else { - this.props.onSelect(null, position); - } - } - - // The native Picker has changed, but the props haven't (yet). If - // the handler decides to not accept the new value or do something - // else with it we might end up in a bad state, so we reset the - // selection on the native component. - // tl;dr: PickerAndroid is a controlled component. - var { selected } = this._getItems(); - if (this.refs[REF_PICKER]) { - this.refs[REF_PICKER].setNativeProps({selected: selected}); - } - }, - -}); - -var cfg = { - nativeOnly: { - items: true, - selected: true, - } -} -var DropdownPicker = requireNativeComponent('AndroidDropdownPicker', PickerAndroid, cfg); -var DialogPicker = requireNativeComponent('AndroidDialogPicker', PickerAndroid, cfg); - -module.exports = PickerAndroid; diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index da90b75c0..6efc9af3c 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -25,7 +25,7 @@ var ReactNative = { get Modal() { return require('Modal'); }, get Navigator() { return require('Navigator'); }, get NavigatorIOS() { return require('NavigatorIOS'); }, - get PickerAndroid() { return require('PickerAndroid'); }, + get Picker() { return require('Picker'); }, get PickerIOS() { return require('PickerIOS'); }, get ProgressBarAndroid() { return require('ProgressBarAndroid'); }, get ProgressViewIOS() { return require('ProgressViewIOS'); }, diff --git a/Libraries/react-native/react-native.js.flow b/Libraries/react-native/react-native.js.flow index 52d7eeddb..a0c20f5e0 100644 --- a/Libraries/react-native/react-native.js.flow +++ b/Libraries/react-native/react-native.js.flow @@ -37,7 +37,7 @@ var ReactNative = Object.assign(Object.create(require('React')), { Modal: require('Modal'), Navigator: require('Navigator'), NavigatorIOS: require('NavigatorIOS'), - PickerAndroid: require('PickerAndroid'), + Picker: require('Picker'), PickerIOS: require('PickerIOS'), ProgressBarAndroid: require('ProgressBarAndroid'), ProgressViewIOS: require('ProgressViewIOS'), diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/picker/ReactPickerManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/picker/ReactPickerManager.java index fe2f23cd0..2c8c24038 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/picker/ReactPickerManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/picker/ReactPickerManager.java @@ -128,7 +128,7 @@ public abstract class ReactPickerManager extends SimpleViewManager } TextView textView = (TextView) convertView; - textView.setText(item.getString("text")); + textView.setText(item.getString("label")); if (!isDropdown && mPrimaryTextColor != null) { textView.setTextColor(mPrimaryTextColor); } else if (item.hasKey("color") && !item.isNull("color")) {