Skip to content

Commit 07584c8

Browse files
IslamRustamovkacperzolkiewskiexploIFpkaramon
authored
feat: add returnKeyType, submitBehavior, onSubmitEditing, returnKeyLabel props (#379)
# Summary Fixes: #295 - This PR solves this issue #295; - This PR adds new props such as **returnKeyType**, **submitBehavior**, **onSubmitEditing**, **returnKeyLabel** (Android only); - For the implementation I took most of the code from the original **TextInput** component of react-native; - This PR impacts the main files with the implementation of text input. ## Test Plan To verify that the props work set the corresponding props on the text input and see how they work. Note that **returnKeyLabel** is visible only in landscape mode. ## Screenshots / Videos Custom **returnKeyLabel**: <img width="2992" height="1344" alt="Screenshot_1768764380" src="https://github.com/user-attachments/assets/3c658f99-fa42-4dc8-9665-eda3f7776bf5" /> Custom **returnKeyType**s: <img width="121" height="72" alt="Screenshot 2026-01-18 at 22 31 07" src="https://github.com/user-attachments/assets/0f3027b2-e699-4880-81ba-16e1b6d2ed75" /> <img width="120" height="94" alt="Screenshot 2026-01-18 at 22 31 22" src="https://github.com/user-attachments/assets/28582b76-9213-4937-a548-9971f5b26aac" /> <img width="123" height="81" alt="Screenshot 2026-01-18 at 22 31 37" src="https://github.com/user-attachments/assets/ce8b5e9d-0591-4a3b-8afc-5ceb7b48a327" /> I advise you to test such props as **submitBehavior**, **onSubmitEditing** yourself by pulling this branch and testing them manually. ## Compatibility | OS | Implemented | | ------- | :---------: | | iOS | ✅ | | Android | ✅ | ### IMPORTANT NOTES Some of the things were probably implemented poorly so I advice you to not hesitate and jump in with proposals/your own changes in this PR since I might not quickly return to this task. 1. I didn't use built-in type for **returnKeyType** in NativeProps because codegen generates a mess when you pass a `type SomeType = "one" | "two" | "three"` and I can't comprehend how to use. If you have a better idea how to deal with it - please inform me about that; 2. Android multiline problem one - **returnKeyType** basically doesn't work; 3. Android multiline problem two - for some reason listener of "Done" button on keyboard doesn't work on multiline inputs, so I had to write a somewhat hack in TextWatcher to handle the press of "Done" button; 4. Problem 3 is also present on iOS. Because of multiline input I have to check for "\n" input to understand that we pressed "Done". So, if user tries to copy-paste "\n" - then it might blur the input if we set **submitBehavior** to blurAndSubmit (which is an unlikely scenario but this a somewhat hack). --------- Co-authored-by: Kacper Żółkiewski <kacper.zolkiewski@swmansion.com> Co-authored-by: Igor Furgała <74370735+exploIF@users.noreply.github.com> Co-authored-by: Kacper Żółkiewski <74975508+kacperzolkiewski@users.noreply.github.com> Co-authored-by: Piotr Karamon <33125365+pkaramon@users.noreply.github.com> Co-authored-by: Piotr Karamon <piotrkaramon3@gmail.com>
1 parent f6ed17e commit 07584c8

12 files changed

Lines changed: 258 additions & 1 deletion

File tree

android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import android.graphics.Color
99
import android.graphics.Rect
1010
import android.graphics.text.LineBreaker
1111
import android.os.Build
12+
import android.text.Editable
1213
import android.text.InputType
1314
import android.text.Spannable
1415
import android.util.AttributeSet
@@ -17,12 +18,14 @@ import android.util.Patterns
1718
import android.util.TypedValue
1819
import android.view.ActionMode
1920
import android.view.Gravity
21+
import android.view.KeyEvent
2022
import android.view.Menu
2123
import android.view.MenuItem
2224
import android.view.MotionEvent
2325
import android.view.inputmethod.EditorInfo
2426
import android.view.inputmethod.InputConnection
2527
import android.view.inputmethod.InputMethodManager
28+
import android.widget.TextView
2629
import androidx.appcompat.widget.AppCompatEditText
2730
import androidx.core.view.ViewCompat
2831
import com.facebook.react.bridge.ReactContext
@@ -43,6 +46,7 @@ import com.swmansion.enriched.textinput.events.OnContextMenuItemPressEvent
4346
import com.swmansion.enriched.textinput.events.OnInputBlurEvent
4447
import com.swmansion.enriched.textinput.events.OnInputFocusEvent
4548
import com.swmansion.enriched.textinput.events.OnRequestHtmlResultEvent
49+
import com.swmansion.enriched.textinput.events.OnSubmitEditingEvent
4650
import com.swmansion.enriched.textinput.spans.EnrichedInputH1Span
4751
import com.swmansion.enriched.textinput.spans.EnrichedInputH2Span
4852
import com.swmansion.enriched.textinput.spans.EnrichedInputH3Span
@@ -72,7 +76,9 @@ import java.util.regex.Pattern
7276
import java.util.regex.PatternSyntaxException
7377
import kotlin.math.ceil
7478

75-
class EnrichedTextInputView : AppCompatEditText {
79+
class EnrichedTextInputView :
80+
AppCompatEditText,
81+
TextView.OnEditorActionListener {
7682
var stateWrapper: StateWrapper? = null
7783
val selection: EnrichedSelection? = EnrichedSelection(this)
7884
val spanState: EnrichedSpanState? = EnrichedSpanState(this)
@@ -105,6 +111,7 @@ class EnrichedTextInputView : AppCompatEditText {
105111

106112
var fontSize: Float? = null
107113
private var lineHeight: Float? = null
114+
var submitBehavior: String? = null
108115
private var autoFocus = false
109116
private var typefaceDirty = false
110117
private var didAttachToWindow = false
@@ -137,6 +144,18 @@ class EnrichedTextInputView : AppCompatEditText {
137144

138145
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
139146
var inputConnection = super.onCreateInputConnection(outAttrs)
147+
148+
if (shouldSubmitOnReturn()) {
149+
// Remove the "No Enter Action" flag if it exists
150+
outAttrs.imeOptions = outAttrs.imeOptions and EditorInfo.IME_FLAG_NO_ENTER_ACTION.inv()
151+
152+
// Force the key to be "Done" (or whatever label you set) instead of "Return"
153+
// This ensures onEditorAction gets called instead of just inserting \n
154+
if (outAttrs.imeOptions and EditorInfo.IME_MASK_ACTION == EditorInfo.IME_ACTION_UNSPECIFIED) {
155+
outAttrs.imeOptions = outAttrs.imeOptions or EditorInfo.IME_ACTION_DONE
156+
}
157+
}
158+
140159
if (inputConnection != null) {
141160
inputConnection =
142161
EnrichedTextInputConnectionWrapper(
@@ -182,6 +201,53 @@ class EnrichedTextInputView : AppCompatEditText {
182201

183202
// Handle checkbox list item clicks
184203
this.setCheckboxClickListener()
204+
205+
setOnEditorActionListener(this)
206+
setReturnKeyLabel(DEFAULT_IME_ACTION_LABEL)
207+
}
208+
209+
// Similar implementation to: https://github.com/facebook/react-native/blob/c1f5445f4a59d0035389725e47da58eb3d2c267c/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt#L940
210+
override fun onEditorAction(
211+
v: TextView?,
212+
actionId: Int,
213+
event: KeyEvent?,
214+
): Boolean {
215+
// Check if it's a valid keyboard action (Done, Next, etc.) or the Enter key (IME_NULL)
216+
val isAction = (actionId and EditorInfo.IME_MASK_ACTION) != 0 || actionId == EditorInfo.IME_NULL
217+
218+
if (isAction) {
219+
val shouldSubmit = shouldSubmitOnReturn()
220+
val shouldBlur = shouldBlurOnReturn()
221+
222+
if (shouldSubmit) {
223+
emitSubmitEditing()
224+
}
225+
226+
if (shouldBlur) {
227+
clearFocus()
228+
}
229+
230+
if (shouldSubmit || shouldBlur) {
231+
return true
232+
}
233+
}
234+
235+
// Return false to let the system handle default behavior (like inserting \n)
236+
return false
237+
}
238+
239+
private fun emitSubmitEditing() {
240+
val context = context as ReactContext
241+
val surfaceId = UIManagerHelper.getSurfaceId(context)
242+
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, id)
243+
dispatcher?.dispatchEvent(
244+
OnSubmitEditingEvent(
245+
surfaceId,
246+
id,
247+
text,
248+
experimentalSynchronousEvents,
249+
),
250+
)
185251
}
186252

187253
// https://github.com/facebook/react-native/blob/36df97f500aa0aa8031098caf7526db358b6ddc1/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt#L295C1-L296C1
@@ -431,6 +497,10 @@ class EnrichedTextInputView : AppCompatEditText {
431497
}
432498
}
433499

500+
fun setReturnKeyLabel(returnKeyLabel: String?) {
501+
setImeActionLabel(returnKeyLabel, EditorInfo.IME_ACTION_UNSPECIFIED)
502+
}
503+
434504
fun setColor(colorInt: Int?) {
435505
if (colorInt == null) {
436506
setTextColor(Color.BLACK)
@@ -666,6 +736,10 @@ class EnrichedTextInputView : AppCompatEditText {
666736
defaultValueDirty = true
667737
}
668738

739+
fun shouldBlurOnReturn(): Boolean = submitBehavior == "blurAndSubmit"
740+
741+
fun shouldSubmitOnReturn(): Boolean = submitBehavior == "submit" || submitBehavior == "blurAndSubmit"
742+
669743
private fun updateDefaultValue() {
670744
if (!defaultValueDirty) return
671745

@@ -1009,5 +1083,6 @@ class EnrichedTextInputView : AppCompatEditText {
10091083
const val TAG = "EnrichedTextInputView"
10101084
const val CLIPBOARD_TAG = "react-native-enriched-clipboard"
10111085
private const val CONTEXT_MENU_ITEM_ID = 10000
1086+
const val DEFAULT_IME_ACTION_LABEL = "DONE"
10121087
}
10131088
}

android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewManager.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import com.swmansion.enriched.textinput.events.OnMentionDetectedEvent
2828
import com.swmansion.enriched.textinput.events.OnMentionEvent
2929
import com.swmansion.enriched.textinput.events.OnPasteImagesEvent
3030
import com.swmansion.enriched.textinput.events.OnRequestHtmlResultEvent
31+
import com.swmansion.enriched.textinput.events.OnSubmitEditingEvent
3132
import com.swmansion.enriched.textinput.spans.EnrichedSpans
3233
import com.swmansion.enriched.textinput.styles.HtmlStyle
3334
import com.swmansion.enriched.textinput.utils.jsonStringToStringMap
@@ -74,6 +75,7 @@ class EnrichedTextInputViewManager :
7475
map.put(OnInputKeyPressEvent.EVENT_NAME, mapOf("registrationName" to OnInputKeyPressEvent.EVENT_NAME))
7576
map.put(OnPasteImagesEvent.EVENT_NAME, mapOf("registrationName" to OnPasteImagesEvent.EVENT_NAME))
7677
map.put(OnContextMenuItemPressEvent.EVENT_NAME, mapOf("registrationName" to OnContextMenuItemPressEvent.EVENT_NAME))
78+
map.put(OnSubmitEditingEvent.EVENT_NAME, mapOf("registrationName" to OnSubmitEditingEvent.EVENT_NAME))
7779

7880
return map
7981
}
@@ -110,6 +112,30 @@ class EnrichedTextInputViewManager :
110112
view?.setCursorColor(color)
111113
}
112114

115+
@ReactProp(name = "returnKeyType")
116+
override fun setReturnKeyType(
117+
view: EnrichedTextInputView?,
118+
returnKeyType: String?,
119+
) {
120+
// Not supported on multiline text input
121+
}
122+
123+
@ReactProp(name = "submitBehavior")
124+
override fun setSubmitBehavior(
125+
view: EnrichedTextInputView?,
126+
submitBehavior: String?,
127+
) {
128+
view?.submitBehavior = submitBehavior
129+
}
130+
131+
@ReactProp(name = "returnKeyLabel")
132+
override fun setReturnKeyLabel(
133+
view: EnrichedTextInputView?,
134+
returnKeyLabel: String?,
135+
) {
136+
view?.setReturnKeyLabel(returnKeyLabel)
137+
}
138+
113139
@ReactProp(name = "selectionColor", customType = "Color")
114140
override fun setSelectionColor(
115141
view: EnrichedTextInputView?,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.swmansion.enriched.textinput.events
2+
3+
import android.text.Editable
4+
import com.facebook.react.bridge.Arguments
5+
import com.facebook.react.bridge.WritableMap
6+
import com.facebook.react.uimanager.events.Event
7+
8+
class OnSubmitEditingEvent(
9+
surfaceId: Int,
10+
viewId: Int,
11+
private val editable: Editable?,
12+
private val experimentalSynchronousEvents: Boolean,
13+
) : Event<OnSubmitEditingEvent>(surfaceId, viewId) {
14+
override fun getEventName(): String = EVENT_NAME
15+
16+
override fun getEventData(): WritableMap {
17+
val eventData: WritableMap = Arguments.createMap()
18+
val text = editable.toString()
19+
val normalizedText = text.replace(Regex("\\u200B"), "")
20+
eventData.putString("text", normalizedText)
21+
return eventData
22+
}
23+
24+
override fun experimental_isSynchronous(): Boolean = experimentalSynchronousEvents
25+
26+
companion object {
27+
const val EVENT_NAME: String = "onSubmitEditing"
28+
}
29+
}

apps/example/src/hooks/useEditorState.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
type OnChangeSelectionEvent,
1010
type OnKeyPressEvent,
1111
type OnPasteImagesEvent,
12+
type OnSubmitEditing,
1213
} from 'react-native-enriched';
1314
import { useRef, useState } from 'react';
1415
import { type MentionItem } from '../components/MentionPopup';
@@ -232,6 +233,10 @@ export function useEditorState() {
232233
}
233234
};
234235

236+
const handleSubmitEditingEvent = (e: OnSubmitEditing) => {
237+
console.log('Submitted editing:', e.text);
238+
};
239+
235240
return {
236241
ref,
237242
stylesState,
@@ -269,6 +274,7 @@ export function useEditorState() {
269274
handleChangeMention,
270275
handleUserMentionSelected,
271276
handleChannelMentionSelected,
277+
handleSubmitEditingEvent,
272278
submitLink,
273279
submitSetValue,
274280
selectImage,

apps/example/src/screens/DevScreen.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ export function DevScreen({ onSwitch }: DevScreenProps) {
5454
onChangeSelection={(e) =>
5555
editor.handleSelectionChangeEvent(e.nativeEvent)
5656
}
57+
onSubmitEditing={(e) =>
58+
editor.handleSubmitEditingEvent(e.nativeEvent)
59+
}
5760
onKeyPress={(e) => editor.handleKeyPress(e.nativeEvent)}
5861
androidExperimentalSynchronousEvents={
5962
ANDROID_EXPERIMENTAL_SYNCHRONOUS_EVENTS

apps/example/src/screens/TestScreen.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ export function TestScreen({ onSwitch }: TestScreenProps) {
8484
editor.handleSelectionChangeEvent(e.nativeEvent)
8585
}
8686
onKeyPress={(e) => editor.handleKeyPress(e.nativeEvent)}
87+
onSubmitEditing={(e) =>
88+
editor.handleSubmitEditingEvent(e.nativeEvent)
89+
}
8790
androidExperimentalSynchronousEvents={
8891
ANDROID_EXPERIMENTAL_SYNCHRONOUS_EVENTS
8992
}

ios/EnrichedTextInputView.mm

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#import "EnrichedTextInputView.h"
22
#import "CoreText/CoreText.h"
33
#import "ImageAttachment.h"
4+
#import "KeyboardUtils.h"
45
#import "LayoutManagerExtension.h"
56
#import "ParagraphAttributesUtils.h"
67
#import "RCTFabricComponentsPlugins.h"
@@ -51,6 +52,7 @@ @implementation EnrichedTextInputView {
5152
BOOL _emitTextChange;
5253
NSMutableDictionary<NSValue *, UIImageView *> *_attachmentViews;
5354
NSArray<NSDictionary *> *_contextMenuItems;
55+
NSString *_submitBehavior;
5456
}
5557

5658
// MARK: - Component utils
@@ -836,6 +838,17 @@ - (void)updateProps:(Props::Shared const &)props
836838
}
837839
}
838840

841+
if (newViewProps.returnKeyType != oldViewProps.returnKeyType) {
842+
NSString *str = [NSString fromCppString:newViewProps.returnKeyType];
843+
844+
textView.returnKeyType =
845+
[KeyboardUtils getUIReturnKeyTypeFromReturnKeyType:str];
846+
}
847+
848+
if (newViewProps.submitBehavior != oldViewProps.submitBehavior) {
849+
_submitBehavior = [NSString fromCppString:newViewProps.submitBehavior];
850+
}
851+
839852
// autoCapitalize
840853
if (newViewProps.autoCapitalize != oldViewProps.autoCapitalize) {
841854
NSString *str = [NSString fromCppString:newViewProps.autoCapitalize];
@@ -1192,6 +1205,15 @@ - (bool)isStyle:(StyleType)type activeInMap:(NSDictionary *)styleMap {
11921205
return false;
11931206
}
11941207

1208+
- (bool)textInputShouldReturn {
1209+
return [_submitBehavior isEqualToString:@"blurAndSubmit"];
1210+
}
1211+
1212+
- (bool)textInputShouldSubmitOnReturn {
1213+
return [_submitBehavior isEqualToString:@"blurAndSubmit"] ||
1214+
[_submitBehavior isEqualToString:@"submit"];
1215+
}
1216+
11951217
- (void)addStyleBlock:(StyleType)blocking to:(StyleType)blocked {
11961218
NSMutableArray *blocksArr = [blockingStyles[@(blocked)] mutableCopy];
11971219
if (![blocksArr containsObject:@(blocking)]) {
@@ -1351,6 +1373,19 @@ - (NSUInteger)getActualIndex:(NSInteger)visibleIndex text:(NSString *)text {
13511373
return actualIndex;
13521374
}
13531375

1376+
- (void)emitOnSubmitEdittingEvent {
1377+
auto emitter = [self getEventEmitter];
1378+
if (emitter != nullptr) {
1379+
NSString *stringToBeEmitted = [[textView.textStorage.string
1380+
stringByReplacingOccurrencesOfString:@"\u200B"
1381+
withString:@""] copy];
1382+
1383+
emitter->onSubmitEditing({
1384+
.text = [stringToBeEmitted toCppString],
1385+
});
1386+
}
1387+
}
1388+
13541389
- (void)emitOnLinkDetectedEvent:(NSString *)text
13551390
url:(NSString *)url
13561391
range:(NSRange)range {
@@ -1969,6 +2004,24 @@ - (void)handleKeyPressInRange:(NSString *)text range:(NSRange)range {
19692004
- (bool)textView:(UITextView *)textView
19702005
shouldChangeTextInRange:(NSRange)range
19712006
replacementText:(NSString *)text {
2007+
// Check if the user pressed "Enter"
2008+
if ([text isEqualToString:@"\n"]) {
2009+
const bool shouldSubmit = [self textInputShouldSubmitOnReturn];
2010+
const bool shouldReturn = [self textInputShouldReturn];
2011+
2012+
if (shouldSubmit) {
2013+
[self emitOnSubmitEdittingEvent];
2014+
}
2015+
2016+
if (shouldReturn) {
2017+
[textView endEditing:NO];
2018+
}
2019+
2020+
if (shouldSubmit || shouldReturn) {
2021+
return NO;
2022+
}
2023+
}
2024+
19722025
recentlyChangedRange = NSMakeRange(range.location, text.length);
19732026
[self handleKeyPressInRange:text range:range];
19742027

ios/utils/KeyboardUtils.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#import <UIKit/UIKit.h>
2+
#pragma once
3+
4+
@interface KeyboardUtils : NSObject
5+
+ (UIReturnKeyType)getUIReturnKeyTypeFromReturnKeyType:
6+
(NSString *)returnKeyType;
7+
@end

0 commit comments

Comments
 (0)