or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

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

class-component.mddocs/

Class Component (Legacy)

The Downshift class component is the original render prop-based API for building accessible dropdown components. While the modern hook-based APIs are recommended for new projects, the class component remains fully supported and provides additional flexibility for complex use cases.

Capabilities

Downshift Class Component

A React class component that uses the render prop pattern to provide complete control over dropdown behavior and rendering.

/**
 * Class component for building accessible dropdown components using render props
 */
class Downshift<Item = any> extends React.Component<DownshiftProps<Item>> {
  /** State change type constants for use in state reducers */
  static stateChangeTypes: {
    unknown: StateChangeTypes.unknown;
    mouseUp: StateChangeTypes.mouseUp;
    itemMouseEnter: StateChangeTypes.itemMouseEnter;
    keyDownArrowUp: StateChangeTypes.keyDownArrowUp;
    keyDownArrowDown: StateChangeTypes.keyDownArrowDown;
    keyDownEscape: StateChangeTypes.keyDownEscape;
    keyDownEnter: StateChangeTypes.keyDownEnter;
    clickItem: StateChangeTypes.clickItem;
    blurInput: StateChangeTypes.blurInput;
    changeInput: StateChangeTypes.changeInput;
    keyDownSpaceButton: StateChangeTypes.keyDownSpaceButton;
    clickButton: StateChangeTypes.clickButton;
    blurButton: StateChangeTypes.blurButton;
    controlledPropUpdatedSelectedItem: StateChangeTypes.controlledPropUpdatedSelectedItem;
    touchEnd: StateChangeTypes.touchEnd;
  };
}

interface DownshiftProps<Item> {
  /** Render prop function that receives state and helpers */
  children?: (options: ControllerStateAndHelpers<Item>) => React.ReactNode;
  
  /** Initial state values */
  initialSelectedItem?: Item;
  initialInputValue?: string;
  initialHighlightedIndex?: number | null;
  initialIsOpen?: boolean;
  
  /** Default values for uncontrolled usage */
  defaultHighlightedIndex?: number | null;
  defaultIsOpen?: boolean;
  
  /** Function to convert an item to its string representation */
  itemToString?: (item: Item | null) => string;
  
  /** Function to determine if selected item has changed */
  selectedItemChanged?: (prevItem: Item, item: Item) => boolean;
  
  /** Function to generate accessibility status messages */
  getA11yStatusMessage?: (options: A11yStatusMessageOptions<Item>) => string;
  
  /** Callback when selection changes */
  onChange?: (
    selectedItem: Item | null,
    stateAndHelpers: ControllerStateAndHelpers<Item>
  ) => void;
  
  /** Callback when item is selected (legacy, use onChange) */
  onSelect?: (
    selectedItem: Item | null,
    stateAndHelpers: ControllerStateAndHelpers<Item>
  ) => void;
  
  /** Callback when any state changes */
  onStateChange?: (
    options: StateChangeOptions<Item>,
    stateAndHelpers: ControllerStateAndHelpers<Item>
  ) => void;
  
  /** Callback when input value changes */
  onInputValueChange?: (
    inputValue: string,
    stateAndHelpers: ControllerStateAndHelpers<Item>
  ) => void;
  
  /** Custom state reducer for advanced state management */
  stateReducer?: (
    state: DownshiftState<Item>,
    changes: StateChangeOptions<Item>
  ) => Partial<StateChangeOptions<Item>>;
  
  /** Total number of items (for virtual scrolling scenarios) */
  itemCount?: number;
  
  /** Controlled state props */
  highlightedIndex?: number | null;
  inputValue?: string | null;
  isOpen?: boolean;
  selectedItem?: Item | null;
  
  /** Custom IDs */
  id?: string;
  inputId?: string;
  labelId?: string;
  menuId?: string;
  getItemId?: (index?: number) => string;
  
  /** Environment for SSR/testing */
  environment?: Environment;
  
  /** Callback when clicking outside the component */
  onOuterClick?: (stateAndHelpers: ControllerStateAndHelpers<Item>) => void;
  
  /** Custom scroll function */
  scrollIntoView?: (node: HTMLElement, menuNode: HTMLElement) => void;
  
  /** Callback for user-initiated actions */
  onUserAction?: (
    options: StateChangeOptions<Item>,
    stateAndHelpers: ControllerStateAndHelpers<Item>
  ) => void;
  
  /** Suppress ref errors in development */
  suppressRefError?: boolean;
}

Usage Examples:

import Downshift from 'downshift';

