Skip to content

Commit 38197c8

Browse files
Peter Arganyfacebook-github-bot
Peter Argany
authored andcommitted
Support Input Accessory View (iOS Only) [1/N]
Reviewed By: mmmulani Differential Revision: D6886573 fbshipit-source-id: 71e1f812b1cc1698e4380211a6cedd59011b5495
1 parent c87d03a commit 38197c8

13 files changed

+427
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/**
2+
* Copyright (c) 2013-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @providesModule InputAccessoryView
8+
* @flow
9+
* @format
10+
*/
11+
'use strict';
12+
13+
const ColorPropType = require('ColorPropType');
14+
const React = require('React');
15+
const StyleSheet = require('StyleSheet');
16+
const ViewPropTypes = require('ViewPropTypes');
17+
18+
const requireNativeComponent = require('requireNativeComponent');
19+
20+
const RCTInputAccessoryView = requireNativeComponent('RCTInputAccessoryView');
21+
22+
/**
23+
* Note: iOS only
24+
*
25+
* A component which enables customization of the keyboard input accessory view.
26+
* The input accessory view is displayed above the keyboard whenever a TextInput
27+
* has focus. This component can be used to create custom toolbars.
28+
*
29+
* To use this component wrap your custom toolbar with the
30+
* InputAccessoryView component, and set a nativeID. Then, pass that nativeID
31+
* as the inputAccessoryViewID of whatever TextInput you desire. A simple
32+
* example:
33+
*
34+
* ```ReactNativeWebPlayer
35+
* import React, { Component } from 'react';
36+
* import { AppRegistry, TextInput, InputAccessoryView, Button } from 'react-native';
37+
*
38+
* export default class UselessTextInput extends Component {
39+
* constructor(props) {
40+
* super(props);
41+
* this.state = {text: 'Placeholder Text'};
42+
* }
43+
*
44+
* render() {
45+
* const inputAccessoryViewID = "uniqueID";
46+
* return (
47+
* <View>
48+
* <ScrollView keyboardDismissMode="interactive">
49+
* <TextInput
50+
* style={{
51+
* padding: 10,
52+
* paddingTop: 50,
53+
* }}
54+
* inputAccessoryViewID=inputAccessoryViewID
55+
* onChangeText={text => this.setState({text})}
56+
* value={this.state.text}
57+
* />
58+
* </ScrollView>
59+
* <InputAccessoryView nativeID=inputAccessoryViewID>
60+
* <Button
61+
* onPress={() => this.setState({text: 'Placeholder Text'})}
62+
* title="Reset Text"
63+
* />
64+
* </InputAccessoryView>
65+
* </View>
66+
* );
67+
* }
68+
* }
69+
*
70+
* // skip this line if using Create React Native App
71+
* AppRegistry.registerComponent('AwesomeProject', () => UselessTextInput);
72+
* ```
73+
*
74+
* This component can also be used to create sticky text inputs (text inputs
75+
* which are anchored to the top of the keyboard). To do this, wrap a
76+
* TextInput with the InputAccessoryView component, and don't set a nativeID.
77+
* For an example, look at InputAccessoryViewExample.js in RNTester.
78+
*/
79+
80+
type Props = {
81+
+children: React.Node,
82+
/**
83+
* An ID which is used to associate this `InputAccessoryView` to
84+
* specified TextInput(s).
85+
*/
86+
nativeID?: string,
87+
style?: ViewPropTypes.style,
88+
backgroundColor?: ColorPropType,
89+
};
90+
91+
class InputAccessoryView extends React.Component<Props> {
92+
render(): React.Node {
93+
if (React.Children.count(this.props.children) === 0) {
94+
return null;
95+
}
96+
97+
return (
98+
<RCTInputAccessoryView
99+
style={[this.props.style, styles.container]}
100+
nativeID={this.props.nativeID}
101+
backgroundColor={this.props.backgroundColor}>
102+
{this.props.children}
103+
</RCTInputAccessoryView>
104+
);
105+
}
106+
}
107+
108+
const styles = StyleSheet.create({
109+
container: {
110+
position: 'absolute',
111+
},
112+
});
113+
114+
module.exports = InputAccessoryView;

