React Native TextInput loses focus when switching tabs

37 Views Asked by At

I'm working on a React Native project where I have a TextInput component inside a TabView.

I'm facing an issue where the TextInput loses focus and the keyboard dismisses when I switch tabs.

When I tap on the TextInput for the first time, the keyboard appears briefly (blinks) and then disappears. It only works as expected on the second tap.

The issue exists only when switching from Apps to Websites tab. If I switch from Apps to Keywords, or any other combination it's fine.

Here's my code:

import * as React from 'react'
import { useState } from 'react'
import { FlatList, StyleSheet, TextInput } from 'react-native'
import { T } from '../../../design-system/theme.ts'
import { SelectableSirenCard } from '../SelectableSirenCard.tsx'
import { SirenType } from '../../../../core/blocklist/blocklist.ts'

export function TextInputSelectionScene(props: {
  onSubmitEditing: (event: { nativeEvent: { text: string } }) => void
  placeholder: string
  sirenType: SirenType.WEBSITES | SirenType.KEYWORDS
  data: string[]
  toggleSiren: (sirenType: SirenType, sirenId: string) => void
  isSirenSelected: (sirenType: SirenType, sirenId: string) => boolean
}) {
  const [isFocused, setIsFocused] = useState(true)

  return (
    <>
      <TextInput
        style={[
          styles.addWebsiteInput,
          { borderColor: isFocused ? randomColor : T.color.white },
        ]}
        placeholder={props.placeholder}
        placeholderTextColor={T.color.white}
        onFocus={() => setIsFocused(true)}
        onBlur={() => setIsFocused(false)}
        onSubmitEditing={props.onSubmitEditing}
      />
      <FlatList
        data={props.data}
        keyExtractor={(item) => item}
        renderItem={({ item }) => (
          <SelectableSirenCard
            sirenType={props.sirenType}
            siren={item}
            onPress={() => props.toggleSiren(props.sirenType, item)}
            isSelected={props.isSirenSelected(props.sirenType, item)}
          />
        )}
      />
    </>
  )
}

const colors = [
  '#FF0000',
  '#00FF00',
  '#0000FF',
  '#FFFF00',
  '#00FFFF',
  '#FF00FF',
]
const randomColor = colors[Math.floor(Math.random() * colors.length)]

const styles = StyleSheet.create({
  addWebsiteInput: {
    borderBottomWidth: 2,
    borderBottomColor: randomColor,
    padding: T.spacing.small,
    color: T.color.white,
  },
})

And its parent component:

import { Dimensions, StyleSheet, Text } from 'react-native'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { ScreenList } from '../../../navigators/screen-lists/screenLists.ts'
import { TabScreens } from '../../../navigators/screen-lists/TabScreens.ts'
import { TiedSLinearBackground } from '../../../design-system/components/TiedSLinearBackground.tsx'
import { TiedSBlurView } from '../../../design-system/components/TiedSBlurView.tsx'
import { T } from '../../../design-system/theme'
import { TiedSTextInput } from '../../../design-system/components/TiedSTextInput.tsx'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { installedAppsRepository } from '../../../dependencies.ts'
import { InstalledApp } from '../../../../core/installed-app/InstalledApp.ts'
import { Route, SceneMap, TabBarProps, TabView } from 'react-native-tab-view'
import { TiedSButton } from '../../../design-system/components/TiedSButton.tsx'
import { BlocklistsStackScreens } from '../../../navigators/screen-lists/BlocklistsStackScreens.ts'
import { ChooseBlockTabBar } from './ChooseBlockTabBar.tsx'
import { useDispatch } from 'react-redux'
import { createBlocklist } from '../../../../core/blocklist/usecases/create-blocklist.usecase.ts'
import { AppDispatch } from '../../../../core/_redux_/createStore.ts'
import {
  Blocklist,
  Sirens,
  SirenType,
} from '../../../../core/blocklist/blocklist.ts'
import { AppsSelectionScene } from './AppsSelectionScene.tsx'
import { TextInputSelectionScene } from './TextInputSelectionScene.tsx'

type BlocklistScreenProps = {
  navigation: NativeStackNavigationProp<ScreenList, TabScreens.BLOCKLIST>
}

