react-native-keyboard-toolkit 0.0.0-stage → 1.0.0

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.
Files changed (70) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1014 -2
  4. package/dist/components/KeyboardAwareScrollView.d.ts +6 -0
  5. package/dist/components/KeyboardAwareScrollView.js +137 -0
  6. package/dist/components/KeyboardAwareView.d.ts +6 -0
  7. package/dist/components/KeyboardAwareView.js +63 -0
  8. package/dist/components/KeyboardDismissButton.d.ts +5 -0
  9. package/dist/components/KeyboardDismissButton.js +16 -0
  10. package/dist/components/KeyboardToolbar.d.ts +5 -0
  11. package/dist/components/KeyboardToolbar.js +109 -0
  12. package/dist/components/index.d.ts +4 -0
  13. package/dist/components/index.js +11 -0
  14. package/dist/context/KeyboardContext.d.ts +6 -0
  15. package/dist/context/KeyboardContext.js +27 -0
  16. package/dist/context/index.d.ts +1 -0
  17. package/dist/context/index.js +5 -0
  18. package/dist/hooks/index.d.ts +4 -0
  19. package/dist/hooks/index.js +11 -0
  20. package/dist/hooks/useFocusedInput.d.ts +6 -0
  21. package/dist/hooks/useFocusedInput.js +42 -0
  22. package/dist/hooks/useKeyboard.d.ts +1 -0
  23. package/dist/hooks/useKeyboard.js +5 -0
  24. package/dist/hooks/useKeyboardAnimation.d.ts +6 -0
  25. package/dist/hooks/useKeyboardAnimation.js +43 -0
  26. package/dist/hooks/useKeyboardState.d.ts +2 -0
  27. package/dist/hooks/useKeyboardState.js +9 -0
  28. package/dist/index.d.ts +5 -0
  29. package/dist/index.js +23 -0
  30. package/dist/types/index.d.ts +69 -0
  31. package/dist/types/index.js +2 -0
  32. package/dist/utils/constants.d.ts +2 -0
  33. package/dist/utils/constants.js +5 -0
  34. package/dist/utils/focusManager.d.ts +10 -0
  35. package/dist/utils/focusManager.js +34 -0
  36. package/dist/utils/index.d.ts +3 -0
  37. package/dist/utils/index.js +13 -0
  38. package/dist/utils/keyboardEvents.d.ts +7 -0
  39. package/dist/utils/keyboardEvents.js +88 -0
  40. package/dist/utils/platform.d.ts +11 -0
  41. package/dist/utils/platform.js +19 -0
  42. package/dist/utils/scrollManager.d.ts +19 -0
  43. package/dist/utils/scrollManager.js +32 -0
  44. package/docs/android.md +26 -0
  45. package/docs/api.md +75 -0
  46. package/docs/assets/demo-preview.svg +24 -0
  47. package/docs/installation.md +22 -0
  48. package/docs/ios.md +22 -0
  49. package/docs/troubleshooting.md +36 -0
  50. package/package.json +90 -4
  51. package/src/components/KeyboardAwareScrollView.tsx +240 -0
  52. package/src/components/KeyboardAwareView.tsx +112 -0
  53. package/src/components/KeyboardDismissButton.tsx +32 -0
  54. package/src/components/KeyboardToolbar.tsx +200 -0
  55. package/src/components/index.ts +4 -0
  56. package/src/context/KeyboardContext.tsx +35 -0
  57. package/src/context/index.ts +1 -0
  58. package/src/hooks/index.ts +4 -0
  59. package/src/hooks/useFocusedInput.ts +52 -0
  60. package/src/hooks/useKeyboard.ts +1 -0
  61. package/src/hooks/useKeyboardAnimation.ts +50 -0
  62. package/src/hooks/useKeyboardState.ts +14 -0
  63. package/src/index.ts +35 -0
  64. package/src/types/index.ts +101 -0
  65. package/src/utils/constants.ts +2 -0
  66. package/src/utils/focusManager.ts +40 -0
  67. package/src/utils/index.ts +10 -0
  68. package/src/utils/keyboardEvents.ts +110 -0
  69. package/src/utils/platform.ts +23 -0
  70. package/src/utils/scrollManager.ts +59 -0