// Basic dropdown example
function BasicDropdown() {
  const items = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'];

  return (
    <Downshift
      onChange={selection => alert(`You selected ${selection}`)}
      itemToString={item => (item ? item : '')}
    >
      {({
        getInputProps,
        getItemProps,
        getLabelProps,
        getMenuProps,
        isOpen,
        inputValue,
        highlightedIndex,
        selectedItem,
        getRootProps,
      }) => (
        <div>
          <label {...getLabelProps()}>Enter a fruit</label>
          <div {...getRootProps()} style={{ position: 'relative' }}>
            <input {...getInputProps()} />
            <ul {...getMenuProps()}>
              {isOpen
                ? items
                    .filter(item => !inputValue || item.includes(inputValue))
                    .map((item, index) => (
                      <li
                        {...getItemProps({
                          key: item,
                          index,
                          item,
                          style: {
                            backgroundColor: highlightedIndex === index ? 'lightgray' : 'white',
                            fontWeight: selectedItem === item ? 'bold' : 'normal',
                          },
                        })}
                      >
                        {item}
                      </li>
                    ))
                : null}
            </ul>
          </div>
        </div>
      )}
    </Downshift>
  );
}

// Advanced example with state reducer
function AdvancedDropdown() {
  const items = [
    { id: 1, name: 'Apple', color: 'red' },
    { id: 2, name: 'Banana', color: 'yellow' },
    { id: 3, name: 'Cherry', color: 'red' },
  ];

  const stateReducer = (state, changes) => {
    switch (changes.type) {
      case Downshift.stateChangeTypes.keyDownEnter:
      case Downshift.stateChangeTypes.clickItem:
        return {
          ...changes,
          isOpen: state.isOpen, // Keep menu open after selection
          highlightedIndex: state.highlightedIndex,
        };
      default:
        return changes;
    }
  };

  return (
    <Downshift
      onChange={selection => console.log('Selected:', selection)}
      itemToString={item => (item ? item.name : '')}
      stateReducer={stateReducer}
    >
      {({
        getInputProps,
        getItemProps,
        getLabelProps,
        getMenuProps,
        getToggleButtonProps,
        isOpen,
        inputValue,
        highlightedIndex,
        selectedItem,
        getRootProps,
      }) => (
        <div>
          <label {...getLabelProps()}>Choose a fruit:</label>
          <div {...getRootProps()} style={{ position: 'relative', display: 'inline-block' }}>
            <input {...getInputProps()} placeholder="Type or click button" />
            <button type="button" {...getToggleButtonProps()}>
              {isOpen ? '↑' : '↓'}
            </button>
            <ul {...getMenuProps()} style={{ position: 'absolute', top: '100%', left: 0, right: 0 }}>
              {isOpen
                ? items
                    .filter(item => !inputValue || item.name.toLowerCase().includes(inputValue.toLowerCase()))
                    .map((item, index) => (
                      <li
                        {...getItemProps({
                          key: item.id,
                          index,
                          item,
                        })}
                        style={{
                          backgroundColor: highlightedIndex === index ? 'lightblue' : 'white',
                          fontWeight: selectedItem === item ? 'bold' : 'normal',
                          padding: '8px',
                          cursor: 'pointer',
                        }}
                      >
                        {item.name} ({item.color})
                      </li>
                    ))
                : null}
            </ul>
          </div>
          {selectedItem && <div>Selected: {selectedItem.name}</div>}
        </div>
      )}
    </Downshift>
  );
}

Render Prop Interface

The render prop function receives a comprehensive object with state, actions, and prop getters:

interface ControllerStateAndHelpers<Item> extends DownshiftState<Item>, PropGetters<Item>, Actions<Item> {
  /** Current component state */
  highlightedIndex: number | null;
  inputValue: string | null;
  isOpen: boolean;
  selectedItem: Item | null;
  
  /** Action functions */
  reset: (otherStateToSet?: Partial<StateChangeOptions<Item>>, cb?: Callback) => void;
  openMenu: (cb?: Callback) => void;
  closeMenu: (cb?: Callback) => void;
  toggleMenu: (otherStateToSet?: Partial<StateChangeOptions<Item>>, cb?: Callback) => void;
  selectItem: (item: Item | null, otherStateToSet?: Partial<StateChangeOptions<Item>>, cb?: Callback) => void;
  selectItemAtIndex: (index: number, otherStateToSet?: Partial<StateChangeOptions<Item>>, cb?: Callback) => void;
  selectHighlightedItem: (otherStateToSet?: Partial<StateChangeOptions<Item>>, cb?: Callback) => void;
  setHighlightedIndex: (index: number, otherStateToSet?: Partial<StateChangeOptions<Item>>, cb?: Callback) => void;
  clearSelection: (cb?: Callback) => void;
  clearItems: () => void;
  setItemCount: (count: number) => void;
  unsetItemCount: () => void;
  setState: (stateToSet: Partial<StateChangeOptions<Item>> | StateChangeFunction<Item>, cb?: Callback) => void;
  itemToString: (item: Item | null) => string;
  
