Skip to content

Commit

Permalink
feat: KeyboardStickyView + example with KeyboardAwareScrollView (#…
Browse files Browse the repository at this point in the history
…245)

Co-authored-by: kirillzyusko <[email protected]>
  • Loading branch information
MarceloPrado and kirillzyusko authored Oct 29, 2023
1 parent 4e65942 commit 468d750
Show file tree
Hide file tree
Showing 27 changed files with 405 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import Reanimated, {
} from 'react-native-reanimated';
import { useSmoothKeyboardHandler } from './useSmoothKeyboardHandler';

const BOTTOM_OFFSET = 50;
type KeyboardAwareScrollViewProps = {
bottomOffset?: number;
} & ScrollViewProps;

/**
* Everything begins from `onStart` handler. This handler is called every time,
Expand Down Expand Up @@ -53,8 +55,9 @@ const BOTTOM_OFFSET = 50;
* +============================+ +============================+ +=====================================+
*
*/
const KeyboardAwareScrollView: FC<ScrollViewProps> = ({
const KeyboardAwareScrollView: FC<KeyboardAwareScrollViewProps> = ({
children,
bottomOffset = 0,
...rest
}) => {
const scrollViewAnimatedRef = useAnimatedRef<Reanimated.ScrollView>();
Expand Down Expand Up @@ -85,11 +88,11 @@ const KeyboardAwareScrollView: FC<ScrollViewProps> = ({
const visibleRect = height - keyboardHeight.value;
const point = (layout.value?.layout.absoluteY || 0) + (layout.value?.layout.height || 0);

if (visibleRect - point <= BOTTOM_OFFSET) {
if (visibleRect - point <= bottomOffset) {
const interpolatedScrollTo = interpolate(
e,
[initialKeyboardSize.value, keyboardHeight.value],
[0, keyboardHeight.value - (height - point) + BOTTOM_OFFSET]
[0, keyboardHeight.value - (height - point) + bottomOffset]
);
const targetScrollY =
Math.max(interpolatedScrollTo, 0) + scrollPosition.value;
Expand All @@ -99,7 +102,7 @@ const KeyboardAwareScrollView: FC<ScrollViewProps> = ({
}

return 0;
}, []);
}, [bottomOffset]);

useSmoothKeyboardHandler(
{
Expand Down Expand Up @@ -158,7 +161,7 @@ const KeyboardAwareScrollView: FC<ScrollViewProps> = ({
scrollPosition.value = position.value;
},
},
[height]
[height, maybeScroll]
);

useAnimatedReaction(() => input.value, (current, previous) => {
Expand Down
1 change: 1 addition & 0 deletions FabricExample/src/components/AwareScrollView/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./KeyboardAwareScrollView";
1 change: 1 addition & 0 deletions FabricExample/src/constants/screenNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export enum ScreenNames {
REANIMATED_CHAT = 'REANIMATED_CHAT',
EVENTS = 'EVENTS',
AWARE_SCROLL_VIEW = 'AWARE_SCROLL_VIEW',
AWARE_SCROLL_VIEW_STICKY_FOOTER = 'AWARE_SCROLL_VIEW_STICKY_FOOTER',
STATUS_BAR = 'STATUS_BAR',
LOTTIE = 'LOTTIE',
EXAMPLES_STACK = 'EXAMPLES_STACK',
Expand Down
10 changes: 10 additions & 0 deletions FabricExample/src/navigation/ExamplesStack/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ import InteractiveKeyboardIOS from '../../screens/Examples/InteractiveKeyboardIO
import NativeStack from '../NestedStack';
import KeyboardAvoidingViewExample from '../../screens/Examples/KeyboardAvoidingView';
import EnabledDisabled from '../../screens/Examples/EnabledDisabled';
import AwareScrollViewStickyFooter from '../../screens/Examples/AwareScrollViewStickyFooter';

export type ExamplesStackParamList = {
[ScreenNames.ANIMATED_EXAMPLE]: undefined;
[ScreenNames.REANIMATED_CHAT]: undefined;
[ScreenNames.EVENTS]: undefined;
[ScreenNames.AWARE_SCROLL_VIEW]: undefined;
[ScreenNames.AWARE_SCROLL_VIEW_STICKY_FOOTER]: undefined;
[ScreenNames.STATUS_BAR]: undefined;
[ScreenNames.LOTTIE]: undefined;
[ScreenNames.NON_UI_PROPS]: undefined;
Expand All @@ -46,6 +48,9 @@ const options = {
[ScreenNames.AWARE_SCROLL_VIEW]: {
title: 'Aware scroll view',
},
[ScreenNames.AWARE_SCROLL_VIEW_STICKY_FOOTER]: {
title: 'Aware scroll view sticky footer',
},
[ScreenNames.STATUS_BAR]: {
headerShown: false,
title: 'Status bar manipulation',
Expand Down Expand Up @@ -95,6 +100,11 @@ const ExamplesStack = () => (
component={AwareScrollView}
options={options[ScreenNames.AWARE_SCROLL_VIEW]}
/>
<Stack.Screen
name={ScreenNames.AWARE_SCROLL_VIEW_STICKY_FOOTER}
component={AwareScrollViewStickyFooter}
options={options[ScreenNames.AWARE_SCROLL_VIEW_STICKY_FOOTER]}
/>
<Stack.Screen
name={ScreenNames.STATUS_BAR}
component={StatusBar}
Expand Down
6 changes: 3 additions & 3 deletions FabricExample/src/screens/Examples/AwareScrollView/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import React from 'react';
import { useResizeMode } from 'react-native-keyboard-controller';

import KeyboardAwareScrollView from './KeyboardAwareScrollView';
import TextInput from './TextInput';
import KeyboardAwareScrollView from '../../../components/AwareScrollView';
import TextInput from '../../../components/TextInput';
import { styles } from './styles';

export default function AwareScrollView() {
useResizeMode();

return (
<KeyboardAwareScrollView style={styles.container} contentContainerStyle={styles.content}>
<KeyboardAwareScrollView bottomOffset={50} style={styles.container} contentContainerStyle={styles.content}>
{new Array(10).fill(0).map((_, i) => (
<TextInput
key={i}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { LayoutChangeEvent, View, Text, Button } from 'react-native';
import { StackScreenProps } from '@react-navigation/stack';
import { useResizeMode, KeyboardStickyView } from 'react-native-keyboard-controller';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import { ExamplesStackParamList } from '../../../navigation/ExamplesStack';
import KeyboardAwareScrollView from '../../../components/AwareScrollView';
import TextInput from '../../../components/TextInput';
import { styles } from './styles';

type Props = StackScreenProps<ExamplesStackParamList>;

const variants = ['v1', 'v2', 'v3'] as const;
type Variant = typeof variants[number];

export default function AwareScrollViewStickyFooter({ navigation }: Props) {
useResizeMode();

const { bottom } = useSafeAreaInsets();
const [footerHeight, setFooterHeight] = useState(0);
const [variant, setVariant] = useState<Variant>("v1");

const handleLayout = useCallback((evt: LayoutChangeEvent) => {
setFooterHeight(evt.nativeEvent.layout.height);
}, []);
const offset = useMemo(() => ({closed: 0, opened: variant === "v1" ? 0 : bottom }), [bottom, variant]);
const offsetV3 = useMemo(() => ({closed: -50, opened: bottom - 25}), [bottom]);

useEffect(() => {
navigation.setOptions({
headerRight: () => (
<Text
style={styles.header}
onPress={() => {
const index = variants.indexOf(variant);
setVariant(
variants[index === variants.length - 1 ? 0 : index + 1]
);
}}
>
{variant}
</Text>
),
});
}, [variant]);

const v1v2 = variant === "v1" || variant === "v2";

return (
<View style={[styles.pageContainer, { paddingBottom: variant === "v1" ? 0 : bottom }]}>
<KeyboardAwareScrollView
style={styles.container}
contentContainerStyle={styles.content}
bottomOffset={(v1v2 ? footerHeight : 0) + 50}
keyboardShouldPersistTaps="handled"
>
{new Array(10).fill(0).map((_, i) => (
<TextInput
key={i}
placeholder={`TextInput#${i}`}
keyboardType={i % 2 === 0 ? 'numeric' : 'default'}
/>
))}
</KeyboardAwareScrollView>
{v1v2 && (
<KeyboardStickyView offset={offset}>
<View onLayout={handleLayout} style={styles.footer}>
<Text style={styles.footerText}>A mocked sticky footer</Text>
<Button title="Click me" />
</View>
</KeyboardStickyView>
)}
{variant === "v3" && (
<KeyboardStickyView offset={offsetV3}>
<View style={styles.circle} />
</KeyboardStickyView>
)}
</View>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { StyleSheet } from 'react-native';

export const styles = StyleSheet.create({
container: {
flexGrow: 1,
paddingHorizontal: 16,
},
content: {
paddingTop: 50,
},
pageContainer: {
flex: 1,
},
footer: {
backgroundColor: 'green',
alignItems: 'center',
justifyContent: 'center',
padding: 20,
width: '100%',
gap: 10,
},
footerText: {
color: 'white',
fontWeight: 'bold',
},
circle: {
position: 'absolute',
bottom: 0,
right: 30,
justifyContent: "flex-end",
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: "#002099",
},
header: {
color: 'black',
paddingRight: 12,
},
});
5 changes: 5 additions & 0 deletions FabricExample/src/screens/Examples/Main/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ export const examples: Example[] = [
info: ScreenNames.AWARE_SCROLL_VIEW,
icons: '🤓',
},
{
title: 'Aware scroll view sticky footer',
info: ScreenNames.AWARE_SCROLL_VIEW_STICKY_FOOTER,
icons: '🤓🩹',
},
{
title: 'Status Bar',
info: ScreenNames.STATUS_BAR,
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Keyboard manager which works in identical way on both iOS and Android.
- module for changing soft input mode on Android 🤔
- reanimated support 🚀
- interactive keyboard dismissing 👆📱
- re-worked prebuilt components (such as `KeyboardAvoidingView`) 📚
- prebuilt components (`KeyboardStickyView`, re-worked `KeyboardAvoidingView`) 📚
- easy focused input information retrieval 📝 🔮
- works with any navigation library 🧭
- and more is coming... Stay tuned! 😊
Expand Down
37 changes: 37 additions & 0 deletions docs/docs/api/components/keyboard-sticky-view/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
keywords: [react-native-keyboard-controller, KeyboardStickyView, keyboard sticky view, keyboard sticky footer, sticky view, sticky footer, keyboard, android]
---

# KeyboardStickyView

A `KeyboardStickyView` component seamlessly ensures that a designated view sticks to the keyboard's movements, maintaining visibility and interaction. Use it when you want to enhance the user experience by preventing important UI elements from being obscured by the keyboard, creating a smooth and user-friendly interface in your React Native application.

import Lottie from 'lottie-react';
import lottie from './ksv.lottie.json';

<div style={{ display: 'flex', justifyContent: 'center', marginBottom: 20 }}>
<Lottie animationData={lottie} style={{ width: 400, height: 400 }} loop />
</div>

:::info `KeyboardAvoidingView` vs `KeyboardStickyView`
Unlike [KeyboardAvoidingView](../keyboard-avoiding-view.mdx) the `KeyboardStickyView` just moves the content along with keyboard and not resizing the inner view. Try to compare animations of `KeyboardStickyView` and `KeyboardAvoidingView` to see a difference in details on how it works and which component is suitable for your needs.
:::

## Example

```tsx
const offset = { closed: 0, opened: 20 };

<KeyboardStickyView offset={offset}>
<Footer />
</KeyboardStickyView>
```

## Props

### offset

An object containing next properties:

- **closed** - additional offset to the view when keyboard is closed. Default value is `0`.
- **opened** - additional offset to the view when keyboard is opened. Default value is `0`.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import Reanimated, {
} from 'react-native-reanimated';
import { useSmoothKeyboardHandler } from './useSmoothKeyboardHandler';

const BOTTOM_OFFSET = 50;
type KeyboardAwareScrollViewProps = {
bottomOffset?: number;
} & ScrollViewProps;

/**
* Everything begins from `onStart` handler. This handler is called every time,
Expand Down Expand Up @@ -53,8 +55,9 @@ const BOTTOM_OFFSET = 50;
* +============================+ +============================+ +=====================================+
*
*/
const KeyboardAwareScrollView: FC<ScrollViewProps> = ({
const KeyboardAwareScrollView: FC<KeyboardAwareScrollViewProps> = ({
children,
bottomOffset = 0,
...rest
}) => {
const scrollViewAnimatedRef = useAnimatedRef<Reanimated.ScrollView>();
Expand Down Expand Up @@ -85,11 +88,11 @@ const KeyboardAwareScrollView: FC<ScrollViewProps> = ({
const visibleRect = height - keyboardHeight.value;
const point = (layout.value?.layout.absoluteY || 0) + (layout.value?.layout.height || 0);

if (visibleRect - point <= BOTTOM_OFFSET) {
if (visibleRect - point <= bottomOffset) {
const interpolatedScrollTo = interpolate(
e,
[initialKeyboardSize.value, keyboardHeight.value],
[0, keyboardHeight.value - (height - point) + BOTTOM_OFFSET]
[0, keyboardHeight.value - (height - point) + bottomOffset]
);
const targetScrollY =
Math.max(interpolatedScrollTo, 0) + scrollPosition.value;
Expand All @@ -99,7 +102,7 @@ const KeyboardAwareScrollView: FC<ScrollViewProps> = ({
}

return 0;
}, []);
}, [bottomOffset]);

useSmoothKeyboardHandler(
{
Expand Down Expand Up @@ -158,7 +161,7 @@ const KeyboardAwareScrollView: FC<ScrollViewProps> = ({
scrollPosition.value = position.value;
},
},
[height]
[height, maybeScroll]
);

useAnimatedReaction(() => input.value, (current, previous) => {
Expand Down
1 change: 1 addition & 0 deletions example/src/components/AwareScrollView/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./KeyboardAwareScrollView";
File renamed without changes.
1 change: 1 addition & 0 deletions example/src/constants/screenNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export enum ScreenNames {
REANIMATED_CHAT_FLATLIST = 'REANIMATED_CHAT_FLATLIST',
EVENTS = 'EVENTS',
AWARE_SCROLL_VIEW = 'AWARE_SCROLL_VIEW',
AWARE_SCROLL_VIEW_STICKY_FOOTER = 'AWARE_SCROLL_VIEW_STICKY_FOOTER',
STATUS_BAR = 'STATUS_BAR',
LOTTIE = 'LOTTIE',
EXAMPLES_STACK = 'EXAMPLES_STACK',
Expand Down
Loading

0 comments on commit 468d750

Please sign in to comment.