diff --git a/docs/docs/api/components/keyboard-chat-scroll-view.mdx b/docs/docs/api/components/keyboard-chat-scroll-view.mdx index f8a07dcedb..d00c74faec 100644 --- a/docs/docs/api/components/keyboard-chat-scroll-view.mdx +++ b/docs/docs/api/components/keyboard-chat-scroll-view.mdx @@ -34,6 +34,8 @@ import { ScrollView } from "react-native-gesture-handler"; When `true`, freezes all keyboard-driven layout changes. This is useful when dismissing the keyboard to show a custom input view (such as an emoji picker or bottom sheet) — it prevents the chat content from shifting while the transition happens. +Accepts either a plain `boolean` or a [Reanimated `SharedValue`](https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue/). Using a `SharedValue` allows you to toggle freezing from the UI thread (e.g., inside a worklet or gesture handler) without triggering a React re-render. + ### `inverted` Set to `true` if your list uses the `inverted` prop (the standard pattern for chat-style lists where the newest messages appear at the bottom). diff --git a/docs/docs/guides/building-chat-app.mdx b/docs/docs/guides/building-chat-app.mdx index 104e71c276..03f7eb56cb 100644 --- a/docs/docs/guides/building-chat-app.mdx +++ b/docs/docs/guides/building-chat-app.mdx @@ -197,6 +197,28 @@ const onKeyboardPress = () => { When `freeze` is `true`, all keyboard-driven layout changes (padding, content offset, scroll position) are paused. +:::tip SharedValue support +`freeze` also accepts a [Reanimated `SharedValue`](https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue/). This is useful when you need to toggle freezing synchronously from a worklet — for example, inside a gesture handler: + +```tsx +import { useSharedValue } from "react-native-reanimated"; +import { Gesture, GestureDetector } from "react-native-gesture-handler"; + +const freeze = useSharedValue(false); + +const gesture = Gesture.Pan().onStart(() => { + "worklet"; + // freeze synchronously on the UI thread before the keyboard starts dismissing + freeze.value = true; +}); + + + {/* ...messages... */} +; +``` + +::: + ### Handling a growing multiline input If your composer has a `multiline` `TextInput` that grows as the user types, you need to tell `KeyboardChatScrollView` about the extra space it takes up — otherwise the component doesn't know the scrollable range has changed and the bottom messages can get clipped under the input. diff --git a/docs/versioned_docs/version-1.21.0/api/components/keyboard-chat-scroll-view.mdx b/docs/versioned_docs/version-1.21.0/api/components/keyboard-chat-scroll-view.mdx index f8a07dcedb..d00c74faec 100644 --- a/docs/versioned_docs/version-1.21.0/api/components/keyboard-chat-scroll-view.mdx +++ b/docs/versioned_docs/version-1.21.0/api/components/keyboard-chat-scroll-view.mdx @@ -34,6 +34,8 @@ import { ScrollView } from "react-native-gesture-handler"; When `true`, freezes all keyboard-driven layout changes. This is useful when dismissing the keyboard to show a custom input view (such as an emoji picker or bottom sheet) — it prevents the chat content from shifting while the transition happens. +Accepts either a plain `boolean` or a [Reanimated `SharedValue`](https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue/). Using a `SharedValue` allows you to toggle freezing from the UI thread (e.g., inside a worklet or gesture handler) without triggering a React re-render. + ### `inverted` Set to `true` if your list uses the `inverted` prop (the standard pattern for chat-style lists where the newest messages appear at the bottom). diff --git a/docs/versioned_docs/version-1.21.0/guides/building-chat-app.mdx b/docs/versioned_docs/version-1.21.0/guides/building-chat-app.mdx index 104e71c276..03f7eb56cb 100644 --- a/docs/versioned_docs/version-1.21.0/guides/building-chat-app.mdx +++ b/docs/versioned_docs/version-1.21.0/guides/building-chat-app.mdx @@ -197,6 +197,28 @@ const onKeyboardPress = () => { When `freeze` is `true`, all keyboard-driven layout changes (padding, content offset, scroll position) are paused. +:::tip SharedValue support +`freeze` also accepts a [Reanimated `SharedValue`](https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue/). This is useful when you need to toggle freezing synchronously from a worklet — for example, inside a gesture handler: + +```tsx +import { useSharedValue } from "react-native-reanimated"; +import { Gesture, GestureDetector } from "react-native-gesture-handler"; + +const freeze = useSharedValue(false); + +const gesture = Gesture.Pan().onStart(() => { + "worklet"; + // freeze synchronously on the UI thread before the keyboard starts dismissing + freeze.value = true; +}); + + + {/* ...messages... */} +; +``` + +::: + ### Handling a growing multiline input If your composer has a `multiline` `TextInput` that grows as the user types, you need to tell `KeyboardChatScrollView` about the extra space it takes up — otherwise the component doesn't know the scrollable range has changed and the bottom messages can get clipped under the input. diff --git a/src/components/KeyboardChatScrollView/index.tsx b/src/components/KeyboardChatScrollView/index.tsx index be5179ac81..27353b57dc 100644 --- a/src/components/KeyboardChatScrollView/index.tsx +++ b/src/components/KeyboardChatScrollView/index.tsx @@ -43,6 +43,9 @@ const KeyboardChatScrollView = forwardRef< ) => { const scrollViewRef = useAnimatedRef(); const onRef = useCombinedRef(ref, scrollViewRef); + const freezeSV = useDerivedValue(() => + typeof freeze === "boolean" ? freeze : freeze.value, + ); const { padding, currentHeight, @@ -55,7 +58,7 @@ const KeyboardChatScrollView = forwardRef< } = useChatKeyboard(scrollViewRef, { inverted, keyboardLiftBehavior, - freeze, + freeze: freezeSV, offset, blankSpace, extraContentPadding, @@ -72,7 +75,7 @@ const KeyboardChatScrollView = forwardRef< contentOffsetY, inverted, keyboardLiftBehavior, - freeze, + freeze: freezeSV, }); const totalPadding = useDerivedValue(() => diff --git a/src/components/KeyboardChatScrollView/types.ts b/src/components/KeyboardChatScrollView/types.ts index 7766cdc77a..a853aece4b 100644 --- a/src/components/KeyboardChatScrollView/types.ts +++ b/src/components/KeyboardChatScrollView/types.ts @@ -46,7 +46,7 @@ export type KeyboardChatScrollViewProps = { * * Default is `false`. */ - freeze?: boolean; + freeze?: boolean | SharedValue; /** * A shared value representing additional padding from external elements * (e.g., a growing multiline `TextInput` in a `KeyboardStickyView`). diff --git a/src/components/KeyboardChatScrollView/useChatKeyboard/__fixtures__/testUtils.ts b/src/components/KeyboardChatScrollView/useChatKeyboard/__fixtures__/testUtils.ts index f7fa93d6b5..141e0929e3 100644 --- a/src/components/KeyboardChatScrollView/useChatKeyboard/__fixtures__/testUtils.ts +++ b/src/components/KeyboardChatScrollView/useChatKeyboard/__fixtures__/testUtils.ts @@ -76,7 +76,7 @@ type RenderOptions = Omit< Parameters[1], "freeze" | "offset" | "blankSpace" | "extraContentPadding" > & { - freeze?: boolean; + freeze?: boolean | SharedValue; offset?: number; blankSpace?: SharedValue; extraContentPadding?: SharedValue; @@ -99,9 +99,11 @@ export function createRender(modulePath: string) { return renderHook(() => { const ref = useAnimatedRef(); + const freeze = options.freeze ?? false; + return mod.useChatKeyboard(ref, { ...options, - freeze: options.freeze ?? false, + freeze: typeof freeze === "boolean" ? sv(freeze) : freeze, offset: options.offset ?? 0, blankSpace: options.blankSpace ?? sv(0), extraContentPadding: options.extraContentPadding ?? sv(0), diff --git a/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts b/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts index e533615877..560df54bae 100644 --- a/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts +++ b/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts @@ -64,7 +64,7 @@ function useChatKeyboard( onStart: (e) => { "worklet"; - if (freeze) { + if (freeze.value) { return; } @@ -229,7 +229,7 @@ function useChatKeyboard( onEnd: (e) => { "worklet"; - if (freeze) { + if (freeze.value) { return; } @@ -242,7 +242,7 @@ function useChatKeyboard( padding.value = effective; }, }, - [inverted, keyboardLiftBehavior, freeze, offset, extraContentPadding], + [inverted, keyboardLiftBehavior, offset, extraContentPadding], ); return { diff --git a/src/components/KeyboardChatScrollView/useChatKeyboard/index.ts b/src/components/KeyboardChatScrollView/useChatKeyboard/index.ts index e118852af7..48178d9246 100644 --- a/src/components/KeyboardChatScrollView/useChatKeyboard/index.ts +++ b/src/components/KeyboardChatScrollView/useChatKeyboard/index.ts @@ -82,7 +82,7 @@ function useChatKeyboard( onStart: (e) => { "worklet"; - if (freeze) { + if (freeze.value) { return; } @@ -166,7 +166,7 @@ function useChatKeyboard( onMove: (e) => { "worklet"; - if (freeze) { + if (freeze.value) { return; } @@ -340,7 +340,7 @@ function useChatKeyboard( onEnd: (e) => { "worklet"; - if (freeze) { + if (freeze.value) { return; } @@ -358,7 +358,7 @@ function useChatKeyboard( } }, }, - [inverted, keyboardLiftBehavior, freeze, offset], + [inverted, keyboardLiftBehavior, offset], ); return { diff --git a/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts b/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts index ebf6f6d4f7..02abf5cd94 100644 --- a/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts +++ b/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts @@ -6,7 +6,7 @@ type KeyboardLiftBehavior = "always" | "whenAtEnd" | "persistent" | "never"; type UseChatKeyboardOptions = { inverted: boolean; keyboardLiftBehavior: KeyboardLiftBehavior; - freeze: boolean; + freeze: SharedValue; offset: number; blankSpace: SharedValue; /** Extra content padding shared value — needed on iOS to correctly clamp contentOffset. */ diff --git a/src/components/KeyboardChatScrollView/useExtraContentPadding/__fixtures__/setup.ts b/src/components/KeyboardChatScrollView/useExtraContentPadding/__fixtures__/setup.ts index e92e4030f1..772ab443bf 100644 --- a/src/components/KeyboardChatScrollView/useExtraContentPadding/__fixtures__/setup.ts +++ b/src/components/KeyboardChatScrollView/useExtraContentPadding/__fixtures__/setup.ts @@ -34,20 +34,23 @@ jest.mock("react-native-reanimated", () => ({ type RenderOptions = Omit< Parameters[0], - "scrollViewRef" | "blankSpace" + "scrollViewRef" | "blankSpace" | "freeze" > & { blankSpace?: SharedValue; + freeze: boolean | SharedValue; }; export const createRender = () => { return function render(options: RenderOptions) { return renderHook(() => { const ref = useAnimatedRef(); + const { freeze, ...rest } = options; useExtraContentPadding({ scrollViewRef: ref, blankSpace: options.blankSpace ?? sv(0), - ...options, + freeze: typeof freeze === "boolean" ? sv(freeze) : freeze, + ...rest, }); }); }; diff --git a/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts b/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts index 24a25ae12f..0f6d7c67a3 100644 --- a/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts +++ b/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts @@ -26,7 +26,7 @@ type UseExtraContentPaddingOptions = { contentOffsetY?: SharedValue; inverted: boolean; keyboardLiftBehavior: KeyboardLiftBehavior; - freeze: boolean; + freeze: SharedValue; }; /** @@ -81,7 +81,7 @@ function useExtraContentPadding(options: UseExtraContentPaddingOptions): void { useAnimatedReaction( () => extraContentPadding.value, (current, previous) => { - if (freeze || previous === null) { + if (freeze.value || previous === null) { return; } @@ -141,7 +141,7 @@ function useExtraContentPadding(options: UseExtraContentPaddingOptions): void { scrollToTarget(target); } }, - [inverted, keyboardLiftBehavior, freeze], + [inverted, keyboardLiftBehavior], ); }