react-native-keyboard-toolkit 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import type { KeyboardAwareViewProps } from '../types';
3
3
  import type { KeyboardAwareViewRef } from '../types';
4
- /** A lightweight view that adds bottom space while the keyboard is visible. */
4
+ /** Adds keyboard spacing to in-flow content and keeps absolute footers visible. */
5
5
  export declare const KeyboardAwareView: React.ForwardRefExoticComponent<KeyboardAwareViewProps & React.RefAttributes<KeyboardAwareViewRef>>;
6
6
  export default KeyboardAwareView;
@@ -7,14 +7,20 @@ const react_native_1 = require("react-native");
7
7
  const constants_1 = require("../utils/constants");
8
8
  const useKeyboardState_1 = require("../hooks/useKeyboardState");
9
9
  const scrollManager_1 = require("../utils/scrollManager");
10
- /** A lightweight view that adds bottom space while the keyboard is visible. */
10
+ /** Adds keyboard spacing to in-flow content and keeps absolute footers visible. */
11
11
  exports.KeyboardAwareView = (0, react_1.forwardRef)(function KeyboardAwareViewComponent({ behavior = 'smart', keyboardSpacing = constants_1.DEFAULT_KEYBOARD_SPACING, enabled = true, style, onLayout, ...props }, forwardedRef) {
12
12
  const keyboard = (0, useKeyboardState_1.useKeyboardState)();
13
13
  const viewRef = (0, react_1.useRef)(null);
14
14
  (0, react_1.useImperativeHandle)(forwardedRef, () => viewRef.current, []);
15
15
  const measurementSequence = (0, react_1.useRef)(0);
16
16
  const [keyboardOverlap, setKeyboardOverlap] = (0, react_1.useState)(0);
17
+ const [keyboardDisplacement, setKeyboardDisplacement] = (0, react_1.useState)(0);
18
+ const animatedDisplacement = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
17
19
  const flattenedStyle = react_native_1.StyleSheet.flatten(style);
20
+ const isAbsolutelyPositioned = flattenedStyle?.position === 'absolute';
21
+ const spacing = Number.isFinite(keyboardSpacing)
22
+ ? Math.max(0, keyboardSpacing)
23
+ : 0;
18
24
  const basePadding = typeof flattenedStyle?.paddingBottom === 'number'
19
25
  ? flattenedStyle.paddingBottom
20
26
  : 0;
@@ -28,36 +34,79 @@ exports.KeyboardAwareView = (0, react_1.forwardRef)(function KeyboardAwareViewCo
28
34
  if (sequence !== measurementSequence.current || !keyboard.isVisible) {
29
35
  return;
30
36
  }
31
- setKeyboardOverlap((0, scrollManager_1.calculateKeyboardOverlap)({ y, height }, keyboard.height, keyboard.screenY));
37
+ if (!isAbsolutelyPositioned) {
38
+ setKeyboardOverlap((0, scrollManager_1.calculateKeyboardOverlap)({ y, height }, keyboard.height, keyboard.screenY));
39
+ }
40
+ setKeyboardDisplacement(isAbsolutelyPositioned && typeof keyboard.screenY === 'number'
41
+ ? (0, scrollManager_1.calculateKeyboardDisplacement)({ y, height }, keyboard.screenY, spacing)
42
+ : 0);
32
43
  });
33
- }, [keyboard.height, keyboard.isVisible, keyboard.screenY]);
44
+ }, [
45
+ isAbsolutelyPositioned,
46
+ keyboard.height,
47
+ keyboard.isVisible,
48
+ keyboard.screenY,
49
+ spacing,
50
+ ]);
34
51
  (0, react_1.useEffect)(() => {
35
52
  if (!keyboard.isVisible) {
36
53
  measurementSequence.current += 1;
37
54
  setKeyboardOverlap(0);
55
+ setKeyboardDisplacement(0);
38
56
  return;
39
57
  }
40
58
  const frame = requestAnimationFrame(measureOverlap);
41
59
  return () => cancelAnimationFrame(frame);
42
60
  }, [keyboard.isVisible, keyboard.height, keyboard.screenY, measureOverlap]);
