Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,6 @@ class KeyboardControllerPackage : BaseReactPackage() {
KeyboardControllerViewManager(reactContext),
KeyboardGestureAreaViewManager(reactContext),
OverKeyboardViewManager(reactContext),
KeyboardToolbarExcludeViewManager(reactContext),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.reactnativekeyboardcontroller.managers

import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ThemedReactContext
import com.reactnativekeyboardcontroller.views.KeyboardToolbarExcludeReactViewGroup

@Suppress("detekt:UnusedPrivateProperty")
class KeyboardToolbarExcludeViewManagerImpl(
mReactContext: ReactApplicationContext,
) {
fun createViewInstance(reactContext: ThemedReactContext): KeyboardToolbarExcludeReactViewGroup =
KeyboardToolbarExcludeReactViewGroup(reactContext)

companion object {
const val NAME = "KeyboardToolbarExcludeView"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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))
}
Expand Down Expand Up @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this check repeats twice, would it be better to use helper function for this?

// If the child is a ViewGroup, check its children recursively
result = findEditTextInHierarchy(child, direction)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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
}
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,6 @@ class KeyboardControllerPackage : TurboReactPackage() {
KeyboardControllerViewManager(reactContext),
KeyboardGestureAreaViewManager(reactContext),
OverKeyboardViewManager(reactContext),
KeyboardToolbarExcludeViewManager(reactContext),
)
}
20 changes: 20 additions & 0 deletions docs/docs/api/components/keyboard-toolbar/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,26 @@ 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.

```tsx
<BottomSheet>
<KeyboardToolbar.Exclude>
<TextInput
contextMenuHidden
keyboardType="numeric"
placeholder="Excluded"
testID="TextInput#14"
title="Excluded"
/>
</KeyboardToolbar.Exclude>
</BottomSheet>
```

## Example

