1.1.1 • Published 3 years ago

@tracksuitdev/use-select v1.1.1

Weekly downloads
-
License
MIT
Repository
github
Last release
3 years ago

use-select npm (scoped) npm bundle size (scoped) NPM

React hooks for building select and combobox components.

Installation

npm install @tracksuitdev/use-select

or if you use yarn

yarn add @tracksuitdev/use-select

useSelect

useSelect<T, S, D>(props: UseSelectProps<T>): UseSelect<T, S, D>

Provides state and callbacks for building select component.

Only required prop are items that can be selected. To control value, provide value and onChange props.

Type parameters

NameType
TT - Type of items
SS: HTMLElement = HTMLDivElement - Type of select element
DD: HTMLElement = HTMLUListElement- Type of dropdown element

Props

UseSelectProps<T> = Items<T> & ValueControl<T> & Handlers & Flags

Return value

UseSelect<T, S, D>

NameTypeDescription
clear(e: ReactMouseEvent) => voidCalls onChange with undefined or empty array value in case of multiple selection. Prevents event propagation
dropdownRefRefObject<D>Ref for dropdown element, used internally to allow closing of dropdown on outside click and scrolling to highlighted index item when using arrow keys to highlighted items.
handleClick(e: ReactMouseEvent) => voidToggles isOpen flag, prevents event propagation
handleItemClick(item: T) => voidCalls select if item isn't selected or remove if item is selected
handleKeyDownKeyboardEventHandler<never>Handles ArrowUp, ArrowDown, Enter and Escape key down event, apply to select and dropdown element (add tabIndex=0 to allow key events on div element)
highlightedIndexnumberIndex of currently highlighted item, used for keyboard control, ArrowUp key decreases this, while ArrowDown key increases it
isOpenbooleanIndicates whether dropdown is open or not
isSelected(item: T) => booleanReturns true if item equals value, or in case of multiple selection, if item is part of value array
open() => voidSets isOpen to true
remove() => voidCalls onChange with value set to undefined
select(item: T) => voidCalls onChange with provided item set as value
selectRefRefObject<S>Ref for combobox element, used internally to allow closing of dropdown on outside click
setHighlightedIndex(index: number) => voidSets highlightedIndex to provided index

Usage

This example uses basic styling and markup, you can style your components however you want. Note that you need to assign selectRef and dropdownRef, this is needed so that isOpen is set to false (dropdown is closed) if you click outside select or dropdown element. If you want your dropdown to scroll to highlighted item when user presses arrow keys make your items direct children of dropdown element.