Libraries/Components/TextInput/TextInput.js

+7
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,13 @@ const TextInput = createReactClass({
590590
* This property is supported only for single-line TextInput component on iOS.
591591
*/
592592
caretHidden: PropTypes.bool,
593+
/**
594+
* An optional identifier which links a custom InputAccessoryView to
595+
* this text input. The InputAccessoryView is rendered above the
596+
* keyboard when this text input is focused.
597+
* @platform ios
598+
*/
599+
inputAccessoryViewID: PropTypes.string,
593600
},
594601
getDefaultProps(): Object {
595602
return {

Libraries/Text/RCTText.xcodeproj/project.pbxproj

+18
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@
103103
5956B1A6200FF35C008D9D16 /* RCTUITextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 5956B103200FEBA9008D9D16 /* RCTUITextField.m */; };
104104
5956B1A7200FF35C008D9D16 /* RCTVirtualTextShadowView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5956B12E200FEBAA008D9D16 /* RCTVirtualTextShadowView.m */; };
105105
5956B1A8200FF35C008D9D16 /* RCTVirtualTextViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5956B12B200FEBAA008D9D16 /* RCTVirtualTextViewManager.m */; };
106+
8F2807C7202D2B6B005D65E6 /* RCTInputAccessoryViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F2807C1202D2B6A005D65E6 /* RCTInputAccessoryViewManager.m */; };
107+
8F2807C8202D2B6B005D65E6 /* RCTInputAccessoryView.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F2807C3202D2B6A005D65E6 /* RCTInputAccessoryView.m */; };
108+
8F2807C9202D2B6B005D65E6 /* RCTInputAccessoryViewContent.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F2807C5202D2B6B005D65E6 /* RCTInputAccessoryViewContent.m */; };
106109
/* End PBXBuildFile section */
107110

108111
/* Begin PBXCopyFilesBuildPhase section */
@@ -229,6 +232,12 @@
229232
5956B12D200FEBAA008D9D16 /* RCTVirtualTextViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTVirtualTextViewManager.h; sourceTree = "<group>"; };
230233
5956B12E200FEBAA008D9D16 /* RCTVirtualTextShadowView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTVirtualTextShadowView.m; sourceTree = "<group>"; };
231234
5956B12F200FEBAA008D9D16 /* RCTConvert+Text.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RCTConvert+Text.m"; sourceTree = "<group>"; };
235+
8F2807C1202D2B6A005D65E6 /* RCTInputAccessoryViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTInputAccessoryViewManager.m; sourceTree = "<group>"; };
236+
8F2807C2202D2B6A005D65E6 /* RCTInputAccessoryViewContent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTInputAccessoryViewContent.h; sourceTree = "<group>"; };
237+
8F2807C3202D2B6A005D65E6 /* RCTInputAccessoryView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTInputAccessoryView.m; sourceTree = "<group>"; };
238+
8F2807C4202D2B6A005D65E6 /* RCTInputAccessoryView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTInputAccessoryView.h; sourceTree = "<group>"; };
239+
8F2807C5202D2B6B005D65E6 /* RCTInputAccessoryViewContent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTInputAccessoryViewContent.m; sourceTree = "<group>"; };
240+
8F2807C6202D2B6B005D65E6 /* RCTInputAccessoryViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTInputAccessoryViewManager.h; sourceTree = "<group>"; };
232241
/* End PBXFileReference section */
233242

