or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

class-component.mdcombobox.mdindex.mdmultiple-selection.mdselect.md
tile.json

combobox.mddocs/

Combobox Components

The useCombobox hook provides functionality for building accessible combobox/autocomplete components with input field and filtering capabilities. It's ideal for scenarios where users need to type to filter options or enter custom values.

Capabilities

useCombobox Hook

Creates a combobox component with input field, filtering, and full accessibility features.

/**
 * Hook for building accessible combobox/autocomplete components
 * @param props - Configuration options for the combobox component
 * @returns Object containing state, actions, and prop getters
 */
function useCombobox<Item>(props: UseComboboxProps<Item>): UseComboboxReturnValue<Item>;

interface UseComboboxProps<Item> {
  /** Array of items to display in the combobox dropdown */
  items: Item[];
  /** Function to convert an item to its string representation */
  itemToString?: (item: Item | null) => string;
  /** Function to generate a unique key for each item */
  itemToKey?: (item: Item | null) => any;
  /** Function to determine if an item is disabled */
  isItemDisabled?: (item: Item, index: number) => boolean;
  /** Currently highlighted item index */
  highlightedIndex?: number;
  /** Initial highlighted index when component mounts */
  initialHighlightedIndex?: number;
  /** Default highlighted index for uncontrolled usage */
  defaultHighlightedIndex?: number;
  /** Whether the dropdown is open */
  isOpen?: boolean;
  /** Initial open state when component mounts */
  initialIsOpen?: boolean;
  /** Default open state for uncontrolled usage */
  defaultIsOpen?: boolean;
  /** Currently selected item */
  selectedItem?: Item | null;
  /** Initial selected item when component mounts */
  initialSelectedItem?: Item | null;
  /** Default selected item for uncontrolled usage */
  defaultSelectedItem?: Item | null;
  /** Current input value */
  inputValue?: string;
  /** Initial input value when component mounts */
  initialInputValue?: string;
  /** Default input value for uncontrolled usage */
  defaultInputValue?: string;
  /** Custom ID for the component */
  id?: string;
  /** ID for the label element */
  labelId?: string;
  /** ID for the menu element */
  menuId?: string;
  /** ID for the toggle button element */
  toggleButtonId?: string;
  /** ID for the input element */
  inputId?: string;
  /** Function to generate IDs for menu items */
  getItemId?: (index: number) => string;
  /** Custom function to handle scrolling items into view */
  scrollIntoView?: (node: HTMLElement, menuNode: HTMLElement) => void;
  /** Custom state reducer for advanced state management */
  stateReducer?: (
    state: UseComboboxState<Item>,
    actionAndChanges: UseComboboxStateChangeOptions<Item>
  ) => Partial<UseComboboxState<Item>>;
  /** Callback when selected item changes */
  onSelectedItemChange?: (changes: UseComboboxSelectedItemChange<Item>) => void;
  /** Callback when open state changes */
  onIsOpenChange?: (changes: UseComboboxIsOpenChange<Item>) => void;
  /** Callback when highlighted index changes */
  onHighlightedIndexChange?: (changes: UseComboboxHighlightedIndexChange<Item>) => void;
  /** Callback when input value changes */
  onInputValueChange?: (changes: UseComboboxInputValueChange<Item>) => void;
  /** Callback when any state changes */
  onStateChange?: (changes: UseComboboxStateChange<Item>) => void;
  /** Function to generate accessibility status messages */
  getA11yStatusMessage?: (options: UseComboboxState<Item>) => string;
  /** Environment object for SSR/testing scenarios */
  environment?: Environment;
}

interface UseComboboxReturnValue<Item> {
  /** Current highlighted item index */
  highlightedIndex: number;
  /** Currently selected item */
  selectedItem: Item | null;
  /** Whether the dropdown menu is open */
  isOpen: boolean;
  /** Current input value */
  inputValue: string;
  
  /** Actions for controlling the combobox programmatically */
  reset: () => void;
  openMenu: () => void;
  closeMenu: () => void;
  toggleMenu: () => void;
  selectItem: (item: Item | null) => void;
  setHighlightedIndex: (index: number) => void;
  setInputValue: (inputValue: string) => void;
  
