Skip to content

Commit 2dbcdba

Browse files
committed
feat: extraContentPadding prop
1 parent 7ece2d8 commit 2dbcdba

12 files changed

Lines changed: 699 additions & 28 deletions

File tree

FabricExample/src/screens/Examples/KeyboardChatScrollView/VirtualizedListScrollView.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,20 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
1111
import { useChatConfigStore } from "./store";
1212
import { MARGIN, TEXT_INPUT_HEIGHT } from "./styles";
1313

14+
import type { SharedValue } from "react-native-reanimated";
15+
16+
type VirtualizedListScrollViewProps = ScrollViewProps & {
17+
extraContentPadding?: SharedValue<number>;
18+
};
19+
1420
export type VirtualizedListScrollViewRef = React.ElementRef<
1521
typeof KeyboardChatScrollView
1622
>;
1723

1824
const VirtualizedListScrollView = forwardRef<
1925
VirtualizedListScrollViewRef,
20-
ScrollViewProps
21-
>(({ onLayout: onLayoutProp, ...props }, ref) => {
26+
VirtualizedListScrollViewProps
27+
>(({ onLayout: onLayoutProp, extraContentPadding, ...props }, ref) => {
2228
const [layoutPass, setLayoutPass] = useState(0);
2329
const { bottom } = useSafeAreaInsets();
2430
const chatKitOffset = bottom - MARGIN;
@@ -54,6 +60,7 @@ const VirtualizedListScrollView = forwardRef<
5460
: contentContainerStyle
5561
}
5662
contentInsetAdjustmentBehavior="never"
63+
extraContentPadding={extraContentPadding}
5764
freeze={freeze}
5865
inverted={isInvertedSupported}
5966
keyboardDismissMode="interactive"

FabricExample/src/screens/Examples/KeyboardChatScrollView/index.tsx

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
KeyboardGestureArea,
2020
KeyboardStickyView,
2121
} from "react-native-keyboard-controller";
22+
import { useSharedValue } from "react-native-reanimated";
2223
import {
2324
SafeAreaView,
2425
useSafeAreaInsets,
@@ -45,6 +46,7 @@ function KeyboardChatScrollViewPlayground() {
4546
const textInputRef = useRef<TextInput>(null);
4647
const textRef = useRef("");
4748
const [inputHeight, setInputHeight] = useState(TEXT_INPUT_HEIGHT);
49+
const extraContentPadding = useSharedValue(0);
4850
const { inverted, messages, reversedMessages, addMessage, mode } =
4951
useChatConfigStore();
5052
const { bottom } = useSafeAreaInsets();
@@ -54,9 +56,16 @@ function KeyboardChatScrollViewPlayground() {
5456
[bottom],
5557
);
5658

57-
const onInputLayoutChanged = useCallback((e: LayoutChangeEvent) => {
58-
setInputHeight(e.nativeEvent.layout.height);
59-
}, []);
59+
const onInputLayoutChanged = useCallback(
60+
(e: LayoutChangeEvent) => {
61+
const height = e.nativeEvent.layout.height;
62+
63+
// eslint-disable-next-line react-compiler/react-compiler
64+
extraContentPadding.value = Math.max(height - TEXT_INPUT_HEIGHT, 0);
65+
setInputHeight(height);
66+
},
67+
[extraContentPadding],
68+
);
6069
const onInput = useCallback((text: string) => {
6170
textRef.current = text;
6271
}, []);
@@ -86,8 +95,13 @@ function KeyboardChatScrollViewPlayground() {
8695
}, [messages]);
8796

8897
const memoList = useCallback(
89-
(props: ScrollViewProps) => <VirtualizedListScrollView {...props} />,
90-
[],
98+
(props: ScrollViewProps) => (
99+
<VirtualizedListScrollView
100+
{...props}
101+
extraContentPadding={extraContentPadding}
102+
/>
103+
),
104+
[extraContentPadding],
91105
);
92106

93107
return (
@@ -118,7 +132,7 @@ function KeyboardChatScrollViewPlayground() {
118132
startRenderingFromBottom: inverted,
119133
}}
120134
renderItem={({ item }) => <Message {...item} />}
121-
renderScrollComponent={VirtualizedListScrollView}
135+
renderScrollComponent={memoList}
122136
/>
123137
)}
124138
{mode === "flat" && (
@@ -132,7 +146,10 @@ function KeyboardChatScrollViewPlayground() {
132146
/>
133147
)}
134148
{mode === "scroll" && (
135-
<VirtualizedListScrollView ref={scrollRef}>
149+
<VirtualizedListScrollView
150+
ref={scrollRef}
151+
extraContentPadding={extraContentPadding}
152+
>
136153
{messages.map((message, index) => (
137154
<Message key={index} {...message} />
138155
))}

example/src/screens/Examples/KeyboardChatScrollView/VirtualizedListScrollView.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,20 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
1111
import { useChatConfigStore } from "./store";
1212
import { MARGIN, TEXT_INPUT_HEIGHT } from "./styles";
1313

14+
import type { SharedValue } from "react-native-reanimated";
15+
16+
type VirtualizedListScrollViewProps = ScrollViewProps & {
17+
extraContentPadding?: SharedValue<number>;
18+
};
19+
1420
export type VirtualizedListScrollViewRef = React.ElementRef<
1521
typeof KeyboardChatScrollView
1622
>;
1723

1824
const VirtualizedListScrollView = forwardRef<
1925
VirtualizedListScrollViewRef,
20-
ScrollViewProps
21-
>(({ onLayout: onLayoutProp, ...props }, ref) => {
26+
VirtualizedListScrollViewProps
27+
>(({ onLayout: onLayoutProp, extraContentPadding, ...props }, ref) => {
2228
const [layoutPass, setLayoutPass] = useState(0);
2329
const { bottom } = useSafeAreaInsets();
2430
const chatKitOffset = bottom - MARGIN;
@@ -53,6 +59,7 @@ const VirtualizedListScrollView = forwardRef<
5359
inverted ? invertedContentContainerStyle : contentContainerStyle
5460
}
5561
contentInsetAdjustmentBehavior="never"
62+
extraContentPadding={extraContentPadding}
5663
freeze={freeze}
5764
inverted={isInvertedSupported}
5865
keyboardDismissMode="interactive"

example/src/screens/Examples/KeyboardChatScrollView/index.tsx

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
KeyboardGestureArea,
2020
KeyboardStickyView,
2121
} from "react-native-keyboard-controller";
22+
import { useSharedValue, withTiming } from "react-native-reanimated";
2223
import {
2324
SafeAreaView,
2425
useSafeAreaInsets,
@@ -45,6 +46,7 @@ function KeyboardChatScrollViewPlayground() {
4546
const textInputRef = useRef<TextInput>(null);
4647
const textRef = useRef("");
4748
const [inputHeight, setInputHeight] = useState(TEXT_INPUT_HEIGHT);
49+
const extraContentPadding = useSharedValue(0);
4850
const { inverted, messages, reversedMessages, addMessage, mode } =
4951
useChatConfigStore();
5052
const { bottom } = useSafeAreaInsets();
@@ -54,9 +56,16 @@ function KeyboardChatScrollViewPlayground() {
5456
[bottom],
5557
);
5658

57-
const onInputLayoutChanged = useCallback((e: LayoutChangeEvent) => {
58-
setInputHeight(e.nativeEvent.layout.height);
59-
}, []);
59+
const onInputLayoutChanged = useCallback(
60+
(e: LayoutChangeEvent) => {
61+
const height = e.nativeEvent.layout.height;
62+
63+
// eslint-disable-next-line react-compiler/react-compiler
64+
extraContentPadding.value = withTiming(Math.max(height - TEXT_INPUT_HEIGHT, 0), {duration: 250});
65+
setInputHeight(height);
66+
},
67+
[extraContentPadding],
68+
);
6069
const onInput = useCallback((text: string) => {
6170
textRef.current = text;
6271
}, []);
@@ -86,8 +95,13 @@ function KeyboardChatScrollViewPlayground() {
8695
}, [messages]);
8796

8897
const memoList = useCallback(
89-
(props: ScrollViewProps) => <VirtualizedListScrollView {...props} />,
90-
[],
98+
(props: ScrollViewProps) => (
99+
<VirtualizedListScrollView
100+
{...props}
101+
extraContentPadding={extraContentPadding}
102+
/>
103+
),
104+
[extraContentPadding],
91105
);
92106

93107
return (
@@ -115,7 +129,7 @@ function KeyboardChatScrollViewPlayground() {
115129
inverted={inverted}
116130
keyExtractor={(item) => item.text}
117131
renderItem={({ item }) => <Message {...item} />}
118-
renderScrollComponent={VirtualizedListScrollView}
132+
renderScrollComponent={memoList}
119133
/>
120134
)}
121135
{mode === "flat" && (
@@ -129,7 +143,10 @@ function KeyboardChatScrollViewPlayground() {
129143
/>
130144
)}
131145
{mode === "scroll" && (
132-
<VirtualizedListScrollView ref={scrollRef}>
146+
<VirtualizedListScrollView
147+
ref={scrollRef}
148+
extraContentPadding={extraContentPadding}
149+
>
133150
{messages.map((message, index) => (
134151
<Message key={index} {...message} />
135152
))}

src/components/KeyboardChatScrollView/index.tsx

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import React, { forwardRef, useCallback, useMemo } from "react";
22
import { StyleSheet } from "react-native";
3-
import { useAnimatedRef, useAnimatedStyle } from "react-native-reanimated";
3+
import {
4+
useAnimatedRef,
5+
useAnimatedStyle,
6+
useDerivedValue,
7+
useSharedValue,
8+
} from "react-native-reanimated";
49
import Reanimated from "react-native-reanimated";
510

611
import useCombinedRef from "../hooks/useCombinedRef";
712
import ScrollViewWithBottomPadding from "../ScrollViewWithBottomPadding";
813

914
import { useChatKeyboard } from "./useChatKeyboard";
15+
import { useExtraContentPadding } from "./useExtraContentPadding";
1016

1117
import type { KeyboardChatScrollViewProps } from "./types";
1218
import type { LayoutChangeEvent } from "react-native";
@@ -23,6 +29,7 @@ const KeyboardChatScrollView = forwardRef<
2329
keyboardLiftBehavior = "always",
2430
freeze = false,
2531
offset = 0,
32+
extraContentPadding,
2633
onLayout: onLayoutProp,
2734
onContentSizeChange: onContentSizeChangeProp,
2835
...rest
@@ -31,11 +38,17 @@ const KeyboardChatScrollView = forwardRef<
3138
) => {
3239
const scrollViewRef = useAnimatedRef<Reanimated.ScrollView>();
3340
const onRef = useCombinedRef(ref, scrollViewRef);
41+
const defaultExtraContentPadding = useSharedValue(0);
42+
const effectiveExtraContentPadding =
43+
extraContentPadding ?? defaultExtraContentPadding;
3444

3545
const {
3646
padding,
3747
currentHeight,
3848
contentOffsetY,
49+
scroll,
50+
layout,
51+
size,
3952
onLayout: onLayoutInternal,
4053
onContentSizeChange: onContentSizeChangeInternal,
4154
} = useChatKeyboard(scrollViewRef, {
@@ -45,6 +58,22 @@ const KeyboardChatScrollView = forwardRef<
4558
offset,
4659
});
4760

61+
useExtraContentPadding({
62+
scrollViewRef,
63+
extraContentPadding: effectiveExtraContentPadding,
64+
keyboardPadding: padding,
65+
scroll,
66+
layout,
67+
size,
68+
inverted,
69+
keyboardLiftBehavior,
70+
freeze,
71+
});
72+
73+
const totalPadding = useDerivedValue(
74+
() => padding.value + effectiveExtraContentPadding.value,
75+
);
76+
4877
const onLayout = useCallback(
4978
(e: LayoutChangeEvent) => {
5079
onLayoutInternal(e);
@@ -81,7 +110,7 @@ const KeyboardChatScrollView = forwardRef<
81110
<ScrollViewWithBottomPadding
82111
ref={onRef}
83112
{...rest}
84-
bottomPadding={padding}
113+
bottomPadding={totalPadding}
85114
contentOffsetY={contentOffsetY}
86115
inverted={inverted}
87116
ScrollViewComponent={ScrollViewComponent}

src/components/KeyboardChatScrollView/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { AnimatedScrollViewComponent } from "../ScrollViewWithBottomPadding";
22
import type { KeyboardLiftBehavior } from "./useChatKeyboard/types";
33
import type { ScrollViewProps } from "react-native";
4+
import type { SharedValue } from "react-native-reanimated";
45

56
export type KeyboardChatScrollViewProps = {
67
/** Custom component for `ScrollView`. Default is `ScrollView`. */
@@ -46,4 +47,15 @@ export type KeyboardChatScrollViewProps = {
4647
* Default is `false`.
4748
*/
4849
freeze?: boolean;
50+
/**
51+
* A shared value representing additional padding from external elements
52+
* (e.g., a growing multiline `TextInput` in a `KeyboardStickyView`).
53+
*
54+
* When this value changes:
55+
* - The scrollable range is always extended/contracted (via `contentInset`).
56+
* - The scroll position is conditionally adjusted based on `keyboardLiftBehavior`.
57+
*
58+
* Default is `undefined` (no extra padding).
59+
*/
60+
extraContentPadding?: SharedValue<number>;
4961
} & ScrollViewProps;

src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,9 @@ function useChatKeyboard(
172172
padding,
173173
currentHeight,
174174
contentOffsetY,
175+
scroll,
176+
layout,
177+
size,
175178
onLayout,
176179
onContentSizeChange,
177180
};

src/components/KeyboardChatScrollView/useChatKeyboard/index.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,7 @@ import type Reanimated from "react-native-reanimated";
2424
* @returns Shared values for padding and contentOffsetY (always `undefined`).
2525
* @example
2626
* ```tsx
27-
* const { padding, contentOffsetY } = useChatKeyboard(ref, {
28-
* inverted: false,
29-
* keyboardLiftBehavior: "always",
30-
* });
27+
* const { padding } = useChatKeyboard(ref, { inverted: false, keyboardLiftBehavior: "always" });
3128
* ```
3229
*/
3330
function useChatKeyboard(
@@ -41,15 +38,13 @@ function useChatKeyboard(
4138
const offsetBeforeScroll = useSharedValue(0);
4239
const targetKeyboardHeight = useSharedValue(0);
4340
const closing = useSharedValue(false);
44-
4541
const {
4642
layout,
4743
size,
4844
offset: scroll,
4945
onLayout,
5046
onContentSizeChange,
5147
} = useScrollState(scrollViewRef);
52-
5348
const clampScrollIfNeeded = (effective: number) => {
5449
"worklet";
5550

@@ -292,6 +287,9 @@ function useChatKeyboard(
292287
padding,
293288
currentHeight,
294289
contentOffsetY: undefined,
290+
scroll,
291+
layout,
292+
size,
295293
onLayout,
296294
onContentSizeChange,
297295
};

src/components/KeyboardChatScrollView/useChatKeyboard/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ type UseChatKeyboardReturn = {
1717
currentHeight: SharedValue<number>;
1818
/** Absolute Y content offset for iOS (set once in onStart). `undefined` on Android. */
1919
contentOffsetY: SharedValue<number> | undefined;
20+
/** Current vertical scroll offset. */
21+
scroll: SharedValue<number>;
22+
/** Visible viewport dimensions. */
23+
layout: SharedValue<{ width: number; height: number }>;
24+
/** Total content dimensions. */
25+
size: SharedValue<{ width: number; height: number }>;
2026
/** Callback to attach to ScrollView's onLayout prop to capture initial viewport dimensions. */
2127
onLayout: (e: LayoutChangeEvent) => void;
2228
/** Callback to attach to ScrollView's onContentSizeChange prop to capture initial content dimensions. */

0 commit comments

Comments
 (0)