Skip to content

feat: KeyboardExtender #982

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 20 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,22 @@ exports[`components rendering should render \`KeyboardControllerView\` 1`] = `
/>
`;

exports[`components rendering should render \`KeyboardExtenderTest\` 1`] = `
<KeyboardExtender
enabled={true}
>
<View
style={
{
"backgroundColor": "black",
"height": 20,
"width": 20,
}
}
/>
</KeyboardExtender>
`;

exports[`components rendering should render \`KeyboardProvider\` 1`] = `
<KeyboardProvider
statusBarTranslucent={true}
Expand Down
9 changes: 9 additions & 0 deletions FabricExample/__tests__/components-rendering.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
KeyboardAwareScrollView,
KeyboardBackgroundView,
KeyboardControllerView,
KeyboardExtender,
KeyboardProvider,
KeyboardStickyView,
KeyboardToolbar,
Expand Down Expand Up @@ -79,6 +80,10 @@ function KeyboardBackgroundViewTest() {
return <KeyboardBackgroundView />;
}

function KeyboardExtenderTest() {
return <KeyboardExtender enabled={true}>{<EmptyView />}</KeyboardExtender>;
}

describe("components rendering", () => {
it("should render `KeyboardControllerView`", () => {
expect(render(<KeyboardControllerViewTest />)).toMatchSnapshot();
Expand Down Expand Up @@ -111,4 +116,8 @@ describe("components rendering", () => {
it("should render `KeyboardBackgroundView`", () => {
expect(render(<KeyboardBackgroundViewTest />)).toMatchSnapshot();
});

it("should render `KeyboardExtenderTest`", () => {
expect(render(<KeyboardExtenderTest />)).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const KeyboardSharedTransitionExample = () => {
>
<ReanimatedTextInput
placeholder="127.0.0.1"
placeholderTextColor="#ecececec"
placeholderTextColor="#ececec"
style={[
{
width: "100%",
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ A universal keyboard handling solution for React Native — lightweight, fully c
- 📐 `KeyboardToolbar` with customizable _**previous**_, _**next**_, and _**done**_ buttons
- 🌐 Display anything over the keyboard (without dismissing it) using `OverKeyboardView`
- 🎨 Match keyboard background with `KeyboardBackgroundView`
- 🧩 Extend keyboard with custom buttons/UI via `KeyboardExtender`
- 📝 Easy retrieval of focused input info
- 🧭 Compatible with any navigation library
- ✨ More coming soon... stay tuned! 😊
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/api/keyboard-background-view/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ const KeyboardSharedTransitionExample = () => {
>
<ReanimatedTextInput
placeholder="127.0.0.1"
placeholderTextColor="#ecececec"
placeholderTextColor="#ececec"
style={[
{
width: "100%",
Expand Down
119 changes: 119 additions & 0 deletions docs/docs/api/keyboard-extender/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
---
sidebar_position: 8
description: Display views over keyboard without closing it.
keywords:
[
react-native-keyboard-controller,
keyboard,
extend keyboard,
UIInputView,
input accessory view,
KeyboardExtender,
custom buttons inside keyboard,
]
---

# KeyboardExtender

import Lottie from "lottie-react";
import lottie from "./keyboard-extender.lottie.json";

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

The `KeyboardExtend` component allows you to extend the keyboard with your own UI that extends the keyboard (i. e. literally increasing its height) moves and matches its appearance.

## `KeyboardStickyView` vs `KeyboardExtender`

While both components serve similar purposes they are intended for different use cases. Below is the table that can help you to decide which component to use:

| Feature | KeyboardExtender | KeyboardStickyView |
| ----------------------------- | ---------------- | ------------------ |
| Matches keyboard design | ✅ | ❌ |
| Is part of the keyboard | ✅ | ❌ |
| Hides when keyboard is hidden | ✅ | ❌ |
| Increases keyboard height | ✅ | ❌ |

## Features

- ✅ **Automatically attaches** to all `TextInput`s when enabled
- 📏 **Automatically calculates its height** based on content
- 🎯 **Moves with the keyboard animation**
- 🎨 **Matches the keyboard UI** for seamless integration
- 🎭 **Renders as if it's part of the keyboard**

## Props

### `enabled`

A boolean prop indicating whether the component is enabled or disabled. If it's `true`, the component attaches to the keyboard. If it's `false`, it detaches.

## Usage

```jsx
import React, { useState } from "react";
import { View, Text, TextInput, StyleSheet } from "react-native";
import { KeyboardExtender } from "react-native-keyboard-controller";

