react-native-ui-lib icon indicating copy to clipboard operation
react-native-ui-lib copied to clipboard

RCTBridge parentBridge is nil when presenting custom keyboard via presentCustomInputComponent

Open toandn-salto opened this issue 6 months ago • 2 comments

Description

When calling presentCustomInputComponent in iOS native code to display a custom keyboard, the following line in Objective-C:

RCTBridge* bridge = [self.bridge valueForKey:@"parentBridge"];

Related to

  • [x] Components
  • [ ] Demo
  • [ ] Docs
  • [ ] Typings

Steps to reproduce

  1. Register a keyboard component using KeyboardRegistry.registerKeyboard("unicorn.ImagesKeyboard", () => ImagesKeyboard).
  2. Call CustomInputControllerTemp.presentCustomInputComponent(...) from JS.
  3. In native code (presentCustomInputComponent), parentBridge is nil.
  4. The keyboard view shows but is blank (white).

Expected behavior

Actual behavior

More Info

Code snippet

import React from "react"
import { ScrollView, StyleProp, ViewStyle } from "react-native"
import { Keyboard, View, Text, Image, Spacings } from "react-native-ui-lib"
import _ from "lodash"

const KeyboardRegistry = Keyboard.KeyboardRegistry

const images: string[] = [
  "https://images.pexels.com/photos/1148521/pexels-photo-1148521.jpeg?auto=compress&cs=tinysrgb&dpr=1&h=200",
  "https://images.pexels.com/photos/1528975/pexels-photo-1528975.jpeg?auto=compress&cs=tinysrgb&dpr=1&h=200",
  "https://images.pexels.com/photos/1495580/pexels-photo-1495580.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=200",
  "https://images.pexels.com/photos/943150/pexels-photo-943150.jpeg?auto=compress&cs=tinysrgb&dpr=1&h=200",
  "https://images.pexels.com/photos/1769408/pexels-photo-1769408.jpeg?auto=compress&cs=tinysrgb&dpr=1&h=200",
]

function ImagesKeyboard() {
  return (
    <View flex>
      <ScrollView
        horizontal
        showsHorizontalScrollIndicator={false}
        contentContainerStyle={{ padding: Spacings.s4 } as StyleProp<ViewStyle>}
      >
        {images.map((image, i) => (
          <Image
            key={i}
            source={{ uri: image }}
            style={{ height: "100%", width: 200 }}
            marginR-s4
          />
        ))}
      </ScrollView>
    </View>
  )
}

function CustomKeyboard() {
  return (
    <View flex bg-violet80 center>
      <Text h2>Custom Keyboard</Text>
    </View>
  )
}

// Register keyboards with registry
KeyboardRegistry.registerKeyboard("unicorn.ImagesKeyboard", () => ImagesKeyboard())
KeyboardRegistry.registerKeyboard("unicorn.CustomKeyboard", () => CustomKeyboard())


import { AppStackScreenProps } from "@/navigators"
import { observer } from "mobx-react-lite"
import React, { useCallback, useRef, useState } from "react"
import { StyleSheet, TextInput } from "react-native"
import {
  Button,
  Colors,
  Constants,
  Keyboard,
  Spacings,
  Switch,
  Text,
  View,
} from "react-native-ui-lib"

import "./CustomKeyboard"
const KeyboardAccessoryView = Keyboard.KeyboardAccessoryView

const KeyboardUtils = Keyboard.KeyboardUtils
const KeyboardRegistry = Keyboard.KeyboardRegistry
const TrackInteractive = true

const demoKeyboards = [
  {
    id: "unicorn.ImagesKeyboard",
  },
  {
    id: "unicorn.CustomKeyboard",
  },
]