  /** Prop getter functions */
  getRootProps: <Options>(options?: GetRootPropsOptions & Options, otherOptions?: GetPropsCommonOptions) => GetRootPropsReturnValue;
  getToggleButtonProps: <Options>(options?: GetToggleButtonPropsOptions & Options) => GetToggleButtonPropsReturnValue;
  getLabelProps: <Options>(options?: GetLabelPropsOptions & Options) => GetLabelPropsReturnValue;
  getMenuProps: <Options>(options?: GetMenuPropsOptions & Options, otherOptions?: GetPropsCommonOptions) => GetMenuPropsReturnValue;
  getInputProps: <Options>(options?: GetInputPropsOptions & Options) => GetInputPropsReturnValue;
  getItemProps: <Options>(options: GetItemPropsOptions<Item> & Options) => GetItemPropsReturnValue;
}

State Structure

The component maintains internal state that can be controlled or uncontrolled:

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

Prop Getters

The class component provides the same prop getters as the hooks, with additional options:

interface GetRootPropsOptions {
  /** Custom ref key (defaults to 'ref') */
  refKey?: string;
  /** React ref object */
  ref?: React.RefObject<any>;
}

interface GetRootPropsReturnValue {
  'aria-expanded': boolean;
  'aria-haspopup': 'listbox';
  'aria-labelledby': string;
  'aria-owns': string | undefined;
  role: 'combobox';
  ref?: React.RefObject<any>;
}

interface GetToggleButtonPropsOptions extends React.HTMLProps<HTMLButtonElement> {
  /** Whether the button is disabled */
  disabled?: boolean;
  /** Event handler for React Native press events */
  onPress?: (event: React.BaseSyntheticEvent) => void;
}

interface GetToggleButtonPropsReturnValue {
  'aria-label': 'close menu' | 'open menu';
  'aria-haspopup': true;
  'data-toggle': true;
  role: 'button';
  type: 'button';
  onPress?: (event: React.BaseSyntheticEvent) => void;
  onClick?: React.MouseEventHandler;
  onKeyDown?: React.KeyboardEventHandler;
  onKeyUp?: React.KeyboardEventHandler;
  onBlur?: React.FocusEventHandler;
}

State Change Types

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

enum StateChangeTypes {
  unknown = '__autocomplete_unknown__',
  mouseUp = '__autocomplete_mouseup__',
  itemMouseEnter = '__autocomplete_item_mouseenter__',
  keyDownArrowUp = '__autocomplete_keydown_arrow_up__',
  keyDownArrowDown = '__autocomplete_keydown_arrow_down__',
  keyDownEscape = '__autocomplete_keydown_escape__',
  keyDownEnter = '__autocomplete_keydown_enter__',
  keyDownHome = '__autocomplete_keydown_home__',
  keyDownEnd = '__autocomplete_keydown_end__',
  clickItem = '__autocomplete_click_item__',
  blurInput = '__autocomplete_blur_input__',
  changeInput = '__autocomplete_change_input__',
  keyDownSpaceButton = '__autocomplete_keydown_space_button__',
  clickButton = '__autocomplete_click_button__',
  blurButton = '__autocomplete_blur_button__',
  controlledPropUpdatedSelectedItem = '__autocomplete_controlled_prop_updated_selected_item__',
  touchEnd = '__autocomplete_touchend__',
}

Access via Downshift.stateChangeTypes:

import Downshift from 'downshift';

const stateReducer = (state, changes) => {
  switch (changes.type) {
    case Downshift.stateChangeTypes.keyDownEnter:
      // Handle Enter key press
      return changes;
    case Downshift.stateChangeTypes.clickItem:
      // Handle item click
      return {
        ...changes,
        isOpen: false, // Close menu after selection
      };
    default:
      return changes;
  }
};

Migration to Hooks

For new projects, consider using the modern hook-based APIs instead:

// Instead of this class component approach:
<Downshift itemToString={item => item.name}>
  {({ getInputProps, getItemProps, isOpen, inputValue, highlightedIndex }) => (
    // JSX here
  )}
</Downshift>

// Use this hook approach:
const { getInputProps, getItemProps, isOpen, inputValue, highlightedIndex } = useCombobox({
  items,
  itemToString: item => item.name,
});

return (
  // JSX here
);

The hooks provide the same functionality with better TypeScript support, smaller bundle size, and modern React patterns.