From 84b11dd5185c017a316510de6ace608dc82321e8 Mon Sep 17 00:00:00 2001 From: Becky Van Bussel Date: Fri, 25 Aug 2017 10:22:17 -0700 Subject: [PATCH] Add Android React Native Checkbox Reviewed By: achen1 Differential Revision: D5281736 fbshipit-source-id: 9a3c93eeace2d80be4ddbd4ffc3258c1d3637480 --- Libraries/Components/CheckBox/CheckBox.js | 127 +++++++++++++++++ .../react-native-implementation.js | 1 + RNTester/js/CheckBoxExample.js | 129 ++++++++++++++++++ RNTester/js/RNTesterList.android.js | 4 + .../main/java/com/facebook/react/shell/BUCK | 1 + .../react/shell/MainReactPackage.java | 2 + .../com/facebook/react/views/checkbox/BUCK | 17 +++ .../react/views/checkbox/ReactCheckBox.java | 38 ++++++ .../views/checkbox/ReactCheckBoxEvent.java | 53 +++++++ .../views/checkbox/ReactCheckBoxManager.java | 64 +++++++++ website/server/docsList.js | 1 + 11 files changed, 437 insertions(+) create mode 100644 Libraries/Components/CheckBox/CheckBox.js create mode 100644 RNTester/js/CheckBoxExample.js create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/checkbox/BUCK create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/checkbox/ReactCheckBox.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/checkbox/ReactCheckBoxEvent.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/checkbox/ReactCheckBoxManager.java diff --git a/Libraries/Components/CheckBox/CheckBox.js b/Libraries/Components/CheckBox/CheckBox.js new file mode 100644 index 000000000..22ce3df87 --- /dev/null +++ b/Libraries/Components/CheckBox/CheckBox.js @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2017-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 CheckBox + * @flow + * @format + */ +'use strict'; + +const NativeMethodsMixin = require('NativeMethodsMixin'); +const PropTypes = require('prop-types'); +const React = require('React'); +const StyleSheet = require('StyleSheet'); +const ViewPropTypes = require('ViewPropTypes'); + +const createReactClass = require('create-react-class'); +const requireNativeComponent = require('requireNativeComponent'); + +type DefaultProps = { + value: boolean, + disabled: boolean, +}; + +/** + * Renders a boolean input. + * + * This is a controlled component that requires an `onValueChange` callback that + * updates the `value` prop in order for the component to reflect user actions. + * If the `value` prop is not updated, the component will continue to render + * the supplied `value` prop instead of the expected result of any user actions. + * + * @keyword checkbox + * @keyword toggle + */ +// $FlowFixMe(>=0.41.0) +let CheckBox = createReactClass({ + displayName: 'CheckBox', + propTypes: { + ...ViewPropTypes, + /** + * The value of the checkbox. If true the checkbox will be turned on. + * Default value is false. + */ + value: PropTypes.bool, + /** + * If true the user won't be able to toggle the checkbox. + * Default value is false. + */ + disabled: PropTypes.bool, + /** + * Used in case the props change removes the component. + */ + onChange: PropTypes.func, + /** + * Invoked with the new value when the value changes. + */ + onValueChange: PropTypes.func, + /** + * Used to locate this view in end-to-end tests. + */ + testID: PropTypes.string, + }, + + getDefaultProps: function(): DefaultProps { + return { + value: false, + disabled: false, + }; + }, + + mixins: [NativeMethodsMixin], + + _rctCheckBox: {}, + _onChange: function(event: Object) { + this._rctCheckBox.setNativeProps({value: this.props.value}); + // Change the props after the native props are set in case the props + // change removes the component + this.props.onChange && this.props.onChange(event); + this.props.onValueChange && + this.props.onValueChange(event.nativeEvent.value); + }, + + render: function() { + let props = {...this.props}; + props.onStartShouldSetResponder = () => true; + props.onResponderTerminationRequest = () => false; + props.enabled = !this.props.disabled; + props.on = this.props.value; + props.style = [styles.rctCheckBox, this.props.style]; + + return ( + { + /* $FlowFixMe(>=0.53.0 site=react_native_fb) This comment suppresses an + * error when upgrading Flow's support for React. Common errors found + * when upgrading Flow's React support are documented at + * https://fburl.com/eq7bs81w */ + this._rctCheckBox = ref; + }} + onChange={this._onChange} + /> + ); + }, +}); + +let styles = StyleSheet.create({ + rctCheckBox: { + height: 32, + width: 32, + }, +}); + +let RCTCheckBox = requireNativeComponent('AndroidCheckBox', CheckBox, { + nativeOnly: { + onChange: true, + on: true, + enabled: true, + }, +}); + +module.exports = CheckBox; diff --git a/Libraries/react-native/react-native-implementation.js b/Libraries/react-native/react-native-implementation.js index bfe032e0b..74cb8622c 100644 --- a/Libraries/react-native/react-native-implementation.js +++ b/Libraries/react-native/react-native-implementation.js @@ -20,6 +20,7 @@ const ReactNative = { get ActivityIndicator() { return require('ActivityIndicator'); }, get ART() { return require('ReactNativeART'); }, get Button() { return require('Button'); }, + get CheckBox() { return require('CheckBox'); }, get DatePickerIOS() { return require('DatePickerIOS'); }, get DrawerLayoutAndroid() { return require('DrawerLayoutAndroid'); }, get FlatList() { return require('FlatList'); }, diff --git a/RNTester/js/CheckBoxExample.js b/RNTester/js/CheckBoxExample.js new file mode 100644 index 000000000..f868dd26e --- /dev/null +++ b/RNTester/js/CheckBoxExample.js @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2017-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. + * + * @flow + * @providesModule CheckBoxExample + * @format + */ +'use strict'; + +const React = require('react'); +const ReactNative = require('react-native'); +const {CheckBox, Text, View} = ReactNative; + +class BasicCheckBoxExample extends React.Component<{}, $FlowFixMeState> { + state = { + trueCheckBoxIsOn: true, + falseCheckBoxIsOn: false, + }; + + render() { + return ( + + this.setState({falseCheckBoxIsOn: value})} + style={{marginBottom: 10}} + value={this.state.falseCheckBoxIsOn} + /> + this.setState({trueCheckBoxIsOn: value})} + value={this.state.trueCheckBoxIsOn} + /> + + ); + } +} + +class DisabledCheckBoxExample extends React.Component<{}, $FlowFixMeState> { + render() { + return ( + + + + + ); + } +} + +class EventCheckBoxExample extends React.Component<{}, $FlowFixMeState> { + state = { + eventCheckBoxIsOn: false, + eventCheckBoxRegressionIsOn: true, + }; + + render() { + return ( + + + this.setState({eventCheckBoxIsOn: value})} + style={{marginBottom: 10}} + value={this.state.eventCheckBoxIsOn} + /> + this.setState({eventCheckBoxIsOn: value})} + style={{marginBottom: 10}} + value={this.state.eventCheckBoxIsOn} + /> + + {this.state.eventCheckBoxIsOn ? 'On' : 'Off'} + + + + + this.setState({eventCheckBoxRegressionIsOn: value})} + style={{marginBottom: 10}} + value={this.state.eventCheckBoxRegressionIsOn} + /> + + this.setState({eventCheckBoxRegressionIsOn: value})} + style={{marginBottom: 10}} + value={this.state.eventCheckBoxRegressionIsOn} + /> + + {this.state.eventCheckBoxRegressionIsOn ? 'On' : 'Off'} + + + + ); + } +} + +let examples = [ + { + title: 'CheckBoxes can be set to true or false', + render(): React.Element { + return ; + }, + }, + { + title: 'CheckBoxes can be disabled', + render(): React.Element { + return ; + }, + }, + { + title: 'Change events can be detected', + render(): React.Element { + return ; + }, + }, + { + title: 'CheckBoxes are controlled components', + render(): React.Element { + return ; + }, + }, +]; + +exports.title = ''; +exports.displayName = 'CheckBoxExample'; +exports.description = 'Native boolean input'; +exports.examples = examples; diff --git a/RNTester/js/RNTesterList.android.js b/RNTester/js/RNTesterList.android.js index 74a9308e4..3eecace85 100644 --- a/RNTester/js/RNTesterList.android.js +++ b/RNTester/js/RNTesterList.android.js @@ -25,6 +25,10 @@ const ComponentExamples: Array = [ key: 'ButtonExample', module: require('./ButtonExample'), }, + { + key: 'CheckBoxExample', + module: require('./CheckBoxExample'), + }, { key: 'FlatListExample', module: require('./FlatListExample'), diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK index fdd003be9..2b2a5ba1c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK @@ -47,6 +47,7 @@ android_library( react_native_target("java/com/facebook/react/modules/websocket:websocket"), react_native_target("java/com/facebook/react/uimanager:uimanager"), react_native_target("java/com/facebook/react/views/art:art"), + react_native_target("java/com/facebook/react/views/checkbox:checkbox"), react_native_target("java/com/facebook/react/views/drawer:drawer"), react_native_target("java/com/facebook/react/views/image:image"), react_native_target("java/com/facebook/react/views/modal:modal"), diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java index c4a32965d..052150617 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java @@ -54,6 +54,7 @@ import com.facebook.react.modules.websocket.WebSocketModule; import com.facebook.react.uimanager.ViewManager; import com.facebook.react.views.art.ARTRenderableViewManager; import com.facebook.react.views.art.ARTSurfaceViewManager; +import com.facebook.react.views.checkbox.ReactCheckBoxManager; import com.facebook.react.views.drawer.ReactDrawerLayoutManager; import com.facebook.react.views.image.ReactImageManager; import com.facebook.react.views.modal.ReactModalHostManager; @@ -309,6 +310,7 @@ public class MainReactPackage extends LazyReactPackage { viewManagers.add(ARTRenderableViewManager.createARTGroupViewManager()); viewManagers.add(ARTRenderableViewManager.createARTShapeViewManager()); viewManagers.add(ARTRenderableViewManager.createARTTextViewManager()); + viewManagers.add(new ReactCheckBoxManager()); viewManagers.add(new ReactDialogPickerManager()); viewManagers.add(new ReactDrawerLayoutManager()); viewManagers.add(new ReactDropdownPickerManager()); diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/checkbox/BUCK b/ReactAndroid/src/main/java/com/facebook/react/views/checkbox/BUCK new file mode 100644 index 000000000..1678736aa --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/checkbox/BUCK @@ -0,0 +1,17 @@ +include_defs("//ReactAndroid/DEFS") + +android_library( + name = "checkbox", + srcs = glob(["*.java"]), + visibility = [ + "PUBLIC", + ], + deps = [ + react_native_dep("third-party/android/support/v4:lib-support-v4"), + react_native_dep("third-party/java/jsr-305:jsr-305"), + react_native_target("java/com/facebook/react/bridge:bridge"), + react_native_target("java/com/facebook/react/common:common"), + react_native_target("java/com/facebook/react/uimanager:uimanager"), + react_native_target("java/com/facebook/react/uimanager/annotations:annotations"), + ], +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/checkbox/ReactCheckBox.java b/ReactAndroid/src/main/java/com/facebook/react/views/checkbox/ReactCheckBox.java new file mode 100644 index 000000000..88e7f6aa7 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/checkbox/ReactCheckBox.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2017-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. + */ +package com.facebook.react.views.checkbox; + +import android.content.Context; +import android.widget.CheckBox; + +/** CheckBox that has its value controlled by JS. */ +/*package*/ class ReactCheckBox extends CheckBox { + + private boolean mAllowChange; + + public ReactCheckBox(Context context) { + super(context); + mAllowChange = true; + } + + @Override + public void setChecked(boolean checked) { + if (mAllowChange) { + mAllowChange = false; + super.setChecked(checked); + } + } + + /*package*/ void setOn(boolean on) { + // If the checkbox has a different value than the value sent by JS, we must change it. + if (isChecked() != on) { + super.setChecked(on); + } + mAllowChange = true; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/checkbox/ReactCheckBoxEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/checkbox/ReactCheckBoxEvent.java new file mode 100644 index 000000000..085edcec6 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/checkbox/ReactCheckBoxEvent.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2017-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. + */ +package com.facebook.react.views.checkbox; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +/** Event emitted by a ReactCheckBoxManager once a checkbox is manipulated. */ +/*package*/ class ReactCheckBoxEvent extends Event { + + public static final String EVENT_NAME = "topChange"; + + private final boolean mIsChecked; + + public ReactCheckBoxEvent(int viewId, boolean isChecked) { + super(viewId); + mIsChecked = isChecked; + } + + public boolean getIsChecked() { + return mIsChecked; + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public short getCoalescingKey() { + // All checkbox events for a given view can be coalesced. + return 0; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); + } + + private WritableMap serializeEventData() { + WritableMap eventData = Arguments.createMap(); + eventData.putInt("target", getViewTag()); + eventData.putBoolean("value", getIsChecked()); + return eventData; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/checkbox/ReactCheckBoxManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/checkbox/ReactCheckBoxManager.java new file mode 100644 index 000000000..b064d940f --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/checkbox/ReactCheckBoxManager.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2017-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. + */ +package com.facebook.react.views.checkbox; + +import android.widget.CompoundButton; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.uimanager.SimpleViewManager; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.ViewProps; +import com.facebook.react.uimanager.annotations.ReactProp; + +/** View manager for {@link ReactCheckBox} components. */ +public class ReactCheckBoxManager extends SimpleViewManager { + + private static final String REACT_CLASS = "AndroidCheckBox"; + + private static final CompoundButton.OnCheckedChangeListener ON_CHECKED_CHANGE_LISTENER = + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + ReactContext reactContext = (ReactContext) buttonView.getContext(); + reactContext + .getNativeModule(UIManagerModule.class) + .getEventDispatcher() + .dispatchEvent(new ReactCheckBoxEvent(buttonView.getId(), isChecked)); + } + }; + + @Override + public String getName() { + return REACT_CLASS; + } + + @Override + protected void addEventEmitters(final ThemedReactContext reactContext, final ReactCheckBox view) { + view.setOnCheckedChangeListener(ON_CHECKED_CHANGE_LISTENER); + } + + @Override + protected ReactCheckBox createViewInstance(ThemedReactContext context) { + ReactCheckBox view = new ReactCheckBox(context); + return view; + } + + @ReactProp(name = ViewProps.ENABLED, defaultBoolean = true) + public void setEnabled(ReactCheckBox view, boolean enabled) { + view.setEnabled(enabled); + } + + @ReactProp(name = ViewProps.ON) + public void setOn(ReactCheckBox view, boolean on) { + // we set the checked change listener to null and then restore it so that we don't fire an + // onChange event to JS when JS itself is updating the value of the checkbox + view.setOnCheckedChangeListener(null); + view.setOn(on); + view.setOnCheckedChangeListener(ON_CHECKED_CHANGE_LISTENER); + } +} diff --git a/website/server/docsList.js b/website/server/docsList.js index 8cfc84fd7..38dfab29b 100644 --- a/website/server/docsList.js +++ b/website/server/docsList.js @@ -12,6 +12,7 @@ const components = [ '../Libraries/Components/ActivityIndicator/ActivityIndicator.js', '../Libraries/Components/Button.js', + '../Libraries/Components/CheckBox/CheckBox.js', '../Libraries/Components/DatePicker/DatePickerIOS.ios.js', '../Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js', '../Libraries/Lists/FlatList.js',