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

Merged
merged 27 commits into from
Jun 26, 2025
Merged
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
463c296
feat: keyboard extender
kirillzyusko Apr 13, 2025
545e271
fix: layout issues
kirillzyusko Apr 14, 2025
2783668
chore: continue experiments
kirillzyusko Apr 14, 2025
9fcc58c
feat: continue experiments
kirillzyusko Apr 14, 2025
292c20c
feat: react on `enabled` changes
kirillzyusko Apr 19, 2025
dbccda2
fix: resolve some TODOs
kirillzyusko Apr 19, 2025
f1c6f72
chore: presentation demo
kirillzyusko Apr 28, 2025
164baba
docs: update README
kirillzyusko May 7, 2025
704f471
docs: added draft lottie
kirillzyusko May 7, 2025
ccdb25f
docs: add chapter in components overview, improve lottie animation
kirillzyusko May 7, 2025
87cf1de
docs: added js doc
kirillzyusko May 15, 2025
ffff42e
e2e: draft changes
kirillzyusko May 17, 2025
d0ea6d8
docs: add keyword
kirillzyusko May 21, 2025
0d062f9
fix: disable on Android for now
kirillzyusko May 23, 2025
fdea23c
fix: broken link
kirillzyusko Jun 15, 2025
ffb4d4f
Optimised images with calibre/image-actions
github-actions[bot] Jun 15, 2025
0d854d1
fix: e2e tests
kirillzyusko Jun 15, 2025
7cff86c
fix: replace color
kirillzyusko Jun 20, 2025
7075a55
fix: documentation build
kirillzyusko Jun 20, 2025
ec4156b
feat: implementation for platforms with missing native API (aka polyf…
kirillzyusko Jun 23, 2025
d19817c
fix: changes after self review
kirillzyusko Jun 23, 2025
a9768b2
fix: use Logger instead of Log
kirillzyusko Jun 23, 2025
5f35347
feat: e2e tests
kirillzyusko Jun 24, 2025
567b856
fix: ktlint
kirillzyusko Jun 24, 2025
53d688d
e2e: complete test cases
kirillzyusko Jun 24, 2025
8607762
fix: Fabric build
kirillzyusko Jun 25, 2025
298be81
fix: changes after self review
kirillzyusko Jun 26, 2025
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
@@ -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}
9 changes: 9 additions & 0 deletions FabricExample/__tests__/components-rendering.spec.tsx
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ import {
KeyboardAwareScrollView,
KeyboardBackgroundView,
KeyboardControllerView,
KeyboardExtender,
KeyboardProvider,
KeyboardStickyView,
KeyboardToolbar,
@@ -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();
@@ -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 FabricExample/src/constants/screenNames.ts
Original file line number Diff line number Diff line change
@@ -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 FabricExample/src/navigation/ExamplesStack/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
@@ -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>();
@@ -132,6 +134,10 @@ const options = {
title: "Keyboard shared transitions",
headerShown: false,
},
[ScreenNames.KEYBOARD_EXTENDER]: {
title: "Keyboard Extender",
headerShown: false,
},
};

const ExamplesStack = () => (
@@ -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>
);

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
115 changes: 115 additions & 0 deletions FabricExample/src/screens/Examples/KeyboardExtender/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React, { useEffect, useState } from "react";
import {
Alert,
Image,
Keyboard,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
TouchableWithoutFeedback,
} from "react-native";
import { KeyboardExtender } from "react-native-keyboard-controller";
import Reanimated, {
useAnimatedStyle,
useSharedValue,
withTiming,
} from "react-native-reanimated";
import { SafeAreaView } from "react-native-safe-area-context";

export default function KeyboardExtendExample() {
const [showExtend, setShowExtend] = useState(true);
const opacity = useSharedValue(1);

useEffect(() => {
opacity.set(withTiming(showExtend ? 1 : 0));
}, [showExtend]);

const animatedStyle = useAnimatedStyle(
() => ({
opacity: opacity.value,
}),
[],
);

return (
<>
<Image source={require("./background.jpg")} style={styles.background} />
<TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}>
<SafeAreaView edges={["top"]} style={styles.container}>
<TextInput
keyboardType="numeric"
placeholder="Donation amount"
placeholderTextColor="#5c5c5c"
style={styles.input}
testID="donation_amount"
onFocus={() => setShowExtend(true)}
/>
<TextInput
keyboardType="numeric"
placeholder="Postal code"
placeholderTextColor="#5c5c5c"
style={styles.input}
testID="postal_code"
onFocus={() => setShowExtend(false)}
/>
</SafeAreaView>
</TouchableWithoutFeedback>
<KeyboardExtender enabled={showExtend}>
<Reanimated.View style={[styles.keyboardExtend, animatedStyle]}>
<TouchableOpacity
testID="donation_10"
onPress={() => Alert.alert("10 dollars")}
>
<Text style={styles.priceText}>10$</Text>
</TouchableOpacity>
<TouchableOpacity
testID="donation_20"
onPress={() => Alert.alert("20 dollars")}
>
<Text style={styles.priceText}>20$</Text>
</TouchableOpacity>
<TouchableOpacity
testID="donation_50"
onPress={() => Alert.alert("50 dollars")}
>
<Text style={styles.priceText}>50$</Text>
</TouchableOpacity>
</Reanimated.View>
</KeyboardExtender>
</>
);
}

