Tim Heilman
Tim Heilman

Reputation: 420

How to display the bottom (tabs) nav bar from expo-router in iOS, portrait orientation, with keyboard open, as in Android, preserving ScrollView?

I am using expo-router with the (tabs) directory. I'm also using react-native-safe-area-context's SafeAreaView. I would like, in portrait orientation, the (tabs) nav bar to appear above the keyboard with the keyboard open on both iOS and Android, and to use a ScrollView in the components above that.

I've found that on Android I do not need a <KeyboardAvoidingView> to preserve correct ScrollView behavior above the keyboard, but on iOS I do. So I place that in a TabWrapper like so:

export const TabWrapper = (props: PropsWithChildren) => {
  return (
    <SafeAreaView style={{ flex: 1 }}>
      {Platform.OS === "ios" ? (
        <KeyboardAvoidingView behavior="padding" style={{ flex: 1 }}>
          {props.children}
        </KeyboardAvoidingView>
      ) : (
        <>{props.children}</>
      )}
    </SafeAreaView>
  );
};

The PlayerTab has a header and a form:

export const PlayerTab = ({ dir }: { dir: DirectionLetter }) => {
  return (
    <TabWrapper>
      <PlayerHeader specifyingPlayerDir={dir} />
      <FormAssignPlayer
        direction={dir}
        nextRoute={
          dir === "N"
            ? "/PlayerW"
            : dir === "W"
              ? "/PlayerE"
              : dir === "E"
                ? "/PlayerS"
                : "/PlayerConfirm"
        }
      />
    </TabWrapper>
  );
};

The form is what provides the scrollview:

const FormAssignPlayer: React.FC<FormAssignPlayerProps> = ({
  direction,
  nextRoute,
}: FormAssignPlayerProps) => {
  const playerAssignment = useAppSelector(selectUnconfirmedPlayers)[direction];
  const [playerFilter, setPlayerFilter] = useState(
    playerAssignment?.displayName ?? "",
  );
  const dispatch = useAppDispatch();
  const roster = Object.entries(useAppSelector(selectPlayers)).map((e) => ({
    playerId: e[0],
    playerDisplayName: e[1].playerDisplayName,
  }));
  const idRef = useRef<TextInput>(null);
  const router = useRouter();
  useFocusEffect(() => {
    log("useFocusEffect", "debug");
    idRef.current!.focus();
  });

  const safeRegExp = safeMakeRegExp(playerFilter);
  const filteredRoster = playerFilter
    ? roster.filter((player) => {
        return player.playerDisplayName.match(safeRegExp);
      })
    : [];
  log("afterFilter", "debug");
  const handleSubmit = ({
    playerId,
    playerDisplayName,
  }: Omit<Player, "clubId">) => {
    setPlayerFilter(playerDisplayName);
    dispatch(
      setUnconfirmedPlayer({
        dir: direction,
        id: playerId,
        displayName: playerDisplayName,
      }),
    );
    router.replace(nextRoute);
  };

  return (
    <View style={{ flexDirection: "row", flex: 1 }}>
      <View style={{ flexDirection: "column", flex: 1 }}>
        <TextInput
          style={styles.inputField}
          value={playerFilter}
          onChangeText={setPlayerFilter}
          selectTextOnFocus={true}
          autoCapitalize={"none"}
          placeholder={"⌨"}
          placeholderTextColor="white"
          multiline={true}
          ref={idRef}
        />
      </View>
      <View
        style={{
          flexDirection: "column",
          flex: 1,
        }}
      >
        {playerFilter ? (
          filteredRoster.length > 0 ? (
            <ScrollView keyboardShouldPersistTaps={"handled"}>
              {filteredRoster.map((player) => {
                if (player.playerId) {
                  log("returningPlayerButton", "debug");
                  return (
                    <BorderedActionButton
                      key={player.playerId}
                      onPress={() => handleSubmit(player)}
                      inFlight={false}
                    >
                      <AppText style={[styles.listItem, { margin: 4 }]}>
                        {player.playerDisplayName}
                      </AppText>
                    </BorderedActionButton>
                  );
                }
              })}
            </ScrollView>
          ) : (
            <Centered>
              <AppRealBig>No matches: &quot;{playerFilter}&quot;</AppRealBig>
            </Centered>
          )
        ) : null}
      </View>
    </View>
  );
};

On Android, this works and keeps the tabs bar above the keyboard:

screenshot of tabs bottom-nav bar above keyboard on android

On iOS, the scrollView is still working, but the tabs bar does not appear above the keyboard:

enter image description here

How can I achieve the same behavior on iOS as I have on Android: that the expo-router (tabs) bottom-nav bar appears above the keyboard while it is open in portrait orientation, while preserving the functionality of the ScrollView (such that scrolling to the bottom of the scrollView brings the bottom element of the list above the keyboard and (tabs) bottom-nav bar, which the conditional KeyboardAvoidingView in TabWrapper was required to achieve for iOS)?

Upvotes: 0

Views: 115

Answers (0)

Related Questions