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.
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +1014 -2
- package/dist/components/KeyboardAwareScrollView.d.ts +6 -0
- package/dist/components/KeyboardAwareScrollView.js +137 -0
- package/dist/components/KeyboardAwareView.d.ts +6 -0
- package/dist/components/KeyboardAwareView.js +63 -0
- package/dist/components/KeyboardDismissButton.d.ts +5 -0
- package/dist/components/KeyboardDismissButton.js +16 -0
- package/dist/components/KeyboardToolbar.d.ts +5 -0
- package/dist/components/KeyboardToolbar.js +109 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.js +11 -0
- package/dist/context/KeyboardContext.d.ts +6 -0
- package/dist/context/KeyboardContext.js +27 -0
- package/dist/context/index.d.ts +1 -0
- package/dist/context/index.js +5 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.js +11 -0
- package/dist/hooks/useFocusedInput.d.ts +6 -0
- package/dist/hooks/useFocusedInput.js +42 -0
- package/dist/hooks/useKeyboard.d.ts +1 -0
- package/dist/hooks/useKeyboard.js +5 -0
- package/dist/hooks/useKeyboardAnimation.d.ts +6 -0
- package/dist/hooks/useKeyboardAnimation.js +43 -0
- package/dist/hooks/useKeyboardState.d.ts +2 -0
- package/dist/hooks/useKeyboardState.js +9 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +23 -0
- package/dist/types/index.d.ts +69 -0
- package/dist/types/index.js +2 -0
- package/dist/utils/constants.d.ts +2 -0
- package/dist/utils/constants.js +5 -0
- package/dist/utils/focusManager.d.ts +10 -0
- package/dist/utils/focusManager.js +34 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.js +13 -0
- package/dist/utils/keyboardEvents.d.ts +7 -0
- package/dist/utils/keyboardEvents.js +88 -0
- package/dist/utils/platform.d.ts +11 -0
- package/dist/utils/platform.js +19 -0
- package/dist/utils/scrollManager.d.ts +19 -0
- package/dist/utils/scrollManager.js +32 -0
- package/docs/android.md +26 -0
- package/docs/api.md +75 -0
- package/docs/assets/demo-preview.svg +24 -0
- package/docs/installation.md +22 -0
- package/docs/ios.md +22 -0
- package/docs/troubleshooting.md +36 -0
- package/package.json +90 -4
- package/src/components/KeyboardAwareScrollView.tsx +240 -0
- package/src/components/KeyboardAwareView.tsx +112 -0
- package/src/components/KeyboardDismissButton.tsx +32 -0
- package/src/components/KeyboardToolbar.tsx +200 -0
- package/src/components/index.ts +4 -0
- package/src/context/KeyboardContext.tsx +35 -0
- package/src/context/index.ts +1 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useFocusedInput.ts +52 -0
- package/src/hooks/useKeyboard.ts +1 -0
- package/src/hooks/useKeyboardAnimation.ts +50 -0
- package/src/hooks/useKeyboardState.ts +14 -0
- package/src/index.ts +35 -0
- package/src/types/index.ts +101 -0
- package/src/utils/constants.ts +2 -0
- package/src/utils/focusManager.ts +40 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/keyboardEvents.ts +110 -0
- package/src/utils/platform.ts +23 -0
- 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,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,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
|
+
}
|
package/docs/android.md
ADDED
|
@@ -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": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|