akveo / react-native-ui-kitten

:boom: React Native UI Library based on Eva Design System :new_moon_with_face::sparkles:Dark Mode
https://akveo.github.io/react-native-ui-kitten/
MIT License
10.19k stars 952 forks source link

Autocomplete can't be focused #1744

Open hugoseri opened 1 year ago

hugoseri commented 1 year ago

🐛 Bug Report

To Reproduce

Steps to reproduce the behavior: Try using the autocomplete async showcase from the docs: https://akveo.github.io/react-native-ui-kitten/docs/assets/playground-build/#/AutocompleteAsync

I first had the issue on my local project and tried to investigate what is happening. It seems to happen when there is no <AutocompleteItem/> to <Autocomplete/> element (this should be handled as it may happen when the input text doesn't match any of the results).

Expected behavior

When clicking on the input, it should become focused, and I should be able to enter my text. However, the input doesn't focus and I can't enter anything.

I first had the issue on my local project and tried to investigate what is happening. It seems to happen when there is no <AutocompleteItem/> children to <Autocomplete/> element.

Link to runnable example or repository (highly encouraged)

https://akveo.github.io/react-native-ui-kitten/docs/assets/playground-build/#/AutocompleteAsync

UI Kitten and Eva version

Package Version
@eva-design/eva
@ui-kitten/components 5.3.1

Environment information

bataevvlad commented 1 year ago

Hello @hugoseri

I will be glad to help you, could you please provide your local code with such issue?

Regards, Vlad

bataevvlad commented 1 year ago

To fix this issue, you can create a ref using the useRef hook and assign it to the Autocomplete component, like so:

const autocompleteRef = React.useRef(null);

 <Autocomplete
      placeholder='For example, Star Wars'
      value={query}
      placement='inner top'
      onChangeText={onChangeText}
      onSelect={onSelect}
      ref={autocompleteRef}
    >

By adding ref={autocompleteRef} to the Autocomplete component and creating a autocompleteRef using React.useRef(null), the input field should now be focused correctly.

We will update our showcase for AutoComplete, thank you!

SteveSB commented 1 year ago

this is still happening, if I type an option that is not there in the list of options, type out then try to focus the Autocomplete again it just refuses to show the keypad

bataevvlad commented 1 year ago

@SteveSB please notice that PR is not merged yet :)

Codelica commented 1 year ago

That PR just updates the docs/samples to show including a ref though, unless I'm missing something(?).

I seem to be seeing the same behavior on Android, where tapping an Autocomplete text field will focus it (and bring up the AutocompleteItem select list) but won't bring up the keyboard unless the Autocomplete text field is tapped a 2nd time.

I only see this on Android (iOS brings up keyboard on initial tap). I already have a ref in place for Autocomplete which is called for various things.

Edit: I'm also seeing the same behavior as @SteveSB, where if you enter text that won't match at least one option and then the component is blurred, it will not ever allow it to be focused again. (both Android and iOS)

Codelica commented 1 year ago

It seems there are a couple bugs involved here with Autocomplete:

1) Regarding not being able to focus/edit the Autocomplete field again once it has some text entered which doesn't result in any item matches, that involves this.state.listVisible being used to determine if the Popover containing the Input and List components is visible. Once there is text in the Input that doesn't result in a match, the Popover can't be visible again currently.

2) Regarding the keyboard not coming up on Android when Autocomplete is initially focused (without a 2nd tap), that seems to be an issue with RN's Modal -- which UI Kitten uses now. There is a workaround here using a setTimeout on onShow, but since that's not exposed for Autocomplete it will require a change in UI Kitten. I did try it manually in Autocomplete.component.tsx and it did work though.

fzf commented 1 year ago

I am also running into this issue. When you have an async autocomplete so you dont have any results when you are first entering text it results in not being able to select the text field at all. Even with the addition of the useRef because the list is empty it loses focus and cannot regain it. This is even when using the example you gave. There used to be a kinda hacky workaround for this behavior but it no longer seems to work. https://github.com/akveo/react-native-ui-kitten/issues/1117

import React, { useState, useEffect, useRef } from 'react';
import AwesomeDebouncePromise from 'awesome-debounce-promise';
import { Autocomplete, AutocompleteItem } from '@ui-kitten/components';
import { SafeAreaView } from 'react-native';

