react-native-keyboard-controller
Version:
Keyboard manager which works in identical way on both iOS and Android
264 lines (249 loc) • 13.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _react = _interopRequireWildcard(require("react"));
var _reactNative = require("react-native");
var _reactNativeReanimated = _interopRequireWildcard(require("react-native-reanimated"));
var _hooks = require("../../hooks");
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`) +
* +============================+ +============================+ +=====================================+
*
*/
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 {
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, _reactNative.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 (absoluteY < 0) {
const positionOnScreen = visibleRect - inputHeight - bottomOffset;
const topOfScreen = scrollPosition.value + absoluteY;
(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 scrollFromCurrentPosition = (0, _react.useCallback)(customHeight => {
"worklet";
var _input$value;
const prevScrollPosition = scrollPosition.value;
const prevLayout = layout.value;
if (!((_input$value = input.value) !== null && _input$value !== void 0 && _input$value.layout)) {
return;
}
// eslint-disable-next-line react-compiler/react-compiler
layout.value = {
...input.value,
layout: {
...input.value.layout,
height: customHeight ?? input.value.layout.height
}
};
scrollPosition.value = position.value;
maybeScroll(keyboardHeight.value, true);
scrollPosition.value = prevScrollPosition;
layout.value = prevLayout;
}, [maybeScroll]);
const onChangeText = (0, _react.useCallback)(() => {
"worklet";
// if typing a text caused layout shift, then we need to ignore this handler
// because this event will be handled in `useAnimatedReaction` below
var _layout$value4, _input$value2;
if (((_layout$value4 = layout.value) === null || _layout$value4 === void 0 ? void 0 : _layout$value4.layout.height) !== ((_input$value2 = input.value) === null || _input$value2 === void 0 ? void 0 : _input$value2.layout.height)) {
return;
}
scrollFromCurrentPosition();
}, [scrollFromCurrentPosition]);
const onSelectionChange = (0, _react.useCallback)(e => {
"worklet";
if (e.selection.start.position !== e.selection.end.position) {
scrollFromCurrentPosition(e.selection.end.y);
}
}, [scrollFromCurrentPosition]);
const onChangeTextHandler = (0, _react.useMemo)(() => (0, _utils.debounce)(onChangeText, 200), [onChangeText]);
(0, _hooks.useFocusedInputHandler)({
onChangeText: onChangeTextHandler,
onSelectionChange: onSelectionChange
}, [onChangeTextHandler, 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
layout.value = input.value;
// 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, _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)) {
const prevLayout = layout.value;
layout.value = input.value;
scrollPosition.value += maybeScroll(keyboardHeight.value, true);
layout.value = prevLayout;
}
}, []);
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, /*#__PURE__*/_react.default.createElement(_reactNativeReanimated.default.View, {
style: view
}));
});
var _default = exports.default = KeyboardAwareScrollView;
//# sourceMappingURL=index.js.map