react-native-keyboard-toolkit 1.0.0 → 1.0.2
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 +13 -0
- package/README.md +155 -883
- package/dist/components/KeyboardAwareScrollView.js +2 -2
- package/dist/components/KeyboardAwareView.d.ts +1 -1
- package/dist/components/KeyboardAwareView.js +61 -12
- package/dist/hooks/useKeyboardState.d.ts +3 -1
- package/dist/hooks/useKeyboardState.js +6 -2
- package/dist/types/index.d.ts +5 -1
- package/dist/utils/keyboardEvents.d.ts +2 -0
- package/dist/utils/keyboardEvents.js +46 -13
- package/dist/utils/scrollManager.d.ts +2 -0
- package/dist/utils/scrollManager.js +5 -0
- package/docs/android.md +5 -0
- package/docs/api.md +14 -4
- package/docs/ios.md +5 -0
- package/docs/troubleshooting.md +35 -0
- package/package.json +5 -5
- package/src/components/KeyboardAwareScrollView.tsx +2 -1
- package/src/components/KeyboardAwareView.tsx +80 -19
- package/src/hooks/useKeyboardState.ts +10 -3
- package/src/types/index.ts +5 -1
- package/src/utils/keyboardEvents.ts +57 -15
- package/src/utils/scrollManager.ts +12 -0
|
@@ -11,8 +11,8 @@ const useKeyboardState_1 = require("../hooks/useKeyboardState");
|
|
|
11
11
|
const constants_1 = require("../utils/constants");
|
|
12
12
|
const platform_1 = require("../utils/platform");
|
|
13
13
|
/** A ScrollView that adds keyboard space and scrolls a covered focused input into view. */
|
|
14
|
-
exports.KeyboardAwareScrollView = (0, react_1.forwardRef)(function KeyboardAwareScrollViewComponent({ behavior = 'smart', keyboardSpacing = constants_1.DEFAULT_KEYBOARD_SPACING, enabled = true, contentContainerStyle, onScroll, onScrollBeginDrag, onContentSizeChange, onLayout, onFocusCapture, scrollEventThrottle = 16, ...props }, forwardedRef) {
|
|
15
|
-
const keyboard = (0, useKeyboardState_1.useKeyboardState)();
|
|
14
|
+
exports.KeyboardAwareScrollView = (0, react_1.forwardRef)(function KeyboardAwareScrollViewComponent({ behavior = 'smart', keyboardSpacing = constants_1.DEFAULT_KEYBOARD_SPACING, enabled = true, isFocused, contentContainerStyle, onScroll, onScrollBeginDrag, onContentSizeChange, onLayout, onFocusCapture, scrollEventThrottle = 16, ...props }, forwardedRef) {
|
|
15
|
+
const keyboard = (0, useKeyboardState_1.useKeyboardState)({ isFocused });
|
|
16
16
|
const debug = (0, KeyboardContext_1.useKeyboardDebug)();
|
|
17
17
|
const scrollRef = (0, react_1.useRef)(null);
|
|
18
18
|
(0, react_1.useImperativeHandle)(forwardedRef, () => scrollRef.current, []);
|
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
11
|
-
exports.KeyboardAwareView = (0, react_1.forwardRef)(function KeyboardAwareViewComponent({ behavior = 'smart', keyboardSpacing = constants_1.DEFAULT_KEYBOARD_SPACING, enabled = true, style, onLayout, ...props }, forwardedRef) {
|
|
12
|
-
const keyboard = (0, useKeyboardState_1.useKeyboardState)();
|
|
10
|
+
/** Adds keyboard spacing to in-flow content and keeps absolute footers visible. */
|
|
11
|
+
exports.KeyboardAwareView = (0, react_1.forwardRef)(function KeyboardAwareViewComponent({ behavior = 'smart', keyboardSpacing = constants_1.DEFAULT_KEYBOARD_SPACING, enabled = true, isFocused, style, onLayout, ...props }, forwardedRef) {
|
|
12
|
+
const keyboard = (0, useKeyboardState_1.useKeyboardState)({ isFocused });
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
|
|
48
|
-
|
|
49
|
-
behavior !== 'none' &&
|
|
50
|
-
behavior !== 'resize' &&
|
|
88
|
+
const adjustPadding = shouldAdjust &&
|
|
89
|
+
!isAbsolutelyPositioned &&
|
|
51
90
|
keyboardOverlap > 0 &&
|
|
52
91
|
typeof flattenedStyle?.paddingBottom !== 'string';
|
|
53
|
-
|
|
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 +
|
|
104
|
+
paddingBottom: Math.max(basePadding, keyboardOverlap + spacing),
|
|
58
105
|
},
|
|
59
106
|
]
|
|
60
|
-
:
|
|
107
|
+
: isAbsolutelyPositioned
|
|
108
|
+
? [style, { transform: stickyTransform }]
|
|
109
|
+
: style }));
|
|
61
110
|
});
|
|
62
111
|
exports.KeyboardAwareView.displayName = 'KeyboardAwareView';
|
|
63
112
|
exports.default = exports.KeyboardAwareView;
|
|
@@ -4,6 +4,10 @@ exports.useKeyboardState = useKeyboardState;
|
|
|
4
4
|
const react_1 = require("react");
|
|
5
5
|
const keyboardEvents_1 = require("../utils/keyboardEvents");
|
|
6
6
|
/** Subscribe to the shared, normalized keyboard state. A provider is optional. */
|
|
7
|
-
function useKeyboardState() {
|
|
8
|
-
|
|
7
|
+
function useKeyboardState(options = {}) {
|
|
8
|
+
const keyboard = (0, react_1.useSyncExternalStore)(keyboardEvents_1.subscribeKeyboardState, keyboardEvents_1.getKeyboardState, keyboardEvents_1.getKeyboardState);
|
|
9
|
+
(0, react_1.useEffect)(() => {
|
|
10
|
+
(0, keyboardEvents_1.synchronizeKeyboardState)(options.isFocused !== false);
|
|
11
|
+
}, [options.isFocused]);
|
|
12
|
+
return keyboard;
|
|
9
13
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -21,14 +21,18 @@ export interface KeyboardAwareScrollViewProps extends ScrollViewProps {
|
|
|
21
21
|
keyboardSpacing?: number;
|
|
22
22
|
/** Disable keyboard-driven padding and focus scrolling. */
|
|
23
23
|
enabled?: boolean;
|
|
24
|
+
/** Pass the navigation focus state to resync keyboard metrics on refocus. */
|
|
25
|
+
isFocused?: boolean;
|
|
24
26
|
}
|
|
25
27
|
export interface KeyboardAwareViewProps extends ViewProps {
|
|
26
|
-
/** Defaults to `smart
|
|
28
|
+
/** Defaults to `smart`; absolute-positioned views are moved above the keyboard. */
|
|
27
29
|
behavior?: KeyboardBehavior;
|
|
28
30
|
/** Extra space between the view content and the keyboard, in points. */
|
|
29
31
|
keyboardSpacing?: number;
|
|
30
32
|
/** Disable keyboard-driven layout changes. */
|
|
31
33
|
enabled?: boolean;
|
|
34
|
+
/** Pass the navigation focus state to resync keyboard metrics on refocus. */
|
|
35
|
+
isFocused?: boolean;
|
|
32
36
|
}
|
|
33
37
|
export interface KeyboardProviderProps {
|
|
34
38
|
children: ReactNode;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { KeyboardState } from '../types';
|
|
2
|
+
/** Reconcile the shared snapshot with the platform's current keyboard metrics. */
|
|
3
|
+
export declare function synchronizeKeyboardState(isFocused?: boolean): void;
|
|
2
4
|
/** Subscribe to the shared keyboard event stream. */
|
|
3
5
|
export declare function subscribeKeyboardState(subscriber: () => void): () => void;
|
|
4
6
|
/** Return the stable current snapshot used by `useSyncExternalStore`. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.synchronizeKeyboardState = synchronizeKeyboardState;
|
|
3
4
|
exports.subscribeKeyboardState = subscribeKeyboardState;
|
|
4
5
|
exports.getKeyboardState = getKeyboardState;
|
|
5
6
|
exports.resetKeyboardStateForTests = resetKeyboardStateForTests;
|
|
@@ -15,9 +16,20 @@ const initialState = {
|
|
|
15
16
|
let state = initialState;
|
|
16
17
|
let listeners = [];
|
|
17
18
|
const subscribers = new Set();
|
|
19
|
+
function publish(nextState) {
|
|
20
|
+
if (state.isVisible === nextState.isVisible &&
|
|
21
|
+
state.height === nextState.height &&
|
|
22
|
+
state.progress === nextState.progress &&
|
|
23
|
+
state.animationDuration === nextState.animationDuration &&
|
|
24
|
+
state.screenY === nextState.screenY) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
state = nextState;
|
|
28
|
+
subscribers.forEach(subscriber => subscriber());
|
|
29
|
+
}
|
|
18
30
|
function update(event, visible) {
|
|
19
31
|
const coordinates = event.endCoordinates;
|
|
20
|
-
|
|
32
|
+
publish({
|
|
21
33
|
isVisible: visible,
|
|
22
34
|
height: visible ? Math.max(0, coordinates?.height ?? 0) : 0,
|
|
23
35
|
progress: visible ? 1 : 0,
|
|
@@ -25,25 +37,32 @@ function update(event, visible) {
|
|
|
25
37
|
...(visible && typeof coordinates?.screenY === 'number'
|
|
26
38
|
? { screenY: coordinates.screenY }
|
|
27
39
|
: {}),
|
|
28
|
-
};
|
|
29
|
-
subscribers.forEach(subscriber => subscriber());
|
|
40
|
+
});
|
|
30
41
|
}
|
|
31
|
-
function
|
|
42
|
+
function resetKeyboardState() {
|
|
43
|
+
publish(initialState);
|
|
44
|
+
}
|
|
45
|
+
/** Reconcile the shared snapshot with the platform's current keyboard metrics. */
|
|
46
|
+
function synchronizeKeyboardState(isFocused = true) {
|
|
47
|
+
if (!isFocused) {
|
|
48
|
+
resetKeyboardState();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
32
51
|
const metrics = typeof react_native_1.Keyboard.metrics === 'function' ? react_native_1.Keyboard.metrics() : undefined;
|
|
33
52
|
const isVisible = typeof react_native_1.Keyboard.isVisible === 'function'
|
|
34
53
|
? react_native_1.Keyboard.isVisible()
|
|
35
54
|
: metrics != null;
|
|
36
55
|
if (!isVisible || !metrics) {
|
|
56
|
+
resetKeyboardState();
|
|
37
57
|
return;
|
|
38
58
|
}
|
|
39
|
-
|
|
59
|
+
publish({
|
|
40
60
|
isVisible: true,
|
|
41
61
|
height: metrics.height,
|
|
42
62
|
progress: 1,
|
|
43
63
|
animationDuration: 0,
|
|
44
64
|
screenY: metrics.screenY,
|
|
45
|
-
};
|
|
46
|
-
subscribers.forEach(subscriber => subscriber());
|
|
65
|
+
});
|
|
47
66
|
}
|
|
48
67
|
function startListening() {
|
|
49
68
|
const names = (0, platform_1.getKeyboardEventNames)();
|
|
@@ -51,17 +70,31 @@ function startListening() {
|
|
|
51
70
|
listeners = [
|
|
52
71
|
react_native_1.Keyboard.addListener(names.frameChange, event => update(event, event.endCoordinates.height > 0)),
|
|
53
72
|
];
|
|
54
|
-
syncCurrentKeyboardState();
|
|
55
|
-
return;
|
|
56
73
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
74
|
+
else {
|
|
75
|
+
const showSubscription = react_native_1.Keyboard.addListener(names.show, event => update(event, true));
|
|
76
|
+
const hideSubscription = react_native_1.Keyboard.addListener(names.hide, event => update(event, false));
|
|
77
|
+
listeners = [showSubscription, hideSubscription];
|
|
78
|
+
}
|
|
79
|
+
listeners.push(react_native_1.AppState.addEventListener('change', nextState => {
|
|
80
|
+
if (nextState === 'active') {
|
|
81
|
+
synchronizeKeyboardState();
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
resetKeyboardState();
|
|
85
|
+
}
|
|
86
|
+
}));
|
|
87
|
+
listeners.push(react_native_1.Dimensions.addEventListener('change', () => {
|
|
88
|
+
if (typeof react_native_1.Keyboard.metrics === 'function') {
|
|
89
|
+
synchronizeKeyboardState();
|
|
90
|
+
}
|
|
91
|
+
}));
|
|
92
|
+
synchronizeKeyboardState();
|
|
61
93
|
}
|
|
62
94
|
function stopListening() {
|
|
63
95
|
listeners.forEach(subscription => subscription.remove());
|
|
64
96
|
listeners = [];
|
|
97
|
+
resetKeyboardState();
|
|
65
98
|
}
|
|
66
99
|
/** Subscribe to the shared keyboard event stream. */
|
|
67
100
|
function subscribeKeyboardState(subscriber) {
|
|
@@ -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
|
@@ -11,6 +11,9 @@ Extends React Native `ScrollViewProps`.
|
|
|
11
11
|
none disables keyboard adjustment.
|
|
12
12
|
- `keyboardSpacing?: number` (default `16`): spacing in points.
|
|
13
13
|
- `enabled?: boolean` (default `true`): disable keyboard adjustments.
|
|
14
|
+
- `isFocused?: boolean`: optional navigation focus state. Pass `useIsFocused()`
|
|
15
|
+
from React Navigation to clear state on blur and resync native keyboard
|
|
16
|
+
metrics on focus; the library does not depend on React Navigation.
|
|
14
17
|
|
|
15
18
|
The component measures its viewport and the focused native input. It adds
|
|
16
19
|
scrollable bottom space based on measured viewport/keyboard-frame overlap,
|
|
@@ -20,10 +23,15 @@ underlying React Native `ScrollView`.
|
|
|
20
23
|
|
|
21
24
|
## `KeyboardAwareView`
|
|
22
25
|
|
|
23
|
-
Extends `ViewProps`. Supports `behavior`, `keyboardSpacing`,
|
|
26
|
+
Extends `ViewProps`. Supports `behavior`, `keyboardSpacing`, `enabled`, and
|
|
27
|
+
`isFocused` (with the same optional navigation integration as
|
|
28
|
+
`KeyboardAwareScrollView`).
|
|
24
29
|
Smart, scroll, and padding add bottom padding for the measured overlap. Resize
|
|
25
|
-
and none do not
|
|
26
|
-
|
|
30
|
+
and none do not adjust the layout. When `style.position` is `absolute`, the
|
|
31
|
+
component animates the view upward by the distance from its measured bottom
|
|
32
|
+
edge to the keyboard frame, plus `keyboardSpacing`. It returns to its original
|
|
33
|
+
position when the keyboard hides. This view does not scroll to reveal a hidden
|
|
34
|
+
child. Its ref points to the underlying React Native `View`.
|
|
27
35
|
|
|
28
36
|
## `KeyboardProvider`
|
|
29
37
|
|
|
@@ -56,8 +64,10 @@ before keyboard dismissal.
|
|
|
56
64
|
|
|
57
65
|
## Hooks
|
|
58
66
|
|
|
59
|
-
- `useKeyboardState()` and its alias `useKeyboard()` return a stable
|
|
67
|
+
- `useKeyboardState({ isFocused? })` and its alias `useKeyboard()` return a stable
|
|
60
68
|
`KeyboardState`: `{ isVisible, height, progress, animationDuration, screenY? }`.
|
|
69
|
+
The optional focus flag resets state when false and re-reads current keyboard
|
|
70
|
+
metrics when true.
|
|
61
71
|
- `useKeyboardAnimation()` returns `{ progress: Animated.Value,
|
|
62
72
|
animationDuration }`.
|
|
63
73
|
- `useFocusedInput()` returns `{ focusedInput, refresh }`; the layout is
|
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.
|
package/docs/troubleshooting.md
CHANGED
|
@@ -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
|
|
@@ -34,3 +42,30 @@ scroll view both respond to the same keyboard state; disable one with
|
|
|
34
42
|
Place the scroll view inside the modal content and verify the modal's measured
|
|
35
43
|
window frame. Presentation styles and window insets vary; test on the target
|
|
36
44
|
platform rather than assuming the main screen's coordinates apply to a modal.
|
|
45
|
+
|
|
46
|
+
## A navigation screen returns with stale keyboard spacing
|
|
47
|
+
|
|
48
|
+
The shared keyboard state is reset when its last subscriber unmounts and is
|
|
49
|
+
reconciled with `Keyboard.isVisible()` / `Keyboard.metrics()` when subscribers
|
|
50
|
+
mount or the app returns to the foreground. For stack screens that stay mounted,
|
|
51
|
+
pass the route's focus state to both aware components:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const isFocused = useIsFocused();
|
|
55
|
+
|
|
56
|
+
<KeyboardAwareScrollView isFocused={isFocused}>...</KeyboardAwareScrollView>
|
|
57
|
+
<KeyboardAwareView isFocused={isFocused}>...</KeyboardAwareView>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`useIsFocused` comes from React Navigation; the toolkit itself does not require
|
|
61
|
+
that package. Verify the lifecycle manually:
|
|
62
|
+
|
|
63
|
+
1. Open the navigation regression demo, focus the Screen A input, and wait for
|
|
64
|
+
the scroll padding and sticky action to move.
|
|
65
|
+
2. Press **Dismiss + open Screen B**, then **Go back to Screen A** without
|
|
66
|
+
focusing another input. The keyboard height and spacing should be zero.
|
|
67
|
+
3. Repeat with a rapid navigation press and, on iOS, a cancelled swipe-back.
|
|
68
|
+
4. Background and foreground the app while the keyboard is open; verify offsets
|
|
69
|
+
reflect the keyboard state after returning.
|
|
70
|
+
5. Rotate the device or resize an Android split-screen window and check that
|
|
71
|
+
layout measurements are recalculated.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-keyboard-toolkit",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Keyboard-aware
|
|
3
|
+
"version": "1.0.2",
|
|
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",
|
|
@@ -28,6 +28,7 @@ export const KeyboardAwareScrollView: React.ForwardRefExoticComponent<
|
|
|
28
28
|
behavior = 'smart',
|
|
29
29
|
keyboardSpacing = DEFAULT_KEYBOARD_SPACING,
|
|
30
30
|
enabled = true,
|
|
31
|
+
isFocused,
|
|
31
32
|
contentContainerStyle,
|
|
32
33
|
onScroll,
|
|
33
34
|
onScrollBeginDrag,
|
|
@@ -39,7 +40,7 @@ export const KeyboardAwareScrollView: React.ForwardRefExoticComponent<
|
|
|
39
40
|
},
|
|
40
41
|
forwardedRef,
|
|
41
42
|
): React.JSX.Element {
|
|
42
|
-
const keyboard = useKeyboardState();
|
|
43
|
+
const keyboard = useKeyboardState({ isFocused });
|
|
43
44
|
const debug = useKeyboardDebug();
|
|
44
45
|
const scrollRef = useRef<KeyboardAwareScrollViewRef>(null);
|
|
45
46
|
useImperativeHandle(forwardedRef, () => scrollRef.current!, []);
|
|
@@ -6,14 +6,17 @@ import React, {
|
|
|
6
6
|
useRef,
|
|
7
7
|
useState,
|
|
8
8
|
} from 'react';
|
|
9
|
-
import {
|
|
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 {
|
|
14
|
+
import {
|
|
15
|
+
calculateKeyboardDisplacement,
|
|
16
|
+
calculateKeyboardOverlap,
|
|
17
|
+
} from '../utils/scrollManager';
|
|
15
18
|
|
|
16
|
-
/**
|
|
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>(
|
|
@@ -22,18 +25,25 @@ export const KeyboardAwareView: React.ForwardRefExoticComponent<
|
|
|
22
25
|
behavior = 'smart',
|
|
23
26
|
keyboardSpacing = DEFAULT_KEYBOARD_SPACING,
|
|
24
27
|
enabled = true,
|
|
28
|
+
isFocused,
|
|
25
29
|
style,
|
|
26
30
|
onLayout,
|
|
27
31
|
...props
|
|
28
32
|
},
|
|
29
33
|
forwardedRef,
|
|
30
34
|
): React.JSX.Element {
|
|
31
|
-
const keyboard = useKeyboardState();
|
|
35
|
+
const keyboard = useKeyboardState({ isFocused });
|
|
32
36
|
const viewRef = useRef<KeyboardAwareViewRef>(null);
|
|
33
37
|
useImperativeHandle(forwardedRef, () => viewRef.current!, []);
|
|
34
38
|
const measurementSequence = useRef(0);
|
|
35
39
|
const [keyboardOverlap, setKeyboardOverlap] = useState(0);
|
|
40
|
+
const [keyboardDisplacement, setKeyboardDisplacement] = useState(0);
|
|
41
|
+
const animatedDisplacement = useRef(new Animated.Value(0)).current;
|
|
36
42
|
const flattenedStyle = StyleSheet.flatten(style);
|
|
43
|
+
const isAbsolutelyPositioned = flattenedStyle?.position === 'absolute';
|
|
44
|
+
const spacing = Number.isFinite(keyboardSpacing)
|
|
45
|
+
? Math.max(0, keyboardSpacing)
|
|
46
|
+
: 0;
|
|
37
47
|
const basePadding =
|
|
38
48
|
typeof flattenedStyle?.paddingBottom === 'number'
|
|
39
49
|
? flattenedStyle.paddingBottom
|
|
@@ -49,26 +59,69 @@ export const KeyboardAwareView: React.ForwardRefExoticComponent<
|
|
|
49
59
|
if (sequence !== measurementSequence.current || !keyboard.isVisible) {
|
|
50
60
|
return;
|
|
51
61
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
if (!isAbsolutelyPositioned) {
|
|
63
|
+
setKeyboardOverlap(
|
|
64
|
+
calculateKeyboardOverlap(
|
|
65
|
+
{ y, height },
|
|
66
|
+
keyboard.height,
|
|
67
|
+
keyboard.screenY,
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
setKeyboardDisplacement(
|
|
72
|
+
isAbsolutelyPositioned && typeof keyboard.screenY === 'number'
|
|
73
|
+
? calculateKeyboardDisplacement(
|
|
74
|
+
{ y, height },
|
|
75
|
+
keyboard.screenY,
|
|
76
|
+
spacing,
|
|
77
|
+
)
|
|
78
|
+
: 0,
|
|
58
79
|
);
|
|
59
80
|
});
|
|
60
|
-
}, [
|
|
81
|
+
}, [
|
|
82
|
+
isAbsolutelyPositioned,
|
|
83
|
+
keyboard.height,
|
|
84
|
+
keyboard.isVisible,
|
|
85
|
+
keyboard.screenY,
|
|
86
|
+
spacing,
|
|
87
|
+
]);
|
|
61
88
|
|
|
62
89
|
useEffect(() => {
|
|
63
90
|
if (!keyboard.isVisible) {
|
|
64
91
|
measurementSequence.current += 1;
|
|
65
92
|
setKeyboardOverlap(0);
|
|
93
|
+
setKeyboardDisplacement(0);
|
|
66
94
|
return;
|
|
67
95
|
}
|
|
68
96
|
const frame = requestAnimationFrame(measureOverlap);
|
|
69
97
|
return () => cancelAnimationFrame(frame);
|
|
70
98
|
}, [keyboard.isVisible, keyboard.height, keyboard.screenY, measureOverlap]);
|
|
71
99
|
|
|
100
|
+
const shouldAdjust =
|
|
101
|
+
enabled &&
|
|
102
|
+
keyboard.isVisible &&
|
|
103
|
+
behavior !== 'none' &&
|
|
104
|
+
behavior !== 'resize';
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (!isAbsolutelyPositioned) {
|
|
107
|
+
animatedDisplacement.setValue(0);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const animation = Animated.timing(animatedDisplacement, {
|
|
111
|
+
toValue: shouldAdjust ? -keyboardDisplacement : 0,
|
|
112
|
+
duration: keyboard.animationDuration,
|
|
113
|
+
useNativeDriver: true,
|
|
114
|
+
});
|
|
115
|
+
animation.start();
|
|
116
|
+
return () => animation.stop();
|
|
117
|
+
}, [
|
|
118
|
+
animatedDisplacement,
|
|
119
|
+
isAbsolutelyPositioned,
|
|
120
|
+
keyboard.animationDuration,
|
|
121
|
+
keyboardDisplacement,
|
|
122
|
+
shouldAdjust,
|
|
123
|
+
]);
|
|
124
|
+
|
|
72
125
|
const handleLayout = useCallback(
|
|
73
126
|
(event: Parameters<NonNullable<typeof onLayout>>[0]) => {
|
|
74
127
|
onLayout?.(event);
|
|
@@ -76,30 +129,38 @@ export const KeyboardAwareView: React.ForwardRefExoticComponent<
|
|
|
76
129
|
},
|
|
77
130
|
[measureOverlap, onLayout],
|
|
78
131
|
);
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
behavior !== 'none' &&
|
|
83
|
-
behavior !== 'resize' &&
|
|
132
|
+
const adjustPadding =
|
|
133
|
+
shouldAdjust &&
|
|
134
|
+
!isAbsolutelyPositioned &&
|
|
84
135
|
keyboardOverlap > 0 &&
|
|
85
136
|
typeof flattenedStyle?.paddingBottom !== 'string';
|
|
137
|
+
const stickyTransform = isAbsolutelyPositioned
|
|
138
|
+
? [
|
|
139
|
+
...(Array.isArray(flattenedStyle?.transform)
|
|
140
|
+
? flattenedStyle.transform
|
|
141
|
+
: []),
|
|
142
|
+
{ translateY: animatedDisplacement },
|
|
143
|
+
]
|
|
144
|
+
: undefined;
|
|
86
145
|
|
|
87
146
|
return (
|
|
88
|
-
<View
|
|
147
|
+
<Animated.View
|
|
89
148
|
{...props}
|
|
90
149
|
ref={viewRef}
|
|
91
150
|
onLayout={handleLayout}
|
|
92
151
|
style={
|
|
93
|
-
|
|
152
|
+
adjustPadding
|
|
94
153
|
? [
|
|
95
154
|
style,
|
|
96
155
|
{
|
|
97
156
|
paddingBottom: Math.max(
|
|
98
157
|
basePadding,
|
|
99
|
-
keyboardOverlap +
|
|
158
|
+
keyboardOverlap + spacing,
|
|
100
159
|
),
|
|
101
160
|
},
|
|
102
161
|
]
|
|
162
|
+
: isAbsolutelyPositioned
|
|
163
|
+
? [style, { transform: stickyTransform }]
|
|
103
164
|
: style
|
|
104
165
|
}
|
|
105
166
|
/>
|