Skip to content

Commit

Permalink
Support Input Accessory View (iOS Only) [1/N]
Browse files Browse the repository at this point in the history
Reviewed By: mmmulani

Differential Revision: D6886573

fbshipit-source-id: 71e1f812b1cc1698e4380211a6cedd59011b5495
  • Loading branch information
Peter Argany authored and facebook-github-bot committed Feb 27, 2018
1 parent c87d03a commit 38197c8
Show file tree
Hide file tree
Showing 13 changed files with 427 additions and 6 deletions.
114 changes: 114 additions & 0 deletions Libraries/Components/TextInput/InputAccessoryView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* Copyright (c) 2013-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 InputAccessoryView
* @flow
* @format
*/
'use strict';

const ColorPropType = require('ColorPropType');
const React = require('React');
const StyleSheet = require('StyleSheet');
const ViewPropTypes = require('ViewPropTypes');

const requireNativeComponent = require('requireNativeComponent');

const RCTInputAccessoryView = requireNativeComponent('RCTInputAccessoryView');

/**
* Note: iOS only
*
* A component which enables customization of the keyboard input accessory view.
* The input accessory view is displayed above the keyboard whenever a TextInput
* has focus. This component can be used to create custom toolbars.
*
* To use this component wrap your custom toolbar with the
* InputAccessoryView component, and set a nativeID. Then, pass that nativeID
* as the inputAccessoryViewID of whatever TextInput you desire. A simple
* example:
*
* ```ReactNativeWebPlayer
* import React, { Component } from 'react';
* import { AppRegistry, TextInput, InputAccessoryView, Button } from 'react-native';
*
* export default class UselessTextInput extends Component {
* constructor(props) {
* super(props);
* this.state = {text: 'Placeholder Text'};
* }
*
* render() {
* const inputAccessoryViewID = "uniqueID";
* return (
* <View>
* <ScrollView keyboardDismissMode="interactive">
* <TextInput
* style={{
* padding: 10,
* paddingTop: 50,
* }}
* inputAccessoryViewID=inputAccessoryViewID
* onChangeText={text => this.setState({text})}
* value={this.state.text}
* />
* </ScrollView>
* <InputAccessoryView nativeID=inputAccessoryViewID>
* <Button
* onPress={() => this.setState({text: 'Placeholder Text'})}
* title="Reset Text"
* />
* </InputAccessoryView>
* </View>
* );
* }
* }
*
* // skip this line if using Create React Native App
* AppRegistry.registerComponent('AwesomeProject', () => UselessTextInput);
* ```
*
* This component can also be used to create sticky text inputs (text inputs
* which are anchored to the top of the keyboard). To do this, wrap a
* TextInput with the InputAccessoryView component, and don't set a nativeID.
* For an example, look at InputAccessoryViewExample.js in RNTester.
*/

type Props = {
+children: React.Node,
/**
* An ID which is used to associate this `InputAccessoryView` to
* specified TextInput(s).
*/
nativeID?: string,
style?: ViewPropTypes.style,
backgroundColor?: ColorPropType,
};

class InputAccessoryView extends React.Component<Props> {
render(): React.Node {
if (React.Children.count(this.props.children) === 0) {
return null;
}

return (
<RCTInputAccessoryView
style={[this.props.style, styles.container]}
nativeID={this.props.nativeID}
backgroundColor={this.props.backgroundColor}>
{this.props.children}
</RCTInputAccessoryView>
);
}
}

const styles = StyleSheet.create({
container: {
position: 'absolute',
},
});

module.exports = InputAccessoryView;
7 changes: 7 additions & 0 deletions Libraries/Components/TextInput/TextInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,13 @@ const TextInput = createReactClass({
* This property is supported only for single-line TextInput component on iOS.
*/
caretHidden: PropTypes.bool,
/**
* An optional identifier which links a custom InputAccessoryView to
* this text input. The InputAccessoryView is rendered above the
* keyboard when this text input is focused.
* @platform ios
*/
inputAccessoryViewID: PropTypes.string,
},
getDefaultProps(): Object {
return {
Expand Down
18 changes: 18 additions & 0 deletions Libraries/Text/RCTText.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@
5956B1A6200FF35C008D9D16 /* RCTUITextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 5956B103200FEBA9008D9D16 /* RCTUITextField.m */; };
5956B1A7200FF35C008D9D16 /* RCTVirtualTextShadowView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5956B12E200FEBAA008D9D16 /* RCTVirtualTextShadowView.m */; };
5956B1A8200FF35C008D9D16 /* RCTVirtualTextViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5956B12B200FEBAA008D9D16 /* RCTVirtualTextViewManager.m */; };
8F2807C7202D2B6B005D65E6 /* RCTInputAccessoryViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F2807C1202D2B6A005D65E6 /* RCTInputAccessoryViewManager.m */; };
8F2807C8202D2B6B005D65E6 /* RCTInputAccessoryView.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F2807C3202D2B6A005D65E6 /* RCTInputAccessoryView.m */; };
8F2807C9202D2B6B005D65E6 /* RCTInputAccessoryViewContent.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F2807C5202D2B6B005D65E6 /* RCTInputAccessoryViewContent.m */; };
/* End PBXBuildFile section */