  /** Prop getters for UI elements */
  getInputProps: <Options>(
    options?: UseComboboxGetInputPropsOptions & Options,
    otherOptions?: GetPropsCommonOptions
  ) => UseComboboxGetInputPropsReturnValue;
  getToggleButtonProps: <Options>(
    options?: UseComboboxGetToggleButtonPropsOptions & Options
  ) => UseComboboxGetToggleButtonPropsReturnValue;
  getLabelProps: <Options>(
    options?: UseComboboxGetLabelPropsOptions & Options
  ) => UseComboboxGetLabelPropsReturnValue;
  getMenuProps: <Options>(
    options?: UseComboboxGetMenuPropsOptions & Options,
    otherOptions?: GetPropsCommonOptions
  ) => UseComboboxGetMenuPropsReturnValue;
  getItemProps: <Options>(
    options: UseComboboxGetItemPropsOptions<Item> & Options
  ) => UseComboboxGetItemPropsReturnValue;
}

Usage Examples:

import { useCombobox } from 'downshift';

// Basic autocomplete example
function BasicCombobox() {
  const items = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'];
  const [inputItems, setInputItems] = useState(items);
  
  const {
    isOpen,
    selectedItem,
    getToggleButtonProps,
    getLabelProps,
    getMenuProps,
    getInputProps,
    highlightedIndex,
    getItemProps,
  } = useCombobox({
    items: inputItems,
    onInputValueChange: ({ inputValue }) => {
      setInputItems(
        items.filter(item =>
          item.toLowerCase().startsWith(inputValue.toLowerCase())
        )
      );
    },
  });

  return (
    <div>
      <label {...getLabelProps()}>Choose a fruit:</label>
      <div style={{ display: 'inline-flex' }}>
        <input {...getInputProps()} />
        <button type="button" {...getToggleButtonProps()} aria-label="toggle menu">
          ↓
        </button>
      </div>
      <ul {...getMenuProps()}>
        {isOpen &&
          inputItems.map((item, index) => (
            <li
              style={highlightedIndex === index ? { backgroundColor: '#bde4ff' } : {}}
              key={`${item}${index}`}
              {...getItemProps({ item, index })}
            >
              {item}
            </li>
          ))}
      </ul>
    </div>
  );
}

// Advanced combobox with complex objects
function AdvancedCombobox() {
  const allItems = [
    { id: 1, name: 'Apple', category: 'fruit', color: 'red' },
    { id: 2, name: 'Banana', category: 'fruit', color: 'yellow' },
    { id: 3, name: 'Carrot', category: 'vegetable', color: 'orange' },
    { id: 4, name: 'Date', category: 'fruit', color: 'brown' },
  ];
  
  const [inputItems, setInputItems] = useState(allItems);

  const {
    isOpen,
    selectedItem,
    getToggleButtonProps,
    getLabelProps,
    getMenuProps,
    getInputProps,
    highlightedIndex,
    getItemProps,
    inputValue,
  } = useCombobox({
    items: inputItems,
    itemToString: (item) => (item ? item.name : ''),
    onInputValueChange: ({ inputValue }) => {
      setInputItems(
        allItems.filter(item =>
          !inputValue || item.name.toLowerCase().includes(inputValue.toLowerCase())
        )
      );
    },
    onSelectedItemChange: ({ selectedItem }) => {
      console.log('Selected:', selectedItem);
      // Reset input after selection
      setInputItems(allItems);
    },
  });

  return (
    <div>
      <label {...getLabelProps()}>Search items:</label>
      <div style={{ display: 'inline-flex' }}>
        <input {...getInputProps()} placeholder="Type to search..." />
        <button type="button" {...getToggleButtonProps()} aria-label="toggle menu">
          {isOpen ? '↑' : '↓'}
        </button>
      </div>
      <ul {...getMenuProps()}>
        {isOpen &&
          inputItems.map((item, index) => (
            <li
              style={highlightedIndex === index ? { backgroundColor: '#bde4ff' } : {}}
              key={item.id}
              {...getItemProps({ item, index })}
            >
              <strong>{item.name}</strong> ({item.category}) - {item.color}
            </li>
          ))}
      </ul>
      {selectedItem && (
        <div>Selected: {selectedItem.name}</div>
      )}
    </div>
  );
}

