react-native-keyboard-controller
Version:
Keyboard manager which works in identical way on both iOS and Android
301 lines (285 loc) • 15.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _react = _interopRequireWildcard(require("react"));
var _reactNativeReanimated = _interopRequireWildcard(require("react-native-reanimated"));
var _hooks = require("../../hooks");
var _findNodeHandle = require("../../utils/findNodeHandle");
var _useSmoothKeyboardHandler = require("./useSmoothKeyboardHandler");
var _utils = require("./utils");
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
// Everything begins from `onStart` handler. This handler is called every time,
// when keyboard changes its size or when focused `TextInput` was changed. In
// this handler we are calculating/memoizing values which later will be used
// during layout movement. For that we calculate:
// - layout of focused field (`layout`) - to understand whether there will be overlap
// - initial keyboard size (`initialKeyboardSize`) - used in scroll interpolation
// - future keyboard height (`keyboardHeight`) - used in scroll interpolation
// - current scroll position (`scrollPosition`) - used to scroll from this point
//
// Once we've calculated all necessary variables - we can actually start to use them.
// It happens in `onMove` handler - this function simply calls `maybeScroll` with
// current keyboard frame height. This functions makes the smooth transition.
//
// When the transition has finished we go to `onEnd` handler. In this handler
// we verify, that the current field is not overlapped within a keyboard frame.
// For full `onStart`/`onMove`/`onEnd` flow it may look like a redundant thing,
// however there could be some cases, when `onMove` is not called:
// - on iOS when TextInput was changed - keyboard transition is instant
// - on Android when TextInput was changed and keyboard size wasn't changed
// So `onEnd` handler handle the case, when `onMove` wasn't triggered.
//
// ====================================================================================================================+
// -----------------------------------------------------Flow chart-----------------------------------------------------+
// ====================================================================================================================+
//
// +============================+ +============================+ +==================================+
// + User Press on TextInput + => + Keyboard starts showing + => + As keyboard moves frame by frame + =>
// + + + (run `onStart`) + + `onMove` is getting called +
// +============================+ +============================+ +==================================+
//
// +============================+ +============================+ +=====================================+
// + Keyboard is shown and we + => + User moved focus to + => + Only `onStart`/`onEnd` maybe called +
// + call `onEnd` handler + + another `TextInput` + + (without involving `onMove`) +
// +============================+ +============================+ +=====================================+
//
/**
* A ScrollView component that automatically handles keyboard appearance and disappearance
* by adjusting its content position to ensure the focused input remains visible.
*
* The component uses a sophisticated animation system to smoothly handle keyboard transitions
* and maintain proper scroll position during keyboard interactions.
*
* @returns A ScrollView component that handles keyboard interactions.
* @see {@link https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/components/keyboard-aware-scroll-view|Documentation} page for more details.
* @example
* ```tsx
* <KeyboardAwareScrollView bottomOffset={20}>
* <TextInput placeholder="Enter text" />
* <TextInput placeholder="Another input" />
* </KeyboardAwareScrollView>
* ```
*/
const KeyboardAwareScrollView = /*#__PURE__*/(0, _react.forwardRef)(({
children,
onLayout,
bottomOffset = 0,
disableScrollOnKeyboardHide = false,
enabled = true,
extraKeyboardSpace = 0,
ScrollViewComponent = _reactNativeReanimated.default.ScrollView,
snapToOffsets,
...rest
}, ref) => {
const scrollViewAnimatedRef = (0, _reactNativeReanimated.useAnimatedRef)();
const scrollViewTarget = (0, _reactNativeReanimated.useSharedValue)(null);
const scrollPosition = (0, _reactNativeReanimated.useSharedValue)(0);
const position = (0, _reactNativeReanimated.useScrollViewOffset)(scrollViewAnimatedRef);
const currentKeyboardFrameHeight = (0, _reactNativeReanimated.useSharedValue)(0);
const keyboardHeight = (0, _reactNativeReanimated.useSharedValue)(0);
const keyboardWillAppear = (0, _reactNativeReanimated.useSharedValue)(false);
const tag = (0, _reactNativeReanimated.useSharedValue)(-1);
const initialKeyboardSize = (0, _reactNativeReanimated.useSharedValue)(0);
const scrollBeforeKeyboardMovement = (0, _reactNativeReanimated.useSharedValue)(0);
const {
input
} = (0, _hooks.useReanimatedFocusedInput)();
const layout = (0, _reactNativeReanimated.useSharedValue)(null);
const lastSelection = (0, _reactNativeReanimated.useSharedValue)(null);
const {
height
} = (0, _hooks.useWindowDimensions)();
const onRef = (0, _react.useCallback)(assignedRef => {
if (typeof ref === "function") {
ref(assignedRef);
} else if (ref) {
ref.current = assignedRef;
}
scrollViewAnimatedRef(assignedRef);
}, []);
const onScrollViewLayout = (0, _react.useCallback)(e => {
scrollViewTarget.value = (0, _findNodeHandle.findNodeHandle)(scrollViewAnimatedRef.current);
onLayout === null || onLayout === void 0 || onLayout(e);
}, [onLayout]);
/**
* Function that will scroll a ScrollView as keyboard gets moving.
*/
const maybeScroll = (0, _react.useCallback)((e, animated = false) => {
"worklet";
var _layout$value, _layout$value2, _layout$value3;
if (!enabled) {
return 0;
}
// input belongs to ScrollView
if (((_layout$value = layout.value) === null || _layout$value === void 0 ? void 0 : _layout$value.parentScrollViewTarget) !== scrollViewTarget.value) {
return 0;
}
const visibleRect = height - keyboardHeight.value;
const absoluteY = ((_layout$value2 = layout.value) === null || _layout$value2 === void 0 ? void 0 : _layout$value2.layout.absoluteY) || 0;
const inputHeight = ((_layout$value3 = layout.value) === null || _layout$value3 === void 0 ? void 0 : _layout$value3.layout.height) || 0;
const point = absoluteY + inputHeight;
if (visibleRect - point <= bottomOffset) {
const relativeScrollTo = keyboardHeight.value - (height - point) + bottomOffset;
const interpolatedScrollTo = (0, _reactNativeReanimated.interpolate)(e, [initialKeyboardSize.value, keyboardHeight.value], [0, (0, _utils.scrollDistanceWithRespectToSnapPoints)(relativeScrollTo + scrollPosition.value, snapToOffsets) - scrollPosition.value]);
const targetScrollY = Math.max(interpolatedScrollTo, 0) + scrollPosition.value;
(0, _reactNativeReanimated.scrollTo)(scrollViewAnimatedRef, 0, targetScrollY, animated);
return interpolatedScrollTo;
}
if (point < 0) {
const positionOnScreen = visibleRect - bottomOffset;
const topOfScreen = scrollPosition.value + point;
(0, _reactNativeReanimated.scrollTo)(scrollViewAnimatedRef, 0, topOfScreen - positionOnScreen, animated);
}
return 0;
}, [bottomOffset, enabled, height, snapToOffsets]);
const syncKeyboardFrame = (0, _react.useCallback)(e => {
"worklet";
const keyboardFrame = (0, _reactNativeReanimated.interpolate)(e.height, [0, keyboardHeight.value], [0, keyboardHeight.value + extraKeyboardSpace]);
currentKeyboardFrameHeight.value = keyboardFrame;
}, [extraKeyboardSpace]);
const updateLayoutFromSelection = (0, _react.useCallback)(() => {
"worklet";
var _lastSelection$value, _input$value;
const customHeight = (_lastSelection$value = lastSelection.value) === null || _lastSelection$value === void 0 ? void 0 : _lastSelection$value.selection.end.y;
if (!((_input$value = input.value) !== null && _input$value !== void 0 && _input$value.layout) || !customHeight) {
return false;
}
layout.value = {
...input.value,
layout: {
...input.value.layout,
// when we have multiline input with limited amount of lines, then custom height can be very big
// so we clamp it to max input height
height: (0, _reactNativeReanimated.clamp)(customHeight, 0, input.value.layout.height)
}
};
return true;
}, [input, lastSelection, layout]);
const scrollFromCurrentPosition = (0, _react.useCallback)(() => {
"worklet";
const prevScrollPosition = scrollPosition.value;
const prevLayout = layout.value;
if (!updateLayoutFromSelection()) {
return;
}
// eslint-disable-next-line react-compiler/react-compiler
scrollPosition.value = position.value;
maybeScroll(keyboardHeight.value, true);
scrollPosition.value = prevScrollPosition;
layout.value = prevLayout;
}, [maybeScroll]);
const onChangeText = (0, _react.useCallback)(() => {
"worklet";
scrollFromCurrentPosition();
}, [scrollFromCurrentPosition]);
const onChangeTextHandler = (0, _react.useMemo)(() => (0, _utils.debounce)(onChangeText, 200), [onChangeText]);
const onSelectionChange = (0, _react.useCallback)(e => {
"worklet";
var _lastSelection$value2, _lastSelection$value3;
const lastTarget = (_lastSelection$value2 = lastSelection.value) === null || _lastSelection$value2 === void 0 ? void 0 : _lastSelection$value2.target;
const latestSelection = (_lastSelection$value3 = lastSelection.value) === null || _lastSelection$value3 === void 0 ? void 0 : _lastSelection$value3.selection;
lastSelection.value = e;
if (e.target !== lastTarget) {
// ignore this event, because "focus changed" event handled in `useSmoothKeyboardHandler`
return;
}
// caret in the end + end coordinates has been changed -> we moved to a new line
// so input may grow
if (e.selection.end.position === e.selection.start.position && (latestSelection === null || latestSelection === void 0 ? void 0 : latestSelection.end.y) !== e.selection.end.y) {
return scrollFromCurrentPosition();
}
// selection has been changed
if (e.selection.start.position !== e.selection.end.position) {
return scrollFromCurrentPosition();
}
onChangeTextHandler();
}, [scrollFromCurrentPosition, onChangeTextHandler]);
(0, _hooks.useFocusedInputHandler)({
onSelectionChange: onSelectionChange
}, [onSelectionChange]);
(0, _useSmoothKeyboardHandler.useSmoothKeyboardHandler)({
onStart: e => {
"worklet";
const keyboardWillChangeSize = keyboardHeight.value !== e.height && e.height > 0;
keyboardWillAppear.value = e.height > 0 && keyboardHeight.value === 0;
const keyboardWillHide = e.height === 0;
const focusWasChanged = tag.value !== e.target && e.target !== -1 || keyboardWillChangeSize;
if (keyboardWillChangeSize) {
initialKeyboardSize.value = keyboardHeight.value;
}
if (keyboardWillHide) {
// on back transition need to interpolate as [0, keyboardHeight]
initialKeyboardSize.value = 0;
scrollPosition.value = scrollBeforeKeyboardMovement.value;
}
if (keyboardWillAppear.value || keyboardWillChangeSize || focusWasChanged) {
// persist scroll value
scrollPosition.value = position.value;
// just persist height - later will be used in interpolation
keyboardHeight.value = e.height;
}
// focus was changed
if (focusWasChanged) {
tag.value = e.target;
// save position of focused text input when keyboard starts to move
updateLayoutFromSelection();
// save current scroll position - when keyboard will hide we'll reuse
// this value to achieve smooth hide effect
scrollBeforeKeyboardMovement.value = position.value;
}
if (focusWasChanged && !keyboardWillAppear.value) {
// update position on scroll value, so `onEnd` handler
// will pick up correct values
position.value += maybeScroll(e.height, true);
}
},
onMove: e => {
"worklet";
syncKeyboardFrame(e);
// if the user has set disableScrollOnKeyboardHide, only auto-scroll when the keyboard opens
if (!disableScrollOnKeyboardHide || keyboardWillAppear.value) {
maybeScroll(e.height);
}
},
onEnd: e => {
"worklet";
keyboardHeight.value = e.height;
scrollPosition.value = position.value;
syncKeyboardFrame(e);
}
}, [maybeScroll, disableScrollOnKeyboardHide, syncKeyboardFrame]);
(0, _react.useEffect)(() => {
(0, _reactNativeReanimated.runOnUI)(maybeScroll)(keyboardHeight.value, true);
}, [bottomOffset]);
(0, _reactNativeReanimated.useAnimatedReaction)(() => input.value, (current, previous) => {
if ((current === null || current === void 0 ? void 0 : current.target) === (previous === null || previous === void 0 ? void 0 : previous.target) && (current === null || current === void 0 ? void 0 : current.layout.height) !== (previous === null || previous === void 0 ? void 0 : previous.layout.height)) {
// input has changed layout - let's check if we need to scroll
// may happen when you paste text, then onSelectionChange will be
// fired earlier than text actually changes its layout
scrollFromCurrentPosition();
}
}, []);
const view = (0, _reactNativeReanimated.useAnimatedStyle)(() => enabled ? {
// animations become choppy when scrolling to the end of the `ScrollView` (when the last input is focused)
// this happens because the layout recalculates on every frame. To avoid this we slightly increase padding
// by `+1`. In this way we assure, that `scrollTo` will never scroll to the end, because it uses interpolation
// from 0 to `keyboardHeight`, and here our padding is `keyboardHeight + 1`. It allows us not to re-run layout
// re-calculation on every animation frame and it helps to achieve smooth animation.
// see: https://github.com/kirillzyusko/react-native-keyboard-controller/pull/342
paddingBottom: currentKeyboardFrameHeight.value + 1
} : {}, [enabled]);
return /*#__PURE__*/_react.default.createElement(ScrollViewComponent, _extends({
ref: onRef
}, rest, {
scrollEventThrottle: 16,
onLayout: onScrollViewLayout
}), children, enabled && /*#__PURE__*/_react.default.createElement(_reactNativeReanimated.default.View, {
style: view
}));
});
var _default = exports.default = KeyboardAwareScrollView;
//# sourceMappingURL=index.js.map