61
+ const shouldAdjust = enabled &&
62
+ keyboard.isVisible &&
63
+ behavior !== 'none' &&
64
+ behavior !== 'resize';
65
+ (0, react_1.useEffect)(() => {
66
+ if (!isAbsolutelyPositioned) {
67
+ animatedDisplacement.setValue(0);
68
+ return;
69
+ }
70
+ const animation = react_native_1.Animated.timing(animatedDisplacement, {
71
+ toValue: shouldAdjust ? -keyboardDisplacement : 0,
72
+ duration: keyboard.animationDuration,
73
+ useNativeDriver: true,
74
+ });
75
+ animation.start();
76
+ return () => animation.stop();
77
+ }, [
78
+ animatedDisplacement,
79
+ isAbsolutelyPositioned,
80
+ keyboard.animationDuration,
81
+ keyboardDisplacement,
82
+ shouldAdjust,
83
+ ]);
43
84
  const handleLayout = (0, react_1.useCallback)((event) => {
44
85
  onLayout?.(event);
45
86
  measureOverlap();
46
87
  }, [measureOverlap, onLayout]);
47
- const adjust = enabled &&
48
- keyboard.isVisible &&
49
- behavior !== 'none' &&
50
- behavior !== 'resize' &&
88
+ const adjustPadding = shouldAdjust &&
89
+ !isAbsolutelyPositioned &&
51
90
  keyboardOverlap > 0 &&
52
91
  typeof flattenedStyle?.paddingBottom !== 'string';
53
- return ((0, jsx_runtime_1.jsx)(react_native_1.View, { ...props, ref: viewRef, onLayout: handleLayout, style: adjust
92
+ const stickyTransform = isAbsolutelyPositioned
93
+ ? [
94
+ ...(Array.isArray(flattenedStyle?.transform)
95
+ ? flattenedStyle.transform
96
+ : []),
97
+ { translateY: animatedDisplacement },
98
+ ]
99
+ : undefined;
100
+ return ((0, jsx_runtime_1.jsx)(react_native_1.Animated.View, { ...props, ref: viewRef, onLayout: handleLayout, style: adjustPadding
54
101
  ? [
55
102
  style,
56
103
  {
57
- paddingBottom: Math.max(basePadding, keyboardOverlap + Math.max(0, keyboardSpacing)),
104
+ paddingBottom: Math.max(basePadding, keyboardOverlap + spacing),
58
105
  },
59
106
  ]
60
- : style }));
107
+ : isAbsolutelyPositioned
108
+ ? [style, { transform: stickyTransform }]
109
+ : style }));
61
110
  });
62
111
  exports.KeyboardAwareView.displayName = 'KeyboardAwareView';
63
112
  exports.default = exports.KeyboardAwareView;
@@ -23,7 +23,7 @@ export interface KeyboardAwareScrollViewProps extends ScrollViewProps {
23
23
  enabled?: boolean;
24
24
  }
