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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1014 -2
  4. package/dist/components/KeyboardAwareScrollView.d.ts +6 -0
  5. package/dist/components/KeyboardAwareScrollView.js +137 -0
  6. package/dist/components/KeyboardAwareView.d.ts +6 -0
  7. package/dist/components/KeyboardAwareView.js +63 -0
  8. package/dist/components/KeyboardDismissButton.d.ts +5 -0
  9. package/dist/components/KeyboardDismissButton.js +16 -0
  10. package/dist/components/KeyboardToolbar.d.ts +5 -0
  11. package/dist/components/KeyboardToolbar.js +109 -0
  12. package/dist/components/index.d.ts +4 -0
  13. package/dist/components/index.js +11 -0
  14. package/dist/context/KeyboardContext.d.ts +6 -0
  15. package/dist/context/KeyboardContext.js +27 -0
  16. package/dist/context/index.d.ts +1 -0
  17. package/dist/context/index.js +5 -0
  18. package/dist/hooks/index.d.ts +4 -0
  19. package/dist/hooks/index.js +11 -0
  20. package/dist/hooks/useFocusedInput.d.ts +6 -0
  21. package/dist/hooks/useFocusedInput.js +42 -0
  22. package/dist/hooks/useKeyboard.d.ts +1 -0
  23. package/dist/hooks/useKeyboard.js +5 -0
  24. package/dist/hooks/useKeyboardAnimation.d.ts +6 -0
  25. package/dist/hooks/useKeyboardAnimation.js +43 -0
  26. package/dist/hooks/useKeyboardState.d.ts +2 -0
  27. package/dist/hooks/useKeyboardState.js +9 -0
  28. package/dist/index.d.ts +5 -0
  29. package/dist/index.js +23 -0
  30. package/dist/types/index.d.ts +69 -0
  31. package/dist/types/index.js +2 -0
  32. package/dist/utils/constants.d.ts +2 -0
  33. package/dist/utils/constants.js +5 -0
  34. package/dist/utils/focusManager.d.ts +10 -0
  35. package/dist/utils/focusManager.js +34 -0
  36. package/dist/utils/index.d.ts +3 -0
  37. package/dist/utils/index.js +13 -0
  38. package/dist/utils/keyboardEvents.d.ts +7 -0
  39. package/dist/utils/keyboardEvents.js +88 -0
  40. package/dist/utils/platform.d.ts +11 -0
  41. package/dist/utils/platform.js +19 -0
  42. package/dist/utils/scrollManager.d.ts +19 -0
  43. package/dist/utils/scrollManager.js +32 -0
  44. package/docs/android.md +26 -0
  45. package/docs/api.md +75 -0
  46. package/docs/assets/demo-preview.svg +24 -0
  47. package/docs/installation.md +22 -0
  48. package/docs/ios.md +22 -0
  49. package/docs/troubleshooting.md +36 -0
  50. package/package.json +90 -4
  51. package/src/components/KeyboardAwareScrollView.tsx +240 -0
  52. package/src/components/KeyboardAwareView.tsx +112 -0
  53. package/src/components/KeyboardDismissButton.tsx +32 -0
  54. package/src/components/KeyboardToolbar.tsx +200 -0
  55. package/src/components/index.ts +4 -0
  56. package/src/context/KeyboardContext.tsx +35 -0
  57. package/src/context/index.ts +1 -0
  58. package/src/hooks/index.ts +4 -0
  59. package/src/hooks/useFocusedInput.ts +52 -0
  60. package/src/hooks/useKeyboard.ts +1 -0
  61. package/src/hooks/useKeyboardAnimation.ts +50 -0
  62. package/src/hooks/useKeyboardState.ts +14 -0
  63. package/src/index.ts +35 -0
  64. package/src/types/index.ts +101 -0
  65. package/src/utils/constants.ts +2 -0
  66. package/src/utils/focusManager.ts +40 -0
  67. package/src/utils/index.ts +10 -0
  68. package/src/utils/keyboardEvents.ts +110 -0
  69. package/src/utils/platform.ts +23 -0
  70. package/src/utils/scrollManager.ts +59 -0