const Select = () => {
  const [value, setValue] = useState<string>();
  const {
    selectRef,
    open,
    handleKeyDown,
    isOpen,
    handleClick,
    dropdownRef,
    handleItemClick,
    isSelected,
    highlightedIndex,
  } = useSelect({
    items: ["item1", "item2", "item3"],
    onChange: value => setValue(value),
    value,
  });

  return (
    <div>
      <div ref={selectRef} tabIndex={0} onFocus={open} onKeyDown={handleKeyDown}> {/* select */}
        {value}
        <button onFocus={e => e.stopPropagation()} onClick={handleClick}>
          {isOpen ? <span>&#9650;</span> : <span>&#9660;</span>}
        </button>
      </div>
      {isOpen && ( // dropdown
        <ul
          ref={dropdownRef}
          tabIndex={0}
          onKeyDown={handleKeyDown}
          style={{ width: "400px", backgroundColor: "grey" }}>
          {items.map((item, index) => ( // item
            <li
              key={item}
              onClick={() => handleItemClick(item)}
              style={{
                color: isSelected(item) ? "blue" : "black",
                backgroundColor: highlightedIndex === index ? "green" : "grey",
                cursor: "pointer",
              }}>
              {item}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
};

useMultipleSelect

useMultipleSelect<T, S, D>(props: UseMultipleSelectProps<T>): UseMultipleSelect<T, S, D>

Allows selection of multiple items. Useful for building multiple select component.

Type parameters

NameType
TT - Type of items
SS: HTMLElement = HTMLDivElement - Type of select element
DD: HTMLElement = HTMLUListElement- Type of dropdown element

Props

UseMultipleSelectProps<T>: Items<T> & MultiValueControl<T> & Handlers & Flags

Same as useSelect props, only difference are value and onChange props, in this case value is an array and onChange expects array parameter.

Return value

UseMultipleSelect<T, S, D>: Omit<UseSelect<T, S, D>, "remove"> & { remove: (item: T) => void ; removeByIndex: (index: number) => void }

Returns a similar object to useSelect, difference is in remove function. Also provides removeByIndex function for removing items according to their index in value array.

NameTypeDescription
remove(item: T) => voidCalls onChange with value array without the provided item
removeByIndex(index: number) => voidCalls onChange with value array without the item at given index

Usage

This example uses basic styling and markup, you can style your components however you want. Note that you need to assign selectRef and dropdownRef, this is needed so that isOpen is set to false (dropdown is closed) if you click outside select or dropdown element. If you want your dropdown to scroll to highlighted item when user presses arrow keys make your items direct children of dropdown element.

const MultipleSelect = () => {
  const [value, setValue] = useState<string[]>();
  const {
    selectRef,
    dropdownRef,
    isOpen,
    open,
    handleKeyDown,
    handleClick,
    handleItemClick,
    isSelected,
    highlightedIndex,
  } = useMultipleSelect({
    items,
    onChange: value => setValue(value),
    value,
  });

  return (
    <div>
      <div ref={selectRef} tabIndex={0} onFocus={open} onKeyDown={handleKeyDown}>
        {value?.join(", ")}
        <button onFocus={e => e.stopPropagation()} onClick={handleClick}>
          {isOpen ? <span>&#9650;</span> : <span>&#9660;</span>}
        </button>
      </div>
      {isOpen && (
        <ul
          ref={dropdownRef}
          tabIndex={0}
          onKeyDown={handleKeyDown}
          style={{ width: "400px", backgroundColor: "grey" }}>
          {items.map((item, index) => (
            <li
              key={item}
              onClick={() => handleItemClick(item)}
              style={{
                color: isSelected(item) ? "blue" : "black",
                backgroundColor: highlightedIndex === index ? "green" : "grey",
                cursor: "pointer",
              }}>
              {item}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
};

useCombobox

useCombobox<T, S, D>(props: UseComboboxProps<T>): UseCombobox<T, S, D>

Hook that returns state and callbacks for controlling combobox component. Updates inputValue according to provided value (currently selected item). This keeps inputValue and value state in sync whenever an item is selected, or value was changed by some code.

Internally uses useSelect hook.

Type parameters

NameType
TT - Type of items
SS: HTMLElement = HTMLDivElement - Type of select element
DD: HTMLElement = HTMLUListElement- Type of dropdown element

Props

UseComboboxProps<T>: UseSelectProps<T> & ComboboxFunctions<T>

Similar to useSelect props with added filter and itemToString functions.

filter function is used to filter items according to current input value of combobox. If not provided, defaults to returning items that start with input value.

itemToString function converts item to string so items can be compared to input value.

Return value

UseCombobox<T, S, D>: UseSelect<T, S, D> & UseComboboxReturnValue<T>

Returns everything useSelect hook returns + everything contained in UseComboboxReturnValue type.

UseComboboxReturnValue<T>

NameTypeDescription
inputRefRefObject<HTMLInputElement>Ref that needs to be applied to combobox input element
inputValuestringValue of input element
itemsT[]Items filtered by filter prop, or in case of async combobox result of fetchItems
setInputValue(value: string) => voidSets input value to given value

Usage

This example uses basic styling and markup, you can style your components however you want. Note that you need to assign selectRef and dropdownRef, this is needed so that isOpen is set to false (dropdown is closed) if you click outside select or dropdown element. If you want your dropdown to scroll to highlighted item when user presses arrow keys make your items direct children of dropdown element.

const Combobox = () => {
  const [value, setValue] = useState<string>();
  const {
    selectRef,
    dropdownRef,
    inputRef,
    inputValue,
    open,
    setInputValue,
    handleKeyDown,
    handleClick,
    handleItemClick,
    isSelected,
    highlightedIndex,
    isOpen,
    items,
  } = useCombobox({
    items: comboboxItems,
    value,
    onChange: value => setValue(value),
    itemToString: item => item ?? "",
  });

  return (
    <div>
      <div ref={selectRef} tabIndex={0} onFocus={open} onKeyDown={handleKeyDown}>
        <input value={inputValue} onChange={({ target: { value } }) => setInputValue(value)} ref={inputRef} />
        <button onFocus={e => e.stopPropagation()} onClick={handleClick}>
          {isOpen ? <span>&#9650;</span> : <span>&#9660;</span>}
        </button>
      </div>
      {isOpen && (
        <ul
          ref={dropdownRef}
          tabIndex={0}
          onKeyDown={handleKeyDown}
          style={{ width: "400px", backgroundColor: "grey" }}>
          {items.map((item, index) => (
            <li
              key={item}
              onClick={() => handleItemClick(item)}
              style={{
                color: isSelected(item) ? "blue" : "black",
                backgroundColor: highlightedIndex === index ? "green" : "grey",
                cursor: "pointer",
              }}>
              {item}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
};

useMultipleCombobox

useMultipleCombobox<T, S, D>(props: UseMultipleComboboxProps<T>): UseMultipleCombobox<T, S, D>

Provides state and callbacks for combobox with multiple selection. When value prop changes, inputValue is set to empty string, thus allowing for selection of new item.

Internally it uses useMultipleSelect hook.

Uses same props as useMultipleSelect + combobox functions (filter and itemToString). Returns same values as useMultipleSelect + values from UseComboboxReturnValue

Type parameters

NameType
TT - Type of items
SS: HTMLElement = HTMLDivElement - Type of select element
DD: HTMLElement = HTMLUListElement- Type of dropdown element

Props

UseMultipleComboboxProps<T>: UseMultipleSelectProps<T> & ComboboxFunctions<T>

Return value

UseMultipleCombobox<T, S, D>: UseMultipleSelect<T, S, D\> & UseComboboxReturnValue<T>

Usage

This example uses basic styling and markup, you can style your components however you want. Note that you need to assign selectRef and dropdownRef, this is needed so that isOpen is set to false (dropdown is closed) if you click outside select or dropdown element. If you want your dropdown to scroll to highlighted item when user presses arrow keys make your items direct children of dropdown element.

const MultipleCombobox = () => {
  const [value, setValue] = useState<string[]>();
  const {
    selectRef,
    dropdownRef,
    inputRef,
    open,
    isOpen,
    highlightedIndex,
    inputValue,
    setInputValue,
    items,
    isSelected,
    handleItemClick,
    handleClick,
    handleKeyDown,
  } = useMultipleCombobox({
    items: comboboxItems,
    itemToString: item => item ?? "",
    value,
    onChange: setValue,
  });

  return (
    <div>
      <div ref={selectRef} tabIndex={0} onFocus={open} onKeyDown={handleKeyDown}>
        {value?.join(", ")}
        <input value={inputValue} onChange={({ target: { value } }) => setInputValue(value)} ref={inputRef} />
        <button onFocus={e => e.stopPropagation()} onClick={handleClick}>
          {isOpen ? <span>&#9650;</span> : <span>&#9660;</span>}
        </button>
      </div>
      {isOpen && (
        <ul
          ref={dropdownRef}
          tabIndex={0}
          onKeyDown={handleKeyDown}
          style={{ width: "400px", backgroundColor: "grey" }}>
          {items.map((item, index) => (
            <li
              key={item}
              onClick={() => handleItemClick(item)}
              style={{
                color: isSelected(item) ? "blue" : "black",
                backgroundColor: highlightedIndex === index ? "green" : "grey",
                cursor: "pointer",
              }}>
              {item}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
};

useAsyncCombobox

useAsyncCombobox<T, S, D>(props: UseAsyncComboboxProps<T>): UseAsyncCombobox<T, S, D>

Returns state and callbacks for building combobox component that fetches items asynchronously.

Internally it uses useCombobox hook, but instead of filtering items this hook calls fetchItems when inputValue changes.

Items returned from this hook are latest result of fetchItems call.

Type parameters

NameType
TT - Type of items
SS: HTMLElement = HTMLDivElement - Type of select element
DD: HTMLElement = HTMLUListElement- Type of dropdown element

Props

UseAsyncComboboxProps<T>: { itemToString: ItemToString<T> } & ValueControl<T> & FetchItems<T> & Handlers & Flags

Similar to useCombobox, but instead of providing items you need to provide fetchItems function that will fetch items asynchronously when input value changes.

Return value

UseAsyncCombobox<T, S, D>: UseCombobox<T, S, D> & Loading

Returns everything useCombobox returns + loading flag that indicates if fetchItems is in progress.

Loading

NameTypeDescription
loadingbooleanTrue if fetchItems has been called but promise hasn't resolved yet.

Usage

This example uses basic styling and markup, you can style your components however you want. Note that you need to assign selectRef and dropdownRef, this is needed so that isOpen is set to false (dropdown is closed) if you click outside select or dropdown element. If you want your dropdown to scroll to highlighted item when user presses arrow keys make your items direct children of dropdown element.

Example uses mock promise that resolves after 100ms timeout for fetchItems. You should use a function that will fetch items from some location and return them.

const AsyncCombobox = () => {
  const [value, setValue] = useState<string>();
  const {
    selectRef,
    dropdownRef,
    inputRef,
    inputValue,
    open,
    setInputValue,
    handleKeyDown,
    handleClick,
    handleItemClick,
    isSelected,
    highlightedIndex,
    isOpen,
    items,
    loading,
  } = useAsyncCombobox({
    fetchItems: async _ => {
      const promise = new Promise<void>(resolve => {
        setTimeout(() => {
          resolve();
        }, 100);
      });
      const [result] = await Promise.all([Promise.resolve(comboboxItems), promise]);

      return result;
    },
    value,
    onChange: value => setValue(value),
    itemToString: item => item ?? "",
  });

  return (
    <div>
      <div ref={selectRef} tabIndex={0} onFocus={open} onKeyDown={handleKeyDown}>
        <input value={inputValue} onChange={({ target: { value } }) => setInputValue(value)} ref={inputRef} />
        <button onFocus={e => e.stopPropagation()} onClick={handleClick}>
          {isOpen ? <span>&#9650;</span> : <span>&#9660;</span>}
        </button>
      </div>
      {isOpen && (
        <ul
          ref={dropdownRef}
          tabIndex={0}
          onKeyDown={handleKeyDown}
          style={{ width: "400px", backgroundColor: "grey" }}>
          {loading
            ? "Loading..."
            : items.map((item, index) => (
                <li
                  key={item}
                  onClick={() => handleItemClick(item)}
                  style={{
                    color: isSelected(item) ? "blue" : "black",
                    backgroundColor: highlightedIndex === index ? "green" : "grey",
                    cursor: "pointer",
                  }}>
                  {item}
                </li>
              ))}
        </ul>
      )}
    </div>
  );
};

useMultipleAsyncCombobox

useMultipleAsyncCombobox<T, S, D>(props: UseMultipleAsyncCombobx<T>): UseMultipleAsyncCombobox<T, S, D>

Similar to useMultipleCombobox only this hook fetches new items on inputValue change.

Uses useMultipleCombobox internally.

Type parameters

NameType
TT - Type of items
SS: HTMLElement = HTMLDivElement - Type of select element
DD: HTMLElement = HTMLUListElement- Type of dropdown element

Props

UseAsyncComboboxProps<T>: { itemToString: ItemToString<T> } & MultiValueControl<T> & FetchItems<T> & Handlers & Flags

Return value

UseMultipleAsyncCombobox<T, S, D>: UseMultipleCombobox<T, S, D\> & Loading

Returns everything useMultipleCombobox returns + loading flag.

Usage

This example uses basic styling and markup, you can style your components however you want. Note that you need to assign selectRef and dropdownRef, this is needed so that isOpen is set to false (dropdown is closed) if you click outside select or dropdown element. If you want your dropdown to scroll to highlighted item when user presses arrow keys make your items direct children of dropdown element.

Example uses mock promise that resolves after 100ms timeout for fetchItems. You should use a function that will fetch items from some location and return them.

const MultipleAsyncCombobox = () => {
  const [value, setValue] = useState<string[]>();
  const {
    selectRef,
    dropdownRef,
    inputRef,
    inputValue,
    open,
    setInputValue,
    handleKeyDown,
    handleClick,
    handleItemClick,
    isSelected,
    highlightedIndex,
    isOpen,
    items,
    loading,
  } = useMultipleAsyncCombobox({
    fetchItems: async _ => {
      const promise = new Promise<void>(resolve => {
        setTimeout(() => {
          resolve();
        }, 100);
      });
      const [result] = await Promise.all([Promise.resolve(comboboxItems), promise]);

      return result;
    },
    value,
    onChange: value => setValue(value),
    itemToString: item => item ?? "",
  });

  return (
    <div>
      <div ref={selectRef} tabIndex={0} onFocus={open} onKeyDown={handleKeyDown}>
        {value?.join(", ")}
        <input value={inputValue} onChange={({ target: { value } }) => setInputValue(value)} ref={inputRef} />
        <button onFocus={e => e.stopPropagation()} onClick={handleClick}>
          {isOpen ? <span>&#9650;</span> : <span>&#9660;</span>}
        </button>
      </div>
      {isOpen && (
        <ul
          ref={dropdownRef}
          tabIndex={0}
          onKeyDown={handleKeyDown}
          style={{ width: "400px", backgroundColor: "grey" }}>
          {loading
            ? "Loading..."
            : items.map((item, index) => (
                <li
                  key={item}
                  onClick={() => handleItemClick(item)}
                  style={{
                    color: isSelected(item) ? "blue" : "black",
                    backgroundColor: highlightedIndex === index ? "green" : "grey",
                    cursor: "pointer",
                  }}>
                  {item}
                </li>
              ))}
        </ul>
      )}
    </div>
  );
};

Common Types

FetchItems<T>

Type parameters

NameDescription
TType of items

Type declaration

NameTypeDescription
fetchItems(query: string) => Promise<T[]>Fetch items asynchronously

Flags

NameTypeDescription
clearable?booleanIf true value can be set to undefined for value, and for array value can be set to an empty array. Note that for array value case it is still possible to set value to an empty array by calling remove or removeByIndex on every selected item.
disabled?booleanIf true open function does nothing, same as readOnly, provided as separate prop for convenience
readOnly?booleanIf true open function does nothing, same as disabled, provided as separate prop for convenience

Handlers

NameTypeDescription
onClose?() => voidThis function is called when isOpen is set to false
onOpen?() => voidThis function is called when isOpen is set to true

Items<T>

Type parameters

NameDescription
TType of items
NameTypeDescription
itemsT[]Options that can be selected

MultiValueControl<T>

onChange handler and value type for hooks where multiple selection is allowed

Type parameters

NameDescription
TType of items

Type declaration

NameType
onChange?(value?: T[]) => void
value?T[]

ValueControl<T>

onChange handler and value type for hooks where only single selection is allowed

Type parameters

NameDescription
TType of items

Type declaration

NameType
onChange?(value?: T) => void
value?T

ComboboxFunctions<T>

Filter and itemToString props for combobox.

Type parameters

NameDescription
TType of items

Type declaration

NameTypeDescription
filter?(items: T[], query: string, itemToString: ItemToString<T>) => T[]Provided items are equal to items prop, query is equal to current input value of combobox, and itemToString is equal to itemToString prop. Should return filtered items. If not provided, defaults to items.filter(item => itemToString(item).toLowerCase().startsWith(query.toLowerCase()))
itemToStringItemToString<T>Function that converts item to string. Since items can be of any type, to compare them we need to have a way of converting them to string.

ItemToString<T>

Function that converts item to string. Since items can be of any type, to compare them we need to have a way of converting them to string.

T - type of item

(item?: T) => string

Examples

To run examples run yarn start inside example directory


Made with tsdx