const styles = StyleSheet.create({
background: {
...StyleSheet.absoluteFillObject,
flex: 1,
width: "100%",
},
container: {
flex: 1,
paddingHorizontal: 20,
},
input: {
height: 40,
borderWidth: 2,
borderColor: "#1c1c1c",
borderRadius: 8,
padding: 10,
fontSize: 18,
marginBottom: 20,
},
keyboardExtend: {
width: "100%",
flexDirection: "row",
justifyContent: "space-around",
alignItems: "center",
},
priceText: {
color: "black",
fontSize: 18,
fontWeight: "600",
padding: 20,
},
});
Original file line number Diff line number Diff line change
@@ -64,7 +64,7 @@ const KeyboardSharedTransitionExample = () => {
>
<ReanimatedTextInput
placeholder="127.0.0.1"
placeholderTextColor="#ecececec"
placeholderTextColor="#ececec"
style={[
{
width: "100%",
6 changes: 6 additions & 0 deletions FabricExample/src/screens/Examples/Main/constants.ts
Original file line number Diff line number Diff line change
@@ -147,4 +147,10 @@ export const examples: Example[] = [
info: ScreenNames.KEYBOARD_SHARED_TRANSITIONS,
icons: "🔄",
},
{
title: "Keyboard Extender",
testID: "keyboard_extender",
info: ScreenNames.KEYBOARD_EXTENDER,
icons: "🧩",
},
];
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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! 😊
Original file line number Diff line number Diff line change
@@ -3,13 +3,14 @@ package com.reactnativekeyboardcontroller.views.background
import android.content.Context
import android.graphics.Color
import android.os.Build
import android.util.Log
import androidx.annotation.ColorInt
import com.facebook.react.uimanager.ThemedReactContext
import com.reactnativekeyboardcontroller.R
import com.reactnativekeyboardcontroller.extensions.currentImePackage
import com.reactnativekeyboardcontroller.extensions.isSystemDarkMode
import com.reactnativekeyboardcontroller.log.Logger

private const val TAG = "Skins"
private const val MAX_RGB_VALUE = 255

object ImePackages {
@@ -76,7 +77,7 @@ fun ThemedReactContext.getInputMethodColor(): Int {
val imePackage = currentImePackage()
val isDark = isSystemDarkMode()

Log.i("Skins", "Current IME: $imePackage")
Logger.i(TAG, "Current IME: $imePackage")

val (lightColorRes, darkColorRes) =
imeColorMap[imePackage]
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// RNKCKeyboardExtenderComponentDescriptor.h
// Pods
//
// Created by Kiryl Ziusko on 25/06/2025.
//

#pragma once

#include "RNKCKeyboardExtenderShadowNode.h"

#include <react/debug/react_native_assert.h>
#include <react/renderer/components/reactnativekeyboardcontroller/Props.h>
#include <react/renderer/core/ConcreteComponentDescriptor.h>

namespace facebook::react {
class KeyboardExtenderComponentDescriptor final
: public ConcreteComponentDescriptor<KeyboardExtenderShadowNode> {
public:
using ConcreteComponentDescriptor::ConcreteComponentDescriptor;
void adopt(ShadowNode &shadowNode) const override {
react_native_assert(dynamic_cast<KeyboardExtenderShadowNode *>(&shadowNode));
ConcreteComponentDescriptor::adopt(shadowNode);
}
};

} // namespace facebook::react
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// RNKCKeyboardExtenderShadowNode.cpp
// Pods
//
// Created by Kiryl Ziusko on 25/06/2025.
//

#include "RNKCKeyboardExtenderShadowNode.h"

namespace facebook::react {

extern const char KeyboardExtenderComponentName[] = "KeyboardExtender";

} // namespace facebook::react
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// RNKCKeyboardExtenderShadowNode.h
// Pods
//
// Created by Kiryl Ziusko on 25/06/2025.
//

#pragma once

#include "RNKCKeyboardExtenderState.h"

#include <react/renderer/components/reactnativekeyboardcontroller/EventEmitters.h>
#include <react/renderer/components/reactnativekeyboardcontroller/Props.h>
#include <react/renderer/components/view/ConcreteViewShadowNode.h>
#include <jsi/jsi.h>

namespace facebook::react {

JSI_EXPORT extern const char KeyboardExtenderComponentName[];

/*
* `ShadowNode` for <KeyboardExtender> component.
*/
using KeyboardExtenderShadowNode = ConcreteViewShadowNode<
KeyboardExtenderComponentName,
KeyboardExtenderProps,
KeyboardExtenderEventEmitter,
KeyboardExtenderState>;

} // namespace facebook::react
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// RNKCKeyboardExtenderState.h
// Pods
//
// Created by Kiryl Ziusko on 25/06/2025.
//

#pragma once

#ifdef ANDROID
#include <folly/dynamic.h>
#endif

namespace facebook::react {

class KeyboardExtenderState {
public:
KeyboardExtenderState() = default;

#ifdef ANDROID
KeyboardExtenderState(KeyboardExtenderState const &previousState, folly::dynamic data) {}
folly::dynamic getDynamic() const {
return {};
}
#endif
};

} // namespace facebook::react
2 changes: 1 addition & 1 deletion docs/docs/api/keyboard-background-view/index.mdx
Original file line number Diff line number Diff line change
@@ -130,7 +130,7 @@ const KeyboardSharedTransitionExample = () => {
>
<ReanimatedTextInput
placeholder="127.0.0.1"
placeholderTextColor="#ecececec"
placeholderTextColor="#ececec"
style={[
{
width: "100%",
Loading