```tsx
Expand Down
10 changes: 10 additions & 0 deletions example/src/screens/Examples/Toolbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,16 @@ function Form() {
title="Flat"
onFocus={onHideAutoFill}
/>
<KeyboardToolbar.Exclude>
<TextInput
contextMenuHidden
keyboardType="numeric"
placeholder="Excluded"
testID="TextInput#14"
title="Excluded"
onFocus={onHideAutoFill}
/>
</KeyboardToolbar.Exclude>
</KeyboardAwareScrollView>
<KeyboardToolbar
blur={blur}
Expand Down
4 changes: 3 additions & 1 deletion ios/traversal/ViewHierarchyNavigator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public class ViewHierarchyNavigator: NSObject {

if let textInput = isValidTextInput(view) {
textInputs.append(textInput)
} else {
} else if String(describing: type(of: view)) != "KeyboardToolbarExcludeView" {
for subview in view.subviews {
findTextInputs(in: subview)
}
Expand Down Expand Up @@ -91,6 +91,8 @@ public class ViewHierarchyNavigator: NSObject {
return validTextInput
}

guard String(describing: type(of: view)) != "KeyboardToolbarExcludeView" else { return nil }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. is it possible to use instance check instead of comparing two strings?
  2. it repeats twice


// Determine the iteration order based on the direction
let subviews = direction == "next" ? view.subviews : view.subviews.reversed()

Expand Down
28 changes: 28 additions & 0 deletions ios/views/KeyboardToolbarExcludeViewManager.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// KeyboardToolbarExcludeViewManager.h
// KeyboardController
//
// Created by Kiryl Ziusko on 26/12/2024.
//

#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTViewComponentView.h>
#else
#import <React/RCTBridge.h>
#endif
#import <React/RCTViewManager.h>
#import <UIKit/UIKit.h>

@interface KeyboardToolbarExcludeViewManager : RCTViewManager
@end

@interface KeyboardToolbarExcludeView :
#ifdef RCT_NEW_ARCH_ENABLED
RCTViewComponentView
#else
UIView

- (instancetype)initWithBridge:(RCTBridge *)bridge;

#endif
@end
88 changes: 88 additions & 0 deletions ios/views/KeyboardToolbarExcludeViewManager.mm
Original file line number Diff line number Diff line change
@@ -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 <react/renderer/components/reactnativekeyboardcontroller/ComponentDescriptors.h>
#import <react/renderer/components/reactnativekeyboardcontroller/EventEmitters.h>
#import <react/renderer/components/reactnativekeyboardcontroller/Props.h>
#import <react/renderer/components/reactnativekeyboardcontroller/RCTComponentViewHelpers.h>

#import "RCTFabricComponentsPlugins.h"
#endif

#import <UIKit/UIKit.h>

#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 () <RCTKeyboardToolbarExcludeViewViewProtocol>
@end
#endif

@implementation KeyboardToolbarExcludeView {
}

#ifdef RCT_NEW_ARCH_ENABLED
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<KeyboardToolbarExcludeViewComponentDescriptor>();
}
#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<RCTComponentViewProtocol> KeyboardToolbarExcludeViewCls(void)
{
return KeyboardToolbarExcludeView.class;
}
#endif

@end
2 changes: 2 additions & 0 deletions src/bindings.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,5 @@ export const KeyboardGestureArea: React.FC<KeyboardGestureAreaProps> =
: ({ children }: KeyboardGestureAreaProps) => children;
export const RCTOverKeyboardView: React.FC<OverKeyboardViewProps> =
require("./specs/OverKeyboardViewNativeComponent").default;
export const RCTKeyboardToolbarExcludeView: React.FC<OverKeyboardViewProps> =
Copy link

Copilot AI Apr 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type for RCTKeyboardToolbarExcludeView should use KeyboardToolbarExcludeViewProps instead of OverKeyboardViewProps for consistency with the rest of the bindings.

Suggested change
export const RCTKeyboardToolbarExcludeView: React.FC<OverKeyboardViewProps> =
export const RCTKeyboardToolbarExcludeView: React.FC<KeyboardToolbarExcludeViewProps> =

Copilot uses AI. Check for mistakes.
require("./specs/KeyboardToolbarExcludeViewNativeComponent").default;
3 changes: 3 additions & 0 deletions src/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
KeyboardControllerProps,
KeyboardEventsModule,
KeyboardGestureAreaProps,
KeyboardToolbarExcludeViewProps,
OverKeyboardViewProps,
WindowDimensionsEventsModule,
} from "./types";
Expand Down Expand Up @@ -40,3 +41,5 @@ export const KeyboardGestureArea =
View as unknown as React.FC<KeyboardGestureAreaProps>;
export const RCTOverKeyboardView =
View as unknown as React.FC<OverKeyboardViewProps>;
export const RCTKeyboardToolbarExcludeView =
View as unknown as React.FC<KeyboardToolbarExcludeViewProps>;
13 changes: 11 additions & 2 deletions src/components/KeyboardToolbar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { StyleSheet, Text, View } from "react-native";

import { FocusedInputEvents } from "../../bindings";
import {
FocusedInputEvents,
RCTKeyboardToolbarExcludeView,
} from "../../bindings";
import { KeyboardController } from "../../module";
import useColorScheme from "../hooks/useColorScheme";
import KeyboardStickyView from "../KeyboardStickyView";
Expand Down Expand Up @@ -71,11 +74,15 @@ const TEST_ID_KEYBOARD_TOOLBAR_DONE = `${TEST_ID_KEYBOARD_TOOLBAR}.done`;
const KEYBOARD_TOOLBAR_HEIGHT = 42;
const DEFAULT_OPACITY: HEX = "FF";

type KeyboardToolbarComponent = {
Exclude: typeof RCTKeyboardToolbarExcludeView;
} & React.FC<KeyboardToolbarProps>;

/**
* `KeyboardToolbar` is a component that is shown above the keyboard with `Prev`/`Next` and
* `Done` buttons.
*/
const KeyboardToolbar: React.FC<KeyboardToolbarProps> = ({
const KeyboardToolbar: KeyboardToolbarComponent = ({
content,
theme = colors,
doneText = "Done",
Expand Down Expand Up @@ -222,6 +229,8 @@ const KeyboardToolbar: React.FC<KeyboardToolbarProps> = ({
);
};

KeyboardToolbar.Exclude = RCTKeyboardToolbarExcludeView;

const styles = StyleSheet.create({
flex: {
flex: 1,
Expand Down
10 changes: 10 additions & 0 deletions src/specs/KeyboardToolbarExcludeViewNativeComponent.ts
Original file line number Diff line number Diff line change
@@ -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<NativeProps>(
"KeyboardToolbarExcludeView",
) as HostComponent<NativeProps>;
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export type KeyboardGestureAreaProps = {
export type OverKeyboardViewProps = PropsWithChildren<{
visible: boolean;
}>;
export type KeyboardToolbarExcludeViewProps = ViewProps;

export type Direction = "next" | "prev" | "current";
export type DismissOptions = {
Expand Down
Loading