UNPKG

react-native-keyboard-controller

Version:

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

240 lines (225 loc) 10.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useChatKeyboard = useChatKeyboard; var _reactNativeReanimated = require("react-native-reanimated"); var _hooks = require("../../../hooks"); var _useScrollState = _interopRequireDefault(require("../../hooks/useScrollState")); var _helpers = require("./helpers"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } /** * Hook that manages keyboard-driven scrolling for chat-style scroll views. * Calculates padding (extra scrollable space) and content shift values, * using per-frame scrollTo updates (Android and other platforms). * * @param scrollViewRef - Animated ref to the scroll view. * @param options - Configuration for inverted and keyboardLiftBehavior. * @returns Shared values for padding and contentOffsetY (always `undefined`). * @example * ```tsx * const { padding } = useChatKeyboard(ref, { inverted: false, keyboardLiftBehavior: "always" }); * ``` */ function useChatKeyboard(scrollViewRef, options) { const { inverted, keyboardLiftBehavior, freeze, offset, blankSpace, extraContentPadding } = options; const padding = (0, _reactNativeReanimated.useSharedValue)(0); const currentHeight = (0, _reactNativeReanimated.useSharedValue)(0); const offsetBeforeScroll = (0, _reactNativeReanimated.useSharedValue)(0); const targetKeyboardHeight = (0, _reactNativeReanimated.useSharedValue)(0); const closing = (0, _reactNativeReanimated.useSharedValue)(false); const minimumPaddingFractionOnOpen = (0, _reactNativeReanimated.useSharedValue)(0); const actualOpenShift = (0, _reactNativeReanimated.useSharedValue)(0); const { layout, size, offset: scroll, onLayout, onContentSizeChange } = (0, _useScrollState.default)(scrollViewRef); const clampScrollIfNeeded = (effective, totalPaddingForMaxScroll) => { "worklet"; const paddingForMax = totalPaddingForMaxScroll !== undefined ? totalPaddingForMaxScroll : effective; const maxScroll = Math.max(size.value.height - layout.value.height + paddingForMax, 0); if (scroll.value > maxScroll) { (0, _reactNativeReanimated.scrollTo)(scrollViewRef, 0, maxScroll, false); } }; (0, _hooks.useKeyboardHandler)({ onStart: e => { "worklet"; if (freeze.value) { return; } if (e.height > 0) { // eslint-disable-next-line react-compiler/react-compiler targetKeyboardHeight.value = e.height; closing.value = false; } else { closing.value = true; } const effective = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset); const atEnd = (0, _helpers.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 = (0, _helpers.getVisibleMinimumPaddingFraction)(scroll.value, layout.value.height, size.value.height, blankSpace.value, inverted); const minimumPaddingAbsorbed = visibleFraction >= 1 ? (0, _helpers.getMinimumPaddingAbsorbed)(blankSpace.value, extraContentPadding.value) : 0; const scrollEffective = (0, _helpers.getScrollEffective)(effective, minimumPaddingAbsorbed); if (inverted && e.duration === -1) { // Android inverted: skip post-interactive snap-back events // (duration === -1 means the keyboard is re-establishing its // position after an interactive gesture, not a real animation) return; } else if (e.height > 0) { // Android: keyboard opening — set padding + capture scroll position minimumPaddingFractionOnOpen.value = visibleFraction >= 1 ? 1 : 0; padding.value = effective; offsetBeforeScroll.value = scroll.value; if (!inverted && keyboardLiftBehavior === "whenAtEnd" && !atEnd) { // Sentinel: don't scroll in onMove (non-inverted only) offsetBeforeScroll.value = -1; } else if (!inverted && scrollEffective === 0) { // blankSpace fully absorbs the keyboard — prevent scroll offsetBeforeScroll.value = -1; } else if (inverted && scrollEffective === 0) { // blankSpace fully absorbs the keyboard — guard for inverted offsetBeforeScroll.value = scroll.value; } } else { // Android: keyboard closing — re-capture scroll position if (inverted) { offsetBeforeScroll.value = scroll.value; } else { // Preserve "whenAtEnd" sentinel: if open didn't shift, close shouldn't either if (offsetBeforeScroll.value !== -1) { // Use the actual displacement recorded at end of open animation // (not the theoretical value) so close is symmetric with open offsetBeforeScroll.value = scroll.value - actualOpenShift.value; } } } }, onMove: e => { "worklet"; if (freeze.value) { return; } currentHeight.value = e.height; if (inverted) { // Skip post-interactive snap-back (duration === -1) if (e.duration === -1) { return; } const effective = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset); const minimumPaddingAbsorbed = (0, _helpers.getMinimumPaddingAbsorbed)(blankSpace.value, extraContentPadding.value) * minimumPaddingFractionOnOpen.value; const scrollEffective = (0, _helpers.getScrollEffective)(effective, minimumPaddingAbsorbed); const actualTotalPadding = Math.max(blankSpace.value, effective + extraContentPadding.value); // Check if we should shift content based on position when keyboard started const wasAtEnd = (0, _helpers.isScrollAtEnd)(offsetBeforeScroll.value, layout.value.height, size.value.height, inverted); // "never" at end: scroll along when keyboard closes to avoid jump if (keyboardLiftBehavior === "never" && wasAtEnd && effective < padding.value) { padding.value = effective; if (scrollEffective === 0 && minimumPaddingAbsorbed > 0) { return; } (0, _reactNativeReanimated.scrollTo)(scrollViewRef, 0, 0, false); return; } if (!(0, _helpers.shouldShiftContent)(keyboardLiftBehavior, wasAtEnd)) { // Closing, not shifting: reduce padding to avoid gap if (closing.value && effective < padding.value) { padding.value = effective; clampScrollIfNeeded(effective, actualTotalPadding); } return; } // When blankSpace fully absorbs the keyboard, skip scroll if (scrollEffective === 0 && minimumPaddingAbsorbed > 0) { return; } // Persistent: don't let shift decrease if (keyboardLiftBehavior === "persistent") { const currentShift = offsetBeforeScroll.value + padding.value - scroll.value; if (effective < currentShift) { // When at end, allow scrolling back (snap to end + reduce padding) if (wasAtEnd) { padding.value = effective; (0, _reactNativeReanimated.scrollTo)(scrollViewRef, 0, 0, false); } else if (closing.value) { // Not at end: reduce padding to avoid gap padding.value = effective; clampScrollIfNeeded(effective, actualTotalPadding); } return; } } const target = offsetBeforeScroll.value + padding.value - scrollEffective; (0, _reactNativeReanimated.scrollTo)(scrollViewRef, 0, target, false); } else { const effective = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset); const minimumPaddingAbsorbed = (0, _helpers.getMinimumPaddingAbsorbed)(blankSpace.value, extraContentPadding.value) * minimumPaddingFractionOnOpen.value; const scrollEffective = (0, _helpers.getScrollEffective)(effective, minimumPaddingAbsorbed); const actualTotalPadding = Math.max(blankSpace.value, effective + extraContentPadding.value); // "never" closing: clamp scroll to valid range as inset shrinks if (keyboardLiftBehavior === "never" && closing.value && effective < padding.value) { clampScrollIfNeeded(effective, actualTotalPadding); return; } if (!(0, _helpers.shouldShiftContent)(keyboardLiftBehavior, true)) { return; } // "whenAtEnd" sentinel check (also used for blankSpace full absorption) if (offsetBeforeScroll.value === -1) { if (closing.value) { // Keyboard didn't shift on open; ensure valid position on close clampScrollIfNeeded(effective, actualTotalPadding); } return; } // "persistent" closing: maintain position, clamped to valid range if (keyboardLiftBehavior === "persistent" && closing.value) { const keepAt = offsetBeforeScroll.value + padding.value; const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, 0); (0, _reactNativeReanimated.scrollTo)(scrollViewRef, 0, Math.min(keepAt, maxScroll), false); return; } const target = (0, _helpers.clampedScrollTarget)(offsetBeforeScroll.value, scrollEffective, size.value.height, layout.value.height, actualTotalPadding); (0, _reactNativeReanimated.scrollTo)(scrollViewRef, 0, target, false); // Track actual (clamped) displacement during open for symmetric close if (!closing.value) { actualOpenShift.value = target - offsetBeforeScroll.value; } } }, onEnd: e => { "worklet"; if (freeze.value) { return; } const effective = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset); padding.value = effective; // Record actual scroll displacement so close can be symmetric if (effective > 0 && offsetBeforeScroll.value !== -1) { actualOpenShift.value = scroll.value - offsetBeforeScroll.value; } } }, [inverted, keyboardLiftBehavior, offset]); return { padding, currentHeight, contentOffsetY: undefined, scroll, layout, size, onLayout, onContentSizeChange }; } //# sourceMappingURL=index.js.map