const requestData = () => fetch('https://reactnative.dev/movies.json');
const requestDataWithDebounce = AwesomeDebouncePromise(requestData, 400);

export default function CreateEventScreen() {
  const autocompleteRef = useRef(null);
  const [query, setQuery] = useState(null);
  const [data, setData] = useState([]);

  const updateData = () => {
    void requestDataWithDebounce()
      .then(response => response.json())
      .then(json => json.movies)
      .then(applyFilter)
      .then(setData);
  };

  useEffect(updateData, [query]);

  const onSelect = (index) => {
    setQuery(data[index].title);
  };

  const onChangeText = (nextQuery) => {
    setQuery(nextQuery);
  };

  const applyFilter = (options) => {
    return options.filter(item => item.title.toLowerCase().includes(query.toLowerCase()));
  };

  const renderOption = (item, index) => (
    <AutocompleteItem
      key={index}
      title={item.title}
    />
  );

  return (
    <SafeAreaView style={{ flex: 1}}>
      <Autocomplete
        ref={autocompleteRef}
        placeholder='For example, Star Wars'
        value={query}
        placement='inner top'
        onChangeText={onChangeText}
        onSelect={onSelect}
      >
        {data.map(renderOption)}
      </Autocomplete>
      </SafeAreaView>
  );
};
fzf commented 1 year ago

You can get around this limitation by setting some initial data:

const [data, setData] = useState(["title": "Loading..."]);

Additionally, and happy to open another bug for this the autocomplete box's width is all off:

Screenshot 2023-06-23 at 12 00 38 Screenshot 2023-06-23 at 11 58 16
Codelica commented 1 year ago

There are still hacky ways around this also -- which I'm sad to say I'm having to do currently. As the Autocomplete component is a class component you can extend it and override things like setOptionsListVisible() to remove or tweak the hasData check it has as needed. I'm also adding the onShow workaround for Android to show keyboard on first tap and trying to get onBlur to report properly. It's not perfect, and feels ugly, but is an option to help things function better for now.

Codelica commented 1 year ago

@fzf if you give your Autocomplete component a pixel width (either directly or as a calculated % of screen width) it should display better. I think what you're seeing is another effect of Kitten UI moving to React Native's modals which introduced some quirks that need fixing. (Like the Android keyboard issue above).

ricardodolnl commented 1 year ago

Any updates? On Android and iOS I also can't seem to select/focus on the autocomplete input field to typ in text (version 5.3.1).

Extra info: I changed ui-kitten back to v5.1.2, the version I used before, and I still have the problem. My guess is that is has to do with the react native upgrade from v0.70.6 to v0.72.0.

ricardodolnl commented 1 year ago

Can someone give me a status update on this bug? Any time indication for the fix? If it takes to long to fix I'll need to find another solution.

varunlakhan commented 8 months ago

What's the current status of this bug? This is a showstopper for us now :( . @Codelica or anyone else do you mind sharing your code snippet? Don't mind it's a hack just want to tide over the issue for now.

Codelica commented 8 months ago

@varun85jobs I have a pretty customized use of this component (and I haven't looks at this in months) so I'm not 100% this will be a drop-in workaround for you. Also it is a 100% hack overriding private class methods, etc.. :) but this is the class I'm using:

import {Autocomplete, Popover, List} from '@ui-kitten/components';

// FIXME: Hack to deal with Autocomplete bugs now that Kitten UI uses React modals
class PatchedAutocomplete extends Autocomplete {
  // FIXME: onBlur not working correctly
  // See: https://github.com/akveo/react-native-ui-kitten/issues/1755
  blur = () => {
    console.log(`Autocomplete: Overridden blur() called`);
    this.inputRef.current?.blur();
    this.props.onBlur?.(); // Added
  };
  // FIXME: Keyboard not showing on Android without second touch
  // See: https://github.com/akveo/react-native-ui-kitten/issues/1744
  // See: https://github.com/react-native-modal/react-native-modal/issues/516#issuecomment-997961846
  onShow = () => {
    if (Platform.OS == 'android') {
      setTimeout(() => {
        this.inputRef.current?.blur();
        this.inputRef.current?.focus();
      }, 100);
    }
  }
  render() {
    const { placement, children, testID, ...inputProps } = this.props;
    return (
      <Popover
        style={styles.popover}
        placement={placement}
        testID={testID}
        visible={this.state.listVisible}
        fullWidth={true}
        anchor={() => this.renderAnchorInputElement(inputProps)}
        onBackdropPress={this.onBackdropPress}
        onShow={this.onShow}
      >
        <View>
          {this.renderInputElement(inputProps)}
          <List
            style={styles.list}
            keyboardShouldPersistTaps='always'
            data={this.data}
            bounces={false}
            renderItem={this.renderItem}
          />
        </View>
      </Popover>
    );
  }
}

