Skip to content

Commit

Permalink
Open sourced spinner aka picker aka drop down for android
Browse files Browse the repository at this point in the history
Reviewed By: mkonicek

Differential Revision: D2830803

fb-gh-sync-id: e6b6fcdbe33d942180cf2c1041076ad71d0473ce
  • Loading branch information
bestander authored and facebook-github-bot-4 committed Jan 15, 2016
1 parent cd89016 commit 1843709
Show file tree
Hide file tree
Showing 11 changed files with 779 additions and 0 deletions.
142 changes: 142 additions & 0 deletions Examples/UIExplorer/PickerAndroidExample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
* Facebook reserves all rights not expressly granted.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* @flow
*/
'use strict';

const React = require('react-native');
const UIExplorerBlock = require('UIExplorerBlock');
const UIExplorerPage = require('UIExplorerPage');

const {
PickerAndroid,
Text,
TouchableWithoutFeedback,
} = React;
const Item = PickerAndroid.Item;

const PickerAndroidExample = React.createClass({
getInitialState: function() {
return {
selected1: 'key1',
selected2: 'key1',
selected3: 'key1',
selected4: 'key1',
color: 'red',
mode: PickerAndroid.MODE_DIALOG,
};
},

displayName: 'Android Picker',

render: function() {
return (
<UIExplorerPage title="<PickerAndroid>">
<UIExplorerBlock title="Basic Picker">
<PickerAndroid
style={{width: 100, height: 56}}
onSelect={this.onSelect.bind(this, 'selected1')}>
<Item text="hello" value="key0" selected={this.state.selected1 === 'key0'} />
<Item text="world" value="key1" selected={this.state.selected1 === 'key1'} />
</PickerAndroid>
</UIExplorerBlock>
<UIExplorerBlock title="Disabled picker">
<PickerAndroid style={{width: 100, height: 56}} enabled={false}>
<Item text="hello" value="key0" selected={this.state.selected1 === 'key0'} />
<Item text="world" value="key1" selected={this.state.selected1 === 'key1'} />
</PickerAndroid>
</UIExplorerBlock>
<UIExplorerBlock title="Dropdown Picker">
<PickerAndroid
style={{width: 100, height: 56}}
onSelect={this.onSelect.bind(this, 'selected2')}
mode="dropdown">
<Item text="hello" value="key0" selected={this.state.selected2 === 'key0'} />
<Item text="world" value="key1" selected={this.state.selected2 === 'key1'} />
</PickerAndroid>
</UIExplorerBlock>
<UIExplorerBlock title="Alternating Picker">
<PickerAndroid
style={{width: 100, height: 56}}
onSelect={this.onSelect.bind(this, 'selected3')}
mode={this.state.mode}>
<Item text="hello" value="key0" selected={this.state.selected3 === 'key0'} />
<Item text="world" value="key1" selected={this.state.selected3 === 'key1'} />
</PickerAndroid>
<TouchableWithoutFeedback onPress={this.changeMode}>
<Text>Tap here to switch between dialog/dropdown.</Text>
</TouchableWithoutFeedback>
</UIExplorerBlock>
<UIExplorerBlock title="Picker with prompt message">
<PickerAndroid
style={{width: 100, height: 56}}
onSelect={this.onSelect.bind(this, 'selected4')}
prompt="Pick one, just one">
<Item text="hello" value="key0" selected={this.state.selected4 === 'key0'} />
<Item text="world" value="key1" selected={this.state.selected4 === 'key1'} />
</PickerAndroid>
</UIExplorerBlock>
<UIExplorerBlock title="Picker with no listener">
<PickerAndroid style={{width: 100, height: 56}}>
<Item text="hello" value="key0" />
<Item text="world" value="key1" />
</PickerAndroid>
<Text>
You can not change the value of this picker because it doesn't set a selected prop on
its items.
</Text>
</UIExplorerBlock>
<UIExplorerBlock title="Colorful pickers">
<PickerAndroid style={{width: 100, height: 56, color: 'black'}}
onSelect={this.onSelect.bind(this, 'color')}
mode="dropdown">
<Item text="red" color="red" value="red" selected={this.state.color === 'red'}/>
<Item text="green" color="green" value="green" selected={this.state.color === 'green'}/>
<Item text="blue" color="blue" value="blue" selected={this.state.color === 'blue'}/>
</PickerAndroid>
<PickerAndroid style={{width: 100, height: 56}}
onSelect={this.onSelect.bind(this, 'color')}
mode="dialog">
<Item text="red" color="red" value="red" selected={this.state.color === 'red'}/>
<Item text="green" color="green" value="green" selected={this.state.color === 'green'}/>
<Item text="blue" color="blue" value="blue" selected={this.state.color === 'blue'} />
</PickerAndroid>
</UIExplorerBlock>
</UIExplorerPage>
);
},

changeMode: function() {
const newMode = this.state.mode === PickerAndroid.MODE_DIALOG
? PickerAndroid.MODE_DROPDOWN
: PickerAndroid.MODE_DIALOG;
this.setState({mode: newMode});
},

onSelect: function(key, value) {
const newState = {};
newState[key] = value;
this.setState(newState);
},
});