25
25
  export interface KeyboardAwareViewProps extends ViewProps {
26
- /** Defaults to `smart`. `resize` relies on the platform window resize behavior. */
26
+ /** Defaults to `smart`; absolute-positioned views are moved above the keyboard. */
27
27
  behavior?: KeyboardBehavior;
28
28
  /** Extra space between the view content and the keyboard, in points. */
29
29
  keyboardSpacing?: number;
@@ -10,6 +10,8 @@ export interface ScrollCalculation {
10
10
  offset: number;
11
11
  delta: number;
12
12
  }
13
+ /** Return how far a bottom edge must move to clear the keyboard and spacing. */
14
+ export declare function calculateKeyboardDisplacement(viewport: ScrollViewport, keyboardTop: number, spacing: number): number;
13
15
  /** Return the part of the measured view actually covered by the keyboard. */
14
16
  export declare function calculateKeyboardOverlap(viewport: ScrollViewport, keyboardHeight: number, keyboardTop?: number): number;
15
17
  /**
@@ -1,7 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.calculateKeyboardDisplacement = calculateKeyboardDisplacement;
3
4
  exports.calculateKeyboardOverlap = calculateKeyboardOverlap;
4
5
  exports.calculateScrollOffset = calculateScrollOffset;
6
+ /** Return how far a bottom edge must move to clear the keyboard and spacing. */
7
+ function calculateKeyboardDisplacement(viewport, keyboardTop, spacing) {
8
+ return Math.max(0, viewport.y + viewport.height - keyboardTop + Math.max(0, spacing));
9
+ }
5
10
  /** Return the part of the measured view actually covered by the keyboard. */
6
11
  function calculateKeyboardOverlap(viewport, keyboardHeight, keyboardTop) {
7
12
  const viewportBottom = viewport.y + viewport.height;
package/docs/android.md CHANGED
@@ -12,6 +12,11 @@ ended the viewport at that frame, the measured overlap is zero and no second
12
12
  keyboard-height inset is added. OEM keyboards and edge-to-edge configurations
13
13
  can still report coordinates relative to different window origins.
14
14
 
15
+ For a sticky submit action, put an absolutely positioned
16
+ `KeyboardAwareView` outside the scroll view. With `adjustResize`, Android may
17
+ already move the footer as the window changes size; the measured keyboard frame
18
+ is used to apply only any remaining displacement and configured spacing.
19
+
15
20
  ## Device checklist
16
21
 
17
22
  - Test Android 10+ devices/emulators and the app's minimum supported OS.
package/docs/api.md CHANGED
@@ -22,8 +22,11 @@ underlying React Native `ScrollView`.
22
22
 
23
23
  Extends `ViewProps`. Supports `behavior`, `keyboardSpacing`, and `enabled`.
24
24
  Smart, scroll, and padding add bottom padding for the measured overlap. Resize
25
- and none do not add padding. This view does not scroll to reveal a hidden child.
26
- Its ref points to the underlying React Native `View`.
25
+ and none do not adjust the layout. When `style.position` is `absolute`, the
26
+ component animates the view upward by the distance from its measured bottom
27
+ edge to the keyboard frame, plus `keyboardSpacing`. It returns to its original
28
+ position when the keyboard hides. This view does not scroll to reveal a hidden
29
+ child. Its ref points to the underlying React Native `View`.
27
30
 
28
31
  ## `KeyboardProvider`
29
32
 
package/docs/ios.md CHANGED
@@ -7,6 +7,11 @@ temporary scroll range, so safe-area-constrained viewports do not blindly add
7
7
  the full keyboard height. The library does not set safe-area, navigation, or
8
8
  modal presentation configuration.
9
9
 
10
+ An absolutely positioned `KeyboardAwareView` uses the same keyboard frame and
11
+ event duration to animate a sticky footer above the keyboard. Place it outside
12
+ the scroll view and account for its height in the scroll view's content padding
13
+ and `keyboardSpacing`.
14
+
10
15
  ## Device checklist
11
16
 
12
17
  - Test iPhone and iPad layouts with safe areas and the home indicator.
@@ -9,6 +9,14 @@
9
9
  - If a custom input wrapper delays focus, call `refresh()` from
10
10
  `useFocusedInput` after its layout settles.
11
11
 
12
+ ## A sticky submit button is behind the keyboard
13
+
14
+ Wrap the footer in `KeyboardAwareView` and set `position: 'absolute'` on that
15
+ component's style. The view then measures its bottom edge and moves above the
16
+ keyboard by the covered distance plus `keyboardSpacing`. Keep it outside the
17
+ scroll view and give the scroll content enough bottom padding to scroll above
18
+ the footer.
19
+
12
20
  ## Android content is moved twice
13
21
 
14
22
  Use `adjustResize` as the standard window mode and avoid adding a second
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-native-keyboard-toolkit",
3
- "version": "1.0.0",
4
- "description": "Keyboard-aware ScrollView, keyboard avoiding view, input focus navigation, and keyboard state hooks for React Native on iOS and Android.",
3
+ "version": "1.0.1",
4
+ "description": "Keyboard-aware scrolling, views, sticky form actions, and keyboard hooks for React Native on iOS and Android.",
5
5
  "author": "Denish Dabhi <dabhidenish57@gmail.com>",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/DenishDabhi/react-native-keyboard-toolkit#readme",
@@ -44,7 +44,9 @@
44
44
  "android",
45
45
  "typescript",
46
46
  "keyboard-dismiss",
47
- "keyboard-handling"
47
+ "keyboard-handling",
48
+ "sticky-button",
49
+ "sticky-footer"
48
50
  ],
49
51
  "publishConfig": {
50
52
  "access": "public"
@@ -67,8 +69,6 @@
67
69
  },
68
70
  "devDependencies": {
69
71
  "@babel/core": "^7.25.2",
70
- "@babel/preset-env": "^7.25.3",
71
- "@babel/runtime": "^7.25.0",
72
72
  "@react-native-community/cli": "20.2.0",
73
73
  "@react-native-community/cli-platform-android": "20.2.0",
74
74
  "@react-native-community/cli-platform-ios": "20.2.0",
@@ -6,14 +6,17 @@ import React, {
6
6
  useRef,
7
7
  useState,
8
8
  } from 'react';
9
- import { StyleSheet, View } from 'react-native';
9
+ import { Animated, StyleSheet } from 'react-native';
10
10
  import type { KeyboardAwareViewProps } from '../types';
11
11
  import type { KeyboardAwareViewRef } from '../types';
12
12
  import { DEFAULT_KEYBOARD_SPACING } from '../utils/constants';
13
13
  import { useKeyboardState } from '../hooks/useKeyboardState';
14
- import { calculateKeyboardOverlap } from '../utils/scrollManager';
14
+ import {
15
+ calculateKeyboardDisplacement,
16
+ calculateKeyboardOverlap,
17
+ } from '../utils/scrollManager';
15
18
 
16
- /** A lightweight view that adds bottom space while the keyboard is visible. */
19
+ /** Adds keyboard spacing to in-flow content and keeps absolute footers visible. */
17
20
  export const KeyboardAwareView: React.ForwardRefExoticComponent<
18
21
  KeyboardAwareViewProps & React.RefAttributes<KeyboardAwareViewRef>
19
22
  > = forwardRef<KeyboardAwareViewRef, KeyboardAwareViewProps>(
@@ -33,7 +36,13 @@ export const KeyboardAwareView: React.ForwardRefExoticComponent<
33
36
  useImperativeHandle(forwardedRef, () => viewRef.current!, []);
34
37
  const measurementSequence = useRef(0);
35
38
  const [keyboardOverlap, setKeyboardOverlap] = useState(0);
39
+ const [keyboardDisplacement, setKeyboardDisplacement] = useState(0);
40
+ const animatedDisplacement = useRef(new Animated.Value(0)).current;
36
41
  const flattenedStyle = StyleSheet.flatten(style);
42
+ const isAbsolutelyPositioned = flattenedStyle?.position === 'absolute';
43
+ const spacing = Number.isFinite(keyboardSpacing)
44
+ ? Math.max(0, keyboardSpacing)
45
+ : 0;
37
46
  const basePadding =
38
47
  typeof flattenedStyle?.paddingBottom === 'number'
39
48
  ? flattenedStyle.paddingBottom
@@ -49,26 +58,69 @@ export const KeyboardAwareView: React.ForwardRefExoticComponent<
49
58
  if (sequence !== measurementSequence.current || !keyboard.isVisible) {
50
59
  return;
51
60
  }
52
- setKeyboardOverlap(
53
- calculateKeyboardOverlap(
54
- { y, height },
55
- keyboard.height,
56
- keyboard.screenY,
57
- ),
61
+ if (!isAbsolutelyPositioned) {
62
+ setKeyboardOverlap(
63
+ calculateKeyboardOverlap(
64
+ { y, height },
65
+ keyboard.height,
66
+ keyboard.screenY,
67
+ ),
68
+ );
69
+ }
70
+ setKeyboardDisplacement(
71
+ isAbsolutelyPositioned && typeof keyboard.screenY === 'number'
72
+ ? calculateKeyboardDisplacement(
73
+ { y, height },
74
+ keyboard.screenY,
75
+ spacing,
76
+ )
77
+ : 0,
58
78
  );
59
79
  });
60
- }, [keyboard.height, keyboard.isVisible, keyboard.screenY]);
80
+ }, [
81
+ isAbsolutelyPositioned,
82
+ keyboard.height,
83
+ keyboard.isVisible,
84
+ keyboard.screenY,
85
+ spacing,
86
+ ]);
61
87
 
