diff --git a/Examples/UIExplorer/SegmentedControlIOSExample.js b/Examples/UIExplorer/SegmentedControlIOSExample.js new file mode 100644 index 00000000000000..e3dc291e55d422 --- /dev/null +++ b/Examples/UIExplorer/SegmentedControlIOSExample.js @@ -0,0 +1,169 @@ +/** + * 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'; + +var React = require('react-native'); +var { + SegmentedControlIOS, + Text, + View, + StyleSheet +} = React; + +var BasicSegmentedControlExample = React.createClass({ + render() { + return ( + + + + + + + + + ); + } +}); + +var PreSelectedSegmentedControlExample = React.createClass({ + render() { + return ( + + + + + + ); + } +}); + +var MomentarySegmentedControlExample = React.createClass({ + render() { + return ( + + + + + + ); + } +}); + +var DisabledSegmentedControlExample = React.createClass({ + render() { + return ( + + + + + + ); + }, +}); + +var ColorSegmentedControlExample = React.createClass({ + render() { + return ( + + + + + + + + + ); + }, +}); + +var EventSegmentedControlExample = React.createClass({ + getInitialState() { + return { + values: ["One", "Two", "Three"], + value: 'Not selected', + selectedSegmentIndex: undefined + }; + }, + + render() { + return ( + + + Value: {this.state.value} + + + Index: {this.state.selectedSegmentIndex} + + + + ); + }, + + _onChange(event) { + this.setState({ + selectedSegmentIndex: event.nativeEvent.selectedSegmentIndex, + }); + }, + + _onValueChange(value) { + this.setState({ + value: value, + }); + } +}); + +var styles = StyleSheet.create({ + text: { + fontSize: 14, + textAlign: 'center', + fontWeight: '500', + margin: 10, + }, +}); + +exports.title = ''; +exports.displayName = 'SegmentedControlExample'; +exports.description = 'Native segmented control'; +exports.examples = [ + { + title: 'Segmented controls can have values', + render(): ReactElement { return ; } + }, + { + title: 'Segmented controls can have a pre-selected value', + render(): ReactElement { return ; } + }, + { + title: 'Segmented controls can be momentary', + render(): ReactElement { return ; } + }, + { + title: 'Segmented controls can be disabled', + render(): ReactElement { return ; } + }, + { + title: 'Custom colors can be provided', + render(): ReactElement { return ; } + }, + { + title: 'Change events can be detected', + render(): ReactElement { return ; } + } +]; diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js index d2f4d6f0c858c8..0e34d5f059d2f1 100644 --- a/Examples/UIExplorer/UIExplorerList.js +++ b/Examples/UIExplorer/UIExplorerList.js @@ -43,6 +43,7 @@ var COMPONENTS = [ NavigatorExample, require('./PickerExample'), require('./ScrollViewExample'), + require('./SegmentedControlIOSExample'), require('./SliderIOSExample'), require('./SwitchExample'), require('./TabBarExample'), diff --git a/IntegrationTests/IntegrationTestsApp.js b/IntegrationTests/IntegrationTestsApp.js index dbb5dde835b5cf..7a254edaca8cc0 100644 --- a/IntegrationTests/IntegrationTestsApp.js +++ b/IntegrationTests/IntegrationTestsApp.js @@ -26,6 +26,7 @@ var TESTS = [ require('./TimersTest'), require('./AsyncStorageTest'), require('./SimpleSnapshotTest'), + require('./SegmentedControlIOSSnapshotTest'), ]; TESTS.forEach( diff --git a/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m b/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m index 578d3915f488da..0bd2603925cc15 100644 --- a/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m +++ b/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m @@ -72,6 +72,11 @@ - (void)testSimpleSnapshot [_runner runTest:_cmd module:@"SimpleSnapshotTest"]; } +- (void)testSegmentedControlIOS +{ + [_runner runTest:_cmd module:@"SegmentedControlIOSSnapshotTest"]; +} + - (void)testZZZ_NotInRecordMode { RCTAssert(_runner.recordMode == NO, @"Don't forget to turn record mode back to NO before commit."); diff --git a/IntegrationTests/IntegrationTestsTests/ReferenceImages/IntegrationTests-IntegrationTestsApp/testSegmentedControlIOS_1@2x.png b/IntegrationTests/IntegrationTestsTests/ReferenceImages/IntegrationTests-IntegrationTestsApp/testSegmentedControlIOS_1@2x.png new file mode 100644 index 00000000000000..a762a88aa2ba65 Binary files /dev/null and b/IntegrationTests/IntegrationTestsTests/ReferenceImages/IntegrationTests-IntegrationTestsApp/testSegmentedControlIOS_1@2x.png differ diff --git a/IntegrationTests/SegmentedControlIOSSnapshotTest.js b/IntegrationTests/SegmentedControlIOSSnapshotTest.js new file mode 100644 index 00000000000000..22164ad92ea4df --- /dev/null +++ b/IntegrationTests/SegmentedControlIOSSnapshotTest.js @@ -0,0 +1,64 @@ +/** + * 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. + */ +'use strict'; + +var React = require('react-native'); +var { + StyleSheet, + View, + SegmentedControlIOS, +} = React; + +var { TestModule } = React.addons; + +var SegmentedControlIOSSnapshotTest = React.createClass({ + componentDidMount() { + if (!TestModule.verifySnapshot) { + throw new Error('TestModule.verifySnapshot not defined.'); + } + requestAnimationFrame(() => TestModule.verifySnapshot(this.done)); + }, + + done() { + TestModule.markTestCompleted(); + }, + + render() { + return ( + + + + + + + + + + + + + + + + + + + + + ); + } +}); + +var styles = StyleSheet.create({ + testRow: { + marginBottom: 10, + } +}); + +module.exports = SegmentedControlIOSSnapshotTest; diff --git a/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.js b/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.js new file mode 100644 index 00000000000000..fc0c780d10e338 --- /dev/null +++ b/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.js @@ -0,0 +1,143 @@ +/** + * 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 SegmentedControlIOS + * @flow + * + * This is a controlled component version of RCTSegmentedControl. + */ +'use strict'; + +var NativeMethodsMixin = require('NativeMethodsMixin'); +var PropTypes = require('ReactPropTypes'); +var React = require('React'); +var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); +var StyleSheet = require('StyleSheet'); +var View = require('View'); + +var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); +var merge = require('merge'); + +var SEGMENTED_CONTROL_REFERENCE = 'segmentedcontrol'; + +type DefaultProps = { + values: Array; + disabled: boolean; +}; + + +type Event = Object; + +/** + * Use `SegmentedControlIOS` to render a UISegmentedControl iOS. + */ +var SegmentedControlIOS = React.createClass({ + mixins: [NativeMethodsMixin], + + propTypes: { + /** + * The labels for the control's segment buttons, in order. + */ + values: PropTypes.arrayOf(PropTypes.string), + + /** + * The index in `props.values` of the segment to be pre-selected + */ + selectedSegmentIndex: PropTypes.number, + + /** + * Used to style and layout the `SegmentedControl`. See `StyleSheet.js` and + * `ViewStylePropTypes.js` for more info. + */ + style: View.propTypes.style, + + /** + * Callback that is called when the user taps a segment; + * passes the segment's value as an argument + */ + onValueChange: PropTypes.func, + + /** + * Callback that is called when the user taps a segment; + * passes the event as an argument + */ + onChange: PropTypes.func, + + /** + * If true the user won't be able to interact with the control. + * Default value is false. + */ + disabled: PropTypes.bool, + + /** + * Accent color of the control. + */ + tintColor: PropTypes.string, + + /** + * If true, then selecting a segment won't persist visually. + * The `onValueChange` callback will still work as expected. + */ + momentary: PropTypes.bool + }, + + getDefaultProps: function(): DefaultProps { + return { + values: [], + disabled: false + }; + }, + + _onChange: function(event: Event) { + this.props.onChange && this.props.onChange(event); + this.props.onValueChange && this.props.onValueChange(event.nativeEvent.value); + }, + + render: function() { + var valuesAndSelectedSegmentIndex = { + selectedSegmentIndex: this.props.selectedSegmentIndex, + values: this.props.values + }; + return ( + + ); + } +}); + + +var styles = StyleSheet.create({ + segmentedControl: { + // Hard-coded to match UISegmentedControl#intrinsicContentSize.height + height: 28 + }, +}); + +var rkSegmentedControlAttributes = merge(ReactIOSViewAttributes.UIView, { + tintColor: true, + momentary: true, + enabled: true, + // Send both values simultaneously, to avoid race condition where + // `selectedSegmentIndex` is set before `values` + valuesAndSelectedSegmentIndex: true +}); + + +var RCTSegmentedControl = createReactIOSNativeComponentClass({ + validAttributes: rkSegmentedControlAttributes, + uiViewClassName: 'RCTSegmentedControl', +}); + +module.exports = SegmentedControlIOS; diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index c0bb989f494744..3d07ea34e2540f 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -17,6 +17,7 @@ // // var ReactNative = {...require('React'), /* additions */} // + var ReactNative = Object.assign(Object.create(require('React')), { // Components ActivityIndicatorIOS: require('ActivityIndicatorIOS'), @@ -28,6 +29,7 @@ var ReactNative = Object.assign(Object.create(require('React')), { PickerIOS: require('PickerIOS'), Navigator: require('Navigator'), ScrollView: require('ScrollView'), + SegmentedControlIOS: require('SegmentedControlIOS'), SliderIOS: require('SliderIOS'), SwitchIOS: require('SwitchIOS'), TabBarIOS: require('TabBarIOS'), diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index ba6ec3aaba120c..ad621f05742440 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -59,6 +59,8 @@ 83CBBA691A601EF300E9B192 /* RCTEventDispatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA661A601EF300E9B192 /* RCTEventDispatcher.m */; }; 83CBBA981A6020BB00E9B192 /* RCTTouchHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA971A6020BB00E9B192 /* RCTTouchHandler.m */; }; 83CBBACC1A6023D300E9B192 /* RCTConvert.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBACB1A6023D300E9B192 /* RCTConvert.m */; }; + F8626E811ACAEB7300B02338 /* RCTSegmentedControlManager.m in Sources */ = {isa = PBXBuildFile; fileRef = F8626E7F1ACAEB7300B02338 /* RCTSegmentedControlManager.m */; }; + F8DEFCE61ACAF38600799827 /* RCTSegmentedControl.m in Sources */ = {isa = PBXBuildFile; fileRef = F8DEFCE51ACAF38600799827 /* RCTSegmentedControl.m */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -190,6 +192,10 @@ 83CBBA971A6020BB00E9B192 /* RCTTouchHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTouchHandler.m; sourceTree = ""; }; 83CBBACA1A6023D300E9B192 /* RCTConvert.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTConvert.h; sourceTree = ""; }; 83CBBACB1A6023D300E9B192 /* RCTConvert.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTConvert.m; sourceTree = ""; }; + F8626E7F1ACAEB7300B02338 /* RCTSegmentedControlManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSegmentedControlManager.m; sourceTree = ""; }; + F8626E801ACAEB7300B02338 /* RCTSegmentedControlManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSegmentedControlManager.h; sourceTree = ""; }; + F8DEFCE41ACAF38600799827 /* RCTSegmentedControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSegmentedControl.h; sourceTree = ""; }; + F8DEFCE51ACAF38600799827 /* RCTSegmentedControl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSegmentedControl.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -275,6 +281,10 @@ 13B07FF81A6947C200A75B9A /* RCTScrollViewManager.h */, 13B07FF91A6947C200A75B9A /* RCTScrollViewManager.m */, 13C325271AA63B6A0048765F /* RCTScrollableProtocol.h */, + F8DEFCE41ACAF38600799827 /* RCTSegmentedControl.h */, + F8DEFCE51ACAF38600799827 /* RCTSegmentedControl.m */, + F8626E801ACAEB7300B02338 /* RCTSegmentedControlManager.h */, + F8626E7F1ACAEB7300B02338 /* RCTSegmentedControlManager.m */, 13E0674B1A70F44B002CDEE1 /* RCTShadowView.h */, 13E0674C1A70F44B002CDEE1 /* RCTShadowView.m */, 14F484541AABFCE100FDF6B9 /* RCTSliderManager.h */, @@ -462,6 +472,7 @@ 830A229E1A66C68A008503DA /* RCTRootView.m in Sources */, 13B07FF01A69327A00A75B9A /* RCTExceptionsManager.m in Sources */, 83CBBA5A1A601E9000E9B192 /* RCTRedBox.m in Sources */, + F8626E811ACAEB7300B02338 /* RCTSegmentedControlManager.m in Sources */, 83CBBA511A601E3B00E9B192 /* RCTAssert.m in Sources */, 58114A501AAE93D500E7D092 /* RCTAsyncLocalStorage.m in Sources */, 832348161A77A5AA00B55238 /* Layout.c in Sources */, @@ -473,6 +484,7 @@ 13B080061A6947C200A75B9A /* RCTScrollViewManager.m in Sources */, 137327EA1AA5CF210034F82E /* RCTTabBarManager.m in Sources */, 13B080261A694A8400A75B9A /* RCTWrapperViewController.m in Sources */, + F8DEFCE61ACAF38600799827 /* RCTSegmentedControl.m in Sources */, 13B080051A6947C200A75B9A /* RCTScrollView.m in Sources */, 13B07FF21A69327A00A75B9A /* RCTTiming.m in Sources */, 1372B70A1AB030C200659ED6 /* RCTAppState.m in Sources */, diff --git a/React/Views/RCTSegmentedControl.h b/React/Views/RCTSegmentedControl.h new file mode 100644 index 00000000000000..2812aba9c0821c --- /dev/null +++ b/React/Views/RCTSegmentedControl.h @@ -0,0 +1,19 @@ +// +// RCTSegmentedControl.h +// React +// +// Created by Clay Allsopp on 3/31/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import + +@class RCTEventDispatcher; + +@interface RCTSegmentedControl : UISegmentedControl + +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; + +- (void)setValuesAndSelectedSegmentIndex:(NSDictionary *)valuesAndSelectedValue; + +@end diff --git a/React/Views/RCTSegmentedControl.m b/React/Views/RCTSegmentedControl.m new file mode 100644 index 00000000000000..b2a7a34b6eb44c --- /dev/null +++ b/React/Views/RCTSegmentedControl.m @@ -0,0 +1,56 @@ +// +// RCTSegmentedControl.m +// React +// +// Created by Clay Allsopp on 3/31/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import "RCTSegmentedControl.h" +#import "UIView+React.h" +#import "RCTEventDispatcher.h" + +@implementation RCTSegmentedControl +{ + RCTEventDispatcher *_eventDispatcher; +} + +- (id)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +{ + if ((self = [super initWithFrame:CGRectZero])) { + _eventDispatcher = eventDispatcher; + [self addTarget:self action:@selector(onChange:) forControlEvents:UIControlEventValueChanged]; + } + return self; +} + + +- (void)setValuesAndSelectedSegmentIndex:(NSDictionary *)valuesAndSelectedSegmentIndex +{ + [self removeAllSegments]; + + NSArray *values = valuesAndSelectedSegmentIndex[@"values"]; + NSNumber *selectedSegmentIndex = valuesAndSelectedSegmentIndex[@"selectedSegmentIndex"]; + + NSUInteger insertAtIndex = 0; + for (NSString *value in values) { + [self insertSegmentWithTitle:value atIndex:insertAtIndex animated:NO]; + insertAtIndex += 1; + } + + if (selectedSegmentIndex) { + [self setSelectedSegmentIndex:[selectedSegmentIndex integerValue]]; + } +} + +- (void)onChange:(UISegmentedControl *)sender +{ + NSDictionary *event = @{ + @"target": self.reactTag, + @"value": [self titleForSegmentAtIndex:sender.selectedSegmentIndex], + @"selectedSegmentIndex": @(sender.selectedSegmentIndex) + }; + [_eventDispatcher sendInputEventWithName:@"topChange" body:event]; +} + +@end diff --git a/React/Views/RCTSegmentedControlManager.h b/React/Views/RCTSegmentedControlManager.h new file mode 100644 index 00000000000000..03647c72edb9bf --- /dev/null +++ b/React/Views/RCTSegmentedControlManager.h @@ -0,0 +1,14 @@ +/** + * 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. + */ + +#import "RCTViewManager.h" + +@interface RCTSegmentedControlManager : RCTViewManager + +@end diff --git a/React/Views/RCTSegmentedControlManager.m b/React/Views/RCTSegmentedControlManager.m new file mode 100644 index 00000000000000..e9108db86f69a5 --- /dev/null +++ b/React/Views/RCTSegmentedControlManager.m @@ -0,0 +1,28 @@ +/** + * 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. + */ + +#import "RCTSegmentedControlManager.h" + +#import "RCTSegmentedControl.h" + +#import "RCTBridge.h" + +@implementation RCTSegmentedControlManager + +- (UIView *)view +{ + return [[RCTSegmentedControl alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; +} + +RCT_EXPORT_VIEW_PROPERTY(valuesAndSelectedSegmentIndex, NSDictionary); +RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor); +RCT_EXPORT_VIEW_PROPERTY(momentary, BOOL); +RCT_EXPORT_VIEW_PROPERTY(enabled, BOOL); + +@end