Search code examples
reactjstypescriptcomboboxreact-aria

React Aria Combobox does not filter the entries based on the input


Hello I'm using the Combobox from React aria I have followed official docs however once I enter something in the input there is no filtering happening. For instance if you write PHP in the combobox it still shows the rest of entries which is incorrect behavior. I won't post the code for styling and types because it doesn't influence the behavior. In a Listbox I have noticed that state.collection.keyMap is not being update, but that just my guess.

Here is my code

//Autocomplete wrapper
export const Autocomplete = ({
  options,
  isLoading,
  isDisabled
}: AutocompleteProps) => {
  return (
    <div>
      <ComboBox items={options} isDisabled={isDisabled}>
        {(item) => (
          <Item textValue={item.label} key={item.label}>
            <p>{item.label}</p>
          </Item>
        )}
      </ComboBox>
    </div>
  );
};
// Combobox with hook to enclose all state
import { useComboBox } from "./useCombobox";

export { Item, Section } from "react-stately";

export function ComboBox<T extends object>(props: ComboBoxProps<T>) {
  const {
    buttonProps,
    inputProps,
    listBoxProps,
    buttonRef,
    inputRef,
    listBoxRef,
    popoverRef,
    state
  } = useComboBox(props);

  return (
    <Styled.Wrapper>
      <Styled.InputGroup isFocused={state.isFocused}>
        <Styled.Input
          {...inputProps}
          ref={inputRef}
          isFocused={state.isFocused}
          aria-label="label"
          placeholder="placeholder"
        />
        <Styled.Button {...buttonProps} ref={buttonRef}>
          <span aria-hidden="true">▼</span>
        </Styled.Button>
      </Styled.InputGroup>
      {state.isOpen && (
        <Popover
          popoverRef={popoverRef}
          triggerRef={inputRef}
          state={state}
          isNonModal
          placement="bottom start"
        >
          <ListBox {...listBoxProps} listBoxRef={listBoxRef} state={state} />
        </Popover>
      )}
    </Styled.Wrapper>
  );
}

// facade hook for managing the state
import * as React from "react";
import {
  useButton,
  useFilter,
  useComboBox as useAriaComboBox
} from "react-aria";
import { useComboBoxState } from "react-stately";
import type { ComboBoxProps } from "@react-types/combobox";

export const useComboBox = <T extends object>(props: ComboBoxProps<T>) => {
  const { contains } = useFilter({ sensitivity: "base" });
  const state = useComboBoxState({ ...props, defaultFilter: contains });

  const buttonRef = React.useRef(null);
  const inputRef = React.useRef(null);
  const listBoxRef = React.useRef(null);
  const popoverRef = React.useRef(null);

  const {
    buttonProps: triggerProps,
    inputProps,
    listBoxProps
  } = useAriaComboBox(
    {
      ...props,
      inputRef,
      buttonRef,
      listBoxRef,
      popoverRef
    },
    state
  );

  const { buttonProps } = useButton(triggerProps, buttonRef);

  return {
    buttonProps,
    inputProps,
    listBoxProps,
    buttonRef,
    inputRef,
    listBoxRef,
    popoverRef,
    state
  };
};
//Listbox and options
const OptionContext = React.createContext<OptionContextValue>({
  labelProps: {},
  descriptionProps: {}
});

export const Option = ({ item, state }: OptionProps) => {
  const ref = React.useRef<HTMLLIElement>(null);
  const { optionProps, labelProps, descriptionProps, isSelected } = useOption(
    {
      key: item.key
    },
    state,
    ref
  );

  return (
    <Styled.ListItem {...optionProps} $isSelected={isSelected} ref={ref}>
      <Styled.ItemContent>
        <OptionContext.Provider value={{ labelProps, descriptionProps }}>
          {item.rendered}
        </OptionContext.Provider>
      </Styled.ItemContent>
      {isSelected && <span role="img">✅</span>}
    </Styled.ListItem>
  );
};

export const ListBox = (props: ListBoxProps) => {
  const ref = React.useRef<HTMLUListElement>(null);
  const { listBoxRef = ref, state } = props;
  const { listBoxProps } = useListBox(props, state, listBoxRef);

  console.log("state.collection", state.collection.keyMap);

  return (
    <Styled.List {...listBoxProps} ref={listBoxRef}>
      {[...state.collection].map((item) => (
        <Option key={item.key} item={item} state={state} />
      ))}
    </Styled.List>
  );
};

Live demo: https://codesandbox.io/s/flamboyant-rosalind-py0ply?file=/src/Combobox/Listbox/Listbox.styles.ts


Solution

  • It turns out that I have used items as static list where I should have used the defaultItems instead, so the behaviour was correct since the list was hardcoded and component wasn't controlled. In wrapper component only change is to change the prop name like in snippet below:

    //Autocomplete wrapper
    export const Autocomplete = ({
      options,
      isLoading,
      isDisabled
    }: AutocompleteProps) => {
      return (
        <div>
          <ComboBox defaultItems={options} isDisabled={isDisabled}>
            {(item) => (
              <Item textValue={item.label} key={item.label}>
                <p>{item.label}</p>
              </Item>
            )}
          </ComboBox>
        </div>
      );
    };