@blueprintjs/select
Version:
Components related to selecting items from a list
395 lines (317 loc) • 14 kB
Markdown
@# Select
The **Select** component renders a UI to choose one item from a list. Its children are wrapped in a
[**Popover**](#core/components/popover) that contains the list and an optional
[**InputGroup**](#core/components/input-group) to filter it.
You may provide a predicate to customize the filtering algorithm. The value of a **Select**
(the currently chosen item) is uncontrolled: listen to changes with the `onItemSelect` callback prop.
SelectExample
@## Usage
In TypeScript, `Select<T>` is a _generic component_ so you must define a local type that specifies `<T>`, the type of
one item in `items`. The props on this local type will now operate on your data type so you can easily define handlers
without transformation steps, but most props are required as a result.
```tsx
import { Button, MenuItem } from "@blueprintjs/core";
import { ItemPredicate, ItemRenderer, Select } from "@blueprintjs/select";
import * as React from "react";
import * as ReactDOM from "react-dom";
export interface Film {
title: string;
year: number;
rank: number;
}
const TOP_100_FILMS: Film[] = [
{ title: "The Shawshank Redemption", year: 1994 },
{ title: "The Godfather", year: 1972 },
// ...
].map((f, index) => ({ ...f, rank: index + 1 }));
const filterFilm: ItemPredicate<Film> = (query, film, _index, exactMatch) => {
const normalizedTitle = film.title.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${film.rank}. ${normalizedTitle} ${film.year}`.indexOf(normalizedQuery) >= 0;
}
};
const renderFilm: ItemRenderer<Film> = (film, { handleClick, handleFocus, modifiers, query }) => {
if (!modifiers.matchesPredicate) {
return null;
}
return (
<MenuItem
active={modifiers.active}
disabled={modifiers.disabled}
key={film.rank}
label={film.year.toString()}
onClick={handleClick}
onFocus={handleFocus}
roleStructure="listoption"
text={`${film.rank}. ${film.title}`}
/>
);
};
const FilmSelect: React.FC = () => {
const [selectedFilm, setSelectedFilm] = React.useState<Film | undefined>();
return (
<Select<Film>
items={TOP_100_FILMS}
itemPredicate={filterFilm}
itemRenderer={renderFilm}
noResults={<MenuItem disabled={true} text="No results." roleStructure="listoption" />}
onItemSelect={setSelectedFilm}
>
<Button text={selectedFilm?.title ?? "Select a film"} rightIcon="double-caret-vertical" />
</Select>
);
};
ReactDOM.render(<FilmSelect />, document.querySelector("#root"));
```
@## Props interface
SelectProps
@## Querying
Supply a predicate to automatically query items based on the **InputGroup** value. Use `itemPredicate` to filter each item
individually; this is great for lightweight searches. Use `itemListPredicate` to query the entire array in one go, and
even reorder it, such as with [fuzz-aldrin-plus](https://github.com/jeancroy/fuzz-aldrin-plus). The array of filtered
items is cached internally by `QueryList` state and only recomputed when `query` or `items`-related props change.
Omitting both `itemPredicate` and `itemListPredicate` props will cause the component to always render all `items`. It
will not hide the **InputGroup**; use the `filterable` prop for that. In this case, you can implement your own filtering
and change the `items` prop.
The **/select** package exports `ItemPredicate<T>` and `ItemListPredicate<T>` type aliases to simplify the
process of implementing these functions. See the code sample in
[Item Renderer API](#select/select-component.item-renderer) below for more info.
@### Non-ideal states
If the query returns no results or `items` is empty, then `noResults` will be rendered in place of the usual list.
You also have the option to provide `initialContent`, which will render in place of the item list if the query is empty.
@## Styling
@### Button styling
**Select** accepts arbitrary child elements, but in most cases this will be a single **Button** component. To make this
button appear like a typical dropdown, apply some common button props such `alignText` and `rightIcon`:
```tsx
const MySelectDropdown: React.FC = () => (
// many props omitted here for brevity
<Select>
<Button alignText="left" fill={true} rightIcon="caret-down" text="Dropdown">
</Select>
);
```
@### Placeholder styling
When a **Select** has no selected item, you may wish to display placeholder text. Use the **Button** component's
`textClassName` prop to accomplish this:
```tsx
const MySelectDropdown: React.FC = () => {
const [selectedValue, setSelectedValue] = React.useState<string | undefined>(undefined);
return (
// many props omitted here for brevity
<Select<string> onItemSelect={setSelectedValue}>
<Button
rightIcon="caret-down"
textClassName={classNames({
[Classes.TEXT_MUTED]: selectedValue === undefined,
})}
text={selectedValue ?? "(No selection)"}
/>
</Select>
);
};
```
@### Disabled styling
Disabling a **Select** requires setting the `disabled={true}` prop _and also_ disabling its children. For example:
```tsx
const FilmSelect: React.FC = () => (
// many props omitted here for brevity
<Select disabled={true}>
<Button disabled={true}>
</Select>
);
```
@## Custom menu
By default, **Select** renders the displayed items in a [**Menu**](#core/components/menu). This behavior can be
overridden by providing the `itemListRenderer` prop, giving you full control over the layout of the items. For example,
you can group items under a common heading, or render large data sets using
[react-virtualized](https://github.com/bvaughn/react-virtualized).
Note that the non-ideal states of `noResults` and `initialContent` are specific to the default renderer. If you provide
the `itemListRenderer` prop, these props will be ignored.
See the code sample in [Item List Renderer API](#select/select-component.item-list-renderer) below for more info.
@## Controlled usage
The input value can be controlled with the `query` and `onQueryChange` props. _Do not use `inputProps` for this;_
the component ignores `inputProps.value` and `inputProps.onChange` in favor of `query` and `onQueryChange`
(as noted in the prop documentation).
The focused item (for keyboard interactions) can be controlled with the `activeItem` and `onActiveItemChange` props.
```tsx
const FilmSelect: React.FC = () => (
<Select<Film>
items={myFilter(ALL_ITEMS, this.state.myQuery)}
itemRenderer={...}
onItemSelect={...}
// controlled active item
activeItem={this.state.myActiveItem}
onActiveItemChange={this.handleActiveItemChange}
// controlled query
query={this.state.myQuery}
onQueryChange={this.handleQueryChange}
/>
);
```
This controlled usage allows you to implement all sorts of advanced behavior on top of the basic **Select**
interactions, such as windowed filtering for large data sets.
<div class="@ns-callout @ns-intent-primary @ns-icon-info-sign">
To control the active item when a "Create Item" option is present, See
[Controlling the active item](#select/select-component.controlling-the-active-item) in the "Creating new items"
section below.
</div>
@## Creating new items
If you wish, you can allow users to select a brand new item that doesn't appear in the list, based on the current query
string. Use `createNewItemFromQuery` and `createNewItemRenderer` to enable this:
- `createNewItemFromQuery`: Specifies how to convert a user-entered query string into an item of type `<T>` that
**Select** understands.
- `createNewItemRenderer`: Renders a custom "Create Item" element that will be shown at the bottom of the list. When
selected via click or `Enter`, this element will invoke `onItemSelect` with the item returned from
`createNewItemFromQuery`.
<div class="@ns-callout @ns-intent-warning @ns-icon-info-sign">
<h5 class="@ns-heading">Avoiding type conflicts</h5>
The "Create Item" option is represented by the reserved type `CreateNewItem` exported from this package. It is
exceedingly unlikely but technically possible for your custom type `<T>` to conflict with this type. If your type
conflicts, you may see unexpected behavior; to resolve, consider changing the schema for your items.
</div>
```tsx
function createFilm(title: string): Film {
return {
rank: /* ... */,
title,
year: /* ... */,
};
}
function renderCreateFilmOption(
query: string,
active: boolean,
handleClick: React.MouseEventHandler<HTMLElement>,
) {
return (
<MenuItem
icon="add"
text={`Create "${query}"`}
roleStructure="listoption"
active={active}
onClick={handleClick}
shouldDismissPopover={false}
/>
)
}
const FilmSelect: React.FC = () => (
<Select<Film>
createNewItemFromQuery={createFilm}
createNewItemRenderer={renderCreateFilmOption}
items={Films.items}
itemPredicate={Films.itemPredicate}
itemRenderer={Films.itemRenderer}
noResults={<MenuItem disabled={true} text="No results." roleStructure="listoption" />}
onItemSelect={...}
/>
);
```
@### Controlling the active item
Controlling the active item is slightly more involved when the "Create Item" option is present. At a high level, the
process works the same way as before: control the `activeItem` value and listen for updates via `onActiveItemChange`.
However, some special handling is required.
When the "Create Item" option is present, the callback will emit `activeItem=null` and `isCreateNewItem=true`:
```tsx
onActiveItemChange(null, true);
```
You can then make the "Create Item" option active by passing the result of `getCreateNewItem()` to the `activeItem`
prop (the `getCreateNewItem` function is exported from this package):
```tsx
activeItem={isCreateNewItemActive ? getCreateNewItem() : activeItem}
```
Altogether, the code might look something like this:
```tsx
const currentActiveItem: Film | CreateNewItem | null;
const isCreateNewItemActive: Film | CreateNewItem | null;
function handleActiveItemChange(
activeItem: Film | CreateNewItem | null,
isCreateNewItem: boolean,
) {
currentActiveItem = activeItem;
isCreateNewItemActive = isCreateNewItem;
}
function getActiveItem() {
return isCreateNewItemActive ? getCreateNewItem() : currentActiveItem;
}
const FilmSelect: React.FC = () => (
<Select<Film>
{...} // Other required props (see previous examples).
activeItem={getActiveItem()}
createNewItemFromQuery={...}
createNewItemRenderer={...}
onActiveItemChange={handleActiveItemChange}
/>
);
```
@### Item renderer
A **Select** component's `itemRenderer` will be called for each item and receives the item and a props object containing
data specific to rendering this item in this frame.
A few things to keep in mind:
- The renderer is called for all items, so don't forget to respect `modifiers.matchesPredicate` to hide items which
do not match the predicate.
- Make sure to forward the provided `ref` to the rendered element (usually via `<MenuItem ref={ref} />`) to ensure
that scrolling to active items works correctly.
- Also, don't forget to define a `key` for each item, or face React's console wrath!
```tsx
import { Classes, MenuItem } from "@blueprintjs/core";
import { ItemRenderer, ItemPredicate, Select } from "@blueprintjs/select";
const filterFilm: ItemPredicate<Film> = (query, film) => {
return film.title.toLowerCase().indexOf(query.toLowerCase()) >= 0;
};
const renderFilm: ItemRenderer<Film> = (film, { handleClick, handleFocus, modifiers }) => {
if (!modifiers.matchesPredicate) {
return null;
}
return (
<MenuItem
text={film.title}
label={film.year}
roleStructure="listoption"
active={modifiers.active}
key={film.title}
onClick={handleClick}
onFocus={handleFocus}
/>
);
};
const FilmSelect: React.FC = () => (
<Select<Film>
itemPredicate={filterFilm}
itemRenderer={renderFilm}
items={...}
onItemSelect={...}
/>
);
```
ItemRendererProps
@### Item list renderer
If provided, the `itemListRenderer` prop will be called to render the contents of the dropdown menu. It has access to the items, the current query, and a `renderItem` callback for rendering a single item. A ref handler (`itemsParentRef`) is given as well; it should be attached to the parent element of the rendered menu items so that the currently selected item can be scrolled into view automatically.
```tsx
import { ItemListRenderer } from "@blueprintjs/select";
const renderMenu: ItemListRenderer<Film> = ({ items, itemsParentRef, query, renderItem, menuProps }) => {
const renderedItems = items.map(renderItem).filter(item => item != null);
return (
<Menu role="listbox" ulRef={itemsParentRef} {...menuProps}>
<MenuItem
disabled={true}
text={`Found ${renderedItems.length} items matching "${query}"`}
roleStructure="listoption"
/>
{renderedItems}
</Menu>
);
};
const FilmSelect: React.FC = () => (
<Select<Film>
itemListRenderer={renderMenu}
itemPredicate={filterFilm}
itemRenderer={renderFilm}
items={...}
onItemSelect={...}
/>
);
```
ItemListRendererProps