UNPKG

react-native-keyboard-controller

Version:

Keyboard manager which works in identical way on both iOS and Android

143 lines (135 loc) 5.83 kB
import { useSharedValue } from "react-native-reanimated"; import { useKeyboardHandler } from "../../../hooks"; import useScrollState from "../../hooks/useScrollState"; import { computeIOSContentOffset, getEffectiveHeight, getScrollEffective, getVisibleMinimumPaddingFraction, isScrollAtEnd, shouldShiftContent } from "./helpers"; /** * Hook that manages keyboard-driven scrolling for chat-style scroll views. * Calculates padding (extra scrollable space) and content shift values, * using iOS-specific strategy (contentOffset set once in onStart). * * @param scrollViewRef - Animated ref to the scroll view. * @param options - Configuration for inverted and keyboardLiftBehavior. * @returns Shared values for padding and contentOffsetY. * @example * ```tsx * const { padding, contentOffsetY } = useChatKeyboard(ref, { * inverted: false, * keyboardLiftBehavior: "always", * }); * ``` */ function useChatKeyboard(scrollViewRef, options) { const { inverted, keyboardLiftBehavior, freeze, offset, blankSpace, extraContentPadding } = options; const padding = useSharedValue(0); const currentHeight = useSharedValue(0); const contentOffsetY = useSharedValue(0); const targetKeyboardHeight = useSharedValue(0); const prevAbsorption = useSharedValue(0); const { layout, size, offset: scroll, onLayout, onContentSizeChange } = useScrollState(scrollViewRef); useKeyboardHandler({ onStart: e => { "worklet"; if (freeze.value) { return; } if (e.height > 0) { // eslint-disable-next-line react-compiler/react-compiler targetKeyboardHeight.value = e.height; } const effective = getEffectiveHeight(e.height, targetKeyboardHeight.value, offset); const atEnd = isScrollAtEnd(scroll.value, layout.value.height, size.value.height, inverted); // Scale minimum padding absorption by how much of it is visible. // Fully visible → full absorption; fully off-screen → no absorption. const visibleFraction = getVisibleMinimumPaddingFraction(scroll.value, layout.value.height, size.value.height, blankSpace.value, inverted); const visiblePadding = visibleFraction * blankSpace.value; const minimumPaddingAbsorbed = Math.max(0, visiblePadding - extraContentPadding.value); const scrollEffective = getScrollEffective(effective, minimumPaddingAbsorbed); const actualTotalPadding = Math.max(blankSpace.value, effective + extraContentPadding.value); // persistent mode: when keyboard shrinks, clamp to valid range if (keyboardLiftBehavior === "persistent" && effective < padding.value) { padding.value = effective; prevAbsorption.value = minimumPaddingAbsorbed; if (inverted) { const maxScroll = Math.max(size.value.height - layout.value.height, 0); contentOffsetY.value = Math.max(-actualTotalPadding, Math.min(scroll.value, maxScroll)); } else { const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, 0); contentOffsetY.value = Math.max(0, Math.min(scroll.value, maxScroll)); } return; } // never mode: when keyboard shrinks, clamp to valid range // to avoid ghost padding if (keyboardLiftBehavior === "never" && effective < padding.value && atEnd) { padding.value = effective; prevAbsorption.value = minimumPaddingAbsorbed; if (inverted) { const maxScroll = Math.max(size.value.height - layout.value.height, 0); contentOffsetY.value = Math.max(-actualTotalPadding, Math.min(scroll.value, maxScroll)); } else { const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, 0); contentOffsetY.value = Math.max(0, Math.min(scroll.value, maxScroll)); } return; } // Undo only the scroll displacement that was actually applied // (not the full padding, which includes the absorbed portion). // Use the stored absorption from the previous event so that // the unwind matches the shift that was originally applied. const prevScrollEffective = getScrollEffective(padding.value, prevAbsorption.value); const relativeScroll = inverted ? scroll.value + prevScrollEffective : scroll.value - prevScrollEffective; padding.value = effective; prevAbsorption.value = minimumPaddingAbsorbed; if (!shouldShiftContent(keyboardLiftBehavior, atEnd)) { // Preserve current scroll position so animated props // don't re-apply a stale contentOffset when padding changes contentOffsetY.value = scroll.value; return; } // When blankSpace fully absorbs the keyboard opening, preserve current scroll position // (only when keyboard is open — effective > 0 — not when closing) if (scrollEffective === 0 && minimumPaddingAbsorbed > 0 && effective > 0) { contentOffsetY.value = scroll.value; return; } contentOffsetY.value = computeIOSContentOffset(relativeScroll, scrollEffective, size.value.height, layout.value.height, inverted, actualTotalPadding); }, onMove: () => { "worklet"; // iOS doesn't need per-frame updates (contentOffset handles it) }, onEnd: e => { "worklet"; if (freeze.value) { return; } const effective = getEffectiveHeight(e.height, targetKeyboardHeight.value, offset); padding.value = effective; } }, [inverted, keyboardLiftBehavior, offset, extraContentPadding]); return { padding, currentHeight, contentOffsetY, scroll, layout, size, onLayout, onContentSizeChange }; } export { useChatKeyboard }; //# sourceMappingURL=index.ios.js.map