State and Actions

The hook returns current state and action functions for programmatic control.

interface UseComboboxState<Item> {
  /** Index of currently highlighted item (-1 if none) */
  highlightedIndex: number;
  /** Currently selected item */
  selectedItem: Item | null;
  /** Whether dropdown menu is open */
  isOpen: boolean;
  /** Current input value */
  inputValue: string;
}

interface UseComboboxActions<Item> {
  /** Reset to initial state */
  reset: () => void;
  /** Open the dropdown menu */
  openMenu: () => void;
  /** Close the dropdown menu */
  closeMenu: () => void;
  /** Toggle the dropdown menu open/closed */
  toggleMenu: () => void;
  /** Select a specific item */
  selectItem: (item: Item | null) => void;
  /** Set the highlighted index */
  setHighlightedIndex: (index: number) => void;
  /** Set the input value */
  setInputValue: (inputValue: string) => void;
}

Prop Getters

Prop getters return props to spread onto DOM elements with proper event handlers and accessibility attributes.

interface UseComboboxGetInputPropsOptions extends React.HTMLProps<HTMLInputElement> {
  /** Custom ref key (defaults to 'ref') */
  refKey?: string;
  /** Whether the input is disabled */
  disabled?: boolean;
}

interface UseComboboxGetInputPropsReturnValue {
  'aria-activedescendant': string;
  'aria-autocomplete': 'list';
  'aria-controls': string;
  'aria-expanded': boolean;
  'aria-labelledby': string | undefined;
  autoComplete: 'off';
  id: string;
  role: 'combobox';
  value: string;
  ref?: React.RefObject<any>;
  onChange?: React.ChangeEventHandler;
  onClick?: React.MouseEventHandler;
  onKeyDown?: React.KeyboardEventHandler;
  onBlur?: React.FocusEventHandler;
}

interface UseComboboxGetToggleButtonPropsOptions extends React.HTMLProps<HTMLButtonElement> {
  /** Custom ref key (defaults to 'ref') */
  refKey?: string;
  /** Event handler for React Native press events */
  onPress?: (event: React.BaseSyntheticEvent) => void;
}

interface UseComboboxGetToggleButtonPropsReturnValue {
  'aria-controls': string;
  'aria-expanded': boolean;
  id: string;
  tabIndex: -1;
  ref?: React.RefObject<any>;
  onClick?: React.MouseEventHandler;
  onPress?: (event: React.BaseSyntheticEvent) => void;
}

interface UseComboboxGetMenuPropsReturnValue {
  'aria-labelledby': string | undefined;
  role: 'listbox';
  id: string;
  ref?: React.RefObject<any>;
  onMouseLeave: React.MouseEventHandler;
}

interface UseComboboxGetItemPropsOptions<Item> {
  /** The item this props object is for */
  item: Item;
  /** Index of the item in the items array */
  index?: number;
  /** Custom ref key (defaults to 'ref') */
  refKey?: string;
}

interface UseComboboxGetItemPropsReturnValue {
  'aria-disabled': boolean;
  'aria-selected': boolean;
  id: string;
  role: 'option';
  ref?: React.RefObject<any>;
  onClick?: React.MouseEventHandler;
  onMouseDown?: React.MouseEventHandler;
  onMouseMove?: React.MouseEventHandler;
  onPress?: React.MouseEventHandler;
}

State Change Types

Constants for identifying different types of state changes in the state reducer.

enum UseComboboxStateChangeTypes {
  InputKeyDownArrowDown = '__input_keydown_arrow_down__',
  InputKeyDownArrowUp = '__input_keydown_arrow_up__',
  InputKeyDownEscape = '__input_keydown_escape__',
  InputKeyDownHome = '__input_keydown_home__',
  InputKeyDownEnd = '__input_keydown_end__',
  InputKeyDownPageUp = '__input_keydown_page_up__',
  InputKeyDownPageDown = '__input_keydown_page_down__',
  InputKeyDownEnter = '__input_keydown_enter__',
  InputChange = '__input_change__',
  InputBlur = '__input_blur__',
  InputClick = '__input_click__',
  MenuMouseLeave = '__menu_mouse_leave__',
  ItemMouseMove = '__item_mouse_move__',
  ItemClick = '__item_click__',
  ToggleButtonClick = '__togglebutton_click__',
  FunctionToggleMenu = '__function_toggle_menu__',
  FunctionOpenMenu = '__function_open_menu__',
  FunctionCloseMenu = '__function_close_menu__',
  FunctionSetHighlightedIndex = '__function_set_highlighted_index__',
  FunctionSelectItem = '__function_select_item__',
  FunctionSetInputValue = '__function_set_input_value__',
  FunctionReset = '__function_reset__',
  ControlledPropUpdatedSelectedItem = '__controlled_prop_updated_selected_item__',
}