62
88
  useEffect(() => {
63
89
  if (!keyboard.isVisible) {
64
90
  measurementSequence.current += 1;
65
91
  setKeyboardOverlap(0);
92
+ setKeyboardDisplacement(0);
66
93
  return;
67
94
  }
68
95
  const frame = requestAnimationFrame(measureOverlap);
69
96
  return () => cancelAnimationFrame(frame);
70
97
  }, [keyboard.isVisible, keyboard.height, keyboard.screenY, measureOverlap]);
71
98
 
99
+ const shouldAdjust =
100
+ enabled &&
101
+ keyboard.isVisible &&
102
+ behavior !== 'none' &&
103
+ behavior !== 'resize';
104
+ useEffect(() => {
105
+ if (!isAbsolutelyPositioned) {
106
+ animatedDisplacement.setValue(0);
107
+ return;
108
+ }
109
+ const animation = Animated.timing(animatedDisplacement, {
110
+ toValue: shouldAdjust ? -keyboardDisplacement : 0,
111
+ duration: keyboard.animationDuration,
112
+ useNativeDriver: true,
113
+ });
114
+ animation.start();
115
+ return () => animation.stop();
116
+ }, [
117
+ animatedDisplacement,
118
+ isAbsolutelyPositioned,
119
+ keyboard.animationDuration,
120
+ keyboardDisplacement,
121
+ shouldAdjust,
122
+ ]);
123
+
72
124
  const handleLayout = useCallback(
73
125
  (event: Parameters<NonNullable<typeof onLayout>>[0]) => {
74
126
  onLayout?.(event);
@@ -76,30 +128,38 @@ export const KeyboardAwareView: React.ForwardRefExoticComponent<
76
128
  },
77
129
  [measureOverlap, onLayout],
78
130
  );
