Skip to content

Commit

Permalink
feat: parentScrollViewTarget detection (#347)
Browse files Browse the repository at this point in the history
## 馃摐 Description

Added `parentScrollViewTarget` filed to `useReanimatedFocusedInput`.

## 馃挕 Motivation and Context

The motivation behind this field is described in
#336

Here I'd like to focus on implementation details. The implementation is
quite straighforward - we add new field to types/specs/events and we're
ready to go. And this is true, but I'd like to give some implementation
details:
- all search of ScrollView parent I moved to extension (added new
property `parentScrollViewTarget` on iOS/Android to focused input);
- on iOS I need to check whether new arch is enabled (the same as in
#191)
to get correct tag of the view;

Also this PR optimizes performance in case if you have a lot of
`KeyboardAwareScrollView`'s in stack-navigator - before it would scroll
all these ScrollView whenever keyboard appears, but now it scrolls only
`KeyboardAwareScrollView` that is actually visible 馃憖

Closes
#336

## 馃摙 Changelog

### Docs

- added a reference about `parentScrollViewTarget`;

### JS

- added `parentScrollViewTarget` to types declaration;
- added `parentScrollViewTarget` to specs;
- started to use `parentScrollViewTarget` in `KeyboardAwareScrollView`
component;
- added a mock + updated unit tests;

### iOS

- find `parentScrollViewTarget` and send it to JS;

### Android

- find `parentScrollViewTarget` and send it to JS;

## 馃 How Has This Been Tested?

Tested manually on:
- Pixel 7 Pro (Android 14);
- iPhone 15 Pro (iOS 17.2);

## 馃摳 Screenshots (if appropriate):


https://github.com/kirillzyusko/react-native-keyboard-controller/assets/22820318/3d0e430a-b284-4038-bddc-1e6462649f4f

## 馃摑 Checklist

- [x] CI successfully passed
- [x] I added new mocks and corresponding unit-tests if library API was
changed
  • Loading branch information
kirillzyusko committed Feb 19, 2024
1 parent 031c64c commit 49124f9
Show file tree
Hide file tree
Showing 18 changed files with 101 additions and 2 deletions.
1 change: 1 addition & 0 deletions FabricExample/__tests__/focused-input.spec.tsx
Expand Up @@ -35,6 +35,7 @@ describe("`useReanimatedFocusedInput` mocking", () => {
input: {
value: {
target: 2,
parentScrollViewTarget: -1,
layout: {
x: 10,
y: 100,
Expand Down
Expand Up @@ -79,6 +79,7 @@ export default function AwareScrollViewStickyFooter({ navigation }: Props) {
<KeyboardStickyView offset={offset}>
<View onLayout={handleLayout} style={styles.footer}>
<Text style={styles.footerText}>A mocked sticky footer</Text>
<TextInput style={styles.inputInFooter} placeholder="Amount" />
<Button title="Click me" />
</View>
</KeyboardStickyView>
Expand Down
Expand Up @@ -37,4 +37,8 @@ export const styles = StyleSheet.create({
color: "black",
paddingRight: 12,
},
inputInFooter: {
width: 200,
backgroundColor: "yellow",
},
});
Expand Up @@ -12,6 +12,7 @@ data class FocusedInputLayoutChangedEventData(
val absoluteX: Double,
val absoluteY: Double,
val target: Int,
val parentScrollViewTarget: Int,
)

class FocusedInputLayoutChangedEvent(
Expand All @@ -26,6 +27,7 @@ class FocusedInputLayoutChangedEvent(

override fun getEventData(): WritableMap? = Arguments.createMap().apply {
putInt("target", event.target)
putInt("parentScrollViewTarget", event.parentScrollViewTarget)
putMap(
"layout",
Arguments.createMap().apply {
Expand Down
Expand Up @@ -3,7 +3,9 @@ package com.reactnativekeyboardcontroller.extensions
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.View
import android.widget.EditText
import android.widget.ScrollView
import com.facebook.react.views.textinput.ReactEditText
import java.lang.reflect.Field

Expand Down Expand Up @@ -64,3 +66,23 @@ fun EditText.addOnTextChangedListener(action: (String) -> Unit): TextWatcher {

return listener
}

val EditText.parentScrollViewTarget: Int
get() {
var currentView: View? = this

while (currentView != null) {
val parentView = currentView.parent as? View

if (parentView is ScrollView) {
// If the parent is a ScrollView, return its id
return parentView.id
}

// Move to the next parent view
currentView = parentView
}

// ScrollView was not found
return -1
}
Expand Up @@ -15,6 +15,7 @@ import com.reactnativekeyboardcontroller.extensions.addOnTextChangedListener
import com.reactnativekeyboardcontroller.extensions.dispatchEvent
import com.reactnativekeyboardcontroller.extensions.dp
import com.reactnativekeyboardcontroller.extensions.emitEvent
import com.reactnativekeyboardcontroller.extensions.parentScrollViewTarget
import com.reactnativekeyboardcontroller.extensions.rootView
import com.reactnativekeyboardcontroller.extensions.screenLocation
import com.reactnativekeyboardcontroller.traversal.FocusedInputHolder
Expand All @@ -28,6 +29,7 @@ val noFocusedInputEvent = FocusedInputLayoutChangedEventData(
absoluteX = 0.0,
absoluteY = 0.0,
target = -1,
parentScrollViewTarget = -1,
)

class FocusedInputObserver(val view: ReactViewGroup, private val context: ThemedReactContext?) {
Expand Down Expand Up @@ -102,6 +104,7 @@ class FocusedInputObserver(val view: ReactViewGroup, private val context: Themed
absoluteX = x.toFloat().dp,
absoluteY = y.toFloat().dp,
target = input.id,
parentScrollViewTarget = input.parentScrollViewTarget,
)

dispatchEventToJS(event)
Expand Down
1 change: 1 addition & 0 deletions docs/docs/api/hooks/input/use-reanimated-focused-input.md
Expand Up @@ -32,6 +32,7 @@ The `input` property from this hook is returned as `SharedValue`. The returned d
```ts
type FocusedInputLayoutChangedEvent = {
target: number; // tag of the focused TextInput
parentScrollViewTarget: number; // tag of the parent ScrollView

// layout of the focused TextInput
layout: {
Expand Down
1 change: 1 addition & 0 deletions example/__tests__/focused-input.spec.tsx
Expand Up @@ -35,6 +35,7 @@ describe("`useReanimatedFocusedInput` mocking", () => {
input: {
value: {
target: 2,
parentScrollViewTarget: -1,
layout: {
x: 10,
y: 100,
Expand Down
Expand Up @@ -79,6 +79,7 @@ export default function AwareScrollViewStickyFooter({ navigation }: Props) {
<KeyboardStickyView offset={offset}>
<View onLayout={handleLayout} style={styles.footer}>
<Text style={styles.footerText}>A mocked sticky footer</Text>
<TextInput style={styles.inputInFooter} placeholder="Amount" />
<Button title="Click me" />
</View>
</KeyboardStickyView>
Expand Down
Expand Up @@ -37,4 +37,8 @@ export const styles = StyleSheet.create({
color: "black",
paddingRight: 12,
},
inputInFooter: {
width: 200,
backgroundColor: "yellow",
},
});
29 changes: 29 additions & 0 deletions ios/Extensions.swift
Expand Up @@ -53,6 +53,35 @@ public extension Optional where Wrapped == UIResponder {
}
}

public extension UIScrollView {
var reactViewTag: NSNumber {
#if KEYBOARD_CONTROLLER_NEW_ARCH_ENABLED
return (superview?.tag ?? -1) as NSNumber
#else
return superview?.reactTag ?? -1
#endif
}
}

public extension Optional where Wrapped: UIResponder {
var parentScrollViewTarget: NSNumber {
var currentResponder: UIResponder? = self

while let currentView = currentResponder {
// If the current responder is a UIScrollView (excluding UITextView), return its tag
if let scrollView = currentView as? UIScrollView, !(currentView is UITextView) {
return scrollView.reactViewTag
}

// Move to the next responder in the chain
currentResponder = currentView.next
}

// UIScrollView is not found
return -1
}
}

public extension UIView {
var globalFrame: CGRect? {
let rootView = UIApplication.shared.keyWindow?.rootViewController?.view
Expand Down
3 changes: 3 additions & 0 deletions ios/events/FocusedInputLayoutChangedEvent.m
Expand Up @@ -11,6 +11,7 @@

@implementation FocusedInputLayoutChangedEvent {
NSNumber *_target;
NSNumber *_parentScrollViewTarget;
NSObject *_layout;
uint16_t _coalescingKey;
}
Expand All @@ -29,6 +30,7 @@ - (instancetype)initWithReactTag:(NSNumber *)reactTag event:(NSObject *)event
if ((self = [super init])) {
_viewTag = reactTag;
_target = [event valueForKey:@"target"];
_parentScrollViewTarget = [event valueForKey:@"parentScrollViewTarget"];
_layout = [event valueForKey:@"layout"];
_coalescingKey = 0;
}
Expand All @@ -46,6 +48,7 @@ - (NSDictionary *)body
{
NSDictionary *body = @{
@"target" : _target,
@"parentScrollViewTarget" : _parentScrollViewTarget,
@"layout" : _layout,
};

Expand Down
2 changes: 2 additions & 0 deletions ios/observers/FocusedInputObserver.swift
Expand Up @@ -11,6 +11,7 @@ import UIKit

let noFocusedInputEvent: [String: Any] = [
"target": -1,
"parentScrollViewTarget": -1,
"layout": [
"absoluteX": 0,
"absoluteY": 0,
Expand Down Expand Up @@ -105,6 +106,7 @@ public class FocusedInputObserver: NSObject {

let data: [String: Any] = [
"target": responder.reactViewTag,
"parentScrollViewTarget": responder.parentScrollViewTarget,
"layout": [
"absoluteX": globalFrame?.origin.x,
"absoluteY": globalFrame?.origin.y,
Expand Down
2 changes: 2 additions & 0 deletions ios/views/KeyboardControllerView.mm
Expand Up @@ -55,6 +55,7 @@ - (instancetype)initWithFrame:(CGRect)frame
initOnLayoutChangedHandler:^(NSDictionary *event) {
if (self->_eventEmitter) {
int target = [event[@"target"] integerValue];
int parentScrollViewTarget = [event[@"parentScrollViewTarget"] integerValue];
double absoluteY = [event[@"layout"][@"absoluteY"] doubleValue];
double absoulteX = [event[@"layout"][@"absoluteX"] doubleValue];
double y = [event[@"layout"][@"y"] doubleValue];
Expand All @@ -68,6 +69,7 @@ - (instancetype)initWithFrame:(CGRect)frame
facebook::react::KeyboardControllerViewEventEmitter::
OnFocusedInputLayoutChanged{
.target = target,
.parentScrollViewTarget = parentScrollViewTarget,
.layout = facebook::react::KeyboardControllerViewEventEmitter::
OnFocusedInputLayoutChangedLayout{
.absoluteY = absoluteY,
Expand Down
1 change: 1 addition & 0 deletions jest/index.js
Expand Up @@ -14,6 +14,7 @@ const focusedInput = {
input: {
value: {
target: 1,
parentScrollViewTarget: -1,
layout: {
x: 0,
y: 0,
Expand Down
24 changes: 22 additions & 2 deletions src/components/KeyboardAwareScrollView/index.tsx
Expand Up @@ -18,7 +18,11 @@ import {
import { useSmoothKeyboardHandler } from "./useSmoothKeyboardHandler";
import { debounce } from "./utils";

import type { ScrollView, ScrollViewProps } from "react-native";
import type {
LayoutChangeEvent,
ScrollView,
ScrollViewProps,
} from "react-native";
import type { FocusedInputLayoutChangedEvent } from "react-native-keyboard-controller";

type KeyboardAwareScrollViewProps = {
Expand Down Expand Up @@ -75,6 +79,7 @@ const KeyboardAwareScrollView = forwardRef<
(
{
children,
onLayout,
bottomOffset = 0,
disableScrollOnKeyboardHide = false,
enabled = true,
Expand All @@ -83,6 +88,7 @@ const KeyboardAwareScrollView = forwardRef<
ref,
) => {
const scrollViewAnimatedRef = useAnimatedRef<Reanimated.ScrollView>();
const scrollViewTarget = useSharedValue<number | null>(null);
const scrollPosition = useSharedValue(0);
const position = useSharedValue(0);
const currentKeyboardFrameHeight = useSharedValue(0);
Expand Down Expand Up @@ -114,6 +120,14 @@ const KeyboardAwareScrollView = forwardRef<

scrollViewAnimatedRef(assignedRef);
}, []);
const onScrollViewLayout = useCallback(
(e: LayoutChangeEvent & { nativeEvent: { target: number } }) => {
scrollViewTarget.value = e.nativeEvent.target;

onLayout?.(e);
},
[onLayout],
);

/**
* Function that will scroll a ScrollView as keyboard gets moving
Expand All @@ -126,6 +140,11 @@ const KeyboardAwareScrollView = forwardRef<
return 0;
}

// input belongs to ScrollView
if (layout.value?.parentScrollViewTarget !== scrollViewTarget.value) {
return 0;
}

const visibleRect = height - keyboardHeight.value;
const absoluteY = layout.value?.layout.absoluteY || 0;
const inputHeight = layout.value?.layout.height || 0;
Expand Down Expand Up @@ -300,7 +319,8 @@ const KeyboardAwareScrollView = forwardRef<
<Reanimated.ScrollView
ref={onRef}
{...rest}
// @ts-expect-error `onScrollReanimated` is a fake prop needed for reanimated to intercept scroll events
// @ts-expect-error https://github.com/facebook/react-native/pull/42785
onLayout={onScrollViewLayout}
onScrollReanimated={onScroll}
scrollEventThrottle={16}
>
Expand Down
1 change: 1 addition & 0 deletions src/specs/KeyboardControllerViewNativeComponent.ts
Expand Up @@ -17,6 +17,7 @@ type KeyboardMoveEvent = Readonly<{

type FocusedInputLayoutChangedEvent = Readonly<{
target: Int32;
parentScrollViewTarget: Int32;
layout: {
x: Double;
y: Double;
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Expand Up @@ -13,6 +13,7 @@ export type NativeEvent = {
};
export type FocusedInputLayoutChangedEvent = {
target: number;
parentScrollViewTarget: number;
layout: {
x: number;
y: number;
Expand Down

0 comments on commit 49124f9

Please sign in to comment.