-
-
Notifications
You must be signed in to change notification settings - Fork 85
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
## 📜 Description Added an ability to specify `offset` for interactive keyboard dismissal on iOS. ## 💡 Motivation and Context In this PR I'm exposing `KeyboardGestureArea` on iOS and adding two props for that: `offset` and `textInputNativeID`. This PR is a re-thinking concept of how we work with `inputAccessoryView` on iOS. To make long text short - default `InputAccessoryView` comes with many restrictions, such as not growing `TextInput`, unability to specify position on the screen, weird animations on unmount, complexity with managing `SafeArea` insets, etc. We already have `KeyboardStickyView` that don't have all that problems, but if you interactively dismiss a keyboard then interactive dismissal starts from a top border of the keyboard (not the input). Taking a step back and utilising `inputAccessoryView` (moving a view from RN hierarchy directly into `inputAccessoryView`) is possible, but comes with a previous set of challenges. In this PR I decided to think about different concepts between iOS/Android and how to make a solution that will work everywhere identically. And the idea is to create an invisible/non-interactable instance of `inputAccessoryView`, that will simply extend the keyboard area (but keyboard-controller will know about that offset and will automatically exclude it from final keyboard dimensions, so you can use everything as you used before). Schematically all process can be shown on a diagram below: ![image](https://github.com/user-attachments/assets/06f85e15-9347-4569-b6a6-06018d61231f) However new approach comes with its own set of challenges. Mainly they come from the fact how keyboard dismissal works on iOS, and in simple words: - when you perform `Keyboard.dismiss()`/press enter then whole combination (keyboard + inputAccessoryView) is treated as a single keyboard and entire element gets hidden in a single animation. - when you perform interactive dismissal, then we have two fold animation - first we dismiss a keyboard, and in second stage we dismiss `inputAccessoryView`. From all the description above it's clear, that we want to ignore `inputAccessoryView` animations or exclude its height from the animation (when its animated as a single element). To solve the first problem (when keyboard dismissed as a single element) we need to remove `inputAccessoryView` and only then perform an animation. Otherwise if we use default hooks `useKeyboardAnimation`/`useReanimatedKeyboardAnimation` that rely on layout animation, then we will see unsynchronized animation (because for example actual keyboard height is 250 + 50, but in JS we give only value of 250, so we will animate from 250 to 0, though actual animation will be from 300 to 0). To fix that I had to swizzle into `resignFirstResponder`. In this method we see, if we have `InvisibleAccessoryView`, then we postpone a keyboard dismissal and remove current `inputAccessoryView`. In this case we will dismiss a keyboard without `inputAccessoryView`, so it will work as it works before. The second main challenge was a time when to remove `inputAccessoryView` during interactive keyboard dismissal. The initial idea was to remove it as soon as dismiss gesture begins. However I rejected this idea in d11afd6 mainly because it was causing a lot of issues (such as ghost animation when keyboard is fully dismissed). When we remove that code it removes additional complexity and we remove `inputAccessoryView` when we call `resignFirstResponder` (happens when keyboard gets dismissed, i. e. first phase passed). In this case it works more predictable. Last but not least - it's wort to note, that the idea with invisible `inputAccessoryView` is not new in iOS community, and some even native projects are utilizing it: https://github.com/iAmrMohamed/AMKeyboardFrameTracker Closes #250 ## 📢 Changelog <!-- High level overview of important changes --> <!-- For example: fixed status bar manipulation; added new types declarations; --> <!-- If your changes don't affect one of platform/language below - then remove this platform/language --> ### Docs - mention that `KeyboardGestureArea` is not Android specific anymore; - add new `textInputNativeID` description + show how to use it. ### JS - don't exclude `iOS` for `KeyboardGestureArea` in codegen; - expose new `textInputNativeID` property for `KeyboardGestureArea`; - applied patch in fabric example app facebook/react-native#48339 - make `interpolator` optional (will be `ios` on `iOS` and `linear` on `Android`) - make growing/multiline `TextInput` in interactive iOS keyboard example; ### iOS - expose `KeyboardGestureArea` on iOS as well - added `InvisibleInputAccessoryView` class; - added `KeyboardEventsIgnorer` class; - added `KeyboardAreaExtender` class; - added `KeyboardOffsetProvider` class; - added `KeyboardEventsIgnorer` class; - added `UIResponderSwizzle` class; - added `shouldIgnoreKeyboardEvents` event to `Notification`; - added `nativeID` extension to `UIResponder` (and it's mock for a native project); ### Android - added no-op setters for `textInputNativeId`; ## 🤔 How Has This Been Tested? Tested locally on: - iPhone 6s (iOS 15.8, real device); - iPhone 11 (iOS 18.0, iOS 18.1, real device) - iPhone 16 Pro (iOS 18.0, simulator) - iPhone 15 Pro (iOS 17.5, simulator) - iPhone 14 Pro (iOS 16.5, simulator) ## 📸 Screenshots (if appropriate): https://github.com/user-attachments/assets/097d76e1-4f79-4a27-89b7-43a479b6b32b ## 📝 Checklist - [x] CI successfully passed - [x] I added new mocks and corresponding unit-tests if library API was changed
- Loading branch information
1 parent
6e7c2db
commit 6da1bb4
Showing
21 changed files
with
653 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
diff --git a/node_modules/react-native/.DS_Store b/node_modules/react-native/.DS_Store | ||
new file mode 100644 | ||
index 0000000..597365c | ||
Binary files /dev/null and b/node_modules/react-native/.DS_Store differ | ||
diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm | ||
index e74500f..c2d4515 100644 | ||
--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm | ||
+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm | ||
@@ -68,6 +68,8 @@ @implementation RCTTextInputComponentView { | ||
* later comparison insensitive to them. | ||
*/ | ||
NSDictionary<NSAttributedStringKey, id> *_originalTypingAttributes; | ||
+ | ||
+ BOOL _hasInputAccessoryView; | ||
} | ||
|
||
#pragma mark - UIView overrides | ||
@@ -590,10 +592,12 @@ - (void)setDefaultInputAccessoryView | ||
keyboardType == UIKeyboardTypeDecimalPad || keyboardType == UIKeyboardTypeASCIICapableNumberPad) && | ||
containsKeyType; | ||
|
||
- if ((_backedTextInputView.inputAccessoryView != nil) == shouldHaveInputAccessoryView) { | ||
+ if (_hasInputAccessoryView == shouldHaveInputAccessoryView) { | ||
return; | ||
} | ||
|
||
+ _hasInputAccessoryView = shouldHaveInputAccessoryView; | ||
+ | ||
if (shouldHaveInputAccessoryView) { | ||
NSString *buttonLabel = [self returnKeyTypeToString:returnKeyType]; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
// | ||
// InvisibleInputAccessoryView.swift | ||
// Pods | ||
// | ||
// Created by Kiryl Ziusko on 01/11/2024. | ||
// | ||
|
||
import Foundation | ||
import UIKit | ||
|
||
public class InvisibleInputAccessoryView: UIView { | ||
var isShown = true | ||
|
||
override init(frame: CGRect) { | ||
super.init(frame: frame) | ||
setupView() | ||
} | ||
|
||
public convenience init(height: CGFloat) { | ||
self.init(frame: CGRect(x: 0, y: 0, width: 0, height: height)) | ||
} | ||
|
||
required init?(coder aDecoder: NSCoder) { | ||
super.init(coder: aDecoder) | ||
setupView() | ||
} | ||
|
||
override public func point(inside _: CGPoint, with _: UIEvent?) -> Bool { | ||
// Return false to allow touch events to pass through | ||
return false | ||
} | ||
|
||
public func updateHeight(to newHeight: CGFloat) { | ||
frame = CGRect(x: 0, y: 0, width: 0, height: newHeight) | ||
|
||
// Invalidate intrinsic content size to trigger a layout update | ||
invalidateIntrinsicContentSize() | ||
layoutIfNeeded() | ||
} | ||
|
||
public func hide() { | ||
guard isShown else { return } | ||
isShown = false | ||
updateHeight(to: 0.0) | ||
superview?.layoutIfNeeded() | ||
} | ||
|
||
override public var intrinsicContentSize: CGSize { | ||
return CGSize(width: UIView.noIntrinsicMetric, height: frame.height) | ||
} | ||
|
||
private func setupView() { | ||
isUserInteractionEnabled = false | ||
// for debug purposes | ||
// backgroundColor = UIColor.red.withAlphaComponent(0.2) | ||
backgroundColor = .clear | ||
autoresizingMask = .flexibleHeight | ||
} | ||
} |
Oops, something went wrong.