export default function KeyboardExtenderExample() {
const [keyboardVisible, setKeyboardVisible] = useState(false);

return (
<View style={styles.container}>
<TextInput
style={styles.input}
placeholder="Type something..."
onFocus={() => setKeyboardVisible(true)}
onBlur={() => setKeyboardVisible(false)}
/>

<KeyboardExtender enabled={keyboardVisible}>
<View style={styles.keyboardExtender}>
<Text style={styles.quickOption}>10$</Text>
<Text style={styles.quickOption}>20$</Text>
<Text style={styles.quickOption}>50$</Text>
</View>
</KeyboardExtender>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
input: {
height: 40,
borderWidth: 1,
borderColor: "#ccc",
padding: 10,
marginBottom: 20,
borderRadius: 4,
},
keyboardExtender: {
flexDirection: "row",
justifyContent: "space-around",
alignItems: "center",
backgroundColor: "#f5f5f5",
borderTopWidth: 1,
borderTopColor: "#ccc",
padding: 15,
},
quickOption: {
fontSize: 16,
fontWeight: "bold",
padding: 10,
},
});
```

## Limitations

- You can not put `TextInput` inside `KeyboardExtender`. Consider to use [KeyboardBackgroundView](../api/components/keyboard-sticky-view) + [KeyboardStickyView](../api/components/keyboard-sticky-view) instead.

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions docs/docs/guides/components-overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ keywords:
react-native-keyboard-controller,
components,
views,
extend keyboard,
keyboard extender,
over keyboard view,
keyboard toolbar,
keyboard avoiding view,
Expand All @@ -16,6 +18,7 @@ keywords:
KeyboardAwareScrollView,
KeyboardToolbar,
OverKeyboardView,
KeyboardExtender,
]
---

Expand All @@ -34,6 +37,7 @@ import KeyboardStickyView from "../api/components/keyboard-sticky-view/ksv.lotti
import KeyboardToolbar from "../api/components/keyboard-toolbar/toolbar.lottie.json";
import OverKeyboardView from "../api/over-keyboard-view/over-keyboard-view.lottie.json";
import KeyboardBackgroundView from "../api/keyboard-background-view/keyboard-background-view.lottie.json";
import KeyboardExtender from "../api/keyboard-extender/keyboard-extender.lottie.json";

<div style={{ display: "flex", justifyContent: "center", marginBottom: 20 }}>
<Lottie
Expand Down Expand Up @@ -120,6 +124,23 @@ A major benefit is its use of the library's native focus control logic (`Keyboar

Its key difference from a standard `Modal` is precisely this ability to keep the keyboard open and active, enabling more seamless keyboard-centric interactions. Its usage is straightforward, primarily controlled via the `visible` boolean prop.

## [`KeyboardExtender`](../api/keyboard-extender)

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

`KeyboardExtender` allows you to render custom content _inside_ the keyboard area — literally extending the keyboard with your own UI. Unlike views that float or move with the keyboard, this component integrates visually and functionally _as part of the keyboard itself_.

It's perfect for adding quick action buttons or other custom input helpers directly above the system keyboard. The extender matches the native keyboard animation and hides automatically when the keyboard dismisses.

Use the `enabled` prop to toggle its activation based on `TextInput` focus, and note that it **cannot contain other `TextInput`s** inside.

## [`KeyboardBackgroundView`](../api/keyboard-background-view)

