npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@tracksuitdev/use-select

v1.1.1

Published

React hooks for building select and combobox components.

Downloads

23

Readme

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

| Name | Type | | :------ | :------ | | T | T - Type of items | | S | S: HTMLElement = HTMLDivElement - Type of select element | | D | D: HTMLElement = HTMLUListElement- Type of dropdown element |

Props

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

Return value

UseSelect<T, S, D>

| Name | Type | Description | | :------ | :------ | :------ | | clear | (e: ReactMouseEvent) => void | Calls onChange with undefined or empty array value in case of multiple selection. Prevents event propagation | | dropdownRef | RefObject<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) => void | Toggles isOpen flag, prevents event propagation | | handleItemClick | (item: T) => void | Calls select if item isn't selected or remove if item is selected | | handleKeyDown | KeyboardEventHandler<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) | | highlightedIndex | number | Index of currently highlighted item, used for keyboard control, ArrowUp key decreases this, while ArrowDown key increases it | | isOpen | boolean | Indicates whether dropdown is open or not | | isSelected | (item: T) => boolean | Returns true if item equals value, or in case of multiple selection, if item is part of value array | | open | () => void | Sets isOpen to true | | remove | () => void | Calls onChange with value set to undefined | | select | (item: T) => void | Calls onChange with provided item set as value | | selectRef | RefObject<S> | Ref for combobox element, used internally to allow closing of dropdown on outside click | | setHighlightedIndex | (index: number) => void | Sets 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

| Name | Type | | :------ | :------ | | T | T - Type of items | | S | S: HTMLElement = HTMLDivElement - Type of select element | | D | D: 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.

| Name | Type | Description | | :------ | :------ | :------ | | remove| (item: T) => void | Calls onChange with value array without the provided item | | removeByIndex| (index: number) => void | Calls 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

| Name | Type | | :------ | :------ | | T | T - Type of items | | S | S: HTMLElement = HTMLDivElement - Type of select element | | D | D: 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>

| Name | Type | Description | | :------ | :------ | :------ | | inputRef | RefObject<HTMLInputElement> | Ref that needs to be applied to combobox input element | | inputValue | string | Value of input element | | items | T[] | Items filtered by filter prop, or in case of async combobox result of fetchItems | | setInputValue | (value: string) => void | Sets 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

| Name | Type | | :------ | :------ | | T | T - Type of items | | S | S: HTMLElement = HTMLDivElement - Type of select element | | D | D: 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

| Name | Type | | :------ | :------ | | T | T - Type of items | | S | S: HTMLElement = HTMLDivElement - Type of select element | | D | D: 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

| Name | Type | Description | | :------ | :------ | :------ | | loading | boolean | True 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

| Name | Type | | :------ | :------ | | T | T - Type of items | | S | S: HTMLElement = HTMLDivElement - Type of select element | | D | D: 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

| Name | Description | | :------ | :------ | | T | Type of items |

Type declaration

| Name | Type | Description | | :------ | :------ | :------ | | fetchItems | (query: string) => Promise<T[]> | Fetch items asynchronously |


Flags

| Name | Type | Description | | :------ | :------ | :------ | | clearable? | boolean | If 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? | boolean | If true open function does nothing, same as readOnly, provided as separate prop for convenience | | readOnly? | boolean | If true open function does nothing, same as disabled, provided as separate prop for convenience |


Handlers

| Name | Type | Description | | :------ | :------ | :------ | | onClose? | () => void | This function is called when isOpen is set to false | | onOpen? | () => void | This function is called when isOpen is set to true |


Items<T>

Type parameters

| Name | Description | | :------ | :------ | | T | Type of items |

| Name | Type | Description | | :------ | :------ | :------ | | items | T[] | Options that can be selected |


MultiValueControl<T>

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

Type parameters

| Name | Description | | :------ | :------ | | T | Type of items |

Type declaration

| Name | Type | | :------ | :------ | | onChange? | (value?: T[]) => void | | value? | T[] |


ValueControl<T>

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

Type parameters

| Name | Description | | :------ | :------ | | T | Type of items |

Type declaration

| Name | Type | | :------ | :------ | | onChange? | (value?: T) => void | | value? | T |


ComboboxFunctions<T>

Filter and itemToString props for combobox.

Type parameters

| Name | Description | | :------ | :------ | | T | Type of items |

Type declaration

| Name | Type | Description | | :------ | :------ | :------ | | 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())) | | itemToString | 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. |

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