234243
/* Begin PBXGroup section */
@@ -274,6 +283,12 @@
274283
5956B0FF200FEBA9008D9D16 /* TextInput */ = {
275284
isa = PBXGroup;
276285
children = (
286+
8F2807C4202D2B6A005D65E6 /* RCTInputAccessoryView.h */,
287+
8F2807C3202D2B6A005D65E6 /* RCTInputAccessoryView.m */,
288+
8F2807C2202D2B6A005D65E6 /* RCTInputAccessoryViewContent.h */,
289+
8F2807C5202D2B6B005D65E6 /* RCTInputAccessoryViewContent.m */,
290+
8F2807C6202D2B6B005D65E6 /* RCTInputAccessoryViewManager.h */,
291+
8F2807C1202D2B6A005D65E6 /* RCTInputAccessoryViewManager.m */,
277292
5956B113200FEBA9008D9D16 /* Multiline */,
278293
5956B10C200FEBA9008D9D16 /* RCTBackedTextInputDelegate.h */,
279294
5956B107200FEBA9008D9D16 /* RCTBackedTextInputDelegateAdapter.h */,
@@ -465,8 +480,11 @@
465480
5956B140200FEBAA008D9D16 /* RCTTextShadowView.m in Sources */,
466481
5956B131200FEBAA008D9D16 /* RCTRawTextViewManager.m in Sources */,
467482
5956B137200FEBAA008D9D16 /* RCTBaseTextInputShadowView.m in Sources */,
483+
8F2807C7202D2B6B005D65E6 /* RCTInputAccessoryViewManager.m in Sources */,
468484
5956B146200FEBAA008D9D16 /* RCTConvert+Text.m in Sources */,
485+
8F2807C9202D2B6B005D65E6 /* RCTInputAccessoryViewContent.m in Sources */,
469486
5956B13F200FEBAA008D9D16 /* RCTTextAttributes.m in Sources */,
487+
8F2807C8202D2B6B005D65E6 /* RCTInputAccessoryView.m in Sources */,
470488
5956B143200FEBAA008D9D16 /* RCTTextView.m in Sources */,
471489
5956B13C200FEBAA008D9D16 /* RCTUITextView.m in Sources */,
472490
5956B136200FEBAA008D9D16 /* RCTBackedTextInputDelegateAdapter.m in Sources */,

Libraries/Text/TextInput/RCTBaseTextInputView.h

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ NS_ASSUME_NONNULL_BEGIN
4646
@property (nonatomic, copy) RCTTextSelection *selection;
4747
@property (nonatomic, strong, nullable) NSNumber *maxLength;
4848
@property (nonatomic, copy) NSAttributedString *attributedText;
49+
@property (nonatomic, copy) NSString *inputAccessoryViewID;
4950

5051
@end
5152

Libraries/Text/TextInput/RCTBaseTextInputView.m

+32-6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
#import <React/RCTUtils.h>
1616
#import <React/UIView+React.h>
1717

18+
#import "RCTInputAccessoryView.h"
19+
#import "RCTInputAccessoryViewContent.h"
1820
#import "RCTTextAttributes.h"
1921
#import "RCTTextSelection.h"
2022

@@ -400,12 +402,33 @@ - (void)didMoveToWindow
400402

401403
- (void)didSetProps:(NSArray<NSString *> *)changedProps
402404
{
403-
[self invalidateInputAccessoryView];
405+
#if !TARGET_OS_TV
406+
if ([changedProps containsObject:@"inputAccessoryViewID"] && self.inputAccessoryViewID) {
407+
[self setCustomInputAccessoryViewWithNativeID:self.inputAccessoryViewID];
408+
} else if (!self.inputAccessoryViewID) {
409+
[self setDefaultInputAccessoryView];
410+
}
411+
#endif
412+
}
413+
414+
- (void)setCustomInputAccessoryViewWithNativeID:(NSString *)nativeID
415+
{
416+
__weak RCTBaseTextInputView *weakSelf = self;
417+
[_bridge.uiManager rootViewForReactTag:self.reactTag withCompletion:^(UIView *rootView) {
418+
RCTBaseTextInputView *strongSelf = weakSelf;
419+
if (rootView) {
420+
UIView *accessoryView = [strongSelf->_bridge.uiManager viewForNativeID:nativeID
421+
withRootTag:rootView.reactTag];
422+
if (accessoryView && [accessoryView isKindOfClass:[RCTInputAccessoryView class]]) {
423+
strongSelf.backedTextInputView.inputAccessoryView = ((RCTInputAccessoryView *)accessoryView).content.inputAccessoryView;
424+
[strongSelf reloadInputViewsIfNecessary];
425+
}
426+
}
427+
}];
404428
}
405429

406-
- (void)invalidateInputAccessoryView
430+
- (void)setDefaultInputAccessoryView
407431
{
408-
#if !TARGET_OS_TV
409432
UIView<RCTBackedTextInputViewProtocol> *textInputView = self.backedTextInputView;
410433
UIKeyboardType keyboardType = textInputView.keyboardType;
411434

@@ -443,12 +466,15 @@ - (void)invalidateInputAccessoryView
443466
else {
444467
textInputView.inputAccessoryView = nil;
445468
}
469+
[self reloadInputViewsIfNecessary];
470+
}
446471

472+
- (void)reloadInputViewsIfNecessary
473+
{
447474
// We have to call `reloadInputViews` for focused text inputs to update an accessory view.
448-
if (textInputView.isFirstResponder) {
449-
[textInputView reloadInputViews];
475+
if (self.backedTextInputView.isFirstResponder) {
476+
[self.backedTextInputView reloadInputViews];
450477
}
451-
#endif
452478
}
453479

454480
- (void)handleInputAccessoryDoneButton

Libraries/Text/TextInput/RCTBaseTextInputViewManager.m

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ @implementation RCTBaseTextInputViewManager
5353
RCT_EXPORT_VIEW_PROPERTY(maxLength, NSNumber)
5454
RCT_EXPORT_VIEW_PROPERTY(selectTextOnFocus, BOOL)
5555
RCT_EXPORT_VIEW_PROPERTY(selection, RCTTextSelection)
56+
RCT_EXPORT_VIEW_PROPERTY(inputAccessoryViewID, NSString)
5657

5758
RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock)
5859
RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import <UIKit/UIKit.h>
9+
10+
@class RCTBridge;
11+
@class RCTInputAccessoryViewContent;
12+
13+
@interface RCTInputAccessoryView : UIView
14+
15+
- (instancetype)initWithBridge:(RCTBridge *)bridge;
16+
17+
@property (nonatomic, readonly, strong) RCTInputAccessoryViewContent *content;
18+
19+
@end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import "RCTInputAccessoryView.h"
9+
10+
#import <React/RCTBridge.h>
11+
#import <React/RCTTouchHandler.h>
12+
#import <React/UIView+React.h>
13+
14+
#import "RCTInputAccessoryViewContent.h"
15+
16+
@implementation RCTInputAccessoryView
17+
{
18+
BOOL _contentShouldBeFirstResponder;
19+
}
20+
21+
- (instancetype)initWithBridge:(RCTBridge *)bridge
22+
{
23+
if (self = [super init]) {
24+
_content = [RCTInputAccessoryViewContent new];
25+
RCTTouchHandler *const touchHandler = [[RCTTouchHandler alloc] initWithBridge:bridge];
26+
[touchHandler attachToView:_content.inputAccessoryView];
27+
[self addSubview:_content];
28+
}
29+
return self;
30+
}
31+
32+
- (void)reactSetFrame:(CGRect)frame
33+
{
34+
[_content.inputAccessoryView setFrame:frame];
35+
[_content.contentView setFrame:frame];
36+
37+
if (_contentShouldBeFirstResponder) {
38+
_contentShouldBeFirstResponder = NO;
39+
[_content becomeFirstResponder];
40+
}
41+
}
42+
43+
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index
44+
{
45+
[super insertReactSubview:subview atIndex:index];
46+
[_content insertReactSubview:subview atIndex:index];
47+
}
48+
49+
- (void)removeReactSubview:(UIView *)subview
50+
{
51+
[super removeReactSubview:subview];
52+
[_content removeReactSubview:subview];
53+
}
54+
55+
- (void)didUpdateReactSubviews
56+
{
57+
// Do nothing, as subviews are managed by `insertReactSubview:atIndex:`
58+
}
59+
60+
- (void)didSetProps:(NSArray<NSString *> *)changedProps
61+
{
62+
// If the accessory view is not linked to a text input via nativeID, assume it is
63+
// a standalone component that should get focus whenever it is rendered
64+
if (![changedProps containsObject:@"nativeID"] && !self.nativeID) {
65+
_contentShouldBeFirstResponder = YES;
66+
}
67+
}
68+
69+
@end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import <UIKit/UIKit.h>
9+
10+
@interface RCTInputAccessoryViewContent : UIView
11+
12+
@property (nonatomic, readwrite, retain) UIView *contentView;
13+
14+
@end

0 commit comments

Comments
 (0)