react-native-keyboard-toolkit 1.0.1 → 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 +4 -0
- package/README.md +44 -14
- package/dist/components/KeyboardAwareScrollView.js +2 -2
- package/dist/components/KeyboardAwareView.js +2 -2
- package/dist/hooks/useKeyboardState.d.ts +3 -1
- package/dist/hooks/useKeyboardState.js +6 -2
- package/dist/types/index.d.ts +4 -0
- package/dist/utils/keyboardEvents.d.ts +2 -0
- package/dist/utils/keyboardEvents.js +46 -13
- package/docs/api.md +9 -2
- package/docs/troubleshooting.md +27 -0
- package/package.json +1 -1
- package/src/components/KeyboardAwareScrollView.tsx +2 -1
- package/src/components/KeyboardAwareView.tsx +2 -1
- package/src/hooks/useKeyboardState.ts +10 -3
- package/src/types/index.ts +4 -0
- package/src/utils/keyboardEvents.ts +57 -15
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented here.
|
|
4
4
|
|
|
5
|
+
## 1.0.2 - 2026-11-14
|
|
6
|
+
|
|
7
|
+
- Fix: stale keyboard height / blank space remaining after going back with the keyboard open and returning to the screen.
|
|
8
|
+
|
|
5
9
|
## 1.0.1 - 2026-11-05
|
|
6
10
|
|
|
7
11
|
- Fix absolutely positioned `KeyboardAwareView` footers remaining behind the
|
package/README.md
CHANGED
|
@@ -48,8 +48,12 @@ export function ContactForm() {
|
|
|
48
48
|
return (
|
|
49
49
|
<KeyboardAwareScrollView
|
|
50
50
|
keyboardShouldPersistTaps="handled"
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
keyboardDismissMode="interactive"
|
|
52
|
+
behavior="smart"
|
|
53
|
+
keyboardSpacing={100}
|
|
54
|
+
contentContainerStyle={{
|
|
55
|
+
flexGrow: 1,
|
|
56
|
+
}}
|
|
53
57
|
>
|
|
54
58
|
<TextInput placeholder="Name" />
|
|
55
59
|
<TextInput placeholder="Email" keyboardType="email-address" />
|
|
@@ -76,7 +80,7 @@ export function StickyForm() {
|
|
|
76
80
|
<View style={{ flex: 1 }}>
|
|
77
81
|
<KeyboardAwareScrollView
|
|
78
82
|
keyboardShouldPersistTaps="handled"
|
|
79
|
-
keyboardSpacing={
|
|
83
|
+
keyboardSpacing={100} // leave room for the footer
|
|
80
84
|
contentContainerStyle={{ padding: 20, paddingBottom: 96 }}
|
|
81
85
|
>
|
|
82
86
|
<TextInput placeholder="Name" />
|
|
@@ -145,15 +149,16 @@ export function SignUpForm() {
|
|
|
145
149
|
|
|
146
150
|
Accepts all `ScrollView` props, forwards its ref to the `ScrollView`, and adds:
|
|
147
151
|
|
|
148
|
-
| Prop | Type | Default | Description
|
|
149
|
-
| ----------------- | -------------------------------------------------------- | --------- |
|
|
150
|
-
| `behavior` | `'smart' \| 'scroll' \| 'resize' \| 'padding' \| 'none'` | `'smart'` | See [Behaviors](#behaviors).
|
|
151
|
-
| `keyboardSpacing` | `number` | `16` | Gap (points) between the focused input and the keyboard. Negative values become `0`.
|
|
152
|
-
| `enabled` | `boolean` | `true` | Turns keyboard padding and focus scrolling on or off.
|
|
152
|
+
| Prop | Type | Default | Description |
|
|
153
|
+
| ----------------- | -------------------------------------------------------- | --------- | -------------------------------------------------------------------------------------- |
|
|
154
|
+
| `behavior` | `'smart' \| 'scroll' \| 'resize' \| 'padding' \| 'none'` | `'smart'` | See [Behaviors](#behaviors). |
|
|
155
|
+
| `keyboardSpacing` | `number` | `16` | Gap (points) between the focused input and the keyboard. Negative values become `0`. |
|
|
156
|
+
| `enabled` | `boolean` | `true` | Turns keyboard padding and focus scrolling on or off. |
|
|
157
|
+
| `isFocused` | `boolean` | – | Optional navigation focus state; resets on blur and resyncs keyboard metrics on focus. |
|
|
153
158
|
|
|
154
159
|
### `KeyboardAwareView`
|
|
155
160
|
|
|
156
|
-
Accepts all `View` props, forwards its ref to the native view, and supports `behavior`, `keyboardSpacing` (default `16`), and `
|
|
161
|
+
Accepts all `View` props, forwards its ref to the native view, and supports `behavior`, `keyboardSpacing` (default `16`), `enabled`, and optional `isFocused` navigation state.
|
|
157
162
|
|
|
158
163
|
- **Normal layout:** adds bottom padding equal to the part of the view covered by the keyboard.
|
|
159
164
|
- **`position: 'absolute'`:** animates the view up so it clears the keyboard plus `keyboardSpacing`.
|
|
@@ -216,11 +221,24 @@ const { focusedInput, refresh } = useFocusedInput();
|
|
|
216
221
|
// focusedInput: { x, y, width, height } in window coordinates, or null
|
|
217
222
|
```
|
|
218
223
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
+
For a screen that stays mounted in a navigation stack, pass its focus state to
|
|
225
|
+
both keyboard-aware components. This does not add a React Navigation dependency
|
|
226
|
+
to the toolkit:
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
const isFocused = useIsFocused(); // from @react-navigation/native
|
|
230
|
+
|
|
231
|
+
<KeyboardAwareScrollView isFocused={isFocused}>...</KeyboardAwareScrollView>
|
|
232
|
+
<KeyboardAwareView isFocused={isFocused} style={footerStyle}>
|
|
233
|
+
...
|
|
234
|
+
</KeyboardAwareView>
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
| Hook | Returns |
|
|
238
|
+
| ---------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
239
|
+
| `useKeyboardState({ isFocused? })` | `{ isVisible, height, progress (0 \| 1), animationDuration, screenY? }` |
|
|
240
|
+
| `useKeyboardAnimation()` | `{ progress: Animated.Value, animationDuration }` |
|
|
241
|
+
| `useFocusedInput()` | `{ focusedInput, refresh }` – the focused input's layout, and a function to re-measure it |
|
|
224
242
|
|
|
225
243
|
Example: animate a view with the keyboard.
|
|
226
244
|
|
|
@@ -250,6 +268,18 @@ function FadeWithKeyboard({ children }: { children: React.ReactNode }) {
|
|
|
250
268
|
- Use only one keyboard-spacing component per region to avoid adding space twice.
|
|
251
269
|
- Keyboard coordinates can differ across devices, edge-to-edge modes, and modals — test on the devices you support.
|
|
252
270
|
|
|
271
|
+
## Troubleshooting
|
|
272
|
+
|
|
273
|
+
The stale keyboard-height / blank-space issue after navigating away with the
|
|
274
|
+
keyboard open and returning is fixed in **1.0.2**. For stack screens that stay
|
|
275
|
+
mounted, pass `useIsFocused()` to both `KeyboardAwareScrollView` and
|
|
276
|
+
`KeyboardAwareView` so the state is reconciled on focus.
|
|
277
|
+
|
|
278
|
+
Manual check: focus a Screen A field, select **Go to Screen B**, then use
|
|
279
|
+
Screen B's **Back** button. Repeat with a fast transition and with the app
|
|
280
|
+
backgrounded while the keyboard is open. Screen A should return with no
|
|
281
|
+
keyboard-sized gap and its sticky button at the original position.
|
|
282
|
+
|
|
253
283
|
More guides: [Installation](docs/installation.md) · [API](docs/api.md) · [Android](docs/android.md) · [iOS](docs/ios.md) · [Troubleshooting](docs/troubleshooting.md)
|
|
254
284
|
|
|
255
285
|
## License
|
|
@@ -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, []);
|
|
@@ -8,8 +8,8 @@ const constants_1 = require("../utils/constants");
|
|
|
8
8
|
const useKeyboardState_1 = require("../hooks/useKeyboardState");
|
|
9
9
|
const scrollManager_1 = require("../utils/scrollManager");
|
|
10
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, style, onLayout, ...props }, forwardedRef) {
|
|
12
|
-
const keyboard = (0, useKeyboardState_1.useKeyboardState)();
|
|
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);
|
|
@@ -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,6 +21,8 @@ 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
28
|
/** Defaults to `smart`; absolute-positioned views are moved above the keyboard. */
|
|
@@ -29,6 +31,8 @@ export interface KeyboardAwareViewProps extends ViewProps {
|
|
|
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) {
|
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,7 +23,9 @@ 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
30
|
and none do not adjust the layout. When `style.position` is `absolute`, the
|
|
26
31
|
component animates the view upward by the distance from its measured bottom
|
|
@@ -59,8 +64,10 @@ before keyboard dismissal.
|
|
|
59
64
|
|
|
60
65
|
## Hooks
|
|
61
66
|
|
|
62
|
-
- `useKeyboardState()` and its alias `useKeyboard()` return a stable
|
|
67
|
+
- `useKeyboardState({ isFocused? })` and its alias `useKeyboard()` return a stable
|
|
63
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.
|
|
64
71
|
- `useKeyboardAnimation()` returns `{ progress: Animated.Value,
|
|
65
72
|
animationDuration }`.
|
|
66
73
|
- `useFocusedInput()` returns `{ focusedInput, refresh }`; the layout is
|
package/docs/troubleshooting.md
CHANGED
|
@@ -42,3 +42,30 @@ scroll view both respond to the same keyboard state; disable one with
|
|
|
42
42
|
Place the scroll view inside the modal content and verify the modal's measured
|
|
43
43
|
window frame. Presentation styles and window insets vary; test on the target
|
|
44
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-keyboard-toolkit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
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",
|
|
@@ -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!, []);
|
|
@@ -25,13 +25,14 @@ export const KeyboardAwareView: React.ForwardRefExoticComponent<
|
|
|
25
25
|
behavior = 'smart',
|
|
26
26
|
keyboardSpacing = DEFAULT_KEYBOARD_SPACING,
|
|
27
27
|
enabled = true,
|
|
28
|
+
isFocused,
|
|
28
29
|
style,
|
|
29
30
|
onLayout,
|
|
30
31
|
...props
|
|
31
32
|
},
|
|
32
33
|
forwardedRef,
|
|
33
34
|
): React.JSX.Element {
|
|
34
|
-
const keyboard = useKeyboardState();
|
|
35
|
+
const keyboard = useKeyboardState({ isFocused });
|
|
35
36
|
const viewRef = useRef<KeyboardAwareViewRef>(null);
|
|
36
37
|
useImperativeHandle(forwardedRef, () => viewRef.current!, []);
|
|
37
38
|
const measurementSequence = useRef(0);
|
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
import { useSyncExternalStore } from 'react';
|
|
1
|
+
import { useEffect, useSyncExternalStore } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
getKeyboardState,
|
|
4
4
|
subscribeKeyboardState,
|
|
5
|
+
synchronizeKeyboardState,
|
|
5
6
|
} from '../utils/keyboardEvents';
|
|
6
7
|
|
|
7
8
|
/** Subscribe to the shared, normalized keyboard state. A provider is optional. */
|
|
8
|
-
export function useKeyboardState() {
|
|
9
|
-
|
|
9
|
+
export function useKeyboardState(options: { isFocused?: boolean } = {}) {
|
|
10
|
+
const keyboard = useSyncExternalStore(
|
|
10
11
|
subscribeKeyboardState,
|
|
11
12
|
getKeyboardState,
|
|
12
13
|
getKeyboardState,
|
|
13
14
|
);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
synchronizeKeyboardState(options.isFocused !== false);
|
|
18
|
+
}, [options.isFocused]);
|
|
19
|
+
|
|
20
|
+
return keyboard;
|
|
14
21
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -49,6 +49,8 @@ export interface KeyboardAwareScrollViewProps extends ScrollViewProps {
|
|
|
49
49
|
keyboardSpacing?: number;
|
|
50
50
|
/** Disable keyboard-driven padding and focus scrolling. */
|
|
51
51
|
enabled?: boolean;
|
|
52
|
+
/** Pass the navigation focus state to resync keyboard metrics on refocus. */
|
|
53
|
+
isFocused?: boolean;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
export interface KeyboardAwareViewProps extends ViewProps {
|
|
@@ -58,6 +60,8 @@ export interface KeyboardAwareViewProps extends ViewProps {
|
|
|
58
60
|
keyboardSpacing?: number;
|
|
59
61
|
/** Disable keyboard-driven layout changes. */
|
|
60
62
|
enabled?: boolean;
|
|
63
|
+
/** Pass the navigation focus state to resync keyboard metrics on refocus. */
|
|
64
|
+
isFocused?: boolean;
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
export interface KeyboardProviderProps {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AppState,
|
|
3
|
+
Dimensions,
|
|
2
4
|
Keyboard,
|
|
3
5
|
type KeyboardEvent,
|
|
4
6
|
type EmitterSubscription,
|
|
@@ -18,9 +20,23 @@ let state = initialState;
|
|
|
18
20
|
let listeners: EmitterSubscription[] = [];
|
|
19
21
|
const subscribers = new Set<() => void>();
|
|
20
22
|
|
|
23
|
+
function publish(nextState: KeyboardState): void {
|
|
24
|
+
if (
|
|
25
|
+
state.isVisible === nextState.isVisible &&
|
|
26
|
+
state.height === nextState.height &&
|
|
27
|
+
state.progress === nextState.progress &&
|
|
28
|
+
state.animationDuration === nextState.animationDuration &&
|
|
29
|
+
state.screenY === nextState.screenY
|
|
30
|
+
) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
state = nextState;
|
|
34
|
+
subscribers.forEach(subscriber => subscriber());
|
|
35
|
+
}
|
|
36
|
+
|
|
21
37
|
function update(event: KeyboardEvent, visible: boolean): void {
|
|
22
38
|
const coordinates = event.endCoordinates;
|
|
23
|
-
|
|
39
|
+
publish({
|
|
24
40
|
isVisible: visible,
|
|
25
41
|
height: visible ? Math.max(0, coordinates?.height ?? 0) : 0,
|
|
26
42
|
progress: visible ? 1 : 0,
|
|
@@ -31,29 +47,39 @@ function update(event: KeyboardEvent, visible: boolean): void {
|
|
|
31
47
|
...(visible && typeof coordinates?.screenY === 'number'
|
|
32
48
|
? { screenY: coordinates.screenY }
|
|
33
49
|
: {}),
|
|
34
|
-
};
|
|
35
|
-
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resetKeyboardState(): void {
|
|
54
|
+
publish(initialState);
|
|
36
55
|
}
|
|
37
56
|
|
|
38
|
-
|
|
57
|
+
/** Reconcile the shared snapshot with the platform's current keyboard metrics. */
|
|
58
|
+
export function synchronizeKeyboardState(isFocused = true): void {
|
|
59
|
+
if (!isFocused) {
|
|
60
|
+
resetKeyboardState();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
39
64
|
const metrics =
|
|
40
65
|
typeof Keyboard.metrics === 'function' ? Keyboard.metrics() : undefined;
|
|
41
66
|
const isVisible =
|
|
42
67
|
typeof Keyboard.isVisible === 'function'
|
|
43
68
|
? Keyboard.isVisible()
|
|
44
69
|
: metrics != null;
|
|
70
|
+
|
|
45
71
|
if (!isVisible || !metrics) {
|
|
72
|
+
resetKeyboardState();
|
|
46
73
|
return;
|
|
47
74
|
}
|
|
48
75
|
|
|
49
|
-
|
|
76
|
+
publish({
|
|
50
77
|
isVisible: true,
|
|
51
78
|
height: metrics.height,
|
|
52
79
|
progress: 1,
|
|
53
80
|
animationDuration: 0,
|
|
54
81
|
screenY: metrics.screenY,
|
|
55
|
-
};
|
|
56
|
-
subscribers.forEach(subscriber => subscriber());
|
|
82
|
+
});
|
|
57
83
|
}
|
|
58
84
|
|
|
59
85
|
function startListening(): void {
|
|
@@ -64,23 +90,39 @@ function startListening(): void {
|
|
|
64
90
|
update(event, event.endCoordinates.height > 0),
|
|
65
91
|
),
|
|
66
92
|
];
|
|
67
|
-
|
|
68
|
-
|
|
93
|
+
} else {
|
|
94
|
+
const showSubscription = Keyboard.addListener(names.show, event =>
|
|
95
|
+
update(event, true),
|
|
96
|
+
);
|
|
97
|
+
const hideSubscription = Keyboard.addListener(names.hide, event =>
|
|
98
|
+
update(event, false),
|
|
99
|
+
);
|
|
100
|
+
listeners = [showSubscription, hideSubscription];
|
|
69
101
|
}
|
|
70
102
|
|
|
71
|
-
|
|
72
|
-
|
|
103
|
+
listeners.push(
|
|
104
|
+
AppState.addEventListener('change', nextState => {
|
|
105
|
+
if (nextState === 'active') {
|
|
106
|
+
synchronizeKeyboardState();
|
|
107
|
+
} else {
|
|
108
|
+
resetKeyboardState();
|
|
109
|
+
}
|
|
110
|
+
}),
|
|
73
111
|
);
|
|
74
|
-
|
|
75
|
-
|
|
112
|
+
listeners.push(
|
|
113
|
+
Dimensions.addEventListener('change', () => {
|
|
114
|
+
if (typeof Keyboard.metrics === 'function') {
|
|
115
|
+
synchronizeKeyboardState();
|
|
116
|
+
}
|
|
117
|
+
}),
|
|
76
118
|
);
|
|
77
|
-
|
|
78
|
-
syncCurrentKeyboardState();
|
|
119
|
+
synchronizeKeyboardState();
|
|
79
120
|
}
|
|
80
121
|
|
|
81
122
|
function stopListening(): void {
|
|
82
123
|
listeners.forEach(subscription => subscription.remove());
|
|
83
124
|
listeners = [];
|
|
125
|
+
resetKeyboardState();
|
|
84
126
|
}
|
|
85
127
|
|
|
86
128
|
/** Subscribe to the shared keyboard event stream. */
|