Access via useCombobox.stateChangeTypes:

import { useCombobox } from 'downshift';

const stateReducer = (state, actionAndChanges) => {
  switch (actionAndChanges.type) {
    case useCombobox.stateChangeTypes.InputChange:
      // Handle input value changes
      return {
        ...actionAndChanges.changes,
        // Custom logic here
      };
    case useCombobox.stateChangeTypes.ItemClick:
      // Handle item selection
      return {
        ...actionAndChanges.changes,
        isOpen: false, // Always close menu on item click
      };
    default:
      return actionAndChanges.changes;
  }
};

Integration with useMultipleSelection

The combobox can be combined with useMultipleSelection for multi-select autocomplete scenarios:

import { useCombobox, useMultipleSelection } from 'downshift';

function MultiCombobox() {
  const items = ['Apple', 'Banana', 'Cherry', 'Date'];
  const [inputItems, setInputItems] = useState(items);
  
  const {
    getSelectedItemProps,
    getDropdownProps,
    addSelectedItem,
    removeSelectedItem,
    selectedItems,
  } = useMultipleSelection({
    initialSelectedItems: [],
  });
  
  const {
    isOpen,
    getToggleButtonProps,
    getLabelProps,
    getMenuProps,
    getInputProps,
    highlightedIndex,
    getItemProps,
    selectItem,
  } = useCombobox({
    items: inputItems,
    stateReducer: (state, actionAndChanges) => {
      const { changes, type } = actionAndChanges;
      switch (type) {
        case useCombobox.stateChangeTypes.InputKeyDownEnter:
        case useCombobox.stateChangeTypes.ItemClick:
          return {
            ...changes,
            isOpen: true, // Keep menu open
            highlightedIndex: 0,
            inputValue: '',
          };
        default:
          return changes;
      }
    },
    onStateChange: ({ inputValue, type, selectedItem }) => {
      switch (type) {
        case useCombobox.stateChangeTypes.InputKeyDownEnter:
        case useCombobox.stateChangeTypes.ItemClick:
          if (selectedItem) {
            addSelectedItem(selectedItem);
            setInputItems(items.filter(item => !selectedItems.includes(item) && item !== selectedItem));
          }
          break;
        case useCombobox.stateChangeTypes.InputChange:
          setInputItems(
            items.filter(
              item =>
                !selectedItems.includes(item) &&
                item.toLowerCase().startsWith(inputValue.toLowerCase())
            )
          );
          break;
        default:
          break;
      }
    },
    onInputValueChange: ({ inputValue }) => {
      setInputItems(
        items.filter(
          item =>
            !selectedItems.includes(item) &&
            item.toLowerCase().startsWith(inputValue.toLowerCase())
        )
      );
    },
  });

  return (
    <div>
      <label {...getLabelProps()}>Choose fruits:</label>
      <div {...getDropdownProps()}>
        {selectedItems.map((selectedItem, index) => (
          <span
            key={`selected-item-${index}`}
            {...getSelectedItemProps({ selectedItem, index })}
          >
            {selectedItem}
            <span onClick={() => removeSelectedItem(selectedItem)}>×</span>
          </span>
        ))}
        <input {...getInputProps()} />
        <button type="button" {...getToggleButtonProps()} aria-label="toggle menu">
          ↓
        </button>
      </div>
      <ul {...getMenuProps()}>
        {isOpen &&
          inputItems.map((item, index) => (
            <li
              style={highlightedIndex === index ? { backgroundColor: '#bde4ff' } : {}}
              key={`${item}${index}`}
              {...getItemProps({ item, index })}
            >
              {item}
            </li>
          ))}
      </ul>
    </div>
  );
}