react-native-keyboard-toolkit 1.0.0 → 1.0.1
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 +9 -0
- package/README.md +135 -893
- package/dist/components/KeyboardAwareView.d.ts +1 -1
- package/dist/components/KeyboardAwareView.js +59 -10
- package/dist/types/index.d.ts +1 -1
- package/dist/utils/scrollManager.d.ts +2 -0
- package/dist/utils/scrollManager.js +5 -0
- package/docs/android.md +5 -0
- package/docs/api.md +5 -2
- package/docs/ios.md +5 -0
- package/docs/troubleshooting.md +8 -0
- package/package.json +5 -5
- package/src/components/KeyboardAwareView.tsx +78 -18
- package/src/types/index.ts +1 -1
- package/src/utils/scrollManager.ts +12 -0
package/README.md
CHANGED
|
@@ -2,78 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/react-native-keyboard-toolkit)
|
|
4
4
|
[](LICENSE)
|
|
5
|
-
|
|
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)
|
|
5
|
+

|
|
37
6
|
|
|
38
|
-
|
|
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
|
|
7
|
+
Keyboard-aware scroll view, sticky submit button, input toolbar, and keyboard hooks for React Native on iOS and Android. Keep focused `TextInput` fields visible, move footers above the keyboard, and read keyboard state — all in plain TypeScript, with no native module, linking, or config plugin.
|
|
61
8
|
|
|
62
|
-
|
|
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) |
|
|
9
|
+
## Features
|
|
75
10
|
|
|
76
|
-
|
|
11
|
+
- **`KeyboardAwareScrollView`** – a `ScrollView` that scrolls a covered input into view.
|
|
12
|
+
- **`KeyboardAwareView`** – adds bottom space to normal content, or lifts an absolutely positioned footer above the keyboard (sticky button).
|
|
13
|
+
- **`KeyboardToolbar`** – Previous / Next / Done controls for moving between inputs.
|
|
14
|
+
- **`KeyboardDismissButton`** – a `Pressable` that dismisses the keyboard.
|
|
15
|
+
- **Hooks** – `useKeyboardState` (alias `useKeyboard`), `useKeyboardAnimation`, `useFocusedInput`.
|
|
16
|
+
- **Utilities** – `focusInput`, `getFocusedInput`, `measureFocusedInput`, `calculateKeyboardOverlap`, `calculateScrollOffset`, `isIOS`, `isAndroid`.
|
|
17
|
+
- Written in TypeScript with bundled type declarations.
|
|
77
18
|
|
|
78
19
|
## Installation
|
|
79
20
|
|
|
@@ -81,935 +22,236 @@ These are automated checks. **The library has not been validated on physical dev
|
|
|
81
22
|
npm install react-native-keyboard-toolkit
|
|
82
23
|
# or
|
|
83
24
|
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
25
|
```
|
|
110
26
|
|
|
111
|
-
|
|
27
|
+
**Requirements:** React `>=18.2.0`, React Native `>=0.71.0`. No iOS setup is needed.
|
|
112
28
|
|
|
113
|
-
|
|
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):
|
|
29
|
+
On Android, set `adjustResize` on your activity in `AndroidManifest.xml`:
|
|
122
30
|
|
|
123
31
|
```xml
|
|
124
|
-
<!-- android/app/src/main/AndroidManifest.xml -->
|
|
125
32
|
<activity
|
|
126
33
|
android:name=".MainActivity"
|
|
127
34
|
android:windowSoftInputMode="adjustResize"
|
|
128
35
|
... />
|
|
129
36
|
```
|
|
130
37
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
On iOS there is nothing to configure.
|
|
134
|
-
|
|
135
|
-
### Expo
|
|
38
|
+
## Quick start
|
|
136
39
|
|
|
137
|
-
|
|
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
|
|
40
|
+
### Keyboard-aware form
|
|
144
41
|
|
|
145
42
|
```tsx
|
|
146
43
|
import React from 'react';
|
|
147
44
|
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
45
|
import { KeyboardAwareScrollView } from 'react-native-keyboard-toolkit';
|
|
217
46
|
|
|
218
|
-
export function ContactForm()
|
|
47
|
+
export function ContactForm() {
|
|
219
48
|
return (
|
|
220
49
|
<KeyboardAwareScrollView
|
|
221
|
-
behavior="smart"
|
|
222
|
-
keyboardSpacing={20}
|
|
223
50
|
keyboardShouldPersistTaps="handled"
|
|
224
|
-
|
|
225
|
-
contentContainerStyle={{ padding: 20
|
|
51
|
+
keyboardSpacing={16}
|
|
52
|
+
contentContainerStyle={{ padding: 20 }}
|
|
226
53
|
>
|
|
227
54
|
<TextInput placeholder="Name" />
|
|
228
55
|
<TextInput placeholder="Email" keyboardType="email-address" />
|
|
229
56
|
<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
57
|
</KeyboardAwareScrollView>
|
|
261
58
|
);
|
|
262
59
|
}
|
|
263
60
|
```
|
|
264
61
|
|
|
265
|
-
|
|
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. |
|
|
62
|
+
### Sticky submit button
|
|
290
63
|
|
|
291
|
-
|
|
64
|
+
Put a `KeyboardAwareView` with `position: 'absolute'` next to the scroll view. It animates above the keyboard and returns to its original place when the keyboard closes.
|
|
292
65
|
|
|
293
66
|
```tsx
|
|
294
67
|
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';
|
|
68
|
+
import { Pressable, Text, TextInput, View } from 'react-native';
|
|
340
69
|
import {
|
|
341
70
|
KeyboardAwareScrollView,
|
|
342
71
|
KeyboardAwareView,
|
|
343
|
-
KeyboardToolbar,
|
|
344
|
-
type TextInputRef,
|
|
345
72
|
} from 'react-native-keyboard-toolkit';
|
|
346
73
|
|
|
347
|
-
export function
|
|
348
|
-
const emailRef = useRef<TextInputRef>(null);
|
|
349
|
-
const passwordRef = useRef<TextInputRef>(null);
|
|
350
|
-
|
|
74
|
+
export function StickyForm() {
|
|
351
75
|
return (
|
|
352
|
-
<View style={
|
|
76
|
+
<View style={{ flex: 1 }}>
|
|
353
77
|
<KeyboardAwareScrollView
|
|
354
78
|
keyboardShouldPersistTaps="handled"
|
|
355
|
-
|
|
79
|
+
keyboardSpacing={112} // leave room for the footer
|
|
80
|
+
contentContainerStyle={{ padding: 20, paddingBottom: 96 }}
|
|
356
81
|
>
|
|
357
|
-
<TextInput
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
/>
|
|
82
|
+
<TextInput placeholder="Name" />
|
|
83
|
+
<TextInput placeholder="Email" keyboardType="email-address" />
|
|
84
|
+
<TextInput placeholder="Company" />
|
|
85
|
+
<TextInput placeholder="Message" multiline />
|
|
371
86
|
</KeyboardAwareScrollView>
|
|
372
87
|
|
|
373
|
-
<KeyboardAwareView
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
88
|
+
<KeyboardAwareView
|
|
89
|
+
keyboardSpacing={12}
|
|
90
|
+
style={{
|
|
91
|
+
position: 'absolute',
|
|
92
|
+
left: 0,
|
|
93
|
+
right: 0,
|
|
94
|
+
bottom: 0,
|
|
95
|
+
padding: 16,
|
|
96
|
+
backgroundColor: 'white',
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
<Pressable onPress={() => console.log('Submit')}>
|
|
100
|
+
<Text>Submit</Text>
|
|
101
|
+
</Pressable>
|
|
383
102
|
</KeyboardAwareView>
|
|
384
103
|
</View>
|
|
385
104
|
);
|
|
386
105
|
}
|
|
387
|
-
|
|
388
|
-
const styles = StyleSheet.create({
|
|
389
|
-
screen: { flex: 1 },
|
|
390
|
-
content: { padding: 20, gap: 12 },
|
|
391
|
-
});
|
|
392
106
|
```
|
|
393
107
|
|
|
394
|
-
|
|
108
|
+
Set the scroll view's `keyboardSpacing` to roughly your footer height so focused inputs are not hidden behind it.
|
|
109
|
+
|
|
110
|
+
### Previous / Next / Done toolbar
|
|
111
|
+
|
|
112
|
+
Pass your input refs in navigation order. The toolbar renders only while the keyboard is visible.
|
|
395
113
|
|
|
396
114
|
```tsx
|
|
397
|
-
import React from 'react';
|
|
398
|
-
import {
|
|
115
|
+
import React, { useRef } from 'react';
|
|
116
|
+
import { TextInput, View } from 'react-native';
|
|
399
117
|
import {
|
|
400
|
-
|
|
118
|
+
KeyboardAwareScrollView,
|
|
401
119
|
KeyboardToolbar,
|
|
120
|
+
type TextInputRef,
|
|
402
121
|
} from 'react-native-keyboard-toolkit';
|
|
403
122
|
|
|
404
|
-
export function
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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`.
|
|
123
|
+
export function SignUpForm() {
|
|
124
|
+
const name = useRef<TextInputRef>(null);
|
|
125
|
+
const email = useRef<TextInputRef>(null);
|
|
126
|
+
const password = useRef<TextInputRef>(null);
|
|
428
127
|
|
|
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
128
|
return (
|
|
436
|
-
<
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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();
|
|
129
|
+
<View style={{ flex: 1 }}>
|
|
130
|
+
<KeyboardAwareScrollView keyboardShouldPersistTaps="handled">
|
|
131
|
+
<TextInput ref={name} placeholder="Name" />
|
|
132
|
+
<TextInput ref={email} placeholder="Email" />
|
|
133
|
+
<TextInput ref={password} placeholder="Password" secureTextEntry />
|
|
134
|
+
</KeyboardAwareScrollView>
|
|
497
135
|
|
|
498
|
-
|
|
499
|
-
<View>
|
|
500
|
-
<Text>{isVisible ? `Keyboard: ${height} pt` : 'Keyboard hidden'}</Text>
|
|
501
|
-
{screenY !== undefined && <Text>Top edge: {screenY} pt</Text>}
|
|
136
|
+
<KeyboardToolbar inputRefs={[name, email, password]} />
|
|
502
137
|
</View>
|
|
503
138
|
);
|
|
504
139
|
}
|
|
505
140
|
```
|
|
506
141
|
|
|
507
|
-
|
|
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:
|
|
142
|
+
## API
|
|
510
143
|
|
|
511
|
-
|
|
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
|
-
```
|
|
144
|
+
### `KeyboardAwareScrollView`
|
|
530
145
|
|
|
531
|
-
|
|
146
|
+
Accepts all `ScrollView` props, forwards its ref to the `ScrollView`, and adds:
|
|
532
147
|
|
|
533
|
-
|
|
148
|
+
| Prop | Type | Default | Description |
|
|
149
|
+
| ----------------- | -------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------ |
|
|
150
|
+
| `behavior` | `'smart' \| 'scroll' \| 'resize' \| 'padding' \| 'none'` | `'smart'` | See [Behaviors](#behaviors). |
|
|
151
|
+
| `keyboardSpacing` | `number` | `16` | Gap (points) between the focused input and the keyboard. Negative values become `0`. |
|
|
152
|
+
| `enabled` | `boolean` | `true` | Turns keyboard padding and focus scrolling on or off. |
|
|
534
153
|
|
|
535
|
-
|
|
154
|
+
### `KeyboardAwareView`
|
|
536
155
|
|
|
537
|
-
|
|
538
|
-
import React from 'react';
|
|
539
|
-
import { Animated, Text } from 'react-native';
|
|
540
|
-
import { useKeyboardAnimation } from 'react-native-keyboard-toolkit';
|
|
156
|
+
Accepts all `View` props, forwards its ref to the native view, and supports `behavior`, `keyboardSpacing` (default `16`), and `enabled`.
|
|
541
157
|
|
|
542
|
-
|
|
543
|
-
|
|
158
|
+
- **Normal layout:** adds bottom padding equal to the part of the view covered by the keyboard.
|
|
159
|
+
- **`position: 'absolute'`:** animates the view up so it clears the keyboard plus `keyboardSpacing`.
|
|
160
|
+
- It does not scroll to reveal hidden children.
|
|
544
161
|
|
|
545
|
-
|
|
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
|
-
```
|
|
162
|
+
### `KeyboardToolbar`
|
|
564
163
|
|
|
565
|
-
|
|
164
|
+
Accepts all `View` props plus:
|
|
566
165
|
|
|
567
|
-
|
|
166
|
+
| Prop | Type | Default | Description |
|
|
167
|
+
| ---------------------- | ----------------------------------- | -------- | ---------------------------------------------------------------------------------------------------- |
|
|
168
|
+
| `inputRefs` | `RefObject<TextInputRef \| null>[]` | `[]` | Inputs in navigation order. The focused input is detected automatically; unmounted refs are skipped. |
|
|
169
|
+
| `onFocusedInputChange` | `(index: number) => void` | – | Called when the focused input changes (`-1` = none of `inputRefs`). |
|
|
170
|
+
| `onDone` | `() => void` | – | Called after Done dismisses the keyboard. |
|
|
171
|
+
| `doneText` | `string` | `'Done'` | Label of the Done button. |
|
|
172
|
+
| `alwaysVisible` | `boolean` | `false` | Show the toolbar even when the keyboard is hidden. |
|
|
173
|
+
| `children` | `ReactNode` | – | Replaces the built-in Previous / Next / Done controls. |
|
|
568
174
|
|
|
569
|
-
|
|
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.
|
|
175
|
+
### `KeyboardDismissButton`
|
|
572
176
|
|
|
573
|
-
|
|
574
|
-
import React from 'react';
|
|
575
|
-
import { Text } from 'react-native';
|
|
576
|
-
import { useFocusedInput } from 'react-native-keyboard-toolkit';
|
|
177
|
+
A `Pressable` (accepts `View` props) that dismisses the keyboard when pressed.
|
|
577
178
|
|
|
578
|
-
|
|
579
|
-
|
|
179
|
+
| Prop | Type | Default | Description |
|
|
180
|
+
| -------------------- | ------------ | -------------------- | ----------------------------------------- |
|
|
181
|
+
| `children` | `ReactNode` | required | Button content. |
|
|
182
|
+
| `onPress` | `() => void` | – | Runs before the keyboard is dismissed. |
|
|
183
|
+
| `dismissKeyboard` | `boolean` | `true` | Set `false` to skip `Keyboard.dismiss()`. |
|
|
184
|
+
| `accessibilityLabel` | `string` | `'Dismiss keyboard'` | Accessibility label. |
|
|
580
185
|
|
|
581
|
-
|
|
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
|
-
```
|
|
186
|
+
### `KeyboardProvider`
|
|
590
187
|
|
|
591
|
-
|
|
188
|
+
Optional. Wrap your app in `<KeyboardProvider debug>` to log keyboard changes in development builds. Components and hooks work without it.
|
|
592
189
|
|
|
593
|
-
|
|
190
|
+
### Behaviors
|
|
594
191
|
|
|
595
|
-
|
|
|
596
|
-
|
|
|
597
|
-
| `
|
|
598
|
-
| `
|
|
599
|
-
| `
|
|
600
|
-
| `
|
|
601
|
-
| `
|
|
602
|
-
| `isIOS`, `isAndroid` | `boolean` | Constants for the current `Platform.OS`. |
|
|
192
|
+
| Value | `KeyboardAwareScrollView` | `KeyboardAwareView` |
|
|
193
|
+
| ----------- | ----------------------------------------------------------------------- | -------------------------------------- |
|
|
194
|
+
| `'smart'` | Adds scroll space and scrolls only when the focused input is covered | Adjusts for keyboard overlap (default) |
|
|
195
|
+
| `'scroll'` | Same as `'smart'` | Same as `'smart'` |
|
|
196
|
+
| `'padding'` | Adds scroll space only, no focus scrolling | Same as `'smart'` |
|
|
197
|
+
| `'resize'` | Leaves resizing to the platform (uses automatic keyboard insets on iOS) | No adjustment |
|
|
198
|
+
| `'none'` | No adjustment | No adjustment |
|
|
603
199
|
|
|
604
|
-
|
|
200
|
+
### Hooks
|
|
605
201
|
|
|
606
202
|
```tsx
|
|
607
|
-
import React, { useRef, useState } from 'react';
|
|
608
|
-
import { Text, TextInput, View } from 'react-native';
|
|
609
203
|
import {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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,
|
|
204
|
+
useKeyboardState,
|
|
205
|
+
useKeyboardAnimation,
|
|
206
|
+
useFocusedInput,
|
|
647
207
|
} from 'react-native-keyboard-toolkit';
|
|
648
208
|
|
|
649
|
-
|
|
650
|
-
//
|
|
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
|
|
209
|
+
const { isVisible, height, progress, animationDuration, screenY } =
|
|
210
|
+
useKeyboardState(); // `useKeyboard()` is an alias
|
|
667
211
|
|
|
668
|
-
|
|
212
|
+
const { progress: anim, animationDuration: ms } = useKeyboardAnimation();
|
|
213
|
+
// `anim` is an Animated.Value (0 → 1); updates instantly if Reduce Motion is on
|
|
669
214
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
```sh
|
|
673
|
-
npx @react-native-community/cli@latest init MyApp
|
|
674
|
-
cd MyApp
|
|
675
|
-
npm install react-native-keyboard-toolkit
|
|
215
|
+
const { focusedInput, refresh } = useFocusedInput();
|
|
216
|
+
// focusedInput: { x, y, width, height } in window coordinates, or null
|
|
676
217
|
```
|
|
677
218
|
|
|
678
|
-
|
|
219
|
+
| Hook | Returns |
|
|
220
|
+
| ------------------------ | ----------------------------------------------------------------------------------------- |
|
|
221
|
+
| `useKeyboardState()` | `{ isVisible, height, progress (0 \| 1), animationDuration, screenY? }` |
|
|
222
|
+
| `useKeyboardAnimation()` | `{ progress: Animated.Value, animationDuration }` |
|
|
223
|
+
| `useFocusedInput()` | `{ focusedInput, refresh }` – the focused input's layout, and a function to re-measure it |
|
|
679
224
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
**4. Replace `App.tsx`**
|
|
225
|
+
Example: animate a view with the keyboard.
|
|
683
226
|
|
|
684
227
|
```tsx
|
|
685
|
-
import
|
|
686
|
-
import {
|
|
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;
|
|
228
|
+
import { Animated } from 'react-native';
|
|
229
|
+
import { useKeyboardAnimation } from 'react-native-keyboard-toolkit';
|
|
878
230
|
|
|
879
|
-
|
|
231
|
+
function FadeWithKeyboard({ children }: { children: React.ReactNode }) {
|
|
232
|
+
const { progress } = useKeyboardAnimation();
|
|
880
233
|
return (
|
|
881
|
-
<
|
|
882
|
-
{...scrollProps}
|
|
883
|
-
accessibilityLabel={`${toolbarProps.doneText ?? ''} ${
|
|
884
|
-
hiddenKeyboard.height
|
|
885
|
-
} ${noLayout?.x ?? 0}`}
|
|
886
|
-
/>
|
|
234
|
+
<Animated.View style={{ opacity: progress }}>{children}</Animated.View>
|
|
887
235
|
);
|
|
888
236
|
}
|
|
889
237
|
```
|
|
890
238
|
|
|
891
|
-
|
|
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.
|
|
239
|
+
### Utilities and types
|
|
957
240
|
|
|
958
|
-
|
|
959
|
-
`
|
|
241
|
+
- `focusInput(ref)` – focuses a `TextInput` ref; returns `false` if the ref is empty.
|
|
242
|
+
- `getFocusedInput()` / `measureFocusedInput(input?)` – get or measure the focused input.
|
|
243
|
+
- `calculateKeyboardOverlap(viewport, keyboardHeight, keyboardTop?)` and `calculateScrollOffset(input, viewport, keyboardHeight, spacing, currentOffset, keyboardTop?)` – the layout math used internally.
|
|
244
|
+
- `isIOS`, `isAndroid` – platform flags.
|
|
245
|
+
- Types: `KeyboardState`, `KeyboardBehavior`, `TextInputRef`, `FocusedInputLayout`, `KeyboardAwareScrollViewProps`, `KeyboardAwareScrollViewRef`, `KeyboardAwareViewProps`, `KeyboardAwareViewRef`, `KeyboardToolbarProps`, `KeyboardDismissButtonProps`, `KeyboardProviderProps`.
|
|
960
246
|
|
|
961
|
-
|
|
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.
|
|
247
|
+
## Tips
|
|
963
248
|
|
|
964
|
-
|
|
965
|
-
|
|
249
|
+
- Use React Native `TextInput` fields inside `KeyboardAwareScrollView`. Custom input wrappers must forward refs and support native measurement.
|
|
250
|
+
- Use only one keyboard-spacing component per region to avoid adding space twice.
|
|
251
|
+
- Keyboard coordinates can differ across devices, edge-to-edge modes, and modals — test on the devices you support.
|
|
966
252
|
|
|
967
|
-
|
|
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).
|
|
253
|
+
More guides: [Installation](docs/installation.md) · [API](docs/api.md) · [Android](docs/android.md) · [iOS](docs/ios.md) · [Troubleshooting](docs/troubleshooting.md)
|
|
1005
254
|
|
|
1006
255
|
## License
|
|
1007
256
|
|
|
1008
257
|
[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)
|