export function CreateBlocklistScreen({
  navigation,
}: Readonly<BlocklistScreenProps>) {
  const dispatch = useDispatch<AppDispatch>()

  const [installedApps, setInstalledApps] = useState<InstalledApp[]>([])
  const [websites, setWebsites] = useState<string[]>([])
  const [keywords, setKeywords] = useState<string[]>([])

  const [blocklist, setBlocklist] = useState<Omit<Blocklist, 'id'>>({
    name: '',
    sirens: {
      android: [],
      ios: [],
      windows: [],
      macos: [],
      linux: [],
      websites: [],
      keywords: [],
    },
  })
  const [index, setIndex] = useState(0)
  const routes = [
    { key: 'apps', title: 'Apps' },
    { key: 'websites', title: 'Websites' },
    { key: 'keywords', title: 'Keywords' },
  ]

  useEffect(() => {
    installedAppsRepository.getInstalledApps().then((apps) => {
      setInstalledApps(apps)
    })
  }, [])

  function toggleSiren(sirenType: keyof Sirens, sirenId: string) {
    setBlocklist((prevBlocklist) => {
      const updatedSirens = { ...prevBlocklist.sirens }

      updatedSirens[sirenType] = updatedSirens[sirenType].includes(sirenId)
        ? updatedSirens[sirenType].filter(
            (selectedSiren) => selectedSiren !== sirenId,
          )
        : [...updatedSirens[sirenType], sirenId]

      return {
        ...prevBlocklist,
        sirens: updatedSirens,
      }
    })
  }

  function isSirenSelected(sirenType: SirenType, sirenId: string) {
    return blocklist.sirens[sirenType].includes(sirenId)
  }

  const renderScene = SceneMap({
    apps: () => (
      <AppsSelectionScene
        data={installedApps}
        toggleSiren={toggleSiren}
        isSirenSelected={isSirenSelected}
      />
    ),
    websites: () => (
      <TextInputSelectionScene
        onSubmitEditing={(event) =>
          setWebsites([...websites, event.nativeEvent.text])
        }
        sirenType={SirenType.WEBSITES}
        placeholder={'Add websites...'}
        data={websites}
        toggleSiren={toggleSiren}
        isSirenSelected={isSirenSelected}
      />
    ),
    keywords: () => (
      <TextInputSelectionScene
        onSubmitEditing={(event) =>
          setKeywords([...keywords, event.nativeEvent.text])
        }
        sirenType={SirenType.KEYWORDS}
        placeholder={'Add keywords...'}
        data={keywords}
        toggleSiren={toggleSiren}
        isSirenSelected={isSirenSelected}
      />
    ),
  })

  return (
    <TiedSLinearBackground>
      <Text style={styles.title}>Name</Text>
      <TiedSBlurView>
        <TiedSTextInput
          placeholder="Blocklist name"
          onChangeText={(text) => setBlocklist({ ...blocklist, name: text })}
        />
      </TiedSBlurView>

      <TabView
        navigationState={{ index, routes }}
        renderScene={renderScene}
        lazy={false}
        onIndexChange={setIndex}
        initialLayout={{ width: Dimensions.get('window').width }}
        renderTabBar={(props: TabBarProps<Route>) => (
          <ChooseBlockTabBar {...props} />
        )}
      />

      <TiedSButton
        text={'Save Blocklist'}
        onPress={() => {
          dispatch(createBlocklist(blocklist))
          navigation.navigate(BlocklistsStackScreens.MAIN_BLOCKLIST)
        }}
      />
    </TiedSLinearBackground>
  )
}

const styles = StyleSheet.create({
  title: {
    fontWeight: T.font.weight.bold,
    color: T.color.text,
    fontFamily: T.font.family.primary,
    fontSize: T.size.small,
    marginTop: T.spacing.small,
    marginBottom: T.spacing.small,
  },
})

I've tried the following solutions but none of them worked:
Preventing the unmounting of the TextInput component when switching tabs by setting lazy={false} on the TabView component.
Using the useEffect hook to manage the focus state of the TextInput component. I tried to focus the TextInput component whenever the isFocused state changes.

0

There are 0 best solutions below