diff --git a/packages/react-native-dom/Libraries/Components/Picker/Picker.dom.js b/packages/react-native-dom/Libraries/Components/Picker/Picker.dom.js new file mode 100644 index 000000000..d35322778 --- /dev/null +++ b/packages/react-native-dom/Libraries/Components/Picker/Picker.dom.js @@ -0,0 +1,156 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule Picker + * @flow + */ + +"use strict"; + +const ColorPropType = require("ColorPropType"); +const PickerDOM = require("PickerDOM"); +const Platform = require("Platform"); +const React = require("React"); +const PropTypes = require("prop-types"); +const StyleSheetPropType = require("StyleSheetPropType"); +const TextStylePropTypes = require("TextStylePropTypes"); +const UnimplementedView = require("UnimplementedView"); +const ViewPropTypes = require("ViewPropTypes"); +const ViewStylePropTypes = require("ViewStylePropTypes"); + +const itemStylePropType = StyleSheetPropType(TextStylePropTypes); + +const pickerStyleType = StyleSheetPropType({ + ...ViewStylePropTypes, + color: ColorPropType +}); + +const MODE_DIALOG = "dialog"; +const MODE_DROPDOWN = "dropdown"; + +/** + * Individual selectable item in a Picker. + */ +class PickerItem extends React.Component<{ + label: string, + value?: any, + color?: ColorPropType, + testID?: string +}> { + static propTypes = { + /** + * Text to display for this item. + */ + label: 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: PropTypes.any, + /** + * Color of this item's text. + * @platform android + */ + color: ColorPropType, + /** + * Used to locate the item in end-to-end tests. + */ + testID: PropTypes.string + }; + + render() { + // The items are not rendered directly + throw null; + } +} + +/** + * Renders the native picker component on iOS and Android. Example: + * + * this.setState({language: itemValue})}> + * + * + * + */ +class Picker extends React.Component<{ + style?: $FlowFixMe, + selectedValue?: any, + onValueChange?: Function, + enabled?: boolean, + mode?: "dialog" | "dropdown", + itemStyle?: $FlowFixMe, + prompt?: string, + testID?: string +}> { + /** + * On Android, display the options in a dialog. + */ + static MODE_DIALOG = MODE_DIALOG; + + /** + * On Android, display the options in a dropdown (this is the default). + */ + static MODE_DROPDOWN = MODE_DROPDOWN; + + static Item = PickerItem; + + static defaultProps = { + mode: MODE_DIALOG + }; + + // $FlowFixMe(>=0.41.0) + static propTypes = { + ...ViewPropTypes, + style: pickerStyleType, + /** + * Value matching value of one of the items. Can be a string or an integer. + */ + selectedValue: 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: 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: 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: PropTypes.oneOf(["dialog", "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: PropTypes.string, + /** + * Used to locate this view in end-to-end tests. + */ + testID: PropTypes.string + }; + + render() { + return {this.props.children}; + } +} + +module.exports = Picker; diff --git a/packages/react-native-dom/Libraries/Components/Picker/PickerDOM.dom.js b/packages/react-native-dom/Libraries/Components/Picker/PickerDOM.dom.js new file mode 100644 index 000000000..fce3c820f --- /dev/null +++ b/packages/react-native-dom/Libraries/Components/Picker/PickerDOM.dom.js @@ -0,0 +1,152 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule PickerDOM + * + * This is a controlled component version of RCTPickerDOM + */ +"use strict"; + +const ColorPropType = require("ColorPropType"); +const NativeMethodsMixin = require("NativeMethodsMixin"); +const React = require("React"); +const PropTypes = require("prop-types"); +const StyleSheet = require("StyleSheet"); +const StyleSheetPropType = require("StyleSheetPropType"); +const TextStylePropTypes = require("TextStylePropTypes"); +const View = require("View"); +const ViewPropTypes = require("ViewPropTypes"); +const ViewStylePropTypes = require("ViewStylePropTypes"); +const processColor = require("processColor"); + +const createReactClass = require("create-react-class"); +const requireNativeComponent = require("requireNativeComponent"); + +const pickerStyleType = StyleSheetPropType({ + ...ViewStylePropTypes, + color: ColorPropType +}); + +const PickerDOM = createReactClass({ + displayName: "PickerDOM", + mixins: [NativeMethodsMixin], + + propTypes: { + ...ViewPropTypes, + style: pickerStyleType, + selectedValue: PropTypes.any, + enabled: PropTypes.bool, + onValueChange: PropTypes.func, + selectedValue: PropTypes.any // string or integer basically + }, + + getInitialState: function() { + return this._stateFromProps(this.props); + }, + + UNSAFE_componentWillReceiveProps: function(nextProps) { + this.setState(this._stateFromProps(nextProps)); + }, + + // Translate PickerDOM prop and children into stuff that RCTPickerDOM understands. + _stateFromProps: function(props) { + let selectedIndex = 0; + const items = []; + React.Children.toArray(props.children).forEach(function(child, index) { + if (child.props.value === props.selectedValue) { + selectedIndex = index; + } + items.push({ + value: child.props.value, + label: child.props.label, + textColor: processColor(child.props.color) + }); + }); + return { selectedIndex, items }; + }, + + render: function() { + return ( + (this._picker = picker)} + style={[styles.pickerDOM, this.props.style]} + items={this.state.items} + selectedIndex={this.state.selectedIndex} + onChange={this._onChange} + enabled={this.props.enabled} + /> + ); + }, + + _onChange: function(event) { + if (this.props.onChange) { + this.props.onChange(event); + } + if (this.props.onValueChange) { + this.props.onValueChange( + event.nativeEvent.newValue, + event.nativeEvent.newIndex + ); + } + + // 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._picker && + this.state.selectedIndex !== event.nativeEvent.newIndex + ) { + this._picker.setNativeProps({ + selectedIndex: this.state.selectedIndex + }); + } + } +}); + +PickerDOM.Item = class extends React.Component { + static propTypes = { + value: PropTypes.any, // string or integer basically + label: PropTypes.string, + color: PropTypes.string + }; + + render() { + // These items don't get rendered directly. + return null; + } +}; + +const styles = StyleSheet.create({ + pickerDOM: { + // 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. + height: 38 + } +}); + +const RCTPickerDOM = requireNativeComponent( + "RCTPicker", + { + propTypes: { + ...ViewPropTypes, + style: pickerStyleType + } + }, + { + nativeOnly: { + items: true, + onChange: true, + selectedIndex: true, + enabled: true + } + } +); + +module.exports = PickerDOM; diff --git a/packages/react-native-dom/ReactDom/index.js b/packages/react-native-dom/ReactDom/index.js index 9dc239866..11c36f6eb 100644 --- a/packages/react-native-dom/ReactDom/index.js +++ b/packages/react-native-dom/ReactDom/index.js @@ -83,7 +83,8 @@ const builtInNativeModules: any[] = [ import("RCTRedBox"), import("RCTWebViewManager"), import("RCTNetworkingNative"), - import("RCTBlobManager") + import("RCTBlobManager"), + import("RCTPickerManager") ]; // Development Specific Native Modules diff --git a/packages/react-native-dom/ReactDom/views/RCTPicker.js b/packages/react-native-dom/ReactDom/views/RCTPicker.js new file mode 100644 index 000000000..20dd64fb6 --- /dev/null +++ b/packages/react-native-dom/ReactDom/views/RCTPicker.js @@ -0,0 +1,131 @@ +/** + * @providesModule RCTPicker + * @flow + */ +import RCTView from "RCTView"; +import type RCTBridge from "RCTBridge"; +import CustomElement from "CustomElement"; +import ColorArrayFromHexARGB from "ColorArrayFromHexARGB"; + +export type PickerItem = { label: string, value: string, textColor: ?number }; +export type SelectOnChange = ({ newIndex: number, newValue: ?string }) => void; + +@CustomElement("rct-picker") +class RCTPicker extends RCTView { + _selectElement: HTMLSelectElement; + + _selectedIndex: number; + _color: string; + _onChange: ?SelectOnChange; + + constructor(bridge: RCTBridge) { + super(bridge); + + this._selectElement = document.createElement("select"); + this._selectedIndex = -1; + + // update select element styles + this._selectElement.style.fontSize = "16px"; + this._selectElement.style.boxSizing = "border-box"; + this._selectElement.style.width = "100%"; + this._selectElement.style.height = "100%"; + this._selectElement.style.background = "none"; + this._selectElement.style.borderStyle = "solid"; + this._selectElement.style.borderWidth = "1px"; + this._selectElement.style.borderColor = "#CCC"; + this._selectElement.style.borderRadius = "0"; + this._selectElement.style.padding = "5px"; + // $FlowFixMe + this._selectElement.style.webkitAppearance = "none"; + // $FlowFixMe + this._selectElement.style.mozAppearance = "none"; + // $FlowFixMe + this._selectElement.style.appearance = "none"; + + // Styles for Custom Arrow + this._selectElement.style.background = `url("data:image/svg+xml;utf8,") no-repeat`; + this._selectElement.style.backgroundSize = "10px"; + this._selectElement.style.backgroundPosition = + "calc(100% - 10px) calc(50%)"; + this._selectElement.style.backgroundRepeat = "no-repeat"; + + this.disabled = false; + + // bind select element events + this._selectElement.addEventListener( + "change", + this.handleValueChange, + false + ); + + // add to dom tree + this.childContainer.appendChild(this._selectElement); + } + + set disabled(value: boolean) { + this._selectElement.disabled = value; + if (value) { + this._selectElement.style.opacity = "0.6"; + this._selectElement.style.pointerEvents = "none"; + this._selectElement.style.cursor = "default"; + } else { + this._selectElement.style.opacity = "1.0"; + this._selectElement.style.pointerEvents = "auto"; + this._selectElement.style.cursor = "pointer"; + } + } + + set selectedIndex(value: number) { + this._selectedIndex = value; + this._selectElement.selectedIndex = value; + } + + set color(value: string) { + this._color = value; + } + + resolveColor(color: ?number) { + if (color != null) { + const [a, r, g, b] = ColorArrayFromHexARGB(color); + return `rgba(${r},${g},${b},${a})`; + } + return this._color ? this._color : "#000"; + } + + set items(items: PickerItem[]) { + // clean up previous item list + while (this._selectElement.firstChild) { + this._selectElement.removeChild(this._selectElement.firstChild); + } + + // construct new option list + for (let item of items) { + const optionElem = document.createElement("option"); + optionElem.innerText = item.label; + optionElem.value = item.value; + optionElem.style.color = + this.resolveColor(item.textColor) + " !important"; + + this._selectElement.appendChild(optionElem); + } + } + + set onChange(value: ?SelectOnChange) { + this._onChange = value; + } + + handleValueChange = (event: Event) => { + event.preventDefault(); + + const onChange = this._onChange; + if (onChange) { + const payload = { + newIndex: this._selectElement.selectedIndex, + newValue: this._selectElement.value + }; + onChange(payload); + } + }; +} + +export default RCTPicker; diff --git a/packages/react-native-dom/ReactDom/views/RCTPickerManager.js b/packages/react-native-dom/ReactDom/views/RCTPickerManager.js new file mode 100644 index 000000000..7c0519c17 --- /dev/null +++ b/packages/react-native-dom/ReactDom/views/RCTPickerManager.js @@ -0,0 +1,60 @@ +/** + * @providesModule RCTPickerManager + * @flow + */ + +import type UIView from "UIView"; +import RCTBridge, { + RCTFunctionTypeNormal, + RCT_EXPORT_METHOD, + RCT_EXPORT_MODULE +} from "RCTBridge"; +import _RCTViewManager from "RCTViewManager"; +import RCTPicker from "RCTPicker"; +import ColorArrayFromHexARGB from "ColorArrayFromHexARGB"; +import type { PickerItem, SelectOnChange } from "RCTPicker"; + +module.exports = (async () => { + const RCTViewManager = await _RCTViewManager; + const { RCT_EXPORT_VIEW_PROP } = RCTViewManager; + + @RCT_EXPORT_MODULE("RCTPickerManager") + class RCTPickerManager extends RCTViewManager { + view(): UIView { + return new RCTPicker(this.bridge); + } + + @RCT_EXPORT_VIEW_PROP("color", "color") + setColor(view: RCTPicker, value: ?number) { + if (value) { + const [a, r, g, b] = ColorArrayFromHexARGB(value); + const stringValue = `rgba(${r},${g},${b},${a})`; + view.color = stringValue; + } else { + view.color = "#000"; + } + } + + @RCT_EXPORT_VIEW_PROP("selectedIndex", "number") + setSelectedIndex(view: RCTPicker, value: ?number) { + view.selectedIndex = value != null ? value : -1; + } + + @RCT_EXPORT_VIEW_PROP("items", "array") + setItems(view: RCTPicker, value: ?(PickerItem[])) { + view.items = value ? value : []; + } + + @RCT_EXPORT_VIEW_PROP("enabled", "bool") + setDisabled(view: RCTPicker, value: boolean = true) { + view.disabled = !value; + } + + @RCT_EXPORT_VIEW_PROP("onChange", "RCTBubblingEventBlock") + setOnChange(view: RCTPicker, value: ?SelectOnChange) { + view.onChange = value; + } + } + + return RCTPickerManager; +})();