From 49d48b32441c13c3ca6ed1f17c56ad03b5a021bc Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Thu, 26 Dec 2024 19:22:26 +0100 Subject: [PATCH 01/22] feat: `KeyboardToolbar.Exclude` --- .../KeyboardToolbarExcludeViewManagerImpl.kt | 16 ++++ .../traversal/ViewHierarchyNavigator.kt | 5 +- .../KeyboardToolbarExcludeReactViewGroup.kt | 10 +++ .../KeyboardToolbarExcludeViewManager.kt | 18 ++++ .../KeyboardControllerPackage.kt | 1 + .../api/components/keyboard-toolbar/index.mdx | 6 ++ .../src/screens/Examples/Toolbar/index.tsx | 10 +++ ios/traversal/ViewHierarchyNavigator.swift | 4 +- ios/views/KeyboardToolbarExcludeViewManager.h | 28 ++++++ .../KeyboardToolbarExcludeViewManager.mm | 88 +++++++++++++++++++ src/bindings.native.ts | 2 + src/bindings.ts | 3 + src/components/KeyboardToolbar/index.tsx | 6 +- ...yboardToolbarExcludeViewNativeComponent.ts | 10 +++ 14 files changed, 203 insertions(+), 4 deletions(-) create mode 100644 android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardToolbarExcludeViewManagerImpl.kt create mode 100644 android/src/main/java/com/reactnativekeyboardcontroller/views/KeyboardToolbarExcludeReactViewGroup.kt create mode 100644 android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardToolbarExcludeViewManager.kt create mode 100644 ios/views/KeyboardToolbarExcludeViewManager.h create mode 100644 ios/views/KeyboardToolbarExcludeViewManager.mm create mode 100644 src/specs/KeyboardToolbarExcludeViewNativeComponent.ts diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardToolbarExcludeViewManagerImpl.kt b/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardToolbarExcludeViewManagerImpl.kt new file mode 100644 index 0000000000..67422fac6e --- /dev/null +++ b/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardToolbarExcludeViewManagerImpl.kt @@ -0,0 +1,16 @@ +package com.reactnativekeyboardcontroller.managers + +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ThemedReactContext +import com.reactnativekeyboardcontroller.views.KeyboardToolbarExcludeReactViewGroup +import com.reactnativekeyboardcontroller.views.overlay.OverKeyboardHostView + +@Suppress("detekt:UnusedPrivateProperty") +class KeyboardToolbarExcludeViewManagerImpl(mReactContext: ReactApplicationContext) { + fun createViewInstance(reactContext: ThemedReactContext): KeyboardToolbarExcludeReactViewGroup = + KeyboardToolbarExcludeReactViewGroup(reactContext) + + companion object { + const val NAME = "KeyboardToolbarExcludeView" + } +} diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/traversal/ViewHierarchyNavigator.kt b/android/src/main/java/com/reactnativekeyboardcontroller/traversal/ViewHierarchyNavigator.kt index 8a0d6ec4ee..975aec8e84 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/traversal/ViewHierarchyNavigator.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/traversal/ViewHierarchyNavigator.kt @@ -5,6 +5,7 @@ import android.view.ViewGroup import android.widget.EditText import com.facebook.react.bridge.UiThreadUtil import com.reactnativekeyboardcontroller.extensions.focus +import com.reactnativekeyboardcontroller.views.KeyboardToolbarExcludeReactViewGroup object ViewHierarchyNavigator { fun setFocusTo( @@ -25,7 +26,7 @@ object ViewHierarchyNavigator { fun findEditTexts(view: View?) { if (isValidTextInput(view)) { editTexts.add(view as EditText) - } else if (view is ViewGroup) { + } else if (view is ViewGroup && view !is KeyboardToolbarExcludeReactViewGroup) { for (i in 0 until view.childCount) { findEditTexts(view.getChildAt(i)) } @@ -91,7 +92,7 @@ object ViewHierarchyNavigator { if (isValidTextInput(child)) { result = child as EditText - } else if (child is ViewGroup) { + } else if (child is ViewGroup && child !is KeyboardToolbarExcludeReactViewGroup) { // If the child is a ViewGroup, check its children recursively result = findEditTextInHierarchy(child, direction) } diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/views/KeyboardToolbarExcludeReactViewGroup.kt b/android/src/main/java/com/reactnativekeyboardcontroller/views/KeyboardToolbarExcludeReactViewGroup.kt new file mode 100644 index 0000000000..bcb0445a3b --- /dev/null +++ b/android/src/main/java/com/reactnativekeyboardcontroller/views/KeyboardToolbarExcludeReactViewGroup.kt @@ -0,0 +1,10 @@ +package com.reactnativekeyboardcontroller.views + +import android.annotation.SuppressLint +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.views.view.ReactViewGroup + +@SuppressLint("ViewConstructor") +class KeyboardToolbarExcludeReactViewGroup(reactContext: ThemedReactContext): ReactViewGroup(reactContext) { + // semantic view used in KeyboardToolbar traverse algorithm +} diff --git a/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardToolbarExcludeViewManager.kt b/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardToolbarExcludeViewManager.kt new file mode 100644 index 0000000000..c27b93e93a --- /dev/null +++ b/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardToolbarExcludeViewManager.kt @@ -0,0 +1,18 @@ +package com.reactnativekeyboardcontroller + +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.views.view.ReactViewManager +import com.reactnativekeyboardcontroller.managers.KeyboardToolbarExcludeViewManagerImpl +import com.reactnativekeyboardcontroller.views.KeyboardToolbarExcludeReactViewGroup + +class KeyboardToolbarExcludeViewManager( + mReactContext: ReactApplicationContext, +) : ReactViewManager() { + private val manager = KeyboardToolbarExcludeViewManagerImpl(mReactContext) + + override fun getName(): String = KeyboardToolbarExcludeViewManagerImpl.NAME + + override fun createViewInstance(reactContext: ThemedReactContext): KeyboardToolbarExcludeReactViewGroup = + manager.createViewInstance(reactContext) +} diff --git a/android/src/turbo/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt b/android/src/turbo/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt index cd576d51ea..0ba47f7d52 100644 --- a/android/src/turbo/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt +++ b/android/src/turbo/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt @@ -60,5 +60,6 @@ class KeyboardControllerPackage : TurboReactPackage() { KeyboardGestureAreaViewManager(), OverKeyboardViewManager(), KeyboardBackgroundViewManager(), + KeyboardToolbarExcludeViewManager(reactContext), ) } diff --git a/docs/docs/api/components/keyboard-toolbar/index.mdx b/docs/docs/api/components/keyboard-toolbar/index.mdx index bf0d023949..649b99fc30 100644 --- a/docs/docs/api/components/keyboard-toolbar/index.mdx +++ b/docs/docs/api/components/keyboard-toolbar/index.mdx @@ -387,6 +387,12 @@ const theme: KeyboardToolbarProps["theme"] = { Don't forget that you need to specify colors for **both** `dark` and `light` theme. The theme will be selected automatically based on the device preferences. ::: +## Components + +### `KeyboardToolbar.Exclude` + +This component is used to exclude some views from the traversal. It is useful when you want to skip specific view from being focused by toolbar arrow buttons. + ## Example ```tsx diff --git a/example/src/screens/Examples/Toolbar/index.tsx b/example/src/screens/Examples/Toolbar/index.tsx index 353bc13baf..b4d1811ca4 100644 --- a/example/src/screens/Examples/Toolbar/index.tsx +++ b/example/src/screens/Examples/Toolbar/index.tsx @@ -154,6 +154,16 @@ function Form() { title="Flat" onFocus={onHideAutoFill} /> + + + +#else +#import +#endif +#import +#import + +@interface KeyboardToolbarExcludeViewManager : RCTViewManager +@end + +@interface KeyboardToolbarExcludeView : +#ifdef RCT_NEW_ARCH_ENABLED + RCTViewComponentView +#else + UIView + +- (instancetype)initWithBridge:(RCTBridge *)bridge; + +#endif +@end diff --git a/ios/views/KeyboardToolbarExcludeViewManager.mm b/ios/views/KeyboardToolbarExcludeViewManager.mm new file mode 100644 index 0000000000..aa7ae6dd79 --- /dev/null +++ b/ios/views/KeyboardToolbarExcludeViewManager.mm @@ -0,0 +1,88 @@ +// +// KeyboardToolbarExcludeViewManager.mm +// react-native-keyboard-controller +// +// Created by Kiryl Ziusko on 26/12/2024. +// + +#import "KeyboardToolbarExcludeViewManager.h" + +#ifdef RCT_NEW_ARCH_ENABLED +#import +#import +#import +#import + +#import "RCTFabricComponentsPlugins.h" +#endif + +#import + +#ifdef RCT_NEW_ARCH_ENABLED +using namespace facebook::react; +#endif + +// MARK: Manager +@implementation KeyboardToolbarExcludeViewManager + +RCT_EXPORT_MODULE(KeyboardToolbarExcludeViewManager) + ++ (BOOL)requiresMainQueueSetup +{ + return NO; +} + +#ifndef RCT_NEW_ARCH_ENABLED +- (UIView *)view +{ + return [[KeyboardToolbarExcludeView alloc] initWithBridge:self.bridge]; +} +#endif + +@end + +// MARK: View +#ifdef RCT_NEW_ARCH_ENABLED +@interface KeyboardToolbarExcludeView () +@end +#endif + +@implementation KeyboardToolbarExcludeView {} + +#ifdef RCT_NEW_ARCH_ENABLED ++ (ComponentDescriptorProvider)componentDescriptorProvider +{ + return concreteComponentDescriptorProvider(); +} +#endif + +// Needed because of this: https://github.com/facebook/react-native/pull/37274 ++ (void)load +{ + [super load]; +} + +// MARK: Constructor +#ifdef RCT_NEW_ARCH_ENABLED +- (instancetype)init +{ + self = [super init]; + return self; +} +#else +- (instancetype)initWithBridge:(RCTBridge *)bridge +{ + self = [super init]; + return self; +} +#endif + + +#ifdef RCT_NEW_ARCH_ENABLED +Class KeyboardToolbarExcludeViewCls(void) +{ + return KeyboardToolbarExcludeView.class; +} +#endif + +@end diff --git a/src/bindings.native.ts b/src/bindings.native.ts index 9489aa9834..1b8316b4c3 100644 --- a/src/bindings.native.ts +++ b/src/bindings.native.ts @@ -73,3 +73,5 @@ export const RCTKeyboardExtender: React.FC = : ({ children }: KeyboardExtenderProps) => children; export const ClippingScrollView: React.FC = require("./specs/ClippingScrollViewDecoratorViewNativeComponent").default; +export const RCTKeyboardToolbarExcludeView: React.FC = + require("./specs/KeyboardToolbarExcludeViewNativeComponent").default; diff --git a/src/bindings.ts b/src/bindings.ts index 65f8c56f2d..83179ac4c4 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -9,6 +9,7 @@ import type { KeyboardEventsModule, KeyboardExtenderProps, KeyboardGestureAreaProps, + KeyboardToolbarExcludeViewProps, OverKeyboardViewProps, WindowDimensionsEventsModule, } from "./types"; @@ -92,3 +93,5 @@ export const RCTKeyboardExtender = */ export const ClippingScrollView = View as unknown as React.FC; +export const RCTKeyboardToolbarExcludeView = + View as unknown as React.FC; diff --git a/src/components/KeyboardToolbar/index.tsx b/src/components/KeyboardToolbar/index.tsx index bc2a807fe7..1afa55604a 100644 --- a/src/components/KeyboardToolbar/index.tsx +++ b/src/components/KeyboardToolbar/index.tsx @@ -1,7 +1,10 @@ import React, { useEffect, useMemo, useState } from "react"; import { StyleSheet, View } from "react-native"; -import { FocusedInputEvents } from "../../bindings"; +import { + FocusedInputEvents, + RCTKeyboardToolbarExcludeView, +} from "../../bindings"; import { useKeyboardState } from "../../hooks"; import KeyboardStickyView from "../KeyboardStickyView"; @@ -227,6 +230,7 @@ KeyboardToolbar.Content = Content; KeyboardToolbar.Prev = Prev; KeyboardToolbar.Next = Next; KeyboardToolbar.Done = Done; +KeyboardToolbar.Exclude = RCTKeyboardToolbarExcludeView; export { colors as DefaultKeyboardToolbarTheme, KeyboardToolbarProps }; export default KeyboardToolbar; diff --git a/src/specs/KeyboardToolbarExcludeViewNativeComponent.ts b/src/specs/KeyboardToolbarExcludeViewNativeComponent.ts new file mode 100644 index 0000000000..bf6c612caf --- /dev/null +++ b/src/specs/KeyboardToolbarExcludeViewNativeComponent.ts @@ -0,0 +1,10 @@ +import codegenNativeComponent from "react-native/Libraries/Utilities/codegenNativeComponent"; + +import type { HostComponent } from "react-native"; +import type { ViewProps } from "react-native/Libraries/Components/View/ViewPropTypes"; + +export interface NativeProps extends ViewProps {} + +export default codegenNativeComponent( + "KeyboardToolbarExcludeView", +) as HostComponent; From c909502afa8755f769d3b224270eda6687803365 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Tue, 25 Mar 2025 15:23:50 +0100 Subject: [PATCH 02/22] fix: include new view in base package too --- .../reactnativekeyboardcontroller/KeyboardControllerPackage.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/android/src/base/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt b/android/src/base/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt index 22fced3ac3..f0e51b7777 100644 --- a/android/src/base/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt +++ b/android/src/base/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt @@ -60,5 +60,6 @@ class KeyboardControllerPackage : BaseReactPackage() { OverKeyboardViewManager(), KeyboardBackgroundViewManager(), ClippingScrollViewDecoratorViewManager(), + KeyboardToolbarExcludeViewManager(), ) } From f7cc032ae1f2710c0f4bbe52775458a73f87ca12 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Tue, 25 Mar 2025 15:28:55 +0100 Subject: [PATCH 03/22] fix: CI --- .../managers/KeyboardToolbarExcludeViewManagerImpl.kt | 5 +++-- .../views/KeyboardToolbarExcludeReactViewGroup.kt | 4 +++- ios/traversal/ViewHierarchyNavigator.swift | 4 ++-- ios/views/KeyboardToolbarExcludeViewManager.mm | 4 ++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardToolbarExcludeViewManagerImpl.kt b/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardToolbarExcludeViewManagerImpl.kt index 67422fac6e..5a3078db0e 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardToolbarExcludeViewManagerImpl.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardToolbarExcludeViewManagerImpl.kt @@ -3,10 +3,11 @@ package com.reactnativekeyboardcontroller.managers import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.uimanager.ThemedReactContext import com.reactnativekeyboardcontroller.views.KeyboardToolbarExcludeReactViewGroup -import com.reactnativekeyboardcontroller.views.overlay.OverKeyboardHostView @Suppress("detekt:UnusedPrivateProperty") -class KeyboardToolbarExcludeViewManagerImpl(mReactContext: ReactApplicationContext) { +class KeyboardToolbarExcludeViewManagerImpl( + mReactContext: ReactApplicationContext, +) { fun createViewInstance(reactContext: ThemedReactContext): KeyboardToolbarExcludeReactViewGroup = KeyboardToolbarExcludeReactViewGroup(reactContext) diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/views/KeyboardToolbarExcludeReactViewGroup.kt b/android/src/main/java/com/reactnativekeyboardcontroller/views/KeyboardToolbarExcludeReactViewGroup.kt index bcb0445a3b..2ef1253f34 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/views/KeyboardToolbarExcludeReactViewGroup.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/views/KeyboardToolbarExcludeReactViewGroup.kt @@ -5,6 +5,8 @@ import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.views.view.ReactViewGroup @SuppressLint("ViewConstructor") -class KeyboardToolbarExcludeReactViewGroup(reactContext: ThemedReactContext): ReactViewGroup(reactContext) { +class KeyboardToolbarExcludeReactViewGroup( + reactContext: ThemedReactContext, +) : ReactViewGroup(reactContext) { // semantic view used in KeyboardToolbar traverse algorithm } diff --git a/ios/traversal/ViewHierarchyNavigator.swift b/ios/traversal/ViewHierarchyNavigator.swift index d5d220891c..49636e6b57 100644 --- a/ios/traversal/ViewHierarchyNavigator.swift +++ b/ios/traversal/ViewHierarchyNavigator.swift @@ -42,7 +42,7 @@ public class ViewHierarchyNavigator: NSObject { if let textInput = isValidTextInput(view) { textInputs.append(textInput) - } else if (String(describing: type(of: view)) != "KeyboardToolbarExcludeView") { + } else if String(describing: type(of: view)) != "KeyboardToolbarExcludeView" { for subview in view.subviews { findTextInputs(in: subview) } @@ -90,7 +90,7 @@ public class ViewHierarchyNavigator: NSObject { if let validTextInput = isValidTextInput(view) { return validTextInput } - + guard String(describing: type(of: view)) != "KeyboardToolbarExcludeView" else { return nil } // Determine the iteration order based on the direction diff --git a/ios/views/KeyboardToolbarExcludeViewManager.mm b/ios/views/KeyboardToolbarExcludeViewManager.mm index aa7ae6dd79..46fda7bdd4 100644 --- a/ios/views/KeyboardToolbarExcludeViewManager.mm +++ b/ios/views/KeyboardToolbarExcludeViewManager.mm @@ -47,7 +47,8 @@ @interface KeyboardToolbarExcludeView () KeyboardToolbarExcludeViewCls(void) { From a5557e58c0a090714016fbde5bf5e3308663190e Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Tue, 25 Mar 2025 15:31:45 +0100 Subject: [PATCH 04/22] docs: show the usage --- .../docs/api/components/keyboard-toolbar/index.mdx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/docs/api/components/keyboard-toolbar/index.mdx b/docs/docs/api/components/keyboard-toolbar/index.mdx index 649b99fc30..a5d8245db5 100644 --- a/docs/docs/api/components/keyboard-toolbar/index.mdx +++ b/docs/docs/api/components/keyboard-toolbar/index.mdx @@ -393,6 +393,20 @@ Don't forget that you need to specify colors for **both** `dark` and `light` the This component is used to exclude some views from the traversal. It is useful when you want to skip specific view from being focused by toolbar arrow buttons. +```tsx + + + + + +``` + ## Example ```tsx From 89764bda3480fbbb6c81a031ad3637702e46bf59 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Thu, 27 Mar 2025 10:37:18 +0100 Subject: [PATCH 05/22] feat: expose custom shadow nodes --- .../KeyboardToolbarExcludeViewManager.kt | 26 +++++++++++++++++++ android/src/main/jni/RNKC.h | 1 + ...RNKCKeyboardToolbarExcludeViewShadowNode.h | 0 .../RNKCKeyboardToolbarExcludeViewState.h | 21 +++++++++++++++ react-native.config.js | 1 + ...yboardToolbarExcludeViewNativeComponent.ts | 3 +++ 6 files changed, 52 insertions(+) create mode 100644 android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardToolbarExcludeViewManager.kt create mode 100644 common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarExcludeViewShadowNode.h create mode 100644 common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarExcludeViewState.h diff --git a/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardToolbarExcludeViewManager.kt b/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardToolbarExcludeViewManager.kt new file mode 100644 index 0000000000..e7c1be19d4 --- /dev/null +++ b/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardToolbarExcludeViewManager.kt @@ -0,0 +1,26 @@ +package com.reactnativekeyboardcontroller + +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.ViewGroupManager +import com.facebook.react.uimanager.ViewManagerDelegate +import com.facebook.react.viewmanagers.KeyboardToolbarExcludeViewManagerDelegate +import com.facebook.react.viewmanagers.KeyboardToolbarExcludeViewManagerInterface +import com.reactnativekeyboardcontroller.managers.KeyboardToolbarExcludeViewManagerImpl +import com.reactnativekeyboardcontroller.views.KeyboardToolbarExcludeReactViewGroup +import com.reactnativekeyboardcontroller.views.overlay.OverKeyboardHostView + +class KeyboardToolbarExcludeViewManager( + mReactContext: ReactApplicationContext, +) : ViewGroupManager(), + KeyboardToolbarExcludeViewManagerInterface { + private val manager = KeyboardToolbarExcludeViewManagerImpl(mReactContext) + private val mDelegate = KeyboardToolbarExcludeViewManagerDelegate(this) + + override fun getDelegate(): ViewManagerDelegate = mDelegate + + override fun getName(): String = KeyboardToolbarExcludeViewManagerImpl.NAME + + override fun createViewInstance(context: ThemedReactContext): KeyboardToolbarExcludeReactViewGroup = + manager.createViewInstance(context) +} diff --git a/android/src/main/jni/RNKC.h b/android/src/main/jni/RNKC.h index 9916c77dc1..6a24460acd 100644 --- a/android/src/main/jni/RNKC.h +++ b/android/src/main/jni/RNKC.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include diff --git a/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarExcludeViewShadowNode.h b/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarExcludeViewShadowNode.h new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarExcludeViewState.h b/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarExcludeViewState.h new file mode 100644 index 0000000000..9d2f20cfbf --- /dev/null +++ b/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarExcludeViewState.h @@ -0,0 +1,21 @@ +#pragma once + +#ifdef ANDROID +#include +#endif + +namespace facebook::react { + +class KeyboardToolbarExcludeViewState { + public: + KeyboardToolbarExcludeViewState() = default; + +#ifdef ANDROID + KeyboardToolbarExcludeViewState(KeyboardToolbarExcludeViewState const &previousState, folly::dynamic data) {} + folly::dynamic getDynamic() const { + return {}; + } +#endif +}; + +} // namespace facebook::react diff --git a/react-native.config.js b/react-native.config.js index fb3c79b6e3..1fc6fa8e18 100644 --- a/react-native.config.js +++ b/react-native.config.js @@ -8,6 +8,7 @@ module.exports = { "OverKeyboardViewComponentDescriptor", "KeyboardBackgroundViewComponentDescriptor", "ClippingScrollViewDecoratorViewComponentDescriptor", + "KeyboardToolbarExcludeViewComponentDescriptor", ], cmakeListsPath: "../android/src/main/jni/CMakeLists.txt", }, diff --git a/src/specs/KeyboardToolbarExcludeViewNativeComponent.ts b/src/specs/KeyboardToolbarExcludeViewNativeComponent.ts index bf6c612caf..c9d2fd14dd 100644 --- a/src/specs/KeyboardToolbarExcludeViewNativeComponent.ts +++ b/src/specs/KeyboardToolbarExcludeViewNativeComponent.ts @@ -7,4 +7,7 @@ export interface NativeProps extends ViewProps {} export default codegenNativeComponent( "KeyboardToolbarExcludeView", + { + interfaceOnly: true, + }, ) as HostComponent; From 65b3dbdc62d2f212d09387e1fc4b0e8184c231a1 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Tue, 10 Mar 2026 12:12:22 +0100 Subject: [PATCH 06/22] feat: rename `Exclude` to `Group` --- .../KeyboardControllerPackage.kt | 2 +- .../KeyboardToolbarExcludeViewManager.kt | 26 ---------------- .../KeyboardToolbarGroupViewManager.kt | 26 ++++++++++++++++ ...=> KeyboardToolbarGroupViewManagerImpl.kt} | 10 +++---- ... => KeyboardToolbarGroupReactViewGroup.kt} | 2 +- android/src/main/jni/RNKC.h | 2 +- ....kt => KeyboardToolbarGroupViewManager.kt} | 12 ++++---- .../KeyboardControllerPackage.kt | 2 +- ...RNKCKeyboardToolbarExcludeViewShadowNode.h | 0 ...boardToolbarGroupViewComponentDescriptor.h | 27 +++++++++++++++++ .../RNKCKeyboardToolbarGroupViewShadowNode.h | 30 +++++++++++++++++++ ....h => RNKCKeyboardToolbarGroupViewState.h} | 6 ++-- ...er.h => KeyboardToolbarGroupViewManager.h} | 6 ++-- ....mm => KeyboardToolbarGroupViewManager.mm} | 20 ++++++------- react-native.config.js | 2 +- src/bindings.native.ts | 5 ++-- src/bindings.ts | 6 ++-- src/components/KeyboardToolbar/index.tsx | 5 ++-- ...eyboardToolbarGroupViewNativeComponent.ts} | 2 +- src/types/views.ts | 1 + 20 files changed, 126 insertions(+), 66 deletions(-) delete mode 100644 android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardToolbarExcludeViewManager.kt create mode 100644 android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardToolbarGroupViewManager.kt rename android/src/main/java/com/reactnativekeyboardcontroller/managers/{KeyboardToolbarExcludeViewManagerImpl.kt => KeyboardToolbarGroupViewManagerImpl.kt} (56%) rename android/src/main/java/com/reactnativekeyboardcontroller/views/{KeyboardToolbarExcludeReactViewGroup.kt => KeyboardToolbarGroupReactViewGroup.kt} (89%) rename android/src/paper/java/com/reactnativekeyboardcontroller/{KeyboardToolbarExcludeViewManager.kt => KeyboardToolbarGroupViewManager.kt} (56%) delete mode 100644 common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarExcludeViewShadowNode.h create mode 100644 common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewComponentDescriptor.h create mode 100644 common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewShadowNode.h rename common/cpp/react/renderer/components/RNKC/{RNKCKeyboardToolbarExcludeViewState.h => RNKCKeyboardToolbarGroupViewState.h} (52%) rename ios/views/{KeyboardToolbarExcludeViewManager.h => KeyboardToolbarGroupViewManager.h} (73%) rename ios/views/{KeyboardToolbarExcludeViewManager.mm => KeyboardToolbarGroupViewManager.mm} (67%) rename src/specs/{KeyboardToolbarExcludeViewNativeComponent.ts => KeyboardToolbarGroupViewNativeComponent.ts} (92%) diff --git a/android/src/base/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt b/android/src/base/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt index f0e51b7777..d3018eac42 100644 --- a/android/src/base/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt +++ b/android/src/base/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt @@ -60,6 +60,6 @@ class KeyboardControllerPackage : BaseReactPackage() { OverKeyboardViewManager(), KeyboardBackgroundViewManager(), ClippingScrollViewDecoratorViewManager(), - KeyboardToolbarExcludeViewManager(), + KeyboardToolbarGroupViewManager(), ) } diff --git a/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardToolbarExcludeViewManager.kt b/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardToolbarExcludeViewManager.kt deleted file mode 100644 index e7c1be19d4..0000000000 --- a/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardToolbarExcludeViewManager.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.reactnativekeyboardcontroller - -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.uimanager.ThemedReactContext -import com.facebook.react.uimanager.ViewGroupManager -import com.facebook.react.uimanager.ViewManagerDelegate -import com.facebook.react.viewmanagers.KeyboardToolbarExcludeViewManagerDelegate -import com.facebook.react.viewmanagers.KeyboardToolbarExcludeViewManagerInterface -import com.reactnativekeyboardcontroller.managers.KeyboardToolbarExcludeViewManagerImpl -import com.reactnativekeyboardcontroller.views.KeyboardToolbarExcludeReactViewGroup -import com.reactnativekeyboardcontroller.views.overlay.OverKeyboardHostView - -class KeyboardToolbarExcludeViewManager( - mReactContext: ReactApplicationContext, -) : ViewGroupManager(), - KeyboardToolbarExcludeViewManagerInterface { - private val manager = KeyboardToolbarExcludeViewManagerImpl(mReactContext) - private val mDelegate = KeyboardToolbarExcludeViewManagerDelegate(this) - - override fun getDelegate(): ViewManagerDelegate = mDelegate - - override fun getName(): String = KeyboardToolbarExcludeViewManagerImpl.NAME - - override fun createViewInstance(context: ThemedReactContext): KeyboardToolbarExcludeReactViewGroup = - manager.createViewInstance(context) -} diff --git a/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardToolbarGroupViewManager.kt b/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardToolbarGroupViewManager.kt new file mode 100644 index 0000000000..68dfbeeb01 --- /dev/null +++ b/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardToolbarGroupViewManager.kt @@ -0,0 +1,26 @@ +package com.reactnativekeyboardcontroller + +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.ViewGroupManager +import com.facebook.react.uimanager.ViewManagerDelegate +import com.facebook.react.viewmanagers.KeyboardToolbarGroupViewManagerDelegate +import com.facebook.react.viewmanagers.KeyboardToolbarGroupViewManagerInterface +import com.reactnativekeyboardcontroller.managers.KeyboardToolbarGroupViewManagerImpl +import com.reactnativekeyboardcontroller.views.KeyboardToolbarGroupReactViewGroup +import com.reactnativekeyboardcontroller.views.overlay.OverKeyboardHostView + +class KeyboardToolbarGroupViewManager( + mReactContext: ReactApplicationContext, +) : ViewGroupManager(), + KeyboardToolbarGroupViewManagerInterface { + private val manager = KeyboardToolbarGroupViewManagerImpl(mReactContext) + private val mDelegate = KeyboardToolbarGroupViewManagerDelegate(this) + + override fun getDelegate(): ViewManagerDelegate = mDelegate + + override fun getName(): String = KeyboardToolbarGroupViewManagerImpl.NAME + + override fun createViewInstance(context: ThemedReactContext): KeyboardToolbarGroupReactViewGroup = + manager.createViewInstance(context) +} diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardToolbarExcludeViewManagerImpl.kt b/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardToolbarGroupViewManagerImpl.kt similarity index 56% rename from android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardToolbarExcludeViewManagerImpl.kt rename to android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardToolbarGroupViewManagerImpl.kt index 5a3078db0e..a2b6500096 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardToolbarExcludeViewManagerImpl.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardToolbarGroupViewManagerImpl.kt @@ -2,16 +2,16 @@ package com.reactnativekeyboardcontroller.managers import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.uimanager.ThemedReactContext -import com.reactnativekeyboardcontroller.views.KeyboardToolbarExcludeReactViewGroup +import com.reactnativekeyboardcontroller.views.KeyboardToolbarGroupReactViewGroup @Suppress("detekt:UnusedPrivateProperty") -class KeyboardToolbarExcludeViewManagerImpl( +class KeyboardToolbarGroupViewManagerImpl( mReactContext: ReactApplicationContext, ) { - fun createViewInstance(reactContext: ThemedReactContext): KeyboardToolbarExcludeReactViewGroup = - KeyboardToolbarExcludeReactViewGroup(reactContext) + fun createViewInstance(reactContext: ThemedReactContext): KeyboardToolbarGroupReactViewGroup = + KeyboardToolbarGroupReactViewGroup(reactContext) companion object { - const val NAME = "KeyboardToolbarExcludeView" + const val NAME = "KeyboardToolbarGroupView" } } diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/views/KeyboardToolbarExcludeReactViewGroup.kt b/android/src/main/java/com/reactnativekeyboardcontroller/views/KeyboardToolbarGroupReactViewGroup.kt similarity index 89% rename from android/src/main/java/com/reactnativekeyboardcontroller/views/KeyboardToolbarExcludeReactViewGroup.kt rename to android/src/main/java/com/reactnativekeyboardcontroller/views/KeyboardToolbarGroupReactViewGroup.kt index 2ef1253f34..7fbaa06be1 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/views/KeyboardToolbarExcludeReactViewGroup.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/views/KeyboardToolbarGroupReactViewGroup.kt @@ -5,7 +5,7 @@ import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.views.view.ReactViewGroup @SuppressLint("ViewConstructor") -class KeyboardToolbarExcludeReactViewGroup( +class KeyboardToolbarGroupReactViewGroup( reactContext: ThemedReactContext, ) : ReactViewGroup(reactContext) { // semantic view used in KeyboardToolbar traverse algorithm diff --git a/android/src/main/jni/RNKC.h b/android/src/main/jni/RNKC.h index 6a24460acd..c099b0ab8f 100644 --- a/android/src/main/jni/RNKC.h +++ b/android/src/main/jni/RNKC.h @@ -15,7 +15,7 @@ #include #include #include -#include +#include #include #include diff --git a/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardToolbarExcludeViewManager.kt b/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardToolbarGroupViewManager.kt similarity index 56% rename from android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardToolbarExcludeViewManager.kt rename to android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardToolbarGroupViewManager.kt index c27b93e93a..5d5c7f199d 100644 --- a/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardToolbarExcludeViewManager.kt +++ b/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardToolbarGroupViewManager.kt @@ -3,16 +3,16 @@ package com.reactnativekeyboardcontroller import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.views.view.ReactViewManager -import com.reactnativekeyboardcontroller.managers.KeyboardToolbarExcludeViewManagerImpl -import com.reactnativekeyboardcontroller.views.KeyboardToolbarExcludeReactViewGroup +import com.reactnativekeyboardcontroller.managers.KeyboardToolbarGroupViewManagerImpl +import com.reactnativekeyboardcontroller.views.KeyboardToolbarGroupReactViewGroup -class KeyboardToolbarExcludeViewManager( +class KeyboardToolbarGroupViewManager( mReactContext: ReactApplicationContext, ) : ReactViewManager() { - private val manager = KeyboardToolbarExcludeViewManagerImpl(mReactContext) + private val manager = KeyboardToolbarGroupViewManagerImpl(mReactContext) - override fun getName(): String = KeyboardToolbarExcludeViewManagerImpl.NAME + override fun getName(): String = KeyboardToolbarGroupViewManagerImpl.NAME - override fun createViewInstance(reactContext: ThemedReactContext): KeyboardToolbarExcludeReactViewGroup = + override fun createViewInstance(reactContext: ThemedReactContext): KeyboardToolbarGroupReactViewGroup = manager.createViewInstance(reactContext) } diff --git a/android/src/turbo/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt b/android/src/turbo/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt index 0ba47f7d52..a28f6be168 100644 --- a/android/src/turbo/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt +++ b/android/src/turbo/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt @@ -60,6 +60,6 @@ class KeyboardControllerPackage : TurboReactPackage() { KeyboardGestureAreaViewManager(), OverKeyboardViewManager(), KeyboardBackgroundViewManager(), - KeyboardToolbarExcludeViewManager(reactContext), + KeyboardToolbarGroupViewManager(reactContext), ) } diff --git a/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarExcludeViewShadowNode.h b/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarExcludeViewShadowNode.h deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewComponentDescriptor.h b/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewComponentDescriptor.h new file mode 100644 index 0000000000..fdb9546a51 --- /dev/null +++ b/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewComponentDescriptor.h @@ -0,0 +1,27 @@ +// +// RNKCKeyboardToolbarGroupViewComponentDescriptor.h +// Pods +// +// Created by Kiryl Ziusko on 26/12/2024. +// + +#pragma once + +#include "RNKCKeyboardToolbarGroupViewShadowNode.h" + +#include +#include +#include + +namespace facebook::react { +class KeyboardToolbarGroupViewComponentDescriptor final + : public ConcreteComponentDescriptor { + public: + using ConcreteComponentDescriptor::ConcreteComponentDescriptor; + void adopt(ShadowNode &shadowNode) const override { + react_native_assert(dynamic_cast(&shadowNode)); + ConcreteComponentDescriptor::adopt(shadowNode); + } +}; + +} // namespace facebook::react diff --git a/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewShadowNode.h b/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewShadowNode.h new file mode 100644 index 0000000000..35086b62fb --- /dev/null +++ b/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewShadowNode.h @@ -0,0 +1,30 @@ +// +// RNKCKeyboardToolbarGroupViewShadowNode.h +// Pods +// +// Created by Kiryl Ziusko on 26/12/2024. +// + +#pragma once + +#include "RNKCKeyboardToolbarGroupViewState.h" + +#include +#include +#include +#include + +namespace facebook::react { + +JSI_EXPORT extern const char KeyboardToolbarGroupViewComponentName[]; + +/* + * `ShadowNode` for component. + */ +using KeyboardToolbarGroupViewShadowNode = ConcreteViewShadowNode< + KeyboardToolbarGroupViewComponentName, + KeyboardToolbarGroupViewProps, + KeyboardToolbarGroupViewEventEmitter, + KeyboardToolbarGroupViewState>; + +} // namespace facebook::react diff --git a/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarExcludeViewState.h b/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewState.h similarity index 52% rename from common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarExcludeViewState.h rename to common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewState.h index 9d2f20cfbf..a69d6a6208 100644 --- a/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarExcludeViewState.h +++ b/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewState.h @@ -6,12 +6,12 @@ namespace facebook::react { -class KeyboardToolbarExcludeViewState { +class KeyboardToolbarGroupViewState { public: - KeyboardToolbarExcludeViewState() = default; + KeyboardToolbarGroupViewState() = default; #ifdef ANDROID - KeyboardToolbarExcludeViewState(KeyboardToolbarExcludeViewState const &previousState, folly::dynamic data) {} + KeyboardToolbarGroupViewState(KeyboardToolbarGroupViewState const &previousState, folly::dynamic data) {} folly::dynamic getDynamic() const { return {}; } diff --git a/ios/views/KeyboardToolbarExcludeViewManager.h b/ios/views/KeyboardToolbarGroupViewManager.h similarity index 73% rename from ios/views/KeyboardToolbarExcludeViewManager.h rename to ios/views/KeyboardToolbarGroupViewManager.h index 1265500d41..a2d478936d 100644 --- a/ios/views/KeyboardToolbarExcludeViewManager.h +++ b/ios/views/KeyboardToolbarGroupViewManager.h @@ -1,5 +1,5 @@ // -// KeyboardToolbarExcludeViewManager.h +// KeyboardToolbarGroupViewManager.h // KeyboardController // // Created by Kiryl Ziusko on 26/12/2024. @@ -13,10 +13,10 @@ #import #import -@interface KeyboardToolbarExcludeViewManager : RCTViewManager +@interface KeyboardToolbarGroupViewManager : RCTViewManager @end -@interface KeyboardToolbarExcludeView : +@interface KeyboardToolbarGroupView : #ifdef RCT_NEW_ARCH_ENABLED RCTViewComponentView #else diff --git a/ios/views/KeyboardToolbarExcludeViewManager.mm b/ios/views/KeyboardToolbarGroupViewManager.mm similarity index 67% rename from ios/views/KeyboardToolbarExcludeViewManager.mm rename to ios/views/KeyboardToolbarGroupViewManager.mm index 46fda7bdd4..96d7c163b3 100644 --- a/ios/views/KeyboardToolbarExcludeViewManager.mm +++ b/ios/views/KeyboardToolbarGroupViewManager.mm @@ -1,11 +1,11 @@ // -// KeyboardToolbarExcludeViewManager.mm +// KeyboardToolbarGroupViewManager.mm // react-native-keyboard-controller // // Created by Kiryl Ziusko on 26/12/2024. // -#import "KeyboardToolbarExcludeViewManager.h" +#import "KeyboardToolbarGroupViewManager.h" #ifdef RCT_NEW_ARCH_ENABLED #import @@ -23,9 +23,9 @@ #endif // MARK: Manager -@implementation KeyboardToolbarExcludeViewManager +@implementation KeyboardToolbarGroupViewManager -RCT_EXPORT_MODULE(KeyboardToolbarExcludeViewManager) +RCT_EXPORT_MODULE(KeyboardToolbarGroupViewManager) + (BOOL)requiresMainQueueSetup { @@ -35,7 +35,7 @@ + (BOOL)requiresMainQueueSetup #ifndef RCT_NEW_ARCH_ENABLED - (UIView *)view { - return [[KeyboardToolbarExcludeView alloc] initWithBridge:self.bridge]; + return [[KeyboardToolbarGroupView alloc] initWithBridge:self.bridge]; } #endif @@ -43,17 +43,17 @@ - (UIView *)view // MARK: View #ifdef RCT_NEW_ARCH_ENABLED -@interface KeyboardToolbarExcludeView () +@interface KeyboardToolbarGroupView () @end #endif -@implementation KeyboardToolbarExcludeView { +@implementation KeyboardToolbarGroupView { } #ifdef RCT_NEW_ARCH_ENABLED + (ComponentDescriptorProvider)componentDescriptorProvider { - return concreteComponentDescriptorProvider(); + return concreteComponentDescriptorProvider(); } #endif @@ -79,9 +79,9 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge #endif #ifdef RCT_NEW_ARCH_ENABLED -Class KeyboardToolbarExcludeViewCls(void) +Class KeyboardToolbarGroupViewCls(void) { - return KeyboardToolbarExcludeView.class; + return KeyboardToolbarGroupView.class; } #endif diff --git a/react-native.config.js b/react-native.config.js index 1fc6fa8e18..33228da090 100644 --- a/react-native.config.js +++ b/react-native.config.js @@ -8,7 +8,7 @@ module.exports = { "OverKeyboardViewComponentDescriptor", "KeyboardBackgroundViewComponentDescriptor", "ClippingScrollViewDecoratorViewComponentDescriptor", - "KeyboardToolbarExcludeViewComponentDescriptor", + "KeyboardToolbarGroupViewComponentDescriptor", ], cmakeListsPath: "../android/src/main/jni/CMakeLists.txt", }, diff --git a/src/bindings.native.ts b/src/bindings.native.ts index 1b8316b4c3..8f29cc10d2 100644 --- a/src/bindings.native.ts +++ b/src/bindings.native.ts @@ -8,6 +8,7 @@ import type { KeyboardEventsModule, KeyboardExtenderProps, KeyboardGestureAreaProps, + KeyboardToolbarGroupViewProps, OverKeyboardViewProps, WindowDimensionsEventsModule, } from "./types"; @@ -73,5 +74,5 @@ export const RCTKeyboardExtender: React.FC = : ({ children }: KeyboardExtenderProps) => children; export const ClippingScrollView: React.FC = require("./specs/ClippingScrollViewDecoratorViewNativeComponent").default; -export const RCTKeyboardToolbarExcludeView: React.FC = - require("./specs/KeyboardToolbarExcludeViewNativeComponent").default; +export const RCTKeyboardToolbarGroupView: React.FC = + require("./specs/KeyboardToolbarGroupViewNativeComponent").default; diff --git a/src/bindings.ts b/src/bindings.ts index 83179ac4c4..a91595afcd 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -9,7 +9,7 @@ import type { KeyboardEventsModule, KeyboardExtenderProps, KeyboardGestureAreaProps, - KeyboardToolbarExcludeViewProps, + KeyboardToolbarGroupViewProps, OverKeyboardViewProps, WindowDimensionsEventsModule, } from "./types"; @@ -93,5 +93,5 @@ export const RCTKeyboardExtender = */ export const ClippingScrollView = View as unknown as React.FC; -export const RCTKeyboardToolbarExcludeView = - View as unknown as React.FC; +export const RCTKeyboardToolbarGroupView = + View as unknown as React.FC; diff --git a/src/components/KeyboardToolbar/index.tsx b/src/components/KeyboardToolbar/index.tsx index 1afa55604a..8ec18c31c7 100644 --- a/src/components/KeyboardToolbar/index.tsx +++ b/src/components/KeyboardToolbar/index.tsx @@ -3,7 +3,7 @@ import { StyleSheet, View } from "react-native"; import { FocusedInputEvents, - RCTKeyboardToolbarExcludeView, + RCTKeyboardToolbarGroupView, } from "../../bindings"; import { useKeyboardState } from "../../hooks"; import KeyboardStickyView from "../KeyboardStickyView"; @@ -44,6 +44,7 @@ const KeyboardToolbar: React.FC & { Prev: typeof Prev; Next: typeof Next; Done: typeof Done; + Group: typeof RCTKeyboardToolbarGroupView; } = (props) => { const { children, @@ -230,7 +231,7 @@ KeyboardToolbar.Content = Content; KeyboardToolbar.Prev = Prev; KeyboardToolbar.Next = Next; KeyboardToolbar.Done = Done; -KeyboardToolbar.Exclude = RCTKeyboardToolbarExcludeView; +KeyboardToolbar.Group = RCTKeyboardToolbarGroupView; export { colors as DefaultKeyboardToolbarTheme, KeyboardToolbarProps }; export default KeyboardToolbar; diff --git a/src/specs/KeyboardToolbarExcludeViewNativeComponent.ts b/src/specs/KeyboardToolbarGroupViewNativeComponent.ts similarity index 92% rename from src/specs/KeyboardToolbarExcludeViewNativeComponent.ts rename to src/specs/KeyboardToolbarGroupViewNativeComponent.ts index c9d2fd14dd..93870efd71 100644 --- a/src/specs/KeyboardToolbarExcludeViewNativeComponent.ts +++ b/src/specs/KeyboardToolbarGroupViewNativeComponent.ts @@ -6,7 +6,7 @@ import type { ViewProps } from "react-native/Libraries/Components/View/ViewPropT export interface NativeProps extends ViewProps {} export default codegenNativeComponent( - "KeyboardToolbarExcludeView", + "KeyboardToolbarGroupView", { interfaceOnly: true, }, diff --git a/src/types/views.ts b/src/types/views.ts index c13446587a..e1da3bd28f 100644 --- a/src/types/views.ts +++ b/src/types/views.ts @@ -49,6 +49,7 @@ export type KeyboardExtenderProps = PropsWithChildren<{ /** Controls whether this `KeyboardExtender` instance should take an effect. Default is `true`. */ enabled?: boolean; }>; +export type KeyboardToolbarGroupViewProps = PropsWithChildren; export type ClippingScrollViewProps = PropsWithChildren< ViewProps & { /** An additional space that gets applied to the bottom of the `ScrollView` (inside a scrollable content). Default is `0`. */ From 7445e1cb9c9e191248cc152d859310eb8746ed02 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Tue, 10 Mar 2026 12:17:26 +0100 Subject: [PATCH 07/22] feat: update algorithm --- .../listeners/FocusedInputObserver.kt | 3 +- .../traversal/ViewHierarchyNavigator.kt | 36 +++++++++++-- ios/observers/FocusedInputObserver.swift | 3 +- ios/traversal/ViewHierarchyNavigator.swift | 52 +++++++++++++++++-- 4 files changed, 83 insertions(+), 11 deletions(-) diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputObserver.kt b/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputObserver.kt index 1e7be518b0..4de762dae0 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputObserver.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputObserver.kt @@ -120,7 +120,8 @@ class FocusedInputObserver( selectionSubscription = newFocus.addOnSelectionChangedListener(selectionListener) FocusedInputHolder.set(newFocus) - val allInputFields = ViewHierarchyNavigator.getAllInputFields(context?.rootView) + val groupAncestor = ViewHierarchyNavigator.findGroupAncestor(newFocus) + val allInputFields = ViewHierarchyNavigator.getAllInputFields(groupAncestor ?: context?.rootView) val currentIndex = allInputFields.indexOf(newFocus) context.emitEvent( diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/traversal/ViewHierarchyNavigator.kt b/android/src/main/java/com/reactnativekeyboardcontroller/traversal/ViewHierarchyNavigator.kt index 975aec8e84..13d43717dd 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/traversal/ViewHierarchyNavigator.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/traversal/ViewHierarchyNavigator.kt @@ -5,7 +5,7 @@ import android.view.ViewGroup import android.widget.EditText import com.facebook.react.bridge.UiThreadUtil import com.reactnativekeyboardcontroller.extensions.focus -import com.reactnativekeyboardcontroller.views.KeyboardToolbarExcludeReactViewGroup +import com.reactnativekeyboardcontroller.views.KeyboardToolbarGroupReactViewGroup object ViewHierarchyNavigator { fun setFocusTo( @@ -26,19 +26,40 @@ object ViewHierarchyNavigator { fun findEditTexts(view: View?) { if (isValidTextInput(view)) { editTexts.add(view as EditText) - } else if (view is ViewGroup && view !is KeyboardToolbarExcludeReactViewGroup) { + } else if (view is ViewGroup && view !is KeyboardToolbarGroupReactViewGroup) { for (i in 0 until view.childCount) { findEditTexts(view.getChildAt(i)) } } } - // Start the search with the provided viewGroup - findEditTexts(viewGroup) + // If the root is a group itself, search within it (for group-scoped queries) + if (viewGroup is KeyboardToolbarGroupReactViewGroup) { + for (i in 0 until viewGroup.childCount) { + findEditTexts(viewGroup.getChildAt(i)) + } + } else { + findEditTexts(viewGroup) + } return editTexts } + /** + * Finds the closest [KeyboardToolbarGroupReactViewGroup] ancestor of the given view. + * Returns null if the view is not inside any group. + */ + fun findGroupAncestor(view: View?): KeyboardToolbarGroupReactViewGroup? { + var current = view?.parent + while (current != null) { + if (current is KeyboardToolbarGroupReactViewGroup) { + return current + } + current = current.parent + } + return null + } + private fun findNextEditText(currentFocus: View): EditText? = findEditTextInDirection(currentFocus, 1) private fun findPreviousEditText(currentFocus: View): EditText? = findEditTextInDirection(currentFocus, -1) @@ -65,6 +86,11 @@ object ViewHierarchyNavigator { i += direction } + // Don't navigate outside the group boundary + if (parentViewGroup is KeyboardToolbarGroupReactViewGroup) { + return null + } + // Recurse to the parent's parent if no sibling EditText is found return findEditTextInDirection(parentViewGroup, direction) } @@ -92,7 +118,7 @@ object ViewHierarchyNavigator { if (isValidTextInput(child)) { result = child as EditText - } else if (child is ViewGroup && child !is KeyboardToolbarExcludeReactViewGroup) { + } else if (child is ViewGroup && child !is KeyboardToolbarGroupReactViewGroup) { // If the child is a ViewGroup, check its children recursively result = findEditTextInHierarchy(child, direction) } diff --git a/ios/observers/FocusedInputObserver.swift b/ios/observers/FocusedInputObserver.swift index 1f01ebd7ef..74ede28b97 100644 --- a/ios/observers/FocusedInputObserver.swift +++ b/ios/observers/FocusedInputObserver.swift @@ -151,7 +151,8 @@ public class FocusedInputObserver: NSObject { FocusedInputHolder.shared.set(currentResponder as? TextInput) - let allInputFields = ViewHierarchyNavigator.getAllInputFields() + let groupAncestor = ViewHierarchyNavigator.findGroupAncestor(currentResponder as? UIView) + let allInputFields = ViewHierarchyNavigator.getAllInputFields(root: groupAncestor) let currentIndex = allInputFields.firstIndex(where: { $0 == currentResponder }) ?? -1 onFocusDidSet([ diff --git a/ios/traversal/ViewHierarchyNavigator.swift b/ios/traversal/ViewHierarchyNavigator.swift index 49636e6b57..29164a8576 100644 --- a/ios/traversal/ViewHierarchyNavigator.swift +++ b/ios/traversal/ViewHierarchyNavigator.swift @@ -11,6 +11,8 @@ import UIKit @objc(ViewHierarchyNavigator) public class ViewHierarchyNavigator: NSObject { + private static let groupViewTypeName = "KeyboardToolbarGroupView" + @objc public static func setFocusTo(direction: String) { DispatchQueue.main.async { if direction == "current" { @@ -30,30 +32,67 @@ public class ViewHierarchyNavigator: NSObject { } public static func getAllInputFields() -> [TextInput] { + return getAllInputFields(root: nil) + } + + public static func getAllInputFields(root: UIView?) -> [TextInput] { var textInputs = [TextInput]() - guard let rootView = UIApplication.topViewController()?.view else { + let rootView: UIView? + if let root = root { + rootView = root + } else { + rootView = UIApplication.topViewController()?.view + } + + guard let rootView = rootView else { return [] } + let isGroupRoot = isGroupView(rootView) + /// Helper function to recursively search for TextInput views func findTextInputs(in view: UIView?) { guard let view = view else { return } if let textInput = isValidTextInput(view) { textInputs.append(textInput) - } else if String(describing: type(of: view)) != "KeyboardToolbarExcludeView" { + } else if !isGroupView(view) { for subview in view.subviews { findTextInputs(in: subview) } } } - findTextInputs(in: rootView) + if isGroupRoot { + // When root is a group, search its children directly + for subview in rootView.subviews { + findTextInputs(in: subview) + } + } else { + findTextInputs(in: rootView) + } return textInputs } + /// Finds the closest KeyboardToolbarGroupView ancestor of the given view. + /// Returns nil if the view is not inside any group. + public static func findGroupAncestor(_ view: UIView?) -> UIView? { + var current = view?.superview + while let parent = current { + if isGroupView(parent) { + return parent + } + current = parent.superview + } + return nil + } + + private static func isGroupView(_ view: UIView) -> Bool { + return String(describing: type(of: view)) == groupViewTypeName + } + private static func findTextInputInDirection(currentFocus: UIView, direction: String) -> TextInput? { // Find the parent view group guard let parentViewGroup = currentFocus.superview else { @@ -81,6 +120,11 @@ public class ViewHierarchyNavigator: NSObject { } } + // Don't navigate outside the group boundary + if isGroupView(parentViewGroup) { + return nil + } + // If no next or previous sibling was found in the parent, recurse to the parent's parent return findTextInputInDirection(currentFocus: parentViewGroup, direction: direction) } @@ -91,7 +135,7 @@ public class ViewHierarchyNavigator: NSObject { return validTextInput } - guard String(describing: type(of: view)) != "KeyboardToolbarExcludeView" else { return nil } + guard !isGroupView(view) else { return nil } // Determine the iteration order based on the direction let subviews = direction == "next" ? view.subviews : view.subviews.reversed() From 138b7c101b132a5e96de3266e6e97c4ca01f60fe Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Tue, 10 Mar 2026 12:20:03 +0100 Subject: [PATCH 08/22] feat: updated tests (native) --- .../ViewHierarchyNavigatorGroupTest.kt | 161 ++++++++++++++++++ .../project.pbxproj | 4 + .../KeyboardToolbarGroupTests.swift | 160 +++++++++++++++++ 3 files changed, 325 insertions(+) create mode 100644 android/src/test/java/com/reactnativekeyboardcontroller/traversal/ViewHierarchyNavigatorGroupTest.kt create mode 100644 ios/KeyboardControllerNative/KeyboardControllerNativeTests/KeyboardToolbarGroupTests.swift diff --git a/android/src/test/java/com/reactnativekeyboardcontroller/traversal/ViewHierarchyNavigatorGroupTest.kt b/android/src/test/java/com/reactnativekeyboardcontroller/traversal/ViewHierarchyNavigatorGroupTest.kt new file mode 100644 index 0000000000..8670458d96 --- /dev/null +++ b/android/src/test/java/com/reactnativekeyboardcontroller/traversal/ViewHierarchyNavigatorGroupTest.kt @@ -0,0 +1,161 @@ +package com.reactnativekeyboardcontroller.traversal + +import android.content.Context +import android.widget.EditText +import android.widget.LinearLayout +import androidx.test.core.app.ApplicationProvider +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ThemedReactContext +import com.reactnativekeyboardcontroller.extensions.focus +import com.reactnativekeyboardcontroller.views.KeyboardToolbarGroupReactViewGroup +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.shadows.ShadowLooper + +@RunWith(RobolectricTestRunner::class) +class ViewHierarchyNavigatorGroupTest { + private lateinit var layout: LinearLayout + private lateinit var editText1: EditText + private lateinit var editText2: EditText + private lateinit var groupEditText1: EditText + private lateinit var groupEditText2: EditText + private lateinit var groupEditText3: EditText + private lateinit var editText3: EditText + private lateinit var editText4: EditText + private lateinit var group: KeyboardToolbarGroupReactViewGroup + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + val reactContext = ReactApplicationContext(context) + val themedReactContext = ThemedReactContext(reactContext, context) + + editText1 = EditText(context).apply { id = 1 } + editText2 = EditText(context).apply { id = 2 } + groupEditText1 = EditText(context).apply { id = 3 } + groupEditText2 = EditText(context).apply { id = 4 } + groupEditText3 = EditText(context).apply { id = 5 } + editText3 = EditText(context).apply { id = 6 } + editText4 = EditText(context).apply { id = 7 } + + group = + KeyboardToolbarGroupReactViewGroup(themedReactContext).apply { + addView(groupEditText1) + addView(groupEditText2) + addView(groupEditText3) + } + + // Layout: editText1, editText2, [group: gET1, gET2, gET3], editText3, editText4 + layout = + LinearLayout(context).apply { + addView(editText1) + addView(editText2) + addView(group) + addView(editText3) + addView(editText4) + } + } + + @Test + fun `getAllInputFields should not include inputs inside a group`() { + val editTexts = ViewHierarchyNavigator.getAllInputFields(layout) + + // Only editText1, editText2, editText3, editText4 (group inputs excluded) + assertTrue(editTexts.size == 4) + } + + @Test + fun `getAllInputFields with group as root should return only group inputs`() { + val editTexts = ViewHierarchyNavigator.getAllInputFields(group) + + assertTrue(editTexts.size == 3) + } + + @Test + fun `setFocusTo 'next' inside group should stay within group`() { + groupEditText1.focus() + + ViewHierarchyNavigator.setFocusTo("next", groupEditText1) + + ShadowLooper.runUiThreadTasksIncludingDelayedTasks() + + assertTrue(groupEditText2.hasFocus()) + } + + @Test + fun `setFocusTo 'prev' inside group should stay within group`() { + groupEditText3.focus() + + ViewHierarchyNavigator.setFocusTo("prev", groupEditText3) + + ShadowLooper.runUiThreadTasksIncludingDelayedTasks() + + assertTrue(groupEditText2.hasFocus()) + } + + @Test + fun `setFocusTo 'next' at last group input should not leave group`() { + groupEditText3.focus() + + ViewHierarchyNavigator.setFocusTo("next", groupEditText3) + + ShadowLooper.runUiThreadTasksIncludingDelayedTasks() + + // Should stay on last group input, not move to editText3 + assertTrue(groupEditText3.hasFocus()) + } + + @Test + fun `setFocusTo 'prev' at first group input should not leave group`() { + groupEditText1.focus() + + ViewHierarchyNavigator.setFocusTo("prev", groupEditText1) + + ShadowLooper.runUiThreadTasksIncludingDelayedTasks() + + // Should stay on first group input, not move to editText2 + assertTrue(groupEditText1.hasFocus()) + } + + @Test + fun `setFocusTo 'next' outside group should skip group inputs`() { + editText2.focus() + + ViewHierarchyNavigator.setFocusTo("next", editText2) + + ShadowLooper.runUiThreadTasksIncludingDelayedTasks() + + // Should skip group and go to editText3 + assertTrue(editText3.hasFocus()) + } + + @Test + fun `setFocusTo 'prev' outside group should skip group inputs`() { + editText3.focus() + + ViewHierarchyNavigator.setFocusTo("prev", editText3) + + ShadowLooper.runUiThreadTasksIncludingDelayedTasks() + + // Should skip group and go to editText2 + assertTrue(editText2.hasFocus()) + } + + @Test + fun `findGroupAncestor should return group for inputs inside group`() { + val ancestor = ViewHierarchyNavigator.findGroupAncestor(groupEditText1) + + assertTrue(ancestor === group) + } + + @Test + fun `findGroupAncestor should return null for inputs outside group`() { + val ancestor = ViewHierarchyNavigator.findGroupAncestor(editText1) + + assertNull(ancestor) + } +} diff --git a/ios/KeyboardControllerNative/KeyboardControllerNative.xcodeproj/project.pbxproj b/ios/KeyboardControllerNative/KeyboardControllerNative.xcodeproj/project.pbxproj index 3c9bae28b7..c80a0bd66d 100644 --- a/ios/KeyboardControllerNative/KeyboardControllerNative.xcodeproj/project.pbxproj +++ b/ios/KeyboardControllerNative/KeyboardControllerNative.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 0837001E2CE8CA4F00D67BBF /* TextInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0837001D2CE8CA4F00D67BBF /* TextInput.swift */; }; 0837001F2CE8CA4F00D67BBF /* TextInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0837001D2CE8CA4F00D67BBF /* TextInput.swift */; }; 083700202CE8CA4F00D67BBF /* TextInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0837001D2CE8CA4F00D67BBF /* TextInput.swift */; }; + 083C81EB2F6035A5005EDD4C /* KeyboardToolbarGroupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083C81EA2F6035A5005EDD4C /* KeyboardToolbarGroupTests.swift */; }; 0850CCBA2CB49ECC000C0F8D /* SpringAnimationPerformanceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0850CCB92CB49ECC000C0F8D /* SpringAnimationPerformanceTest.swift */; }; 0850CCC22CB49F74000C0F8D /* SpringAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0850CCBF2CB49F74000C0F8D /* SpringAnimation.swift */; }; 0850CCC32CB49F74000C0F8D /* SpringAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0850CCBF2CB49F74000C0F8D /* SpringAnimation.swift */; }; @@ -76,6 +77,7 @@ 081006AB2C36906800578E07 /* UIScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UIScrollView.swift; path = ../../extensions/UIScrollView.swift; sourceTree = ""; }; 0828F07A2D4BFFDC005D4701 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UIView.swift; path = ../extensions/UIView.swift; sourceTree = SOURCE_ROOT; }; 0837001D2CE8CA4F00D67BBF /* TextInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TextInput.swift; path = ../protocols/TextInput.swift; sourceTree = SOURCE_ROOT; }; + 083C81EA2F6035A5005EDD4C /* KeyboardToolbarGroupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardToolbarGroupTests.swift; sourceTree = ""; }; 0850CCB92CB49ECC000C0F8D /* SpringAnimationPerformanceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpringAnimationPerformanceTest.swift; sourceTree = ""; }; 0850CCBF2CB49F74000C0F8D /* SpringAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SpringAnimation.swift; path = ../../animations/SpringAnimation.swift; sourceTree = ""; }; 0850CCC02CB49F74000C0F8D /* KeyboardAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = KeyboardAnimation.swift; path = ../../animations/KeyboardAnimation.swift; sourceTree = ""; }; @@ -177,6 +179,7 @@ 0873ED652BB6B7390004F3A4 /* KeyboardControllerNativeTests */ = { isa = PBXGroup; children = ( + 083C81EA2F6035A5005EDD4C /* KeyboardToolbarGroupTests.swift */, 0873ED662BB6B7390004F3A4 /* KeyboardControllerNativeTests.swift */, 0850CCB92CB49ECC000C0F8D /* SpringAnimationPerformanceTest.swift */, 08833D512CB56DB9007D4380 /* TimingAnimationPerformanceTest.swift */, @@ -358,6 +361,7 @@ 081006AD2C36906900578E07 /* UIScrollView.swift in Sources */, 0850CCCD2CB4A096000C0F8D /* UIUtils.swift in Sources */, 08833D522CB56DB9007D4380 /* TimingAnimationPerformanceTest.swift in Sources */, + 083C81EB2F6035A5005EDD4C /* KeyboardToolbarGroupTests.swift in Sources */, 083700202CE8CA4F00D67BBF /* TextInput.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/KeyboardControllerNative/KeyboardControllerNativeTests/KeyboardToolbarGroupTests.swift b/ios/KeyboardControllerNative/KeyboardControllerNativeTests/KeyboardToolbarGroupTests.swift new file mode 100644 index 0000000000..a7eb21ee9a --- /dev/null +++ b/ios/KeyboardControllerNative/KeyboardControllerNativeTests/KeyboardToolbarGroupTests.swift @@ -0,0 +1,160 @@ +// +// KeyboardToolbarGroupTests.swift +// KeyboardControllerNativeTests +// +// Created by Kiryl Ziusko on 10/03/2026. +// + +@testable import KeyboardControllerNative +import XCTest + +/// A mock group view whose type name matches what ViewHierarchyNavigator checks. +class KeyboardToolbarGroupView: UIView {} + +final class KeyboardToolbarGroupTests: XCTestCase { + var rootView: UIView! + var groupView: KeyboardToolbarGroupView! + var editText1: TestableInput! + var editText2: TestableInput! + var groupEditText1: TestableInput! + var groupEditText2: TestableInput! + var groupEditText3: TestableInput! + var editText3: TestableInput! + var editText4: TestableInput! + + override func setUpWithError() throws { + super.setUp() + + rootView = UIView() + groupView = KeyboardToolbarGroupView() + + editText1 = TestableTextField() + editText1.tag = 1 + editText2 = TestableTextView() + editText2.tag = 2 + groupEditText1 = TestableTextField() + groupEditText1.tag = 3 + groupEditText2 = TestableTextView() + groupEditText2.tag = 4 + groupEditText3 = TestableTextField() + groupEditText3.tag = 5 + editText3 = TestableTextView() + editText3.tag = 6 + editText4 = TestableTextField() + editText4.tag = 7 + + groupView.addSubview(groupEditText1) + groupView.addSubview(groupEditText2) + groupView.addSubview(groupEditText3) + + // Layout: editText1, editText2, [group: gET1, gET2, gET3], editText3, editText4 + rootView.addSubview(editText1) + rootView.addSubview(editText2) + rootView.addSubview(groupView) + rootView.addSubview(editText3) + rootView.addSubview(editText4) + } + + // MARK: - getAllInputFields + + func testGetAllInputFieldsExcludesGroupInputs() { + let allFields = ViewHierarchyNavigator.getAllInputFields(root: rootView) + + // Only editText1, editText2, editText3, editText4 (group inputs excluded) + XCTAssertEqual(allFields.count, 4) + } + + func testGetAllInputFieldsWithGroupRootReturnsOnlyGroupInputs() { + let groupFields = ViewHierarchyNavigator.getAllInputFields(root: groupView) + + XCTAssertEqual(groupFields.count, 3) + } + + // MARK: - Navigation within group + + func testNextInsideGroupStaysWithinGroup() { + FocusedInputHolder.shared.set(groupEditText1 as? TextInput) + + ViewHierarchyNavigator.setFocusTo(direction: "next") + + waitForFocusChange(to: groupEditText2) + } + + func testPrevInsideGroupStaysWithinGroup() { + FocusedInputHolder.shared.set(groupEditText3 as? TextInput) + + ViewHierarchyNavigator.setFocusTo(direction: "prev") + + waitForFocusChange(to: groupEditText2) + } + + // MARK: - Group boundary: cannot leave + + func testNextAtLastGroupInputDoesNotLeaveGroup() { + FocusedInputHolder.shared.set(groupEditText3 as? TextInput) + + ViewHierarchyNavigator.setFocusTo(direction: "next") + + // Should NOT move to editText3 — should stay at groupEditText3 + let expectation = XCTestExpectation(description: "Wait for main queue") + DispatchQueue.main.async { + XCTAssertFalse( + (self.editText3 as! TestableInput).becomeFirstResponderCalled, + "Should not have moved focus to editText3 outside group" + ) + expectation.fulfill() + } + wait(for: [expectation], timeout: 10.0) + } + + func testPrevAtFirstGroupInputDoesNotLeaveGroup() { + FocusedInputHolder.shared.set(groupEditText1 as? TextInput) + + ViewHierarchyNavigator.setFocusTo(direction: "prev") + + // Should NOT move to editText2 — should stay at groupEditText1 + let expectation = XCTestExpectation(description: "Wait for main queue") + DispatchQueue.main.async { + XCTAssertFalse( + (self.editText2 as! TestableInput).becomeFirstResponderCalled, + "Should not have moved focus to editText2 outside group" + ) + expectation.fulfill() + } + wait(for: [expectation], timeout: 10.0) + } + + // MARK: - Navigation outside group skips group inputs + + func testNextOutsideGroupSkipsGroupInputs() { + FocusedInputHolder.shared.set(editText2 as? TextInput) + + ViewHierarchyNavigator.setFocusTo(direction: "next") + + // Should skip group and go to editText3 + waitForFocusChange(to: editText3) + } + + func testPrevOutsideGroupSkipsGroupInputs() { + FocusedInputHolder.shared.set(editText3 as? TextInput) + + ViewHierarchyNavigator.setFocusTo(direction: "prev") + + // Should skip group and go to editText2 + waitForFocusChange(to: editText2) + } + + // MARK: - findGroupAncestor + + func testFindGroupAncestorReturnsGroupForInputsInsideGroup() { + let ancestor = ViewHierarchyNavigator.findGroupAncestor(groupEditText1) + + XCTAssertTrue(ancestor === groupView) + } + + func testFindGroupAncestorReturnsNilForInputsOutsideGroup() { + let ancestor = ViewHierarchyNavigator.findGroupAncestor(editText1) + + XCTAssertNil(ancestor) + } +} From 2aad44b942de35e37a9affeea3b6ae642329e509 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Tue, 10 Mar 2026 12:21:33 +0100 Subject: [PATCH 09/22] docs: update docs page --- .../api/components/keyboard-toolbar/index.mdx | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/docs/docs/api/components/keyboard-toolbar/index.mdx b/docs/docs/api/components/keyboard-toolbar/index.mdx index a5d8245db5..24d58d15ee 100644 --- a/docs/docs/api/components/keyboard-toolbar/index.mdx +++ b/docs/docs/api/components/keyboard-toolbar/index.mdx @@ -389,24 +389,26 @@ Don't forget that you need to specify colors for **both** `dark` and `light` the ## Components -### `KeyboardToolbar.Exclude` +### `KeyboardToolbar.Group` -This component is used to exclude some views from the traversal. It is useful when you want to skip specific view from being focused by toolbar arrow buttons. +This component defines a group of inputs that form an isolated navigation region. When the user presses the prev/next arrows on `KeyboardToolbar`, the focus will only cycle through inputs **within the same group**. Inputs outside the group are not reachable from within, and inputs inside the group are not reachable from outside. + +This is useful for scenarios like bottom sheets, tab views, or any UI where a subset of inputs should have independent navigation. ```tsx - - - + + + + + ``` +:::info Toolbar button state +When a grouped input is focused, the toolbar's prev/next buttons reflect the position within that group (not the global list of inputs). For example, the "prev" button will be disabled when focused on the first input of a group. +::: + ## Example ```tsx From 86bc1911bfb42daa45db840366090c9e5d527d60 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Tue, 10 Mar 2026 12:21:42 +0100 Subject: [PATCH 10/22] chore: update example app --- example/src/screens/Examples/Toolbar/index.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/example/src/screens/Examples/Toolbar/index.tsx b/example/src/screens/Examples/Toolbar/index.tsx index b4d1811ca4..e3e6da2220 100644 --- a/example/src/screens/Examples/Toolbar/index.tsx +++ b/example/src/screens/Examples/Toolbar/index.tsx @@ -154,16 +154,22 @@ function Form() { title="Flat" onFocus={onHideAutoFill} /> - + - + + Date: Tue, 10 Mar 2026 12:28:15 +0100 Subject: [PATCH 11/22] fix: changes after self review --- .../KeyboardControllerPackage.kt | 2 +- src/specs/KeyboardToolbarGroupViewNativeComponent.ts | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/android/src/turbo/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt b/android/src/turbo/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt index a28f6be168..7cc100fce8 100644 --- a/android/src/turbo/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt +++ b/android/src/turbo/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt @@ -60,6 +60,6 @@ class KeyboardControllerPackage : TurboReactPackage() { KeyboardGestureAreaViewManager(), OverKeyboardViewManager(), KeyboardBackgroundViewManager(), - KeyboardToolbarGroupViewManager(reactContext), + KeyboardToolbarGroupViewManager(), ) } diff --git a/src/specs/KeyboardToolbarGroupViewNativeComponent.ts b/src/specs/KeyboardToolbarGroupViewNativeComponent.ts index 93870efd71..b1116c1ae9 100644 --- a/src/specs/KeyboardToolbarGroupViewNativeComponent.ts +++ b/src/specs/KeyboardToolbarGroupViewNativeComponent.ts @@ -5,9 +5,6 @@ import type { ViewProps } from "react-native/Libraries/Components/View/ViewPropT export interface NativeProps extends ViewProps {} -export default codegenNativeComponent( - "KeyboardToolbarGroupView", - { - interfaceOnly: true, - }, -) as HostComponent; +export default codegenNativeComponent("KeyboardToolbarGroupView", { + interfaceOnly: true, +}) as HostComponent; From 64ac6c90157d21fe204745c9147a320d4246f226 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Tue, 10 Mar 2026 12:37:48 +0100 Subject: [PATCH 12/22] fix: android compilation --- .../KeyboardToolbarGroupViewManager.kt | 8 +++----- .../managers/KeyboardToolbarGroupViewManagerImpl.kt | 6 +----- .../KeyboardToolbarGroupViewManager.kt | 7 ++----- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardToolbarGroupViewManager.kt b/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardToolbarGroupViewManager.kt index 68dfbeeb01..75aa3bf11a 100644 --- a/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardToolbarGroupViewManager.kt +++ b/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardToolbarGroupViewManager.kt @@ -1,6 +1,5 @@ package com.reactnativekeyboardcontroller -import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.ViewGroupManager import com.facebook.react.uimanager.ViewManagerDelegate @@ -10,11 +9,10 @@ import com.reactnativekeyboardcontroller.managers.KeyboardToolbarGroupViewManage import com.reactnativekeyboardcontroller.views.KeyboardToolbarGroupReactViewGroup import com.reactnativekeyboardcontroller.views.overlay.OverKeyboardHostView -class KeyboardToolbarGroupViewManager( - mReactContext: ReactApplicationContext, -) : ViewGroupManager(), +class KeyboardToolbarGroupViewManager : + ViewGroupManager(), KeyboardToolbarGroupViewManagerInterface { - private val manager = KeyboardToolbarGroupViewManagerImpl(mReactContext) + private val manager = KeyboardToolbarGroupViewManagerImpl() private val mDelegate = KeyboardToolbarGroupViewManagerDelegate(this) override fun getDelegate(): ViewManagerDelegate = mDelegate diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardToolbarGroupViewManagerImpl.kt b/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardToolbarGroupViewManagerImpl.kt index a2b6500096..1dd6b5ea8e 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardToolbarGroupViewManagerImpl.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardToolbarGroupViewManagerImpl.kt @@ -1,13 +1,9 @@ package com.reactnativekeyboardcontroller.managers -import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.uimanager.ThemedReactContext import com.reactnativekeyboardcontroller.views.KeyboardToolbarGroupReactViewGroup -@Suppress("detekt:UnusedPrivateProperty") -class KeyboardToolbarGroupViewManagerImpl( - mReactContext: ReactApplicationContext, -) { +class KeyboardToolbarGroupViewManagerImpl { fun createViewInstance(reactContext: ThemedReactContext): KeyboardToolbarGroupReactViewGroup = KeyboardToolbarGroupReactViewGroup(reactContext) diff --git a/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardToolbarGroupViewManager.kt b/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardToolbarGroupViewManager.kt index 5d5c7f199d..074f8c1791 100644 --- a/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardToolbarGroupViewManager.kt +++ b/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardToolbarGroupViewManager.kt @@ -1,15 +1,12 @@ package com.reactnativekeyboardcontroller -import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.views.view.ReactViewManager import com.reactnativekeyboardcontroller.managers.KeyboardToolbarGroupViewManagerImpl import com.reactnativekeyboardcontroller.views.KeyboardToolbarGroupReactViewGroup -class KeyboardToolbarGroupViewManager( - mReactContext: ReactApplicationContext, -) : ReactViewManager() { - private val manager = KeyboardToolbarGroupViewManagerImpl(mReactContext) +class KeyboardToolbarGroupViewManager : ReactViewManager() { + private val manager = KeyboardToolbarGroupViewManagerImpl() override fun getName(): String = KeyboardToolbarGroupViewManagerImpl.NAME From e7168f2daa83c317e7286da364e523d01b8095b9 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Tue, 10 Mar 2026 12:49:23 +0100 Subject: [PATCH 13/22] fix: fabric builds --- .../KeyboardToolbarGroupViewManager.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardToolbarGroupViewManager.kt b/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardToolbarGroupViewManager.kt index 75aa3bf11a..b8399e90a2 100644 --- a/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardToolbarGroupViewManager.kt +++ b/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardToolbarGroupViewManager.kt @@ -7,11 +7,10 @@ import com.facebook.react.viewmanagers.KeyboardToolbarGroupViewManagerDelegate import com.facebook.react.viewmanagers.KeyboardToolbarGroupViewManagerInterface import com.reactnativekeyboardcontroller.managers.KeyboardToolbarGroupViewManagerImpl import com.reactnativekeyboardcontroller.views.KeyboardToolbarGroupReactViewGroup -import com.reactnativekeyboardcontroller.views.overlay.OverKeyboardHostView class KeyboardToolbarGroupViewManager : ViewGroupManager(), - KeyboardToolbarGroupViewManagerInterface { + KeyboardToolbarGroupViewManagerInterface { private val manager = KeyboardToolbarGroupViewManagerImpl() private val mDelegate = KeyboardToolbarGroupViewManagerDelegate(this) From 455ff6eb6155f8c92252bd9c6ae17c26b0c5b2d8 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Tue, 10 Mar 2026 12:57:50 +0100 Subject: [PATCH 14/22] fix: build issues --- .../views/KeyboardToolbarGroupReactViewGroup.kt | 7 ++++--- .../traversal/ViewHierarchyNavigatorGroupTest.kt | 6 +----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/views/KeyboardToolbarGroupReactViewGroup.kt b/android/src/main/java/com/reactnativekeyboardcontroller/views/KeyboardToolbarGroupReactViewGroup.kt index 7fbaa06be1..09b1b23ef5 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/views/KeyboardToolbarGroupReactViewGroup.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/views/KeyboardToolbarGroupReactViewGroup.kt @@ -1,12 +1,13 @@ package com.reactnativekeyboardcontroller.views import android.annotation.SuppressLint +import android.content.Context import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.views.view.ReactViewGroup @SuppressLint("ViewConstructor") -class KeyboardToolbarGroupReactViewGroup( - reactContext: ThemedReactContext, -) : ReactViewGroup(reactContext) { +class KeyboardToolbarGroupReactViewGroup : ReactViewGroup { + constructor(reactContext: ThemedReactContext) : super(reactContext) + internal constructor(context: Context) : super(context) // semantic view used in KeyboardToolbar traverse algorithm } diff --git a/android/src/test/java/com/reactnativekeyboardcontroller/traversal/ViewHierarchyNavigatorGroupTest.kt b/android/src/test/java/com/reactnativekeyboardcontroller/traversal/ViewHierarchyNavigatorGroupTest.kt index 8670458d96..939af4c7e5 100644 --- a/android/src/test/java/com/reactnativekeyboardcontroller/traversal/ViewHierarchyNavigatorGroupTest.kt +++ b/android/src/test/java/com/reactnativekeyboardcontroller/traversal/ViewHierarchyNavigatorGroupTest.kt @@ -4,8 +4,6 @@ import android.content.Context import android.widget.EditText import android.widget.LinearLayout import androidx.test.core.app.ApplicationProvider -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.uimanager.ThemedReactContext import com.reactnativekeyboardcontroller.extensions.focus import com.reactnativekeyboardcontroller.views.KeyboardToolbarGroupReactViewGroup import org.junit.Assert.assertNull @@ -31,8 +29,6 @@ class ViewHierarchyNavigatorGroupTest { @Before fun setUp() { val context = ApplicationProvider.getApplicationContext() - val reactContext = ReactApplicationContext(context) - val themedReactContext = ThemedReactContext(reactContext, context) editText1 = EditText(context).apply { id = 1 } editText2 = EditText(context).apply { id = 2 } @@ -43,7 +39,7 @@ class ViewHierarchyNavigatorGroupTest { editText4 = EditText(context).apply { id = 7 } group = - KeyboardToolbarGroupReactViewGroup(themedReactContext).apply { + KeyboardToolbarGroupReactViewGroup(context).apply { addView(groupEditText1) addView(groupEditText2) addView(groupEditText3) From da715bb01ae64127c785cb35612876f42d134394 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Tue, 10 Mar 2026 13:28:44 +0100 Subject: [PATCH 15/22] fix: iOS fabric build --- ios/views/KeyboardToolbarGroupViewManager.mm | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ios/views/KeyboardToolbarGroupViewManager.mm b/ios/views/KeyboardToolbarGroupViewManager.mm index 96d7c163b3..3c91700015 100644 --- a/ios/views/KeyboardToolbarGroupViewManager.mm +++ b/ios/views/KeyboardToolbarGroupViewManager.mm @@ -8,10 +8,9 @@ #import "KeyboardToolbarGroupViewManager.h" #ifdef RCT_NEW_ARCH_ENABLED -#import -#import -#import -#import +#import +#import +#import #import "RCTFabricComponentsPlugins.h" #endif From 24b1c8223acf1b63495b09b99dd75024a5869673 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Tue, 10 Mar 2026 13:36:18 +0100 Subject: [PATCH 16/22] fix: swiftlint --- .../KeyboardToolbarGroupTests.swift | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/ios/KeyboardControllerNative/KeyboardControllerNativeTests/KeyboardToolbarGroupTests.swift b/ios/KeyboardControllerNative/KeyboardControllerNativeTests/KeyboardToolbarGroupTests.swift index a7eb21ee9a..7b629a693f 100644 --- a/ios/KeyboardControllerNative/KeyboardControllerNativeTests/KeyboardToolbarGroupTests.swift +++ b/ios/KeyboardControllerNative/KeyboardControllerNativeTests/KeyboardToolbarGroupTests.swift @@ -98,10 +98,14 @@ final class KeyboardToolbarGroupTests: XCTestCase { // Should NOT move to editText3 — should stay at groupEditText3 let expectation = XCTestExpectation(description: "Wait for main queue") DispatchQueue.main.async { - XCTAssertFalse( - (self.editText3 as! TestableInput).becomeFirstResponderCalled, - "Should not have moved focus to editText3 outside group" - ) + if let input = self.editText3 as? TestableInput { + XCTAssertFalse( + input.becomeFirstResponderCalled, + "Should not have moved focus to editText3 outside group" + ) + } else { + XCTFail("editText3 is not a TestableInput") + } expectation.fulfill() } wait(for: [expectation], timeout: 10.0) @@ -115,10 +119,14 @@ final class KeyboardToolbarGroupTests: XCTestCase { // Should NOT move to editText2 — should stay at groupEditText1 let expectation = XCTestExpectation(description: "Wait for main queue") DispatchQueue.main.async { - XCTAssertFalse( - (self.editText2 as! TestableInput).becomeFirstResponderCalled, - "Should not have moved focus to editText2 outside group" - ) + if let input = self.editText2 as? TestableInput { + XCTAssertFalse( + input.becomeFirstResponderCalled, + "Should not have moved focus to editText2 outside group" + ) + } else { + XCTFail("editText2 is not a TestableInput") + } expectation.fulfill() } wait(for: [expectation], timeout: 10.0) From 39d38cb9d19e9966863200203ac188413b199042 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Tue, 10 Mar 2026 14:09:07 +0100 Subject: [PATCH 17/22] fix: fabric compilation issues --- .../RNKC/RNKCKeyboardToolbarGroupViewShadowNode.cpp | 7 +++++++ ios/views/KeyboardToolbarGroupViewManager.mm | 1 + 2 files changed, 8 insertions(+) create mode 100644 common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewShadowNode.cpp diff --git a/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewShadowNode.cpp b/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewShadowNode.cpp new file mode 100644 index 0000000000..876bff3cc7 --- /dev/null +++ b/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewShadowNode.cpp @@ -0,0 +1,7 @@ +#include "RNKCKeyboardToolbarGroupViewShadowNode.h" + +namespace facebook::react { + +extern const char KeyboardToolbarGroupViewComponentName[] = "KeyboardToolbarGroupView"; + +} // namespace facebook::react diff --git a/ios/views/KeyboardToolbarGroupViewManager.mm b/ios/views/KeyboardToolbarGroupViewManager.mm index 3c91700015..083a2fcb0c 100644 --- a/ios/views/KeyboardToolbarGroupViewManager.mm +++ b/ios/views/KeyboardToolbarGroupViewManager.mm @@ -11,6 +11,7 @@ #import #import #import +#import #import "RCTFabricComponentsPlugins.h" #endif From 4033b4451fd701d327d86328f0d52f06b5dc96b2 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Tue, 10 Mar 2026 15:02:53 +0100 Subject: [PATCH 18/22] docs: move docs into different position --- .../api/components/keyboard-toolbar/index.mdx | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/docs/docs/api/components/keyboard-toolbar/index.mdx b/docs/docs/api/components/keyboard-toolbar/index.mdx index 24d58d15ee..e44bd94450 100644 --- a/docs/docs/api/components/keyboard-toolbar/index.mdx +++ b/docs/docs/api/components/keyboard-toolbar/index.mdx @@ -332,6 +332,26 @@ The property that allows to specify custom text for `Done` button. ``` +### `KeyboardToolbar.Group` + +This component defines a group of inputs that form an isolated navigation region. When the user presses the prev/next arrows on `KeyboardToolbar`, the focus will only cycle through inputs **within the same group**. Inputs outside the group are not reachable from within, and inputs inside the group are not reachable from outside. + +This is useful for scenarios like bottom sheets, tab views, or any UI where a subset of inputs should have independent navigation. + +```tsx + + + + + + + +``` + +:::info Toolbar button state +When a grouped input is focused, the toolbar's prev/next buttons reflect the position within that group (not the global list of inputs). For example, the "prev" button will be disabled when focused on the first input of a group. +::: + ## Props ### [`View Props`](https://reactnative.dev/docs/view#props) @@ -387,28 +407,6 @@ const theme: KeyboardToolbarProps["theme"] = { Don't forget that you need to specify colors for **both** `dark` and `light` theme. The theme will be selected automatically based on the device preferences. ::: -## Components - -### `KeyboardToolbar.Group` - -This component defines a group of inputs that form an isolated navigation region. When the user presses the prev/next arrows on `KeyboardToolbar`, the focus will only cycle through inputs **within the same group**. Inputs outside the group are not reachable from within, and inputs inside the group are not reachable from outside. - -This is useful for scenarios like bottom sheets, tab views, or any UI where a subset of inputs should have independent navigation. - -```tsx - - - - - - - -``` - -:::info Toolbar button state -When a grouped input is focused, the toolbar's prev/next buttons reflect the position within that group (not the global list of inputs). For example, the "prev" button will be disabled when focused on the first input of a group. -::: - ## Example ```tsx From 40c7de4bea7697796c33f9fa23bf7322346e8ff5 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Tue, 10 Mar 2026 15:03:31 +0100 Subject: [PATCH 19/22] docs: make new component consistent with other components --- docs/docs/api/components/keyboard-toolbar/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/api/components/keyboard-toolbar/index.mdx b/docs/docs/api/components/keyboard-toolbar/index.mdx index e44bd94450..8d9f2cb3cc 100644 --- a/docs/docs/api/components/keyboard-toolbar/index.mdx +++ b/docs/docs/api/components/keyboard-toolbar/index.mdx @@ -332,7 +332,7 @@ The property that allows to specify custom text for `Done` button. ``` -### `KeyboardToolbar.Group` +### `` This component defines a group of inputs that form an isolated navigation region. When the user presses the prev/next arrows on `KeyboardToolbar`, the focus will only cycle through inputs **within the same group**. Inputs outside the group are not reachable from within, and inputs inside the group are not reachable from outside. From 89355fbfabf25973dcc5e89f377f4af2bc03cd2a Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Tue, 10 Mar 2026 17:46:22 +0100 Subject: [PATCH 20/22] fix: mocks --- .../components-rendering.spec.tsx.snap | 35 +++++++++++++++++++ .../__tests__/components-rendering.spec.tsx | 27 +++++++++++++- .../components-rendering.spec.tsx.snap | 35 +++++++++++++++++++ .../__tests__/components-rendering.spec.tsx | 27 +++++++++++++- jest/index.js | 9 ++++- 5 files changed, 130 insertions(+), 3 deletions(-) diff --git a/FabricExample/__tests__/__snapshots__/components-rendering.spec.tsx.snap b/FabricExample/__tests__/__snapshots__/components-rendering.spec.tsx.snap index 6897ce4170..9fca9ee417 100644 --- a/FabricExample/__tests__/__snapshots__/components-rendering.spec.tsx.snap +++ b/FabricExample/__tests__/__snapshots__/components-rendering.spec.tsx.snap @@ -133,3 +133,38 @@ exports[`components rendering should render \`OverKeyboardView\` 1`] = ` /> `; + +exports[`components rendering should render compound \`KeyboardToolbar\` 1`] = ` +[ + + + + + + + + + + + , + + + , +] +`; diff --git a/FabricExample/__tests__/components-rendering.spec.tsx b/FabricExample/__tests__/components-rendering.spec.tsx index f154e642a9..429542e4c0 100644 --- a/FabricExample/__tests__/components-rendering.spec.tsx +++ b/FabricExample/__tests__/components-rendering.spec.tsx @@ -1,6 +1,6 @@ import { render } from "@testing-library/react-native"; import React from "react"; -import { View } from "react-native"; +import { TextInput, View } from "react-native"; import { KeyboardAvoidingView, KeyboardAwareScrollView, @@ -68,6 +68,27 @@ function KeyboardToolbarTest() { return ; } +function KeyboardToolbarCompoundTest() { + return ( + <> + + + + + + + + + + + + + + + + ); +} + function OverKeyboardViewTest() { return ( @@ -109,6 +130,10 @@ describe("components rendering", () => { expect(render()).toMatchSnapshot(); }); + it("should render compound `KeyboardToolbar`", () => { + expect(render()).toMatchSnapshot(); + }); + it("should render `OverKeyboardView`", () => { expect(render()).toMatchSnapshot(); }); diff --git a/example/__tests__/__snapshots__/components-rendering.spec.tsx.snap b/example/__tests__/__snapshots__/components-rendering.spec.tsx.snap index 13e0011b0c..542d694b5f 100644 --- a/example/__tests__/__snapshots__/components-rendering.spec.tsx.snap +++ b/example/__tests__/__snapshots__/components-rendering.spec.tsx.snap @@ -142,3 +142,38 @@ exports[`components rendering should render \`OverKeyboardView\` 1`] = ` /> `; + +exports[`components rendering should render compound \`KeyboardToolbar\` 1`] = ` +[ + + + + + + + + + + + , + + + , +] +`; diff --git a/example/__tests__/components-rendering.spec.tsx b/example/__tests__/components-rendering.spec.tsx index f154e642a9..429542e4c0 100644 --- a/example/__tests__/components-rendering.spec.tsx +++ b/example/__tests__/components-rendering.spec.tsx @@ -1,6 +1,6 @@ import { render } from "@testing-library/react-native"; import React from "react"; -import { View } from "react-native"; +import { TextInput, View } from "react-native"; import { KeyboardAvoidingView, KeyboardAwareScrollView, @@ -68,6 +68,27 @@ function KeyboardToolbarTest() { return ; } +function KeyboardToolbarCompoundTest() { + return ( + <> + + + + + + + + + + + + + + + + ); +} + function OverKeyboardViewTest() { return ( @@ -109,6 +130,10 @@ describe("components rendering", () => { expect(render()).toMatchSnapshot(); }); + it("should render compound `KeyboardToolbar`", () => { + expect(render()).toMatchSnapshot(); + }); + it("should render `OverKeyboardView`", () => { expect(render()).toMatchSnapshot(); }); diff --git a/jest/index.js b/jest/index.js index 7ca5a79610..17e7a2c6df 100644 --- a/jest/index.js +++ b/jest/index.js @@ -122,7 +122,14 @@ const mock = { KeyboardStickyView: View, KeyboardAvoidingView: View, KeyboardAwareScrollView: ScrollView, - KeyboardToolbar: View, + KeyboardToolbar: Object.assign(View, { + Background: "KeyboardToolbar.Background", + Content: "KeyboardToolbar.Content", + Prev: "KeyboardToolbar.Prev", + Next: "KeyboardToolbar.Next", + Done: "KeyboardToolbar.Done", + Group: "KeyboardToolbar.Group", + }), KeyboardChatScrollView: ScrollView, // themes DefaultKeyboardToolbarTheme, From f84ee763b26933ba208ebb1912fa783c1f2e9082 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Fri, 13 Mar 2026 10:29:47 +0100 Subject: [PATCH 21/22] feat: add working and testable example --- .../src/screens/Examples/Toolbar/index.tsx | 22 +++++++++++ .../src/screens/Examples/Toolbar/index.tsx | 38 +++++++++++-------- package.json | 3 +- 3 files changed, 46 insertions(+), 17 deletions(-) diff --git a/FabricExample/src/screens/Examples/Toolbar/index.tsx b/FabricExample/src/screens/Examples/Toolbar/index.tsx index 353bc13baf..51c96a4200 100644 --- a/FabricExample/src/screens/Examples/Toolbar/index.tsx +++ b/FabricExample/src/screens/Examples/Toolbar/index.tsx @@ -1,3 +1,4 @@ +import BottomSheet, { BottomSheetView } from "@gorhom/bottom-sheet"; import React, { useCallback, useEffect, useState } from "react"; import { Modal, Platform, StyleSheet, Text, View } from "react-native"; import { @@ -214,6 +215,24 @@ export default function ToolbarExample({ navigation }: Props) {
+ + + + + + + + ); } @@ -245,4 +264,7 @@ const styles = StyleSheet.create({ modal: { marginTop: 32, }, + bottomSheetContent: { + flex: 1, + }, }); diff --git a/example/src/screens/Examples/Toolbar/index.tsx b/example/src/screens/Examples/Toolbar/index.tsx index e3e6da2220..51c96a4200 100644 --- a/example/src/screens/Examples/Toolbar/index.tsx +++ b/example/src/screens/Examples/Toolbar/index.tsx @@ -1,3 +1,4 @@ +import BottomSheet, { BottomSheetView } from "@gorhom/bottom-sheet"; import React, { useCallback, useEffect, useState } from "react"; import { Modal, Platform, StyleSheet, Text, View } from "react-native"; import { @@ -154,22 +155,6 @@ function Form() { title="Flat" onFocus={onHideAutoFill} /> - - - - + + + + + + + + ); } @@ -261,4 +264,7 @@ const styles = StyleSheet.create({ modal: { marginTop: 32, }, + bottomSheetContent: { + flex: 1, + }, }); diff --git a/package.json b/package.json index 97a4f2c626..2648f520fe 100644 --- a/package.json +++ b/package.json @@ -194,7 +194,8 @@ "OverKeyboardView": "OverKeyboardView", "KeyboardBackgroundView": "KeyboardBackgroundView", "KeyboardExtender": "KeyboardExtender", - "ClippingScrollViewDecoratorView": "ClippingScrollViewDecoratorView" + "ClippingScrollViewDecoratorView": "ClippingScrollViewDecoratorView", + "KeyboardToolbarGroupView": "KeyboardToolbarGroupView" } } }, From 6a24a17903c590fae915a6239e34acdd3a9d3858 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Fri, 13 Mar 2026 10:56:05 +0100 Subject: [PATCH 22/22] docs: add more comments to the code --- .../KeyboardControllerPackage.kt | 1 + .../RNKCKeyboardToolbarGroupViewComponentDescriptor.h | 7 ------- .../RNKC/RNKCKeyboardToolbarGroupViewShadowNode.h | 7 ------- src/bindings.ts | 11 ++++++++++- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/android/src/turbo/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt b/android/src/turbo/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt index 7cc100fce8..4e61039c9b 100644 --- a/android/src/turbo/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt +++ b/android/src/turbo/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt @@ -60,6 +60,7 @@ class KeyboardControllerPackage : TurboReactPackage() { KeyboardGestureAreaViewManager(), OverKeyboardViewManager(), KeyboardBackgroundViewManager(), + ClippingScrollViewDecoratorViewManager(), KeyboardToolbarGroupViewManager(), ) } diff --git a/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewComponentDescriptor.h b/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewComponentDescriptor.h index fdb9546a51..36605c4b32 100644 --- a/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewComponentDescriptor.h +++ b/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewComponentDescriptor.h @@ -1,10 +1,3 @@ -// -// RNKCKeyboardToolbarGroupViewComponentDescriptor.h -// Pods -// -// Created by Kiryl Ziusko on 26/12/2024. -// - #pragma once #include "RNKCKeyboardToolbarGroupViewShadowNode.h" diff --git a/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewShadowNode.h b/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewShadowNode.h index 35086b62fb..2db75c9f83 100644 --- a/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewShadowNode.h +++ b/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewShadowNode.h @@ -1,10 +1,3 @@ -// -// RNKCKeyboardToolbarGroupViewShadowNode.h -// Pods -// -// Created by Kiryl Ziusko on 26/12/2024. -// - #pragma once #include "RNKCKeyboardToolbarGroupViewState.h" diff --git a/src/bindings.ts b/src/bindings.ts index a91595afcd..96e40ac91b 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -87,11 +87,20 @@ export const KeyboardBackgroundView = export const RCTKeyboardExtender = View as unknown as React.FC; /** - * A decorator that will clip the content of the `ScrollView`. It helps to simulate `contentInset` behavior on Android. + * A decorator that will clip the content of the `ScrollView`. It helps to simulate `contentInset` behavior on Android * Supports only `bottom` property (`paddingBottom` is not supported property of `ScrollView.style`). * Using this component we can modify bottom inset without having a fake view. + * + * On iOS we use swizzling to apply runtime patches to fix some broken internal methods. + * Ideally this component shouldn't exist and all its fixes/polyfills must be added directly to react-native and + * we will port features/fixes back to upstream, but at the moment we use this view to + * deliver desired functionality regardless of react-native version used. */ export const ClippingScrollView = View as unknown as React.FC; +/** + * A View that defines a group of `TextInput`s. + * Used in toolbar navigation to assure that you can navigate only between inputs withing the same group. + */ export const RCTKeyboardToolbarGroupView = View as unknown as React.FC;