Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: keyboard offset on iOS #727

Merged
merged 50 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
7930686
feat: add basic fs structure for managing keyboard offset (just save …
kirillzyusko Nov 2, 2024
b5c1a04
feat: more changes to actually move progress further
kirillzyusko Nov 3, 2024
97dc506
fix: lint, remove iav
kirillzyusko Nov 4, 2024
74a4c03
fix: iOS 16 incorrect height in onEnd event
kirillzyusko Nov 4, 2024
c379e5e
chore: update TODO
kirillzyusko Nov 7, 2024
afef791
fix: grow of input breaks `onInteractive` handler
kirillzyusko Nov 8, 2024
11d7e88
docs: document new API
kirillzyusko Nov 16, 2024
8021ee4
fix: objc lint
kirillzyusko Nov 16, 2024
4722c72
fix: lint ts
kirillzyusko Nov 16, 2024
169ea95
fix: platform inconsistency, fabric Android build
kirillzyusko Nov 16, 2024
ae95fa5
fix: lint ts
kirillzyusko Nov 16, 2024
87991b6
fix: fabric build
kirillzyusko Nov 16, 2024
8aa0ef6
fix: suppress detekt violation
kirillzyusko Nov 16, 2024
be6e504
fix: conflicts after rebasing to main
kirillzyusko Nov 16, 2024
da16320
fix: iOS unit tests
kirillzyusko Nov 16, 2024
b1d3504
fix: mock `nativeID` to pass iOS unit tests
kirillzyusko Nov 16, 2024
0c2fc3a
fix: swiftformat
kirillzyusko Nov 18, 2024
0881ba2
refactor: create separate class to simplify KeyboardMovementObserver …
kirillzyusko Nov 24, 2024
54e48a6
refactor: keep progress
kirillzyusko Nov 24, 2024
5cbfc48
chore: update list of bugs
kirillzyusko Nov 24, 2024
c1fd2b9
fix: no random onStart/onEnd events during interactive keyboard dismi…
kirillzyusko Nov 26, 2024
d11afd6
feat: try to de-attach input accessory view only when keyboard got cl…
kirillzyusko Nov 26, 2024
244279c
fix: SpringAnimation synchronization when keyboard gets hidden
kirillzyusko Nov 26, 2024
88ff6a6
fix: synchronize Timing animation
kirillzyusko Dec 2, 2024
bff56d3
chore: update TODO list
kirillzyusko Dec 3, 2024
bf736be
fix: swiftformat
kirillzyusko Dec 4, 2024
2df02a9
fix: one swiftlint problem
kirillzyusko Dec 4, 2024
2d75ce7
chore: remove TODO (and keep commented code)
kirillzyusko Dec 4, 2024
5eb2254
fix: swiftformat
kirillzyusko Dec 4, 2024
3065de6
chore: reduce LOC in `KeyboardMovementObserver`
kirillzyusko Dec 4, 2024
d44ae11
fix: iOS 15 flickering
kirillzyusko Dec 4, 2024
063facc
chore: updated todo list
kirillzyusko Dec 4, 2024
454cab9
fix: don't make async resign for RCTRootView
kirillzyusko Dec 5, 2024
a12cdff
fix: negative height
kirillzyusko Dec 6, 2024
d9deddd
fix: update todo list
kirillzyusko Dec 9, 2024
2d4e87b
chore: formatting after all rebases
kirillzyusko Dec 10, 2024
75acff2
fix: random events after keyboard dismissal
kirillzyusko Dec 11, 2024
ef3396a
fix: crash on iOS 15 after re-entering page
kirillzyusko Dec 11, 2024
108919c
chore: updated todo list
kirillzyusko Dec 11, 2024
ecf1b9d
fix: keep focus in offset map to prevent a case, when key/offset pair…
kirillzyusko Dec 11, 2024
9be1a8e
refactor: remove pan gesture code
kirillzyusko Dec 12, 2024
e9eca4a
fix: objc lint
kirillzyusko Dec 12, 2024
df65897
fix: attempt to make fabric working
kirillzyusko Dec 12, 2024
ffd4432
fix: fabric RN changes
kirillzyusko Dec 20, 2024
9016aad
fix: missing nativeId on Fabric
kirillzyusko Dec 21, 2024
ce6224b
fix: fabric nativeId lookup
kirillzyusko Dec 21, 2024
cee3449
fix: update props of TextInput
kirillzyusko Dec 23, 2024
e6d6bad
chore: cleanup
kirillzyusko Jan 14, 2025
ae735e4
fix: TS
kirillzyusko Jan 14, 2025
f88d811
chore: remove last print statement
kirillzyusko Jan 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions FabricExample/patches/react-native+0.76.2.patch
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];

Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import React, { useCallback, useRef } from "react";
import { TextInput, View } from "react-native";
import { useKeyboardHandler } from "react-native-keyboard-controller";
import React, { useCallback, useRef, useState } from "react";
import { TextInput } from "react-native";
import {
KeyboardGestureArea,
useKeyboardHandler,
} from "react-native-keyboard-controller";
import Reanimated, {
useAnimatedProps,
useAnimatedScrollHandler,
Expand All @@ -13,6 +16,8 @@ import { history } from "../../../components/Message/data";

import styles from "./styles";

import type { LayoutChangeEvent } from "react-native";

const AnimatedTextInput = Reanimated.createAnimatedComponent(TextInput);

const useKeyboardAnimation = () => {
Expand Down Expand Up @@ -86,6 +91,12 @@ const contentContainerStyle = {
function InteractiveKeyboard() {
const ref = useRef<Reanimated.ScrollView>(null);
const { height, onScroll, inset, offset } = useKeyboardAnimation();
const [inputHeight, setInputHeight] = useState(TEXT_INPUT_HEIGHT);
const [text, setText] = useState("");

const onInputLayoutChanged = useCallback((e: LayoutChangeEvent) => {
setInputHeight(e.nativeEvent.layout.height);
}, []);

const scrollToBottom = useCallback(() => {
ref.current?.scrollToEnd({ animated: false });
Expand All @@ -94,7 +105,7 @@ function InteractiveKeyboard() {
const textInputStyle = useAnimatedStyle(
() => ({
position: "absolute",
height: TEXT_INPUT_HEIGHT,
minHeight: TEXT_INPUT_HEIGHT,
width: "100%",
backgroundColor: "#BCBCBC",
transform: [{ translateY: -height.value }],
Expand All @@ -113,7 +124,11 @@ function InteractiveKeyboard() {
}));

return (
<View style={styles.container}>
<KeyboardGestureArea
offset={inputHeight}
style={styles.container}
textInputNativeID="chat-input"
>
<Reanimated.ScrollView
ref={ref}
// simulation of `automaticallyAdjustKeyboardInsets` behavior on RN < 0.73
Expand All @@ -130,8 +145,16 @@ function InteractiveKeyboard() {
<Message key={index} {...message} />
))}
</Reanimated.ScrollView>
<AnimatedTextInput style={textInputStyle} testID="chat.input" />
</View>
<AnimatedTextInput
multiline
nativeID="chat-input"
style={textInputStyle}
testID="chat.input"
value={text}
onChangeText={setText}
onLayout={onInputLayoutChanged}
/>
</KeyboardGestureArea>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,12 @@ class KeyboardGestureAreaViewManager(
) {
manager.setScrollKeyboardOffScreenWhenVisible(view as KeyboardGestureAreaReactViewGroup, value)
}

@ReactProp(name = "textInputNativeID")
override fun setTextInputNativeID(
view: ReactViewGroup,
value: String?,
) {
// no-op
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,13 @@ class KeyboardGestureAreaViewManager(
) {
manager.setScrollKeyboardOffScreenWhenVisible(view, value)
}

@Suppress("detekt:UnusedParameter")
@ReactProp(name = "textInputNativeID")
fun setTextInputNativeID(
view: KeyboardGestureAreaReactViewGroup,
value: String,
) {
// no-op
}
}
23 changes: 13 additions & 10 deletions docs/docs/api/keyboard-gesture-area.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,6 @@ keywords:
]
---

<!-- prettier-ignore-start -->
<!-- we explicitly specify title and h1 because we add badge to h1 and we don't want this element to go to table of contents -->
<!-- markdownlint-disable-next-line MD025 -->
# KeyboardGestureArea <div className="label android"></div>
<!-- prettier-ignore-end -->

`KeyboardGestureArea` allows you to define a region on the screen, where gestures will control the keyboard position.

:::info Platform availability
Expand All @@ -28,27 +22,36 @@ This component is available only for Android >= 11. For iOS and Android < 11 it

Extra distance to the keyboard. Default is `0`.

### `interpolator`
### `interpolator` <div className="label android"></div>

String with possible values `linear` and `ios`:

- **ios** - interactive keyboard dismissing will work as in iOS: swipes in non-keyboard area will not affect keyboard positioning, but if your swipe touches keyboard - keyboard will follow finger position.
- **linear** - gestures inside the component will linearly affect the position of the keyboard, i.e. if the user swipes down by 20 pixels, then the keyboard will also be moved down by 20 pixels, even if the gesture was not made over the keyboard area.

### `showOnSwipeUp`
### `showOnSwipeUp` <div className="label android"></div>

A boolean prop which allows to customize interactive keyboard behavior. If set to `true` then it allows to show keyboard (if it's already closed) by swipe up gesture. `false` by default.

### `enableSwipeToDismiss`
### `enableSwipeToDismiss` <div className="label android"></div>

A boolean prop which allows to customize interactive keyboard behavior. If set to `false`, then any gesture will not affect keyboard position if the keyboard is shown. `true` by default.

### `textInputNativeID` <div className="label ios"></div>

A corresponding `nativeID` value from the corresponding `TextInput`.

## Example

```tsx
<KeyboardGestureArea interpolator="ios" offset={50}>
<KeyboardGestureArea
interpolator="ios"
offset={50}
textInputNativeID="composer"
>
<ScrollView>
{/* The other UI components of application in your tree */}
</ScrollView>
<TextInput nativeID="composer" />
</KeyboardGestureArea>
```
37 changes: 30 additions & 7 deletions example/src/screens/Examples/InteractiveKeyboardIOS/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import React, { useCallback, useRef } from "react";
import { TextInput, View } from "react-native";
import { useKeyboardHandler } from "react-native-keyboard-controller";
import React, { useCallback, useRef, useState } from "react";
import { TextInput } from "react-native";
import {
KeyboardGestureArea,
useKeyboardHandler,
} from "react-native-keyboard-controller";
import Reanimated, {
useAnimatedProps,
useAnimatedScrollHandler,
Expand All @@ -13,6 +16,8 @@ import { history } from "../../../components/Message/data";

import styles from "./styles";

import type { LayoutChangeEvent } from "react-native";

const AnimatedTextInput = Reanimated.createAnimatedComponent(TextInput);

const useKeyboardAnimation = () => {
Expand Down Expand Up @@ -86,6 +91,12 @@ const contentContainerStyle = {
function InteractiveKeyboard() {
const ref = useRef<Reanimated.ScrollView>(null);
const { height, onScroll, inset, offset } = useKeyboardAnimation();
const [inputHeight, setInputHeight] = useState(TEXT_INPUT_HEIGHT);
const [text, setText] = useState("");

const onInputLayoutChanged = useCallback((e: LayoutChangeEvent) => {
setInputHeight(e.nativeEvent.layout.height);
}, []);

const scrollToBottom = useCallback(() => {
ref.current?.scrollToEnd({ animated: false });
Expand All @@ -94,7 +105,7 @@ function InteractiveKeyboard() {
const textInputStyle = useAnimatedStyle(
() => ({
position: "absolute",
height: TEXT_INPUT_HEIGHT,
minHeight: TEXT_INPUT_HEIGHT,
width: "100%",
backgroundColor: "#BCBCBC",
transform: [{ translateY: -height.value }],
Expand All @@ -113,7 +124,11 @@ function InteractiveKeyboard() {
}));

return (
<View style={styles.container}>
<KeyboardGestureArea
offset={inputHeight}
style={styles.container}
textInputNativeID="chat-input"
>
<Reanimated.ScrollView
ref={ref}
// simulation of `automaticallyAdjustKeyboardInsets` behavior on RN < 0.73
Expand All @@ -130,8 +145,16 @@ function InteractiveKeyboard() {
<Message key={index} {...message} />
))}
</Reanimated.ScrollView>
<AnimatedTextInput style={textInputStyle} testID="chat.input" />
</View>
<AnimatedTextInput
multiline
nativeID="chat-input"
style={textInputStyle}
testID="chat.input"
value={text}
onChangeText={setText}
onLayout={onInputLayoutChanged}
/>
</KeyboardGestureArea>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ public extension UIView {
var reactTag: NSNumber {
return tag as NSNumber
}

var nativeID: String {
return accessibilityIdentifier ?? ""
}
}
4 changes: 4 additions & 0 deletions ios/extensions/Notification.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ extension Notification {
return (duration, keyboardFrame)
}
}

extension Notification.Name {
static let shouldIgnoreKeyboardEvents = Notification.Name("shouldIgnoreKeyboardEvents")
}
10 changes: 10 additions & 0 deletions ios/extensions/UIResponder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ public extension Optional where Wrapped == UIResponder {
return (self as? UIView)?.superview?.reactTag ?? -1
#endif
}

var nativeID: String? {
guard let superview = (self as? UIView)?.superview else { return nil }

#if KEYBOARD_CONTROLLER_NEW_ARCH_ENABLED
return (superview as NSObject).value(forKey: "nativeId") as? String
#else
return (superview as? UIView)?.nativeID
#endif
}
}

public extension Optional where Wrapped: UIResponder {
Expand Down
59 changes: 59 additions & 0 deletions ios/interactive/InvisibleInputAccessoryView.swift
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
}
}
Loading
Loading