Support Input Accessory View (iOS Only) [1/N]

Reviewed By: mmmulani

Differential Revision: D6886573

fbshipit-source-id: 71e1f812b1cc1698e4380211a6cedd59011b5495
This commit is contained in:
Peter Argany 2018-02-27 10:42:44 -08:00 committed by Facebook Github Bot
parent c87d03a8b2
commit 38197c8230
13 changed files with 427 additions and 6 deletions

View File

@ -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;

View File

@ -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 {

View File

@ -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 */
@ -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 */
@ -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 */,
@ -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 */,

View File

@ -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

View File

@ -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"
@ -400,12 +402,33 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame)
- (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)invalidateInputAccessoryView
- (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)setDefaultInputAccessoryView
{
#if !TARGET_OS_TV
UIView<RCTBackedTextInputViewProtocol> *textInputView = self.backedTextInputView;
UIKeyboardType keyboardType = textInputView.keyboardType;
@ -443,12 +466,15 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame)
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

View File

@ -53,6 +53,7 @@ RCT_EXPORT_VIEW_PROPERTY(clearTextOnFocus, BOOL)
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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,75 @@
/**
* 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 "RCTInputAccessoryViewContent.h"
#import <React/UIView+React.h>
@interface RCTInputAccessoryViewContent()
// Overriding `inputAccessoryView` to `readwrite`.
@property (nonatomic, readwrite, retain) UIView *inputAccessoryView;
@end
@implementation RCTInputAccessoryViewContent
- (BOOL)canBecomeFirstResponder
{
return true;
}
- (BOOL)becomeFirstResponder
{
const BOOL becameFirstResponder = [super becomeFirstResponder];
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */
// Avoiding the home pill and notch (landscape mode) on iphoneX.
if (becameFirstResponder) {
if (@available(iOS 11.0, *)) {
[_contentView.bottomAnchor
constraintLessThanOrEqualToSystemSpacingBelowAnchor:_contentView.window.safeAreaLayoutGuide.bottomAnchor
multiplier:1.0f].active = YES;
[_contentView.leftAnchor
constraintLessThanOrEqualToSystemSpacingAfterAnchor:_contentView.window.safeAreaLayoutGuide.leftAnchor
multiplier:1.0f].active = YES;
[_contentView.rightAnchor
constraintLessThanOrEqualToSystemSpacingAfterAnchor:_contentView.window.safeAreaLayoutGuide.rightAnchor
multiplier:1.0f].active = YES;
}
}
#endif
return becameFirstResponder;
}
- (UIView *)inputAccessoryView
{
if (!_inputAccessoryView) {
_inputAccessoryView = [UIView new];
_contentView = [UIView new];
[_inputAccessoryView addSubview:_contentView];
}
return _inputAccessoryView;
}
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index
{
[super insertReactSubview:subview atIndex:index];
[_contentView insertSubview:subview atIndex:index];
}
- (void)removeReactSubview:(UIView *)subview
{
[super removeReactSubview:subview];
[subview removeFromSuperview];
if ([[_inputAccessoryView subviews] count] == 0 && [self isFirstResponder]) {
[self resignFirstResponder];
}
}
@end

View File

@ -0,0 +1,12 @@
/**
* 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 <React/RCTViewManager.h>
@interface RCTInputAccessoryViewManager : RCTViewManager
@end

View File

@ -0,0 +1,28 @@
/**
* 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 "RCTInputAccessoryViewManager.h"
#import "RCTInputAccessoryView.h"
@implementation RCTInputAccessoryViewManager
RCT_EXPORT_MODULE()
+ (BOOL)requiresMainQueueSetup
{
return NO;
}
- (UIView *)view
{
return [[RCTInputAccessoryView alloc] initWithBridge:self.bridge];
}
RCT_REMAP_VIEW_PROPERTY(backgroundColor, content.inputAccessoryView.backgroundColor, UIColor)
@end

View File

@ -9,6 +9,8 @@
*/
'use strict';
const Button = require('Button');
const InputAccessoryView = require('InputAccessoryView');
var React = require('react');
var ReactNative = require('react-native');
var {
@ -91,6 +93,35 @@ class TextEventsExample extends React.Component<{}, $FlowFixMeState> {
}
}
class TextInputAccessoryViewExample extends React.Component<{}, *> {
constructor(props) {
super(props);
this.state = {text: 'Placeholder Text'};
}
render() {
const inputAccessoryViewID = 'inputAccessoryView1';
return (
<View>
<TextInput
style={styles.default}
inputAccessoryViewID={inputAccessoryViewID}
onChangeText={text => this.setState({text})}
value={this.state.text}
/>
<InputAccessoryView nativeID={inputAccessoryViewID}>
<View style={{backgroundColor: 'white'}}>
<Button
onPress={() => this.setState({text: 'Placeholder Text'})}
title="Reset Text"
/>
</View>
</InputAccessoryView>
</View>
);
}
}
class RewriteExample extends React.Component<$FlowFixMeProps, any> {
constructor(props) {
super(props);
@ -485,6 +516,12 @@ exports.examples = [
return <RewriteExampleInvalidCharacters />;
}
},
{
title: 'Keyboard Accessory View',
render: function() {
return <TextInputAccessoryViewExample />;
}
},
{
title: 'Auto-capitalize',
render: function() {