/* Begin PBXCopyFilesBuildPhase section */
Expand Down Expand Up @@ -229,6 +232,12 @@
5956B12D200FEBAA008D9D16 /* RCTVirtualTextViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTVirtualTextViewManager.h; sourceTree = "<group>"; };
5956B12E200FEBAA008D9D16 /* RCTVirtualTextShadowView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTVirtualTextShadowView.m; sourceTree = "<group>"; };
5956B12F200FEBAA008D9D16 /* RCTConvert+Text.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RCTConvert+Text.m"; sourceTree = "<group>"; };
8F2807C1202D2B6A005D65E6 /* RCTInputAccessoryViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTInputAccessoryViewManager.m; sourceTree = "<group>"; };
8F2807C2202D2B6A005D65E6 /* RCTInputAccessoryViewContent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTInputAccessoryViewContent.h; sourceTree = "<group>"; };
8F2807C3202D2B6A005D65E6 /* RCTInputAccessoryView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTInputAccessoryView.m; sourceTree = "<group>"; };
8F2807C4202D2B6A005D65E6 /* RCTInputAccessoryView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTInputAccessoryView.h; sourceTree = "<group>"; };
8F2807C5202D2B6B005D65E6 /* RCTInputAccessoryViewContent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTInputAccessoryViewContent.m; sourceTree = "<group>"; };
8F2807C6202D2B6B005D65E6 /* RCTInputAccessoryViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTInputAccessoryViewManager.h; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXGroup section */
Expand Down Expand Up @@ -274,6 +283,12 @@
5956B0FF200FEBA9008D9D16 /* TextInput */ = {
isa = PBXGroup;
children = (
8F2807C4202D2B6A005D65E6 /* RCTInputAccessoryView.h */,
8F2807C3202D2B6A005D65E6 /* RCTInputAccessoryView.m */,
8F2807C2202D2B6A005D65E6 /* RCTInputAccessoryViewContent.h */,
8F2807C5202D2B6B005D65E6 /* RCTInputAccessoryViewContent.m */,
8F2807C6202D2B6B005D65E6 /* RCTInputAccessoryViewManager.h */,
8F2807C1202D2B6A005D65E6 /* RCTInputAccessoryViewManager.m */,
5956B113200FEBA9008D9D16 /* Multiline */,
5956B10C200FEBA9008D9D16 /* RCTBackedTextInputDelegate.h */,
5956B107200FEBA9008D9D16 /* RCTBackedTextInputDelegateAdapter.h */,
Expand Down Expand Up @@ -465,8 +480,11 @@
5956B140200FEBAA008D9D16 /* RCTTextShadowView.m in Sources */,
5956B131200FEBAA008D9D16 /* RCTRawTextViewManager.m in Sources */,
5956B137200FEBAA008D9D16 /* RCTBaseTextInputShadowView.m in Sources */,
8F2807C7202D2B6B005D65E6 /* RCTInputAccessoryViewManager.m in Sources */,
5956B146200FEBAA008D9D16 /* RCTConvert+Text.m in Sources */,
8F2807C9202D2B6B005D65E6 /* RCTInputAccessoryViewContent.m in Sources */,
5956B13F200FEBAA008D9D16 /* RCTTextAttributes.m in Sources */,
8F2807C8202D2B6B005D65E6 /* RCTInputAccessoryView.m in Sources */,
5956B143200FEBAA008D9D16 /* RCTTextView.m in Sources */,
5956B13C200FEBAA008D9D16 /* RCTUITextView.m in Sources */,
5956B136200FEBAA008D9D16 /* RCTBackedTextInputDelegateAdapter.m in Sources */,
Expand Down
1 change: 1 addition & 0 deletions Libraries/Text/TextInput/RCTBaseTextInputView.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, copy) RCTTextSelection *selection;
@property (nonatomic, strong, nullable) NSNumber *maxLength;
@property (nonatomic, copy) NSAttributedString *attributedText;
@property (nonatomic, copy) NSString *inputAccessoryViewID;

@end

Expand Down
38 changes: 32 additions & 6 deletions Libraries/Text/TextInput/RCTBaseTextInputView.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
#import <React/RCTUtils.h>
#import <React/UIView+React.h>

#import "RCTInputAccessoryView.h"
#import "RCTInputAccessoryViewContent.h"
#import "RCTTextAttributes.h"
#import "RCTTextSelection.h"

Expand Down Expand Up @@ -400,12 +402,33 @@ - (void)didMoveToWindow

- (void)didSetProps:(NSArray<NSString *> *)changedProps
{
[self invalidateInputAccessoryView];
#if !TARGET_OS_TV
if ([changedProps containsObject:@"inputAccessoryViewID"] && self.inputAccessoryViewID) {
[self setCustomInputAccessoryViewWithNativeID:self.inputAccessoryViewID];
} else if (!self.inputAccessoryViewID) {
[self setDefaultInputAccessoryView];
}
#endif
}

- (void)setCustomInputAccessoryViewWithNativeID:(NSString *)nativeID
{
__weak RCTBaseTextInputView *weakSelf = self;
[_bridge.uiManager rootViewForReactTag:self.reactTag withCompletion:^(UIView *rootView) {
RCTBaseTextInputView *strongSelf = weakSelf;
if (rootView) {
UIView *accessoryView = [strongSelf->_bridge.uiManager viewForNativeID:nativeID
withRootTag:rootView.reactTag];
if (accessoryView && [accessoryView isKindOfClass:[RCTInputAccessoryView class]]) {
strongSelf.backedTextInputView.inputAccessoryView = ((RCTInputAccessoryView *)accessoryView).content.inputAccessoryView;
[strongSelf reloadInputViewsIfNecessary];
}
}
}];
}

- (void)invalidateInputAccessoryView
- (void)setDefaultInputAccessoryView
{
#if !TARGET_OS_TV
UIView<RCTBackedTextInputViewProtocol> *textInputView = self.backedTextInputView;
UIKeyboardType keyboardType = textInputView.keyboardType;

Expand Down Expand Up @@ -443,12 +466,15 @@ - (void)invalidateInputAccessoryView
else {
textInputView.inputAccessoryView = nil;
}
[self reloadInputViewsIfNecessary];
}

- (void)reloadInputViewsIfNecessary
{
// We have to call `reloadInputViews` for focused text inputs to update an accessory view.
if (textInputView.isFirstResponder) {
[textInputView reloadInputViews];
if (self.backedTextInputView.isFirstResponder) {
[self.backedTextInputView reloadInputViews];
}
#endif
}

- (void)handleInputAccessoryDoneButton
Expand Down
1 change: 1 addition & 0 deletions Libraries/Text/TextInput/RCTBaseTextInputViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ @implementation RCTBaseTextInputViewManager
RCT_EXPORT_VIEW_PROPERTY(maxLength, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(selectTextOnFocus, BOOL)
RCT_EXPORT_VIEW_PROPERTY(selection, RCTTextSelection)
RCT_EXPORT_VIEW_PROPERTY(inputAccessoryViewID, NSString)

RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock)
Expand Down
19 changes: 19 additions & 0 deletions Libraries/Text/TextInput/RCTInputAccessoryView.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* 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.
*/

#import <UIKit/UIKit.h>

@class RCTBridge;
@class RCTInputAccessoryViewContent;

@interface RCTInputAccessoryView : UIView

- (instancetype)initWithBridge:(RCTBridge *)bridge;

@property (nonatomic, readonly, strong) RCTInputAccessoryViewContent *content;

@end
69 changes: 69 additions & 0 deletions Libraries/Text/TextInput/RCTInputAccessoryView.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* 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.
*/

#import "RCTInputAccessoryView.h"

#import <React/RCTBridge.h>
#import <React/RCTTouchHandler.h>
#import <React/UIView+React.h>

#import "RCTInputAccessoryViewContent.h"

@implementation RCTInputAccessoryView
{
BOOL _contentShouldBeFirstResponder;
}

- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if (self = [super init]) {
_content = [RCTInputAccessoryViewContent new];
RCTTouchHandler *const touchHandler = [[RCTTouchHandler alloc] initWithBridge:bridge];
[touchHandler attachToView:_content.inputAccessoryView];
[self addSubview:_content];
}
return self;
}

- (void)reactSetFrame:(CGRect)frame
{
[_content.inputAccessoryView setFrame:frame];
[_content.contentView setFrame:frame];

if (_contentShouldBeFirstResponder) {
_contentShouldBeFirstResponder = NO;
[_content becomeFirstResponder];
}
}

- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index
{
[super insertReactSubview:subview atIndex:index];
[_content insertReactSubview:subview atIndex:index];
}

- (void)removeReactSubview:(UIView *)subview
{
[super removeReactSubview:subview];
[_content removeReactSubview:subview];
}

- (void)didUpdateReactSubviews
{
// Do nothing, as subviews are managed by `insertReactSubview:atIndex:`
}

- (void)didSetProps:(NSArray<NSString *> *)changedProps
{
// If the accessory view is not linked to a text input via nativeID, assume it is
// a standalone component that should get focus whenever it is rendered
if (![changedProps containsObject:@"nativeID"] && !self.nativeID) {
_contentShouldBeFirstResponder = YES;
}
}

@end
14 changes: 14 additions & 0 deletions Libraries/Text/TextInput/RCTInputAccessoryViewContent.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* 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.
*/

#import <UIKit/UIKit.h>

@interface RCTInputAccessoryViewContent : UIView

@property (nonatomic, readwrite, retain) UIView *contentView;

@end
Loading

0 comments on commit 38197c8

Please sign in to comment.