export const ConversationDetailScreen = observer(
  ({ route }: AppStackScreenProps<"ConversationDetail">) => {
    const [customKeyboard, setCustomKeyboard] = useState<any>({})
    const [receivedKeyboardData, setReceivedKeyboardData] = useState<string | undefined>()
    const [useSafeArea, setUseSafeArea] = useState<boolean>(true)
    const [keyboardOpenState, setKeyboardOpenState] = useState<boolean>(false)
    const [keyboardAccessoryViewHeight, setKeyboardAccessoryViewHeight] = useState<
      number | undefined
    >()
    const textInputRef = useRef(null)
    const textInputRef2 = useRef(null)

    const onKeyboardItemSelected = useCallback((component?: string, args?: any) => {
      const receivedData = `onItemSelected from "${component}"\nreceived params: ${JSON.stringify(args)}`
      setReceivedKeyboardData(receivedData)
    }, [])

    const onKeyboardResigned = useCallback(() => {
      resetKeyboardView()
    }, [])

    const isCustomKeyboardOpen = useCallback(() => {
      return keyboardOpenState && !_.isEmpty(customKeyboard)
    }, [keyboardOpenState, customKeyboard])

    const resetKeyboardView = useCallback(() => {
      setCustomKeyboard({})
    }, [])

    const dismissKeyboard = useCallback(() => {
      KeyboardUtils.dismiss()
    }, [])

    const toggleUseSafeArea = useCallback(() => {
      setUseSafeArea((prev) => !prev)

      if (isCustomKeyboardOpen()) {
        dismissKeyboard()
        showLastKeyboard()
      }
    }, [isCustomKeyboardOpen, dismissKeyboard])

    const showLastKeyboard = useCallback(() => {
      setCustomKeyboard({})
      setTimeout(() => {
        setKeyboardOpenState(true)
        setCustomKeyboard((prev: any) => ({ ...prev }))
      }, 500)
    }, [])

    const showKeyboardView = useCallback((component: string, title?: string) => {
      setKeyboardOpenState(true)
      setCustomKeyboard({
        component,
        initialProps: { title },
      })
    }, [])

    const onHeightChanged = useCallback((height: number) => {
      if (Constants.isIOS) {
        setKeyboardAccessoryViewHeight(height)
      }
    }, [])

    const renderKeyboardAccessoryViewContent = useCallback(
      () => (
        <View style={styles.keyboardContainer} paddingV-s4>
          <View bg-white row spread centerV paddingH-s5 paddingV-s3>
            <TextInput ref={textInputRef} placeholder="Test" onFocus={resetKeyboardView} />
            <Button link grey10 onPress={KeyboardUtils.dismiss} marginL-s2 label="Test" />
          </View>
          <View row paddingH-s4 marginT-s2 spread>
            <View row>
              {demoKeyboards.map((keyboard, index) => (
                <Button
                  key={`${keyboard.id}_${index}`}
                  grey10
                  link
                  onPress={() => showKeyboardView(keyboard.id)}
                  marginR-s2
                  label="Test"
                />
              ))}
            </View>
            <Button grey10 label="Reset" link onPress={resetKeyboardView} />
          </View>
        </View>
      ),
      [showKeyboardView, resetKeyboardView],
    )

    const requestShowKeyboard = useCallback(() => {
      KeyboardRegistry.requestShowKeyboard("unicorn.ImagesKeyboard")
    }, [])

    const onRequestShowKeyboard = useCallback(() => {
      setCustomKeyboard({
        component: "unicorn.ImagesKeyboard",
        initialProps: { title: "Keyboard 1 opened by button" },
      })
    }, [])

    const safeAreaSwitchToggle = useCallback(() => {
      if (!Constants.isIOS) {
        return null
      }
      return (
        <View center>
          <View style={styles.separatorLine} />
          <View centerV row margin-10>
            <Text text80 grey40>
              Safe Area Enabled:
            </Text>
            <Switch value={useSafeArea} onValueChange={toggleUseSafeArea} marginL-14 />
          </View>
          <View style={styles.separatorLine} />
        </View>
      )
    }, [useSafeArea, toggleUseSafeArea])

    return (
      <View flex style={{ marginTop: 100 }}>
        <Button
          grey10
          link
          onPress={() => showKeyboardView("unicorn.ImagesKeyboard")}
          label="Test213"
        />
        <View flex>
          <KeyboardAccessoryView
            renderContent={renderKeyboardAccessoryViewContent}
            onHeightChanged={onHeightChanged}
            trackInteractive={TrackInteractive}
            kbInputRef={textInputRef}
            kbComponent={customKeyboard.component}
            kbInitialProps={customKeyboard.initialProps}
            onItemSelected={onKeyboardItemSelected}
            onKeyboardResigned={onKeyboardResigned}
            onRequestShowKeyboard={onRequestShowKeyboard}
            useSafeArea={useSafeArea}
            addBottomView={useSafeArea}
            allowHitsOutsideBounds
          />
        </View>
      </View>
    )
  },
)

const styles = StyleSheet.create({
  scrollContainer: {
    paddingHorizontal: Spacings.s5,
    flex: 1,
    justifyContent: "center",
  },
  textField: {
    flex: 1,
    backgroundColor: Colors.grey60,
    paddingVertical: Spacings.s2,
    paddingHorizontal: Spacings.s4,
    borderRadius: 8,
  },
  button: {
    padding: Spacings.s2,
  },
  keyboardContainer: {
    backgroundColor: Colors.white,
    borderWidth: 1,
    borderColor: Colors.grey60,
  },
  separatorLine: {
    flex: 1,
    height: 1,
    backgroundColor: Colors.grey80,
  },
})

Screenshots/Video

Environment

  • React Native version: 0.76.9
  • React Native UI Lib version: 7.44.0

Affected platforms

  • [ ] Android
  • [x] iOS
  • [ ] Web

toandn-salto avatar Jul 12 '25 11:07 toandn-salto

@ethanshar please help me check, thanks

toandn-salto avatar Jul 25 '25 08:07 toandn-salto

Hello,

We have a version that supports new-arch (RN77), you can use the next tag for now. Please make sure to go over the v8 doc, it includes breaking changes and some known issues.

Please close this ticket if it solved your bug.

M-i-k-e-l avatar Oct 23 '25 10:10 M-i-k-e-l