react-native-keyboard-toolkit 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +1014 -2
- package/dist/components/KeyboardAwareScrollView.d.ts +6 -0
- package/dist/components/KeyboardAwareScrollView.js +137 -0
- package/dist/components/KeyboardAwareView.d.ts +6 -0
- package/dist/components/KeyboardAwareView.js +63 -0
- package/dist/components/KeyboardDismissButton.d.ts +5 -0
- package/dist/components/KeyboardDismissButton.js +16 -0
- package/dist/components/KeyboardToolbar.d.ts +5 -0
- package/dist/components/KeyboardToolbar.js +109 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.js +11 -0
- package/dist/context/KeyboardContext.d.ts +6 -0
- package/dist/context/KeyboardContext.js +27 -0
- package/dist/context/index.d.ts +1 -0
- package/dist/context/index.js +5 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.js +11 -0
- package/dist/hooks/useFocusedInput.d.ts +6 -0
- package/dist/hooks/useFocusedInput.js +42 -0
- package/dist/hooks/useKeyboard.d.ts +1 -0
- package/dist/hooks/useKeyboard.js +5 -0
- package/dist/hooks/useKeyboardAnimation.d.ts +6 -0
- package/dist/hooks/useKeyboardAnimation.js +43 -0
- package/dist/hooks/useKeyboardState.d.ts +2 -0
- package/dist/hooks/useKeyboardState.js +9 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +23 -0
- package/dist/types/index.d.ts +69 -0
- package/dist/types/index.js +2 -0
- package/dist/utils/constants.d.ts +2 -0
- package/dist/utils/constants.js +5 -0
- package/dist/utils/focusManager.d.ts +10 -0
- package/dist/utils/focusManager.js +34 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.js +13 -0
- package/dist/utils/keyboardEvents.d.ts +7 -0
- package/dist/utils/keyboardEvents.js +88 -0
- package/dist/utils/platform.d.ts +11 -0
- package/dist/utils/platform.js +19 -0
- package/dist/utils/scrollManager.d.ts +19 -0
- package/dist/utils/scrollManager.js +32 -0
- package/docs/android.md +26 -0
- package/docs/api.md +75 -0
- package/docs/assets/demo-preview.svg +24 -0
- package/docs/installation.md +22 -0
- package/docs/ios.md +22 -0
- package/docs/troubleshooting.md +36 -0
- package/package.json +90 -4
- package/src/components/KeyboardAwareScrollView.tsx +240 -0
- package/src/components/KeyboardAwareView.tsx +112 -0
- package/src/components/KeyboardDismissButton.tsx +32 -0
- package/src/components/KeyboardToolbar.tsx +200 -0
- package/src/components/index.ts +4 -0
- package/src/context/KeyboardContext.tsx +35 -0
- package/src/context/index.ts +1 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useFocusedInput.ts +52 -0
- package/src/hooks/useKeyboard.ts +1 -0
- package/src/hooks/useKeyboardAnimation.ts +50 -0
- package/src/hooks/useKeyboardState.ts +14 -0
- package/src/index.ts +35 -0
- package/src/types/index.ts +101 -0
- package/src/utils/constants.ts +2 -0
- package/src/utils/focusManager.ts +40 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/keyboardEvents.ts +110 -0
- package/src/utils/platform.ts +23 -0
- package/src/utils/scrollManager.ts +59 -0
package/README.md
CHANGED
|
@@ -1,3 +1,1015 @@
|
|
|
1
|
-
#
|
|
1
|
+
# react-native-keyboard-toolkit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/react-native-keyboard-toolkit)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](#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)
|