package/README.md CHANGED
@@ -1,3 +1,1015 @@
1
- # Temporary Holding Version
1
+ # react-native-keyboard-toolkit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version](https://img.shields.io/npm/v/react-native-keyboard-toolkit.svg)](https://www.npmjs.com/package/react-native-keyboard-toolkit)
4
+ [![license](https://img.shields.io/npm/l/react-native-keyboard-toolkit.svg)](LICENSE)
5
+ [![platforms](https://img.shields.io/badge/platforms-iOS%20%7C%20Android-blue.svg)](#supported-platforms)
6
+
7
+ Keyboard-aware scrolling, layout helpers, hooks, and focus utilities for React Native.
8
+
9
+ `react-native-keyboard-toolkit` is a **JavaScript-only** library. It is built on React Native's own `Keyboard` events and view measurements (`measureInWindow`), so it has **no native module, no CocoaPods step, no manual linking, and no Babel plugin**.
10
+
11
+ - [Features](#features)
12
+ - [Supported platforms](#supported-platforms)
13
+ - [Compatibility and requirements](#compatibility-and-requirements)
14
+ - [Installation](#installation)
15
+ - [Setup](#setup)
16
+ - [Basic usage](#basic-usage)
17
+ - [Components](#components)
18
+ - [`KeyboardAwareScrollView`](#keyboardawarescrollview)
19
+ - [`KeyboardAwareView`](#keyboardawareview)
20
+ - [`KeyboardToolbar`](#keyboardtoolbar)
21
+ - [`KeyboardDismissButton`](#keyboarddismissbutton)
22
+ - [`KeyboardProvider`](#keyboardprovider)
23
+ - [Hooks](#hooks)
24
+ - [`useKeyboardState` / `useKeyboard`](#usekeyboardstate--usekeyboard)
25
+ - [`useKeyboardAnimation`](#usekeyboardanimation)
26
+ - [`useFocusedInput`](#usefocusedinput)
27
+ - [Utility functions](#utility-functions)
28
+ - [Complete React Native CLI example](#complete-react-native-cli-example)
29
+ - [TypeScript](#typescript)
30
+ - [API reference](#api-reference)
31
+ - [How it works](#how-it-works)
32
+ - [Troubleshooting](#troubleshooting)
33
+ - [Example project](#example-project)
34
+ - [Contributing](#contributing)
35
+ - [License](#license)
36
+ - [Links](#links)
37
+
38
+ ## Features
39
+
40
+ - **`KeyboardAwareScrollView`** – a `ScrollView` that adds space for the keyboard and scrolls a covered, focused `TextInput` into view.
41
+ - **`KeyboardAwareView`** – a lightweight `View` that adds bottom space while the keyboard covers it.
42
+ - **`KeyboardToolbar`** – an in-layout toolbar with Previous / Next / Done controls for moving between input refs.
43
+ - **`KeyboardDismissButton`** – an accessible pressable that dismisses the keyboard.
44
+ - **Hooks** – `useKeyboardState` (alias `useKeyboard`), `useKeyboardAnimation`, and `useFocusedInput`.
45
+ - **Shared keyboard state** – one native event subscription is shared by every component and hook, and it is cleaned up automatically.
46
+ - **Focus and layout utilities** – `getFocusedInput`, `measureFocusedInput`, `focusInput`, `calculateKeyboardOverlap`, and `calculateScrollOffset`.
47
+ - **Overlap-based adjustment** – components measure how much of _themselves_ the keyboard actually covers instead of blindly adding the full keyboard height, which avoids double spacing when Android has already resized the window.
48
+ - **Five layout behaviors** – `smart`, `scroll`, `padding`, `resize`, and `none`.
49
+ - **Written in TypeScript** – type declarations are bundled.
50
+
51
+ ## Supported platforms
52
+
53
+ | Platform | Support |
54
+ | ------------------- | ----------------------------------------------------------------------- |
55
+ | iOS | Supported |
56
+ | Android | Supported |
57
+ | Expo | JavaScript-only, so no config plugin is needed (not tested – see below) |
58
+ | Web, Windows, macOS | Not supported |
59
+
60
+ ## Compatibility and requirements
61
+
62
+ | Requirement | Version |
63
+ | -------------- | --------------------------------------------------------------------------- |
64
+ | `react` | `>=18.2.0` (uses `useSyncExternalStore`) |
65
+ | `react-native` | `>=0.71.0` |
66
+ | TypeScript | Optional. Declarations are included and work with `@types/react` 18 and 19. |
67
+
68
+ The package was checked against these combinations (TypeScript compile of an app that imports the packed package, Jest render of every component and hook, and a Metro bundle where noted):
69
+
70
+ | React Native | React | Type check | Jest render | Metro bundle |
71
+ | ------------ | ------ | :--------: | :---------: | :---------------: |
72
+ | 0.71.19 | 18.2.0 | ✅ | ✅ | ✅ |
73
+ | 0.76.9 | 18.3.1 | ✅ | ✅ | – |
74
+ | 0.88.1 | 19.3.0 | ✅ | ✅ | ✅ (Android, iOS) |
75
+
76
+ These are automated checks. **The library has not been validated on physical devices or simulators/emulators**, and Expo SDK projects have not been tested. Keyboard geometry differs between devices, OS versions, keyboards, and window configurations, so please test the screens you ship. See [`docs/ios.md`](docs/ios.md) and [`docs/android.md`](docs/android.md) for device checklists.
77
+
78
+ ## Installation
79
+
80
+ ```sh
81
+ npm install react-native-keyboard-toolkit
82
+ # or
83
+ yarn add react-native-keyboard-toolkit
84
+ # or
85
+ pnpm add react-native-keyboard-toolkit
86
+ ```
87
+
88
+ ### Required peer dependencies
89
+
90
+ These must already be in your app (every React Native app has them):
91
+
92
+ | Package | Version | Why |
93
+ | -------------- | ---------- | ------------------------------------------------------- |
94
+ | `react` | `>=18.2.0` | Hooks and `useSyncExternalStore`. |
95
+ | `react-native` | `>=0.71.0` | `Keyboard`, `TextInput`, `ScrollView`, `Animated`, etc. |
96
+
97
+ ### Optional dependencies
98
+
99
+ The library has **no other runtime dependencies** and does not import any third-party package.
100
+
101
+ | Package | When you might want it |
102
+ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
103
+ | `react-native-safe-area-context` | To lay out your screens inside the safe area (notch, home indicator). **Not required by this library** – it is only used by the repository's demo app. |
104
+
105
+ If you want it, install it as described in its own documentation:
106
+
107
+ ```sh
108
+ npm install react-native-safe-area-context
109
+ ```
110
+
111
+ ### Dependency installation checklist
112
+
113
+ 1. `npm install react-native-keyboard-toolkit`
114
+ 2. Nothing else for the library itself. There is no `pod install` step _for this package_. (As usual in a React Native CLI app, run `cd ios && pod install` after adding any other native dependency.)
115
+ 3. Restart Metro (`npx react-native start --reset-cache`) if it was already running.
116
+
117
+ ## Setup
118
+
119
+ ### React Native CLI
120
+
121
+ No native changes are required for the library. For the best results on Android, keep the standard resizing window mode on your activity (the React Native CLI template already uses it):
122
+
123
+ ```xml
124
+ <!-- android/app/src/main/AndroidManifest.xml -->
125
+ <activity
126
+ android:name=".MainActivity"
127
+ android:windowSoftInputMode="adjustResize"
128
+ ... />
129
+ ```
130
+
131
+ Keep the other attributes of your existing `MainActivity` declaration. If you use `adjustPan` or an edge-to-edge configuration, see [Troubleshooting](#troubleshooting) and [`docs/android.md`](docs/android.md).
132
+
133
+ On iOS there is nothing to configure.
134
+
135
+ ### Expo
136
+
137
+ The package contains only JavaScript, so it needs no config plugin or prebuild step. This has not been tested in an Expo project by the maintainers; please report any problems.
138
+
139
+ ### Provider (optional)
140
+
141
+ Wrapping your app in [`KeyboardProvider`](#keyboardprovider) is **optional**. The components and hooks work without it. It only enables development logging through its `debug` prop.
142
+
143
+ ## Basic usage
144
+
145
+ ```tsx
146
+ import React from 'react';
147
+ import { TextInput } from 'react-native';
148
+ import {
149
+ KeyboardAwareScrollView,
150
+ KeyboardProvider,
151
+ } from 'react-native-keyboard-toolkit';
152
+
153
+ export default function ProfileForm(): React.JSX.Element {
154
+ return (
155
+ <KeyboardProvider>
156
+ <KeyboardAwareScrollView
157
+ behavior="smart"
158
+ keyboardShouldPersistTaps="handled"
159
+ keyboardSpacing={16}
160
+ contentContainerStyle={{ padding: 20, gap: 12 }}
161
+ >
162
+ <TextInput placeholder="Name" />
163
+ <TextInput placeholder="Email" keyboardType="email-address" />
164
+ <TextInput placeholder="Notes" multiline />
165
+ </KeyboardAwareScrollView>
166
+ </KeyboardProvider>
167
+ );
168
+ }
169
+ ```
170
+
171
+ All public exports come from the package root:
172
+
173
+ ```ts
174
+ import {
175
+ // components
176
+ KeyboardAwareScrollView,
177
+ KeyboardAwareView,
178
+ KeyboardToolbar,
179
+ KeyboardDismissButton,
180
+ KeyboardProvider,
181
+ // hooks
182
+ useKeyboard,
183
+ useKeyboardState,
184
+ useKeyboardAnimation,
185
+ useFocusedInput,
186
+ // utilities
187
+ calculateKeyboardOverlap,
188
+ calculateScrollOffset,
189
+ focusInput,
190
+ getFocusedInput,
191
+ measureFocusedInput,
192
+ isAndroid,
193
+ isIOS,
194
+ } from 'react-native-keyboard-toolkit';
195
+ ```
196
+
197
+ Deep imports (for example `react-native-keyboard-toolkit/src/...`) are not supported.
198
+
199
+ ## Components
200
+
201
+ ### `KeyboardAwareScrollView`
202
+
203
+ A drop-in replacement for React Native's `ScrollView`. It accepts every `ScrollViewProps` plus the props below, and forwards a `ref` to the underlying `ScrollView` instance.
204
+
205
+ | Prop | Type | Default | Description |
206
+ | ----------------- | -------------------------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------- |
207
+ | `behavior` | `'smart' \| 'scroll' \| 'padding' \| 'resize' \| 'none'` | `'smart'` | Layout strategy. See [Behaviors](#behaviors). |
208
+ | `keyboardSpacing` | `number` | `16` | Space, in points, kept between the focused input / content and the keyboard. Negative values are treated as `0`. |
209
+ | `enabled` | `boolean` | `true` | When `false`, keyboard padding and focus scrolling are disabled and it behaves like a normal `ScrollView`. |
210
+
211
+ `scrollEventThrottle` defaults to `16`. The component wraps `onScroll`, `onScrollBeginDrag`, `onContentSizeChange`, `onLayout`, and `onFocusCapture`; your handlers are still called.
212
+
213
+ ```tsx
214
+ import React from 'react';
215
+ import { Pressable, Text, TextInput } from 'react-native';
216
+ import { KeyboardAwareScrollView } from 'react-native-keyboard-toolkit';
217
+
218
+ export function ContactForm(): React.JSX.Element {
219
+ return (
220
+ <KeyboardAwareScrollView
221
+ behavior="smart"
222
+ keyboardSpacing={20}
223
+ keyboardShouldPersistTaps="handled"
224
+ keyboardDismissMode="interactive"
225
+ contentContainerStyle={{ padding: 20, paddingBottom: 28 }}
226
+ >
227
+ <TextInput placeholder="Name" />
228
+ <TextInput placeholder="Email" keyboardType="email-address" />
229
+ <TextInput placeholder="Message" multiline />
230
+ <Pressable onPress={() => {}}>
231
+ <Text>Submit</Text>
232
+ </Pressable>
233
+ </KeyboardAwareScrollView>
234
+ );
235
+ }
236
+ ```
237
+
238
+ **Using the ref** (for example to call `scrollTo`):
239
+
240
+ ```tsx
241
+ import React, { useRef } from 'react';
242
+ import { Pressable, Text, TextInput } from 'react-native';
243
+ import {
244
+ KeyboardAwareScrollView,
245
+ type KeyboardAwareScrollViewRef,
246
+ } from 'react-native-keyboard-toolkit';
247
+
248
+ export function ScrollToTopForm(): React.JSX.Element {
249
+ const scrollRef = useRef<KeyboardAwareScrollViewRef>(null);
250
+
251
+ return (
252
+ <KeyboardAwareScrollView ref={scrollRef}>
253
+ <TextInput placeholder="Name" />
254
+ <TextInput placeholder="Email" />
255
+ <Pressable
256
+ onPress={() => scrollRef.current?.scrollTo({ y: 0, animated: true })}
257
+ >
258
+ <Text>Back to top</Text>
259
+ </Pressable>
260
+ </KeyboardAwareScrollView>
261
+ );
262
+ }
263
+ ```
264
+
265
+ #### Behaviors
266
+
267
+ | `behavior` | Adds bottom padding for the keyboard | Scrolls the focused input into view | Notes |
268
+ | ----------- | :----------------------------------: | :---------------------------------: | --------------------------------------------------------------------------------------------------------------------------------------------------- |
269
+ | `'smart'` | ✅ | ✅ (only if it is covered) | Default. Currently identical to `'scroll'`. |
270
+ | `'scroll'` | ✅ | ✅ (only if it is covered) | Currently identical to `'smart'`. |
271
+ | `'padding'` | ✅ | ❌ | Extends the scroll range only; you decide what scrolls. |
272
+ | `'resize'` | ❌ | ❌ | On iOS, turns on `ScrollView`'s `automaticallyAdjustKeyboardInsets` (unless you set that prop yourself). On Android, relies on the window resizing. |
273
+ | `'none'` | ❌ | ❌ | No keyboard adjustment. |
274
+
275
+ Details that are useful to know:
276
+
277
+ - The padding added is `max(existing paddingBottom, keyboardOverlap + keyboardSpacing)`. An existing numeric `paddingBottom` is preserved, not added to. A non-numeric (string) `paddingBottom` disables the extra padding.
278
+ - Padding is based on the part of the scroll view's viewport the keyboard actually covers. If Android's `adjustResize` has already shrunk the view, the overlap is `0` and no extra padding is added.
279
+ - Do not stack multiple keyboard-padding wrappers (for example your own `KeyboardAvoidingView`) around the same content unless you want extra spacing.
280
+
281
+ ### `KeyboardAwareView`
282
+
283
+ A `View` that adds bottom padding for the part of itself that the keyboard covers. It accepts every `ViewProps` plus the same three props as the scroll view, and forwards a `ref` to the underlying `View`.
284
+
285
+ | Prop | Type | Default | Description |
286
+ | ----------------- | -------------------------------------------------------- | --------- | ------------------------------------------------------------------------- |
287
+ | `behavior` | `'smart' \| 'scroll' \| 'padding' \| 'resize' \| 'none'` | `'smart'` | `smart`, `scroll`, and `padding` add padding; `resize` and `none` do not. |
288
+ | `keyboardSpacing` | `number` | `16` | Extra space, in points, between the content and the keyboard. |
289
+ | `enabled` | `boolean` | `true` | When `false`, the view never adds padding. |
290
+
291
+ It does **not** scroll anything. Use [`KeyboardAwareScrollView`](#keyboardawarescrollview) for long or scrollable forms. As with the scroll view, a numeric `paddingBottom` in `style` is preserved (the larger value wins) and a string `paddingBottom` disables the adjustment.
292
+
293
+ ```tsx
294
+ import React from 'react';
295
+ import { Pressable, Text, TextInput } from 'react-native';
296
+ import { KeyboardAwareView } from 'react-native-keyboard-toolkit';
297
+
298
+ export function ShortForm(): React.JSX.Element {
299
+ return (
300
+ <KeyboardAwareView
301
+ behavior="smart"
302
+ keyboardSpacing={12}
303
+ style={{ flex: 1, justifyContent: 'space-between', padding: 20 }}
304
+ >
305
+ <TextInput placeholder="One short field" />
306
+ <Pressable onPress={() => {}}>
307
+ <Text>Continue</Text>
308
+ </Pressable>
309
+ </KeyboardAwareView>
310
+ );
311
+ }
312
+ ```
313
+
314
+ ### `KeyboardToolbar`
315
+
316
+ An **in-layout** toolbar (a regular `View`, not a native iOS input accessory view) with _Previous_, _Next_, and _Done_ controls. Pass the refs of your inputs in navigation order.
317
+
318
+ | Prop | Type | Default | Description |
319
+ | -------------------------- | -------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------ |
320
+ | `inputRefs` | `readonly RefObject<TextInputRef \| null>[]` | `[]` | Refs of the inputs to navigate between, in order. Null / unmounted refs are skipped. |
321
+ | `onFocusedInputChange` | `(index: number) => void` | – | Called when the focused input among `inputRefs` changes. `index` is `-1` when none of them is focused. |
322
+ | `onDone` | `() => void` | – | Called after _Done_ has blurred the input and dismissed the keyboard. |
323
+ | `doneText` | `string` | `'Done'` | Label of the built-in _Done_ button. |
324
+ | `alwaysVisible` | `boolean` | `false` | By default the toolbar renders `null` while the keyboard is hidden. Set `true` to keep it rendered. |
325
+ | `children` | `ReactNode` | – | Replaces the built-in Previous / Next / Done controls entirely. |
326
+ | `style`, other `ViewProps` | `ViewProps` | – | Applied to the toolbar container. |
327
+ | `focusedInputIndex` | `number` | – | **Deprecated.** Focus is detected automatically from `inputRefs`. |
328
+
329
+ Behavior notes:
330
+
331
+ - The focused input is detected by calling `isFocused()` on the supplied refs (polled every 100 ms while the toolbar is active), so you do not need to track an index yourself.
332
+ - _Previous_ / _Next_ call `focus()` on the nearest mounted ref in that direction and are disabled when there is none.
333
+ - _Done_ blurs the currently focused native input, calls `Keyboard.dismiss()`, then calls `onDone`.
334
+ - The built-in button labels ("Previous", "Next") and accessibility labels are English only. Pass `children` to render your own controls.
335
+ - Because it is a normal view, place it in your layout above the keyboard. The pattern used by the demo app is to put it at the bottom of the screen, wrapped in a `KeyboardAwareView` so it is lifted above the keyboard.
336
+
337
+ ```tsx
338
+ import React, { useRef } from 'react';
339
+ import { StyleSheet, TextInput, View } from 'react-native';
340
+ import {
341
+ KeyboardAwareScrollView,
342
+ KeyboardAwareView,
343
+ KeyboardToolbar,
344
+ type TextInputRef,
345
+ } from 'react-native-keyboard-toolkit';
346
+
347
+ export function LoginScreen(): React.JSX.Element {
348
+ const emailRef = useRef<TextInputRef>(null);
349
+ const passwordRef = useRef<TextInputRef>(null);
350
+
351
+ return (
352
+ <View style={styles.screen}>
353
+ <KeyboardAwareScrollView
354
+ keyboardShouldPersistTaps="handled"
355
+ contentContainerStyle={styles.content}
356
+ >
357
+ <TextInput
358
+ ref={emailRef}
359
+ placeholder="Email"
360
+ keyboardType="email-address"
361
+ autoCapitalize="none"
362
+ returnKeyType="next"
363
+ onSubmitEditing={() => passwordRef.current?.focus()}
364
+ />
365
+ <TextInput
366
+ ref={passwordRef}
367
+ placeholder="Password"
368
+ secureTextEntry
369
+ returnKeyType="done"
370
+ />
371
+ </KeyboardAwareScrollView>
372
+
373
+ <KeyboardAwareView keyboardSpacing={0}>
374
+ <KeyboardToolbar
375
+ inputRefs={[emailRef, passwordRef]}
376
+ doneText="Hide keyboard"
377
+ onFocusedInputChange={index => {
378
+ // index is -1 when none of the supplied refs is focused.
379
+ console.log('Focused input index:', index);
380
+ }}
381
+ onDone={() => console.log('Keyboard dismissed')}
382
+ />
383
+ </KeyboardAwareView>
384
+ </View>
385
+ );
386
+ }
387
+
388
+ const styles = StyleSheet.create({
389
+ screen: { flex: 1 },
390
+ content: { padding: 20, gap: 12 },
391
+ });
392
+ ```
393
+
394
+ Custom toolbar content:
395
+
396
+ ```tsx
397
+ import React from 'react';
398
+ import { Text } from 'react-native';
399
+ import {
400
+ KeyboardDismissButton,
401
+ KeyboardToolbar,
402
+ } from 'react-native-keyboard-toolkit';
403
+
404
+ export function CustomToolbar(): React.JSX.Element {
405
+ return (
406
+ <KeyboardToolbar style={{ justifyContent: 'flex-end' }}>
407
+ <KeyboardDismissButton>
408
+ <Text>Close keyboard</Text>
409
+ </KeyboardDismissButton>
410
+ </KeyboardToolbar>
411
+ );
412
+ }
413
+ ```
414
+
415
+ ### `KeyboardDismissButton`
416
+
417
+ An accessible `Pressable` (`accessibilityRole="button"`) that dismisses the software keyboard.
418
+
419
+ | Prop | Type | Default | Description |
420
+ | -------------------- | ---------------------- | -------------------- | --------------------------------------------------------------------------------------- |
421
+ | `children` | `ReactNode` | – (required) | Button content. |
422
+ | `onPress` | `() => void` | – | Called first; the keyboard is dismissed afterwards unless `dismissKeyboard` is `false`. |
423
+ | `dismissKeyboard` | `boolean` | `true` | Set `false` to run `onPress` without dismissing the keyboard. |
424
+ | `accessibilityLabel` | `string` | `'Dismiss keyboard'` | Accessibility label. |
425
+ | `style` | `StyleProp<ViewStyle>` | – | Style of the pressable (function styles are not supported). |
426
+
427
+ It also accepts the other `ViewProps`, which are passed through to the underlying `Pressable`.
428
+
429
+ ```tsx
430
+ import React from 'react';
431
+ import { Text } from 'react-native';
432
+ import { KeyboardDismissButton } from 'react-native-keyboard-toolkit';
433
+
434
+ export function DismissControl(): React.JSX.Element {
435
+ return (
436
+ <KeyboardDismissButton
437
+ accessibilityLabel="Close keyboard"
438
+ onPress={() => console.log('Dismiss pressed')}
439
+ style={{ padding: 12 }}
440
+ >
441
+ <Text>Dismiss keyboard</Text>
442
+ </KeyboardDismissButton>
443
+ );
444
+ }
445
+ ```
446
+
447
+ ### `KeyboardProvider`
448
+
449
+ Optional. It renders its children unchanged and, when `debug` is `true`, logs keyboard visibility/height changes with `console.debug('[keyboard-toolkit]', state)` and the scroll calculations of `KeyboardAwareScrollView`. Logging only happens in development builds (`__DEV__`).
450
+
451
+ | Prop | Type | Default | Description |
452
+ | ---------- | ----------- | ------- | ---------------------------------- |
453
+ | `children` | `ReactNode` | – | Your app or screen. |
454
+ | `debug` | `boolean` | `false` | Log keyboard state in development. |
455
+
456
+ ```tsx
457
+ import React from 'react';
458
+ import { KeyboardProvider } from 'react-native-keyboard-toolkit';
459
+ import { Screens } from './Screens';
460
+
461
+ export default function App(): React.JSX.Element {
462
+ return (
463
+ <KeyboardProvider debug={__DEV__}>
464
+ <Screens />
465
+ </KeyboardProvider>
466
+ );
467
+ }
468
+ ```
469
+
470
+ ## Hooks
471
+
472
+ All hooks share a single native subscription and work with or without `KeyboardProvider`.
473
+
474
+ ### `useKeyboardState` / `useKeyboard`
475
+
476
+ `useKeyboard()` is an alias for `useKeyboardState()`. Both return the current `KeyboardState`:
477
+
478
+ ```ts
479
+ interface KeyboardState {
480
+ isVisible: boolean;
481
+ height: number; // keyboard frame height in points; 0 when hidden
482
+ progress: 0 | 1; // 1 when visible, 0 when hidden
483
+ animationDuration: number; // native event duration in ms
484
+ screenY?: number; // top edge of the keyboard on screen, when reported
485
+ }
486
+ ```
487
+
488
+ `height` and `screenY` describe the **keyboard frame**, not the overlap with a particular view; the aware components measure their own overlap. On Android, React Native reports a `duration` of `0`, so `animationDuration` is `0` there.
489
+
490
+ ```tsx
491
+ import React from 'react';
492
+ import { Text, View } from 'react-native';
493
+ import { useKeyboardState } from 'react-native-keyboard-toolkit';
494
+
495
+ export function KeyboardIndicator(): React.JSX.Element {
496
+ const { isVisible, height, screenY } = useKeyboardState();
497
+
498
+ return (
499
+ <View>
500
+ <Text>{isVisible ? `Keyboard: ${height} pt` : 'Keyboard hidden'}</Text>
501
+ {screenY !== undefined && <Text>Top edge: {screenY} pt</Text>}
502
+ </View>
503
+ );
504
+ }
505
+ ```
506
+
507
+ #### Reacting to keyboard events
508
+
509
+ The hooks re-render your component whenever the keyboard state changes, so any effect that depends on the state is a keyboard event handler:
510
+
511
+ ```tsx
512
+ import React, { useEffect } from 'react';
513
+ import { Text } from 'react-native';
514
+ import { useKeyboard } from 'react-native-keyboard-toolkit';
515
+
516
+ export function KeyboardLogger(): React.JSX.Element {
517
+ const keyboard = useKeyboard();
518
+
519
+ useEffect(() => {
520
+ if (keyboard.isVisible) {
521
+ console.log(`Keyboard opened (${keyboard.height} pt)`);
522
+ } else {
523
+ console.log('Keyboard closed');
524
+ }
525
+ }, [keyboard.isVisible, keyboard.height]);
526
+
527
+ return <Text>{keyboard.isVisible ? 'Typing…' : 'Idle'}</Text>;
528
+ }
529
+ ```
530
+
531
+ ### `useKeyboardAnimation`
532
+
533
+ Returns `{ progress: Animated.Value; animationDuration: number }`. `progress` animates between `0` (hidden) and `1` (visible) over the native event duration, and jumps instantly when the system _Reduce Motion_ setting is enabled.
534
+
535
+ The animation uses the native driver, so apply `progress` only to properties the native driver supports (such as `opacity` and `transform`), not to layout properties like `height`.
536
+
537
+ ```tsx
538
+ import React from 'react';
539
+ import { Animated, Text } from 'react-native';
540
+ import { useKeyboardAnimation } from 'react-native-keyboard-toolkit';
541
+
542
+ export function AnimatedKeyboardLabel(): React.JSX.Element {
543
+ const { progress } = useKeyboardAnimation();
544
+
545
+ return (
546
+ <Animated.View
547
+ style={{
548
+ opacity: progress,
549
+ transform: [
550
+ {
551
+ translateY: progress.interpolate({
552
+ inputRange: [0, 1],
553
+ outputRange: [12, 0],
554
+ }),
555
+ },
556
+ ],
557
+ }}
558
+ >
559
+ <Text>Keyboard is visible</Text>
560
+ </Animated.View>
561
+ );
562
+ }
563
+ ```
564
+
565
+ ### `useFocusedInput`
566
+
567
+ Returns `{ focusedInput, refresh }`.
568
+
569
+ - `focusedInput` is `null` or a window-relative `{ x, y, width, height }` layout (`FocusedInputLayout`) of the focused `TextInput`.
570
+ - It measures while the keyboard is visible (on open, on height changes, and every 120 ms), and resets to `null` when the keyboard hides.
571
+ - `refresh()` measures immediately and resolves with the new layout (or `null`). Call it after a layout change if you need an up-to-date value right away.
572
+
573
+ ```tsx
574
+ import React from 'react';
575
+ import { Text } from 'react-native';
576
+ import { useFocusedInput } from 'react-native-keyboard-toolkit';
577
+
578
+ export function FocusedFieldPosition(): React.JSX.Element {
579
+ const { focusedInput, refresh } = useFocusedInput();
580
+
581
+ return (
582
+ <Text onPress={() => void refresh()}>
583
+ {focusedInput
584
+ ? `Focused field top: ${Math.round(focusedInput.y)} pt`
585
+ : 'No measured focused field'}
586
+ </Text>
587
+ );
588
+ }
589
+ ```
590
+
591
+ ## Utility functions
592
+
593
+ These low-level helpers are what the components are built on. Most apps will not need them.
594
+
595
+ | Function | Returns | Description |
596
+ | ---------------------------------------------------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
597
+ | `getFocusedInput()` | `{ measureInWindow } \| null` | The currently focused native `TextInput` (from `TextInput.State`), or `null`. |
598
+ | `measureFocusedInput(input?)` | `Promise<FocusedInputLayout \| null>` | Measures an input (default: the focused one) in window coordinates. |
599
+ | `focusInput(ref)` | `boolean` | Calls `focus()` on a `RefObject<TextInputRef \| null>`; `false` if the ref is empty. |
600
+ | `calculateKeyboardOverlap(viewport, keyboardHeight, keyboardTop?)` | `number` | How many points of `viewport` (`{ y, height }`) the keyboard covers. Uses `keyboardTop` (the keyboard's `screenY`) when given. |
601
+ | `calculateScrollOffset(input, viewport, keyboardHeight, spacing, currentOffset, keyboardTop?)` | `{ offset: number; delta: number }` | The smallest scroll adjustment that brings `input` (`{ y, height }`) into the unobscured part of `viewport`. |
602
+ | `isIOS`, `isAndroid` | `boolean` | Constants for the current `Platform.OS`. |
603
+
604
+ All coordinates passed to the calculation helpers must use the same window-coordinate system (for example values from `measureInWindow`).
605
+
606
+ ```tsx
607
+ import React, { useRef, useState } from 'react';
608
+ import { Text, TextInput, View } from 'react-native';
609
+ import {
610
+ focusInput,
611
+ getFocusedInput,
612
+ isIOS,
613
+ measureFocusedInput,
614
+ type FocusedInputLayout,
615
+ type TextInputRef,
616
+ } from 'react-native-keyboard-toolkit';
617
+
618
+ export function UtilityDemo(): React.JSX.Element {
619
+ const inputRef = useRef<TextInputRef>(null);
620
+ const [layout, setLayout] = useState<FocusedInputLayout | null>(null);
621
+
622
+ return (
623
+ <View>
624
+ <TextInput ref={inputRef} placeholder="Tap a button below" />
625
+ <Text onPress={() => focusInput(inputRef)}>Focus the input</Text>
626
+ <Text
627
+ onPress={async () =>
628
+ setLayout(await measureFocusedInput(getFocusedInput()))
629
+ }
630
+ >
631
+ Measure the focused input ({isIOS ? 'iOS' : 'Android'})
632
+ </Text>
633
+ {layout && (
634
+ <Text>
635
+ y = {Math.round(layout.y)}, height = {Math.round(layout.height)}
636
+ </Text>
637
+ )}
638
+ </View>
639
+ );
640
+ }
641
+ ```
642
+
643
+ ```ts
644
+ import {
645
+ calculateKeyboardOverlap,
646
+ calculateScrollOffset,
647
+ } from 'react-native-keyboard-toolkit';
648
+
649
+ // A 600 pt tall viewport starting at y = 100, with a 300 pt keyboard whose
650
+ // top edge is at y = 500:
651
+ const viewport = { y: 100, height: 600 };
652
+ const overlap = calculateKeyboardOverlap(viewport, 300, 500); // 200
653
+
654
+ // An input at y = 520 (height 40) is hidden behind the keyboard. With 16 pt
655
+ // of spacing and the content currently scrolled to 0, scroll by this much:
656
+ const { delta, offset } = calculateScrollOffset(
657
+ { y: 520, height: 40 },
658
+ viewport,
659
+ 300,
660
+ 16,
661
+ 0,
662
+ 500,
663
+ ); // delta = 76, offset = 76
664
+ ```
665
+
666
+ ## Complete React Native CLI example
667
+
668
+ This walks through a fresh React Native CLI project from start to finish.
669
+
670
+ **1. Create the app and install the library**
671
+
672
+ ```sh
673
+ npx @react-native-community/cli@latest init MyApp
674
+ cd MyApp
675
+ npm install react-native-keyboard-toolkit
676
+ ```
677
+
678
+ **2. Android window mode** – the template's `AndroidManifest.xml` already contains `android:windowSoftInputMode="adjustResize"` on `MainActivity`. Keep it.
679
+
680
+ **3. iOS** – nothing to do for this library. If you have not installed pods yet, run `bundle install && cd ios && bundle exec pod install && cd ..` as usual for a new React Native project.
681
+
682
+ **4. Replace `App.tsx`**
683
+
684
+ ```tsx
685
+ import React, { useRef, useState } from 'react';
686
+ import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
687
+ import {
688
+ KeyboardAwareScrollView,
689
+ KeyboardAwareView,
690
+ KeyboardDismissButton,
691
+ KeyboardProvider,
692
+ KeyboardToolbar,
693
+ useKeyboard,
694
+ type TextInputRef,
695
+ } from 'react-native-keyboard-toolkit';
696
+
697
+ function KeyboardBadge(): React.JSX.Element {
698
+ const { isVisible, height } = useKeyboard();
699
+ return (
700
+ <Text style={styles.badge}>
701
+ {isVisible
702
+ ? `Keyboard open (${Math.round(height)} pt)`
703
+ : 'Keyboard closed'}
704
+ </Text>
705
+ );
706
+ }
707
+
708
+ function SignUpScreen(): React.JSX.Element {
709
+ const nameRef = useRef<TextInputRef>(null);
710
+ const emailRef = useRef<TextInputRef>(null);
711
+ const bioRef = useRef<TextInputRef>(null);
712
+ const [saved, setSaved] = useState(false);
713
+
714
+ return (
715
+ <View style={styles.screen}>
716
+ <KeyboardBadge />
717
+
718
+ <KeyboardAwareScrollView
719
+ behavior="smart"
720
+ keyboardSpacing={20}
721
+ keyboardShouldPersistTaps="handled"
722
+ keyboardDismissMode="interactive"
723
+ contentContainerStyle={styles.content}
724
+ >
725
+ <Text style={styles.title}>Create your profile</Text>
726
+
727
+ <TextInput
728
+ ref={nameRef}
729
+ style={styles.input}
730
+ placeholder="Full name"
731
+ returnKeyType="next"
732
+ onSubmitEditing={() => emailRef.current?.focus()}
733
+ />
734
+ <TextInput
735
+ ref={emailRef}
736
+ style={styles.input}
737
+ placeholder="Email"
738
+ keyboardType="email-address"
739
+ autoCapitalize="none"
740
+ returnKeyType="next"
741
+ onSubmitEditing={() => bioRef.current?.focus()}
742
+ />
743
+ <TextInput
744
+ ref={bioRef}
745
+ style={[styles.input, styles.multiline]}
746
+ placeholder="A little about you"
747
+ multiline
748
+ textAlignVertical="top"
749
+ />
750
+
751
+ <Pressable style={styles.submit} onPress={() => setSaved(true)}>
752
+ <Text style={styles.submitText}>
753
+ {saved ? 'Saved' : 'Save profile'}
754
+ </Text>
755
+ </Pressable>
756
+
757
+ <KeyboardDismissButton style={styles.dismiss}>
758
+ <Text>Dismiss keyboard</Text>
759
+ </KeyboardDismissButton>
760
+ </KeyboardAwareScrollView>
761
+
762
+ <KeyboardAwareView keyboardSpacing={0}>
763
+ <KeyboardToolbar inputRefs={[nameRef, emailRef, bioRef]} />
764
+ </KeyboardAwareView>
765
+ </View>
766
+ );
767
+ }
768
+
769
+ export default function App(): React.JSX.Element {
770
+ return (
771
+ <KeyboardProvider debug={__DEV__}>
772
+ <SignUpScreen />
773
+ </KeyboardProvider>
774
+ );
775
+ }
776
+
777
+ const styles = StyleSheet.create({
778
+ screen: { flex: 1, backgroundColor: '#ffffff', paddingTop: 48 },
779
+ badge: { textAlign: 'center', color: '#475467', paddingVertical: 8 },
780
+ content: { padding: 20, gap: 12 },
781
+ title: { fontSize: 22, fontWeight: '700' },
782
+ input: {
783
+ borderWidth: 1,
784
+ borderColor: '#d0d5dd',
785
+ borderRadius: 8,
786
+ paddingHorizontal: 12,
787
+ paddingVertical: 10,
788
+ fontSize: 16,
789
+ },
790
+ multiline: { minHeight: 96 },
791
+ submit: {
792
+ backgroundColor: '#155eef',
793
+ borderRadius: 8,
794
+ alignItems: 'center',
795
+ paddingVertical: 12,
796
+ },
797
+ submitText: { color: '#ffffff', fontWeight: '600' },
798
+ dismiss: { alignItems: 'center', padding: 12 },
799
+ });
800
+ ```
801
+
802
+ **5. Run it**
803
+
804
+ ```sh
805
+ npx react-native start
806
+ # in another terminal
807
+ npx react-native run-android # or: npx react-native run-ios
808
+ ```
809
+
810
+ Prefer the safe area? Wrap the screen with the component from `react-native-safe-area-context` (installed separately) – the library does not depend on it and works the same inside it:
811
+
812
+ ```tsx
813
+ import React from 'react';
814
+ import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
815
+ import { KeyboardProvider } from 'react-native-keyboard-toolkit';
816
+ import { Screens } from './Screens';
817
+
818
+ export default function App(): React.JSX.Element {
819
+ return (
820
+ <SafeAreaProvider>
821
+ <KeyboardProvider>
822
+ <SafeAreaView style={{ flex: 1 }}>
823
+ <Screens />
824
+ </SafeAreaView>
825
+ </KeyboardProvider>
826
+ </SafeAreaProvider>
827
+ );
828
+ }
829
+ ```
830
+
831
+ ## TypeScript
832
+
833
+ Type declarations ship with the package. The following types are exported from the package root:
834
+
835
+ | Type | Description |
836
+ | ------------------------------ | -------------------------------------------------------------------------------- |
837
+ | `KeyboardBehavior` | `'smart' \| 'scroll' \| 'resize' \| 'padding' \| 'none'` |
838
+ | `KeyboardState` | Shape returned by `useKeyboardState` / `useKeyboard`. |
839
+ | `FocusedInputLayout` | `{ x, y, width, height }` returned by `useFocusedInput` / `measureFocusedInput`. |
840
+ | `TextInputRef` | The `TextInput` instance type, for refs passed to the toolbar. |
841
+ | `KeyboardAwareScrollViewProps` | Props of `KeyboardAwareScrollView`. |
842
+ | `KeyboardAwareScrollViewRef` | Instance type of the `ScrollView` behind `KeyboardAwareScrollView`. |
843
+ | `KeyboardAwareViewProps` | Props of `KeyboardAwareView`. |
844
+ | `KeyboardAwareViewRef` | Instance type of the `View` behind `KeyboardAwareView`. |
845
+ | `KeyboardToolbarProps` | Props of `KeyboardToolbar`. |
846
+ | `KeyboardDismissButtonProps` | Props of `KeyboardDismissButton`. |
847
+ | `KeyboardProviderProps` | Props of `KeyboardProvider`. |
848
+
849
+ ```tsx
850
+ import React from 'react';
851
+ import {
852
+ KeyboardAwareScrollView,
853
+ type FocusedInputLayout,
854
+ type KeyboardAwareScrollViewProps,
855
+ type KeyboardBehavior,
856
+ type KeyboardState,
857
+ type KeyboardToolbarProps,
858
+ } from 'react-native-keyboard-toolkit';
859
+
860
+ const formBehavior: KeyboardBehavior = 'smart';
861
+
862
+ const scrollProps: KeyboardAwareScrollViewProps = {
863
+ behavior: formBehavior,
864
+ keyboardSpacing: 18,
865
+ keyboardShouldPersistTaps: 'handled',
866
+ };
867
+
868
+ const toolbarProps: KeyboardToolbarProps = { doneText: 'Close' };
869
+
870
+ const hiddenKeyboard: KeyboardState = {
871
+ isVisible: false,
872
+ height: 0,
873
+ progress: 0,
874
+ animationDuration: 0,
875
+ };
876
+
877
+ const noLayout: FocusedInputLayout | null = null;
878
+
879
+ export function TypedForm(): React.JSX.Element {
880
+ return (
881
+ <KeyboardAwareScrollView
882
+ {...scrollProps}
883
+ accessibilityLabel={`${toolbarProps.doneText ?? ''} ${
884
+ hiddenKeyboard.height
885
+ } ${noLayout?.x ?? 0}`}
886
+ />
887
+ );
888
+ }
889
+ ```
890
+
891
+ ## API reference
892
+
893
+ ### Components
894
+
895
+ | Export | Props | Ref instance |
896
+ | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
897
+ | `KeyboardAwareScrollView` | `ScrollViewProps` + `behavior`, `keyboardSpacing`, `enabled` | `KeyboardAwareScrollViewRef` |
898
+ | `KeyboardAwareView` | `ViewProps` + `behavior`, `keyboardSpacing`, `enabled` | `KeyboardAwareViewRef` |
899
+ | `KeyboardToolbar` | `ViewProps` + `inputRefs`, `onFocusedInputChange`, `onDone`, `doneText`, `alwaysVisible`, `children`, `focusedInputIndex` (deprecated) | – |
900
+ | `KeyboardDismissButton` | `ViewProps` + `children`, `onPress`, `dismissKeyboard`, `accessibilityLabel`, `style` | – |
901
+ | `KeyboardProvider` | `children`, `debug` | – |
902
+
903
+ ### Hooks
904
+
905
+ | Export | Signature |
906
+ | ---------------------- | -------------------------------------------------------------------------------------------------------- |
907
+ | `useKeyboardState` | `() => KeyboardState` |
908
+ | `useKeyboard` | Alias of `useKeyboardState`. |
909
+ | `useKeyboardAnimation` | `() => { progress: Animated.Value; animationDuration: number }` |
910
+ | `useFocusedInput` | `() => { focusedInput: FocusedInputLayout \| null; refresh: () => Promise<FocusedInputLayout \| null> }` |
911
+
912
+ ### Functions and constants
913
+
914
+ | Export | Signature |
915
+ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
916
+ | `getFocusedInput` | `() => Pick<TextInputRef, 'measureInWindow'> \| null` |
917
+ | `measureFocusedInput` | `(input?: Pick<TextInputRef, 'measureInWindow'> \| null) => Promise<FocusedInputLayout \| null>` |
918
+ | `focusInput` | `(input: RefObject<TextInputRef \| null>) => boolean` |
919
+ | `calculateKeyboardOverlap` | `(viewport: { y: number; height: number }, keyboardHeight: number, keyboardTop?: number) => number` |
920
+ | `calculateScrollOffset` | `(input: { y: number; height: number }, viewport: { y: number; height: number }, keyboardHeight: number, spacing: number, currentOffset: number, keyboardTop?: number) => { offset: number; delta: number }` |
921
+ | `isIOS`, `isAndroid` | `boolean` |
922
+
923
+ ## How it works
924
+
925
+ - **One shared subscription.** The first component or hook that subscribes starts the native keyboard listeners; the last one to unmount stops them. If the keyboard is already open when the first subscriber mounts, the state is initialised from `Keyboard.metrics()`.
926
+ - **iOS** listens to `keyboardWillChangeFrame`. iOS reports a hidden keyboard as a frame moved _below_ the screen (with its height unchanged), so the keyboard is considered visible only while its top edge is above the bottom of the screen.
927
+ - **Android** listens to `keyboardDidShow` and `keyboardDidHide`.
928
+ - **Overlap, not height.** `KeyboardAwareScrollView` and `KeyboardAwareView` measure themselves with `measureInWindow` and intersect that rectangle with the keyboard frame. The reported keyboard height can therefore be non-zero while the measured overlap is `0` (for example when Android `adjustResize` already shrank the view).
929
+ - **Focused input.** The focused field comes from `TextInput.State.currentlyFocusedInput()` and is measured with `measureInWindow`.
930
+ - Floating or split keyboards, third-party keyboards, and modal windows can report different frames. Measurement-based adjustment is best-effort; verify the devices and configurations you ship.
931
+
932
+ ## Troubleshooting
933
+
934
+ **An input is still covered by the keyboard.**
935
+ Use `KeyboardAwareScrollView`, confirm the focused native `TextInput` is _inside_ that scroll view, and check that no ancestor clips its content. `behavior` must be `'smart'` or `'scroll'` and `enabled` must be `true` for focus scrolling.
936
+
937
+ **The focused field does not scroll.**
938
+ Make sure it is a native React Native `TextInput` (not an unmeasurable third-party wrapper) that has completed layout. If a custom wrapper delays focus, call `refresh()` from [`useFocusedInput`](#usefocusedinput) after layout settles.
939
+
940
+ **A bottom button is not reachable.**
941
+ Keep the button _inside_ the aware scroll view so it is part of the scroll range, and set `keyboardShouldPersistTaps="handled"` so the first tap is not swallowed by dismissing the keyboard.
942
+
943
+ **The layout moves twice / there is too much space.**
944
+ Avoid stacking keyboard-padding wrappers (an app-level `KeyboardAvoidingView`, keyboard insets, or multiple aware components) around the same content. When a `KeyboardAwareView` wraps a `KeyboardAwareScrollView`, the outer view lifts the content and the inner scroll view then measures no overlap. If two components still both adjust, disable one with `enabled={false}` or `behavior="none"`.
945
+
946
+ **The keyboard height is non-zero but the view does not add the same amount.**
947
+ This is expected. Components add padding for the _measured overlap_, which is `0` when Android has already resized the window (`adjustResize`).
948
+
949
+ **Android: content moves twice, or nothing moves.**
950
+ Use `android:windowSoftInputMode="adjustResize"` on your activity. Edge-to-edge configurations and some OEM keyboards report coordinates relative to a different window origin; test on your target devices. See [`docs/android.md`](docs/android.md).
951
+
952
+ **Keyboard height is `0` or unexpected.**
953
+ A hardware keyboard may not open a software keyboard. Floating and split keyboards occupy only part of the screen. Heights can vary with navigation bars and OS versions.
954
+
955
+ **`KeyboardToolbar` controls are disabled.**
956
+ Pass refs to _mounted_ `TextInput`s in the intended order. A ref must point at a mounted input for navigation to work. The toolbar is not rendered while the keyboard is hidden unless `alwaysVisible` is set.
957
+
958
+ **The toolbar is not above the keyboard.**
959
+ `KeyboardToolbar` is a normal in-layout view, not a native input accessory. Place it at the bottom of your screen layout, ideally inside a `KeyboardAwareView` as shown in the examples.
960
+
961
+ **The keyboard hides a field inside a modal.**
962
+ Place the aware scroll view inside the modal content and test on the target platform; modal window frames and insets differ from the main screen.
963
+
964
+ **`ref` on `KeyboardAwareScrollView` is `null`.**
965
+ The ref is set after mount. Read `ref.current` in event handlers or effects, not during render.
966
+
967
+ **State does not update.**
968
+ Verify that focusing the input actually opens the _software_ keyboard (not available on some simulators with a connected hardware keyboard: toggle _I/O → Keyboard → Toggle Software Keyboard_ on the iOS simulator).
969
+
970
+ **Using the hooks in Jest tests.**
971
+ The hooks and components render under React Native's Jest preset without extra setup. To simulate keyboard events, spy on `Keyboard.addListener`, capture the listener, and call it with a `KeyboardEvent`-shaped object (see the repository's `tests/` folder for examples).
972
+
973
+ For more, see the platform notes: [`docs/ios.md`](docs/ios.md) and [`docs/android.md`](docs/android.md).
974
+
975
+ ## Example project
976
+
977
+ The repository contains a React Native CLI host app that exercises every component and hook against the local source (`demo/App.tsx`, mounted from `App.tsx`). It includes a keyboard-state panel, behavior pickers, a long form, a compact `KeyboardAwareView` layout, toolbar navigation, and utility buttons.
978
+
979
+ ```sh
980
+ git clone https://github.com/DenishDabhi/react-native-keyboard-toolkit.git
981
+ cd react-native-keyboard-toolkit
982
+ npm ci
983
+
984
+ # iOS only, first time
985
+ bundle install
986
+ cd ios && bundle exec pod install && cd ..
987
+
988
+ npm start # Metro
989
+ npm run android # in another terminal
990
+ npm run ios # in another terminal
991
+ ```
992
+
993
+ The demo uses `react-native-safe-area-context` (a dev dependency of this repository, not of the library). The native projects are still named `MyKeywordHandler` (the module name registered in `app.json`); the name shown on the device is "Keyboard Toolkit Demo".
994
+
995
+ ## Contributing
996
+
997
+ Contributions are welcome. See [`CONTRIBUTING.md`](CONTRIBUTING.md) and the [Code of Conduct](CODE_OF_CONDUCT.md).
998
+
999
+ ```sh
1000
+ npm ci
1001
+ npm run validate # typecheck, lint, prettier check, tests, build, npm pack --dry-run
1002
+ ```
1003
+
1004
+ Individual scripts: `npm run typecheck`, `npm run lint`, `npm run format:check`, `npm test`, `npm run build`. Please add tests for behavior changes and update this README when the public API changes. Report security issues as described in [`SECURITY.md`](SECURITY.md).
1005
+
1006
+ ## License
1007
+
1008
+ [MIT](LICENSE) © Denish Dabhi
1009
+
1010
+ ## Links
1011
+
1012
+ - GitHub: <https://github.com/DenishDabhi/react-native-keyboard-toolkit>
1013
+ - Issues: <https://github.com/DenishDabhi/react-native-keyboard-toolkit/issues>
1014
+ - npm: <https://www.npmjs.com/package/react-native-keyboard-toolkit>
1015
+ - Changelog: [`CHANGELOG.md`](CHANGELOG.md)