Skip to content

Commit d887ec6

Browse files
authored
feat: components (#229)
## 📜 Description Added `KeyboardAvoidingView` based on `useKeyboardHandler` hook. ## 💡 Motivation and Context It was requested functionality and I agree that similar components should be provided out-of-box. As a first component I decided to re-implement `KeyboardAvoidingView`, because the implementation is very easy 👀 Later I'm planning to add more components, so stay tuned 😎 Closes #154 ## 📢 Changelog <!-- High level overview of important changes --> <!-- For example: fixed status bar manipulation; added new types declarations; --> <!-- If your changes don't affect one of platform/language below - then remove this platform/language --> ### JS - added `KeyboardAvoidingView`; - added corresponding example app. ### Docs - added section about `KeyboardAvoidingView`; - added lottie that shows visual difference between implementations. ## 🤔 How Has This Been Tested? Tested on: - Xiaomi Redmi Note 5 Pro; - Pixel 7 Pro; - iPhone 14 Pro (iOS 16.5, iOS 17) <- simulator; - iPhone 6s (iOS 15.6). ## 📸 Screenshots (if appropriate): https://github.com/kirillzyusko/react-native-keyboard-controller/assets/22820318/0fc849d4-4c15-41ab-875f-f49c555256f3 ## 📝 Checklist - [x] CI successfully passed
1 parent dada809 commit d887ec6

File tree

22 files changed

+595
-3
lines changed

22 files changed

+595
-3
lines changed

FabricExample/src/constants/screenNames.ts

+1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ export enum ScreenNames {
1111
INTERACTIVE_KEYBOARD = 'INTERACTIVE_KEYBOARD',
1212
INTERACTIVE_KEYBOARD_IOS = 'INTERACTIVE_KEYBOARD_IOS',
1313
NATIVE_STACK = 'NATIVE_STACK',
14+
KEYBOARD_AVOIDING_VIEW = 'KEYBOARD_AVOIDING_VIEW',
1415
}

FabricExample/src/navigation/ExamplesStack/index.tsx

+10
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import NonUIProps from '../../screens/Examples/NonUIProps';
1313
import InteractiveKeyboard from '../../screens/Examples/InteractiveKeyboard';
1414
import InteractiveKeyboardIOS from '../../screens/Examples/InteractiveKeyboardIOS';
1515
import NativeStack from '../NestedStack';
16+
import KeyboardAvoidingViewExample from '../../screens/Examples/KeyboardAvoidingView';
1617

1718
export type ExamplesStackParamList = {
1819
[ScreenNames.ANIMATED_EXAMPLE]: undefined;
@@ -25,6 +26,7 @@ export type ExamplesStackParamList = {
2526
[ScreenNames.INTERACTIVE_KEYBOARD]: undefined;
2627
[ScreenNames.INTERACTIVE_KEYBOARD_IOS]: undefined;
2728
[ScreenNames.NATIVE_STACK]: undefined;
29+
[ScreenNames.KEYBOARD_AVOIDING_VIEW]: undefined;
2830
};
2931

3032
const Stack = createStackNavigator<ExamplesStackParamList>();
@@ -61,6 +63,9 @@ const options = {
6163
[ScreenNames.NATIVE_STACK]: {
6264
title: 'Native stack',
6365
},
66+
[ScreenNames.KEYBOARD_AVOIDING_VIEW]: {
67+
title: 'KeyboardAvoidingView',
68+
},
6469
};
6570

6671
const ExamplesStack = () => (
@@ -115,6 +120,11 @@ const ExamplesStack = () => (
115120
component={NativeStack}
116121
options={options[ScreenNames.NATIVE_STACK]}
117122
/>
123+
<Stack.Screen
124+
name={ScreenNames.KEYBOARD_AVOIDING_VIEW}
125+
component={KeyboardAvoidingViewExample}
126+
options={options[ScreenNames.KEYBOARD_AVOIDING_VIEW]}
127+
/>
118128
</Stack.Navigator>
119129
);
120130

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React, { useState, useEffect } from 'react';
2+
import {
3+
Platform,
4+
KeyboardAvoidingView as RNKeyboardAvoidingView,
5+
Text,
6+
TextInput,
7+
TouchableOpacity,
8+
View,
9+
} from 'react-native';
10+
import { KeyboardAvoidingView } from 'react-native-keyboard-controller';
11+
import { StackScreenProps } from '@react-navigation/stack';
12+
import { ExamplesStackParamList } from '../../../navigation/ExamplesStack';
13+
import styles from './styles';
14+
15+
type Props = StackScreenProps<ExamplesStackParamList>;
16+
17+
export default function KeyboardAvoidingViewExample({ navigation }: Props) {
18+
const [isPackageImplementation, setPackageImplementation] = useState(true);
19+
20+
useEffect(() => {
21+
navigation.setOptions({
22+
headerRight: () => (
23+
<Text
24+
style={styles.header}
25+
onPress={() => setPackageImplementation((value) => !value)}
26+
>
27+
{isPackageImplementation ? 'Package' : 'RN'}
28+
</Text>
29+
),
30+
});
31+
}, [isPackageImplementation]);
32+
33+
const Container = isPackageImplementation
34+
? KeyboardAvoidingView
35+
: RNKeyboardAvoidingView;
36+
37+
return (
38+
<Container
39+
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
40+
contentContainerStyle={styles.container}
41+
keyboardVerticalOffset={100}
42+
style={styles.content}
43+
>
44+
<View style={styles.inner}>
45+
<Text style={styles.heading}>Header</Text>
46+
<View>
47+
<TextInput placeholder="Username" style={styles.textInput} />
48+
<TextInput placeholder="Password" style={styles.textInput} />
49+
</View>
50+
<TouchableOpacity style={styles.button}>
51+
<Text style={styles.text}>Submit</Text>
52+
</TouchableOpacity>
53+
</View>
54+
</Container>
55+
);
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { StyleSheet } from 'react-native';
2+
3+
export default StyleSheet.create({
4+
header: {
5+
color: 'black',
6+
marginRight: 12,
7+
},
8+
container: {
9+
flex: 1,
10+
},
11+
content: {
12+
flex: 1,
13+
maxHeight: 600,
14+
},
15+
heading: {
16+
color: 'black',
17+
fontSize: 36,
18+
marginBottom: 48,
19+
fontWeight: '600',
20+
},
21+
inner: {
22+
padding: 24,
23+
flex: 1,
24+
justifyContent: 'space-between',
25+
},
26+
textInput: {
27+
height: 45,
28+
borderColor: '#000000',
29+
borderWidth: 1,
30+
borderRadius: 10,
31+
marginBottom: 36,
32+
paddingLeft: 10,
33+
},
34+
button: {
35+
marginTop: 12,
36+
height: 45,
37+
borderRadius: 10,
38+
backgroundColor: 'rgb(40, 64, 147)',
39+
justifyContent: 'center',
40+
alignItems: 'center',
41+
},
42+
text: {
43+
fontWeight: '500',
44+
fontSize: 16,
45+
color: 'white',
46+
},
47+
});

FabricExample/src/screens/Examples/Main/constants.ts

+5
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,9 @@ export const examples: Example[] = [
4444
info: ScreenNames.NATIVE_STACK,
4545
icons: '⚛️',
4646
},
47+
{
48+
title: 'KeyboardAvoidingView',
49+
info: ScreenNames.KEYBOARD_AVOIDING_VIEW,
50+
icons: '😶',
51+
},
4752
];

FabricExample/src/screens/Examples/Main/index.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import type { RootStackParamList } from '../../../navigation/RootStack';
99

1010
const styles = StyleSheet.create({
1111
scrollViewContainer: {
12-
flex: 1,
1312
paddingVertical: 10,
1413
paddingHorizontal: 8,
1514
},
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"label": "📚 Components",
3+
"position": 2
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
---
2+
keywords: [react-native-keyboard-controller, KeyboardAvoidingView, keyboard avoiding view, avoid keyboard, android]
3+
---
4+
5+
# KeyboardAvoidingView
6+
7+
This component will automatically adjust its height, position, or bottom padding based on the keyboard height to remain visible while the virtual keyboard is displayed.
8+
9+
## Why another `KeyboardAvoidingView` is needed?
10+
11+
This new `KeyboardAvoidingView` maintains the familiar React Native [API](https://reactnative.dev/docs/keyboardavoidingview) but ensures consistent behavior and animations on both `iOS` and `Android` platforms. Unlike the existing solution, which primarily caters to `iOS`, this component eliminates platform discrepancies, providing a unified user experience. By reproducing the same animations and behaviors on both platforms, it simplifies cross-platform development, meets user expectations for consistency, and enhances code maintainability. Ultimately, it addresses the need for a reliable and uniform keyboard interaction solution across different devices.
12+
13+
Below is a visual difference between the implementations (the animation is _**4x**_ times slower for better visual perception).
14+
15+
import KeyboardAvoidingViewComparison from '../../../src/components/KeyboardAvoidingViewComparison'
16+
17+
<KeyboardAvoidingViewComparison />
18+
19+
:::info Found a bug? Help the project and report it!
20+
21+
If you found any bugs or inconsistent behavior comparing to `react-native` implementation - don't hesitate to open an [issue](https://github.com/kirillzyusko/react-native-keyboard-controller/issues/new?assignees=kirillzyusko&labels=bug&template=bug_report.md&title=). It will help the project 🙏
22+
23+
Also if there is any well-known problems in original `react-native` implementation which can not be fixed for a long time and they are present in this implementation as well - also feel free to submit an [issue](https://github.com/kirillzyusko/react-native-keyboard-controller/issues/new?assignees=kirillzyusko&labels=bug&template=bug_report.md&title=). Let's make this world better together 😎
24+
25+
:::
26+
27+
## Example
28+
29+
```tsx
30+
import React from 'react';
31+
import {
32+
Text,
33+
TextInput,
34+
TouchableOpacity,
35+
View,
36+
StyleSheet,
37+
} from 'react-native';
38+
import { KeyboardAvoidingView } from 'react-native-keyboard-controller';
39+
40+
export default function KeyboardAvoidingViewExample() {
41+
return (
42+
<KeyboardAvoidingView
43+
behavior={'padding'}
44+
contentContainerStyle={styles.container}
45+
keyboardVerticalOffset={100}
46+
style={styles.content}
47+
>
48+
<View style={styles.inner}>
49+
<Text style={styles.heading}>Header</Text>
50+
<View>
51+
<TextInput placeholder="Username" style={styles.textInput} />
52+
<TextInput placeholder="Password" style={styles.textInput} />
53+
</View>
54+
<TouchableOpacity style={styles.button}>
55+
<Text style={styles.text}>Submit</Text>
56+
</TouchableOpacity>
57+
</View>
58+
</KeyboardAvoidingView>
59+
);
60+
}
61+
62+
const styles = StyleSheet.create({
63+
container: {
64+
flex: 1,
65+
},
66+
content: {
67+
flex: 1,
68+
maxHeight: 600,
69+
},
70+
heading: {
71+
fontSize: 36,
72+
marginBottom: 48,
73+
fontWeight: '600',
74+
},
75+
inner: {
76+
padding: 24,
77+
flex: 1,
78+
justifyContent: 'space-between',
79+
},
80+
textInput: {
81+
height: 45,
82+
borderColor: '#000000',
83+
borderWidth: 1,
84+
borderRadius: 10,
85+
marginBottom: 36,
86+
paddingLeft: 10,
87+
},
88+
button: {
89+
marginTop: 12,
90+
height: 45,
91+
borderRadius: 10,
92+
backgroundColor: 'rgb(40, 64, 147)',
93+
justifyContent: 'center',
94+
alignItems: 'center',
95+
},
96+
text: {
97+
fontWeight: '500',
98+
fontSize: 16,
99+
color: 'white',
100+
},
101+
});
102+
```
103+
104+
## Props
105+
106+
### `behavior`
107+
108+
Specify how to react to the presence of the keyboard. Could be one value of:
109+
110+
- `position`
111+
- `padding`
112+
- `height`
113+
114+
### `contentContainerStyle`
115+
116+
The style of the content container (View) when behavior is `position`.
117+
118+
### `enabled`
119+
120+
A boolean prop indicating whether `KeyboardAvoidingView` is enabled or disabled. Default is `true`.
121+
122+
### `keyboardVerticalOffset`
123+
124+
This is the distance between the top of the user screen and the react native view, may be non-zero in some use cases. Default is `0`.

docs/docs/api/hooks/_category_.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"label": "Hooks",
2+
"label": "🎣 Hooks",
33
"position": 1
44
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import React, { CSSProperties } from 'react';
2+
import Lottie from 'lottie-react';
3+
4+
import before from './kav.lottie.json';
5+
import after from './kav-animated.lottie.json';
6+
7+
const withoutBorders = { border: 'none' };
8+
const lottieView = { paddingLeft: '20%', paddingRight: '20%' };
9+
const label: CSSProperties = {
10+
...withoutBorders,
11+
maxWidth: 400,
12+
textAlign: 'center',
13+
};
14+
const labels = {
15+
...withoutBorders,
16+
backgroundColor: '#00000000',
17+
};
18+
19+
export default function KeyboardAvoidingViewComparison(): JSX.Element {
20+
return (
21+
<table>
22+
<tbody>
23+
<tr style={withoutBorders}>
24+
<td style={withoutBorders}>
25+
<Lottie animationData={before} style={lottieView} loop />
26+
</td>
27+
<td style={withoutBorders}>
28+
<Lottie animationData={after} style={lottieView} loop />
29+
</td>
30+
</tr>
31+
<tr style={labels}>
32+
<td style={label}>
33+
<i>
34+
Default <code>react-native</code> implementation on Android
35+
</i>
36+
</td>
37+
<td style={label}>
38+
<i>
39+
Implementation from <code>react-native-keyboard-controller</code>{' '}
40+
with better animations
41+
</i>
42+
</td>
43+
</tr>
44+
</tbody>
45+
</table>
46+
);
47+
}

docs/src/components/KeyboardAvoidingViewComparison/kav-animated.lottie.json

+1
Large diffs are not rendered by default.

docs/src/components/KeyboardAvoidingViewComparison/kav.lottie.json

+1
Large diffs are not rendered by default.

example/src/constants/screenNames.ts

+1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ export enum ScreenNames {
1111
INTERACTIVE_KEYBOARD = 'INTERACTIVE_KEYBOARD',
1212
INTERACTIVE_KEYBOARD_IOS = 'INTERACTIVE_KEYBOARD_IOS',
1313
NATIVE_STACK = 'NATIVE_STACK',
14+
KEYBOARD_AVOIDING_VIEW = 'KEYBOARD_AVOIDING_VIEW',
1415
}

example/src/navigation/ExamplesStack/index.tsx

+10
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import NonUIProps from '../../screens/Examples/NonUIProps';
1313
import InteractiveKeyboard from '../../screens/Examples/InteractiveKeyboard';
1414
import InteractiveKeyboardIOS from '../../screens/Examples/InteractiveKeyboardIOS';
1515
import NativeStack from '../NestedStack';
16+
import KeyboardAvoidingViewExample from '../../screens/Examples/KeyboardAvoidingView';
1617

1718
export type ExamplesStackParamList = {
1819
[ScreenNames.ANIMATED_EXAMPLE]: undefined;
@@ -25,6 +26,7 @@ export type ExamplesStackParamList = {
2526
[ScreenNames.INTERACTIVE_KEYBOARD]: undefined;
2627
[ScreenNames.INTERACTIVE_KEYBOARD_IOS]: undefined;
2728
[ScreenNames.NATIVE_STACK]: undefined;
29+
[ScreenNames.KEYBOARD_AVOIDING_VIEW]: undefined;
2830
};
2931

3032
const Stack = createStackNavigator<ExamplesStackParamList>();
@@ -61,6 +63,9 @@ const options = {
6163
[ScreenNames.NATIVE_STACK]: {
6264
title: 'Native stack',
6365
},
66+
[ScreenNames.KEYBOARD_AVOIDING_VIEW]: {
67+
title: 'KeyboardAvoidingView',
68+
},
6469
};
6570

6671
const ExamplesStack = () => (
@@ -115,6 +120,11 @@ const ExamplesStack = () => (
115120
component={NativeStack}
116121
options={options[ScreenNames.NATIVE_STACK]}
117122
/>
123+
<Stack.Screen
124+
name={ScreenNames.KEYBOARD_AVOIDING_VIEW}
125+
component={KeyboardAvoidingViewExample}
126+
options={options[ScreenNames.KEYBOARD_AVOIDING_VIEW]}
127+
/>
118128
</Stack.Navigator>
119129
);
120130

0 commit comments

Comments
 (0)