exports.title = '<PickerAndroid>';
exports.displayName = 'PickerAndroidExample';
exports.description = 'The Android Picker component provides multiple options to choose from';
exports.examples = [
{
title: 'PickerAndroidExample',
render(): ReactElement { return <PickerAndroidExample />; }
},
];
1 change: 1 addition & 0 deletions Examples/UIExplorer/UIExplorerList.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ var COMPONENTS = [
require('./ScrollViewSimpleExample'),
require('./SwitchExample'),
require('./RefreshControlExample'),
require('./PickerAndroidExample'),
require('./PullToRefreshViewAndroidExample.android'),
require('./TextExample.android'),
require('./TextInputExample.android'),
Expand Down
213 changes: 213 additions & 0 deletions Libraries/Components/PickerAndroid/PickerAndroid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
/**
* 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');
},

});

/**
* <PickerAndroid> - 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:
*
* <PickerAndroid>
* <PickerAndroid.Item text="Java" value="js" />
* <PickerAndroid.Item text="JavaScript" value="java" selected={true} />
* </PickerAndroid>
*/
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 <Picker ref={REF_PICKER} {...nativeProps} />;
},

/**
* 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;
1 change: 1 addition & 0 deletions Libraries/react-native/react-native.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +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 PickerIOS() { return require('PickerIOS'); },
get ProgressBarAndroid() { return require('ProgressBarAndroid'); },
get ProgressViewIOS() { return require('ProgressViewIOS'); },
Expand Down
1 change: 1 addition & 0 deletions Libraries/react-native/react-native.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ var ReactNative = Object.assign(Object.create(require('React')), {
Modal: require('Modal'),
Navigator: require('Navigator'),
NavigatorIOS: require('NavigatorIOS'),
PickerAndroid: require('PickerAndroid'),
PickerIOS: require('PickerIOS'),
ProgressBarAndroid: require('ProgressBarAndroid'),
ProgressViewIOS: require('ProgressViewIOS'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import com.facebook.react.views.art.ARTSurfaceViewManager;
import com.facebook.react.views.drawer.ReactDrawerLayoutManager;
import com.facebook.react.views.image.ReactImageManager;
import com.facebook.react.views.picker.ReactDialogPickerManager;
import com.facebook.react.views.picker.ReactDropdownPickerManager;
import com.facebook.react.views.progressbar.ReactProgressBarViewManager;
import com.facebook.react.views.recyclerview.RecyclerViewBackedScrollViewManager;
import com.facebook.react.views.scroll.ReactHorizontalScrollViewManager;
Expand Down Expand Up @@ -82,7 +84,9 @@ public List<ViewManager> createViewManagers(ReactApplicationContext reactContext
ARTRenderableViewManager.createARTShapeViewManager(),
ARTRenderableViewManager.createARTTextViewManager(),
new ARTSurfaceViewManager(),
new ReactDialogPickerManager(),
new ReactDrawerLayoutManager(),
new ReactDropdownPickerManager(),
new ReactHorizontalScrollViewManager(),
new ReactImageManager(),
new ReactProgressBarViewManager(),
Expand Down
Loading

3 comments on commit 1843709

@mkonicek
Copy link
Contributor

Choose a reason for hiding this comment

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

🚀

@kanerogers
Copy link

Choose a reason for hiding this comment

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

Oh my GOD.

@danleveille
Copy link

Choose a reason for hiding this comment

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

So excited about this. Trying it now! It's not possible to style the actual dropdown or dialog, is it? (I'm trying to set the item color to white, but it shows up white on white in the dropdown.)

Please sign in to comment.