@@ -0,0 +1,69 @@
1
+ import type { ScrollView, ScrollViewProps, StyleProp, TextInput, View, ViewProps, ViewStyle } from 'react-native';
2
+ import type { ComponentPropsWithRef, ElementRef, ReactNode, RefObject } from 'react';
3
+ type RefTarget<T> = T extends RefObject<infer Value> ? NonNullable<Value> : never;
4
+ export type TextInputRef = RefTarget<NonNullable<ComponentPropsWithRef<typeof TextInput>['ref']>>;
5
+ export type KeyboardAwareScrollViewRef = ElementRef<typeof ScrollView>;
6
+ export type KeyboardAwareViewRef = ElementRef<typeof View>;
7
+ /** Layout strategies supported by the keyboard-aware components. */
8
+ export type KeyboardBehavior = 'smart' | 'scroll' | 'resize' | 'padding' | 'none';
9
+ /** A normalized snapshot of the most recent on-screen keyboard event. */
10
+ export interface KeyboardState {
11
+ isVisible: boolean;
12
+ height: number;
13
+ progress: 0 | 1;
14
+ animationDuration: number;
15
+ screenY?: number;
16
+ }
17
+ export interface KeyboardAwareScrollViewProps extends ScrollViewProps {
18
+ /** Defaults to `smart`, which scrolls only when the focused field is covered. */
19
+ behavior?: KeyboardBehavior;
20
+ /** Space to leave between the focused field and keyboard, in points. */
21
+ keyboardSpacing?: number;
22
+ /** Disable keyboard-driven padding and focus scrolling. */
23
+ enabled?: boolean;
24
+ }
25
+ export interface KeyboardAwareViewProps extends ViewProps {
26
+ /** Defaults to `smart`. `resize` relies on the platform window resize behavior. */
27
+ behavior?: KeyboardBehavior;
28
+ /** Extra space between the view content and the keyboard, in points. */
29
+ keyboardSpacing?: number;
30
+ /** Disable keyboard-driven layout changes. */
31
+ enabled?: boolean;
32
+ }
33
+ export interface KeyboardProviderProps {
34
+ children: ReactNode;
35
+ /** Log keyboard state changes in development builds. */
36
+ debug?: boolean;
37
+ }
38
+ export interface KeyboardToolbarProps extends ViewProps {
39
+ /** Refs to the form inputs the toolbar should navigate between. */
40
+ inputRefs?: readonly RefObject<TextInputRef | null>[];
41
+ /** @deprecated Focus is detected automatically from the provided input refs. */
42
+ focusedInputIndex?: number;
43
+ /** Called when the focused input in `inputRefs` changes. */
44
+ onFocusedInputChange?: (index: number) => void;
45
+ /** Called after the Done action dismisses the keyboard. */
46
+ onDone?: () => void;
47
+ /** Label shown by the built-in Done button. */
48
+ doneText?: string;
49
+ /** Replaces the built-in previous/next/done controls. */
50
+ children?: ReactNode;
51
+ /** Render the toolbar while the keyboard is hidden. Defaults to false. */
52
+ alwaysVisible?: boolean;
53
+ }
54
+ export interface KeyboardDismissButtonProps extends ViewProps {
55
+ children: ReactNode;
56
+ /** Custom press handler. The keyboard is also dismissed unless disabled. */
57
+ onPress?: () => void;
58
+ /** Defaults to true. */
59
+ dismissKeyboard?: boolean;
60
+ accessibilityLabel?: string;
61
+ style?: StyleProp<ViewStyle>;
62
+ }
63
+ export interface FocusedInputLayout {
64
+ x: number;
65
+ y: number;
66
+ width: number;
67
+ height: number;
68
+ }
69
+ export {};
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,2 @@
1
+ export declare const DEFAULT_KEYBOARD_SPACING = 16;
2
+ export declare const DEFAULT_ANIMATION_DURATION = 250;
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_ANIMATION_DURATION = exports.DEFAULT_KEYBOARD_SPACING = void 0;
4
+ exports.DEFAULT_KEYBOARD_SPACING = 16;
5
+ exports.DEFAULT_ANIMATION_DURATION = 250;
@@ -0,0 +1,10 @@
1
+ import type { RefObject } from 'react';
2
+ import type { FocusedInputLayout, TextInputRef } from '../types';
3
+ type Measurable = Pick<TextInputRef, 'measureInWindow'>;
4
+ /** Returns the currently focused native TextInput when React Native exposes it. */
5
+ export declare function getFocusedInput(): Measurable | null;
6
+ /** Measure the focused TextInput in window coordinates. */
7
+ export declare function measureFocusedInput(input?: Measurable | null): Promise<FocusedInputLayout | null>;
8
+ /** Request focus on a TextInput ref and return whether a native input was available. */
9
+ export declare function focusInput(input: RefObject<TextInputRef | null>): boolean;
10
+ export {};
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getFocusedInput = getFocusedInput;
4
+ exports.measureFocusedInput = measureFocusedInput;
5
+ exports.focusInput = focusInput;
6
+ const react_native_1 = require("react-native");
7
+ /** Returns the currently focused native TextInput when React Native exposes it. */
8
+ function getFocusedInput() {
9
+ const input = react_native_1.TextInput.State.currentlyFocusedInput?.();
10
+ if (input) {
11
+ return input;
12
+ }
13
+ return null;
14
+ }
15
+ /** Measure the focused TextInput in window coordinates. */
16
+ function measureFocusedInput(input = getFocusedInput()) {
17
+ if (!input?.measureInWindow) {
18
+ return Promise.resolve(null);
19
+ }
20
+ return new Promise(resolve => {
21
+ input.measureInWindow?.((x, y, width, height) => {
22
+ resolve({ x, y, width, height });
23
+ });
24
+ });
25
+ }
26
+ /** Request focus on a TextInput ref and return whether a native input was available. */
27
+ function focusInput(input) {
28
+ const node = input.current;
29
+ if (!node) {
30
+ return false;
31
+ }
32
+ node.focus();
33
+ return true;
34
+ }
@@ -0,0 +1,3 @@
1
+ export { calculateKeyboardOverlap, calculateScrollOffset, } from './scrollManager';
2
+ export { focusInput, getFocusedInput, measureFocusedInput, } from './focusManager';
3
+ export { isAndroid, isIOS } from './platform';
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isIOS = exports.isAndroid = exports.measureFocusedInput = exports.getFocusedInput = exports.focusInput = exports.calculateScrollOffset = exports.calculateKeyboardOverlap = void 0;
4
+ var scrollManager_1 = require("./scrollManager");
5
+ Object.defineProperty(exports, "calculateKeyboardOverlap", { enumerable: true, get: function () { return scrollManager_1.calculateKeyboardOverlap; } });
6
+ Object.defineProperty(exports, "calculateScrollOffset", { enumerable: true, get: function () { return scrollManager_1.calculateScrollOffset; } });
7
+ var focusManager_1 = require("./focusManager");
8
+ Object.defineProperty(exports, "focusInput", { enumerable: true, get: function () { return focusManager_1.focusInput; } });
9
+ Object.defineProperty(exports, "getFocusedInput", { enumerable: true, get: function () { return focusManager_1.getFocusedInput; } });
10
+ Object.defineProperty(exports, "measureFocusedInput", { enumerable: true, get: function () { return focusManager_1.measureFocusedInput; } });
11
+ var platform_1 = require("./platform");
12
+ Object.defineProperty(exports, "isAndroid", { enumerable: true, get: function () { return platform_1.isAndroid; } });
13
+ Object.defineProperty(exports, "isIOS", { enumerable: true, get: function () { return platform_1.isIOS; } });
@@ -0,0 +1,7 @@
1
+ import type { KeyboardState } from '../types';
2
+ /** Subscribe to the shared keyboard event stream. */
3
+ export declare function subscribeKeyboardState(subscriber: () => void): () => void;
4
+ /** Return the stable current snapshot used by `useSyncExternalStore`. */
5
+ export declare function getKeyboardState(): KeyboardState;
6
+ /** Reset the shared state and event listeners; intended for tests. */
7
+ export declare function resetKeyboardStateForTests(): void;
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.subscribeKeyboardState = subscribeKeyboardState;
4
+ exports.getKeyboardState = getKeyboardState;
5
+ exports.resetKeyboardStateForTests = resetKeyboardStateForTests;
6
+ const react_native_1 = require("react-native");
7
+ const constants_1 = require("./constants");
8
+ const platform_1 = require("./platform");
9
+ const initialState = {
10
+ isVisible: false,
11
+ height: 0,
12
+ progress: 0,
13
+ animationDuration: 0,
14
+ };
15
+ let state = initialState;
16
+ let listeners = [];
17
+ const subscribers = new Set();
18
+ function update(event, visible) {
19
+ const coordinates = event.endCoordinates;
20
+ state = {
21
+ isVisible: visible,
22
+ height: visible ? Math.max(0, coordinates?.height ?? 0) : 0,
23
+ progress: visible ? 1 : 0,
24
+ animationDuration: Math.max(0, event.duration ?? constants_1.DEFAULT_ANIMATION_DURATION),
25
+ ...(visible && typeof coordinates?.screenY === 'number'
26
+ ? { screenY: coordinates.screenY }
27
+ : {}),
28
+ };
29
+ subscribers.forEach(subscriber => subscriber());
30
+ }
31
+ function syncCurrentKeyboardState() {
32
+ const metrics = typeof react_native_1.Keyboard.metrics === 'function' ? react_native_1.Keyboard.metrics() : undefined;
33
+ const isVisible = typeof react_native_1.Keyboard.isVisible === 'function'
34
+ ? react_native_1.Keyboard.isVisible()
35
+ : metrics != null;
36
+ if (!isVisible || !metrics) {
37
+ return;
38
+ }
39
+ state = {
40
+ isVisible: true,
41
+ height: metrics.height,
42
+ progress: 1,
43
+ animationDuration: 0,
44
+ screenY: metrics.screenY,
45
+ };
46
+ subscribers.forEach(subscriber => subscriber());
47
+ }
48
+ function startListening() {
49
+ const names = (0, platform_1.getKeyboardEventNames)();
50
+ if (platform_1.isIOS && names.frameChange) {
51
+ listeners = [
52
+ react_native_1.Keyboard.addListener(names.frameChange, event => update(event, event.endCoordinates.height > 0)),
53
+ ];
54
+ syncCurrentKeyboardState();
55
+ return;
56
+ }
57
+ const showSubscription = react_native_1.Keyboard.addListener(names.show, event => update(event, true));
58
+ const hideSubscription = react_native_1.Keyboard.addListener(names.hide, event => update(event, false));
59
+ listeners = [showSubscription, hideSubscription];
60
+ syncCurrentKeyboardState();
61
+ }
62
+ function stopListening() {
63
+ listeners.forEach(subscription => subscription.remove());
64
+ listeners = [];
65
+ }
66
+ /** Subscribe to the shared keyboard event stream. */
67
+ function subscribeKeyboardState(subscriber) {
68
+ subscribers.add(subscriber);
69
+ if (subscribers.size === 1) {
70
+ startListening();
71
+ }
72
+ return () => {
73
+ subscribers.delete(subscriber);
74
+ if (subscribers.size === 0) {
75
+ stopListening();
76
+ }
77
+ };
78
+ }
79
+ /** Return the stable current snapshot used by `useSyncExternalStore`. */
80
+ function getKeyboardState() {
81
+ return state;
82
+ }
83
+ /** Reset the shared state and event listeners; intended for tests. */
84
+ function resetKeyboardStateForTests() {
85
+ stopListening();
86
+ subscribers.clear();
87
+ state = initialState;
88
+ }
@@ -0,0 +1,11 @@
1
+ import type { KeyboardEventName } from 'react-native';
2
+ /** True when running on iOS. */
3
+ export declare const isIOS: boolean;
4
+ /** True when running on Android. */
5
+ export declare const isAndroid: boolean;
6
+ /** Selects the native keyboard event names for the current platform. */
7
+ export declare function getKeyboardEventNames(): {
8
+ show: KeyboardEventName;
9
+ hide: KeyboardEventName;
10
+ frameChange?: KeyboardEventName;
11
+ };
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isAndroid = exports.isIOS = void 0;
4
+ exports.getKeyboardEventNames = getKeyboardEventNames;
5
+ const react_native_1 = require("react-native");
6
+ /** True when running on iOS. */
7
+ exports.isIOS = react_native_1.Platform.OS === 'ios';
8
+ /** True when running on Android. */
9
+ exports.isAndroid = react_native_1.Platform.OS === 'android';
10
+ /** Selects the native keyboard event names for the current platform. */
11
+ function getKeyboardEventNames() {
12
+ return exports.isIOS
13
+ ? {
14
+ show: 'keyboardWillShow',
15
+ hide: 'keyboardWillHide',
16
+ frameChange: 'keyboardWillChangeFrame',
17
+ }
18
+ : { show: 'keyboardDidShow', hide: 'keyboardDidHide' };
19
+ }
@@ -0,0 +1,19 @@
1
+ export interface ScrollViewport {
2
+ y: number;
3
+ height: number;
4
+ }
5
+ export interface ScrollInput {
6
+ y: number;
7
+ height: number;
8
+ }
9
+ export interface ScrollCalculation {
10
+ offset: number;
11
+ delta: number;
12
+ }
13
+ /** Return the part of the measured view actually covered by the keyboard. */
14
+ export declare function calculateKeyboardOverlap(viewport: ScrollViewport, keyboardHeight: number, keyboardTop?: number): number;
15
+ /**
16
+ * Calculate the smallest scroll adjustment needed to bring an input into the
17
+ * visible viewport. `keyboardTop` avoids double-counting Android adjustResize.
18
+ */
19
+ export declare function calculateScrollOffset(input: ScrollInput, viewport: ScrollViewport, keyboardHeight: number, spacing: number, currentOffset: number, keyboardTop?: number): ScrollCalculation;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.calculateKeyboardOverlap = calculateKeyboardOverlap;
4
+ exports.calculateScrollOffset = calculateScrollOffset;
5
+ /** Return the part of the measured view actually covered by the keyboard. */
6
+ function calculateKeyboardOverlap(viewport, keyboardHeight, keyboardTop) {
7
+ const viewportBottom = viewport.y + viewport.height;
8
+ const obscuredTop = typeof keyboardTop === 'number'
9
+ ? Math.max(viewport.y, Math.min(viewportBottom, keyboardTop))
10
+ : Math.max(viewport.y, viewportBottom - Math.max(0, keyboardHeight));
11
+ return Math.max(0, viewportBottom - obscuredTop);
12
+ }
13
+ /**
14
+ * Calculate the smallest scroll adjustment needed to bring an input into the
15
+ * visible viewport. `keyboardTop` avoids double-counting Android adjustResize.
16
+ */
17
+ function calculateScrollOffset(input, viewport, keyboardHeight, spacing, currentOffset, keyboardTop) {
18
+ const viewportBottom = viewport.y + viewport.height;
19
+ const unobscuredBottom = viewportBottom -
20
+ calculateKeyboardOverlap(viewport, keyboardHeight, keyboardTop) -
21
+ spacing;
22
+ const unobscuredTop = viewport.y + spacing;
23
+ const inputBottom = input.y + input.height;
24
+ let delta = 0;
25
+ if (inputBottom > unobscuredBottom) {
26
+ delta = inputBottom - unobscuredBottom;
27
+ }
28
+ else if (input.y < unobscuredTop) {
29
+ delta = input.y - unobscuredTop;
30
+ }
31
+ return { delta, offset: Math.max(0, currentOffset + delta) };
32
+ }
@@ -0,0 +1,26 @@
1
+ # Android testing and setup
2
+
3
+ The library listens to `keyboardDidShow` / `keyboardDidHide`, measures native
4
+ input and scroll-view frames, and reads the reported keyboard screen coordinate
5
+ when available. It does not edit `MainActivity`, `MainApplication`, or the
6
+ manifest.
7
+
8
+ For common form behavior, configure the activity to use `adjustResize`. The
9
+ repository app already uses this setting. The components intersect the measured
10
+ viewport with the keyboard's reported top frame; if `adjustResize` has already
11
+ ended the viewport at that frame, the measured overlap is zero and no second
12
+ keyboard-height inset is added. OEM keyboards and edge-to-edge configurations
13
+ can still report coordinates relative to different window origins.
14
+
15
+ ## Device checklist
16
+
17
+ - Test Android 10+ devices/emulators and the app's minimum supported OS.
18
+ - Try both gesture and three-button navigation.
19
+ - Test `adjustResize` and, if the app supports it, `adjustPan`.
20
+ - Test status/navigation bars, edge-to-edge, and any translucent system bars.
21
+ - Test multiline inputs, modal screens, nested scroll views, and input focus
22
+ during navigation transitions.
23
+ - Try at least one hardware keyboard and one alternate software keyboard.
24
+
25
+ This project has not been validated on physical Android devices. Do not treat
26
+ these notes as device compatibility claims.
package/docs/api.md ADDED
@@ -0,0 +1,75 @@
1
+ # API reference
2
+
3
+ ## `KeyboardAwareScrollView`
4
+
5
+ Extends React Native `ScrollViewProps`.
6
+
7
+ - `behavior?: 'smart' | 'scroll' | 'resize' | 'padding' | 'none'` (default
8
+ `'smart'`): smart and scroll behaviors minimally scroll a covered focused
9
+ input; padding extends the scroll range without focus scrolling; resize uses
10
+ automatic keyboard insets on iOS and relies on window resizing on Android;
11
+ none disables keyboard adjustment.
12
+ - `keyboardSpacing?: number` (default `16`): spacing in points.
13
+ - `enabled?: boolean` (default `true`): disable keyboard adjustments.
14
+
15
+ The component measures its viewport and the focused native input. It adds
16
+ scrollable bottom space based on measured viewport/keyboard-frame overlap,
17
+ avoiding blindly adding the full keyboard height when the OS has resized the
18
+ window. Existing numeric bottom padding is preserved. Its ref points to the
19
+ underlying React Native `ScrollView`.
20
+
21
+ ## `KeyboardAwareView`
22
+
23
+ Extends `ViewProps`. Supports `behavior`, `keyboardSpacing`, and `enabled`.
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`.
27
+
28
+ ## `KeyboardProvider`
29
+
30
+ `<KeyboardProvider debug={false}>children</KeyboardProvider>`. It is optional;
31
+ set `debug` to log state changes in development.
32
+
33
+ ## `KeyboardToolbar`
34
+
35
+ Extends `ViewProps` and accepts:
36
+
37
+ - `inputRefs?: readonly RefObject<TextInputRef | null>[]`: refs ordered for
38
+ Previous/Next navigation. The currently focused ref is detected
39
+ automatically.
40
+ - `onFocusedInputChange?: (index: number) => void`: called when a supplied ref
41
+ becomes or ceases to be focused; `-1` means no supplied ref is focused.
42
+ - `onDone?: () => void`, `doneText?: string`, and `alwaysVisible?: boolean`.
43
+ - `children?: ReactNode`: replaces the default controls.
44
+
45
+ Previous and Next skip null/unmounted refs and are disabled when no mounted
46
+ input exists in that direction. Done blurs the focused native input when
47
+ available, calls `Keyboard.dismiss()`, then invokes `onDone`. The deprecated
48
+ `focusedInputIndex` prop remains supported for compatibility; it is not needed
49
+ for automatic focus tracking.
50
+
51
+ ## `KeyboardDismissButton`
52
+
53
+ Accepts `children`, `onPress`, `dismissKeyboard` (default `true`),
54
+ `accessibilityLabel`, and React Native `ViewProps`. The optional callback runs
55
+ before keyboard dismissal.
56
+
57
+ ## Hooks
58
+
59
+ - `useKeyboardState()` and its alias `useKeyboard()` return a stable
60
+ `KeyboardState`: `{ isVisible, height, progress, animationDuration, screenY? }`.
61
+ - `useKeyboardAnimation()` returns `{ progress: Animated.Value,
62
+ animationDuration }`.
63
+ - `useFocusedInput()` returns `{ focusedInput, refresh }`; the layout is
64
+ window-relative or `null` when the focused input cannot be measured.
65
+
66
+ The package entry point also exports `calculateKeyboardOverlap`,
67
+ `calculateScrollOffset`, `focusInput`, `getFocusedInput`, `isAndroid`, `isIOS`,
68
+ `measureFocusedInput`, and the `TextInputRef` type used by toolbar refs. The
69
+ `KeyboardAwareScrollViewRef` and `KeyboardAwareViewRef` types describe the
70
+ corresponding component refs.
71
+
72
+ `calculateKeyboardOverlap(viewport, keyboardHeight, keyboardTop?)` returns the
73
+ measured overlap. `calculateScrollOffset(input, viewport, keyboardHeight,
74
+ spacing, currentOffset, keyboardTop?)` returns `{ offset, delta }`. The
75
+ coordinate values must use the same window-coordinate system.
@@ -0,0 +1,24 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="960" height="540" viewBox="0 0 960 540" role="img" aria-labelledby="title description">
2
+ <title id="title">Keyboard Shepherd illustrative demo preview</title>
3
+ <desc id="description">An illustration of a keyboard-aware profile form above a software keyboard.</desc>
4
+ <rect width="960" height="540" fill="#f4f6fb"/>
5
+ <text x="54" y="58" fill="#101828" font-family="Arial, sans-serif" font-size="26" font-weight="700">Keyboard Shepherd</text>
6
+ <text x="54" y="86" fill="#667085" font-family="Arial, sans-serif" font-size="15">Illustrative form layout — keyboard-aware scrolling</text>
7
+ <rect x="250" y="110" width="460" height="290" rx="18" fill="#ffffff" stroke="#d0d5dd"/>
8
+ <text x="282" y="151" fill="#101828" font-family="Arial, sans-serif" font-size="21" font-weight="700">Create your profile</text>
9
+ <text x="282" y="181" fill="#475467" font-family="Arial, sans-serif" font-size="13">Email</text>
10
+ <rect x="282" y="190" width="396" height="42" rx="8" fill="#ffffff" stroke="#98a2b3"/>
11
+ <text x="296" y="216" fill="#667085" font-family="Arial, sans-serif" font-size="14">name@example.com</text>
12
+ <text x="282" y="258" fill="#475467" font-family="Arial, sans-serif" font-size="13">Description</text>
13
+ <rect x="282" y="267" width="396" height="62" rx="8" fill="#ffffff" stroke="#98a2b3"/>
14
+ <text x="296" y="293" fill="#667085" font-family="Arial, sans-serif" font-size="14">Input remains visible while typing</text>
15
+ <rect x="282" y="345" width="396" height="38" rx="8" fill="#155eef"/>
16
+ <text x="480" y="370" fill="#ffffff" font-family="Arial, sans-serif" font-size="14" font-weight="700" text-anchor="middle">Submit form</text>
17
+ <rect x="0" y="420" width="960" height="120" fill="#e4e7ec"/>
18
+ <rect x="24" y="434" width="912" height="30" rx="7" fill="#ffffff"/>
19
+ <text x="480" y="454" fill="#475467" font-family="Arial, sans-serif" font-size="13" text-anchor="middle">Keyboard toolbar: Previous Next Done</text>
20
+ <g fill="#ffffff">
21
+ <rect x="38" y="475" width="84" height="25" rx="5"/><rect x="132" y="475" width="84" height="25" rx="5"/><rect x="226" y="475" width="84" height="25" rx="5"/><rect x="320" y="475" width="84" height="25" rx="5"/><rect x="414" y="475" width="84" height="25" rx="5"/><rect x="508" y="475" width="84" height="25" rx="5"/><rect x="602" y="475" width="84" height="25" rx="5"/><rect x="696" y="475" width="84" height="25" rx="5"/><rect x="790" y="475" width="84" height="25" rx="5"/>
22
+ <rect x="82" y="507" width="84" height="25" rx="5"/><rect x="176" y="507" width="84" height="25" rx="5"/><rect x="270" y="507" width="84" height="25" rx="5"/><rect x="364" y="507" width="84" height="25" rx="5"/><rect x="458" y="507" width="84" height="25" rx="5"/><rect x="552" y="507" width="84" height="25" rx="5"/><rect x="646" y="507" width="84" height="25" rx="5"/><rect x="740" y="507" width="84" height="25" rx="5"/>
23
+ </g>
24
+ </svg>
@@ -0,0 +1,22 @@
1
+ # Installation
2
+
3
+ ```sh
4
+ npm install react-native-keyboard-toolkit
5
+ ```
6
+
7
+ React and React Native are peer dependencies. The package uses only React Native
8
+ JavaScript APIs, so it does not require native linking, a Pod, a config plugin,
9
+ or a custom Expo development client.
10
+
11
+ ## React Native CLI
12
+
13
+ 1. Install the package in the app.
14
+ 2. Use `KeyboardAwareScrollView` around the form content.
15
+ 3. On Android, keep the activity's `windowSoftInputMode` at `adjustResize` for
16
+ the conventional resizable-window behavior.
17
+
18
+ ## Expo
19
+
20
+ Use the package from an Expo app as you would any JavaScript-only dependency.
21
+ The keyboard APIs are included in Expo Go. The development demo lives in the
22
+ repository's React Native host app; it is not a generated Expo project.
package/docs/ios.md ADDED
@@ -0,0 +1,22 @@
1
+ # iOS testing
2
+
3
+ The library consumes iOS keyboard frame-change events and uses
4
+ `measureInWindow` for the focused field and its scroll viewport. The intersection
5
+ of those measured window coordinates with the keyboard frame determines the
6
+ temporary scroll range, so safe-area-constrained viewports do not blindly add
7
+ the full keyboard height. The library does not set safe-area, navigation, or
8
+ modal presentation configuration.
9
+
10
+ ## Device checklist
11
+
12
+ - Test iPhone and iPad layouts with safe areas and the home indicator.
13
+ - Verify navigation headers and bottom tab bars are outside the measured form
14
+ region or included in its layout.
15
+ - Test multiline inputs, interactive keyboard dismissal, and modal screens.
16
+ - On iPad, test floating and split keyboards where available.
17
+ - Test hardware keyboard attachment and focus transitions.
18
+ - Check input behavior during navigation animations.
19
+
20
+ Floating/split keyboards and transition-time measurements may not describe one
21
+ contiguous obscured area. Automated unit tests cannot validate native keyboard
22
+ geometry; test on actual simulators/devices before release.
@@ -0,0 +1,36 @@
1
+ # Troubleshooting
2
+
3
+ ## The focused field does not scroll
4
+
5
+ - Confirm the input is a native React Native `TextInput`, not an unmeasurable
6
+ third-party wrapper.
7
+ - Ensure the focused input belongs to the scroll view and has completed layout.
8
+ - Check that `enabled` is true and the behavior is `smart` or `scroll`.
9
+ - If a custom input wrapper delays focus, call `refresh()` from
10
+ `useFocusedInput` after its layout settles.
11
+
12
+ ## Android content is moved twice
13
+
14
+ Use `adjustResize` as the standard window mode and avoid adding a second
15
+ keyboard-height spacer around the same content. The scroll view uses the event
16
+ screen coordinate where available, but unusual edge-to-edge or OEM keyboard
17
+ configurations may need a custom layout.
18
+
19
+ ## Keyboard height is zero or unexpected
20
+
21
+ Hardware keyboards may not open a software keyboard. Floating and split
22
+ keyboards can occupy only part of the screen and do not have a useful full-width
23
+ height. Keyboard-reported heights can differ with navigation bars and OS
24
+ versions.
25
+
26
+ ## Nested views add too much space
27
+
28
+ Use one keyboard-padding component per layout region. A nested aware view and
29
+ scroll view both respond to the same keyboard state; disable one with
30
+ `behavior="resize"` or `enabled={false}` when only one should adjust.
31
+
32
+ ## The keyboard hides a modal field
33
+
34
+ Place the scroll view inside the modal content and verify the modal's measured
35
+ window frame. Presentation styles and window insets vary; test on the target
36
+ platform rather than assuming the main screen's coordinates apply to a modal.
package/package.json CHANGED
@@ -1,6 +1,92 @@
1
1
  {
2
2
  "name": "react-native-keyboard-toolkit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
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.",
5
+ "author": "Denish Dabhi <dabhidenish57@gmail.com>",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/DenishDabhi/react-native-keyboard-toolkit#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/DenishDabhi/react-native-keyboard-toolkit.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/DenishDabhi/react-native-keyboard-toolkit/issues"
14
+ },
15
+ "main": "dist/index.js",
16
+ "react-native": "src/index.ts",
17
+ "types": "dist/index.d.ts",
18
+ "files": [
19
+ "dist",
20
+ "src",
21
+ "docs",
22
+ "README.md",
23
+ "CHANGELOG.md",
24
+ "LICENSE"
25
+ ],
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "react-native": "./src/index.ts",
30
+ "default": "./dist/index.js"
31
+ }
32
+ },
33
+ "sideEffects": false,
34
+ "keywords": [
35
+ "react-native",
36
+ "keyboard",
37
+ "keyboard-aware",
38
+ "keyboard-management",
39
+ "keyboard-avoiding",
40
+ "scrollview",
41
+ "textinput",
42
+ "expo",
43
+ "ios",
44
+ "android",
45
+ "typescript",
46
+ "keyboard-dismiss",
47
+ "keyboard-handling"
48
+ ],
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "scripts": {
53
+ "android": "react-native run-android",
54
+ "ios": "react-native run-ios",
55
+ "build": "tsc -p tsconfig.build.json",
56
+ "typecheck": "tsc --noEmit -p tsconfig.json",
57
+ "lint": "eslint src tests demo App.tsx --ext .ts,.tsx",
58
+ "format:check": "prettier --check src tests demo App.tsx package.json README.md CHANGELOG.md docs",
59
+ "start": "react-native start",
60
+ "test": "jest --runInBand",
61
+ "validate": "npm run typecheck && npm run lint && npm run format:check && npm test && npm run build && npm pack --dry-run",
62
+ "prepublishOnly": "npm run validate"
63
+ },
64
+ "peerDependencies": {
65
+ "react": ">=18.2.0",
66
+ "react-native": ">=0.71.0"
67
+ },
68
+ "devDependencies": {
69
+ "@babel/core": "^7.25.2",
70
+ "@babel/preset-env": "^7.25.3",
71
+ "@babel/runtime": "^7.25.0",
72
+ "@react-native-community/cli": "20.2.0",
73
+ "@react-native-community/cli-platform-android": "20.2.0",
74
+ "@react-native-community/cli-platform-ios": "20.2.0",
75
+ "@react-native/babel-preset": "0.88.1",
76
+ "@react-native/eslint-config": "0.88.1",
77
+ "@react-native/jest-preset": "0.88.1",
78
+ "@react-native/metro-config": "0.88.1",
79
+ "@react-native/typescript-config": "0.88.1",
80
+ "@types/jest": "^29.5.13",
81
+ "@types/react": "^19.2.0",
82
+ "@types/react-test-renderer": "^19.1.0",
83
+ "eslint": "^8.19.0",
84
+ "jest": "^29.6.3",
85
+ "prettier": "2.8.8",
86
+ "react": "19.3.0",
87
+ "react-native": "0.88.1",
88
+ "react-native-safe-area-context": "^5.5.2",
89
+ "react-test-renderer": "19.3.0",
90
+ "typescript": "^6.0.3"
91
+ }
92
+ }