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 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
- keyboardSpacing={16}
52
- contentContainerStyle={{ padding: 20 }}
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={112} // leave room for the footer
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 `enabled`.
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
- | Hook | Returns |
220
- | ------------------------ | ----------------------------------------------------------------------------------------- |
221
- | `useKeyboardState()` | `{ isVisible, height, progress (0 \| 1), animationDuration, screenY? }` |
222
- | `useKeyboardAnimation()` | `{ progress: Animated.Value, animationDuration }` |
223
- | `useFocusedInput()` | `{ focusedInput, refresh }` – the focused input's layout, and a function to re-measure it |
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);
@@ -1,2 +1,4 @@
1
1
  /** Subscribe to the shared, normalized keyboard state. A provider is optional. */
2
- export declare function useKeyboardState(): import("..").KeyboardState;
2
+ export declare function useKeyboardState(options?: {
3
+ isFocused?: boolean;
4
+ }): import("..").KeyboardState;
@@ -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
- return (0, react_1.useSyncExternalStore)(keyboardEvents_1.subscribeKeyboardState, keyboardEvents_1.getKeyboardState, keyboardEvents_1.getKeyboardState);
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
  }
@@ -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
- state = {
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 syncCurrentKeyboardState() {
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
- state = {
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
- 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();
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`, and `enabled`.
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
@@ -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.1",
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
- return useSyncExternalStore(
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
  }
@@ -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
- state = {
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
- subscribers.forEach(subscriber => subscriber());
50
+ });
51
+ }
52
+
53
+ function resetKeyboardState(): void {
54
+ publish(initialState);
36
55
  }
37
56
 
38
- function syncCurrentKeyboardState(): void {
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
- state = {
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
- syncCurrentKeyboardState();
68
- return;
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
- const showSubscription = Keyboard.addListener(names.show, event =>
72
- update(event, true),
103
+ listeners.push(
104
+ AppState.addEventListener('change', nextState => {
105
+ if (nextState === 'active') {
106
+ synchronizeKeyboardState();
107
+ } else {
108
+ resetKeyboardState();
109
+ }
110
+ }),
73
111
  );
74
- const hideSubscription = Keyboard.addListener(names.hide, event =>
75
- update(event, false),
112
+ listeners.push(
113
+ Dimensions.addEventListener('change', () => {
114
+ if (typeof Keyboard.metrics === 'function') {
115
+ synchronizeKeyboardState();
116
+ }
117
+ }),
76
118
  );
77
- listeners = [showSubscription, hideSubscription];
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. */