79
- const adjust =
80
- enabled &&
81
- keyboard.isVisible &&
82
- behavior !== 'none' &&
83
- behavior !== 'resize' &&
131
+ const adjustPadding =
132
+ shouldAdjust &&
133
+ !isAbsolutelyPositioned &&
84
134
  keyboardOverlap > 0 &&
85
135
  typeof flattenedStyle?.paddingBottom !== 'string';
136
+ const stickyTransform = isAbsolutelyPositioned
137
+ ? [
138
+ ...(Array.isArray(flattenedStyle?.transform)
139
+ ? flattenedStyle.transform
140
+ : []),
141
+ { translateY: animatedDisplacement },
142
+ ]
143
+ : undefined;
86
144
 
87
145
  return (
88
- <View
146
+ <Animated.View
89
147
  {...props}
90
148
  ref={viewRef}
91
149
  onLayout={handleLayout}
92
150
  style={
93
- adjust
151
+ adjustPadding
94
152
  ? [
95
153
  style,
96
154
  {
97
155
  paddingBottom: Math.max(
98
156
  basePadding,
99
- keyboardOverlap + Math.max(0, keyboardSpacing),
157
+ keyboardOverlap + spacing,
100
158
  ),
101
159
  },
102
160
  ]
161
+ : isAbsolutelyPositioned
162
+ ? [style, { transform: stickyTransform }]
103
163
  : style
104
164
  }
105
165
  />
@@ -52,7 +52,7 @@ export interface KeyboardAwareScrollViewProps extends ScrollViewProps {
52
52
  }
53
53
 
54
54
  export interface KeyboardAwareViewProps extends ViewProps {
55
- /** Defaults to `smart`. `resize` relies on the platform window resize behavior. */
55
+ /** Defaults to `smart`; absolute-positioned views are moved above the keyboard. */
56
56
  behavior?: KeyboardBehavior;
57
57
  /** Extra space between the view content and the keyboard, in points. */
58
58
  keyboardSpacing?: number;
@@ -13,6 +13,18 @@ export interface ScrollCalculation {
13
13
  delta: number;
14
14
  }
15
15
 
16
+ /** Return how far a bottom edge must move to clear the keyboard and spacing. */
17
+ export function calculateKeyboardDisplacement(
18
+ viewport: ScrollViewport,
19
+ keyboardTop: number,
20
+ spacing: number,
21
+ ): number {
22
+ return Math.max(
23
+ 0,
24
+ viewport.y + viewport.height - keyboardTop + Math.max(0, spacing),
25
+ );
26
+ }
27
+
16
28
  /** Return the part of the measured view actually covered by the keyboard. */
17
29
  export function calculateKeyboardOverlap(
18
30
  viewport: ScrollViewport,