And then in an onBlur() handler (now that it's working with the hack above) I check to make sure what's been entered is either nothing or matches an item -- if not I RN alert() the user and clear the value.

varunlakhan commented 8 months ago

Thanks @Codelica for sharing the snippet., however this didn't work for me. I had to use another lib for this. :(

naojamg commented 7 months ago

I found a solution following @fzf idea of adding an element to the beginning of the array but also hiding it from the UI.

I share the complete code in case it could be useful to someone.

import React, {useCallback, useMemo} from 'react';
import {Autocomplete, AutocompleteItem, Text} from '@ui-kitten/components';
import {Dimensions} from 'react-native';

interface IMovie {
  id: number;
  title: string;
  visible: boolean;
}

//Simulate data base
const db: IMovie[] = [
  {id: 1, title: 'Star Wars', visible: true},
  {id: 2, title: 'Back to the Future', visible: true},
  {id: 3, title: 'The Matrix', visible: true},
  {id: 4, title: 'Inception', visible: true},
  {id: 5, title: 'Interstellar', visible: true},
];

//Simulate data base query
const filter = (item: IMovie, query: string): boolean =>
  item.title.toLowerCase().includes(query.toLowerCase());

export const Test = (): React.ReactElement => {
  const [movie, setMovie] = React.useState<string>('');
  const [movies, setMovies] = React.useState<IMovie[]>([]);

  const onSelect = useCallback(
    (index: number): void => {
      setMovie(updatedMovies[index].title);
    },
    [movies],
  );

  const onChangeText = useCallback((query: string): void => {
    if (query.length === 0) {
      setMovie('');
      setMovies([]);
    } else {
      setMovie(query);
      setMovies(db.filter(item => filter(item, query)));
    }
  }, []);

  const renderAutocompleteItem = useCallback(
    (item: IMovie, index: number) => (
      <AutocompleteItem
        key={index}
        style={[!item.visible ? {display: 'none'} : {}]}
        title={item.title}
      />
    ),
    [movies],
  );

  const updatedMovies: IMovie[] = useMemo(() => {
    const movs: IMovie[] = [
      {
        id: 0,
        title: '',
        visible: false,
      },
    ];

    movies.forEach(u => {
      movs.push({
        id: u.id,
        title: u.title,
        visible: u.visible,
      });
    });

    return movs;
  }, [movies]);

  return (
    <Autocomplete
      status="primary"
      size="large"
      label="Movies"
      placeholder="Search movies"
      value={movie}
      placement="inner top"
      onSelect={onSelect}
      onChangeText={onChangeText}
      style={[
        {
          width: Dimensions.get('screen').width - 50,
        },
      ]}>
      {updatedMovies.map(renderAutocompleteItem)}
    </Autocomplete>
  );
};
SrMouraSilva commented 3 months ago

It seems there are a couple bugs involved here with Autocomplete:

1. Regarding **not being able to focus/edit the Autocomplete field again once it has some text entered which doesn't result in any item matches**, that involves `this.state.listVisible` being used to determine if the `Popover` containing the `Input` and `List` components is visible.  Once there is text in the `Input` that doesn't result in a match, the `Popover` can't be visible again currently.

A workaround to this is including the query text as a new option from the items.

const defaultOptions = ['house', 'car', 'ball']

const [content, setContent] = React.useState({
    query: undefined as string | undefined,
    items: defaultOptions,
});

const onChangeText = useCallback(
    (query: string) => {
      const items = defaultOptions.filter(item => item.toLowerCase().includes(query.toLowerCase()))

      // If there is any alternative, show them
      items.length > 0
        ? setContent({query, items})
        // Else, show the query string as a new option
        : setContent({query, items: [query]});
    },
    [defaultOptions],
  );
<Autocomplete
    //...
    onChangeText={onChangeText}
  >
    {items.map(content => <AutocompleteItem key={content} title={content} />}
</Autocomplete>