<div style={{ display: "flex", justifyContent: "center", marginBottom: 20 }}>
Expand Down Expand Up @@ -156,6 +177,7 @@ Use this when you want your UI to feel like it shares the same visual space as t
| `KeyboardAwareScrollView` | Scrolls content to focused input | ✅ (scroll position) | Large Scrollable Forms/Lists | Auto-scrolls within ScrollView, respects native animation |
| `KeyboardToolbar` | Adds Nav/Done buttons, sticks to keyboard | ❌ (it's sticky) | Multi-Input Forms | Provides UI + native logic for input navigation/dismissal |
| `OverKeyboardView` | Displays content _over_ the keyboard | ❌ (overlays content) | Menus, Modals over keyboard | Keeps keyboard open while showing overlay content |
| `KeyboardExtender` | Displays the content inside the keyboard | ❌ (moves with keyboard) | Quick actions, shortcuts | Appears as part of keyboard, matches animation & style |
| `KeyboardBackgroundView` | Matches keyboard background color | ❌ (visual only) | Visual Blending/Transitions | Synchronizes color with keyboard for seamless UI effects |

## Which Component Should You Use?
Expand All @@ -167,6 +189,7 @@ Here's a simple guide to choosing:
- For **scrollable screens containing multiple inputs** (like forms or long lists), `KeyboardAwareScrollView` will handle keeping the focused input visible automatically.
- To add standard **"Previous/Next/Done" navigation buttons** to your forms, `KeyboardToolbar` offers a convenient and customizable solution.
- When you need to display **contextual content like suggestions or menus over an active keyboard** without dismissing it, `OverKeyboardView` provides this unique capability.
- If you want to **extend the keyboard with your own UI**, such as quick actions or input helpers that appear _inside_ the keyboard area, use `KeyboardExtender`.
- If you're aiming for **visual consistency between your UI and the keyboard background** — for example, to blend a panel into the keyboard area — `KeyboardBackgroundView` helps match system colors for a polished, seamless effect.

This library offers specialized tools for common keyboard challenges in React Native. Choose the one that best fits your UI need.
25 changes: 25 additions & 0 deletions e2e/kit/015-keyboard-extender.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { expectBitmapsToBeEqual } from "./asserts";
import {
scrollDownUntilElementIsVisible,
waitAndTap,
waitForElementById,
waitForExpect,
} from "./helpers";

describe("`KeyboardExtender` specification", () => {
it("should navigate to `Keyboard Extender` screen", async () => {
await scrollDownUntilElementIsVisible(
"main_scroll_view",
"keyboard_extender",
);
await waitAndTap("keyboard_extender");
await waitForElementById("donation_amount");
});

it("should have `KeyboardExtender` above the keyboard", async () => {
await waitAndTap("donation_amount");
await waitForExpect(async () => {
await expectBitmapsToBeEqual("KeyboardExtenderIsAttached");
});
});
});
16 changes: 16 additions & 0 deletions example/__tests__/__snapshots__/components-rendering.spec.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,22 @@ exports[`components rendering should render \`KeyboardControllerView\` 1`] = `
/>
`;

exports[`components rendering should render \`KeyboardExtenderTest\` 1`] = `
<KeyboardExtender
enabled={true}
>
<View
style={
{
"backgroundColor": "black",
"height": 20,
"width": 20,
}
}
/>
</KeyboardExtender>
`;

exports[`components rendering should render \`KeyboardProvider\` 1`] = `
<KeyboardProvider
statusBarTranslucent={true}
Expand Down
9 changes: 9 additions & 0 deletions example/__tests__/components-rendering.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
KeyboardAwareScrollView,
KeyboardBackgroundView,
KeyboardControllerView,
KeyboardExtender,
KeyboardProvider,
KeyboardStickyView,
KeyboardToolbar,
Expand Down Expand Up @@ -79,6 +80,10 @@ function KeyboardBackgroundViewTest() {
return <KeyboardBackgroundView />;
}

function KeyboardExtenderTest() {
return <KeyboardExtender enabled={true}>{<EmptyView />}</KeyboardExtender>;
}

describe("components rendering", () => {
it("should render `KeyboardControllerView`", () => {
expect(render(<KeyboardControllerViewTest />)).toMatchSnapshot();
Expand Down Expand Up @@ -111,4 +116,8 @@ describe("components rendering", () => {
it("should render `KeyboardBackgroundView`", () => {
expect(render(<KeyboardBackgroundViewTest />)).toMatchSnapshot();
});

it("should render `KeyboardExtenderTest`", () => {
expect(render(<KeyboardExtenderTest />)).toMatchSnapshot();
});
});
1 change: 1 addition & 0 deletions example/src/constants/screenNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ export enum ScreenNames {
USE_KEYBOARD_STATE = "USE_KEYBOARD_STATE",
LIQUID_KEYBOARD = "LIQUID_KEYBOARD",
KEYBOARD_SHARED_TRANSITIONS = "KEYBOARD_SHARED_TRANSITIONS",
KEYBOARD_EXTENDER = "KEYBOARD_EXTENDER",
}
11 changes: 11 additions & 0 deletions example/src/navigation/ExamplesStack/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import InteractiveKeyboard from "../../screens/Examples/InteractiveKeyboard";
import InteractiveKeyboardIOS from "../../screens/Examples/InteractiveKeyboardIOS";
import KeyboardAnimation from "../../screens/Examples/KeyboardAnimation";
import KeyboardAvoidingViewExample from "../../screens/Examples/KeyboardAvoidingView";
import KeyboardExtender from "../../screens/Examples/KeyboardExtender";
import KeyboardSharedTransitionExample from "../../screens/Examples/KeyboardSharedTransitions";
import UseKeyboardState from "../../screens/Examples/KeyboardStateHook";
import LiquidKeyboardExample from "../../screens/Examples/LiquidKeyboard";
Expand Down Expand Up @@ -52,6 +53,7 @@ export type ExamplesStackParamList = {
[ScreenNames.USE_KEYBOARD_STATE]: undefined;
[ScreenNames.LIQUID_KEYBOARD]: undefined;
[ScreenNames.KEYBOARD_SHARED_TRANSITIONS]: undefined;
[ScreenNames.KEYBOARD_EXTENDER]: undefined;
};

const Stack = createStackNavigator<ExamplesStackParamList>();
Expand Down Expand Up @@ -132,6 +134,10 @@ const options = {
title: "Keyboard shared transitions",
headerShown: false,
},
[ScreenNames.KEYBOARD_EXTENDER]: {
title: "Keyboard Extender",
headerShown: false,
},
};

const ExamplesStack = () => (
Expand Down Expand Up @@ -256,6 +262,11 @@ const ExamplesStack = () => (
name={ScreenNames.KEYBOARD_SHARED_TRANSITIONS}
options={options[ScreenNames.KEYBOARD_SHARED_TRANSITIONS]}
/>
<Stack.Screen
component={KeyboardExtender}
name={ScreenNames.KEYBOARD_EXTENDER}
options={options[ScreenNames.KEYBOARD_EXTENDER]}
/>
</Stack.Navigator>
);

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading